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:
+10
-119
@@ -513,33 +513,6 @@ pub enum Term {
|
||||
source: Box<Term>,
|
||||
body: Box<Term>,
|
||||
},
|
||||
/// Iter mut.1: local mutable-state block. `vars` declares zero or
|
||||
/// more lexically-scoped mutable bindings (initialised in order);
|
||||
/// `body` is a single Term evaluated in scope of all vars. The
|
||||
/// block's static type is `body`'s static type. `vars` stays
|
||||
/// present in canonical JSON even when empty (no
|
||||
/// `skip_serializing_if` — the field is part of the shape).
|
||||
///
|
||||
/// Typecheck and codegen recognition land in iters mut.2 / mut.3;
|
||||
/// in iter mut.1 the dispatch entry points (`synth` in
|
||||
/// `ailang-check`, `lower_term` in `ailang-codegen`) stub these
|
||||
/// variants with `CheckError::Internal` / `CodegenError::Internal`
|
||||
/// respectively per spec `docs/specs/2026-05-15-mut-local.md`
|
||||
/// §"Iteration mut.1".
|
||||
Mut {
|
||||
vars: Vec<MutVar>,
|
||||
body: Box<Term>,
|
||||
},
|
||||
/// Iter mut.1: in-block update of a mut-var. Only legal as a
|
||||
/// sub-term of a `Term::Mut` whose `vars` includes a var with the
|
||||
/// same `name`. Static type is Unit. The lexical-scope invariant
|
||||
/// is enforced at typecheck (iter mut.2,
|
||||
/// `CheckError::MutAssignOutOfScope`); codegen relies on it (iter
|
||||
/// mut.3).
|
||||
Assign {
|
||||
name: String,
|
||||
value: Box<Term>,
|
||||
},
|
||||
/// loop-recur iter 1: a strict iteration block. `binders`
|
||||
/// declares one or more loop parameters (name, type, init),
|
||||
/// evaluated in order on loop entry; `body` is evaluated with
|
||||
@@ -547,8 +520,8 @@ pub enum Term {
|
||||
/// the iteration that exits via a non-`recur` branch. Strictly
|
||||
/// additive: pre-existing fixtures hash bit-identically because
|
||||
/// none carry the `"t":"loop"` tag. `binders` has no
|
||||
/// `skip_serializing_if` (mirrors `Term::Mut.vars` — the field
|
||||
/// is part of the shape). Typecheck (loop-recur.2): `synth`
|
||||
/// `skip_serializing_if` — the field is part of the shape.
|
||||
/// Typecheck (loop-recur.2): `synth`
|
||||
/// types each binder init, the loop's type is the body's type;
|
||||
/// `recur` arity/type is checked positionally via `loop_stack`
|
||||
/// and `verify_loop_body` enforces `recur`-in-tail-position; a
|
||||
@@ -587,42 +560,12 @@ pub struct Arm {
|
||||
pub body: Term,
|
||||
}
|
||||
|
||||
/// Iter mut.1: a mutable binding inside [`Term::Mut`]. Lives as a
|
||||
/// nested field of `Term::Mut`, not as a standalone `Term` variant —
|
||||
/// mut-vars are not first-class values (they cannot escape the
|
||||
/// enclosing `Term::Mut` scope; see
|
||||
/// `docs/specs/2026-05-15-mut-local.md` §"Why three AST nodes, not
|
||||
/// one fused construct").
|
||||
///
|
||||
/// The JSON field for `ty` is `"type"`, mirroring the spec's schema
|
||||
/// (`{ "name": "<id>", "type": Type, "init": Term }`). Derives match
|
||||
/// [`Arm`]'s for cross-tree consistency.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MutVar {
|
||||
/// The mut-var's lexical name. Within the enclosing `Term::Mut`,
|
||||
/// a `Term::Var { name }` resolves to this binding (innermost-wins
|
||||
/// shadowing per spec §"Term::Var resolution"); a
|
||||
/// `Term::Assign { name, .. }` updates it.
|
||||
pub name: String,
|
||||
/// The mut-var's declared type. Restricted to
|
||||
/// `{ Int, Float, Bool, Unit }` in this milestone — the typecheck
|
||||
/// pass (iter mut.2) rejects other choices with
|
||||
/// `CheckError::UnsupportedMutVarType`.
|
||||
#[serde(rename = "type")]
|
||||
pub ty: Type,
|
||||
/// Initial value. Evaluated in the outer scope plus the
|
||||
/// already-declared vars of the same `Term::Mut` (vars are
|
||||
/// initialised in declaration order).
|
||||
pub init: Term,
|
||||
}
|
||||
|
||||
/// loop-recur iter 1: one binder of a [`Term::Loop`]. Mirrors
|
||||
/// [`MutVar`]'s `(name, type, init)` triple exactly so the Form-A
|
||||
/// surface vocabulary is shared (`(NAME TYPE INIT)`); it is a
|
||||
/// nested field of `Term::Loop`, not a first-class `Term` (loop
|
||||
/// binders cannot escape the enclosing loop). `recur` rebinds these
|
||||
/// positionally per iteration. The `ty` JSON field is `"type"`,
|
||||
/// matching `MutVar` and the spec schema.
|
||||
/// loop-recur iter 1: one binder of a [`Term::Loop`]. The
|
||||
/// `(name, type, init)` triple is the Form-A surface vocabulary
|
||||
/// `(NAME TYPE INIT)`; it is a nested field of `Term::Loop`, not a
|
||||
/// first-class `Term` (loop binders cannot escape the enclosing
|
||||
/// loop). `recur` rebinds these positionally per iteration. The
|
||||
/// `ty` JSON field is `"type"`, matching the spec schema.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LoopBinder {
|
||||
/// The binder's lexical name. Within the enclosing `Term::Loop`,
|
||||
@@ -952,61 +895,9 @@ fn is_false(b: &bool) -> bool {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Iter mut.1: pin the canonical-bytes shape of an empty-`vars`
|
||||
/// `Term::Mut`. Spec `docs/specs/2026-05-15-mut-local.md`
|
||||
/// §"Canonical-form invariants" requires the `vars` array to stay
|
||||
/// present in canonical JSON rather than being omitted; a future
|
||||
/// addition of `skip_serializing_if = "Vec::is_empty"` to the
|
||||
/// field would silently break this property and is the failure
|
||||
/// mode this pin protects against.
|
||||
#[test]
|
||||
fn term_mut_empty_vars_serialises_with_explicit_vars_field() {
|
||||
let t = Term::Mut {
|
||||
vars: Vec::new(),
|
||||
body: Box::new(Term::Lit {
|
||||
lit: Literal::Int { value: 0 },
|
||||
}),
|
||||
};
|
||||
let bytes = serde_json::to_string(&t).expect("serialise");
|
||||
assert_eq!(
|
||||
bytes,
|
||||
r#"{"t":"mut","vars":[],"body":{"t":"lit","lit":{"kind":"int","value":0}}}"#,
|
||||
);
|
||||
}
|
||||
|
||||
/// Iter mut.1: round-trip a `Term::Assign` through JSON.
|
||||
/// Pins the canonical-form shape `{ "t": "assign", "name": ..., "value": ... }`.
|
||||
#[test]
|
||||
fn term_assign_round_trips_through_json() {
|
||||
let t = Term::Assign {
|
||||
name: "x".into(),
|
||||
value: Box::new(Term::Lit {
|
||||
lit: Literal::Int { value: 42 },
|
||||
}),
|
||||
};
|
||||
let bytes = serde_json::to_string(&t).expect("serialise");
|
||||
assert_eq!(
|
||||
bytes,
|
||||
r#"{"t":"assign","name":"x","value":{"t":"lit","lit":{"kind":"int","value":42}}}"#,
|
||||
);
|
||||
let back: Term = serde_json::from_str(&bytes).expect("deserialise");
|
||||
match back {
|
||||
Term::Assign { name, value } => {
|
||||
assert_eq!(name, "x");
|
||||
match *value {
|
||||
Term::Lit {
|
||||
lit: Literal::Int { value: 42 },
|
||||
} => {}
|
||||
other => panic!("inner literal mismatch: {other:?}"),
|
||||
}
|
||||
}
|
||||
other => panic!("variant mismatch: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// loop-recur iter 1: pin the canonical-bytes shape of a
|
||||
/// `Term::Loop` with one binder. Mirrors the mut-empty-vars pin:
|
||||
/// `binders` stays present (no `skip_serializing_if`).
|
||||
/// `Term::Loop` with one binder: `binders` stays present in
|
||||
/// canonical JSON (no `skip_serializing_if`).
|
||||
#[test]
|
||||
fn term_loop_one_binder_serialises_with_explicit_binders_field() {
|
||||
let t = Term::Loop {
|
||||
|
||||
Reference in New Issue
Block a user