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:
2026-05-18 11:28:34 +02:00
parent 4837871ddf
commit 5bb721178f
8 changed files with 414 additions and 19 deletions
+8
View File
@@ -198,3 +198,11 @@ The milestone is closed and fully verified — built across three steps plus a f
The readable prose rendering of a loop no longer misleads. Until now, a loop's carried values were printed as plain assignment lines inside the loop body — which reads exactly like "reset these to their starting values on every pass", the opposite of what a loop actually does. The starting values now sit in parentheses on the loop keyword itself, the way function parameters do, and the body block contains only the body. This also makes the loop header line up one-to-one with the re-entry form that supplies the next round's values, so the relationship between the two is visible at a glance instead of obscured.
Nothing about how loops are written, type-checked, compiled, or run changed — this is purely the human-reading projection telling the truth about the code it shows. You raised this directly; it is a one-change milestone, drift-reviewed clean and benchmarked clean (the one flagged benchmark metric is the same pre-existing machine-drift item already on the list, not caused by this).
## 2026-05-18 — The mutable-local construct has been removed
AILang no longer has the block of mutable local variables (`mut`/`var`/`assign`). Everything that used it is now written with ordinary value bindings and loops — plain bindings and `if` for straight-line and conditional code, the loop construct for iteration. A program that still uses the old keyword is rejected at the first check with a clear error that lists the forms to use instead; there is no lingering half-support.
The removal was justified by examining every existing use: in all of them the mutable block was doing nothing a plain binding or a loop accumulator does not already do. It was then field-tested by writing four fresh real-world programs entirely from the documentation — a running sum of squares, a grade-classification cascade, a polynomial evaluation, and a multi-state bracket-balance scanner. All four worked correctly on the first try with no mutable locals. The one thing the old construct did differently — letting a value silently carry from one loop pass to the next without being restated — is exactly the class of hard-to-follow state bug the language is designed to make impossible, so removing it makes programs easier to reason about, not harder.
The milestone is closed and fully verified — built in one atomic change, drift-reviewed (with one documentation-only follow-up fix), benchmarked clean (the one flagged metric is the same pre-existing machine-drift item already on the list, proven unrelated here by byte-identical binaries before and after), and field-tested. The next milestone is a fresh decision — a documentation-honesty sweep so the specification shows only the language as it actually is, with no wishful "we would like" and no "this went wrong" — and that is the natural place to pick up when you are back.
+1
View File
@@ -90,3 +90,4 @@
- 2026-05-18 — audit prose-loop-binders (milestone CLOSE, clean / carry-on): milestone-close tidy for the single-iteration prose-loop-binders milestone (range `6cbd0fe..c9355d7`). Architect drift review **clean** — zero drift, zero debt, verified against the diff not journal trust: single `Term::Loop` `write_term` hunk in `crates/ailang-prose/src/lib.rs`, the two non-render `Term::Loop` sites + all `Term::Recur` arms absent from the diff, DESIGN.md/PROSE_ROUNDTRIP.md make no claim about prose `loop` rendering (so the spec's "no doc edit needed" is correct not a skipped obligation), no carve-out/hash-pin/round-trip lockstep bypassed (prose is off the Form-A↔JSON round-trip path by construction). Bench: `compile_check.py` 0/24 + `cross_lang.py` 0/25 both **exit 0** (cross_lang's independent `ail_bump_s` +1.50% ±15% ok corroborates check.py's bump_s firing is baseline-staleness not a real regression); `check.py` exit 1, first run 3 regressed but a confirmatory identical re-run (no code change) returned `latency.implicit_at_rc.p99_9_us` +37.30%→+10.22% **ok** and `.max_us` +114.14%→+22.17% **ok** (median/p99 within tol both runs) — the two latency-tail firings proven single-sample 5-run RC-mode jitter by re-run; `bench_list_sum.bump_s` persisted ~+12% = the already-tracked P2 roadmap todo (byte-oracle-proven environmental at the 2026-05-16 iteration-discipline-revert audit, causally impossible to attribute to a projection-only `ailang-prose` change). **All items carry-on**: no fix iteration, no baseline ratify — explicitly NOT bumping the `*.bump_s` baseline opportunistically inside an unrelated projection-only milestone (the P2 todo owns a holistic recapture; piecemeal bump is the "regression is expected" rationalisation the Iron Law forbids). Fieldtest **not applicable / not dispatched**: projection-only change to the Form-B *reading* surface; the fieldtester authors `.ail` Form-A and would not exercise the prose-render change (no friction/bug/spec-gap axis); the two whole-file byte-equality `.prose.txt` snapshots + live-CLI `ail prose|diff` checks are the projection-only E2E gate. Milestone **CLOSED clean**; roadmap P0 entry flipped `[~]``[x]`, WhatsNew.md user-facing entry appended (user present → no notify.sh fired per notification policy). → 2026-05-18-audit-prose-loop-binders.md
- 2026-05-18 — iter remove-mut-var-assign.1 (single terminal iteration, DONE): `mut`/`var`/`assign` removed from AILang entirely and atomically — `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 + drift trio + carve-out + roadmap so the schema is honest at every commit (no `_ =>` wildcard introduced — Boss-verified). `loop`/`recur` + `let`/`if` are the surviving forms. Shared codegen alloca machinery survives (loop reuses it): `mut_var_allocas``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`) kept byte-equivalent. Feature-acceptance applied inverted (removed feature fails clause 2 redundant + clause 3 IS the iterated-mutable-state bug class). Behaviour-preservation proof executable: `mut_counter`/`mut_sum_floats` still print `55` after the faithful `let`/`if` rewrite (factorial→120, horner→18, classify 22→2, has_small_factor 91→true); the "removal made executable" gate is the new `mut_removed_pin.rs` (4 must-fail pins: `mut`/`assign` keyword rejected, `{"t":"mut"}`/`{"t":"assign"}` serde unknown-variant). Independent Boss verification: full `cargo test --workspace` **605/0**, zero residual mut symbols in any crate source, loop/recur non-regression all green (55 / 500000500000 / infinite-compiles / `lambda_capturing_loop_binder` pin), `roundtrip_cli` PASS. One systemic DONE_WITH_CONCERNS — 4th recurrence of the recon-undercount `feedback_plan_pseudo_vs_reality` class (in-source `mod tests` fns + a drift-pin fn + 5 orphaned mut `.ail.json` carve-outs + a non-enumerated `codegen_import_map_fallback_pin.rs` E0599 the recon missed; all resolved within implementer remit via Task-3's identical whole-file-deletion rationale, no behaviour change, e2e bodies preserved per plan) — worth a planner/recon-agent countermeasure, pairs with the existing loop-recur.tidy P2 todo. Milestone-close `audit` then `fieldtest` (surface-touching) are the Boss's next pipeline steps. spec `docs/specs/2026-05-18-remove-mut-var-assign.md` (grounding-check PASS), plan `docs/plans/remove-mut-var-assign.1.md` → 2026-05-18-iter-remove-mut-var-assign.1.md
- 2026-05-18 — audit remove-mut-var-assign (milestone close, CLEAN after one inline `.tidy`): scope `48e7774..07f0802`. Architect drift: one `[high]``crates/ailang-core/specs/form_a.md` (live in-tree Form-A grammar still documented the full `mut`/`var`/`assign` construct: EBNF 286-288, prose 308-328, dangling `like mut` at 332); the spec named it for deletion, the iter missed it. Root cause = recon-undercount class **4th recurrence + a Boss error**: a plan-time grep checked only `docs/form_a.md` (dir-scoped), got "No such file", and the Boss propagated that unverified non-existence claim into the recon brief — the real file is `crates/ailang-core/specs/form_a.md`, actively maintained, and a stale doc never produces a compile error so the implement compile-sweep could not catch it. Fixed inline as a Boss `.tidy` (CLAUDE.md context-already-loaded carve-out; full review discipline kept — tree-wide verified zero construct residue except the roadmap entry that names the milestone itself; grammar reads coherently reuse-as→loop→recur). One `[medium]` process-drift: escalated the existing loop-recur.tidy `ailang-plan-recon` P2 in place — broadened from cross-crate-caller to a structural recon-contract defect with two named gaps (non-compile-checked spec-named sites; unverified "X does not exist" claims inheriting into a recon brief) + a concrete fix (compile-driven enumeration as authoritative code-site set PLUS an `ls`-verified spec-named-path existence table). Bench: `compile_check.py` 0/24 + `cross_lang.py` 0/25 exit 0; `check.py` exit 1 (3 firings: `bench_list_sum.bump_s` +12.51%, `latency.{explicit,implicit}_at_rc.max_us` +108%/+66%). Bencher causal exoneration **decisive**: HEAD vs `48e7774` `list_sum_bump`/`lat_expl_rc`/`lat_impl_rc` binaries `cmp`-byte-identical — the 2520-line removal alters zero bytes of these fixtures' machine code, no causal mechanism; `bump_s` = the tracked P2 environmental staleness, both `*.max_us` = single-sample `-n 5` jitter (`explicit_at_rc.max_us` +108%→**-2.30% ok** on identical-code rerun, medians held). **PRISTINE met on the merits; NO baseline ratify** (Iron Law forbids ratifying noise, spec forecloses it). Filed a NEW distinct P2 (separate from `*.bump_s` staleness): `check.py` RC-latency `*.max_us` at `-n 5` is a 3-for-3 structural false-positive on no-runtime-change milestones → drop from gating / raise `-n` / cpuset-pin. Milestone audit **CLEAN**; only `fieldtest` (surface-touching) remains before close → 2026-05-18-audit-remove-mut-var-assign.md
- 2026-05-18 — fieldtest remove-mut-var-assign (milestone CLOSE, clean — removal thesis empirically confirmed): post-audit downstream-LLM-author field test of the post-removal Form-A surface. The fieldtester (DESIGN.md + form_a.md + public examples only, never compiler source) wrote 4 FRESH real-world programs an imperative-trained author would instinctively reach for a mutable local for — running sum-of-squares accumulator (`loop`/`recur`, 2 binders → `385`), grade-classification cascade (let-threaded flag through 4 `if``4`/`2`/`0`), Horner polynomial straight-line build-up (5 `let acc` shadows → `73`), multi-state bracket-balance scanner threading `(rest,depth,ok)` by tail recursion → `true`/`false`) plus an out-of-tree `mut`-keyword rejection probe. **0 bugs, 0 friction, 0 spec_gap, 4 working**: every task expressed cleanly and correctly on the FIRST try with only `let`/`if`/`loop`/`recur`; all 4 `ail parse|render|parse` byte-identical. The closest-to-friction case (the multi-state machine must restate the full state tuple at every tail-call even when a branch changes one component) was carefully classified `working` not `spec_gap` — that explicit-dataflow cost IS the local-reasoning pillar working as designed, and `mut`'s implicit cross-iteration persistence of un-restated state is precisely the clause-3 failure the removal eliminates; the fieldtester explicitly noted the only conceivable sugar (a record to bundle state) is an orthogonal additive future question, NOT evidence against the removal (refused the scope-creep). The `mut` rejection is fail-closed with an actively-guiding diagnostic that enumerates the surviving term heads incl. `loop`/`recur`/`let` (no tombstone, no-nostalgia, as spec'd). Independent Boss verification: all 4 fixtures re-run → `385`/`4 2 0`/`73`/`true false`, zero `mut`/`var`/`assign` construct tokens in any fixture (a doc-faithful author did not produce the removed construct — the affirmative clause-1 evidence). **Verdict: the removal thesis is confirmed**`mut` was redundant with `let`/`if`/`loop`/`recur` in 100% of the fresh real tasks; no expressivity lost. **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 on the removal axis. Fixtures `examples/fieldtest/remove-mut_*.ail` + spec `docs/specs/2026-05-18-fieldtest-remove-mut-var-assign.md`; roadmap P0 flipped `[~]``[x]`, WhatsNew.md user-facing entry appended; next queue item (DESIGN.md/docs honesty-lint) is a NEW milestone = a /boss new-milestone bounce-back (not auto-started) → 2026-05-18-fieldtest-remove-mut-var-assign.md
+21 -19
View File
@@ -35,26 +35,28 @@ work progresses.
## P0 — In flight
- [~] **\[milestone\]** Remove `mut` / `var` / `assign`atomic
hard-removal of the local-mutable-state construct (`Term::Mut` +
`Term::Assign` + the `MutVar` surface decl; one indivisible
feature). `loop`/`recur` (shipped, closed) + `let`/`if` cover
every shipped use; the construct fails the feature-acceptance
criterion applied inverted (clause 2 redundant, clause 3 IS the
iterated-mutable-state bug class). Approach A: no deprecation
window, JSON tags fail closed as generic unknown-variant, codegen
alloca machinery kept (loop reuses it) + renamed `binder_allocas`,
shared `Term::Lam` escape guard keeps its `loop` half. Explicitly
decoupled from Stateful-islands (separate, deferred). Spec
approved + planned + implemented (605/0, loop/recur non-regression
green, removal made executable); milestone-close `audit` clean —
architect `[high]` (a spec-named in-tree grammar doc the iter
missed) fixed in `.tidy`, bench causally exonerated (HEAD vs
pre-milestone binaries `cmp`-byte-identical; the 3 `check.py`
firings are the tracked P2 `bump_s` staleness + the new P2
`max_us` `-n 5` false-positive — no ratify). Only `fieldtest`
(surface-touching) remains before close.
- [x] **\[milestone\]** Remove `mut` / `var` / `assign`CLOSED
2026-05-18. The local-mutable-state construct removed entirely
and atomically (`Term::Mut`/`Term::Assign`/`MutVar`, the 3 Form-A
keywords, the 4 `mut` `CheckError` variants, `mut_scope_stack`,
every exhaustive `Term` arm across 17 src files) in lockstep with
DESIGN.md + fixtures + drift trio + carve-out + roadmap; `loop`/
`recur` + `let`/`if` are the surviving forms. Justified by the
feature-acceptance criterion applied inverted (clause 2 redundant,
clause 3 IS the iterated-mutable-state bug class). Approach A: no
deprecation window, JSON tags fail closed; shared codegen alloca
machinery kept (loop reuses it, renamed `binder_allocas`) + the
`Term::Lam` escape-guard loop half byte-equivalent. spec/plan/
iter (`d1ad50e`/`f355899`/`07f0802`), milestone-close `audit`
clean (architect `[high]` form_a.md fixed in `.tidy` `4837871`;
bench causally exonerated HEAD vs pre-milestone bench binaries
`cmp`-byte-identical, no ratify), `fieldtest` clean (4 fresh
programs, all worked first try without mutable locals the
removal thesis empirically confirmed). Decoupled from
Stateful-islands. Stays here briefly for context; remove once
stale (full record in `docs/journals/INDEX.md`).
- context: `docs/specs/2026-05-18-remove-mut-var-assign.md`;
`docs/specs/2026-05-18-fieldtest-remove-mut-var-assign.md`;
`docs/journals/2026-05-18-iter-remove-mut-var-assign.1.md`;
`docs/journals/2026-05-18-audit-remove-mut-var-assign.md`
@@ -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 99108 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.