feat(minga): multi-lenguaje en parser — Python, TypeScript, JavaScript, Go
Minga deja de ser Rust-only. Cualquiera de los cinco dialectos
(Rust + 4 nuevos) se ingresa al CAS por su AST normalizado, hashea
estructuralmente, sincroniza por DHT como cualquier nodo. La
auto-deteccion por extension hace que minga ingest archivo.{py,ts,js,go}
"simplemente funcione".
API nueva en minga_core::parse:
- Funciones por dialecto: python, typescript, javascript, go (~6 LOC
c/u sobre el parse_with comun). Mas la rust existente.
- Enum Dialect con parse(source) y name() para logging.
- detect_by_extension(ext) -> Option<Dialect>: rs/py/pyi/ts/js/mjs/
cjs/go (case-insensitive). None para extensiones desconocidas.
Wire en minga-cli:
- cmd_ingest deja de hardcodear parse::rust — usa
detect_dialect(file)?.parse(...).
- initial_scan + cmd_watch cambian is_rs_file -> is_supported_source.
- CliError::UnsupportedLanguage { path, extension } nuevo, lista las
extensiones reconocidas en el mensaje.
Notas sobre hashing:
- Hashing estructural (cas::hash_node) funciona para todos. NO es
alpha-equivalente.
- Hashing alpha-equivalente (alpha::hash_node_alpha) sigue siendo
Rust-only — cada lenguaje tiene reglas distintas para binder vs
constructor; implementacion per-language queda como work futuro
(requiere conocimiento profundo de cada gramatica).
- Sanity test structural_hash_distinguishes_languages verifica que
"x = 1" parseado como Python != JS — las gramaticas no comparten
kinds, hashes salen distintos. Importante para evitar colisiones.
Deps nuevas (workspace + minga-core):
- tree-sitter-python 0.23, tree-sitter-typescript 0.23 (modo
LANGUAGE_TYPESCRIPT, no TSX), tree-sitter-javascript 0.23,
tree-sitter-go 0.23.
Tests: 9 nuevos en parse::tests (parse basico para 5 dialectos +
detect_by_extension canonical/case-insensitive + name() +
structural_hash_distinguishes_languages). 108 verdes en minga-core,
10 en minga-cli, sin regresion.
Pendientes: alpha-hashing per-language; alpha-Rust documentados en
alpha.rs (if let, while let, let-else, let-chains, or_pattern con
bindings).
This commit is contained in:
@@ -9,6 +9,10 @@ description = "Minga core: semantic AST, content addressing, Merkle Search Tree.
|
||||
[dependencies]
|
||||
tree-sitter = { workspace = true }
|
||||
tree-sitter-rust = { workspace = true }
|
||||
tree-sitter-python = { workspace = true }
|
||||
tree-sitter-typescript = { workspace = true }
|
||||
tree-sitter-javascript = { workspace = true }
|
||||
tree-sitter-go = { workspace = true }
|
||||
blake3 = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
ed25519-dalek = { workspace = true }
|
||||
|
||||
@@ -1,8 +1,29 @@
|
||||
//! Adaptadores de parsing por dialecto. Hoy: Rust vía tree-sitter-rust.
|
||||
//! Adaptadores de parsing por dialecto.
|
||||
//!
|
||||
//! `parse::rust` produce un `SemanticNode` normalizado a partir de una
|
||||
//! cadena de código fuente. El error es opaco a propósito: el caller no
|
||||
//! necesita distinguir "gramática inválida" de "fallo del parser".
|
||||
//! Cada función devuelve un [`SemanticNode`] normalizado a partir del
|
||||
//! source code. La normalización vive en `ast::SemanticNode::from_tree_sitter`
|
||||
//! y es agnóstica al lenguaje — cualquier tree-sitter grammar produce
|
||||
//! el mismo shape de árbol semántico (sin whitespace, sin comentarios).
|
||||
//!
|
||||
//! Lenguajes soportados (cada uno son ~6 LOC + dep tree-sitter-X):
|
||||
//! - [`rust`] — Rust completo (con α-hashing en `alpha::hash_node_alpha`).
|
||||
//! - [`python`] — Python 3.x.
|
||||
//! - [`typescript`] — TypeScript (no TSX).
|
||||
//! - [`javascript`] — JavaScript / ECMAScript.
|
||||
//! - [`go`] — Go.
|
||||
//!
|
||||
//! Para hashing α-equivalente, sólo Rust tiene implementación dedicada
|
||||
//! hoy. Otros lenguajes caen al [`crate::cas::hash_node`] estructural,
|
||||
//! que es α-NO-equivalente: dos versiones del mismo término que
|
||||
//! difieren en nombres de variables ligadas tendrán hashes distintos.
|
||||
//! Suficiente para detección de cambios; no para detección de
|
||||
//! equivalencia semántica.
|
||||
//!
|
||||
//! ## Auto-detección por extensión
|
||||
//!
|
||||
//! [`detect_by_extension`] mapea `.rs` → Rust, `.py` → Python, etc.
|
||||
//! Útil para `minga ingest` cuando el caller no quiere especificar
|
||||
//! el dialecto a mano.
|
||||
|
||||
use crate::ast::SemanticNode;
|
||||
use thiserror::Error;
|
||||
@@ -16,10 +37,183 @@ pub enum ParseError {
|
||||
NoTree,
|
||||
}
|
||||
|
||||
pub fn rust(source: &str) -> Result<SemanticNode, ParseError> {
|
||||
let lang: Language = tree_sitter_rust::LANGUAGE.into();
|
||||
/// Identificadores estables de cada dialecto soportado.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Dialect {
|
||||
Rust,
|
||||
Python,
|
||||
TypeScript,
|
||||
JavaScript,
|
||||
Go,
|
||||
}
|
||||
|
||||
impl Dialect {
|
||||
/// Nombre canónico para logging / display.
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Dialect::Rust => "rust",
|
||||
Dialect::Python => "python",
|
||||
Dialect::TypeScript => "typescript",
|
||||
Dialect::JavaScript => "javascript",
|
||||
Dialect::Go => "go",
|
||||
}
|
||||
}
|
||||
|
||||
/// Parsea `source` con la gramática de este dialecto.
|
||||
pub fn parse(self, source: &str) -> Result<SemanticNode, ParseError> {
|
||||
match self {
|
||||
Dialect::Rust => rust(source),
|
||||
Dialect::Python => python(source),
|
||||
Dialect::TypeScript => typescript(source),
|
||||
Dialect::JavaScript => javascript(source),
|
||||
Dialect::Go => go(source),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mapea una extensión de archivo (sin el `.`) al dialecto correspondiente.
|
||||
/// `None` si la extensión no corresponde a un lenguaje soportado.
|
||||
///
|
||||
/// ```
|
||||
/// use minga_core::parse::{detect_by_extension, Dialect};
|
||||
/// assert_eq!(detect_by_extension("rs"), Some(Dialect::Rust));
|
||||
/// assert_eq!(detect_by_extension("py"), Some(Dialect::Python));
|
||||
/// assert_eq!(detect_by_extension("unknown"), None);
|
||||
/// ```
|
||||
pub fn detect_by_extension(ext: &str) -> Option<Dialect> {
|
||||
match ext.to_ascii_lowercase().as_str() {
|
||||
"rs" => Some(Dialect::Rust),
|
||||
"py" | "pyi" => Some(Dialect::Python),
|
||||
"ts" => Some(Dialect::TypeScript),
|
||||
"js" | "mjs" | "cjs" => Some(Dialect::JavaScript),
|
||||
"go" => Some(Dialect::Go),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_with(lang: Language, source: &str) -> Result<SemanticNode, ParseError> {
|
||||
let mut parser = Parser::new();
|
||||
parser.set_language(&lang).map_err(|_| ParseError::Language)?;
|
||||
let tree = parser.parse(source, None).ok_or(ParseError::NoTree)?;
|
||||
Ok(SemanticNode::from_tree_sitter(tree.root_node(), source.as_bytes()))
|
||||
}
|
||||
|
||||
pub fn rust(source: &str) -> Result<SemanticNode, ParseError> {
|
||||
parse_with(tree_sitter_rust::LANGUAGE.into(), source)
|
||||
}
|
||||
|
||||
pub fn python(source: &str) -> Result<SemanticNode, ParseError> {
|
||||
parse_with(tree_sitter_python::LANGUAGE.into(), source)
|
||||
}
|
||||
|
||||
pub fn typescript(source: &str) -> Result<SemanticNode, ParseError> {
|
||||
parse_with(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(), source)
|
||||
}
|
||||
|
||||
pub fn javascript(source: &str) -> Result<SemanticNode, ParseError> {
|
||||
parse_with(tree_sitter_javascript::LANGUAGE.into(), source)
|
||||
}
|
||||
|
||||
pub fn go(source: &str) -> Result<SemanticNode, ParseError> {
|
||||
parse_with(tree_sitter_go::LANGUAGE.into(), source)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn assert_parses(d: Dialect, source: &str) -> SemanticNode {
|
||||
let node = d.parse(source).expect("parse should succeed");
|
||||
// Sanity: el root siempre tiene al menos un child para code real.
|
||||
assert!(
|
||||
!node.children.is_empty(),
|
||||
"{}: root node sin children — parse posiblemente vacío",
|
||||
d.name()
|
||||
);
|
||||
node
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rust_parses_basic() {
|
||||
assert_parses(Dialect::Rust, "fn add(a: i32, b: i32) -> i32 { a + b }");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn python_parses_basic() {
|
||||
assert_parses(
|
||||
Dialect::Python,
|
||||
"def add(a: int, b: int) -> int:\n return a + b\n",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typescript_parses_basic() {
|
||||
assert_parses(
|
||||
Dialect::TypeScript,
|
||||
"function add(a: number, b: number): number { return a + b; }",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn javascript_parses_basic() {
|
||||
assert_parses(
|
||||
Dialect::JavaScript,
|
||||
"function add(a, b) { return a + b; }",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn go_parses_basic() {
|
||||
assert_parses(
|
||||
Dialect::Go,
|
||||
"package main\n\nfunc add(a, b int) int {\n return a + b\n}\n",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_extension_canonical() {
|
||||
assert_eq!(detect_by_extension("rs"), Some(Dialect::Rust));
|
||||
assert_eq!(detect_by_extension("py"), Some(Dialect::Python));
|
||||
assert_eq!(detect_by_extension("pyi"), Some(Dialect::Python));
|
||||
assert_eq!(detect_by_extension("ts"), Some(Dialect::TypeScript));
|
||||
assert_eq!(detect_by_extension("js"), Some(Dialect::JavaScript));
|
||||
assert_eq!(detect_by_extension("mjs"), Some(Dialect::JavaScript));
|
||||
assert_eq!(detect_by_extension("cjs"), Some(Dialect::JavaScript));
|
||||
assert_eq!(detect_by_extension("go"), Some(Dialect::Go));
|
||||
assert_eq!(detect_by_extension("unknown"), None);
|
||||
assert_eq!(detect_by_extension(""), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_extension_case_insensitive() {
|
||||
assert_eq!(detect_by_extension("RS"), Some(Dialect::Rust));
|
||||
assert_eq!(detect_by_extension("Py"), Some(Dialect::Python));
|
||||
assert_eq!(detect_by_extension("TS"), Some(Dialect::TypeScript));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dialect_name_canonical() {
|
||||
assert_eq!(Dialect::Rust.name(), "rust");
|
||||
assert_eq!(Dialect::Python.name(), "python");
|
||||
assert_eq!(Dialect::TypeScript.name(), "typescript");
|
||||
assert_eq!(Dialect::JavaScript.name(), "javascript");
|
||||
assert_eq!(Dialect::Go.name(), "go");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn structural_hash_distinguishes_languages() {
|
||||
// Mismo "shape" textual pero distintos lenguajes producen
|
||||
// árboles distintos (las gramáticas no coinciden) y por tanto
|
||||
// hashes estructurales distintos. Importante para evitar
|
||||
// colisiones en el CAS cuando el mismo source se ingiere
|
||||
// bajo dialectos distintos.
|
||||
use crate::cas::hash_node;
|
||||
let py = Dialect::Python.parse("x = 1").unwrap();
|
||||
let js = Dialect::JavaScript.parse("x = 1").unwrap();
|
||||
assert_ne!(
|
||||
hash_node(&py),
|
||||
hash_node(&js),
|
||||
"py y js deberían tener hashes distintos para el mismo source"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user