All 176 files in the four accumulating directories now use a zero-padded 4-digit counter prefix that reflects creation order (`NNNN-slug.md`). The counter is assigned per directory in strict git-log creation order; ties broken alphabetically by original name. The old `YYYY-MM-DD-` prefix on docs/specs/ and docs/plans/ files is dropped — the date is recoverable from git log and the counter carries the ordering. A file's counter is stable for the life of the file: never reassigned, never reused, never compacted. Deleted files retire their counter; subsequent files do not fill the gap. This is the property that lets cross-references stay literal — refs use the full filename including the counter (`design/contracts/0007-honesty-rule.md`) so they grep cleanly and resolve directly without a glob step. 313 cross-references updated across .md/.rs/.toml/.c/.json files (test pins, include_str! paths, design-INDEX entries, baseline notes, runtime C comments, inter-contract markdown links incl. bare basename and `../models/foo.md` forms). CLAUDE.md gets a new "File-naming convention" section spelling out the rule and rationale. skills/brainstorm/SKILL.md and skills/planner/SKILL.md updated so new spec/plan creation produces counter-prefixed names from the start. The full test suite (cargo test --workspace) passes.
10 KiB
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 = 385via aloopwith two binders (icounter,acctotal) and a tailrecur. - Fits the milestone's axis-1 (the canonical "imperative running
accumulator over a range" —
s = 0; for i: s += i*i). - Outcome:
ail checkclean first try (ok (22 symbols across 2 modules));ail buildclean; stdout385— 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-threadedgupdated by a cascade of fourif-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 checkclean first try;ail buildclean; stdout4/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+7as a straight chain of fivelet accshadowings.poly(2) = 73. - Fits axis-3 (the textbook straight-line "acc = ...; acc = acc*x + c; ..." build-up).
- Outcome:
ail checkclean first try;ail buildclean; stdout73— matches expected.
examples/fieldtest/remove-mut_4_bracket_scanner.ail — multi-state streaming state machine
- A bracket-balance scanner over a token stream (recursive
Intlist, 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 checkclean first try (ok (24 symbols across 2 modules));ail buildclean; stdouttrue/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 afor. The AILang shape is oneloopwith two binders + onerecur—(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 extraletthreading, no extrarecurargument beyond the two values the imperative loop also carries. Compiled and produced385on the first try. - Why working: the milestone predicts
loop/recurcovers the bounded-accumulator case with no expressivity loss. The example is the affirmative evidence — the removedmutwould 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
letbindings (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/0and73). - Why working: this is the milestone's clause-2 thesis made
executable from the author's chair — every
mutaccumulator/flag in these shapes is alet-shadow chain with no behavioural difference. The removal lost nothing; if anything theletform is safer (the grade cascade's final value is structurally the lastletbody, so the dead-write hazardmutpermits — see the spec'smut_nested_shadowexample — 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;okimplicitly untouched) orok = false(one mutation;depthimplicitly untouched). The surviving-surface form is a tail-recursivescanthreading(rest, depth, ok)where everytail-appmust restate all three arguments even in a branch that changes only one — e.g.(tail-app scan more (+ depth 1) ok)re-passes the unchangedokexplicitly. 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 producedtrue/falsecorrectly. - 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. Noteloop/recuris not usable here (it rebinds positionally with no structural pattern; an ADT traversal needsmatch, so the surviving form is ordinary tail recursion) — but that is a property of ADT traversal, not of themutremoval:mutnever 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 tomut; 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 ownmut_removed_pin.rsalready pins this and the §"What you DO NOT ship" rule forbids a rejection-only fixture with nolet/ifequivalent). - What happened: a doc-faithful-but-stale author who still emits
(mut (var s (con Int) 0) (assign s (app + s 1)) s)gets, atail check, exit code 1 and:error: [surface-parse-error] parse error in term: unknown term headmut; expected one ofapp,tail-app,lam,let,let-rec,if,match,do,tail-do,seq,term-ctor,clone,reuse-as,loop,recur,lit-unitat byte 137. Identical structured payload via--json(code: surface-parse-error,severity: error).ail buildalso exits- 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 headpath delivers exactly that — and as a bonus it enumerates the surviving valid term heads includingloop,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.