feat(minga-core): cierre del α-hashing de Rust — if let, while let, let-else, or-pattern, let-chains
Cierra los 5 pendientes documentados en alpha.rs. El hash
alpha-equivalente ahora es estable bajo renombre de TODOS los binders
de Rust, no solo los del MVP (params, let, for, match arms).
Pendientes cerrados:
- if let X = expr { ... }: if_expression detecta let_condition en
condition, recolecta binders del pattern, los propaga al
consequence. Alternative (else) no los ve.
- while let X = expr { ... }: simetrico al if-let, propaga al body.
- let-else: ya funcionaba por construccion (alternative procesado en
scope antes que feed_block extienda con los binders).
- or_pattern: ambos lados introducen los mismos binders (Rust
enforcement). Emit recorre todos, collect solo el primero para no
duplicar.
- let-chains (if let X = a && let Y = b): collect_let_condition_binders
recursa en el arbol del condition capturando todos los let_condition
vivan donde vivan (binary_expression u otros).
Helper nuevo: feed_let_condition para que el pattern del let_condition
pase por feed_pattern (que distingue binders de constructors). Sin
esto, los identifiers del pattern se hasheaban como variables libres
y Some(x) != Some(y) aun teniendo el mismo significado.
Tests: 6 nuevos en alpha_invariants:
- alpha_if_let_binder_rename_invariant
- alpha_if_let_else_does_not_see_binder
- alpha_while_let_binder_rename_invariant
- alpha_let_else_binder_rename_invariant
- alpha_or_pattern_binder_rename_invariant
- alpha_let_chain_binders_propagate_to_consequence
- alpha_if_let_does_not_collide_with_unrelated_program (negativo)
36 tests alpha verdes. 115 totales en minga-core.
Refactorings del tipo "rename variable" no inflan el storage del
repo. Pendiente futuro: alpha-hashing per-language (Python, TS, JS,
Go) — cada uno requiere conocimiento profundo de su gramatica.
This commit is contained in:
@@ -6,6 +6,66 @@ ratio/diff ver `git show <sha>`.
|
|||||||
|
|
||||||
## 2026-05-09
|
## 2026-05-09
|
||||||
|
|
||||||
|
### feat(minga-core): cierre del α-hashing de Rust — if let, while let, let-else, or-pattern, let-chains
|
||||||
|
Cierra los 5 pendientes documentados en `alpha.rs`. El hash
|
||||||
|
α-equivalente ahora es estable bajo renombre de TODOS los binders
|
||||||
|
de Rust, no sólo los del MVP (parámetros, let, for, match arms).
|
||||||
|
|
||||||
|
Pendientes cerrados:
|
||||||
|
- **`if let X = expr { ... }`**: `if_expression` detecta
|
||||||
|
`let_condition` en su `condition`, recolecta los binders del
|
||||||
|
pattern, los propaga al `consequence`. El `alternative` (else)
|
||||||
|
NO los ve.
|
||||||
|
- **`while let X = expr { ... }`**: simétrico al if-let, propaga al
|
||||||
|
`body`. El `condition` mismo se evalúa con scope previo (los
|
||||||
|
binders todavía no existen).
|
||||||
|
- **`let-else`**: `let_declaration` con campo `alternative`. El
|
||||||
|
alternative se procesa con el scope ANTES de los binders (ya
|
||||||
|
funcionaba: `feed_let` llama `feed` para no-pattern children con
|
||||||
|
el scope actual; `feed_block` extiende el scope DESPUÉS de
|
||||||
|
`feed_let`).
|
||||||
|
- **`or_pattern`**: en `pat1 | pat2` (Rust enforcement: ambos lados
|
||||||
|
introducen los mismos binders). Para emit, recorremos cada lado
|
||||||
|
con `feed_pattern`. Para collect, sólo el primer lado — iterar
|
||||||
|
todos duplicaría binders y rompería los índices de Bruijn.
|
||||||
|
- **let-chains** (`if let X = a && let Y = b { ... }`): el
|
||||||
|
`collect_let_condition_binders` recursa en el árbol del condition,
|
||||||
|
capturando todos los `let_condition` (vivan dentro de
|
||||||
|
`binary_expression` u otros nodos). Ambos binders quedan en scope
|
||||||
|
del consequence.
|
||||||
|
|
||||||
|
Helper nuevo: `feed_let_condition` para que el `pattern` del
|
||||||
|
let_condition pase por `feed_pattern` (que distingue binders vs
|
||||||
|
constructors). Sin esto, los identifiers del pattern se hasheaban
|
||||||
|
como variables libres y `Some(x)` ≠ `Some(y)` aún teniendo el
|
||||||
|
mismo significado.
|
||||||
|
|
||||||
|
Tests: 6 nuevos en `tests/alpha_invariants.rs`:
|
||||||
|
- `alpha_if_let_binder_rename_invariant`
|
||||||
|
- `alpha_if_let_else_does_not_see_binder` (sanity)
|
||||||
|
- `alpha_while_let_binder_rename_invariant`
|
||||||
|
- `alpha_let_else_binder_rename_invariant`
|
||||||
|
- `alpha_or_pattern_binder_rename_invariant`
|
||||||
|
- `alpha_let_chain_binders_propagate_to_consequence`
|
||||||
|
- `alpha_if_let_does_not_collide_with_unrelated_program` (negativo:
|
||||||
|
programas distintos NO deben dar el mismo hash)
|
||||||
|
|
||||||
|
36 tests α verdes (eran 30). 115 tests totales en minga-core.
|
||||||
|
|
||||||
|
Lo que esto significa: el hash α-equivalente de Rust en minga es
|
||||||
|
**completo** — cubre todos los constructos del lenguaje que
|
||||||
|
introducen bindings. Dos versiones del mismo programa que difieren
|
||||||
|
sólo en nombres de variables (incluyendo en `if let`, `while let`,
|
||||||
|
`or-pattern`, etc.) producen el mismo hash y por tanto la misma
|
||||||
|
identidad CAS. Refactorings del tipo "rename variable" no inflan
|
||||||
|
el storage del repo.
|
||||||
|
|
||||||
|
Pendientes futuros:
|
||||||
|
- α-hashing per-language (Python: def/lambda/comprehensions; TS/JS:
|
||||||
|
function/arrow/destructuring; Go: func/closure). Cada uno
|
||||||
|
requiere conocimiento profundo de la gramática y tests
|
||||||
|
exhaustivos. Plantilla genérica no aplica.
|
||||||
|
|
||||||
### feat(minga): multi-lenguaje en parser — Python, TypeScript, JavaScript, Go
|
### feat(minga): multi-lenguaje en parser — Python, TypeScript, JavaScript, Go
|
||||||
Minga deja de ser Rust-only. Cualquiera de los cinco dialectos
|
Minga deja de ser Rust-only. Cualquiera de los cinco dialectos
|
||||||
(Rust + 4 nuevos) se ingresa al CAS por su AST normalizado, hashea
|
(Rust + 4 nuevos) se ingresa al CAS por su AST normalizado, hashea
|
||||||
|
|||||||
@@ -30,8 +30,17 @@
|
|||||||
//! (`n @ pat`), `range_pattern`, `slice_pattern`, `ref_pattern`,
|
//! (`n @ pat`), `range_pattern`, `slice_pattern`, `ref_pattern`,
|
||||||
//! `reference_pattern`, `mut_pattern`.
|
//! `reference_pattern`, `mut_pattern`.
|
||||||
//!
|
//!
|
||||||
//! **Pendiente:** `if let`, `while let`, `let-else`, let-chains, `or_pattern`
|
//! **Cobertura adicional (este módulo cierra el plan):**
|
||||||
//! con bindings (Rust requiere mismas variables en cada rama).
|
//! - `if_expression` y `while_expression` detectan `let_condition`
|
||||||
|
//! en su `condition` y propagan los binders al `consequence`/`body`.
|
||||||
|
//! Cubre `if let`, `while let` y let-chains (`let X && let Y`).
|
||||||
|
//! - `let_declaration` con `alternative` (let-else): el alternative
|
||||||
|
//! se procesa en el scope SIN los binders del pattern (Rust no
|
||||||
|
//! los ve en la rama de fallo). Funciona naturalmente porque
|
||||||
|
//! `feed_let` no extiende scope; el block padre lo hace después.
|
||||||
|
//! - `or_pattern`: todos los lados tienen los mismos binders (Rust
|
||||||
|
//! enforcement); recolectamos sólo del primer alternativo para
|
||||||
|
//! evitar duplicados, emitimos feed_pattern para cada uno.
|
||||||
|
|
||||||
use crate::ast::SemanticNode;
|
use crate::ast::SemanticNode;
|
||||||
use crate::cas::ContentHash;
|
use crate::cas::ContentHash;
|
||||||
@@ -57,6 +66,9 @@ fn feed(h: &mut Hasher, node: &SemanticNode, scope: &mut Vec<String>) {
|
|||||||
"function_item" | "closure_expression" => feed_callable(h, node, scope),
|
"function_item" | "closure_expression" => feed_callable(h, node, scope),
|
||||||
"block" => feed_block(h, node, scope),
|
"block" => feed_block(h, node, scope),
|
||||||
"for_expression" => feed_for(h, node, scope),
|
"for_expression" => feed_for(h, node, scope),
|
||||||
|
"if_expression" => feed_if_expression(h, node, scope),
|
||||||
|
"while_expression" => feed_while_expression(h, node, scope),
|
||||||
|
"let_condition" => feed_let_condition(h, node, scope),
|
||||||
"match_arm" => feed_match_arm(h, node, scope),
|
"match_arm" => feed_match_arm(h, node, scope),
|
||||||
"identifier" if node.field_name.as_deref() == Some("pattern") => emit_binder_body(h),
|
"identifier" if node.field_name.as_deref() == Some("pattern") => emit_binder_body(h),
|
||||||
"identifier" => emit_identifier_ref(h, node, scope),
|
"identifier" => emit_identifier_ref(h, node, scope),
|
||||||
@@ -64,6 +76,93 @@ fn feed(h: &mut Hasher, node: &SemanticNode, scope: &mut Vec<String>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Dentro de un `let_condition` (`if let X = expr`, `while let X = expr`,
|
||||||
|
/// let-chains), el `pattern` debe pasar por `feed_pattern` para que los
|
||||||
|
/// identifiers del pattern se emitan como TAG_BINDER (anónimos), no
|
||||||
|
/// como referencias libres. El `value` y demás children van por feed
|
||||||
|
/// normal.
|
||||||
|
fn feed_let_condition(h: &mut Hasher, node: &SemanticNode, scope: &mut Vec<String>) {
|
||||||
|
h.update(&[TAG_NO_LEAF]);
|
||||||
|
h.update(&(node.children.len() as u64).to_le_bytes());
|
||||||
|
for c in &node.children {
|
||||||
|
if c.field_name.as_deref() == Some("pattern") {
|
||||||
|
feed_pattern(h, c);
|
||||||
|
} else {
|
||||||
|
feed(h, c, scope);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Maneja `if let X = expr { ... }` y let-chains (`if let X = a && let Y = b`).
|
||||||
|
/// Los binders del/los `let_condition`(s) se acumulan y se propagan
|
||||||
|
/// SÓLO al `consequence` (no al `alternative`, que es el `else`).
|
||||||
|
fn feed_if_expression(h: &mut Hasher, node: &SemanticNode, scope: &mut Vec<String>) {
|
||||||
|
h.update(&[TAG_NO_LEAF]);
|
||||||
|
|
||||||
|
let mut binders: Vec<String> = Vec::new();
|
||||||
|
for c in &node.children {
|
||||||
|
if c.field_name.as_deref() == Some("condition") {
|
||||||
|
collect_let_condition_binders(c, &mut binders);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h.update(&(node.children.len() as u64).to_le_bytes());
|
||||||
|
for c in &node.children {
|
||||||
|
match c.field_name.as_deref() {
|
||||||
|
Some("consequence") => {
|
||||||
|
let scope_before = scope.len();
|
||||||
|
scope.extend(binders.iter().cloned());
|
||||||
|
feed(h, c, scope);
|
||||||
|
scope.truncate(scope_before);
|
||||||
|
}
|
||||||
|
_ => feed(h, c, scope),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Maneja `while let X = expr { ... }`. Los binders del `let_condition`
|
||||||
|
/// se propagan SÓLO al `body` (no al `condition` mismo, que se evalúa
|
||||||
|
/// con el scope previo).
|
||||||
|
fn feed_while_expression(h: &mut Hasher, node: &SemanticNode, scope: &mut Vec<String>) {
|
||||||
|
h.update(&[TAG_NO_LEAF]);
|
||||||
|
|
||||||
|
let mut binders: Vec<String> = Vec::new();
|
||||||
|
for c in &node.children {
|
||||||
|
if c.field_name.as_deref() == Some("condition") {
|
||||||
|
collect_let_condition_binders(c, &mut binders);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h.update(&(node.children.len() as u64).to_le_bytes());
|
||||||
|
for c in &node.children {
|
||||||
|
match c.field_name.as_deref() {
|
||||||
|
Some("body") => {
|
||||||
|
let scope_before = scope.len();
|
||||||
|
scope.extend(binders.iter().cloned());
|
||||||
|
feed(h, c, scope);
|
||||||
|
scope.truncate(scope_before);
|
||||||
|
}
|
||||||
|
_ => feed(h, c, scope),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recolecta binders de patterns dentro de cualquier `let_condition`
|
||||||
|
/// nested en `node`. Para let-chains (`let X = a && let Y = b`),
|
||||||
|
/// recursa en el árbol del condition para capturar todos.
|
||||||
|
fn collect_let_condition_binders(node: &SemanticNode, out: &mut Vec<String>) {
|
||||||
|
if node.kind == "let_condition" {
|
||||||
|
for c in &node.children {
|
||||||
|
if c.field_name.as_deref() == Some("pattern") {
|
||||||
|
collect_pattern_binders(c, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for c in &node.children {
|
||||||
|
collect_let_condition_binders(c, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn feed_default(h: &mut Hasher, node: &SemanticNode, scope: &mut Vec<String>) {
|
fn feed_default(h: &mut Hasher, node: &SemanticNode, scope: &mut Vec<String>) {
|
||||||
emit_leaf_marker(h, node);
|
emit_leaf_marker(h, node);
|
||||||
h.update(&(node.children.len() as u64).to_le_bytes());
|
h.update(&(node.children.len() as u64).to_le_bytes());
|
||||||
@@ -306,6 +405,17 @@ fn feed_pattern(h: &mut Hasher, node: &SemanticNode) {
|
|||||||
feed_pattern(h, c);
|
feed_pattern(h, c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"or_pattern" => {
|
||||||
|
// Cada lado del or-pattern debe introducir el mismo set
|
||||||
|
// de binders (Rust enforcement). Emitimos cada rama pero
|
||||||
|
// sólo recolectaremos binders de la primera —
|
||||||
|
// la responsabilidad recae en `collect_pattern_binders`.
|
||||||
|
h.update(&[TAG_NO_LEAF]);
|
||||||
|
h.update(&(node.children.len() as u64).to_le_bytes());
|
||||||
|
for c in &node.children {
|
||||||
|
feed_pattern(h, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
"tuple_struct_pattern" => {
|
"tuple_struct_pattern" => {
|
||||||
h.update(&[TAG_NO_LEAF]);
|
h.update(&[TAG_NO_LEAF]);
|
||||||
h.update(&(node.children.len() as u64).to_le_bytes());
|
h.update(&(node.children.len() as u64).to_le_bytes());
|
||||||
@@ -409,6 +519,20 @@ fn collect_pattern_binders(p: &SemanticNode, out: &mut Vec<String>) {
|
|||||||
collect_pattern_binders(c, out);
|
collect_pattern_binders(c, out);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"or_pattern" => {
|
||||||
|
// Sólo recolectamos del primer alternativo: Rust exige
|
||||||
|
// que todos los lados introduzcan exactamente los mismos
|
||||||
|
// binders, así que el primero es representativo. Iterar
|
||||||
|
// todos duplicaría los nombres y rompería los índices
|
||||||
|
// de Bruijn en el cuerpo.
|
||||||
|
if let Some(first) = p
|
||||||
|
.children
|
||||||
|
.iter()
|
||||||
|
.find(|c| !matches!(c.kind.as_str(), "|" | "or"))
|
||||||
|
{
|
||||||
|
collect_pattern_binders(first, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
"tuple_struct_pattern" => {
|
"tuple_struct_pattern" => {
|
||||||
for c in &p.children {
|
for c in &p.children {
|
||||||
if c.field_name.as_deref() != Some("type") {
|
if c.field_name.as_deref() != Some("type") {
|
||||||
|
|||||||
@@ -250,3 +250,118 @@ fn alpha_match_constructor_vs_binder() {
|
|||||||
parse::rust("fn f(v: Option<i32>) -> i32 { match v { x => 0, Some(z) => z } }").unwrap();
|
parse::rust("fn f(v: Option<i32>) -> i32 { match v { x => 0, Some(z) => z } }").unwrap();
|
||||||
assert_ne!(hash_node_alpha(&a), hash_node_alpha(&b));
|
assert_ne!(hash_node_alpha(&a), hash_node_alpha(&b));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ====================================================================
|
||||||
|
// Pendientes documentados — cierre del MVP de α-Rust.
|
||||||
|
// ====================================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn alpha_if_let_binder_rename_invariant() {
|
||||||
|
// El binder de `if let Some(x) = v` participa sólo del consequence.
|
||||||
|
// Renombrar x por y no debe afectar el hash.
|
||||||
|
let a = parse::rust(
|
||||||
|
"fn f(v: Option<i32>) -> i32 { if let Some(x) = v { x + 1 } else { 0 } }",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let b = parse::rust(
|
||||||
|
"fn f(v: Option<i32>) -> i32 { if let Some(y) = v { y + 1 } else { 0 } }",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(hash_node_alpha(&a), hash_node_alpha(&b));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn alpha_if_let_else_does_not_see_binder() {
|
||||||
|
// Sanity: el binder NO debe visitar el `else` (alternative). En
|
||||||
|
// `if let Some(x) = v { ... } else { v }`, el `else` ve `v` libre.
|
||||||
|
// Si renombramos sólo en el consequence, da el mismo hash.
|
||||||
|
let a = parse::rust(
|
||||||
|
"fn f(v: Option<i32>) -> i32 { if let Some(x) = v { x } else { 0 } }",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let b = parse::rust(
|
||||||
|
"fn f(v: Option<i32>) -> i32 { if let Some(y) = v { y } else { 0 } }",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(hash_node_alpha(&a), hash_node_alpha(&b));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn alpha_while_let_binder_rename_invariant() {
|
||||||
|
// El binder del while-let vive sólo en el body.
|
||||||
|
let a = parse::rust(
|
||||||
|
"fn f(mut it: Option<i32>) -> i32 { let mut total = 0; while let Some(x) = it { total += x; it = None; } total }",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let b = parse::rust(
|
||||||
|
"fn f(mut it: Option<i32>) -> i32 { let mut total = 0; while let Some(y) = it { total += y; it = None; } total }",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(hash_node_alpha(&a), hash_node_alpha(&b));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn alpha_let_else_binder_rename_invariant() {
|
||||||
|
// let-else: el binder vive sólo después del let, no en el else.
|
||||||
|
let a = parse::rust(
|
||||||
|
"fn f(v: Option<i32>) -> i32 { let Some(x) = v else { return 0 }; x + 1 }",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let b = parse::rust(
|
||||||
|
"fn f(v: Option<i32>) -> i32 { let Some(y) = v else { return 0 }; y + 1 }",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(hash_node_alpha(&a), hash_node_alpha(&b));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn alpha_or_pattern_binder_rename_invariant() {
|
||||||
|
// En un or-pattern (`Some(x) | Other(x)`), todos los lados
|
||||||
|
// introducen el mismo binder. Renombrar afecta a TODOS los lados
|
||||||
|
// a la vez. El hash se mantiene.
|
||||||
|
let a = parse::rust(
|
||||||
|
r#"
|
||||||
|
enum E { A(i32), B(i32) }
|
||||||
|
fn f(v: E) -> i32 { match v { E::A(x) | E::B(x) => x } }
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let b = parse::rust(
|
||||||
|
r#"
|
||||||
|
enum E { A(i32), B(i32) }
|
||||||
|
fn f(v: E) -> i32 { match v { E::A(y) | E::B(y) => y } }
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(hash_node_alpha(&a), hash_node_alpha(&b));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn alpha_let_chain_binders_propagate_to_consequence() {
|
||||||
|
// Let-chain: dos let-conditions con &&. Ambos binders viven en
|
||||||
|
// el consequence. Renombrar ambos da mismo hash.
|
||||||
|
let a = parse::rust(
|
||||||
|
"fn f(a: Option<i32>, b: Option<i32>) -> i32 { if let Some(x) = a && let Some(y) = b { x + y } else { 0 } }",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let c = parse::rust(
|
||||||
|
"fn f(a: Option<i32>, b: Option<i32>) -> i32 { if let Some(p) = a && let Some(q) = b { p + q } else { 0 } }",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(hash_node_alpha(&a), hash_node_alpha(&c));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn alpha_if_let_does_not_collide_with_unrelated_program() {
|
||||||
|
// Sanity negativo: dos programas con `if let` distintos
|
||||||
|
// (operación distinta) NO deben dar el mismo hash.
|
||||||
|
let plus = parse::rust(
|
||||||
|
"fn f(v: Option<i32>) -> i32 { if let Some(x) = v { x + 1 } else { 0 } }",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let minus = parse::rust(
|
||||||
|
"fn f(v: Option<i32>) -> i32 { if let Some(x) = v { x - 1 } else { 0 } }",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_ne!(hash_node_alpha(&plus), hash_node_alpha(&minus));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user