tawasuyu

tawasuyu/brahman

Rust 326TOML 123JSON 17Markdown 144606 Definitions

⎇ 72758e75ce515f355f27615671c95ecaf8c2a8bc crates/modules/semantic_dht/minga-core/src/parse.rs Rust · 219 lines · 7.4 KB History Who touched what raw

feat(minga): multi-lenguaje en parser — Python, TypeScript, JavaScript, Go 4db168253 · Sergio ·

1//! Adaptadores de parsing por dialecto.
2//!
3//! Cada función devuelve un [`SemanticNode`] normalizado a partir del
4//! source code. La normalización vive en `ast::SemanticNode::from_tree_sitter`
5//! y es agnóstica al lenguaje — cualquier tree-sitter grammar produce
6//! el mismo shape de árbol semántico (sin whitespace, sin comentarios).
7//!
8//! Lenguajes soportados (cada uno son ~6 LOC + dep tree-sitter-X):
9//! - [`rust`] — Rust completo (con α-hashing en `alpha::hash_node_alpha`).
10//! - [`python`] — Python 3.x.
11//! - [`typescript`] — TypeScript (no TSX).
12//! - [`javascript`] — JavaScript / ECMAScript.
13//! - [`go`] — Go.
14//!
15//! Para hashing α-equivalente, sólo Rust tiene implementación dedicada
16//! hoy. Otros lenguajes caen al [`crate::cas::hash_node`] estructural,
17//! que es α-NO-equivalente: dos versiones del mismo término que
18//! difieren en nombres de variables ligadas tendrán hashes distintos.
19//! Suficiente para detección de cambios; no para detección de
20//! equivalencia semántica.
21//!
22//! ## Auto-detección por extensión
23//!
24//! [`detect_by_extension`] mapea `.rs` → Rust, `.py` → Python, etc.
25//! Útil para `minga ingest` cuando el caller no quiere especificar
26//! el dialecto a mano.
27
28use crate::ast::SemanticNode;
29use thiserror::Error;
30use tree_sitter::{Language, Parser};
31
32#[derive(Debug, Error)]
33pub enum ParseError {
34    #[error("tree-sitter no pudo configurar el lenguaje")]
35    Language,
36    #[error("tree-sitter no produjo árbol para la entrada")]
37    NoTree,
38}
39
40/// Identificadores estables de cada dialecto soportado.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42pub enum Dialect {
43    Rust,
44    Python,
45    TypeScript,
46    JavaScript,
47    Go,
48}
49
50impl Dialect {
51    /// Nombre canónico para logging / display.
52    pub fn name(self) -> &'static str {
53        match self {
54            Dialect::Rust => "rust",
55            Dialect::Python => "python",
56            Dialect::TypeScript => "typescript",
57            Dialect::JavaScript => "javascript",
58            Dialect::Go => "go",
59        }
60    }
61
62    /// Parsea `source` con la gramática de este dialecto.
63    pub fn parse(self, source: &str) -> Result<SemanticNode, ParseError> {
64        match self {
65            Dialect::Rust => rust(source),
66            Dialect::Python => python(source),
67            Dialect::TypeScript => typescript(source),
68            Dialect::JavaScript => javascript(source),
69            Dialect::Go => go(source),
70        }
71    }
72}
73
74/// Mapea una extensión de archivo (sin el `.`) al dialecto correspondiente.
75/// `None` si la extensión no corresponde a un lenguaje soportado.
76///
77/// ```
78/// use minga_core::parse::{detect_by_extension, Dialect};
79/// assert_eq!(detect_by_extension("rs"), Some(Dialect::Rust));
80/// assert_eq!(detect_by_extension("py"), Some(Dialect::Python));
81/// assert_eq!(detect_by_extension("unknown"), None);
82/// ```
83pub fn detect_by_extension(ext: &str) -> Option<Dialect> {
84    match ext.to_ascii_lowercase().as_str() {
85        "rs" => Some(Dialect::Rust),
86        "py" | "pyi" => Some(Dialect::Python),
87        "ts" => Some(Dialect::TypeScript),
88        "js" | "mjs" | "cjs" => Some(Dialect::JavaScript),
89        "go" => Some(Dialect::Go),
90        _ => None,
91    }
92}
93
94fn parse_with(lang: Language, source: &str) -> Result<SemanticNode, ParseError> {
95    let mut parser = Parser::new();
96    parser.set_language(&lang).map_err(|_| ParseError::Language)?;
97    let tree = parser.parse(source, None).ok_or(ParseError::NoTree)?;
98    Ok(SemanticNode::from_tree_sitter(tree.root_node(), source.as_bytes()))
99}
100
101pub fn rust(source: &str) -> Result<SemanticNode, ParseError> {
102    parse_with(tree_sitter_rust::LANGUAGE.into(), source)
103}
104
105pub fn python(source: &str) -> Result<SemanticNode, ParseError> {
106    parse_with(tree_sitter_python::LANGUAGE.into(), source)
107}
108
109pub fn typescript(source: &str) -> Result<SemanticNode, ParseError> {
110    parse_with(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(), source)
111}
112
113pub fn javascript(source: &str) -> Result<SemanticNode, ParseError> {
114    parse_with(tree_sitter_javascript::LANGUAGE.into(), source)
115}
116
117pub fn go(source: &str) -> Result<SemanticNode, ParseError> {
118    parse_with(tree_sitter_go::LANGUAGE.into(), source)
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    fn assert_parses(d: Dialect, source: &str) -> SemanticNode {
126        let node = d.parse(source).expect("parse should succeed");
127        // Sanity: el root siempre tiene al menos un child para code real.
128        assert!(
129            !node.children.is_empty(),
130            "{}: root node sin children — parse posiblemente vacío",
131            d.name()
132        );
133        node
134    }
135
136    #[test]
137    fn rust_parses_basic() {
138        assert_parses(Dialect::Rust, "fn add(a: i32, b: i32) -> i32 { a + b }");
139    }
140
141    #[test]
142    fn python_parses_basic() {
143        assert_parses(
144            Dialect::Python,
145            "def add(a: int, b: int) -> int:\n    return a + b\n",
146        );
147    }
148
149    #[test]
150    fn typescript_parses_basic() {
151        assert_parses(
152            Dialect::TypeScript,
153            "function add(a: number, b: number): number { return a + b; }",
154        );
155    }
156
157    #[test]
158    fn javascript_parses_basic() {
159        assert_parses(
160            Dialect::JavaScript,
161            "function add(a, b) { return a + b; }",
162        );
163    }
164
165    #[test]
166    fn go_parses_basic() {
167        assert_parses(
168            Dialect::Go,
169            "package main\n\nfunc add(a, b int) int {\n    return a + b\n}\n",
170        );
171    }
172
173    #[test]
174    fn detect_extension_canonical() {
175        assert_eq!(detect_by_extension("rs"), Some(Dialect::Rust));
176        assert_eq!(detect_by_extension("py"), Some(Dialect::Python));
177        assert_eq!(detect_by_extension("pyi"), Some(Dialect::Python));
178        assert_eq!(detect_by_extension("ts"), Some(Dialect::TypeScript));
179        assert_eq!(detect_by_extension("js"), Some(Dialect::JavaScript));
180        assert_eq!(detect_by_extension("mjs"), Some(Dialect::JavaScript));
181        assert_eq!(detect_by_extension("cjs"), Some(Dialect::JavaScript));
182        assert_eq!(detect_by_extension("go"), Some(Dialect::Go));
183        assert_eq!(detect_by_extension("unknown"), None);
184        assert_eq!(detect_by_extension(""), None);
185    }
186
187    #[test]
188    fn detect_extension_case_insensitive() {
189        assert_eq!(detect_by_extension("RS"), Some(Dialect::Rust));
190        assert_eq!(detect_by_extension("Py"), Some(Dialect::Python));
191        assert_eq!(detect_by_extension("TS"), Some(Dialect::TypeScript));
192    }
193
194    #[test]
195    fn dialect_name_canonical() {
196        assert_eq!(Dialect::Rust.name(), "rust");
197        assert_eq!(Dialect::Python.name(), "python");
198        assert_eq!(Dialect::TypeScript.name(), "typescript");
199        assert_eq!(Dialect::JavaScript.name(), "javascript");
200        assert_eq!(Dialect::Go.name(), "go");
201    }
202
203    #[test]
204    fn structural_hash_distinguishes_languages() {
205        // Mismo "shape" textual pero distintos lenguajes producen
206        // árboles distintos (las gramáticas no coinciden) y por tanto
207        // hashes estructurales distintos. Importante para evitar
208        // colisiones en el CAS cuando el mismo source se ingiere
209        // bajo dialectos distintos.
210        use crate::cas::hash_node;
211        let py = Dialect::Python.parse("x = 1").unwrap();
212        let js = Dialect::JavaScript.parse("x = 1").unwrap();
213        assert_ne!(
214            hash_node(&py),
215            hash_node(&js),
216            "py y js deberían tener hashes distintos para el mismo source"
217        );
218    }
219}