# Fieldtest — remove-mut-var-assign — 2026-05-18 **Status:** Draft — awaiting orchestrator triage **Author:** ailang-fieldtester (dispatched by skills/fieldtest) ## Scope The `mut` / `var` / `assign` local-mutable-state construct was removed from AILang entirely (milestone `remove-mut-var-assign`, single terminal iteration + one inline audit `.tidy`). The three Form-A keywords no longer lex; `Term::Mut` / `Term::Assign` / `struct MutVar` are gone from the AST; a JSON-AST carrying `{"t":"mut"}` / `{"t":"assign"}` fails closed as a serde unknown-variant. The surviving forms for everything that previously used a mutable local are `let`, `if`, `loop`, `recur`. There is no tombstone diagnostic by design (no-nostalgia). This field test empirically probes the removal thesis — that `mut` was redundant — by writing the four real-world tasks an imperative-trained author would *instinctively* reach for a mutable accumulator / flag / build-up / multi-state loop for, in the surface as it exists now, and quantifying whether the absence of `mut` forced a materially more awkward shape. ## Examples ### `examples/fieldtest/remove-mut_1_sum_of_squares.ail` — running numeric accumulator over a bounded range - Computes `sum_sq(10) = Σ i² for i in 1..=10 = 385` via a `loop` with two binders (`i` counter, `acc` total) and a tail `recur`. - Fits the milestone's axis-1 (the canonical "imperative running accumulator over a range" — `s = 0; for i: s += i*i`). - Outcome: `ail check` clean first try (`ok (22 symbols across 2 modules)`); `ail build` clean; stdout `385` — matches expected. ### `examples/fieldtest/remove-mut_2_grade_cascade.ail` — conditional classification cascade - `grade(score)` returns a letter-grade code (4=A … 0=F) via a let-threaded `g` updated by a cascade of four `if`-expressions. `grade(95)=4`, `grade(72)=2`, `grade(50)=0`. - Fits axis-2 (the "set a code variable through a cascade of if-branches" shape an imperative author writes with one mutable flag). - Outcome: `ail check` clean first try; `ail build` clean; stdout `4`/`2`/`0` — matches expected. ### `examples/fieldtest/remove-mut_3_horner_poly.ail` — multi-step straight-line numeric build-up - Horner-form evaluation of `2x⁴+3x³+0x²+5x+7` as a straight chain of five `let acc` shadowings. `poly(2) = 73`. - Fits axis-3 (the textbook straight-line "acc = ...; acc = acc*x + c; ..." build-up). - Outcome: `ail check` clean first try; `ail build` clean; stdout `73` — matches expected. ### `examples/fieldtest/remove-mut_4_bracket_scanner.ail` — multi-state streaming state machine - A bracket-balance scanner over a token stream (recursive `Int` list, 1=`(`, 2=`)`, 0=other) threading `(rest, depth, ok)` via tail recursion; each match arm updates a *subset* of the state. `balanced "(()())" = true`, `balanced ")(" = false`. - Fits axis-4 (the streaming-feel loop that threads more than one piece of state and updates a subset per branch — the strongest stress on the removal: an imperative author mutates one variable and leaves the others implicitly alone). - Outcome: `ail check` clean first try (`ok (24 symbols across 2 modules)`); `ail build` clean; stdout `true`/`false` — matches expected. All four `.ail` fixtures are additionally `ail parse | render | parse` byte-identical (Roundtrip Invariant holds for the surviving surface an author writes post-removal). ## Findings ### [working] Bounded-range accumulator: `loop`/`recur` is structurally equal to the imperative form, zero extra threading - Surfaced in: `remove-mut_1_sum_of_squares.ail`. - What happened: the imperative shape is two mutated locals (`s`, `i`) inside a `for`. The AILang shape is one `loop` with two binders + one `recur` — `(loop (i Int 1) (acc Int 0) (if (> i n) acc (recur (+ i 1) (+ acc (* i i)))))`. Binder count, update count, and exit-condition are 1:1 with the imperative loop. No extra `let` threading, no extra `recur` argument beyond the two values the imperative loop also carries. Compiled and produced `385` on the first try. - Why working: the milestone predicts `loop`/`recur` covers the bounded-accumulator case with no expressivity loss. The example is the affirmative evidence — the removed `mut` would have produced an *identical* binder/update count; the construct added nothing here. The diagnostic surface was never exercised because nothing was wrong (clean first try). - Recommended downstream action: carry-on. ### [working] Conditional cascade and straight-line build-up: `let`/`if` is the faithful, clean form - Surfaced in: `remove-mut_2_grade_cascade.ail`, `remove-mut_3_horner_poly.ail`. - What happened: both express what an imperative author writes as "one mutable variable, reassigned through a cascade" as a chain of shadowing `let` bindings (`let g (if … g) …` ×4 / `let acc (+ (* acc x) c) …` ×4), each step a pure expression, the final value unambiguous. Both checked clean first try, built, and produced exactly the expected output (`4`/`2`/`0` and `73`). - Why working: this is the milestone's clause-2 thesis made executable from the author's chair — every `mut` accumulator/flag in these shapes *is* a `let`-shadow chain with no behavioural difference. The removal lost nothing; if anything the `let` form is *safer* (the grade cascade's final value is structurally the last `let` body, so the dead-write hazard `mut` permits — see the spec's `mut_nested_shadow` example — cannot arise here). - Recommended downstream action: carry-on. ### [working] Multi-state streaming machine: every branch must restate the full state tuple — and that is the local-reasoning pillar working as intended - Surfaced in: `remove-mut_4_bracket_scanner.ail`. - What happened: the imperative author writes `depth = depth + 1` (one mutation; `ok` implicitly untouched) or `ok = false` (one mutation; `depth` implicitly untouched). The surviving-surface form is a tail-recursive `scan` threading `(rest, depth, ok)` where every `tail-app` must restate *all three* arguments even in a branch that changes only one — e.g. `(tail-app scan more (+ depth 1) ok)` re-passes the unchanged `ok` explicitly. Quantified: 5 tail-calls, each restating the ≥1 unchanged component (the "implicit leave-alone" of mutable state becomes an explicit pass-through). It checked clean on the *first* try and produced `true`/`false` correctly. - Why working (and explicitly *not* a spec_gap): this is the closest any of the four tasks comes to friction — and it is the decisive negative-control for the removal thesis, so it is worth the careful classification. The "restate all state at each step" cost is not awkwardness imposed by the removal; it is precisely the explicit-dataflow property `docs/DESIGN.md`'s local-reasoning pillar exists to enforce. `mut`'s "update one, the rest implicitly persist across the loop back-edge" *is* the iterated-mutable-state reasoning lines 99–108 name as the canonical clause-3 failure: the reader must mentally carry the unwritten state across iterations. The tail-recursive form makes that state-flow syntactically present and locally checkable — exactly the trade the language is designed to make. Note `loop`/`recur` is *not* usable here (it rebinds positionally with no structural pattern; an ADT traversal needs `match`, so the surviving form is ordinary tail recursion) — but that is a property of ADT traversal, not of the `mut` removal: `mut` never offered ADT pattern-matching either. No expressivity was lost; verbosity rose by the count of unchanged-but-restated arguments, which is the intended cost of explicit dataflow. - Recommended downstream action: carry-on. (If the orchestrator wants a forward note: the only conceivable ergonomic sugar here — a record/struct to bundle `(depth, ok)` so a branch updates one field — is an *additive* future question entirely orthogonal to `mut`; it must not be read as evidence against this removal.) ### [working] `mut` keyword rejection is fail-closed with an actively-guiding diagnostic - Surfaced in: an out-of-tree probe (`/tmp/mut_probe.ail`, deleted after the run — not left as a fixture; the milestone's own `mut_removed_pin.rs` already pins this and the §"What you DO NOT ship" rule forbids a rejection-only fixture with no `let`/`if` equivalent). - What happened: a doc-faithful-but-stale author who still emits `(mut (var s (con Int) 0) (assign s (app + s 1)) s)` gets, at `ail check`, exit code 1 and: `error: [surface-parse-error] parse error in term: unknown term head `mut`; expected one of `app`, `tail-app`, `lam`, `let`, `let-rec`, `if`, `match`, `do`, `tail-do`, `seq`, `term-ctor`, `clone`, `reuse-as`, `loop`, `recur`, `lit-unit` at byte 137`. Identical structured payload via `--json` (`code: surface-parse-error`, `severity: error`). `ail build` also exits 1. No panic, no tombstone, no degraded internal error. - Why working: the removal spec mandates fail-closed with *no* dedicated "construct removed" diagnostic (no-nostalgia). The generic `unknown term head` path delivers exactly that — and as a bonus it enumerates the surviving valid term heads *including* `loop`, `recur`, `let`, so the very diagnostic an out-of-date author hits points them at the replacement forms. This is the clause-3 discriminator ("the wrong code fails to typecheck, not merely advised against") working precisely as the spec's "Removal made executable" criterion requires. - Recommended downstream action: carry-on. ## Recommendation summary | Finding | Class | Action | |---|---|---| | Bounded-range accumulator = `loop`/`recur`, zero extra threading | working | carry-on | | Cascade + straight-line build-up = clean `let`/`if` | working | carry-on | | Multi-state machine restates full tuple = local-reasoning pillar working | working | carry-on | | `mut` keyword rejection fail-closed + guiding diagnostic | working | carry-on | **Verdict on the removal thesis:** *Confirmed.* Across all four imperative-instinct tasks the surviving `let`/`if`/`loop`/`recur` surface expressed the program cleanly and correctly on the first try; the removed `mut` would have added no expressivity (equal binder/update counts in the loop cases, a redundant shadow chain in the straight-line cases) and its only behavioural difference — the implicit cross-iteration persistence of un-restated state — is the exact clause-3 failure mode the removal exists to eliminate. No `spec_gap`, no `friction`, no `bug` surfaced. The empirical signal is fully consistent with the milestone's inverted feature-acceptance argument.