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 {
|
||||
|
||||
@@ -346,23 +346,6 @@ fn collect_used_in_term(t: &Term, used: &mut BTreeSet<String>) {
|
||||
collect_used_in_term(source, used);
|
||||
collect_used_in_term(body, used);
|
||||
}
|
||||
Term::Mut { vars, body } => {
|
||||
// Iter mut.1: mut-var declarations introduce source-level
|
||||
// names — track them so a generated fresh-name cannot
|
||||
// accidentally collide with one. Then recurse into each
|
||||
// var's `init` and the block's body.
|
||||
for v in vars {
|
||||
used.insert(v.name.clone());
|
||||
collect_used_in_term(&v.init, used);
|
||||
}
|
||||
collect_used_in_term(body, used);
|
||||
}
|
||||
Term::Assign { name, value } => {
|
||||
// Iter mut.1: the assigned `name` is a *use* of the
|
||||
// mut-var; record it and recurse into `value`.
|
||||
used.insert(name.clone());
|
||||
collect_used_in_term(value, used);
|
||||
}
|
||||
Term::Loop { binders, body } => {
|
||||
for b in binders {
|
||||
used.insert(b.name.clone());
|
||||
@@ -568,47 +551,12 @@ impl Desugarer {
|
||||
source: Box::new(self.desugar_term(source, scope)),
|
||||
body: Box::new(self.desugar_term(body, scope)),
|
||||
},
|
||||
Term::Mut { vars, body } => {
|
||||
// Iter mut.1: structural recursion. Each `var`'s `init`
|
||||
// is evaluated in scope of the outer environment plus
|
||||
// the *already-declared* vars of the same `Term::Mut`
|
||||
// (spec §"Iteration mut.2" — first-class scoping
|
||||
// formally lands at typecheck). For desugar's purposes
|
||||
// we extend the scope per-var with a `LetBound`
|
||||
// sentinel so a generated fresh name does not collide
|
||||
// with mut-var names; the LetRec lifter does not
|
||||
// consult mut-vars (mut-vars cannot appear in let-rec
|
||||
// capture sets — letrec lifting happens before
|
||||
// typecheck of the mut body).
|
||||
let mut inner = scope.clone();
|
||||
let new_vars: Vec<MutVar> = vars
|
||||
.iter()
|
||||
.map(|v| {
|
||||
let init = self.desugar_term(&v.init, &inner);
|
||||
inner.insert(v.name.clone(), ScopeEntry::LetBound);
|
||||
MutVar {
|
||||
name: v.name.clone(),
|
||||
ty: v.ty.clone(),
|
||||
init,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Term::Mut {
|
||||
vars: new_vars,
|
||||
body: Box::new(self.desugar_term(body, &inner)),
|
||||
}
|
||||
}
|
||||
Term::Assign { name, value } => Term::Assign {
|
||||
name: name.clone(),
|
||||
value: Box::new(self.desugar_term(value, scope)),
|
||||
},
|
||||
Term::Loop { binders, body } => {
|
||||
// loop-recur iter 1: structural recursion mirroring
|
||||
// the Term::Mut arm. Each binder's init is desugared
|
||||
// in scope of the outer env plus already-declared
|
||||
// binders; the body sees all binders. The LetBound
|
||||
// sentinel keeps generated fresh names off binder
|
||||
// names.
|
||||
// loop-recur iter 1: structural recursion. Each
|
||||
// binder's init is desugared in scope of the outer env
|
||||
// plus already-declared binders; the body sees all
|
||||
// binders. The LetBound sentinel keeps generated fresh
|
||||
// names off binder names.
|
||||
let mut inner = scope.clone();
|
||||
let new_binders: Vec<LoopBinder> = binders
|
||||
.iter()
|
||||
@@ -1254,33 +1202,10 @@ pub fn free_vars_in_term(t: &Term, bound: &BTreeSet<String>, out: &mut BTreeSet<
|
||||
free_vars_in_term(source, bound, out);
|
||||
free_vars_in_term(body, bound, out);
|
||||
}
|
||||
Term::Mut { vars, body } => {
|
||||
// Iter mut.1: a mut-var name binds inside the block. Each
|
||||
// `var`'s `init` is evaluated in scope of the OUTER
|
||||
// environment plus the already-declared vars (init order
|
||||
// matches declaration order); the body is in scope of all
|
||||
// vars. Mirrors the `Term::Let` pattern but generalised
|
||||
// across N vars.
|
||||
let mut b = bound.clone();
|
||||
for v in vars {
|
||||
free_vars_in_term(&v.init, &b, out);
|
||||
b.insert(v.name.clone());
|
||||
}
|
||||
free_vars_in_term(body, &b, out);
|
||||
}
|
||||
Term::Assign { name, value } => {
|
||||
// Iter mut.1: the assigned `name` is a free var unless
|
||||
// bound by an enclosing `Term::Mut`. The value is walked
|
||||
// as usual.
|
||||
if !bound.contains(name) {
|
||||
out.insert(name.clone());
|
||||
}
|
||||
free_vars_in_term(value, bound, out);
|
||||
}
|
||||
Term::Loop { binders, body } => {
|
||||
// loop-recur iter 1: binder names bind inside the loop —
|
||||
// each init sees the outer env plus already-declared
|
||||
// binders; the body sees all. Mirrors the Term::Mut arm.
|
||||
// binders; the body sees all.
|
||||
let mut b = bound.clone();
|
||||
for bd in binders {
|
||||
free_vars_in_term(&bd.init, &b, out);
|
||||
@@ -1437,60 +1362,10 @@ pub fn subst_var(t: &Term, from: &str, to: &str) -> Term {
|
||||
source: Box::new(subst_var(source, from, to)),
|
||||
body: Box::new(subst_var(body, from, to)),
|
||||
},
|
||||
Term::Mut { vars, body } => {
|
||||
// Iter mut.1: mut-vars lexically shadow outer names. Walk
|
||||
// each var's `init` substituting only if no earlier var in
|
||||
// the same block already shadows `from`; once a var named
|
||||
// `from` is declared, subsequent inits AND the body see
|
||||
// that var, so substitution must stop.
|
||||
let mut shadowed = false;
|
||||
let new_vars: Vec<MutVar> = vars
|
||||
.iter()
|
||||
.map(|v| {
|
||||
let init = if shadowed {
|
||||
v.init.clone()
|
||||
} else {
|
||||
subst_var(&v.init, from, to)
|
||||
};
|
||||
if v.name == from {
|
||||
shadowed = true;
|
||||
}
|
||||
MutVar {
|
||||
name: v.name.clone(),
|
||||
ty: v.ty.clone(),
|
||||
init,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let body_rw = if shadowed {
|
||||
(**body).clone()
|
||||
} else {
|
||||
subst_var(body, from, to)
|
||||
};
|
||||
Term::Mut {
|
||||
vars: new_vars,
|
||||
body: Box::new(body_rw),
|
||||
}
|
||||
}
|
||||
Term::Assign { name, value } => Term::Assign {
|
||||
// Iter mut.1: substitute inside `value`; the `name` field
|
||||
// refers to a mut-var declared by an enclosing
|
||||
// `Term::Mut`. Renaming it here without renaming the
|
||||
// declaration would break the scope link. The desugar
|
||||
// pass's `subst_var` is used only to rewrite let-rec
|
||||
// captures (KnownType / LetBound / MatchArm — never
|
||||
// mut-vars), so leaving the `name` untouched is
|
||||
// semantically correct: any `from` that matches the
|
||||
// assigned `name` is in a different namespace by
|
||||
// construction.
|
||||
name: name.clone(),
|
||||
value: Box::new(subst_var(value, from, to)),
|
||||
},
|
||||
Term::Loop { binders, body } => {
|
||||
// loop-recur iter 1: binders lexically shadow outer
|
||||
// names, symmetric to the Term::Mut arm — once a binder
|
||||
// named `from` is declared, later inits and the body
|
||||
// stop being substituted.
|
||||
// names — once a binder named `from` is declared, later
|
||||
// inits and the body stop being substituted.
|
||||
let mut shadowed = false;
|
||||
let new_binders: Vec<LoopBinder> = binders
|
||||
.iter()
|
||||
@@ -1644,26 +1519,6 @@ pub fn subst_call_with_extras(t: &Term, name: &str, lifted: &str, extras: &[Stri
|
||||
source: Box::new(subst_call_with_extras(source, name, lifted, extras)),
|
||||
body: Box::new(subst_call_with_extras(body, name, lifted, extras)),
|
||||
},
|
||||
Term::Mut { vars, body } => Term::Mut {
|
||||
// Iter mut.1: structural recursion through each var's
|
||||
// `init` and the block's body. Mut-var declarations
|
||||
// cannot shadow a top-level fn name (mut-vars are
|
||||
// first-order scalars, fns are not assignable), so the
|
||||
// call-substitution sees through the block uniformly.
|
||||
vars: vars
|
||||
.iter()
|
||||
.map(|v| MutVar {
|
||||
name: v.name.clone(),
|
||||
ty: v.ty.clone(),
|
||||
init: subst_call_with_extras(&v.init, name, lifted, extras),
|
||||
})
|
||||
.collect(),
|
||||
body: Box::new(subst_call_with_extras(body, name, lifted, extras)),
|
||||
},
|
||||
Term::Assign { name: assign_name, value } => Term::Assign {
|
||||
name: assign_name.clone(),
|
||||
value: Box::new(subst_call_with_extras(value, name, lifted, extras)),
|
||||
},
|
||||
Term::Loop { binders, body } => Term::Loop {
|
||||
binders: binders
|
||||
.iter()
|
||||
@@ -1735,24 +1590,6 @@ pub fn find_non_callee_use(t: &Term, name: &str) -> Option<Term> {
|
||||
Term::ReuseAs { source, body } => {
|
||||
find_non_callee_use(source, name).or_else(|| find_non_callee_use(body, name))
|
||||
}
|
||||
// Iter mut.1: scan each var's init plus the body. Mut-vars
|
||||
// are first-order scalars and never appear in callee position,
|
||||
// so any matching `Term::Var` inside a mut block is non-callee
|
||||
// by construction.
|
||||
Term::Mut { vars, body } => vars
|
||||
.iter()
|
||||
.find_map(|v| find_non_callee_use(&v.init, name))
|
||||
.or_else(|| find_non_callee_use(body, name)),
|
||||
// Iter mut.1: the assigned `name` is a non-callee *use* of
|
||||
// the mut-var; if it matches the searched name, surface the
|
||||
// node itself. Otherwise recurse into the value.
|
||||
Term::Assign { name: assign_name, value } => {
|
||||
if assign_name == name {
|
||||
Some(t.clone())
|
||||
} else {
|
||||
find_non_callee_use(value, name)
|
||||
}
|
||||
}
|
||||
Term::Loop { binders, body } => binders
|
||||
.iter()
|
||||
.find_map(|b| find_non_callee_use(&b.init, name))
|
||||
@@ -1802,11 +1639,6 @@ mod tests {
|
||||
}
|
||||
Term::Clone { value } => any_nested_ctor(value),
|
||||
Term::ReuseAs { source, body } => any_nested_ctor(source) || any_nested_ctor(body),
|
||||
// Iter mut.1: scan each var's init plus the body.
|
||||
Term::Mut { vars, body } => {
|
||||
vars.iter().any(|v| any_nested_ctor(&v.init)) || any_nested_ctor(body)
|
||||
}
|
||||
Term::Assign { value, .. } => any_nested_ctor(value),
|
||||
Term::Loop { binders, body } => {
|
||||
binders.iter().any(|b| any_nested_ctor(&b.init)) || any_nested_ctor(body)
|
||||
}
|
||||
@@ -1836,11 +1668,6 @@ mod tests {
|
||||
Term::LetRec { .. } => true,
|
||||
Term::Clone { value } => any_let_rec(value),
|
||||
Term::ReuseAs { source, body } => any_let_rec(source) || any_let_rec(body),
|
||||
// Iter mut.1: scan each var's init plus the body.
|
||||
Term::Mut { vars, body } => {
|
||||
vars.iter().any(|v| any_let_rec(&v.init)) || any_let_rec(body)
|
||||
}
|
||||
Term::Assign { value, .. } => any_let_rec(value),
|
||||
Term::Loop { binders, body } => {
|
||||
binders.iter().any(|b| any_let_rec(&b.init)) || any_let_rec(body)
|
||||
}
|
||||
@@ -2961,11 +2788,6 @@ mod tests {
|
||||
}
|
||||
Term::Clone { value } => any_lit_pattern(value),
|
||||
Term::ReuseAs { source, body } => any_lit_pattern(source) || any_lit_pattern(body),
|
||||
// Iter mut.1: scan each var's init plus the body.
|
||||
Term::Mut { vars, body } => {
|
||||
vars.iter().any(|v| any_lit_pattern(&v.init)) || any_lit_pattern(body)
|
||||
}
|
||||
Term::Assign { value, .. } => any_lit_pattern(value),
|
||||
Term::Loop { binders, body } => {
|
||||
binders.iter().any(|b| any_lit_pattern(&b.init)) || any_lit_pattern(body)
|
||||
}
|
||||
|
||||
@@ -1247,18 +1247,6 @@ where
|
||||
walk_term_embedded_types(source, f)?;
|
||||
walk_term_embedded_types(body, f)
|
||||
}
|
||||
// Iter mut.1: each `MutVar.ty` is an embedded type — walk
|
||||
// it. Then recurse into each var's `init` and the block's
|
||||
// body. `Term::Assign` carries no embedded type; recurse
|
||||
// into its `value` only.
|
||||
Term::Mut { vars, body } => {
|
||||
for v in vars {
|
||||
walk_type(&v.ty, f)?;
|
||||
walk_term_embedded_types(&v.init, f)?;
|
||||
}
|
||||
walk_term_embedded_types(body, f)
|
||||
}
|
||||
Term::Assign { value, .. } => walk_term_embedded_types(value, f),
|
||||
Term::Loop { binders, body } => {
|
||||
for b in binders {
|
||||
walk_type(&b.ty, f)?;
|
||||
@@ -1394,17 +1382,6 @@ where
|
||||
walk_term(source, f)?;
|
||||
walk_term(body, f)
|
||||
}
|
||||
// Iter mut.1: `Term::Ctor.type_name` is the only string this
|
||||
// walker reports — mut-vars carry no Ctor-type strings. Just
|
||||
// recurse into each var's `init` and the body, and into
|
||||
// assign's `value`.
|
||||
Term::Mut { vars, body } => {
|
||||
for v in vars {
|
||||
walk_term(&v.init, f)?;
|
||||
}
|
||||
walk_term(body, f)
|
||||
}
|
||||
Term::Assign { value, .. } => walk_term(value, f),
|
||||
Term::Loop { binders, body } => {
|
||||
for b in binders {
|
||||
walk_term(&b.init, f)?;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user