From f355899fdf6071c0aeb7de182e93c08568ca743a Mon Sep 17 00:00:00 2001 From: Brummel Date: Mon, 18 May 2026 10:21:16 +0200 Subject: [PATCH] =?UTF-8?q?plan:=20remove-mut-var-assign.1=20=E2=80=94=20s?= =?UTF-8?q?ingle=20terminal=20iteration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One iteration (not the 4-then-3 split the brainstorm carrier assumed): the recon's cross-references prove a Rust exhaustive-match enum-variant removal is atomic and the workspace test suite is necessarily red between the Rust cut and the fixture/doc/drift cleanup, so the full cargo-test green gate can only sit at the final task. 6 tasks: T1 RED must-fail pins, T2 the atomic Rust cut (~40 match arms + mut_scope_stack signature + all ~37 callers + escape-guard loop-half-preserving simplification + codegen rename mut_var_allocas→binder_allocas; gate cargo build 0-errors + T1 GREEN), T3 dangling test-pin deletions, T4 DESIGN.md hard-delete, T5 faithful let/if fixture rewrites + 3 rejection-probe deletions, T6 carve_out 18→12 + roadmap + final full-suite + loop/recur non-regression gate. Plan-recon resolved 3 open Qs (form_a.md does not exist — grammar lives in parse.rs doc-comment; round_trip.rs + roundtrip_cli.rs auto-sweep examples/; escape.rs:533 walker is loop-reachable). --- docs/plans/remove-mut-var-assign.1.md | 507 ++++++++++++++++++++++++++ 1 file changed, 507 insertions(+) create mode 100644 docs/plans/remove-mut-var-assign.1.md diff --git a/docs/plans/remove-mut-var-assign.1.md b/docs/plans/remove-mut-var-assign.1.md new file mode 100644 index 0000000..c925c8a --- /dev/null +++ b/docs/plans/remove-mut-var-assign.1.md @@ -0,0 +1,507 @@ +# Remove `mut` / `var` / `assign` — Implementation Plan (iteration 1, terminal) + +> **Parent spec:** `docs/specs/2026-05-18-remove-mut-var-assign.md` +> +> **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement` +> to run this plan. Steps use `- [ ]` checkboxes for tracking. + +**Goal:** Remove the `mut`/`var`/`assign` construct from AILang +entirely (AST + surface + checker + codegen + DESIGN.md + fixtures + +drift trio), atomically, with `loop`/`recur` + `let`/`if` as the +surviving forms. + +**Architecture:** A Rust exhaustive-`match` enum-variant removal is +atomic by language design — there is no compile-preserving +intermediate, and the workspace test suite is necessarily red +between the Rust cut and the fixture/doc/drift cleanup. Therefore +this is **one terminal iteration**: Task 1 writes the RED must-fail +pins; Task 2 is the atomic Rust cut (gate: `cargo build --workspace` +0 errors + Task-1 pins GREEN; full suite intentionally red, stated); +Tasks 3–6 land the dangling-pin deletions, DESIGN.md, fixtures, +carve-out/roadmap; the **full `cargo test --workspace` green gate + +loop/recur non-regression is the final step of Task 6 only**. + +**Tech Stack:** `ailang-core` (ast/desugar/workspace), `ailang-surface` +(parse/print), `ailang-check` (synth/escape/CheckError), `ailang-codegen` +(lib/escape/lambda), `ailang-prose`, `ail` (main), the drift-trio + +e2e + round-trip test crates. + +--- + +**Files this plan creates or modifies:** + +- Create: `crates/ailang-surface/tests/mut_removed_pin.rs` — RED→GREEN must-fail pins (`mut` keyword rejected; `{"t":"mut"}` serde-rejected) +- Modify: `crates/ailang-core/src/ast.rs:516-617` (delete `Term::Mut`/`Term::Assign`/`MutVar`; in-source test arms 964/981/994) +- Modify: `crates/ailang-core/src/desugar.rs` (Mut/Assign arms — lines in Task 2 step) +- Modify: `crates/ailang-core/src/workspace.rs:1254/1261, 1401/1407` +- Modify: `crates/ailang-check/src/lib.rs` (synth arms, 4 `CheckError`, `mut_scope_stack` param + all callers, escape guard 3637, match arms) +- Modify: `crates/ailang-check/src/{linearity,uniqueness,reuse_shape,pre_desugar_validation}.rs` (Mut/Assign arms) +- Modify: `crates/ailang-check/src/{builtins,lift,mono}.rs` (`mut_scope_stack` external callers) +- Modify: `crates/ailang-codegen/src/lib.rs` (lower arms 1779/1834, `mut_var_allocas`→`binder_allocas`, `synth_with_extras` 3237/3238) +- Modify: `crates/ailang-codegen/src/escape.rs` (Mut/Assign arms 197/203, 400/408, 533/548) +- Modify: `crates/ailang-codegen/src/lambda.rs:486` (Mut arm; rename save/restore 144/152/265/267) +- Modify: `crates/ailang-prose/src/lib.rs:909/933, 1140/1159, 1300/1333` +- Modify: `crates/ailang-surface/src/parse.rs` (dispatch 1213/1214, `parse_mut` 1587+, `parse_assign` 1637+, reservation 1676-1677, grammar doc-comment 70-72, test arms 2597-2656) +- Modify: `crates/ailang-surface/src/print.rs:551-602` +- Modify: `crates/ail/src/main.rs:1520/1538, 2772/2797` +- Delete: `crates/ailang-check/tests/mut_typecheck_pin.rs` (whole file) +- Modify: `crates/ail/tests/ct1_check_cli.rs:209-211` (drop mut case) +- Modify: `crates/ailang-core/tests/schema_coverage.rs:50-51, 99-100, 239/247` +- Modify: `crates/ailang-core/tests/design_schema_drift.rs:141, 148, 184/185` +- Modify: `crates/ailang-core/tests/spec_drift.rs:163/164` +- Modify: `docs/DESIGN.md:2404-2422, 2448-2458, 2891-2912` +- Modify (rewrite): `examples/mut.ail`, `examples/mut_counter.ail`, `examples/mut_sum_floats.ail`, `examples/fieldtest/mut-local_{1,2,3,4}*.ail` +- Delete: `examples/fieldtest/mut-local_5_lambda_capture_probe.ail`, `examples/fieldtest/mut-local_6_diag_probe.ail`, `examples/test_mut_var_captured_by_lambda.ail.json` +- Modify: `crates/ailang-core/tests/carve_out_inventory.rs:6-57` (`EXPECTED` 18→12, drop 6 mut entries) +- Modify: `docs/roadmap.md:439-445` (delete false-foundation sub-bullet) +- Test (non-regression, unchanged, must stay green): `crates/ail/tests/e2e.rs:2863/2876` (mut_counter/mut_sum_floats →55) and the loop/recur e2e + `loop_recur_typecheck_pin.rs::lambda_capturing_loop_binder_emits_loop_binder_captured_by_lambda` + +--- + +### Task 1: RED must-fail pins (the removal made executable) + +**Files:** +- Create: `crates/ailang-surface/tests/mut_removed_pin.rs` + +- [ ] **Step 1: Write the failing pins** + +```rust +//! Pins the mut/var/assign removal: the keyword no longer parses and +//! the canonical JSON tag is an unknown serde variant. RED until the +//! Task-2 atomic cut lands. +use ailang_surface::parse; + +#[test] +fn mut_keyword_is_rejected_by_form_a_parse() { + let src = "(module m (fn f (type (fn-type (params) (ret (con Int)))) (params) (body (mut (var x (con Int) 0) (assign x (app + x 1)) x))))"; + let r = parse(src); + assert!(r.is_err(), "mut keyword must no longer parse, got Ok"); +} + +#[test] +fn assign_keyword_is_rejected_by_form_a_parse() { + let src = "(module m (fn f (type (fn-type (params) (ret (con Int)))) (params) (body (assign x 1))))"; + assert!(parse(src).is_err(), "assign keyword must no longer parse, got Ok"); +} + +#[test] +fn json_tag_mut_is_unknown_serde_variant() { + let j = r#"{"t":"mut","vars":[],"body":{"t":"lit","lit":{"kind":"int","value":0}}}"#; + let r: Result = serde_json::from_str(j); + assert!(r.is_err(), "{{\"t\":\"mut\"}} must be an unknown variant, got Ok"); +} + +#[test] +fn json_tag_assign_is_unknown_serde_variant() { + let j = r#"{"t":"assign","name":"x","value":{"t":"lit","lit":{"kind":"int","value":1}}}"#; + let r: Result = serde_json::from_str(j); + assert!(r.is_err(), "{{\"t\":\"assign\"}} must be an unknown variant, got Ok"); +} +``` + +- [ ] **Step 2: Run to verify RED** + +Run: `cargo test --workspace -p ailang-surface --test mut_removed_pin` +Expected: 4 tests, all FAIL (today `mut`/`assign` parse fine and the +serde tags deserialize), e.g. `mut keyword must no longer parse, got Ok`. + +--- + +### Task 2: The atomic Rust cut + +> Single logical unit: the enum-variant removal forces every +> exhaustive `Term` match arm + the `mut_scope_stack` signature + +> all its callers to change together. Gate at task end is +> `cargo build --workspace` 0 errors **plus Task-1 pins GREEN**. The +> rest of `cargo test --workspace` is **expected red** here (fixtures +> still contain `mut`, drift trio still asserts deleted anchors) — +> that is cleaned in Tasks 3–6; do not attempt to green it in Task 2. + +- [ ] **Step 1: Delete the AST variants + struct (`ailang-core/src/ast.rs`)** + +Delete `Term::Mut` (lines 516–532, doc + body) and `Term::Assign` +(533–542). Delete `struct MutVar` (590–617). Keep `Term::Loop`/ +`Term::Recur` (543–575) and `struct LoopBinder` (619–638); in +`LoopBinder`'s doc (620,625) drop the now-dangling `MutVar` +cross-reference phrase, leaving the sentence standing alone. Delete +the in-source `#[cfg(test)]` round-trip helpers/arms at 956–974 +(`Term::Mut`) and 977–1004 (`Term::Assign`, incl. arm 994). + +- [ ] **Step 2: Delete every exhaustive `Term::Mut`/`Term::Assign` match arm** + +At each location below delete the `Term::Mut { .. } => …` and/or +`Term::Assign { .. } => …` arm (1–10 lines each; an exhaustive +no-`_` match — the arm head is `Term::Mut { vars, body }` / +`Term::Assign { name, value }`). Leave the sibling `Term::Loop`/ +`Term::Recur` arms intact. + +``` +ailang-core/src/desugar.rs: 349,360 · 571,601 · 1257,1271 · 1440,1475 · 1647,1663 · 1742,1749 · 1806,1809 · 1840,1843 · 2965,2968 +ailang-core/src/workspace.rs: 1254,1261 (walk_term_embedded_types) · 1401,1407 (walk_term) +ailang-check/src/lib.rs: 249,260 (type-subst rebuild) · 2752,2761 (verify_tail_positions) · 2880,2886 (verify_loop_body) · 3829,3830 (string-tag arm) +ailang-check/src/linearity.rs: 601,607 · 807,808 · 942,947 +ailang-check/src/uniqueness.rs: 346,352 +ailang-check/src/reuse_shape.rs: 261,267 +ailang-check/src/pre_desugar_validation.rs: 129,135 +ailang-codegen/src/lib.rs: 3237,3238 (synth_with_extras) +ailang-codegen/src/escape.rs: 197,203 · 400,408 · 533,548 (collect_free_vars — Term::Loop arm at ~555 SURVIVES) +ailang-codegen/src/lambda.rs: 486 (Term::Mut arm; Term::Loop arm 516 SURVIVES) +ailang-prose/src/lib.rs: 909,933 · 1140,1159 · 1300,1333 +ail/src/main.rs: 1520,1538 (Display/walker) · 2772,2797 (type-rewrite walker) +``` + +(The `ailang-check/src/lib.rs` synth arms 3852/3888, the codegen +`lower_term` arms 1779/1834, and the surface ctor/print sites are +their own steps below.) + +- [ ] **Step 3: Surface — delete the parser, dispatch, reservation, grammar (`ailang-surface/src/parse.rs`)** + +- Delete dispatch lines 1213 (`"mut" => self.parse_mut()`) and 1214 + (`"assign" => self.parse_assign()`). +- Delete `fn parse_mut` (from 1587 `expect_keyword("mut")` through + the `Term::Mut` ctor at 1623, whole fn) and `fn parse_assign` + (1637 through `Term::Assign` ctor 1641, whole fn). +- In the head-ident reservation list, delete `"mut"` and `"assign"` + at 1676–1677; keep `"loop" | "recur"` (1678–1679). +- In the grammar doc-comment EBNF block delete the three lines + `//! mut-term ::= …`, `//! var-decl ::= …`, `//! assign-term ::= …` + (the `Iter mut.1` lines at ~70–72). +- Delete the in-source `#[cfg(test)]` mut/assign parse tests at + 2597–2644 and the arm at 2656 / test arms 2606,2627,2633. + +- [ ] **Step 4: Surface — delete the print arms (`ailang-surface/src/print.rs`)** + +Delete the `Term::Mut` arm (551–585) and `Term::Assign` arm +(586–602). Keep the surrounding `Term::Loop`/`Term::Recur` arms. + +- [ ] **Step 5: Checker — delete the 4 `CheckError` variants + registry/ctx (`ailang-check/src/lib.rs`)** + +Delete variant defs: `MutAssignOutOfScope` (661–665), +`AssignTypeMismatch` (670–675), `UnsupportedMutVarType` (682–686), +`MutVarCapturedByLambda` (697). Delete their `code()` arms (786–789) +and `ctx()` arms (857, 860, 863, 866). Keep +`LoopBinderCapturedByLambda` and all `Recur*` variants/codes/ctx +untouched. + +- [ ] **Step 6: Checker — delete the synth arms + `mut_scope_stack` push/pop (`ailang-check/src/lib.rs`)** + +Delete the `Term::Mut` synth arm (3852–3883, incl. the +`UnsupportedMutVarType` raise 3856, push/pop 3867/3872 and +3876/3881) and the `Term::Assign` synth arm (3888–3925, incl. +`MutAssignOutOfScope` 3905, `AssignTypeMismatch` 3918). Delete the +`Term::Var`-resolution read of `mut_scope_stack` at 2987–2988 +(the `Term::Var` arm itself stays; only the mut-scope lookup block +goes — `loop` binder names live in `locals`, not here). + +- [ ] **Step 7: Checker — remove the `mut_scope_stack` parameter from `synth` and ALL callers (`ailang-check`)** + +This is a crate-wide signature change; every site below must change +in this step so the Step-9 `cargo build` gate is satisfiable. + +- Remove the `mut_scope_stack: &mut Vec<…>` formal param from + `synth` (decl 2938) and from its two local-decl helpers (2022, + 2910). +- Delete the argument at every internal recursive `synth` call: + `lib.rs` 3339,3375,3384,3386,3398,3400,3401,3419,3514,3523,3541, + 3616,3618,3668,3756,3783,3799,3813,3843,3869,3878,3912,3943,3953, + 3991 and the helper call sites 2027, 2914. +- Delete the argument at the external callers: + `builtins.rs` 345/359 and 592/606; `lift.rs` 745/750; + `mono.rs` 719/728 and 1365/1374. +- Delete the argument at the test-only `synth` callers in + `lib.rs`: 4379/4393, 4420/4439, 4464/4486, 4581/4592, 4624/4635, + 4666/4677, 4808/4819, 4860/4869, 7600/7614. + +(`loop_stack` is a separate param and stays at every one of these +sites — only `mut_scope_stack` is removed.) + +- [ ] **Step 8: Checker — simplify the shared `Term::Lam` escape guard, keep the loop half (`ailang-check/src/lib.rs:3620-3660`)** + +Change the gate at 3637 from +`if !mut_scope_stack.is_empty() || !loop_stack.is_empty() {` +to `if !loop_stack.is_empty() {`. Delete the mut half (3645–3651, +the `for frame in mut_scope_stack … MutVarCapturedByLambda` loop). +Keep verbatim: the shared free-var scan (3638–3643) and the loop +half (3652–3658, `for frame in loop_stack … LoopBinderCapturedByLambda`). + +- [ ] **Step 9: Codegen — delete the lower arms; keep + rename the alloca machinery (`ailang-codegen`)** + +- `lib.rs`: delete the `Term::Mut` `lower_term` arm (1779–1825) and + `Term::Assign` arm (1834–1855). +- `lib.rs`: rename the field `mut_var_allocas` → `binder_allocas` + at the SURVIVING sites only: decl 731, ctor-init 871, clear 1077, + `Term::Var` load 1405, `Term::Loop` arm 1899/1914/1917, + `synth_with_extras` `Term::Var` 3014. (Sites 1805/1817/1820 and + 1836 vanish with the deleted arms — nothing to rename there.) + Update the field's doc-comment to state it holds loop-binder + allocas (drop any "mut-var" wording — make it truthful, no + post-mortem). +- `lib.rs`: `pending_entry_allocas` — the mut-arm push at 1787 + vanishes with Step 2/this step's arm deletion; keep decl 748, + init 873, clear 1078, fn-body splice 1265/1271/1279, loop-arm + 1886 (all loop/shared, unchanged). +- `lambda.rs`: rename the save/restore locals to match + (`saved_mut_allocas` 144 → `saved_binder_allocas`, + `saved_pending_allocas` 152 unchanged name is fine; restore + 265/267). The `Term::Mut` arm at 486 was deleted in Step 2; + `Term::Loop` arm 516 stays. +- `escape.rs`: the Mut/Assign arms (197/203, 400/408, 533/548) + were deleted in Step 2 — nothing further here. + +- [ ] **Step 10: Compile gate + Task-1 pins GREEN** + +Run: `cargo build --workspace 2>&1 | tail -5` +Expected: `Finished` / 0 errors. (If any `error[E0...]`, a match +arm or `mut_scope_stack` caller was missed — fix before proceeding; +do NOT add a `_ =>` wildcard.) + +Run: `cargo test --workspace -p ailang-surface --test mut_removed_pin` +Expected: 4 tests, all PASS. + +Run (sanity, expected RED suite elsewhere — informational only): +`cargo test --workspace 2>&1 | tail -3` — failures in fixture/drift +tests here are EXPECTED and resolved in Tasks 3–6. + +--- + +### Task 3: Delete the dangling test pins + +**Files:** `crates/ailang-check/tests/mut_typecheck_pin.rs` (delete), +`crates/ail/tests/ct1_check_cli.rs`, +`crates/ailang-core/tests/{schema_coverage,design_schema_drift,spec_drift}.rs` + +- [ ] **Step 1: Delete the whole-file mut pin** + +`rm crates/ailang-check/tests/mut_typecheck_pin.rs` (all 6 tests pin +removed `CheckError` codes — no surviving behaviour to assert). + +- [ ] **Step 2: Drop the mut case in `ct1_check_cli.rs`** + +At 209–211 remove the `check_human_mode_renders_mut_diagnostic_code_exactly_once` +test (or just its mut table row if the test also covers a surviving +code — read 200–230 first; the recon located the mut row at 210–211). +The loop/recur F2 sibling test stays. + +- [ ] **Step 3: Drop the mut entries in the drift trio** + +- `schema_coverage.rs`: delete the test-local enum variants + `TermMut` (50) / `TermAssign` (51), the expected-set entries + (99–100), and the `visit_term` arms (239, 247). +- `design_schema_drift.rs`: delete the `"t": "mut"` anchor (141), + the `"t": "assign"` anchor (148), and the tag-helper arms + (184, 185). +- `spec_drift.rs`: delete the tag-helper arms (163, 164). + +(These drop in lockstep with the DESIGN.md schema deletion in +Task 4 — drift trio asserts DESIGN.md and schema agree; both sides +of the mut anchor go together.) + +--- + +### Task 4: DESIGN.md hard-delete (present-tense, no nostalgia) + +**Files:** `docs/DESIGN.md` + +- [ ] **Step 1: Delete the data-model entries** + +Delete the `Term::Mut` JSONC block (2404–2413) and the `Term::Assign` +JSONC block (2415–2422). Keep the `loop`/`recur` blocks (2424–2441). + +- [ ] **Step 2: Delete the mut prose, keep+reword the loop half** + +Delete 2448–2454 (the `mut`-specific post-schema paragraph). Lines +2454–2458 are the loop-binder half (`Loop binders are alloca-resident +… LoopBinderCapturedByLambda (iter loop-recur.tidy)`) — keep it, and +reword its opening so it stands alone (no "likewise"/"the same" +back-reference to the deleted mut sentence). Delete the +"Local mutable state" feature prose (2891–2910) and the +`mut`-specific accumulator-over-iteration idiom line (2912). Do not +touch DESIGN.md 99/106–107 (the clause-3 rationale prose — it names +iterated-mutable-state as a bug class, not as a live construct). + +- [ ] **Step 3: Verify no residue** + +Run: `grep -n -iE '\bmut\b|\bvar-decl\b|\bassign\b|MutVar|Term::Mut|Term::Assign' docs/DESIGN.md` +Expected: no line that *documents the construct*. (Allowed survivors: +the clause-3 prose at ~99/106 saying "iterated-mutable-state"; any +`loop`-binder sentence. If a `mut`-as-construct line remains, delete +it. There must be zero aspirational/post-mortem mention.) + +--- + +### Task 5: Fixture rewrites + rejection-probe deletions + +**Files:** `examples/mut.ail`, `examples/mut_counter.ail`, +`examples/mut_sum_floats.ail`, +`examples/fieldtest/mut-local_{1,2,3,4}*.ail` (rewrite); +`examples/fieldtest/mut-local_5_lambda_capture_probe.ail`, +`examples/fieldtest/mut-local_6_diag_probe.ail`, +`examples/test_mut_var_captured_by_lambda.ail.json` (delete) + +- [ ] **Step 1: Delete the three rejection-probe fixtures** + +``` +rm examples/fieldtest/mut-local_5_lambda_capture_probe.ail \ + examples/fieldtest/mut-local_6_diag_probe.ail \ + examples/test_mut_var_captured_by_lambda.ail.json +``` + +(Their sole purpose is the removed mut-specific rejection; no +`let`/`if` equivalent exists.) + +- [ ] **Step 2: Rewrite `examples/mut.ail` (6 fns, faithful 1:1 let-threading)** + +Replace each `(body …)`: + +```clojure +;; mut_empty +(body 0) +;; mut_single_var → 1 +(body (let x 0 (let x (app + x 1) x))) +;; mut_two_vars → 2.0 +(body (let sum 0.0 (let count 0 (let sum (app + sum 1.0) (let count (app + count 1) (app + sum (app int_to_float count))))))) +;; mut_nested_shadow → 101 +(body (let x 10 (let x (app + x 1) (let x 100 (let x (app + x 1) x))))) +;; mut_returns_bool → true +(body (let flag false (let flag true flag))) +;; mut_returns_unit → unit +(body (let u (lit-unit) (let u (lit-unit) u))) +``` + +Update each `(doc …)` line to describe the `let` form (drop the +"Iter mut.1 — …" wording; present-tense, e.g. `"Nested let +shadowing; inner binding is the block value."`). No `mut`/`var`/ +`assign` token may remain in the file. + +- [ ] **Step 3: Rewrite `mut_counter.ail` / `mut_sum_floats.ail` (spec's fixed shape)** + +`mut_counter.ail` `main` body → `(body (app print (app sum_helper 1 10 0)))`. +`mut_sum_floats.ail` `main` body → `(body (app print (app sum_helper 1.0 10.0 0.0)))`. +`sum_helper` unchanged in both. Update the two `(doc …)` lines to +present-tense (drop "Iter mut.3"; e.g. `"Sum 1..10 via a +tail-recursive helper. Expected stdout: 55."`). + +- [ ] **Step 4: Rewrite `mut-local_1..4` (faithful)** + +```clojure +;; mut-local_1_factorial :: main body → prints 120 +(body (app print (let prod 1 (let prod (app * prod 1) (let prod (app * prod 2) (let prod (app * prod 3) (let prod (app * prod 4) (let prod (app * prod 5) prod)))))))) + +;; mut-local_2_classify_temp :: classify body → classify 22 = 2 +(body (let code 0 (let code (if (app < t 0) 0 (if (app < t 15) 1 (if (app < t 28) 2 3))) code))) + +;; mut-local_3_horner :: main body → prints 18 +(body (app print (let acc 2.0 (let acc (app - (app * acc 2.5) 3.0) (let acc (app + (app * acc 2.5) 5.0) (let acc (app - (app * acc 2.5) 7.0) acc)))))) + +;; mut-local_4_has_small_factor :: has_small_factor body → has_small_factor 91 = true +(body (let found false (let found (if (app == (app % n 2) 0) true found) (let found (if (app == (app % n 3) 0) true found) (let found (if (app == (app % n 5) 0) true found) (let found (if (app == (app % n 7) 0) true found) found)))))) +``` + +Replace the leading `;` comment block of each file with a short +present-tense one-liner (no "Why this fits mut-local's scope", +no "Iter", no post-mortem). The `main` fns of `_2`/`_4` are +unchanged. No `mut`/`var`/`assign` token may remain. + +- [ ] **Step 5: Verify the rewrites parse, run, and round-trip** + +Run: `cargo run -q -p ail -- run examples/mut_counter.ail` → +Expected stdout: `55` +Run: `cargo run -q -p ail -- run examples/mut_sum_floats.ail` → +Expected stdout: `55` +Run: `cargo run -q -p ail -- run examples/fieldtest/mut-local_1_factorial.ail` → +Expected stdout: `120` +Run: `cargo run -q -p ail -- run examples/fieldtest/mut-local_3_horner.ail` → +Expected stdout: `18` +Run: `for f in examples/mut.ail examples/fieldtest/mut-local_2_classify_temp.ail examples/fieldtest/mut-local_4_has_small_factor.ail; do cargo run -q -p ail -- check "$f"; done` +Expected: each exits 0 (no diagnostics). + +--- + +### Task 6: carve-out + roadmap, then the final green gate + +**Files:** `crates/ailang-core/tests/carve_out_inventory.rs`, +`docs/roadmap.md` + +- [ ] **Step 1: `carve_out_inventory.rs` EXPECTED 18 → 12** + +In `const EXPECTED` (33–57) delete the 6 mut carve-out entries at +43–49 (`test_mut_assign_out_of_scope`, +`test_mut_assign_outside_mut`, `test_mut_assign_type_mismatch`, +`test_mut_nested_shadow_legal`, `test_mut_var_unsupported_type`, +`test_mut_var_captured_by_lambda`). Update the count in the test's +assertion / any literal `18` to `12`. Update the header doc (6–29) +to drop the mut carve-out lines; keep the loop carve-out lines +(incl. `test_loop_binder_captured_by_lambda.ail.json`, which +survives). + +- [ ] **Step 2: `docs/roadmap.md` — delete the false-foundation sub-bullet** + +Delete lines 439–445 (the `Sub-milestone 1 closed 2026-05-15: +**mut-local** — sealed-by-construction mut/var/assign…` sub-bullet). +Keep the Stateful-islands milestone header at 421–435 (decoupled but +surviving per spec §Goal) — but if any line in 421–435 still asserts +mut-local *is the foundation*, reword it to state Stateful-islands +has no existing scaffolding (present-tense honest; the spec's +decoupling). Do not delete the milestone entry itself. + +- [ ] **Step 3: FINAL green gate — full suite + loop/recur non-regression** + +Run: `cargo test --workspace 2>&1 | tail -5` +Expected: `test result: ok` for every crate; 0 failed. (Count must +be ≥ the pre-iteration green count minus the 6 deleted +`mut_typecheck_pin` tests minus the dropped `ct1` mut test; no +*other* test lost.) + +Run: `cargo test --workspace -p ailang-check --test loop_recur_typecheck_pin lambda_capturing_loop_binder_emits_loop_binder_captured_by_lambda` +Expected: PASS (the shared escape guard's loop half survived). + +Run: `cargo run -q -p ail -- run examples/loop_sum_to_run.ail` → `55`; +`cargo run -q -p ail -- run examples/loop_sum_to_deep.ail` → +`500000500000`; `cargo build -q -p ail` then +`cargo run -q -p ail -- build examples/loop_forever_build.ail` +exits 0 (loop/recur codegen byte-unaffected by the rename). + +Run: `cargo run -q -p ail -- parse examples/loop_sum_to_run.ail | cargo run -q -p ail -- render - | cargo run -q -p ail -- parse -` +Expected: byte-identical to `cargo run -q -p ail -- parse examples/loop_sum_to_run.ail` (Roundtrip Invariant holds for surviving modules; adjust to the actual `ail` round-trip CLI form if `-` stdin is unsupported — use `roundtrip_cli` test names instead: `cargo test --workspace -p ail --test roundtrip_cli`, expected PASS). + +--- + +## Self-review (planner Step 5) + +1. **Spec coverage:** Component 1→T2.S1-2; C2→T2.S3-4; C3→T2.S5-8; + C4→T2.S9; C5→T5; C6→T4; C7→T6.S1-2; the must-fail executable + pin (spec §"Concrete code shapes"/§Testing)→T1+T2.S10; loop/recur + non-regression (spec §Error handling/§Testing)→T6.S3. All spec + sections mapped. +2. **Placeholder scan:** no "TBD/TODO/implement later/similar to + Task/add appropriate". The carve-out integer is concrete (18→12, + recon-pinned). One conditional in T6.S3 (round-trip CLI form) has + a named concrete fallback (`roundtrip_cli` test), not a + placeholder. +3. **Type/path consistency:** `mut_scope_stack`, `loop_stack`, + `mut_var_allocas`→`binder_allocas`, `binder_allocas`, + `LoopBinderCapturedByLambda` used consistently across T2 steps. +4. **Step granularity:** the only large step is T2 (atomic by Rust + semantics — cannot be split without a non-compiling intermediate + or a forbidden `_ =>` wildcard); its sub-steps are each a + bounded delete-list. Documented as the irreducible unit. +5. **No commit steps:** none present. +6. **Pin/replacement substring contiguity:** T1's pins assert + `is_err()` (no `.contains(substring)` against a verbatim doc + body), and T4.S3 is a `grep` audit not a presence-pin paired + with a verbatim replacement — the line-wrap family does not + apply here. +7. **Compile-gate vs deferred-caller:** T2.S7 threads the `synth` + signature change AND every caller (internal + builtins/lift/mono + + test) in the same step; the only `cargo build` 0-errors gate + (T2.S10) sits after S7, never before a caller it needs. The full + `cargo test` gate is the last step of the last task (T6.S3), + after every test-affecting change. +8. **Verification-filter strings resolve:** `--test mut_removed_pin` + is a file this plan creates (T1); `--test loop_recur_typecheck_pin` + + the named fn was ratified green by the grounding-check; + `--test roundtrip_cli` exists (recon C-Q2); the e2e + `mut_counter_prints_55`/`mut_sum_floats_prints_55` are run via + `ail run` on the rewritten fixtures (T5.S5) rather than a name + filter, asserting the `55` literal directly. No filter resolves + to zero tests.