All 176 files in the four accumulating directories now use a zero-padded 4-digit counter prefix that reflects creation order (`NNNN-slug.md`). The counter is assigned per directory in strict git-log creation order; ties broken alphabetically by original name. The old `YYYY-MM-DD-` prefix on docs/specs/ and docs/plans/ files is dropped — the date is recoverable from git log and the counter carries the ordering. A file's counter is stable for the life of the file: never reassigned, never reused, never compacted. Deleted files retire their counter; subsequent files do not fill the gap. This is the property that lets cross-references stay literal — refs use the full filename including the counter (`design/contracts/0007-honesty-rule.md`) so they grep cleanly and resolve directly without a glob step. 313 cross-references updated across .md/.rs/.toml/.c/.json files (test pins, include_str! paths, design-INDEX entries, baseline notes, runtime C comments, inter-contract markdown links incl. bare basename and `../models/foo.md` forms). CLAUDE.md gets a new "File-naming convention" section spelling out the rule and rationale. skills/brainstorm/SKILL.md and skills/planner/SKILL.md updated so new spec/plan creation produces counter-prefixed names from the start. The full test suite (cargo test --workspace) passes.
25 KiB
Remove mut / var / assign — Implementation Plan (iteration 1, terminal)
Parent spec:
docs/specs/0037-remove-mut-var-assign.mdFor agentic workers: REQUIRED SUB-SKILL: use
skills/implementto 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 (mutkeyword rejected;{"t":"mut"}serde-rejected) - Modify:
crates/ailang-core/src/ast.rs:516-617(deleteTerm::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, 4CheckError,mut_scope_stackparam + 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_stackexternal callers) - Modify:
crates/ailang-codegen/src/lib.rs(lower arms 1779/1834,mut_var_allocas→binder_allocas,synth_with_extras3237/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_mut1587+,parse_assign1637+, 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(EXPECTED18→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
//! 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
Termmatch arm + themut_scope_stacksignature + all its callers to change together. Gate at task end iscargo build --workspace0 errors plus Task-1 pins GREEN. The rest ofcargo test --workspaceis expected red here (fixtures still containmut, 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::Assignmatch 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 1587expect_keyword("mut")through theTerm::Mutctor at 1623, whole fn) andfn parse_assign(1637 throughTerm::Assignctor 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 ::= …(theIter mut.1lines 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
CheckErrorvariants + 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_stackpush/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_stackparameter fromsynthand 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 fromsynth(decl 2938) and from its two local-decl helpers (2022, 2910). - Delete the argument at every internal recursive
synthcall:lib.rs3339,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.rs345/359 and 592/606;lift.rs745/750;mono.rs719/728 and 1365/1374. - Delete the argument at the test-only
synthcallers inlib.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::Lamescape 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 theTerm::Mutlower_termarm (1779–1825) andTerm::Assignarm (1834–1855). -
lib.rs: rename the fieldmut_var_allocas→binder_allocasat the SURVIVING sites only: decl 731, ctor-init 871, clear 1077,Term::Varload 1405,Term::Looparm 1899/1914/1917,synth_with_extrasTerm::Var3014. (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_allocas144 →saved_binder_allocas,saved_pending_allocas152 unchanged name is fine; restore 265/267). TheTerm::Mutarm at 486 was deleted in Step 2;Term::Looparm 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 variantsTermMut(50) /TermAssign(51), the expected-set entries (99–100), and thevisit_termarms (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 …):
;; 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)
;; 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.rsEXPECTED 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)
- 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.
- 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_clitest), not a placeholder. - Type/path consistency:
mut_scope_stack,loop_stack,mut_var_allocas→binder_allocas,binder_allocas,LoopBinderCapturedByLambdaused consistently across T2 steps. - 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. - No commit steps: none present.
- Pin/replacement substring contiguity: T1's pins assert
is_err()(no.contains(substring)against a verbatim doc body), and T4.S3 is agrepaudit not a presence-pin paired with a verbatim replacement — the line-wrap family does not apply here. - Compile-gate vs deferred-caller: T2.S7 threads the
synthsignature change AND every caller (internal + builtins/lift/mono- test) in the same step; the only
cargo build0-errors gate (T2.S10) sits after S7, never before a caller it needs. The fullcargo testgate is the last step of the last task (T6.S3), after every test-affecting change.
- test) in the same step; the only
- Verification-filter strings resolve:
--test mut_removed_pinis a file this plan creates (T1);--test loop_recur_typecheck_pin- the named fn was ratified green by the grounding-check;
--test roundtrip_cliexists (recon C-Q2); the e2emut_counter_prints_55/mut_sum_floats_prints_55are run viaail runon the rewritten fixtures (T5.S5) rather than a name filter, asserting the55literal directly. No filter resolves to zero tests.
- the named fn was ratified green by the grounding-check;