# 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.
