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:
@@ -40,7 +40,6 @@
|
||||
//! | app-term | tail-app-term | match-term | ctor-term
|
||||
//! | do-term | tail-do-term | seq-term | lam-term | if-term
|
||||
//! | let-term | let-rec-term | clone-term | reuse-as-term
|
||||
//! | mut-term | assign-term
|
||||
//! var-ref ::= ident ; reserved: true/false → bool-lit
|
||||
//! int-lit ::= integer ; numeric atom
|
||||
//! str-lit ::= string ; string atom
|
||||
@@ -67,9 +66,6 @@
|
||||
//! "(" "in" term ")" ")"
|
||||
//! clone-term ::= "(" "clone" term ")" ; Iter 18c.1
|
||||
//! reuse-as-term ::= "(" "reuse-as" term term ")" ; Iter 18d.1
|
||||
//! mut-term ::= "(" "mut" var-decl* term+ ")" ; Iter mut.1
|
||||
//! var-decl ::= "(" "var" ident type term ")" ; legal only inside mut-term
|
||||
//! assign-term ::= "(" "assign" ident term ")" ; Iter mut.1; legal only inside mut-term
|
||||
//!
|
||||
//! pattern ::= pat-var | pat-ctor | pat-lit | pat-wild
|
||||
//! pat-var ::= ident
|
||||
@@ -1210,8 +1206,6 @@ impl<'a> Parser<'a> {
|
||||
"let-rec" => self.parse_let_rec(),
|
||||
"clone" => self.parse_clone(),
|
||||
"reuse-as" => self.parse_reuse_as(),
|
||||
"mut" => self.parse_mut(),
|
||||
"assign" => self.parse_assign(),
|
||||
"loop" => self.parse_loop(),
|
||||
"recur" => self.parse_recur(),
|
||||
other => {
|
||||
@@ -1221,8 +1215,8 @@ impl<'a> Parser<'a> {
|
||||
message: format!(
|
||||
"unknown term head `{other}`; expected one of \
|
||||
`app`, `tail-app`, `lam`, `let`, `let-rec`, `if`, `match`, `do`, \
|
||||
`tail-do`, `seq`, `term-ctor`, `clone`, `reuse-as`, `mut`, \
|
||||
`assign`, `loop`, `recur`, `lit-unit`"
|
||||
`tail-do`, `seq`, `term-ctor`, `clone`, `reuse-as`, \
|
||||
`loop`, `recur`, `lit-unit`"
|
||||
),
|
||||
pos,
|
||||
})
|
||||
@@ -1574,82 +1568,11 @@ impl<'a> Parser<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Iter mut.1: `(mut (var NAME TYPE INIT)* BODY_TERM+)` — local
|
||||
/// mutable-state block. Reads zero or more `(var ...)` entries
|
||||
/// off the front of the body, then ≥ 1 trailing terms. The
|
||||
/// trailing sequence is right-folded into `Term::Seq` with the
|
||||
/// final term as the seed (so the JSON-AST always sees a single
|
||||
/// `body: Term`). Spec `docs/specs/2026-05-15-mut-local.md`
|
||||
/// §"Form A surface" + §"Canonical-form invariants".
|
||||
fn parse_mut(&mut self) -> Result<Term, ParseError> {
|
||||
let head_pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
||||
self.expect_lparen("mut-term")?;
|
||||
self.expect_keyword("mut")?;
|
||||
|
||||
// Pull off all leading (var NAME TYPE INIT) entries.
|
||||
let mut vars: Vec<ailang_core::ast::MutVar> = Vec::new();
|
||||
while matches!(self.peek_head_ident(), Some("var")) {
|
||||
self.expect_lparen("mut-var")?;
|
||||
self.expect_keyword("var")?;
|
||||
let name = self.expect_ident("mut-var-name")?;
|
||||
let ty = self.parse_type()?;
|
||||
let init = self.parse_term()?;
|
||||
self.expect_rparen("mut-var")?;
|
||||
vars.push(ailang_core::ast::MutVar { name, ty, init });
|
||||
}
|
||||
|
||||
// Then read ≥ 1 trailing body terms. Right-fold into Seq
|
||||
// with the final term as the seed.
|
||||
let mut body_stmts: Vec<Term> = Vec::new();
|
||||
while !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
|
||||
body_stmts.push(self.parse_term()?);
|
||||
}
|
||||
if body_stmts.is_empty() {
|
||||
return Err(ParseError::Production {
|
||||
production: "mut-term",
|
||||
message: "(mut ...) requires at least one body expression after vars".into(),
|
||||
pos: head_pos,
|
||||
});
|
||||
}
|
||||
self.expect_rparen("mut-term")?;
|
||||
|
||||
let mut body = body_stmts.pop().expect("non-empty after the check above");
|
||||
while let Some(s) = body_stmts.pop() {
|
||||
body = Term::Seq {
|
||||
lhs: Box::new(s),
|
||||
rhs: Box::new(body),
|
||||
};
|
||||
}
|
||||
Ok(Term::Mut {
|
||||
vars,
|
||||
body: Box::new(body),
|
||||
})
|
||||
}
|
||||
|
||||
/// Iter mut.1: `(assign NAME VALUE)` — in-block update of a
|
||||
/// mut-var. Positional shape: name then value. The lexical-scope
|
||||
/// rule ("legal only inside a `(mut ...)` whose `vars` includes
|
||||
/// a var with the same `name`") is enforced at typecheck
|
||||
/// (mut.2, `CheckError::MutAssignOutOfScope`); the parser
|
||||
/// accepts the shape unconditionally.
|
||||
fn parse_assign(&mut self) -> Result<Term, ParseError> {
|
||||
self.expect_lparen("assign-term")?;
|
||||
self.expect_keyword("assign")?;
|
||||
let name = self.expect_ident("assign-name")?;
|
||||
let value = self.parse_term()?;
|
||||
self.expect_rparen("assign-term")?;
|
||||
Ok(Term::Assign {
|
||||
name,
|
||||
value: Box::new(value),
|
||||
})
|
||||
}
|
||||
|
||||
/// loop-recur iter 1: `(loop (NAME TYPE INIT)* BODY_TERM+)` —
|
||||
/// strict iteration block. Reads zero or more `(NAME TYPE INIT)`
|
||||
/// binder triples (bare, no leading keyword — unlike `(var ...)`,
|
||||
/// the loop binder triple has no `var` keyword), then ≥ 1
|
||||
/// trailing terms right-folded into `Term::Seq` (mirrors
|
||||
/// `parse_mut`). The binder/body boundary is disambiguated by the
|
||||
/// binder triples (bare, no leading keyword), then ≥ 1
|
||||
/// trailing terms right-folded into `Term::Seq`. The binder/body
|
||||
/// boundary is disambiguated by the
|
||||
/// inner head: a binder's first inner token is an ident NAME
|
||||
/// (never a term-head keyword), whereas a parenthesised body form
|
||||
/// (`(if …)`, `(recur …)`, `(app …)`, …) opens with a term-head
|
||||
@@ -1673,8 +1596,6 @@ impl<'a> Parser<'a> {
|
||||
| "let-rec"
|
||||
| "clone"
|
||||
| "reuse-as"
|
||||
| "mut"
|
||||
| "assign"
|
||||
| "loop"
|
||||
| "recur"
|
||||
)
|
||||
@@ -2594,89 +2515,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Iter mut.1: `(mut 0)` parses as a `Term::Mut` with empty `vars`
|
||||
/// and an `Int 0` body. Spec
|
||||
/// `docs/specs/2026-05-15-mut-local.md` §"Canonical-form
|
||||
/// invariants" — the empty-vars form is a legal degenerate case
|
||||
/// the schema accepts uniformly.
|
||||
#[test]
|
||||
fn parses_empty_mut_with_int_body() {
|
||||
let t = parse_term("(mut 0)").expect("parse");
|
||||
match t {
|
||||
Term::Mut { vars, body } => {
|
||||
assert!(vars.is_empty(), "vars should be empty, got {vars:?}");
|
||||
match *body {
|
||||
Term::Lit { lit: Literal::Int { value: 0 } } => {}
|
||||
other => panic!("expected Lit Int 0, got {other:?}"),
|
||||
}
|
||||
}
|
||||
other => panic!("expected Term::Mut, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Iter mut.1: `(mut (var x ...) (assign x ...) x)` parses with
|
||||
/// the trailing statement-and-final-expression sequence right-
|
||||
/// folded into a `Term::Seq`. The schema's `Term::Mut.body` is
|
||||
/// always a single Term; multi-statement bodies are folded
|
||||
/// through `Seq` at parse time.
|
||||
#[test]
|
||||
fn parses_mut_with_one_var_and_one_assign_and_final() {
|
||||
let src = "(mut (var x (con Int) 0) (assign x (app + x 1)) x)";
|
||||
let t = parse_term(src).expect("parse");
|
||||
match t {
|
||||
Term::Mut { vars, body } => {
|
||||
assert_eq!(vars.len(), 1, "expected exactly one var");
|
||||
assert_eq!(vars[0].name, "x");
|
||||
match *body {
|
||||
Term::Seq { lhs, rhs } => {
|
||||
match *lhs {
|
||||
Term::Assign { name, .. } => assert_eq!(name, "x"),
|
||||
other => panic!("expected Assign as Seq.lhs, got {other:?}"),
|
||||
}
|
||||
match *rhs {
|
||||
Term::Var { name } => assert_eq!(name, "x"),
|
||||
other => panic!("expected Var x as Seq.rhs, got {other:?}"),
|
||||
}
|
||||
}
|
||||
other => panic!("expected Seq as Mut.body, got {other:?}"),
|
||||
}
|
||||
}
|
||||
other => panic!("expected Term::Mut, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Iter mut.1: `(mut)` with neither vars nor body is rejected at
|
||||
/// parse — the spec's §"Canonical-form invariants" mandates at
|
||||
/// least one body expression after the `var` entries.
|
||||
#[test]
|
||||
fn rejects_mut_with_no_body() {
|
||||
let err = parse_term("(mut)").expect_err("must reject empty mut");
|
||||
let msg = format!("{err}");
|
||||
assert!(
|
||||
msg.contains("mut"),
|
||||
"diagnostic should mention `mut`, got: {msg}"
|
||||
);
|
||||
assert!(
|
||||
msg.contains("body") || msg.contains("expression"),
|
||||
"diagnostic should mention the missing body, got: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Iter mut.1: `(mut (var x ...))` declares a var but has no
|
||||
/// trailing body expression — also rejected (the rule is "≥ 0
|
||||
/// vars followed by ≥ 1 body terms"; zero body terms is the
|
||||
/// rejection trigger).
|
||||
#[test]
|
||||
fn rejects_mut_with_only_vars_no_final_expression() {
|
||||
let err = parse_term("(mut (var x (con Int) 0))")
|
||||
.expect_err("must reject vars-only mut");
|
||||
let msg = format!("{err}");
|
||||
assert!(
|
||||
msg.contains("body") || msg.contains("expression"),
|
||||
"diagnostic should mention missing body, got: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_loop_and_recur_round_trip_via_term() {
|
||||
let src = "(loop (acc (con Int) 0) (i (con Int) 1) (if (app > i n) acc (recur (app + acc i) (app + i 1))))";
|
||||
|
||||
@@ -548,57 +548,10 @@ fn write_term(out: &mut String, t: &Term, level: usize) {
|
||||
write_term(out, body, level);
|
||||
out.push(')');
|
||||
}
|
||||
Term::Mut { vars, body } => {
|
||||
// Iter mut.1: print as
|
||||
// `(mut (var NAME TYPE INIT)* STMT* FINAL_EXPR)`
|
||||
// where the body's right-spine of `Term::Seq` is walked
|
||||
// and each lhs is printed as a top-level statement; the
|
||||
// terminal expression (the rightmost non-Seq term) is the
|
||||
// block's value. This is the inverse of the parser's
|
||||
// right-fold over the trailing sequence.
|
||||
out.push_str("(mut");
|
||||
for v in vars {
|
||||
out.push_str(" (var ");
|
||||
out.push_str(&v.name);
|
||||
out.push(' ');
|
||||
write_type(out, &v.ty);
|
||||
out.push(' ');
|
||||
write_term(out, &v.init, level);
|
||||
out.push(')');
|
||||
}
|
||||
let mut cursor: &Term = body;
|
||||
loop {
|
||||
match cursor {
|
||||
Term::Seq { lhs, rhs } => {
|
||||
out.push(' ');
|
||||
write_term(out, lhs, level);
|
||||
cursor = rhs;
|
||||
}
|
||||
other => {
|
||||
out.push(' ');
|
||||
write_term(out, other, level);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
out.push(')');
|
||||
}
|
||||
Term::Assign { name, value } => {
|
||||
// Iter mut.1: print as `(assign NAME VALUE)`. Legal only
|
||||
// inside a `Term::Mut.body`; the parser enforces the
|
||||
// structural rule, the typechecker (mut.2) enforces the
|
||||
// scope rule.
|
||||
out.push_str("(assign ");
|
||||
out.push_str(name);
|
||||
out.push(' ');
|
||||
write_term(out, value, level);
|
||||
out.push(')');
|
||||
}
|
||||
Term::Loop { binders, body } => {
|
||||
// loop-recur iter 1: print as
|
||||
// `(loop (NAME TYPE INIT)* STMT* FINAL_EXPR)`
|
||||
// — inverse of parse_loop's binder read + Seq right-fold,
|
||||
// mirroring the Term::Mut printer.
|
||||
// — inverse of parse_loop's binder read + Seq right-fold.
|
||||
out.push_str("(loop");
|
||||
for b in binders {
|
||||
out.push_str(" (");
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
//! Pins the mut/var/assign removal: the keyword no longer parses and
|
||||
//! the canonical JSON tag is an unknown serde variant. RED until the
|
||||
//! Task-2 atomic cut lands.
|
||||
use ailang_surface::parse;
|
||||
|
||||
#[test]
|
||||
fn mut_keyword_is_rejected_by_form_a_parse() {
|
||||
let src = "(module m (fn f (type (fn-type (params) (ret (con Int)))) (params) (body (mut (var x (con Int) 0) (assign x (app + x 1)) x))))";
|
||||
let r = parse(src);
|
||||
assert!(r.is_err(), "mut keyword must no longer parse, got Ok");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assign_keyword_is_rejected_by_form_a_parse() {
|
||||
let src = "(module m (fn f (type (fn-type (params) (ret (con Int)))) (params) (body (assign x 1))))";
|
||||
assert!(parse(src).is_err(), "assign keyword must no longer parse, got Ok");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_tag_mut_is_unknown_serde_variant() {
|
||||
let j = r#"{"t":"mut","vars":[],"body":{"t":"lit","lit":{"kind":"int","value":0}}}"#;
|
||||
let r: Result<ailang_core::ast::Term, _> = serde_json::from_str(j);
|
||||
assert!(r.is_err(), "{{\"t\":\"mut\"}} must be an unknown variant, got Ok");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_tag_assign_is_unknown_serde_variant() {
|
||||
let j = r#"{"t":"assign","name":"x","value":{"t":"lit","lit":{"kind":"int","value":1}}}"#;
|
||||
let r: Result<ailang_core::ast::Term, _> = serde_json::from_str(j);
|
||||
assert!(r.is_err(), "{{\"t\":\"assign\"}} must be an unknown variant, got Ok");
|
||||
}
|
||||
Reference in New Issue
Block a user