a179ec30a0
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.
181 lines
9.4 KiB
Markdown
181 lines
9.4 KiB
Markdown
# 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
|