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:
@@ -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<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.
|
||||
Reference in New Issue
Block a user