832375f2ac
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.
200 lines
10 KiB
Markdown
200 lines
10 KiB
Markdown
# 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.
|