plan: remove-mut-var-assign.1 — single terminal iteration

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).
This commit is contained in:
2026-05-18 10:21:16 +02:00
parent d1ad50e1b1
commit f355899fdf
+507
View File
@@ -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 36 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<ailang_core::ast::Term, _> = 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<ailang_core::ast::Term, _> = 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 36; 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 516532, doc + body) and `Term::Assign`
(533542). Delete `struct MutVar` (590617). Keep `Term::Loop`/
`Term::Recur` (543575) and `struct LoopBinder` (619638); 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 956974
(`Term::Mut`) and 9771004 (`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 (110 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 16761677; keep `"loop" | "recur"` (16781679).
- In the grammar doc-comment EBNF block delete the three lines
`//! mut-term ::= …`, `//! var-decl ::= …`, `//! assign-term ::= …`
(the `Iter mut.1` lines at ~7072).
- Delete the in-source `#[cfg(test)]` mut/assign parse tests at
25972644 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 (551585) and `Term::Assign` arm
(586602). 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` (661665),
`AssignTypeMismatch` (670675), `UnsupportedMutVarType` (682686),
`MutVarCapturedByLambda` (697). Delete their `code()` arms (786789)
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 (38523883, incl. the
`UnsupportedMutVarType` raise 3856, push/pop 3867/3872 and
3876/3881) and the `Term::Assign` synth arm (38883925, incl.
`MutAssignOutOfScope` 3905, `AssignTypeMismatch` 3918). Delete the
`Term::Var`-resolution read of `mut_scope_stack` at 29872988
(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 (36453651,
the `for frame in mut_scope_stack … MutVarCapturedByLambda` loop).
Keep verbatim: the shared free-var scan (36383643) and the loop
half (36523658, `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 (17791825) and
`Term::Assign` arm (18341855).
- `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 36.
---
### 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 209211 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 200230 first; the recon located the mut row at 210211).
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
(99100), 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 (24042413) and the `Term::Assign`
JSONC block (24152422). Keep the `loop`/`recur` blocks (24242441).
- [ ] **Step 2: Delete the mut prose, keep+reword the loop half**
Delete 24482454 (the `mut`-specific post-schema paragraph). Lines
24542458 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 (28912910) and the
`mut`-specific accumulator-over-iteration idiom line (2912). Do not
touch DESIGN.md 99/106107 (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` (3357) delete the 6 mut carve-out entries at
4349 (`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 (629)
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 439445 (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 421435 (decoupled but
surviving per spec §Goal) — but if any line in 421435 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.