fieldtest: remove-mut-var-assign — 4 examples, all working; milestone CLOSED
Post-audit downstream-LLM-author field test of the post-removal Form-A surface. 4 fresh real-world programs written from DESIGN.md + form_a.md + public examples only (no compiler source): running sum-of-squares accumulator (loop/recur → 385), grade cascade (let-threaded if → 4/2/0), Horner polynomial straight-line build-up (let-shadow chain → 73), multi-state bracket scanner threading (rest,depth,ok) by tail recursion → true/false. 0 bugs, 0 friction, 0 spec_gap, 4 working — every task clean and correct on the first try with only let/if/loop/recur; all parse|render|parse byte-identical; zero mut/var/assign tokens produced (a doc-faithful author did not reach for the removed construct). The closest-to-friction case (multi-state machine restates the full state tuple per tail-call) is correctly classified working, not a spec_gap: that explicit-dataflow cost is the local-reasoning pillar working as designed; mut's implicit cross-iteration persistence is exactly the failure class the removal eliminates. mut-keyword rejection is fail-closed with a diagnostic that enumerates the surviving forms (no tombstone, no-nostalgia, as spec'd). Removal thesis empirically CONFIRMED — mut was redundant with let/if/loop/recur in 100% of the fresh tasks. The remove-mut-var-assign milestone is fully ratified and CLOSED: spec+plan+iter, audit clean (architect [high] form_a.md fixed in .tidy, bench causally exonerated), fieldtest clean. Roadmap P0 flipped [~]→[x]; WhatsNew user-facing entry appended.
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user