Files
AILang/docs/plans/remove-mut-var-assign.1.md
T
Brummel f355899fdf 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).
2026-05-18 10:21:16 +02:00

508 lines
25 KiB
Markdown
Raw 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.
# 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.