Files
AILang/docs/journals/2026-05-17-iter-loop-recur.1.md
T
Brummel a179ec30a0 iter loop-recur.1: additive Term::Loop / Term::Recur / LoopBinder foundation
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.
2026-05-17 23:00:15 +02:00

9.4 KiB
Raw Blame History

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::Recur variants
    • struct 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 an is_term_head_kw discriminator (the plan's flagged "one genuine parser judgement"). parse_loop_and_recur_round_trip_via_term green (exactly 2 binders + if body + 2-arg recur).
  • iter loop-recur.1.3: Prose projection — write_term_prec, count_free_var, subst_var_with_term Loop/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-core GREEN.
  • iter loop-recur.1.5: ailang-check — 14 steps across 7 files including the Boss-call-1 verify_tail_positions two mandatory additive arms and the Boss-call-2 synth CheckError::Internal stub. cargo build -p ailang-check GREEN.
  • iter loop-recur.1.6: ailang-codegen + ail/main.rs — lower_term CodegenError::Internal stub + real structural pass-through at the pure analysis walkers (lambda/escape ×3) + synth_with_extras pass-through + main.rs walk_term/rewrite_term. cargo build --workspace GREEN.
  • 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.ail positive fixture. Full suite 608/0; tail-app non-regression green.

Boss design calls (mirrored from the plan header at iter close)

  1. 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-_-wildcard match-on-Term; the two additive variants require two new arms there or ailang-check does 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 (all tail-filtered tests green), NOT a frozen-source diff. mut.1 set the exact precedent (it added Term::Mut/Term::Assign arms inside the same function). Settled; not reopened — the spec-compliance phase did NOT emit unclear/BLOCKED on the anticipated "spec says byte-unchanged but the function changed" tension, per the plan header's authority.
  2. 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-codegen carries no-wildcard exhaustive Term matches; the workspace would not compile without arms. Resolved exactly as mut.1: CodegenError::Internal stub at the lower_term dispatch + real structural pass-through at the pure analysis walkers (lambda.rs collect_captures, escape.rs walk/escapes/ collect_free_vars) and synth_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:
    1. Task-1 serde expected-bytes literal. The plan asserted "type":{"t":"con","name":"Int","args":[]}; the real canonical Type::Con form is {"k":"con","name":"Int"} (discriminator "k", empty args omitted — established since mut.2). The test's stated intent (pin one-binder Loop canonical bytes; prove binders has no skip_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.
    2. Bare Int vs (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 bare Int. The real parse_type parses a bare Int ident as Type::Var{"Int"} (a type variable), not the Int constructor — semantically wrong and inconsistent with every mut fixture ((var x (con Int) 0)), the spec schema, and the Task-1 serde pin. Corrected to (con Int) in the parser test source and the examples/loop_sum_to.ail fixture. This is the LLM-natural AND correct Form-A type (not a fixture-adapts-to-bug case — every sibling fixture uses (con Int)).
    3. ailang_core::ast::LoopBinder path inside ailang-core. The plan's literal desugar.rs/workspace.rs arms used the downstream-crate path ailang_core::ast::LoopBinder; inside ailang-core itself (use crate::ast::*;) the correct path is unqualified LoopBinder (consistent with the adjacent bare MutVar usage). 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 the ailang_core::ast::LoopBinder path.

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 run e2e would only exercise the deliberately-temporary synth/lower_term Internal stubs (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 exhaustive Term match 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