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

181 lines
9.4 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.
# iter loop-recur.1 — Additive AST-Node Foundation (Term::Loop / Term::Recur / LoopBinder)
**Date:** 2026-05-17
**Started from:** a5eebcd5fd9d703cc4f8a6374f95e00061d86c55
**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