# Iteration-discipline revert — Design Spec **Date:** 2026-05-16 **Status:** Draft — awaiting user spec review **Authors:** Brummel (orchestrator) + Claude ## Goal Back the *Iteration-discipline* milestone out of the working language and restore the pre-milestone iteration story, by a single **forward** iteration (`main` is sacrosanct — no `git reset`/`git revert` of the shipped commits `96db54d`/`a4be1e5`). Why this is a milestone, not a tidy iteration: the milestone being removed was an *identity-level* change (a new AST node family, a new checker pass, a new effect, DESIGN.md Decision-3 prose). Reverting it is an equally identity-level decision about what AILang's iteration story *is*, and it touches DESIGN.md invariants. It earns a spec. Root cause being corrected (recorded so a future reader does not re-derive the same mistake): - The milestone was scoped from fieldtest finding **F1** (`docs/specs/0030-fieldtest-mut-local.md`), which was tagged `[friction]` and whose *own* summary table named the honest minimal action: "tighten DESIGN.md to name the gap — less work, more honest". It was instead escalated into "retire `tail-app`, recursion is total-by-construction, add `Diverge`". - That thesis recognised exactly **one** total-by-construction scheme (ADT-structural over an acyclic ADT). The it.3 corpus sweep proved it incomplete: `build(d: Int) = if d==0 then Leaf else Node(1, build(d-1), build(d-1))` — the single most LLM-natural tree-builder — is neither ADT-structural nor `loop`/`recur`-able, so the milestone *as specified* made a trivially-total, maximally-natural function inexpressible (it.3 BLOCKED). - The only in-thesis escape (A1 / it.2b — Int-bounded recursion as a second total class) requires conceding the language's **first** documented-but-unenforced totality precondition (`build(-1)` diverges silently; Decision 4 keeps refinements opaque with no SMT, so `p >= 0` is unenforceable today *by construction*). That is a dilution of a purity pillar the user personally co-designed. - `loop`/`recur` (it.1) was never run through the feature-acceptance gate for LLM-naturalness in its own right; it rode in on the thesis. it.2's `NonStructuralRecursion` checker is **not** a stable additive stopping point: it rejects bare non-structural recursion while *grandfathering* `tail`-marked calls, i.e. it half-enforces a dichotomy the language only commits to once the *destructive* it.3 completes. "Mechanically additive" (nothing deleted) is not the same as "a coherent end-state". User decision (2026-05-16): **full revert to the pre-milestone state.** Both it.1 and it.2 come out. `tail-app` / structural recursion remain the iteration mechanism exactly as before `9973546`. The genuine iteration-totality ambition is *not* abandoned — it is re-sequenced behind a future `Nat`/refinement-types milestone, because doing it *properly* (enforced non-negativity) requires type machinery the language has not built. Stateful-islands (P2, the real descendant of the user's `~/sma_factory.myc` streaming analysis) is untouched: that analysis motivates the *streaming* milestone, not the iteration story. The revert is gate-satisfied by **subtraction**: the milestone fails feature-acceptance clause 3 (via A1); the revert restores the pre-milestone state, which already passed all three clauses. The only *addition* is a DESIGN.md documentation note for F1/F4 — and the criterion states verbatim that "a documentation note is not a reshape", so it introduces no bug class and asserts nothing at the typecheck level. ## Architecture One forward iteration. The target tree state for the **iteration/checker/codegen/AST/surface/fixture** surface is the state at commit `1ff7e81` (the last commit before milestone-start `9973546`). The revert is **surgical, not a range revert**, because three hunks landed *with* the milestone but are independently sound and are *kept*: | Landed in | Hunk | Disposition | |---|---|---| | `9973546` | DESIGN.md feature-acceptance **clause 3** | **KEEP** — independently sound; it is the very tool that diagnosed the over-escalation. | | `9973546` | `skills/brainstorm/SKILL.md` Step-4 clause-3 wording | **KEEP** — part of the clause-3 strengthening (see §Components for the worked-example caveat). | | `9973546` | `docs/roadmap.md` Iteration-discipline milestone entry | **REVERT** — replaced by a deferred future entry. | | `4079f09` | `docs/roadmap.md` F3 (zero-arg `(app f)`) P2 todo | **KEEP** — independent of the iteration story; stays a P2 todo. | `git revert 9973546..a4be1e5` is therefore **forbidden twice over**: (1) it is a history-rewind class operation on `main`; (2) it would also delete clause 3 and the F3 todo. The revert is reconstructed forward, file by file, against the `1ff7e81` tree as the reference, and committed as new `main` history. ## Components The exact removal / restoration / addition surface. The planner turns this into bite-sized tasks; the implementer executes against `1ff7e81` as the byte reference (`git show 1ff7e81:` is the oracle for every *restored* file). ### Remove (it.1 + it.2 production surface) - **AST (`crates/ailang-core/src/ast.rs`).** `Term::Loop { binders, body }`, `Term::Recur { args }`, `struct LoopBinder`, and their serde round-trip unit tests. Restore the `Term` enum and its serde to the `1ff7e81` shape. - **Schema-drift / coverage anchors.** `crates/ailang-core/tests/design_schema_drift.rs`, `schema_coverage.rs`, `spec_drift.rs`, `crates/ailang-core/tests/carve_out_inventory.rs`: remove the `Term::Loop → "loop"` / `Term::Recur → "recur"` anchors and the it.2 carve-out additions; recompute the `EXPECTED` inventory count back to its `1ff7e81` value. - **Surface (`crates/ailang-surface`).** `parse.rs`: `parse_loop` / `parse_recur`, the dispatch arms, the keyword-list entries, the EBNF block. `print.rs`: the `Term::Loop` / `Term::Recur` arms. - **Prose (`crates/ailang-prose/src/lib.rs`).** The read + rebuild arms for `Term::Loop` / `Term::Recur`. - **Form-A grammar (`crates/ailang-core/specs/form_a.md`).** The `loop` / `recur` grammar entries. - **Check (`crates/ailang-check/src/lib.rs`).** `Term::Loop` typing, `Term::Recur` unification, the `loop_stack` threading, `verify_loop_body`; the **whole** `verify_structural_recursion` pass (it.2, ~772 lines) including `term_contains_loop`, the `Diverge` injection at the `FnDef`/lam-arrow/`LetRec` effect- reconciliation sites, the `tail==false` grandfather and the no-ADT-candidate skip. `verify_tail_positions` is restored to its pure pre-it.1 `tail-app` role (it.1 repurposed it to also enforce `recur`-in-tail-position; that addition is removed — the function itself stays, only the it.1 arms are deleted). Walker arms for `Term::Loop` / `Term::Recur` removed from `desugar.rs`, `lift.rs`, `mono.rs`, `linearity.rs`, `uniqueness.rs`, `reuse_shape.rs`, `pre_desugar_validation.rs`, `builtins.rs` (~25 sites, the it.1 blast radius in reverse). - **`CheckError` (`crates/ailang-check`).** Remove the five variants `RecurOutsideLoop`, `RecurArityMismatch`, `RecurTypeMismatch`, `RecurNotInTailPosition`, `NonStructuralRecursion`, including their `code()` arms **and their `ctx()` arms**. The `ctx()` removal is **not** uniform (the original draft wrongly said no `ctx()` arms exist — corrected here after Step-7.5 grounding): `RecurArityMismatch`, `RecurTypeMismatch`, and `NonStructuralRecursion` each carry a *dedicated* `ctx()` arm (`crates/ailang-check/src/lib.rs` ~870–878) which must be deleted; `RecurOutsideLoop` and `RecurNotInTailPosition` fall into the `_ =>` catch-all (`lib.rs` ~879) and need no `ctx()` edit. The diagnostic-code registry is `CheckError::code()` itself; the `//!` "Stable codes" doc-list in `crates/ailang-check/src/diagnostic.rs` ends at mq.3 and never gained `loop`/`recur`/`non-structural-recursion` entries (it.1 already proved the "add codes to a doc-list" premise false — see project memory `feedback_plan_pseudo_vs_reality`), so there is **nothing in `diagnostic.rs` for the revert to touch**. - **Codegen (`crates/ailang-codegen`).** `lib.rs`: the loop-header basic-block + per-binder `phi` lowering, the `Term::Recur` back-edge `br`, and the **new parallel `block_terminated` setter** it.1 added — the `block_terminated` seam returns to being solely tail-driven (`emit_call` / `emit_indirect_call` / `lower_effect_op` on `tail: true`), exactly as at `1ff7e81`. `escape.rs`: the `Term::Loop` / `Term::Recur` escape-walk arms. `lambda.rs`: the `loop_frames` save/restore. `crates/ail/src/main.rs`: any it.1 loop wiring. `crates/ailang-core/src/workspace.rs`: it.1 additions. - **CLI / e2e wiring.** `crates/ail/tests/e2e.rs`, `crates/ail/tests/codegen_import_map_fallback_pin.rs`, `crates/ailang-check/tests/loop_recur_pin.rs`, `crates/ailang-check/tests/structural_recursion_pin.rs`: the it.1 and it.2 pin files are **deleted** (they pin the removed surface). ### DESIGN.md > **Handling rule (forestalls the obvious failure mode).** DESIGN.md > is edited **in place, hunk by hunk** — it is *never* wholesale- > restored to `git show 1ff7e81:docs/DESIGN.md`, because the > `1ff7e81` DESIGN.md does **not** contain clause 3 (clause 3 landed > *with* the milestone in `9973546` and is explicitly KEPT). Only the > two named hunks below are reverted, using `1ff7e81` as the > *per-hunk* byte oracle for those hunks only. - **Decision 3** restored to its pre-`9973546` placeholder wording (`Diverge` is nominal/wired-up-only, *not* "the real effect of `loop`-bearing code"). Use the Decision-3 section of `git show 1ff7e81:docs/DESIGN.md` as the per-hunk byte oracle — diff-and-apply that one section, do not touch the rest of the file. - The §"Data model" `loop` / `recur` entries removed. - **Decision 8** is *untouched* — it.3 never ran, so `tail-app` / Decision 8 are already exactly in their pre-milestone state. - **Feature-acceptance criterion clause 3** is **kept** (the `9973546` DESIGN.md hunk for clause 3 stays). One caveat: the clause-3 *canonical worked example* added in `9973546` uses "the iteration story" (structural recursion + named `loop`/`recur` passing all three clauses) as its illustration. After the revert there is no `loop`/`recur` in the language, so any sentence that reads as "we *shipped* X" must be rephrased to "consider X" / hypothetical-illustration form. The example is retained as a *teaching* illustration of the criterion (it does not depend on the machinery existing); it is only de-claimed from "shipped" to "illustrative". This is the one prose edit inside the kept clause-3 block; the brainstorm SKILL.md Step-4 wording that points at it needs the same de-claiming pass. - **Add the F1/F4 note (the only net addition).** A short, honest paragraph in the DESIGN.md §"Local mutable state" / iteration vicinity: the accumulator-over-an-iteration shape from the mut-local motivation is *not* achievable by `mut` alone; until a future iteration-totality milestone (gated behind `Nat`/refinement types) lands, the canonical pattern for that shape is the tail-recursive accumulator-parameter helper, and `examples/mut_counter.ail` is the reference. Phrased explicitly as a documented idiom, **not** an enforced rule (gate-honest per the criterion's "a documentation note is not a reshape"). This discharges F1 and F4 by the fieldtester's own named minimal action. ### Fixtures - **Delete** (it.1 / it.2 net-new): `examples/loop_counter.ail`, `examples/loop_in_lambda_e2e.ail`, `examples/loop_nested_in_lambda.ail`, `examples/loop_smoke.ail`, `examples/loop_needs_diverge.ail`, `examples/struct_rec_foldl_sum.ail`, `examples/struct_rec_list_len.ail`, `examples/struct_rec_sum_e2e.ail`, `examples/struct_rec_tree_forest.ail`, `examples/test_recur_arity_mismatch.ail.json`, `examples/test_recur_not_in_tail_position.ail.json`, `examples/test_recur_outside_loop.ail.json`, `examples/test_recur_type_mismatch.ail.json`, `examples/test_loop_missing_diverge.ail.json`, `examples/test_non_structural_recursion.ail.json`, `examples/test_mutual_cross_family.ail.json`. (The planner's recon re-derives the exact file set off `git diff --stat 1ff7e81 HEAD -- examples/`; this list is the spec's best enumeration, not a substitute for that diff.) - **Restore** (pre-milestone fixtures that it.2 *modified* — must be reverted to content, not deleted): `examples/rc_pin_recurse_implicit.ail`, `examples/rc_let_alias_implicit_param.ail`. Byte oracle: `git show 1ff7e81:examples/`. `examples/mut_counter.ail` / `examples/mut_sum_floats.ail` were *not* migrated by it.1/it.2 (migration was the never-run it.3) — verify they are already at their `1ff7e81` content and leave them. - **Delete** `bench/it3-oracle/` in its entirety (40 `.out` files + `MANIFEST.tsv`) — the it.3 Task-1 deliverable, dead weight once the milestone is reverted. Forward delete (the revert commit moves `main` forward; nothing is rewound). ### Working tree / docs hygiene - Discard the uncommitted A1 amendment to `docs/specs/0031-iteration-discipline.md` (`git checkout -- docs/specs/0031-iteration-discipline.md`), then add a **superseded header** to that committed spec pointing to this one (specs are per-milestone historical records — superseded, not deleted). The it.1/it.2/it.3 plans under `docs/plans/` and the it.1/it.2/it.3 journals stay as historical record (append-only discipline; they record what happened). - **Roadmap (`docs/roadmap.md`).** Remove the Iteration-discipline P1 milestone entry *and* the "blocking fork" section. Add one deferred entry: `[idea]`/`[milestone]` "Iteration-totality story — structural + Int-bounded total recursion with *enforced* non-negativity", `depends on:` a future `Nat`/refinement-types milestone, `context:` this spec + the it.3 journal (so the genuine ambition and *why it was deferred* are not lost). Stateful-islands and the F3 P2 todo are left exactly as they are. ## Data flow After the revert the pipeline is byte-for-byte the `1ff7e81` path: `.ail` → `ailang_surface::parse` → JSON-AST (no `Term::Loop` / `Term::Recur` node exists) → `ailang-check`: typecheck → the `verify_tail_positions` pass in its pre-it.1 `tail-app`-only role → effect reconciliation **without** any `Diverge`-injection (no guardedness pass at all) → desugar / lift / mono (walkers with no Loop/Recur arms) → `ailang-codegen` (no loop-header path; the `block_terminated` seam is solely `tail: true`-driven) → LLVM text → `clang -O2`. There is no new back-edge class; the only iteration in generated IR is tail-call / structural recursion exactly as before the milestone. ## Error handling No new diagnostics — the revert *removes* six `CheckError` variants (five `Recur*` + `NonStructuralRecursion`), their `code()` arms, and the three dedicated `ctx()` arms for `RecurArityMismatch` / `RecurTypeMismatch` / `NonStructuralRecursion` (the other two `Recur*` variants use the `ctx()` catch-all and need no edit — see §Components/`CheckError` for the exact split). `UndeclaredEffect` returns to its pre-milestone behaviour (no `Diverge` antecedent path). `verify_tail_positions` emits exactly the diagnostics it emitted at `1ff7e81` (the `tail-app` tail-position checks). The schema-drift / diagnostic-code surface is validated by the existing drift tests once they are reverted to their `1ff7e81` anchor set. ## Testing strategy - **Revert-completeness pins (RED → GREEN for the revert).** - `crates/ailang-core/tests/design_schema_drift.rs` passes with **no** `loop`/`recur` anchors and **with** the `1ff7e81` anchor set. This is the structural proof the AST/schema surface is fully reverted. - `structural_recursion_pin.rs` and `loop_recur_pin.rs` are *deleted*; their absence + a green `cargo test --workspace` is the proof the pinned surface is gone (no orphan references). - `carve_out_inventory` `EXPECTED` equals its `1ff7e81` value. - **Behavioural-equivalence pins (the correctness gate).** Every example fixture that existed at `1ff7e81` and still exists must `ail check` and `ail run` to **byte-identical** stdout vs. its `1ff7e81` behaviour — in particular the two *restored* fixtures (`rc_pin_recurse_implicit.ail`, `rc_let_alias_implicit_param.ail`) and the RC-regression guards they exist for. This proves the revert reintroduced no behavioural drift. Drive it off a corpus `ail check` / `ail run` sweep, diffing against `git show 1ff7e81:` behaviour (the planner scripts the exact comparison; the `bench/it3-oracle/` snapshots are *not* the oracle — they are deleted; `1ff7e81` is the oracle). - **Doc-presence anchor.** A test (mirroring the existing DESIGN.md drift-anchor mechanism) asserts the F1/F4 note is present in DESIGN.md, so a future careless edit cannot silently drop the documented fallback the fieldtest finding required. - **Full suite.** `cargo test --workspace` green. Milestone-close `audit` runs the three bench regression scripts; the baseline is expected **pristine** (the revert returns codegen to the `1ff7e81` path — any non-pristine result is a revert defect, not a ratifiable delta). ## Acceptance criteria - `Term::Loop` / `Term::Recur` / `LoopBinder` / `verify_structural_ recursion` / `term_contains_loop` / the `Diverge`-injection / the five `Recur*` + `NonStructuralRecursion` `CheckError` variants / the loop codegen path / the it.1 parallel `block_terminated` setter are all absent; `verify_tail_positions` is byte-equivalent to its `1ff7e81` role. - `design_schema_drift.rs` green with the `1ff7e81` anchor set and no `loop`/`recur` anchors; `structural_recursion_pin.rs` / `loop_recur_pin.rs` deleted; `cargo test --workspace` green. - DESIGN.md Decision 3 is back to its placeholder wording; the §"Data model" loop/recur entries are gone; Decision 8 is unchanged; feature-acceptance **clause 3 is retained**, with its worked example de-claimed from "shipped" to "illustrative" (and the matching brainstorm SKILL.md wording de-claimed in lockstep). - The F1/F4 documentation note is present in DESIGN.md, phrased as a documented idiom (not an enforced rule), naming the tail-recursive accumulator-parameter fallback and `examples/mut_counter.ail`; a doc-presence test guards it. - Every `1ff7e81`-era fixture `ail check` + `ail run` byte-identical to its `1ff7e81` behaviour; `rc_pin_recurse_implicit.ail` / `rc_let_alias_implicit_param.ail` restored to `1ff7e81` content; `bench/it3-oracle/` deleted. - The uncommitted A1 spec amendment is discarded; the `2026-05-15-iteration-discipline.md` spec carries a superseded header pointing here; the it.1/it.2/it.3 journals and plans remain as historical record. - `docs/roadmap.md`: the Iteration-discipline milestone entry and the blocking-fork section removed; one deferred entry added for the iteration-totality ambition, `depends on:` a future `Nat`/ refinement milestone; Stateful-islands and the F3 P2 todo untouched. - Milestone-close `audit`: bench baseline pristine (not ratified — pristine), drift review clean against the restored DESIGN.md. - Net effect: AILang's iteration story is exactly the pre-`9973546` state (structural / tail recursion; `tail-app` intact), plus one honest documentation note, plus a retained-and-de-claimed feature-acceptance clause 3. The genuine total-Int-recursion ambition is preserved on the roadmap, correctly sequenced behind the type machinery it needs, not diluted to fit today.