iter remove-mut-var-assign.1: atomic removal of mut/var/assign

mut/var/assign removed from AILang entirely and atomically. Deleted:
Term::Mut/Term::Assign/struct MutVar; the three Form-A keywords +
parse_mut/parse_assign + grammar EBNF; the 4 mut CheckError variants;
the mut_scope_stack synth threading (param dropped from synth + every
internal/external/test caller); the two lower_term arms; and every
exhaustive no-_ Term::Mut/Term::Assign match arm across 17 source
files — cut in lockstep with DESIGN.md, fixtures, the drift trio,
carve-out and roadmap so the schema is honest at every commit. No
catch-all wildcard introduced (verified). loop/recur + let/if are
the surviving forms.

The shared codegen alloca machinery survives (loop reuses it):
mut_var_allocas renamed binder_allocas (representation-only, loop
codegen byte-identical) and the shared Term::Lam escape guard
simplified to !loop_stack.is_empty() with the loop half
(LoopBinderCapturedByLambda) byte-equivalent. Feature-acceptance
applied inverted: the removed feature fails clause 2 (redundant)
and clause 3 (IS the iterated-mutable-state bug class).

Behaviour preservation is executable: mut_counter/mut_sum_floats
still print 55 after the faithful let/if rewrite. The removal is
made executable by the new mut_removed_pin.rs (4 must-fail pins).
Independent verification: cargo test --workspace 605/0, zero
residual mut symbols in any crate source, loop/recur non-regression
all green (55 / 500000500000 / infinite-compiles / the
lambda_capturing_loop_binder pin), roundtrip_cli PASS.

One DONE_WITH_CONCERNS: a 4th recurrence of the recon-undercount
class (in-source mod tests + a drift-pin fn + 5 orphaned mut
.ail.json carve-outs + a non-enumerated E0599); all resolved within
implementer remit, no behaviour change. Milestone-close audit then
fieldtest remain.

spec docs/specs/2026-05-18-remove-mut-var-assign.md (grounding PASS)
plan docs/plans/remove-mut-var-assign.1.md
This commit is contained in:
2026-05-18 11:06:17 +02:00
parent f355899fdf
commit 07f080256c
48 changed files with 331 additions and 2523 deletions
+2 -99
View File
@@ -906,35 +906,6 @@ fn write_term_prec(out: &mut String, t: &Term, level: usize, parent_prec: u8, ow
out.push_str(&body_buf);
}
}
Term::Mut { vars, body } => {
// Iter mut.1: prose-side minimal-correctness rendering for
// the `(mut ...)` block. The prose-form for mut blocks is
// not yet fully designed (the prose surface predates this
// milestone); render the block as `mut { var <name>: <ty>
// = <init>; ...; <body> }` so a reader can see the shape
// without overcommitting to a final prose syntax. Refined
// in a follow-on prose iter once the LLM-author signal
// arrives.
out.push_str("mut {\n");
for v in vars {
indent(out, level + 1);
out.push_str("var ");
out.push_str(&v.name);
out.push_str(" = ");
write_term(out, &v.init, level + 1, owning_module);
out.push_str(";\n");
}
indent(out, level + 1);
write_term(out, body, level + 1, owning_module);
out.push('\n');
indent(out, level);
out.push('}');
}
Term::Assign { name, value } => {
out.push_str(name);
out.push_str(" := ");
write_term(out, value, level, owning_module);
}
Term::Loop { binders, body } => {
out.push_str("loop(");
for (i, b) in binders.iter().enumerate() {
@@ -1133,33 +1104,6 @@ fn count_free_var(name: &str, t: &Term) -> usize {
}
Term::Clone { value } => count_free_var(name, value),
Term::ReuseAs { source, body } => count_free_var(name, source) + count_free_var(name, body),
// Iter mut.1: a `Term::Mut` whose `vars` includes a name
// matching `name` shadows the outer binding for both later
// var inits and the body. Var inits before the shadowing one
// still see the outer `name`.
Term::Mut { vars, body } => {
let mut total = 0usize;
let mut shadowed = false;
for v in vars {
if !shadowed {
total += count_free_var(name, &v.init);
}
if v.name == name {
shadowed = true;
}
}
if !shadowed {
total += count_free_var(name, body);
}
total
}
// Iter mut.1: the assigned `name` field is a use of the
// mut-var binding (which may or may not be `name`); the
// value is recursed.
Term::Assign { name: assign_name, value } => {
let n_use = if assign_name == name { 1 } else { 0 };
n_use + count_free_var(name, value)
}
// loop-recur iter 1: a binder named `name` shadows the outer
// binding for later binder inits and the body. Inits before
// the shadowing binder still see the outer `name`.
@@ -1293,50 +1237,9 @@ fn subst_var_with_term(t: &Term, name: &str, replacement: &Term) -> Term {
source: Box::new(subst_var_with_term(source, name, replacement)),
body: Box::new(subst_var_with_term(body, name, replacement)),
},
// Iter mut.1: lexical-shadow semantics for mut-vars symmetric
// to `count_free_var` above: once a var named `name` is
// declared, neither later inits nor the body should be
// loop-recur iter 1: lexical-shadow semantics — once a binder
// named `name` is declared, later inits and the body are not
// rewritten.
Term::Mut { vars, body } => {
let mut shadowed = false;
let new_vars: Vec<ailang_core::ast::MutVar> = vars
.iter()
.map(|v| {
let init = if shadowed {
v.init.clone()
} else {
subst_var_with_term(&v.init, name, replacement)
};
if v.name == name {
shadowed = true;
}
ailang_core::ast::MutVar {
name: v.name.clone(),
ty: v.ty.clone(),
init,
}
})
.collect();
let body_rw = if shadowed {
(**body).clone()
} else {
subst_var_with_term(body, name, replacement)
};
Term::Mut {
vars: new_vars,
body: Box::new(body_rw),
}
}
// Iter mut.1: substitute only inside `value`. The `name`
// field is a binding reference (cf. the desugar.rs
// `subst_var` arm).
Term::Assign { name: assign_name, value } => Term::Assign {
name: assign_name.clone(),
value: Box::new(subst_var_with_term(value, name, replacement)),
},
// loop-recur iter 1: lexical-shadow semantics symmetric to
// count_free_var — once a binder named `name` is declared,
// later inits and the body are not rewritten.
Term::Loop { binders, body } => {
let mut shadowed = false;
let new_binders: Vec<ailang_core::ast::LoopBinder> = binders