From 5bb721178f522a65a5b969f738ff5a3543c1d372 Mon Sep 17 00:00:00 2001 From: Brummel Date: Mon, 18 May 2026 11:28:34 +0200 Subject: [PATCH] =?UTF-8?q?fieldtest:=20remove-mut-var-assign=20=E2=80=94?= =?UTF-8?q?=204=20examples,=20all=20working;=20milestone=20CLOSED?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/WhatsNew.md | 8 + docs/journals/INDEX.md | 1 + docs/roadmap.md | 40 ++-- ...6-05-18-fieldtest-remove-mut-var-assign.md | 199 ++++++++++++++++++ .../fieldtest/remove-mut_1_sum_of_squares.ail | 29 +++ .../fieldtest/remove-mut_2_grade_cascade.ail | 38 ++++ .../fieldtest/remove-mut_3_horner_poly.ail | 35 +++ .../remove-mut_4_bracket_scanner.ail | 83 ++++++++ 8 files changed, 414 insertions(+), 19 deletions(-) create mode 100644 docs/specs/2026-05-18-fieldtest-remove-mut-var-assign.md create mode 100644 examples/fieldtest/remove-mut_1_sum_of_squares.ail create mode 100644 examples/fieldtest/remove-mut_2_grade_cascade.ail create mode 100644 examples/fieldtest/remove-mut_3_horner_poly.ail create mode 100644 examples/fieldtest/remove-mut_4_bracket_scanner.ail diff --git a/docs/WhatsNew.md b/docs/WhatsNew.md index 364880f..a31f4b1 100644 --- a/docs/WhatsNew.md +++ b/docs/WhatsNew.md @@ -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. diff --git a/docs/journals/INDEX.md b/docs/journals/INDEX.md index f5f37ec..c147e41 100644 --- a/docs/journals/INDEX.md +++ b/docs/journals/INDEX.md @@ -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 diff --git a/docs/roadmap.md b/docs/roadmap.md index b8220cc..e9db0b0 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -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` diff --git a/docs/specs/2026-05-18-fieldtest-remove-mut-var-assign.md b/docs/specs/2026-05-18-fieldtest-remove-mut-var-assign.md new file mode 100644 index 0000000..1bfd1fa --- /dev/null +++ b/docs/specs/2026-05-18-fieldtest-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 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. diff --git a/examples/fieldtest/remove-mut_1_sum_of_squares.ail b/examples/fieldtest/remove-mut_1_sum_of_squares.ail new file mode 100644 index 0000000..01d2322 --- /dev/null +++ b/examples/fieldtest/remove-mut_1_sum_of_squares.ail @@ -0,0 +1,29 @@ +; Axis: running numeric accumulator over a bounded range. +; +; Imperative instinct (the shape `mut` would invite): +; s = 0 +; for i in 1..=n: s = s + i*i +; return s +; +; Surviving-surface shape: a `loop` with two binders (i counter, +; acc accumulator) and a `recur` in the tail of the else branch. +; sum_sq(10) = 1+4+9+16+25+36+49+64+81+100 = 385. +; Expected stdout: 385 + +(module remove-mut_1_sum_of_squares + + (fn sum_sq + (doc "Sum of i*i for i in 1..=n via loop/recur. Two binders: i (counter), acc (running total).") + (type (fn-type (params (con Int)) (ret (con Int)))) + (params n) + (body + (loop (i (con Int) 1) (acc (con Int) 0) + (if (app > i n) + acc + (recur (app + i 1) (app + acc (app * i i))))))) + + (fn main + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body + (app print (app sum_sq 10))))) diff --git a/examples/fieldtest/remove-mut_2_grade_cascade.ail b/examples/fieldtest/remove-mut_2_grade_cascade.ail new file mode 100644 index 0000000..88bc87a --- /dev/null +++ b/examples/fieldtest/remove-mut_2_grade_cascade.ail @@ -0,0 +1,38 @@ +; Axis: conditional classification an imperative author writes as +; "set a code variable through a cascade of if-branches". +; +; Imperative instinct (the shape `mut` would invite): +; g = 0 ; 0=F +; if score >= 60: g = 1 ; D +; if score >= 70: g = 2 ; C +; if score >= 80: g = 3 ; B +; if score >= 90: g = 4 ; A +; return g +; +; Surviving-surface shape: either a let-threaded `g` updated by +; nested `if`, or a single nested `if` cascade. Both are pure +; expressions; the `mut` flag was never needed. +; grade(95)=4 (A), grade(72)=2 (C), grade(50)=0 (F). +; Expected stdout (per line): 4, 2, 0 + +(module remove-mut_2_grade_cascade + + (fn grade + (doc "Letter-grade code (4=A..0=F) from a numeric score, written as the let-threaded equivalent of a mutable cascade.") + (type (fn-type (params (con Int)) (ret (con Int)))) + (params score) + (body + (let g 0 + (let g (if (app >= score 60) 1 g) + (let g (if (app >= score 70) 2 g) + (let g (if (app >= score 80) 3 g) + (let g (if (app >= score 90) 4 g) + g))))))) + + (fn main + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body + (seq (app print (app grade 95)) + (seq (app print (app grade 72)) + (app print (app grade 50))))))) diff --git a/examples/fieldtest/remove-mut_3_horner_poly.ail b/examples/fieldtest/remove-mut_3_horner_poly.ail new file mode 100644 index 0000000..e2e37a3 --- /dev/null +++ b/examples/fieldtest/remove-mut_3_horner_poly.ail @@ -0,0 +1,35 @@ +; Axis: multi-step straight-line numeric build-up (Horner-form +; polynomial evaluation over fixed coefficients). +; +; Evaluate p(x) = 2x^4 + 3x^3 + 0x^2 + 5x + 7 at x. +; Imperative instinct (the shape `mut` would invite): +; acc = 2 +; acc = acc*x + 3 +; acc = acc*x + 0 +; acc = acc*x + 5 +; acc = acc*x + 7 +; return acc +; +; Surviving-surface shape: a straight chain of `let acc` shadowings, +; each step a pure expression. p(2) = 2*16+3*8+0+5*2+7 = 32+24+10+7 +; = 73. Expected stdout: 73 + +(module remove-mut_3_horner_poly + + (fn poly + (doc "Horner-form evaluation of 2x^4+3x^3+0x^2+5x+7 as a straight-line let chain (the faithful let/if form of a mutable acc).") + (type (fn-type (params (con Int)) (ret (con Int)))) + (params x) + (body + (let acc 2 + (let acc (app + (app * acc x) 3) + (let acc (app + (app * acc x) 0) + (let acc (app + (app * acc x) 5) + (let acc (app + (app * acc x) 7) + acc))))))) + + (fn main + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body + (app print (app poly 2))))) diff --git a/examples/fieldtest/remove-mut_4_bracket_scanner.ail b/examples/fieldtest/remove-mut_4_bracket_scanner.ail new file mode 100644 index 0000000..7cf0817 --- /dev/null +++ b/examples/fieldtest/remove-mut_4_bracket_scanner.ail @@ -0,0 +1,83 @@ +; Axis: a small streaming-feel state machine threading MORE THAN +; ONE piece of state, updating only a SUBSET per branch. +; +; Scan a token stream (a recursive Int list; 1 = '(', 2 = ')', +; 0 = any other token) and decide if the brackets are balanced. +; +; Imperative instinct (exactly what `mut` would invite): +; depth = 0 +; ok = true +; for t in stream: +; if t == 1: depth = depth + 1 ; only depth changes +; elif t == 2: +; if depth == 0: ok = false ; only ok changes +; else: depth = depth - 1 ; only depth changes +; ; t == 0 -> neither changes +; return ok && depth == 0 +; +; Surviving-surface shape: a recursive scanner fn threading +; (rest, depth, ok) — `loop`/`recur` cannot walk an ADT (it rebinds +; positionally, there is no structural pattern in a recur), so the +; stream traversal is ordinary tail recursion; the per-branch +; "update a subset of the state" becomes "pass the unchanged values +; through unchanged". This is the task that most stresses the +; removal: an imperative author updates one variable and leaves the +; others implicitly alone; here every recur must restate all three. +; +; balanced "(()())" -> true ; stream 1 1 2 1 2 2 +; balanced ")(" -> false ; stream 2 1 +; Expected stdout (per line): true, false + +(module remove-mut_4_bracket_scanner + + (data TokStream + (doc "1='(' , 2=')' , 0=other.") + (ctor Nil) + (ctor Cons (con Int) (con TokStream))) + + (fn scan + (doc "Thread (rest, depth, ok). Each arm updates a subset of state; the unchanged components must be restated in every tail call.") + (type + (fn-type + (params (own (con TokStream)) (con Int) (con Bool)) + (ret (con Bool)))) + (params rest depth ok) + (body + (match rest + (case (pat-ctor Nil) + (if ok (app == depth 0) false)) + (case (pat-ctor Cons t more) + (match t + (case (pat-lit 1) + (tail-app scan more (app + depth 1) ok)) + (case (pat-lit 2) + (if (app == depth 0) + (tail-app scan more depth false) + (tail-app scan more (app - depth 1) ok))) + (case _ + (tail-app scan more depth ok))))))) + + (fn balanced + (type (fn-type (params (own (con TokStream))) (ret (con Bool)))) + (params s) + (body (app scan s 0 true))) + + (fn main + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body + (seq + (app print + (app balanced + (term-ctor TokStream Cons 1 + (term-ctor TokStream Cons 1 + (term-ctor TokStream Cons 2 + (term-ctor TokStream Cons 1 + (term-ctor TokStream Cons 2 + (term-ctor TokStream Cons 2 + (term-ctor TokStream Nil))))))))) + (app print + (app balanced + (term-ctor TokStream Cons 2 + (term-ctor TokStream Cons 1 + (term-ctor TokStream Nil)))))))))