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
+10 -119
View File
@@ -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 {
+8 -186
View File
@@ -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)
}
-23
View File
@@ -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 {
-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",
};