Files
AILang/docs/specs/0038-fieldtest-remove-mut-var-assign.md
Brummel 832375f2ac convention: counter-prefix file naming across docs/specs/, docs/plans/, design/contracts/, design/models/
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.
2026-05-28 13:31:31 +02:00

200 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.