First of three iterations of the standalone loop/recur milestone
(spec 97c1ed1). loop/recur become real parseable / printable /
round-trippable / hash-stable strictly-additive AST nodes across
every no-wildcard exhaustive Term match in all six crates, plus
parse/print, prose arms, DESIGN.md + form_a.md schema blocks,
drift/coverage/hash anchors, and the spec's worked sum_to program
as a green round-trip fixture. NO typecheck semantics and NO real
codegen this iter by design: synth and lower_term are stubbed
CheckError::Internal / CodegenError::Internal exactly as mut.1
(real typecheck = iter 2, real loop-header/phi/back-edge = iter 3).
Two binding Boss design calls recorded in the plan header and the
journal: (1) verify_tail_positions "byte-unchanged" = its tail-app
verification role is unchanged, not its source — it is an
exhaustive no-wildcard match so two descent-only arms are
mandatory; acceptance evidence is the tail-app non-regression test.
(2) codegen is in iter-1 scope as stubs/pass-throughs because the
workspace must compile for cargo test --workspace.
Three plan-pseudo-vs-reality substitutions (serde Type::Con
literal, bare Int -> (con Int), unqualified LoopBinder) and one
recon-undercount integration-test site, all intent-preserving and
journalled. Boss forward-fix folded in: the committed specs
themselves wrote bare Int in the loop-binder snippets (parses as
Type::Var) — corrected the three headline snippets to (con Int) for
doc-honesty (no design/schema change).
cargo test --workspace 600 -> 608 / 0 red (Boss-reran
independently); iter13a + new loop_recur hash pins green.
9.4 KiB
iter loop-recur.1 — Additive AST-Node Foundation (Term::Loop / Term::Recur / LoopBinder)
Date: 2026-05-17
Started from: a5eebcd5fd
Status: DONE
Tasks completed: 7 of 7
Summary
loop / recur become real, parseable, printable, round-trippable,
hash-stable, strictly-additive AST nodes across all six crates plus
the drift tests — with NO typecheck semantics and NO real codegen
lowering (those are loop-recur iters 2 and 3). Added Term::Loop { binders, body }, Term::Recur { args }, and struct LoopBinder
(mirroring MutVar exactly); Form-A (loop (NAME TYPE INIT)* BODY+)
/ (recur ARG*) parse + print + grammar/notes; prose render /
count_free_var / subst_var arms; ~30 structural pass-through walker
arms; the two semantic dispatch points stubbed with
CheckError::Internal (synth) / CodegenError::Internal
(lower_term) exactly as mut.1 did; DESIGN.md + form_a.md schema
blocks; drift/coverage/hash anchors; and the spec's worked sum_to
program as a green examples/loop_sum_to.ail round-trip fixture.
cargo test --workspace 608 green / 0 red; the iter13a + new
loop-recur hash pins both green (additivity proven — pre-existing
canonical-JSON hashes byte-stable); all tail-filtered tests green
(Boss-call-1 operational evidence).
Per-task notes
- iter loop-recur.1.1: Core AST —
Term::Loop/Term::Recurvariantsstruct LoopBinder+ 2 serde round-trip unit tests. RED-first (error[E0599]); GREEN after Task 4 (desugar/workspace are in ailang-core, so the lib's own exhaustive matches gate it — see Concerns). Both serde tests green.
- iter loop-recur.1.2: Surface parser (
parse_loop/parse_recur+ head dispatch + unknown-head message), printer arms, form_a.md grammar+notes. Binder/body boundary disambiguated via anis_term_head_kwdiscriminator (the plan's flagged "one genuine parser judgement").parse_loop_and_recur_round_trip_via_termgreen (exactly 2 binders +ifbody + 2-arg recur). - iter loop-recur.1.3: Prose projection —
write_term_prec,count_free_var,subst_var_with_termLoop/Recur arms (verbatim plan code, lexical-shadow-aware mirroring the Mut arms). - iter loop-recur.1.4: ailang-core walkers — 9 desugar.rs sites + 2
workspace.rs sites.
cargo build -p ailang-coreGREEN. - iter loop-recur.1.5: ailang-check — 14 steps across 7 files
including the Boss-call-1
verify_tail_positionstwo mandatory additive arms and the Boss-call-2synthCheckError::Internalstub.cargo build -p ailang-checkGREEN. - iter loop-recur.1.6: ailang-codegen + ail/main.rs —
lower_termCodegenError::Internalstub + real structural pass-through at the pure analysis walkers (lambda/escape ×3) +synth_with_extraspass-through + main.rs walk_term/rewrite_term.cargo build --workspaceGREEN. - iter loop-recur.1.7: DESIGN.md + form_a.md schema blocks;
design_schema_drift + spec_drift + schema_coverage anchors; new
loop_recur hash pin;
examples/loop_sum_to.ailpositive fixture. Full suite 608/0; tail-app non-regression green.
Boss design calls (mirrored from the plan header at iter close)
verify_tail_positions"byte-unchanged" = tail-app role unchanged, NOT source frozen.verify_tail_positions(crates/ailang-check/src/lib.rs) is an exhaustive no-_-wildcardmatch-on-Term; the two additive variants require two new arms there orailang-checkdoes not compile. The added arms only define how the tail-app walker descends through two brand-new node kinds that no pre-existing fixture contains — zero behaviour change for any pre-existing construct. The acceptance evidence is the tail-app non-regression test (alltail-filtered tests green), NOT a frozen-source diff. mut.1 set the exact precedent (it addedTerm::Mut/Term::Assignarms inside the same function). Settled; not reopened — the spec-compliance phase did NOT emitunclear/BLOCKED on the anticipated "spec says byte-unchanged but the function changed" tension, per the plan header's authority.- Codegen IS in iter-1 scope as stubs/pass-throughs (mirrors
mut.1 site-for-site). "No codegen in iter 1" means no real
loop-header/phi/back-edge lowering (that is iter 3). But
ailang-codegencarries no-wildcard exhaustiveTermmatches; the workspace would not compile without arms. Resolved exactly as mut.1:CodegenError::Internalstub at thelower_termdispatch + real structural pass-through at the pure analysis walkers (lambda.rs collect_captures, escape.rs walk/escapes/ collect_free_vars) andsynth_with_extras. No real loop lowering written this iter.
Concerns
- DONE_WITH_CONCERNS (iter loop-recur.1.1/1.2/1.4): three
plan-pseudo-vs-reality substitutions, all of the recurring
"specs need concrete code / plan pseudo vs reality" class
(
feedback_specs_need_concrete_code,feedback_plan_pseudo_vs_reality), each resolved by preserving the arm/test's evident intent and fixing to the real codebase:- Task-1 serde expected-bytes literal. The plan asserted
"type":{"t":"con","name":"Int","args":[]}; the real canonicalType::Conform is{"k":"con","name":"Int"}(discriminator"k", emptyargsomitted — established since mut.2). The test's stated intent (pin one-binder Loop canonical bytes; provebindershas noskip_serializing_if) is preserved; only the mis-transcribed expected string was corrected. NOT a bug-dodge —{"k":"con",…}is the canonical form every Type fixture uses; the plan author mis-recalled the Type JSON. - Bare
Intvs(con Int)in binder triples. The spec's worked example and the plan's Step-1 test source / Step-6 fixture write(loop (acc Int 0) …)with bareInt. The realparse_typeparses a bareIntident asType::Var{"Int"}(a type variable), not the Int constructor — semantically wrong and inconsistent with everymutfixture ((var x (con Int) 0)), the spec schema, and the Task-1 serde pin. Corrected to(con Int)in the parser test source and theexamples/loop_sum_to.ailfixture. This is the LLM-natural AND correct Form-A type (not a fixture-adapts-to-bug case — every sibling fixture uses(con Int)). ailang_core::ast::LoopBinderpath inside ailang-core. The plan's literal desugar.rs/workspace.rs arms used the downstream-crate pathailang_core::ast::LoopBinder; inside ailang-core itself (use crate::ast::*;) the correct path is unqualifiedLoopBinder(consistent with the adjacent bareMutVarusage).subst_call_with_extras's plan arm also used(target, extras); the real signature is(name, lifted, extras)— mirrored from the local Mut arm per the plan's own instruction. ailang-check (downstream) correctly keeps theailang_core::ast::LoopBinderpath.
- Task-1 serde expected-bytes literal. The plan asserted
Boss addendum (post-orchestrator, at commit)
Concern 2 surfaced that the bare-Int-in-binder imprecision is not
just in the plan/test but in the committed specs themselves:
docs/specs/2026-05-17-loop-recur.md's headline clause-1 worked
example + the must-fail bad_recur fixture, and
docs/specs/2026-05-17-llm-surface-discipline.md §5's illustrative
anchor, all wrote (loop (acc Int 0) (i Int 1) …). Bare Int
parses as Type::Var{"Int"}, so the spec's headline evidence —
the program an LLM author would copy verbatim — does not parse to
the typed loop it claims. Same doc-honesty class as the mono.rs
header / effect-doc-honesty: a spec must show code that actually
parses to what it asserts. Boss forward-fixed all three snippets to
(con Int) (the established convention every mut fixture + the
spec's own schema use); folded into this iter's commit. No design,
semantics, schema, or load-bearing assumption changed — a
surface-syntax precision fix, so no re-brainstorm / re-grounding.
Known debt
- No additional E2E fixture written (Phase 3). iter-1 ships NO
typecheck and NO codegen by design; an
ail rune2e would only exercise the deliberately-temporarysynth/lower_termInternalstubs (a negative-value test asserting a transient error). The milestone invariant worth protecting at iter-1 — additivity (hash pins), round-trip (the new fixture), drift trio, tail-app non-regression — is fully covered by tests added in Tasks 1/2/7. The spec's positive sum_to-runs-to-a-value E2E is explicitly iter-3 scope.
Files touched
- AST + core walkers:
crates/ailang-core/src/ast.rs,desugar.rs,workspace.rs - Surface:
crates/ailang-surface/src/parse.rs,print.rs;crates/ailang-core/specs/form_a.md - Prose:
crates/ailang-prose/src/lib.rs - Check:
crates/ailang-check/src/{lib,lift,mono,linearity, uniqueness,reuse_shape,pre_desugar_validation}.rs - Codegen + CLI:
crates/ailang-codegen/src/{lambda,escape,lib}.rs,crates/ail/src/main.rs - Docs/drift/coverage/hash:
docs/DESIGN.md,crates/ailang-core/tests/{design_schema_drift,spec_drift, schema_coverage,hash_pin}.rs,crates/ail/tests/codegen_import_map_fallback_pin.rs(recon-undercount: an integration-test exhaustiveTermmatch not in the plan's site inventory; same additive-arm class, mirrors the Mut defensive-recursion shape — Boss-call-2-class "recon under-scoped it") - New fixture:
examples/loop_sum_to.ail
Stats
bench/orchestrator-stats/2026-05-17-iter-loop-recur.1.json