# 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<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 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.
