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.
28 KiB
SUPERSEDED 2026-05-16 by
docs/specs/0032-iteration-discipline-revert.md. This milestone was reverted in full (it.1 + it.2 backed out forward; it.3 never ran). It is retained as a historical record of the attempt and the design-thesis gap it surfaced; it is not the current iteration-story design. Do not plan or implement from this file.
Iteration discipline — Design Spec
Date: 2026-05-15 Status: Draft — awaiting user spec review Authors: Brummel (orchestrator) + Claude
Goal
Give AILang exactly two iteration forms and remove the third:
-
Structural recursion — a recursive call permitted iff it is structurally guarded (recurses on a constructor-smaller component of an algebraic argument). Under Decision 10 (ADTs are acyclic by construction) such recursion is total by construction: it terminates because the value it descends cannot be infinite. It stays a plain call, pure, effect-free, and is the most LLM-natural correct iteration pattern (tree / list / AST / JSON match-and-recurse — saturated in every FP corpus).
-
Named
(loop …)+recur— the single explicit backward jump, lexically scoped to an enclosing named loop. Every repetition that is not structurally decreasing (accumulator loops that are not structural, counted iteration, state machines, streaming) is expressed here, and only here. -
tail-app/tail-do— retired. The marked-tail-call concept (Term::App { tail: bool },Term::Do { tail: bool }, the two surface keywords,musttaillowering,verify_tail_positions's old role,CheckError::TailCallNotInTailPosition, Decision 8) is removed. A call is either structural recursion (a plain call, checked total) or it isloop/recur. No marked-tail-call concept survives.
This discharges fieldtest finding F1 (mut-local's own stated
motivation — "removes friction from accumulators that today need
tail-recursive helpers" — was structurally unmet, because without a
loop the only iteration mechanism remained the tail-recursive
helper). It closes the "forgot to mark a recursive call as tail →
silent stack overflow" trap completely: a non-structural
recursion-by-call is now a named compile error, not a runtime crash.
It unifies with Decision 10: acyclic values and an acyclic
implicit call graph — the only cycle anywhere in a program is an
explicit, named, typed loop. Fieldtest finding F4 (DESIGN.md
never named the tail-rec fallback) is absorbed: there is no tail-rec
fallback to document anymore.
Architecture
Three coupled mechanisms, shipped as three ordered iterations. The ordering is forced by a codegen invariant, not by convenience:
- The codegen block-termination invariant (
emit_call/emit_indirect_call/lower_effect_opsetself.block_terminated = trueonly ontail: true;match_lower.rspre-tail-call shallow-dec and thelib.rs:1238–1577branch-termination bookkeeping depend on it) meanstail: truecurrently owns the "this block ends with a self-terminating control transfer" seam.recuris precisely a self-terminating backward control transfer. Sorecurmust occupy that seam beforetailcan vacate it.
Iteration order:
-
it.1 —
loop/recur, additive. NewTerm::Loop/Term::RecurAST + schema/canonical + Form-A surface + round-trip fixture + checker (scope + tail-position rules forrecur) + codegen (loop-header block with phi binders;recur= backward branch occupying a new block-termination seam, alongside the still-presenttailseam). Nothing is removed. End state: a workingloop/recurandtail-appcoexist. -
it.2 — structural-guardedness checker +
Divergeeffect. The recursion-guardedness analysis +CheckError::NonStructuralRecursion; and the first real implementation of the Decision-3Divergeeffect (anyFnDefwhose body contains aTerm::Loop, or calls aDivergecallee, must declareDiverge). End state: structural recursion is pure+total; non-structural recursion-by-call is a compile error directing the author toloop;loop-bearing functions carry!Diverge.tail-appstill exists but is now redundant with structural recursion. -
it.3 — retire
tail-app/tail-do+ migrate corpus. Remove thetailfields, the two keywords, themusttaillowering,verify_tail_positions's old role,TailCallNotInTailPosition, Decision 8; rework the codegen block-termination invariant so the seam is solelyrecur's; migrate the full corpus non-structural- recursion set (scope-corrected — see note). Destructive; last; depends on it.1 (loop occupies the seam) and it.2 (the structural fixtures are proven total before their redundant tail marker is stripped).Scope correction (it.2-surfaced). The original estimate "21
.ailfixtures / 34(tail-app …)sites" under-counted the corpus's non-structural-recursion surface. The true it.3 migration set is three classes: (1) the ~20tail-app-marked fixtures; (2) the ~18 no-ADT-candidate counter-recursion fixtures (e.g.build_tree(depth: Int)inbench_latency_explicit.ail) that it.2 deferred via the no-candidate skip; (3) the two RC-regression fixtures (rc_pin_recurse_implicit.ail,rc_let_alias_implicit_param.ail) that it.2 joined to the transitionaltail-appgrandfather. it.3 removes thetail==falsegrandfather AND the no-ADT- candidate skip together — after it.3 every non-structural recursion isloop/recur. Seedocs/journals/2026-05-15-iter-it.2.md§Concerns §1/§2 for the discovery; the per-fixture enumeration is the it.3 plan's recon job (drive it off a corpuscargo build/ail checksweep, not this estimate).
First-iteration planner scope: it.1 in full.
Components
Per-construct touch-point set follows the mut-local milestone
template (docs/specs/0029-mut-local.md §Components): AST+serde
→ schema-drift + DESIGN.md §Data-model anchor → Form-A surface
parse+print → round-trip fixture → check arm + diagnostics → codegen
lower + escape → e2e example.
it.1 — loop / recur
- AST (
crates/ailang-core/src/ast.rs).Term::Loop { binders: Vec<LoopBinder>, body: Box<Term> };Term::Recur { args: Vec<Term> };struct LoopBinder { name: String, #[serde(rename="type")] ty: Type, init: Box<Term> }. Serde round-trip unit tests alongside the existingTerm::Mutones (ast.rs ~899–940). New variants require new anchors incrates/ailang-core/tests/design_schema_drift.rs(Term::Loop → "loop",Term::Recur → "recur") and matching §"Data model" entries indocs/DESIGN.mdin the same iteration, or the drift test fails. - Form-A surface. No lexer change —
loop/recurare head-idents exactly asmut/var/assignare (confirmed: mut.1 needed nolex.rschange).crates/ailang-surface/src/parse.rs: EBNF block, dispatch arm,parse_loop/parse_recur, keyword list.crates/ailang-surface/src/print.rs:Term::Loop/Term::Recurarms.crates/ailang-prose/src/lib.rs: read + rebuild arms (the prose projection is a lockstep member with parse/print/serde). - Round-trip. New
examples/loop_*.ailfixture(s) — auto-discovered bycrates/ailang-surface/tests/round_trip.rs(globsexamples/*.ail); this is the round-trip gate. - Check (
crates/ailang-check/src/lib.rs).Term::Looptyped by typing each binder'sinit, bindingname:tyin a fresh frame, typingbody(the loop's type is the body's type, and everyrecursite must unify with the binder vector).Term::Recurarity + per-position type must match the lexically-enclosingTerm::Loop's binders. New diagnostics:RecurOutsideLoop(arecurwith no enclosingloop),RecurArityMismatch,RecurTypeMismatch.recurmust be in tail position of the enclosing loop body —verify_tail_positionsis repurposed: from it.1 on, the tail-position pass also enforces "everyrecuris in tail position of its enclosing loop"; the diagnostic for a mis-placedrecurisRecurNotInTailPosition(it.3 removes the oldtail-app-flavoured role of the same pass — see it.3). The Term-walker arms (desugar, lift, mono, theMutVarCapturedByLambda-style free-var helpers) all gainTerm::Loop/Term::Recurcases (~25 walker sites, mirroring the mut.1 blast radius). - Codegen (
crates/ailang-codegen/src/lib.rs,escape.rs).Term::Loop→ an LLVM loop-header basic block with onephiper binder (entry edge =init, back edge =recurargs); body lowered with the header in scope.Term::Recur→ evaluate args, emit the back-edgebrto the header, setself.block_terminated = true. This is a new block-termination seam, added alongside the existingtail: trueseam (no existing codegen path is modified in it.1).escape.rsgainsTerm::Loop/Term::Recurescape-walk arms. - e2e.
examples/loop_counter.ail(accumulator counter that today would be atail-apphelper) runs and prints the same value the equivalent tail-rec helper would.
it.2 — guardedness checker + Diverge
- Structural-guardedness analysis (
crates/ailang-check). A new pass over each self- or mutually-recursiveFnDef/LetRecclause. No annotation — the structural argument position is found by analysis (see Design decision D1 for why implicit, not an explicit{struct}marker). For each recursive call, the argument in the candidate structural position must be a proper constructor sub-component of the function's value at that position, established bymatch-pattern destructuring of that parameter. Accumulator arguments (any non-structural position) are unconstrained. Mutual recursion uses the conservative same-ADT-family rule (D1). Failure →CheckError::NonStructuralRecursionnaming the unguarded call and directing the author to(loop …)/recur. Transitional grandfather (it.2-only): a recursive call that is not structurally guarded but is markedtail: trueis exempt from the rejection. During the it.2→it.3 window a marked tail call is precisely the author's explicit "this is intended iteration, not accidental unbounded recursion" assertion — its original Decision-8 purpose — so grandfathering it keeps the it.1/it.2-additive vs it.3-destructive split honest: the corpus still type-checks through it.2 unchanged. (it.2 discovered the grandfather needs two cooperating exemptions, not one — thetail==falsemarker check and a no-ADT-candidate skip for counter-recursions that have no structural position to verify; see the Scope-correction note under Architecture/it.3.) it.3 removes thetailfield, thetail==falsegrandfather, and the no-ADT-candidate skip in the same destructive step, by which point every previously-exempt site has been migrated toloopor to a now-checked structural plain call. Divergeeffect (first implementation; Decision 3 was a placeholder). TheFnDefeffect-collection site (where raised effects are reconciled with the declaredType::Fn.effectsrow) gains: if the body syntactically contains aTerm::Loop, OR calls a function whose declared effect set contains"Diverge", then"Diverge"must be in the declared row, elseCheckError::UndeclaredEffect(existing diagnostic, existing machinery —Divergerides the sameVec<String>effect row asIO). Structural recursion contributes no effect. See D2.- DESIGN.md. Decision 3 amended:
Divergeis no longer "wired up in the MVP only nominally" — it is the effect ofloop-bearing code. §"Feature-acceptance criterion" already carries the canonical worked example (the iteration story) from the 2026-05-15 edit; this iteration makes the prose match the implementation.
it.3 — retire tail-app / tail-do
-
Remove.
Term::App.tail,Term::Do.tail(the twotailfields only — not theis_falseserde helper: it.1/it.2-era recon foundis_falseis shared withWorkspaceDef.drop_iterative; it survives, only its doc-comment is re-scoped);tail-app/tail-dodispatch +parse_tail_app/parse_tail_do+ the keyword-list entries + theprint.rs(tail-app …)/(tail-do …)arms + the prose"tail "projection (render + subst arms; the free-var counter arms are alreadytail-agnostic);verify_tail_positions's tail-app role (the pass keeps only therecur-in-tail-position role from it.1 — noteverify_tail_positionsandverify_loop_bodyare entangled mutual recursion: the tail-app arms are deleted in place, the functions are not removed);CheckError::TailCallNotInTailPosition(+ itscode()arm; it has no dedicatedctx()arm — it falls into the catch-all, so noctx()edit); themusttaillowering inemit_call/emit_indirect_calland thetail callhint inlower_effect_op. DESIGN.md Decision 8 rewritten as superseded (kept as a tombstone explaining why explicit tail calls were retired — the schema-drift anchors and EBNF/jsonc blocks updated in lockstep); the EBNFtail-app-term/tail-do-termproductions removed.Correction (it.3-recon-surfaced). The original draft also said "the Decision-3
Divergeline removed". That clause is struck: it.2 already rewrote Decision 3 to makeDivergethe real effect ofloop-bearing code — that rewrite is the milestone's central payload and stays verbatim. it.3 touches Decision 8 only (tombstone), never Decision 3. -
Codegen block-termination rework + the 18g.1 husk-dec deletion (the real non-mechanical part). Two coupled things, only one of which the original draft named:
block_terminatedseam. Only three setters are tail-driven (emit_call,emit_indirect_call,lower_effect_op) — not "seven"; the it.1 journal's "seven" was stale. They are removed with themusttaillowering. Every otherblock_terminatedconsumer (lib.rsfn-body fall-throughret, Let-scope binder dec, if-branch join bookkeeping;match_lower.rsarm-close/phi gates;lambda.rssave/restore) isblock_terminated-mediated andrecur's it.1 setter feeds the identical signal — these re-verify clean.- 18g.1 pre-tail-call shallow husk-dec
(
match_lower.rs:658–736). This is the one consumer that readsTerm::App{tail:true}DIRECTLY, not viablock_terminated. It is an RC-correctness optimisation written to fix the 18f.2 tail-latency RC-RSS pin (amusttail call … retstrands the moved-from scrutinee's outer cons-cell husk; 18g.1 shallow-decs it pre-emptively). Removing thetailfield forces this block's deletion (it no longer compiles). The original spec framed the codegen rework as "solely theblock_terminatedseam" and missed this.recuris a back-edgebr(nomusttail, no frame-exitret) so it strands no husk iff it.1's loop codegen drops superseded owned binder values across the back-edge. The spec's "it.1 ships the seam first" claim is verified for theblock_terminatedhalf but is unverified for the owned-binder-drop half. it.3 therefore: (i) deletes 18g.1 (mechanically forced); (ii) re-runs the 18f.2 / RC-RSS bench pins post-migration as the gate; (iii) if RED, the remediation is owned-loop-binder-drop-on-recuradded to loop codegen — this is in scope for it.3 (it is the loop-side of the seam the spec wrongly claimed it.1 fully shipped), not a reason to keep 18g.1 (which cannot survive the field removal). This contingency + its named remediation is the load-bearing risk of the milestone; the it.3 plan carries it as a RED-gated step, not a footnote.
-
Corpus migration (full non-structural set — see the Scope-correction note; ~20 tail-app + ~18 no-ADT-candidate + the 2 RC fixtures, exact set is the it.3 plan's recon job). Accumulator-shaped recursions (
bench_compute_collatz,bench_compute_intsum,bench_latency_explicit/_implicit,bench_list_sum*,rc_tail_sum_explicit_leak,mut_counter,mut_sum_floats,floats_1_newton_sqrt,forma_1_factorial, …) →(loop …)/recur. Structurally-decreasing recursions (list_map_poly,sort,std_list,floats_2_average_int_list) → plain calls (now total-checked by it.2). Each migrated fixture must stillail checkclean andail runto byte-identical stdout (behavioural pin). Bench fixtures migrate too; the milestone-close audit re-ratifiesbench/baseline*if loop-header codegen diverges from the retiredmusttailself-call (expected equivalent-or-better: a header block + phi is at least as good as amusttailself-call, and removes the call-frame entirely).
Data flow
.ail source → ailang_surface::parse → JSON-AST (Term::Loop /
Term::Recur are first-class canonical nodes, hashable, round-trip
gated) → ailang-check:
- typecheck (binders typed;
recurunified with enclosing loop) → - tail-position pass (
recur-in-tail-position; from it.3 this is the pass's only job) → - structural-guardedness pass (it.2) — rejects non-structural recursion-by-call →
- effect reconciliation (it.2) —
loop-bearing orDiverge-calling ⇒Divergein declared row.
→ desugar / lift / mono (walker arms thread Term::Loop/Recur
unchanged) → ailang-codegen (Term::Loop = header block + phi;
Term::Recur = back-edge br + block-terminate) → LLVM text →
clang -O2.
The acyclic-call-graph invariant: after it.2, the only back-edge in
generated IR comes from a Term::Recur to its lexically-paired
Term::Loop header. Every other call is forward and (for recursive
defs) structurally decreasing — i.e. statically known to bottom out.
Error handling
New CheckError variants (bracket-[code]:-free Display body, per
the 2026-05-15 F2 bugfix convention — the CLI formatter prepends the
[code]; the Display string must not embed it):
NonStructuralRecursion— "recursive call tofis not on a structurally-smaller argument; express this iteration as(loop …)/recur". Carries the call site and the offending argument.RecurOutsideLoop— arecurwith no lexically-enclosingloop.RecurArityMismatch—recurarg count ≠ enclosing loop binder count.RecurTypeMismatch—recurarg type ≠ corresponding binder type.RecurNotInTailPosition—recurnot in tail position of the enclosing loop body. (Replaces the role of the retiredTailCallNotInTailPosition.)
UndeclaredEffect (existing) is reused for the
Diverge-not-declared case — no new variant; Diverge is just
another effect-row string.
it.3 removes TailCallNotInTailPosition and its code()/ctx()
arms; the schema-drift / diagnostic-doc lists update in lockstep.
Testing strategy
- it.1: serde round-trip unit tests for
Term::Loop/Recur;round_trip.rsauto-picks newexamples/loop_*.ail; check-side pins forRecurOutsideLoop/RecurArityMismatch/RecurTypeMismatch/RecurNotInTailPosition(negative fixtures, carve-out inventory extended); a positive nested-loop + loop-inside-lambda fixture (mirrors the mut.3 lambda-boundary concern — loop allocas/phi must scope to the closure's own header); e2eexamples/loop_counter.ailruns and prints the expected value. - it.2: positive pins — structural list/tree/JSON walks check
clean and stay
Diverge-free; accumulator-over-structural (foldl shape) checks clean as structural (D1); negative pins — non-structural recursion-by-call firesNonStructuralRecursion; aloop-bearing fn without!Divergein its row firesUndeclaredEffect; mutual same-family recursion passes, mutual cross-family fails. The mut-local e2e fixtures (mut_counter/mut_sum_floats) are not migrated yet in it.2 ( they still usetail-app); they migrate in it.3. - it.3: every one of the 21 migrated fixtures keeps a
before/after stdout-equality pin;
cargo test --workspacegreen;design_schema_drift.rsgreen withtailanchors removed andloop/recuranchors present; milestone-closeauditrunsbench/(re-ratify baseline only if loop codegen measurably differs from the retiredmusttailpath, with the regression ratified as a known accepted delta the way mut.4-tidy ratified the typecheck-surface tax).
Design decisions & rejected alternatives
D1 — Structural guardedness is inferred, not annotated; the accumulator-carrying walk is structural; mutual recursion uses the conservative same-family rule.
- Inferred, not annotated. No
{struct n}-style marker. Rationale (clause 1 + clause 2): an LLM author does not annotate the decreasing argument — there is no FP-corpus precedent for it outside Coq/Agda, which are not in the LLM-natural distribution; a required annotation would be exactly the redundancy the language exists to remove (clause 2), and the analysis is local and cheap (walkmatch-pattern bindings of one parameter). Rejected: explicit decreasing-argument annotation — rejected because it fails clause 1 (LLM would not write it unprompted) and adds redundancy (clause 2), not because it is more work. - Accumulator-carrying structural walk = structural recursion
(plain, pure, total). Only the structural argument must decrease;
accumulator positions are free. Forced by coherence with D2: a
foldl/sum-with-accumulator over a list is the single most LLM-natural iteration shape in the entire FP corpus; classifying it as non-structural would push the whole functional-iteration corpus throughloop/recurand!Diverge— self-defeating. The accumulator is not the recursion's well-founded measure; the structural argument is, and Decision 10's acyclicity bounds the depth regardless of accumulator growth. This resolves the load-bearing open question (2) from the roadmap entry. - Mutual recursion = conservative same-ADT-family rule. A
mutually-recursive group is total iff there is a single ADT family
F such that every member has a structural parameter of a type in F
and every recursive call (self or cross-group) passes, at the
callee's structural position, a proper sub-term of the caller's
structural parameter. Rejected for this milestone: a general
lexicographic / size-measure ordering — deferred, named in the
spec as out of scope. The same-family rule covers tree/forest,
even/odd-over-Nat, and the mutual JSON walk (the LLM-natural set);
anything outside it must use
loop/recur. This is a deliberate, recorded conservative scope cut, not an oversight. This resolves the load-bearing open question (1) from the roadmap entry.
D2 — loop/recur carries the Diverge effect; structural
recursion stays pure+total. (The genuine fork.)
- Chosen (Option B): non-structural recursion-by-call is a hard
compile error (
NonStructuralRecursion); the only way to write an unbounded cyclic computation isloop/recur; and anyFnDefwhose body contains aTerm::Loop(or calls aDivergecallee) must declareDivergein its effect row. This is the first real implementation of the Decision-3Divergeplaceholder.- Rationale, from the language: local reasoning — "every
definition carries its full type and effect set, so a signature
can be trusted without reading the body" — is AILang's
load-bearing pillar after machine-readability. "Can this function
fail to terminate?" is precisely a body-fact the effect row
exists to lift to the signature.
!IOalready does this for observable effects;!Divergefor non-termination is the same move, andloopis the first construct that gives a free syntactic hook (no termination prover needed — the check is "does the body contain aTerm::Loop"). It also makes clause 3 self-reinforcing: structural recursion stays pure and total, so an LLM author who wants a clean (!IO-free,!Diverge-free) signature is structurally pulled toward structural recursion and pays!Divergeonly when genuinely writing an unbounded loop. That behavioral nudge is the milestone's reason to exist.
- Rationale, from the language: local reasoning — "every
definition carries its full type and effect set, so a signature
can be trusted without reading the body" — is AILang's
load-bearing pillar after machine-readability. "Can this function
fail to terminate?" is precisely a body-fact the effect row
exists to lift to the signature.
- Rejected (Option A):
loop/recureffect-free;Divergedeleted from DESIGN.md entirely; non-termination simply localized to theloopkeyword with no signature consequence. Rejected because it discards local reasoning about termination at exactly the momentloopmakes it free to provide — a caller could no longer tell from a signature whether a callee may spin, violating the local-reasoning pillar. Effort is not the reason; the local-reasoning contract is. - Conservativeness note: every
loop-bearing fn gets!Divergeeven if itsrecurhappens to be structurally bounded. This is a sound over-approximation and intentional: proving a generalloopterminates is the totality analysis we deliberately do not build. The author who wants the pure+total guarantee writes structural recursion; the author who reaches forloopis telling the compiler "termination is mine to argue", and the signature says so. - Transitional note: the "hard compile error" lands in it.2 with a
single, deliberately temporary exemption — a still-
tail: true-marked recursive call is grandfathered until it.3 removes thetailfield and the exemption together. See it.2 Components ("Transitional grandfather"). This is what lets it.1/it.2 stay additive and confines all corpus breakage to the one destructive iteration (it.3).
D3 — loop is Clojure-shaped (named binders + positional
recur), recur is tail-position-only and lexically scoped.
recur re-enters the lexically-enclosing loop only (never an
implicit fn head — Clojure permits the implicit form; AILang does
not, for the same no-outward-inference reason that originally
motivated explicit tail-app: the jump target must be syntactically
present, not inferred from call position). Tail-position-only keeps
the back-edge the last thing a loop iteration does, which is what
makes the phi-node codegen well-defined and the construct a true
constant-stack loop.
D4 — One milestone, three ordered iterations; the order is forced by the codegen block-termination seam, not by convenience. See Architecture. Recording it here so a future reader does not "optimise" the order and break the seam handover.
Acceptance criteria
- it.1:
examples/loop_counter.ailbuilds and runs to the expected value;loop/recurround-trips (parse→print→parse idempotent, JSON-AST hashable); all five newRecur*diagnostics fire on their negative fixtures;tail-appstill works unchanged;cargo test --workspacegreen. - it.2: a structural list/tree/JSON walk and a foldl-shaped
accumulator walk both check clean with no
Divergein their effect row; a non-structural recursion-by-call firesNonStructuralRecursion; aloop-bearing fn missing!DivergefiresUndeclaredEffect; mutual same-family recursion passes, cross-family fails; DESIGN.md Decision 3 + §Feature-acceptance prose match the implementation;cargo test --workspacegreen. - it.3:
Term::App.tail/Term::Do.tail/ both keywords /musttaillowering /verify_tail_positions' tail-app role /TailCallNotInTailPosition/ Decision 8 all removed;design_schema_drift.rsgreen withloop/recuranchors and notailanchors; every fixture in the full non-structural-recursion migration set (Scope-correction note)ail checkclean andail runto byte-identical pre-migration stdout; notail==falsegrandfather or no-ADT-candidate skip remains in the checker; milestone-closeauditclean (bench baseline pristine or regression ratified);cargo test --workspacegreen. - Milestone: the language has exactly two iteration forms; the only
back-edge in any generated program is an explicit, named, typed,
!Diverge-surfacedloop; F1 and F4 are discharged; the feature passes all three feature-acceptance clauses (recorded above and in DESIGN.md §"Feature-acceptance criterion").