plan: it.3 retire tail-app/tail-do + corpus migration — 7 tasks
Destructive terminal iteration. Safe order: oracle-capture →
migrate corpus → remove it.2 exemptions (the migration-completeness
gate) → remove tail machinery + 18g.1 husk-dec → DESIGN/drift
lockstep → accept. DD-4 carries the OQ1 RC-RSS contingency as an
explicit RED-gated step (Task 5.5) with named remediation
(owned-loop-binder-drop-on-recur, Task 5b) per the corrected spec.
DD-5 keeps is_false (shared with drop_iterative). Class-(b)
enumeration is a live sweep (Task 1.3, spec-delegated). Spec
fda9b78 + corrections 2018178/10a0595.
This commit is contained in:
@@ -0,0 +1,560 @@
|
|||||||
|
# it.3 — retire tail-app/tail-do + corpus migration — Implementation Plan
|
||||||
|
|
||||||
|
> **Parent spec:** `docs/specs/2026-05-15-iteration-discipline.md`
|
||||||
|
> (scope-corrected at 2018178 + it.3-recon-corrected at 10a0595)
|
||||||
|
>
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement`
|
||||||
|
> to run this plan. Steps use `- [ ]` checkboxes for tracking.
|
||||||
|
|
||||||
|
**Goal:** Delete the marked-tail-call concept entirely
|
||||||
|
(`Term::App.tail`/`Term::Do.tail`, both surface keywords, the
|
||||||
|
`musttail` lowering, `verify_tail_positions`' tail-app role,
|
||||||
|
`TailCallNotInTailPosition`, the 18g.1 husk-dec, the it.2
|
||||||
|
transitional grandfather + no-ADT-candidate skip, Decision 8 →
|
||||||
|
tombstone) and migrate the full corpus non-structural-recursion set
|
||||||
|
to `(loop …)`/`recur` (or plain structural calls), every fixture
|
||||||
|
byte-identical in observable behaviour to before.
|
||||||
|
|
||||||
|
**Architecture:** Destructive + migration. The safe order is
|
||||||
|
**oracle → migrate → remove-exemptions → remove-machinery →
|
||||||
|
docs/drift → accept**. Migration *before* removal keeps the
|
||||||
|
round-trip invariant green throughout (a migrated `.ail` has no
|
||||||
|
`(tail-app …)` so it parses even while the field still exists).
|
||||||
|
Removing the it.2 exemptions *before* removing the tail machinery is
|
||||||
|
the correctness proof that migration was complete (if any fixture
|
||||||
|
still needed the grandfather, that task goes RED — a migration bug,
|
||||||
|
fixed in the migration task, never by re-adding the exemption). The
|
||||||
|
one genuinely non-mechanical risk is the spec's §Codegen-rework
|
||||||
|
contingency: deleting the 18g.1 husk-dec is RC-safe only if it.1
|
||||||
|
loop codegen drops superseded owned binders across `recur`; this is
|
||||||
|
a RED-gated step with a named remediation (Task 5b).
|
||||||
|
|
||||||
|
**Tech Stack:** `ailang-core` (ast/serde), `ailang-surface`
|
||||||
|
(parse/print), `ailang-prose`, `ailang-check`
|
||||||
|
(verify_tail_positions, the it.2 exemptions, CheckError),
|
||||||
|
`ailang-codegen` (musttail, 18g.1 husk-dec, loop-binder RC),
|
||||||
|
DESIGN.md, `crates/ailang-core/specs/form_a.md`, the drift/spec/
|
||||||
|
schema-coverage test crates, ~25+ `examples/*.ail` fixtures.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Design decisions (Boss-resolved; not implementer judgement calls)
|
||||||
|
|
||||||
|
- **DD-1 — oracle = existing tests first, captured snapshots only
|
||||||
|
for the gap.** Many migration-set fixtures already have an
|
||||||
|
e2e/round-trip/bench assertion (authoritative, already in
|
||||||
|
`cargo test --workspace`). The byte-identical-stdout requirement
|
||||||
|
is satisfied by: (a) those existing tests staying green across
|
||||||
|
migration; (b) for a fixture with a `main` but no dedicated stdout
|
||||||
|
test, a snapshot captured in Task 1 to
|
||||||
|
`bench/it3-oracle/<stem>.out` and diffed post-migration; (c) for a
|
||||||
|
check-only fixture (no `main`), the oracle is `ail check` exit
|
||||||
|
status + diagnostic-code list, captured the same way. No fixture
|
||||||
|
is migrated without an oracle.
|
||||||
|
- **DD-2 — migrate before remove.** The `Term::App.tail` field stays
|
||||||
|
present through Tasks 1–4; only Task 5 deletes it. This keeps
|
||||||
|
parse/print/serde/round-trip compiling and green while fixtures
|
||||||
|
are rewritten. The field is simply unused by migrated fixtures.
|
||||||
|
- **DD-3 — exemption-removal is the migration-completeness gate.**
|
||||||
|
Task 4 removes the it.2 `tail==false` grandfather AND the
|
||||||
|
no-ADT-candidate skip. Its green `cargo test --workspace` *is* the
|
||||||
|
proof that Task 3's migration left no fixture depending on either
|
||||||
|
exemption. A RED here is a Task-3 migration defect to fix in
|
||||||
|
Task 3, never by softening Task 4.
|
||||||
|
- **DD-4 — the 18g.1 / RC-RSS contingency (spec §Codegen-rework).**
|
||||||
|
Task 5 deletes the 18g.1 pre-tail-call husk-dec
|
||||||
|
(`match_lower.rs:658–736`) — field-forced, it cannot compile once
|
||||||
|
`Term::App.tail` is gone. RC-safety is **not assumed**: Task 5
|
||||||
|
re-runs the RC-RSS bench pins (18f.2 + the RC-RSS suite) as a
|
||||||
|
hard gate. If RED → Task 5b: add owned-loop-binder-drop-on-`recur`
|
||||||
|
to it.1 loop codegen (the loop-side of the seam; in scope for
|
||||||
|
it.3 per the corrected spec). Keeping 18g.1 is not an option (it
|
||||||
|
cannot survive the field removal).
|
||||||
|
- **DD-5 — `is_false` survives.** Recon: `is_false`
|
||||||
|
(`ast.rs:908–916`) is shared with `WorkspaceDef.drop_iterative`
|
||||||
|
(`ast.rs:168`). Delete only the two `tail`-field
|
||||||
|
`skip_serializing_if="is_false"` uses; keep the helper; re-scope
|
||||||
|
its doc-comment to `drop_iterative`.
|
||||||
|
|
||||||
|
> Recon line numbers (agent ad6bb077) are HEAD-at-recon. Drive every
|
||||||
|
> edit off `grep`/`cargo build` E0004/unused, not the literal
|
||||||
|
> numbers (memory: recon misindexing recurs; symbols authoritative).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files this plan creates or modifies
|
||||||
|
|
||||||
|
**Create:**
|
||||||
|
- `bench/it3-oracle/MANIFEST.tsv` + `bench/it3-oracle/<stem>.out` —
|
||||||
|
pre-migration behavioural oracle (Task 1; deleted in Task 6 after
|
||||||
|
it has served its purpose, or kept under `bench/` if cheap — Boss
|
||||||
|
call at commit)
|
||||||
|
- Test: `crates/ailang-check/tests/no_tail_grandfather_pin.rs` —
|
||||||
|
asserts a non-structural non-tail recursion now hard-fails
|
||||||
|
`non-structural-recursion` (the exemption is gone)
|
||||||
|
|
||||||
|
**Modify (HEAD anchors from recon ad6bb077; verify by symbol):**
|
||||||
|
- `crates/ailang-core/src/ast.rs:402–408` (`Term::App.tail`),
|
||||||
|
`:442–447` (`Term::Do.tail`), `:908–916` (`is_false` doc only),
|
||||||
|
`:974–1006` (serde test ctors dropping `tail:`)
|
||||||
|
- `crates/ailang-surface/src/parse.rs:40–55,1203,1207,1224–1227,
|
||||||
|
1274–1317,1368–1395` (EBNF, dispatch, keyword list,
|
||||||
|
parse_tail_app/parse_tail_do, parse_app_body/parse_do_body `tail`
|
||||||
|
param)
|
||||||
|
- `crates/ailang-surface/src/print.rs:417–422,464–466`
|
||||||
|
- `crates/ailang-prose/src/lib.rs:573–597,655–658,782–784,
|
||||||
|
1203–1229` (+ delete tests `:1486–1494,2050,2177`)
|
||||||
|
- `crates/ailang-check/src/lib.rs:558–562` (`TailCallNotInTailPosition`
|
||||||
|
variant), `:781` (`code()` arm), `:2706–2817`
|
||||||
|
(`verify_tail_positions` tail-app arms `:2709–2727`; KEEP Loop
|
||||||
|
`:2803–2808`/Recur `:2815` + `verify_loop_body:2829–2869`),
|
||||||
|
`:3026–3037` (grandfather `if !*tail` `:3027`), `:3197–3199`
|
||||||
|
(no-ADT-candidate skip)
|
||||||
|
- `crates/ailang-codegen/src/lib.rs:2120,2397–2432,2445–2513,
|
||||||
|
2615–2654` (musttail in lower_app/emit_call/emit_indirect_call/
|
||||||
|
lower_effect_op), `crates/ailang-codegen/src/match_lower.rs:658–736`
|
||||||
|
(18g.1 husk-dec — delete)
|
||||||
|
- `docs/DESIGN.md`: Decision 8 `:709–747+` (→ tombstone), EBNF
|
||||||
|
`:552–565` (`tail-app-term`/`tail-do-term`), §Data-model
|
||||||
|
`:2357–2359,2373–2375,2452–2467` (tail jsonc + transitional
|
||||||
|
paragraph). **Decision 3 `:163–176` UNTOUCHED** (it.2 payload).
|
||||||
|
- `crates/ailang-core/specs/form_a.md:268,270,295–298,378–381,
|
||||||
|
405–407,508–539` (tail grammar + §4 tail-rec exhibit)
|
||||||
|
- `crates/ailang-core/tests/design_schema_drift.rs:53–60,87–94`
|
||||||
|
(drop `tail:`), `spec_drift.rs:30–37,64–69` + delete
|
||||||
|
`spec_mentions_tail_variants:331–337`, `schema_coverage.rs`
|
||||||
|
(no tail anchor — verify only)
|
||||||
|
- `examples/*.ail` — the full migration set (enumerated in Task 1)
|
||||||
|
- `crates/ail/tests/e2e.rs` + any IR-snapshot — migrated/retired
|
||||||
|
per Task 1's fallout map
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Pre-migration oracle + migration-set enumeration
|
||||||
|
|
||||||
|
**Files:** `bench/it3-oracle/*` (create), no code change.
|
||||||
|
|
||||||
|
- [ ] **Step 1.1: Build the current `ail` once.**
|
||||||
|
|
||||||
|
Run: `cargo build -p ail 2>&1 | tail -1`
|
||||||
|
Expected: clean build (HEAD = 10a0595, tail-app fully present).
|
||||||
|
|
||||||
|
- [ ] **Step 1.2: Class-(a) — every `(tail-app|tail-do)` fixture.**
|
||||||
|
|
||||||
|
Run: `grep -rl -E '\(tail-app|\(tail-do' examples/ | sort | tee /tmp/it3_class_a.txt | wc -l`
|
||||||
|
Expected: 23 files (recon-confirmed).
|
||||||
|
|
||||||
|
- [ ] **Step 1.3: Class-(b) — no-ADT-candidate counter recursions.**
|
||||||
|
|
||||||
|
Read `crates/ailang-check/src/lib.rs` `verify_structural_recursion`
|
||||||
|
+ the grandfather (`:3027 if !*tail`) + the no-ADT-candidate skip
|
||||||
|
(`:3197–3199 if cand.is_empty() && group.members.is_empty()`).
|
||||||
|
These define exactly what it.2 currently spares. Then sweep every
|
||||||
|
recursive `.ail`/`.ail.json` under `examples/` NOT in
|
||||||
|
`/tmp/it3_class_a.txt`:
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```
|
||||||
|
for f in $(grep -rl -E '\(fn ' examples/ | grep -vFf /tmp/it3_class_a.txt); do
|
||||||
|
cargo run -q -p ail -- check "$f" >/dev/null 2>&1 && echo "CLEAN $f" || echo "ERR $f"
|
||||||
|
done | tee /tmp/it3_sweep.txt
|
||||||
|
```
|
||||||
|
Then, for each `CLEAN` fixture containing a self-recursive fn,
|
||||||
|
classify by inspection against the two exemptions: a fixture that
|
||||||
|
checks clean *only* because (cand is empty: recurses on a
|
||||||
|
primitive/no-ADT param) OR (the rec call is `tail`-marked) is
|
||||||
|
class-(b)/(a). Produce `/tmp/it3_migration_set.tsv` with columns:
|
||||||
|
`stem class(a|b|c) migration(loop|plaincall|already-loop)
|
||||||
|
runnable(main|checkonly)`. Recon-verified seeds: `bench_tree_walk`
|
||||||
|
(`fn build_tree`, class-b pure, no tail-app), `bench_latency_explicit`
|
||||||
|
(`build_tree` class-b + tail-app sites class-a). Class-(c) =
|
||||||
|
`rc_pin_recurse_implicit`, `rc_let_alias_implicit_param` (recon
|
||||||
|
items 1c).
|
||||||
|
|
||||||
|
- [ ] **Step 1.4: Capture the behavioural oracle.**
|
||||||
|
|
||||||
|
```
|
||||||
|
mkdir -p bench/it3-oracle
|
||||||
|
while IFS=$'\t' read stem class mig run; do
|
||||||
|
if [ "$run" = main ]; then
|
||||||
|
cargo run -q -p ail -- run "examples/$stem" > "bench/it3-oracle/$stem.out" 2>&1
|
||||||
|
else
|
||||||
|
cargo run -q -p ail -- check "examples/$stem" > "bench/it3-oracle/$stem.out" 2>&1
|
||||||
|
fi
|
||||||
|
done < /tmp/it3_migration_set.tsv
|
||||||
|
cp /tmp/it3_migration_set.tsv bench/it3-oracle/MANIFEST.tsv
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 1.5: Map existing-test fallout.**
|
||||||
|
|
||||||
|
Run: `grep -rln -E '$(paste -sd"|" <(cut -f1 /tmp/it3_migration_set.tsv))' crates/*/tests crates/*/src bench 2>/dev/null`
|
||||||
|
Record, per migrated fixture, which e2e/round-trip/bench/IR-snapshot
|
||||||
|
test references it (these are the primary oracle per DD-1; they must
|
||||||
|
stay green across migration). Also list the tail-app-specific tests
|
||||||
|
recon named for deletion: `prose` `:1486–1494 app_tail_renders_*`,
|
||||||
|
`:2050 binop_with_tail_flag_*`, `:2177 not_with_tail_flag_*`;
|
||||||
|
`spec_drift::spec_mentions_tail_variants`; any
|
||||||
|
`tail_call_in_tail_position_is_accepted` /
|
||||||
|
`iter14e_*musttail*` e2e.
|
||||||
|
|
||||||
|
- [ ] **Step 1.6: Gate.**
|
||||||
|
|
||||||
|
Expected: `bench/it3-oracle/MANIFEST.tsv` non-empty; every listed
|
||||||
|
fixture has a `.out`; `/tmp/it3_migration_set.tsv` classifies every
|
||||||
|
recursive corpus fixture. No code changed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: Migrate the corpus (tail-app field still present)
|
||||||
|
|
||||||
|
**Files:** every `examples/*.ail` in `MANIFEST.tsv` with
|
||||||
|
`migration ∈ {loop, plaincall}`.
|
||||||
|
|
||||||
|
- [ ] **Step 2.1: Migrate `plaincall` fixtures.**
|
||||||
|
|
||||||
|
For each `migration=plaincall` stem (structurally-decreasing
|
||||||
|
recursion that only carried a redundant `tail-app` marker, e.g.
|
||||||
|
`list_map_poly`, `sort`, `std_list`,
|
||||||
|
`fieldtest/floats_2_average_int_list`): replace `(tail-app f …)`
|
||||||
|
with `(app f …)` (drop the marker only — the recursion is already
|
||||||
|
structural; it.2's checker will verify it total once the grandfather
|
||||||
|
is irrelevant). One stem at a time:
|
||||||
|
|
||||||
|
Run (per stem): `cargo run -q -p ail -- run examples/<stem> | diff - bench/it3-oracle/<stem>.out`
|
||||||
|
(or `ail check` for check-only). Expected: empty diff (byte-identical).
|
||||||
|
|
||||||
|
- [ ] **Step 2.2: Migrate `loop` fixtures.**
|
||||||
|
|
||||||
|
For each `migration=loop` stem (accumulator/counter recursion:
|
||||||
|
`bench_compute_collatz`, `bench_compute_intsum`,
|
||||||
|
`bench_latency_explicit/_implicit`, `bench_list_sum*`,
|
||||||
|
`rc_tail_sum_explicit_leak`, `mut_counter`, `mut_sum_floats`,
|
||||||
|
`fieldtest/floats_1_newton_sqrt`, `fieldtest/forma_1_factorial`,
|
||||||
|
`bench_closure_chain`, `bench_hof_pipeline`, `bench_mono_dispatch`,
|
||||||
|
`ct_1_ordering_signum`, `rc_drop_iterative_long_list`,
|
||||||
|
`test_mono_recursive_fn_bug`, the class-b `build_tree` fixtures,
|
||||||
|
class-c `rc_pin_recurse_implicit`/`rc_let_alias_implicit_param`):
|
||||||
|
rewrite the recursive helper as a `(loop ((var <p> <ty> <init>)…)
|
||||||
|
<body-with-recur>)`. The accumulator/counter params become loop
|
||||||
|
binders; the terminal `return`-value path becomes the non-`recur`
|
||||||
|
exit; the recursive `(tail-app f a b)` / `(app f a b)` becomes
|
||||||
|
`(recur a b)`. Preserve the function's public signature and add
|
||||||
|
`!Diverge` to its effect row (loop-bearing ⇒ Diverge, per it.2;
|
||||||
|
mirror the it.1 `loop_counter.ail` shape + the it.2-migrated
|
||||||
|
`loop_needs_diverge.ail`). One stem at a time, each immediately
|
||||||
|
gated:
|
||||||
|
|
||||||
|
Run (per stem): `cargo run -q -p ail -- check examples/<stem>` →
|
||||||
|
Expected: clean (the loop form passes it.2's checker; `!Diverge`
|
||||||
|
declared).
|
||||||
|
Run (per stem): `cargo run -q -p ail -- run examples/<stem> | diff - bench/it3-oracle/<stem>.out`
|
||||||
|
→ Expected: empty diff. (For RC fixtures the e2e RC==GC guard is the
|
||||||
|
real oracle — see 2.4.)
|
||||||
|
|
||||||
|
- [ ] **Step 2.3: it.1 loop fixtures — confirm unaffected.**
|
||||||
|
|
||||||
|
`loop_counter.ail`, `loop_smoke.ail`, `loop_nested_in_lambda.ail`,
|
||||||
|
`loop_in_lambda_e2e.ail` have no `tail-app`; migration does not
|
||||||
|
touch them. Run their e2e (`loop_counter`→55,
|
||||||
|
`loop_in_lambda`→49) — Expected: still green (sanity).
|
||||||
|
|
||||||
|
- [ ] **Step 2.4: Workspace gate (field still present).**
|
||||||
|
|
||||||
|
Run: `cargo test --workspace 2>&1 | tail -3`
|
||||||
|
Expected: green. Round-trip auto-rechecks every migrated `.ail`
|
||||||
|
(globs `examples/*.ail`). The two RC fixtures' RC==GC e2e guards
|
||||||
|
(`e2e.rs` `rc_pin_recurse_implicit`/`rc_let_alias_implicit_param`)
|
||||||
|
stay green (the regression is in the unchanged `pin`/`pin_aliased`
|
||||||
|
bodies; only the driver became a loop). If a bench fixture's e2e
|
||||||
|
stdout pin moved, that is a migration error — fix the migration to
|
||||||
|
restore byte-identical output, do NOT edit the pin.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: (folded into Task 2 gating — no separate task)
|
||||||
|
|
||||||
|
> Migration correctness is proven by Task 2.4 (green with the
|
||||||
|
> exemptions still present) followed by Task 4 (green with the
|
||||||
|
> exemptions removed). No standalone task; this header is kept so
|
||||||
|
> the numbering matches the journal's task log.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: Remove the it.2 transitional exemptions (completeness gate)
|
||||||
|
|
||||||
|
**Files:** `crates/ailang-check/src/lib.rs`,
|
||||||
|
`crates/ailang-check/tests/no_tail_grandfather_pin.rs` (create).
|
||||||
|
|
||||||
|
- [ ] **Step 4.1: RED — the exemption is gone.**
|
||||||
|
|
||||||
|
Create `crates/ailang-check/tests/no_tail_grandfather_pin.rs`
|
||||||
|
(harness copied verbatim from `structural_recursion_pin.rs`):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[test]
|
||||||
|
fn tail_marked_non_structural_is_now_rejected() {
|
||||||
|
// A tail-marked non-structural self-call must NO LONGER be
|
||||||
|
// grandfathered once it.3 removes the exemption.
|
||||||
|
assert!(
|
||||||
|
check_fixture("test_non_structural_recursion.ail.json")
|
||||||
|
.contains(&"non-structural-recursion".to_string()));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
(Reuse the it.2 negative fixture; if it was tail-marked to exercise
|
||||||
|
the grandfather, this pin asserts the grandfather is dead. If the
|
||||||
|
it.2 fixture is not tail-marked, add a tail-marked sibling
|
||||||
|
`examples/test_tail_marked_non_structural.ail.json` and inventory
|
||||||
|
it in `carve_out_inventory.rs` EXPECTED 20→21.)
|
||||||
|
|
||||||
|
Run: `cargo test --workspace -p ailang-check tail_marked_non_structural_is_now_rejected`
|
||||||
|
Expected: PASS already if fixture untail-marked; FAIL (still
|
||||||
|
grandfathered) if tail-marked — driving the removal.
|
||||||
|
|
||||||
|
- [ ] **Step 4.2: Delete the `tail==false` grandfather.**
|
||||||
|
|
||||||
|
In `verify_structural_recursion`'s rec-call collection
|
||||||
|
(recon `lib.rs:3026–3037`, the `if !*tail {` guard at `:3027`):
|
||||||
|
collect the recursive call **regardless of `tail`**. Delete the
|
||||||
|
guard; the comment blocks `:2919–2927,3007–3014,3260–3262`
|
||||||
|
referencing the grandfather are removed/re-scoped.
|
||||||
|
|
||||||
|
- [ ] **Step 4.3: Delete the no-ADT-candidate skip.**
|
||||||
|
|
||||||
|
Recon `lib.rs:3197–3199`: delete
|
||||||
|
`if cand.is_empty() && group.members.is_empty() { return Ok(()) }`
|
||||||
|
and its rationale comment `:3183–3196`. A no-candidate counter
|
||||||
|
recursion now reaches the `guarded` verdict and emits
|
||||||
|
`NonStructuralRecursion`. The only legitimate early-OKs that remain
|
||||||
|
are `calls.is_empty()` (`:3215`) and `self_clear` (`:3247`).
|
||||||
|
|
||||||
|
- [ ] **Step 4.4: GREEN — the completeness gate (DD-3).**
|
||||||
|
|
||||||
|
Run: `cargo test --workspace 2>&1 | tail -3`
|
||||||
|
Expected: **all green.** This is the proof Task 2 migrated the
|
||||||
|
entire set. If ANY fixture now fires `non-structural-recursion`,
|
||||||
|
that fixture was not fully migrated in Task 2 — fix it in Task 2's
|
||||||
|
migration (re-open 2.1/2.2 for that stem), NOT by restoring the
|
||||||
|
exemption. Re-run until green with zero exemptions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: Remove the tail-app/tail-do machinery + 18g.1 husk-dec
|
||||||
|
|
||||||
|
**Files:** `ast.rs`, `parse.rs`, `print.rs`, `prose/lib.rs`,
|
||||||
|
`check/lib.rs`, `codegen/lib.rs`, `codegen/match_lower.rs`.
|
||||||
|
|
||||||
|
- [ ] **Step 5.1: Delete the AST fields (DD-5).**
|
||||||
|
|
||||||
|
`ast.rs`: remove `Term::App.tail` (`:406–407`) + `Term::Do.tail`
|
||||||
|
(`:445–446`) + their `#[serde]` attrs. **Keep `is_false`**; only
|
||||||
|
delete its two `tail` uses; re-scope its doc-comment
|
||||||
|
(`:908–912`) to mention only `WorkspaceDef.drop_iterative`. Fix
|
||||||
|
serde-roundtrip test ctors (`:974–1006`) that pass `tail:`.
|
||||||
|
|
||||||
|
- [ ] **Step 5.2: Build-driven removal sweep.**
|
||||||
|
|
||||||
|
Run: `cargo build --workspace 2>&1 | grep -E "error|tail" | head -60`
|
||||||
|
Every error is a `tail` read/construct site. Fix each by deleting
|
||||||
|
the `tail` handling, driven off the compiler (authoritative over
|
||||||
|
recon line numbers):
|
||||||
|
- `parse.rs`: delete `parse_tail_app`/`parse_tail_do`, the
|
||||||
|
`"tail-app"`/`"tail-do"` dispatch + keyword-list strings, the
|
||||||
|
EBNF doc lines; collapse `parse_app_body`/`parse_do_body`'s
|
||||||
|
`tail` param.
|
||||||
|
- `print.rs`: `Term::App`/`Term::Do` arms emit only `(app `/`(do `.
|
||||||
|
- `prose/lib.rs`: `as_binop`/`as_unary_not` drop the `tail` guard;
|
||||||
|
render App/Do drop the `"tail "` emit; subst App/Do drop
|
||||||
|
`tail: *tail`. (Free-var counter arms use `..` — no change.)
|
||||||
|
- `check/lib.rs`: delete `CheckError::TailCallNotInTailPosition` +
|
||||||
|
its `code()` arm (no `ctx()` arm exists — verify); in
|
||||||
|
`verify_tail_positions` delete the tail-app arms
|
||||||
|
(`Term::App`/`Term::Do` `if *tail && !is_tail` checks) but **keep
|
||||||
|
the function, the recursive walk skeleton, the `Term::Loop`/
|
||||||
|
`Term::Recur` arms, and `verify_loop_body`** (they are entangled
|
||||||
|
mutual recursion — `verify_loop_body` calls back into
|
||||||
|
`verify_tail_positions` at 7 sites; only the tail-app *arms* go).
|
||||||
|
|
||||||
|
- [ ] **Step 5.3: Delete the 18g.1 husk-dec (DD-4).**
|
||||||
|
|
||||||
|
`match_lower.rs:658–736`: the `arm_body_is_tail_call =
|
||||||
|
matches!(&arm.body, Term::App{tail:true,..}|Term::Do{tail:true,..})`
|
||||||
|
gate (`:697–704`) no longer compiles. Delete the entire 18g.1
|
||||||
|
pre-tail-call shallow-dec block (recon: it is wholly tail-call
|
||||||
|
specific, no recur analogue). Also delete the now-dead `musttail`
|
||||||
|
lowering: `emit_call` `tail` param + `:2427–2431` ret-block,
|
||||||
|
`emit_indirect_call` `:2508–2512`, `lower_effect_op` `:2627`
|
||||||
|
`"tail call"` + `:2650–2653`, `lower_app`'s `tail` param threading.
|
||||||
|
|
||||||
|
- [ ] **Step 5.4: Build + functional GREEN.**
|
||||||
|
|
||||||
|
Run: `cargo build --workspace` → Expected: clean.
|
||||||
|
Run: `cargo test --workspace 2>&1 | tail -3` → Expected: all green.
|
||||||
|
|
||||||
|
- [ ] **Step 5.5: RC-RSS gate (DD-4 — the load-bearing risk).**
|
||||||
|
|
||||||
|
Run the RC-RSS / 18f.2 tail-latency bench pins (the ones
|
||||||
|
`match_lower.rs:674–677` cited):
|
||||||
|
Run: `bash bench/run.sh 2>&1 | tail -20` (or the specific RC-RSS
|
||||||
|
harness; identify it from `match_lower.rs:674–677`'s comment +
|
||||||
|
`bench/`'s RC-RSS script). Compare against `bench/baseline*`.
|
||||||
|
Expected: RC-RSS within the established noise envelope (no RSS
|
||||||
|
regression on the migrated accumulators — loops strand no husk).
|
||||||
|
|
||||||
|
- [ ] **Step 5b (CONDITIONAL — only if 5.5 RED): owned-binder
|
||||||
|
drop-on-recur.**
|
||||||
|
|
||||||
|
If 5.5 shows an RC-RSS regression, it.1 loop codegen is not dropping
|
||||||
|
superseded owned binder values across the `recur` back-edge. Add
|
||||||
|
that drop: in the `Term::Recur` lowering (`codegen/lib.rs:1979–1996`)
|
||||||
|
/ the loop-header phi machinery, before the back-edge `br`, emit an
|
||||||
|
RC dec for each loop binder whose old value is owned and is being
|
||||||
|
replaced by the recur arg (mirror the husk-dec shape 18g.1 used, but
|
||||||
|
keyed on loop-binder ownership, not `tail`). RED test: the failing
|
||||||
|
RC-RSS pin from 5.5. GREEN: 5.5 within envelope. (This is the
|
||||||
|
loop-side of the seam the spec wrongly claimed it.1 fully shipped —
|
||||||
|
in scope for it.3 per the corrected spec §Codegen-rework.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6: DESIGN.md + form_a.md + drift lockstep
|
||||||
|
|
||||||
|
**Files:** `docs/DESIGN.md`, `crates/ailang-core/specs/form_a.md`,
|
||||||
|
`design_schema_drift.rs`, `spec_drift.rs`, `schema_coverage.rs`.
|
||||||
|
|
||||||
|
- [ ] **Step 6.1: DESIGN.md.**
|
||||||
|
|
||||||
|
Decision 8 (`:709–747+`): rewrite as a *superseded tombstone* —
|
||||||
|
keep the heading, replace the body with "Superseded by the
|
||||||
|
iteration-discipline milestone (2026-05-15): explicit tail calls
|
||||||
|
were retired in favour of structural recursion (total by
|
||||||
|
construction under Decision 10) + named `loop`/`recur`. See
|
||||||
|
`docs/specs/2026-05-15-iteration-discipline.md`." EBNF block
|
||||||
|
`:552–565`: remove `tail-app-term`/`tail-do-term` productions.
|
||||||
|
§"Data model" `:2357–2359` (`"app"` tail jsonc + comment),
|
||||||
|
`:2373–2375` (`"do"`), `:2452–2467` (the it.1/it.2 transitional
|
||||||
|
paragraph's tail-app/tail-do + grandfather/retire sentences —
|
||||||
|
reword to "retired in it.3"). **Decision 3 `:163–176` is NOT
|
||||||
|
touched** (it.2 payload; the corrected spec struck the
|
||||||
|
"remove Decision-3 Diverge line" clause).
|
||||||
|
|
||||||
|
- [ ] **Step 6.2: form_a.md.**
|
||||||
|
|
||||||
|
Remove tail grammar `:268,270`; tail prose `:295–298,378–381,
|
||||||
|
405–407`; the §4 "Tail-recursive sum" exhibit `:508–539` (incl.
|
||||||
|
`:527 (tail-app sum_acc …)`) — replace §4 with a `(loop …)`/`recur`
|
||||||
|
worked example (mirror an it.1 loop fixture). Keep loop/recur
|
||||||
|
grammar `:287–288,327–341`.
|
||||||
|
|
||||||
|
- [ ] **Step 6.3: Drift/spec/coverage lockstep.**
|
||||||
|
|
||||||
|
`design_schema_drift.rs:53–60,87–94`: drop `tail:false` from the
|
||||||
|
App/Do ctors. `spec_drift.rs:30–37,64–69`: drop `tail:false`;
|
||||||
|
**delete `spec_mentions_tail_variants` (`:331–337`)** (it asserts
|
||||||
|
the form_a spec contains `tail-app`/`tail-do` — now false by
|
||||||
|
design). `schema_coverage.rs`: no `tail` tag exists; verify
|
||||||
|
`TermLoop`/`TermRecur` anchors untouched.
|
||||||
|
|
||||||
|
Run: `cargo test --workspace -p ailang-core design_schema_drift spec_drift schema_coverage`
|
||||||
|
Expected: green; `loop`/`recur` anchors present, zero `tail`
|
||||||
|
anchors.
|
||||||
|
|
||||||
|
- [ ] **Step 6.4: Delete the tail-app-specific tests (Task 1.5 list).**
|
||||||
|
|
||||||
|
Delete `prose` `app_tail_renders_with_keyword`
|
||||||
|
(`:1486–1494`), `binop_with_tail_flag_keeps_prefix_form` (`:2050`),
|
||||||
|
`not_with_tail_flag_keeps_prefix_form` (`:2177`); any
|
||||||
|
`tail_call_in_tail_position_is_accepted` /
|
||||||
|
`tail_call_in_non_tail_position_is_rejected` (the
|
||||||
|
`TailCallNotInTailPosition` pins — the diagnostic is gone); the
|
||||||
|
`iter14e_*musttail*` e2e. Each deletion is justified: the behaviour
|
||||||
|
it pinned was deliberately removed.
|
||||||
|
|
||||||
|
Run: `cargo test --workspace 2>&1 | tail -3` → Expected: all green.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 7: Acceptance gate
|
||||||
|
|
||||||
|
**Files:** none (verification only).
|
||||||
|
|
||||||
|
- [ ] **Step 7.1: No exemption remains (spec it.3 acceptance).**
|
||||||
|
|
||||||
|
Run: `grep -n "if !\*tail\|cand.is_empty() && group" crates/ailang-check/src/lib.rs || echo "NONE — exemptions gone"`
|
||||||
|
Expected: `NONE`. The `tail==false` grandfather and the
|
||||||
|
no-ADT-candidate skip are gone.
|
||||||
|
|
||||||
|
- [ ] **Step 7.2: No tail-app concept remains.**
|
||||||
|
|
||||||
|
Run: `grep -rn "tail-app\|tail-do\|TailCallNotInTailPosition\|musttail\|\.tail\b" crates/ --include=*.rs | grep -v "block_terminated\|loop\|recur\|drop_iterative" | head`
|
||||||
|
Expected: empty (or only unrelated `.tail` like iterator methods —
|
||||||
|
inspect each; zero AILang-tail-call references).
|
||||||
|
Run: `grep -rl -E '\(tail-app|\(tail-do' examples/` → Expected: empty.
|
||||||
|
|
||||||
|
- [ ] **Step 7.3: Behavioural identity.**
|
||||||
|
|
||||||
|
For every `MANIFEST.tsv` stem: re-run and diff against
|
||||||
|
`bench/it3-oracle/<stem>.out`. Expected: every diff empty
|
||||||
|
(byte-identical observable behaviour pre/post migration).
|
||||||
|
|
||||||
|
- [ ] **Step 7.4: Full workspace + design-drift.**
|
||||||
|
|
||||||
|
Run: `cargo test --workspace 2>&1 | tail -3` → Expected: all green.
|
||||||
|
Run: `cargo build --workspace` → Expected: clean (no dead-code
|
||||||
|
warnings from half-removed tail machinery).
|
||||||
|
|
||||||
|
- [ ] **Step 7.5: Spec it.3 acceptance bullets, itemised.**
|
||||||
|
|
||||||
|
Verify each: `Term::App.tail`/`Term::Do.tail`/both keywords/
|
||||||
|
`musttail`/`verify_tail_positions` tail-app role/
|
||||||
|
`TailCallNotInTailPosition`/Decision 8 all removed (7.2 + grep);
|
||||||
|
`design_schema_drift` green with `loop`/`recur` anchors and no
|
||||||
|
`tail` anchors (6.3); every migration-set fixture `ail check` clean
|
||||||
|
+ `ail run` byte-identical (7.3); no grandfather/no-candidate skip
|
||||||
|
(7.1); `cargo test --workspace` green (7.4). Note for the
|
||||||
|
milestone-close `audit`: bench may need re-ratification if
|
||||||
|
loop-header codegen diverged from the retired `musttail` (5.5
|
||||||
|
result + any 5b remediation is the evidence the audit consumes).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-review (planner Step 5)
|
||||||
|
|
||||||
|
1. **Spec coverage.** §Components it.3 §Remove → T5 + T6.1/6.2;
|
||||||
|
§Codegen-rework (incl. the corrected 18g.1/RC-RSS contingency)
|
||||||
|
→ T5.3/5.5/5b; §Corpus-migration (full corrected set) → T1
|
||||||
|
(enumeration+oracle) + T2; the two exemptions → T4; Decision-8
|
||||||
|
tombstone + EBNF + §Data-model + form_a.md + drift → T6;
|
||||||
|
§Acceptance it.3 (incl. "no grandfather/no-candidate skip
|
||||||
|
remains") → T7. Decision 3 explicitly NOT touched (corrected
|
||||||
|
spec). Every it.3 spec clause has a task.
|
||||||
|
2. **Placeholder scan.** No "TBD/TODO/implement later/similar to/
|
||||||
|
add appropriate". The build-driven removal (T5.2) is an exact
|
||||||
|
method (compiler enumerates sites), not a placeholder — same
|
||||||
|
device the planner memory mandates over recon line numbers. Per
|
||||||
|
memory `plan-pseudo-vs-reality`: no prose round-trip asserting
|
||||||
|
AST-equality is scripted; no `diagnostic.rs` doc-list claim
|
||||||
|
(T5.2 says delete the `code()` arm, explicitly notes no `ctx()`
|
||||||
|
arm exists). Recon lines flagged drift-prone in the DD preamble.
|
||||||
|
3. **Type/name consistency.** `Term::App.tail`/`Term::Do.tail`,
|
||||||
|
`is_false` (kept), `verify_tail_positions`/`verify_loop_body`
|
||||||
|
(kept, arms deleted), `TailCallNotInTailPosition` (deleted),
|
||||||
|
18g.1/`arm_body_is_tail_call`, `bench/it3-oracle/`, the
|
||||||
|
exemption greps — consistent across tasks + files section.
|
||||||
|
4. **Step granularity.** Each step is one action (one grep, one
|
||||||
|
fixture migration with its gate, one symbol-class removal). The
|
||||||
|
per-stem migration loops in T2 are explicitly "one stem at a
|
||||||
|
time, each immediately gated".
|
||||||
|
5. **No commit steps.** None. Working tree only; Boss commits the
|
||||||
|
whole it.3 diff at iter end.
|
||||||
|
|
||||||
|
Recorded risk (named, decision rule — not a placeholder): the
|
||||||
|
class-(b) enumeration (T1.3) is the one step that cannot be fully
|
||||||
|
pre-scripted (the spec + it.2 journal §1 explicitly delegate it to
|
||||||
|
a live sweep). The method is exact (read the two exemptions, sweep,
|
||||||
|
classify by the shape they spare). If the sweep reveals a fixture
|
||||||
|
shape that is neither cleanly loop-able nor structural (a genuine
|
||||||
|
non-structural recursion with no natural loop form), that is a
|
||||||
|
real finding → it surfaces as a Task-2 BLOCKED to the Boss with the
|
||||||
|
fixture named, not a silent reshape (memory: don't adapt fixtures
|
||||||
|
to dodge — but here the fixture SHOULD become a loop; a fixture
|
||||||
|
that resists looping is a spec-signal worth surfacing).
|
||||||
Reference in New Issue
Block a user