Files
AILang/docs/plans/0073-iter-it.3.md
T
Brummel 832375f2ac convention: counter-prefix file naming across docs/specs/, docs/plans/, design/contracts/, design/models/
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.
2026-05-28 13:31:31 +02:00

561 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# it.3 — retire tail-app/tail-do + corpus migration — Implementation Plan
> **Parent spec:** `docs/specs/0031-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 14; 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:658736`) — 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:908916`) 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:402408` (`Term::App.tail`),
`:442447` (`Term::Do.tail`), `:908916` (`is_false` doc only),
`:9741006` (serde test ctors dropping `tail:`)
- `crates/ailang-surface/src/parse.rs:4055,1203,1207,12241227,
12741317,13681395` (EBNF, dispatch, keyword list,
parse_tail_app/parse_tail_do, parse_app_body/parse_do_body `tail`
param)
- `crates/ailang-surface/src/print.rs:417422,464466`
- `crates/ailang-prose/src/lib.rs:573597,655658,782784,
12031229` (+ delete tests `:14861494,2050,2177`)
- `crates/ailang-check/src/lib.rs:558562` (`TailCallNotInTailPosition`
variant), `:781` (`code()` arm), `:27062817`
(`verify_tail_positions` tail-app arms `:27092727`; KEEP Loop
`:28032808`/Recur `:2815` + `verify_loop_body:28292869`),
`:30263037` (grandfather `if !*tail` `:3027`), `:31973199`
(no-ADT-candidate skip)
- `crates/ailang-codegen/src/lib.rs:2120,23972432,24452513,
26152654` (musttail in lower_app/emit_call/emit_indirect_call/
lower_effect_op), `crates/ailang-codegen/src/match_lower.rs:658736`
(18g.1 husk-dec — delete)
- `docs/DESIGN.md`: Decision 8 `:709747+` (→ tombstone), EBNF
`:552565` (`tail-app-term`/`tail-do-term`), §Data-model
`:23572359,23732375,24522467` (tail jsonc + transitional
paragraph). **Decision 3 `:163176` UNTOUCHED** (it.2 payload).
- `crates/ailang-core/specs/form_a.md:268,270,295298,378381,
405407,508539` (tail grammar + §4 tail-rec exhibit)
- `crates/ailang-core/tests/design_schema_drift.rs:5360,8794`
(drop `tail:`), `spec_drift.rs:3037,6469` + delete
`spec_mentions_tail_variants:331337`, `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
(`:31973199 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` `:14861494 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:30263037`, the `if !*tail {` guard at `:3027`):
collect the recursive call **regardless of `tail`**. Delete the
guard; the comment blocks `:29192927,30073014,32603262`
referencing the grandfather are removed/re-scoped.
- [ ] **Step 4.3: Delete the no-ADT-candidate skip.**
Recon `lib.rs:31973199`: delete
`if cand.is_empty() && group.members.is_empty() { return Ok(()) }`
and its rationale comment `:31833196`. 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` (`:406407`) + `Term::Do.tail`
(`:445446`) + their `#[serde]` attrs. **Keep `is_false`**; only
delete its two `tail` uses; re-scope its doc-comment
(`:908912`) to mention only `WorkspaceDef.drop_iterative`. Fix
serde-roundtrip test ctors (`:9741006`) 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:658736`: the `arm_body_is_tail_call =
matches!(&arm.body, Term::App{tail:true,..}|Term::Do{tail:true,..})`
gate (`:697704`) 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 + `:24272431` ret-block,
`emit_indirect_call` `:25082512`, `lower_effect_op` `:2627`
`"tail call"` + `:26502653`, `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:674677` cited):
Run: `bash bench/run.sh 2>&1 | tail -20` (or the specific RC-RSS
harness; identify it from `match_lower.rs:674677`'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:19791996`)
/ 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 (`:709747+`): 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/0031-iteration-discipline.md`." EBNF block
`:552565`: remove `tail-app-term`/`tail-do-term` productions.
§"Data model" `:23572359` (`"app"` tail jsonc + comment),
`:23732375` (`"do"`), `:24522467` (the it.1/it.2 transitional
paragraph's tail-app/tail-do + grandfather/retire sentences —
reword to "retired in it.3"). **Decision 3 `:163176` 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 `:295298,378381,
405407`; the §4 "Tail-recursive sum" exhibit `:508539` (incl.
`:527 (tail-app sum_acc …)`) — replace §4 with a `(loop …)`/`recur`
worked example (mirror an it.1 loop fixture). Keep loop/recur
grammar `:287288,327341`.
- [ ] **Step 6.3: Drift/spec/coverage lockstep.**
`design_schema_drift.rs:5360,8794`: drop `tail:false` from the
App/Do ctors. `spec_drift.rs:3037,6469`: drop `tail:false`;
**delete `spec_mentions_tail_variants` (`:331337`)** (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`
(`:14861494`), `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).