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
@@ -3,16 +3,9 @@
//! under `examples/*.ail.json` after milestone close. The list is
//! hardcoded; any change is a deliberate, brainstorm-level decision.
//!
//! Eighteen carve-outs post iter loop-recur.tidy (2026-05-18):
//! Twelve carve-outs:
//! - §C4 (a) subject-matter: 7 fixtures from form-a.1 milestone
//! close (canonical-form rejection / unbound / class-def rejection).
//! - Iter mut.2: 5 negative typecheck fixtures for the new mut /
//! var / assign forms. The diagnostic code is the load-bearing
//! assertion (mut-assign-out-of-scope, assign-type-mismatch,
//! mut-var-unsupported-type) — mirroring the §C4(a) negative-
//! fixture convention. The positive nested-shadow case is a
//! .ail.json carve-out for symmetry with its negative siblings
//! (the driver tests all five from one helper).
//! - §C4 (b) compile-time-embed: retired 2026-05-14 by milestone
//! prelude-decouple; the prelude is now embedded as `prelude.ail`
//! in `ailang-surface` and parsed at compile time via
@@ -21,12 +14,11 @@
//! four `Recur*` diagnostics (recur-outside-loop /
//! recur-arity-mismatch / recur-type-mismatch /
//! recur-not-in-tail-position) — the diagnostic code is the
//! load-bearing assertion, mirroring the §C4(a) / mut.2
//! load-bearing assertion, mirroring the §C4(a)
//! negative-fixture convention.
//! - loop-recur iter loop-recur.tidy: 1 negative typecheck fixture
//! for the lambda-capture-of-loop-binder rejection
//! (loop-binder-captured-by-lambda) — symmetric to the mut.4-tidy
//! `test_mut_var_captured_by_lambda.ail.json` carve-out.
//! (loop-binder-captured-by-lambda).
use std::collections::BTreeSet;
@@ -39,14 +31,6 @@ const EXPECTED: &[&str] = &[
"test_ct1_bad_qualifier.ail.json",
"test_ct1_bare_xmod_rejected.ail.json",
"test_ct1_qualified_class_rejected.ail.json",
// Iter mut.2 — negative typecheck fixtures + positive shadow-pin
"test_mut_assign_out_of_scope.ail.json",
"test_mut_assign_outside_mut.ail.json",
"test_mut_assign_type_mismatch.ail.json",
"test_mut_nested_shadow_legal.ail.json",
"test_mut_var_unsupported_type.ail.json",
// Iter mut.4-tidy — lambda-capture-of-mut-var rejection
"test_mut_var_captured_by_lambda.ail.json",
// Iter loop-recur.tidy — lambda-capture-of-loop-binder rejection
"test_loop_binder_captured_by_lambda.ail.json",
// loop-recur iter 2 — negative typecheck fixtures
@@ -137,20 +137,6 @@ fn design_md_anchors_every_term_variant() {
body: Box::new(Term::Var { name: "y".into() }),
},
),
(
r#""t": "mut""#,
Term::Mut {
vars: Vec::new(),
body: Box::new(Term::Lit { lit: Literal::Unit }),
},
),
(
r#""t": "assign""#,
Term::Assign {
name: "x".into(),
value: Box::new(Term::Lit { lit: Literal::Unit }),
},
),
(
r#""t": "loop""#,
Term::Loop {
@@ -181,8 +167,6 @@ fn design_md_anchors_every_term_variant() {
Term::Seq { .. } => "seq",
Term::Clone { .. } => "clone",
Term::ReuseAs { .. } => "reuse-as",
Term::Mut { .. } => "mut",
Term::Assign { .. } => "assign",
Term::Loop { .. } => "loop",
Term::Recur { .. } => "recur",
};
@@ -444,19 +428,3 @@ fn data_model_section_is_bounded() {
);
}
#[test]
fn design_md_documents_the_accumulator_over_iteration_idiom() {
let design = std::fs::read_to_string(
concat!(env!("CARGO_MANIFEST_DIR"), "/../../docs/DESIGN.md"),
)
.expect("read DESIGN.md");
assert!(
design.contains("Accumulator-over-iteration shape (documented idiom, not an enforced rule)"),
"the F1/F4 documented-idiom note must not be silently dropped from DESIGN.md"
);
assert!(
design.contains("examples/mut_counter.ail is the reference")
|| design.contains("`examples/mut_counter.ail` is the reference"),
"the F1/F4 note must keep pointing at the canonical fallback fixture"
);
}
@@ -47,8 +47,6 @@ enum VariantTag {
TermSeq,
TermClone,
TermReuseAs,
TermMut,
TermAssign,
TermLoop,
TermRecur,
// Pattern
@@ -96,8 +94,6 @@ const EXPECTED_VARIANTS: &[VariantTag] = &[
VariantTag::TermSeq,
VariantTag::TermClone,
VariantTag::TermReuseAs,
VariantTag::TermMut,
VariantTag::TermAssign,
VariantTag::TermLoop,
VariantTag::TermRecur,
VariantTag::PatternWild,
@@ -236,18 +232,6 @@ fn visit_term(t: &Term, observed: &mut HashSet<VariantTag>) {
visit_term(source, observed);
visit_term(body, observed);
}
Term::Mut { vars, body } => {
observed.insert(VariantTag::TermMut);
for v in vars {
visit_type(&v.ty, observed);
visit_term(&v.init, observed);
}
visit_term(body, observed);
}
Term::Assign { value, .. } => {
observed.insert(VariantTag::TermAssign);
visit_term(value, observed);
}
Term::Loop { binders, body } => {
observed.insert(VariantTag::TermLoop);
for b in binders {
-16
View File
@@ -114,20 +114,6 @@ fn spec_mentions_every_term_variant() {
body: Box::new(Term::Var { name: "y".into() }),
},
),
(
"(mut",
Term::Mut {
vars: Vec::new(),
body: Box::new(Term::Lit { lit: Literal::Unit }),
},
),
(
"(assign",
Term::Assign {
name: "x".into(),
value: Box::new(Term::Lit { lit: Literal::Unit }),
},
),
(
"(loop",
Term::Loop {
@@ -160,8 +146,6 @@ fn spec_mentions_every_term_variant() {
Term::Seq { .. } => "seq",
Term::Clone { .. } => "clone",
Term::ReuseAs { .. } => "reuse-as",
Term::Mut { .. } => "mut",
Term::Assign { .. } => "assign",
Term::Loop { .. } => "loop",
Term::Recur { .. } => "recur",
};