diff --git a/bootstrap/src/compiler.rs b/bootstrap/src/compiler.rs index a2c60685c..ba904b273 100644 --- a/bootstrap/src/compiler.rs +++ b/bootstrap/src/compiler.rs @@ -6498,6 +6498,56 @@ fn parse_int_value(s: &str) -> Option { } } +/// Collect all local names that are reassigned anywhere in a statement list. +/// This includes: +/// - Simple assignments: `x = expr` → x is mutable +/// - Index assignments: `arr[i] = expr` → arr is mutable (Rust requires `let mut`) +/// - Field assignments: `s.f = expr` → s is mutable +/// Recurses into if/while/for bodies. +fn collect_mutable_names(stmts: &[Node], set: &mut std::collections::HashSet) { + for stmt in stmts { + collect_mutable_names_one(stmt, set); + } +} + +fn collect_mutable_names_one(stmt: &Node, set: &mut std::collections::HashSet) { + match stmt.kind { + NodeKind::StmtAssign if !stmt.children.is_empty() => { + let lhs = &stmt.children[0]; + // Simple identifier: `x = expr` + if lhs.kind == NodeKind::ExprIdentifier { + set.insert(lhs.name.clone()); + } + // Index assignment: `arr[i] = expr` → arr needs mut + // The LHS is ExprIndex with children[0] = base identifier + if lhs.kind == NodeKind::ExprIndex && !lhs.children.is_empty() { + let base = &lhs.children[0]; + if base.kind == NodeKind::ExprIdentifier { + set.insert(base.name.clone()); + } + } + // Field assignment: `s.f = expr` → s needs mut + if lhs.kind == NodeKind::ExprFieldAccess && !lhs.children.is_empty() { + let base = &lhs.children[0]; + if base.kind == NodeKind::ExprIdentifier { + set.insert(base.name.clone()); + } + } + } + NodeKind::StmtIf | NodeKind::StmtWhile | NodeKind::StmtFor | NodeKind::StmtForRange => { + // Recurse into condition + body blocks + for c in &stmt.children { + if c.kind == NodeKind::Module { + collect_mutable_names(&c.children, set); + } else { + collect_mutable_names_one(c, set); + } + } + } + _ => {} + } +} + fn copy_propagate(stmts: &mut Vec, stats: &mut OptStats) { let mut replacements: Vec<(String, String)> = Vec::new(); for stmt in stmts.iter() { @@ -7610,6 +7660,10 @@ fn resolve_type_str(s: &str) -> TypeInfo { pub struct RustCodegen { output: String, indent: usize, + /// Names of locals that are reassigned in the current function body + /// (via simple assignment `x = ...` or index assignment `arr[i] = ...`). + /// Used to infer `let mut` for locals declared with `let`. + mut_names: std::collections::HashSet, } #[allow(dead_code)] @@ -7618,6 +7672,7 @@ impl RustCodegen { RustCodegen { output: String::new(), indent: 0, + mut_names: std::collections::HashSet::new(), } } @@ -7774,6 +7829,10 @@ impl RustCodegen { matches!(c.kind, NodeKind::ExprReturn | NodeKind::StmtExpr | NodeKind::StmtLocal | NodeKind::StmtIf | NodeKind::StmtWhile | NodeKind::StmtFor | NodeKind::StmtForRange) }); + // Infer which locals need `let mut`: scan body for any assignment. + self.mut_names.clear(); + collect_mutable_names(&node.children, &mut self.mut_names); + if has_body { self.output.push('\n'); self.indent += 1; @@ -7794,7 +7853,7 @@ impl RustCodegen { } } NodeKind::StmtLocal => { - let mutable = child.extra_mutable; + let mutable = child.extra_mutable || self.mut_names.contains(&child.name); let kw = if mutable { "let mut" } else { "let" }; let var_name = &child.name; let typ = Self::t27_type_to_rust(&child.extra_type); @@ -7935,7 +7994,7 @@ impl RustCodegen { } } NodeKind::StmtLocal => { - let kw = if stmt.extra_mutable { "let mut" } else { "let" }; + let kw = if stmt.extra_mutable || self.mut_names.contains(&stmt.name) { "let mut" } else { "let" }; let typ = Self::t27_type_to_rust(&stmt.extra_type); if stmt.children.is_empty() { if stmt.extra_type.is_empty() { diff --git a/bootstrap/stage0/FROZEN_HASH b/bootstrap/stage0/FROZEN_HASH index 1e3ad52fc..062518f1a 100644 --- a/bootstrap/stage0/FROZEN_HASH +++ b/bootstrap/stage0/FROZEN_HASH @@ -1 +1 @@ -197d6eb5a40bed62ae806cb09c14fbde4d2664ed7cc6991c1e3bfa60983706ff bootstrap/src/compiler.rs +9f64cb53fdfdd3c7e6fea85c74a4f6243ee8117f5b6544c987988f7f48eb72b0 bootstrap/src/compiler.rs diff --git a/docs/NOW.md b/docs/NOW.md index dc647be6e..1640ab342 100644 --- a/docs/NOW.md +++ b/docs/NOW.md @@ -1,7 +1,13 @@ # NOW — IGLA cycle 1 + Wave Loop 469 context (2026-07-07) -Last updated: 2026-07-07 +Last updated: 2026-07-13 +## t27c codegen: mut-inference for reassigned locals (Fixes #1463) + +- PR #1461 adds mutability inference to the Rust codegen backend. +- `collect_mutable_names()` scans function bodies for assignment targets + (simple, index, field), emitting `let mut` where needed. +- Eliminates 117 E0384 errors in tri-net specs. ## IGLA cycle 1 — process debt needles (Refs #1438, #1440, #1442, #1444, #1446) - Charter: `docs/nona-03-manifest/IGLA_IMPROVEMENT_LOOP.md` + audit `docs/reports/IGLA_AUDIT_W470_2026-07-07.md`.