Files
AILang/docs/journals/2026-05-17-iter-loop-recur.tidy.md
T
Brummel 2ee97943bd iter loop-recur.tidy GREEN: reject lambda-captures-loop-binder at check + milestone-close audit resolution
GREEN side of the RED audit-trail commit 39380d3. The loop/recur
milestone-close audit found a [high] correctness defect — a lambda
capturing a loop binder passed `ail check` then panicked
unreachable!() at codegen (crates/ailang-codegen/src/lambda.rs:102)
on type-correct input. Fixed symmetrically to mut.4-tidy: new
CheckError::LoopBinderCapturedByLambda (code
loop-binder-captured-by-lambda, bracket-free F2 Display), the
Term::Lam escape guard gained a parallel loop_stack pass. Minimal
mechanism: loop_stack element Vec<Type> -> Vec<(String,Type)> so
the already-threaded per-loop frame carries binder names; recur
still reads .1 by position (Boss-call-2 positional invariant
preserved verbatim — the name is a second field for the escape
guard only, a consumer iter-2 did not anticipate). Codegen
byte-unchanged (typecheck-only fix). carve_out 17->18 + ct1-F2 +
DESIGN.md note lockstep.

Folded in (Boss-side, audit resolution): the two [medium]
doc-honesty edits the audit surfaced (mut_var_allocas rustdoc now
states its mut-var+loop-binder dual use; Term::Loop doc-comment
now describes the real shipped state, not iter-1's stale
"per-binder phi" forward-look) + a [low] P2 roadmap todo
(plan-recon undercount countermeasure, pairs with planner Step-5
items 7+8). Bench gate: all 3 scripts exit 0, 25 metrics 0
regressed — pristine, carry-on (no baseline/ratify).

cargo test --workspace 619 -> 622 / 0 red (Boss-reran
independently, hash_pin 11/0). The loop/recur milestone is
audit-clean; fieldtest is the only remaining step before close.
2026-05-18 00:21:06 +02:00

134 lines
6.3 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.tidy — lambda capturing a loop binder must fail `ail check`, not panic codegen
**Date:** 2026-05-18
**Started from:** 39380d361d530a95a4f1c718c0e201f8312d3ac4
**Status:** DONE
**Tasks completed:** 1 of 1
## Summary
A `Term::Lam` whose body captured an enclosing `Term::Loop` binder
passed `ail check` (exit 0) and then panicked `unreachable!()` at
`crates/ailang-codegen/src/lambda.rs:102` — type-correct input
crashing the compiler. This iter adds the symmetric escape guard:
such a capture now fails `ail check` with the stable kebab code
`loop-binder-captured-by-lambda`, exactly as mut.4-tidy did for
`mut-var-captured-by-lambda`. The fix is the minimal mechanism that
reuses the scope structure the `Term::Loop` arm already maintains.
## Per-task notes
- iter loop-recur.tidy.1: GREEN side of the debug RED test
`lambda_capturing_loop_binder_emits_loop_binder_captured_by_lambda`.
Added `CheckError::LoopBinderCapturedByLambda { name }`
(bracket-free F2 Display), its `code()` arm
(`loop-binder-captured-by-lambda`), and its `ctx()` arm
(`{name}`, mirroring `MutVarCapturedByLambda`). Extended
`loop_stack`'s element type from `Vec<Type>` to
`Vec<(String, Type)>` so the existing per-loop scope frame also
carries binder names (the `Term::Recur` arm reads `.1` for the
positional type check; the `Term::Lam` escape guard reads `.0`).
The `Term::Lam` arm's existing free-var scan (shared
`free_vars_in_term` walker) gained a parallel pass over
`loop_stack` next to the `mut_scope_stack` pass; its gate widened
to `!mut_scope_stack.is_empty() || !loop_stack.is_empty()`.
Mirrored the two mut.4-tidy in-source unit tests
(kebab-code test + synth-level capturing test). Lockstep:
`carve_out_inventory.rs` 17→18 (header count word + new bullet +
EXPECTED entry), `ct1_check_cli.rs` mut-F2 sibling `cases` gained
the new fixture (where `mut-var-captured-by-lambda` is asserted —
the exact precedent). `docs/DESIGN.md` one-line note adjacent to
the `MutVarCapturedByLambda` mention.
## Root cause
The `Term::Lam` escape guard iterated only `mut_scope_stack` to
reject lambda captures of alloca-resident locals. Loop binders are
inserted into the ordinary `locals` by the `Term::Loop` arm and
never entered any escape-checked scope, so a lambda body referencing
a loop binder was accepted by `ail check`. Codegen's lambda capture
resolver searches only `self.locals` after `mut_var_allocas` is
`std::mem::take`-emptied at the lambda frame boundary, so the
loop-binder reference resolved to nothing and hit `unreachable!()`.
Loop binders are alloca-resident under the same scheme as mut-vars
and obey the same no-escape-into-lambda rule.
## Design note — why extend `loop_stack` rather than add a parallel stack
The carrier offered "reuse whatever scope structure the `Term::Loop`
arm already maintains" as an explicit minimal option. `loop_stack`
is already threaded through every `synth` recursive call and is
pushed/popped at exactly the loop scope boundary. Extending its
frame element from `Type` to `(String, Type)` preserves all existing
push/pop discipline and `Term::Recur` positional-type semantics
unchanged — it is not a redesign. The alternative (a parallel
`mut_scope_stack`-shaped parameter) would have touched all 35
recursive `synth` call sites plus declarations, strictly larger.
Element-type extension is the minimal correct mechanism.
## Concerns
(none)
## Known debt
(none)
## Files touched
- `crates/ailang-check/src/lib.rs` — variant, code/ctx/Display, the
`Term::Loop` push + `Term::Recur` read + `Term::Lam` escape guard,
two in-source unit tests
- `crates/ailang-check/src/builtins.rs``loop_stack` decl type
- `crates/ailang-check/src/lift.rs``loop_stack` decl type
- `crates/ailang-check/src/mono.rs``loop_stack` decl type (x2)
- `crates/ailang-core/tests/carve_out_inventory.rs` — 17→18 lockstep
- `crates/ail/tests/ct1_check_cli.rs` — mut-F2 sibling `cases`
- `docs/DESIGN.md` — one-line symmetric-rule note
## Milestone-close audit resolution (Boss-side)
The loop/recur milestone-close `audit` (architect drift review +
bench gate) produced four items; this tidy commit resolves all:
- **`[high]` lambda-captures-loop-binder codegen crash** — fixed by
this iter's GREEN (the symmetric `loop-binder-captured-by-lambda`
guard). RED test committed separately as the audit trail
(`39380d3`); GREEN + the items below fold into this commit.
- **`[medium]` `mut_var_allocas` rustdoc lied about dual use** —
`crates/ailang-codegen/src/lib.rs` rustdoc said "Populated when
entering a `Term::Mut` block" / "matching the typecheck-side
mut-scope-stack precedence"; as of iter-3 loop binders ride the
same map. Boss-edited to present-tense dual-use truth (mut-vars
AND loop binders; the shared map is a representation detail, not
a scope-precedence claim). Doc-honesty class.
- **`[medium]` `Term::Loop` doc-comment stale "per-binder phi"** —
`crates/ailang-core/src/ast.rs` still described iter-1's
forward-looking `synth`/`lower_term` `Internal` stubs + "per-binder
phi"; iters 2/3 shipped real typecheck + alloca+mem2reg lowering
(Boss-call-1 explicitly rejected hand-emitted phi). Boss-edited to
the real shipped state. Doc-honesty class.
- **`[low]` recon-undercount 3× pattern** — added a P2 `[todo]` to
`docs/roadmap.md` (`ailang-plan-recon` cross-crate-caller-undercount
countermeasure; pairs with the planner Step-5 items 7+8 added this
milestone — those scrub the plan, this scrubs the recon). Not a
milestone-blocking fix; tracked forward.
- **Bench gate (audit Step 2)** — `bench/check.py` /
`compile_check.py` / `cross_lang.py` all exit 0; 25 metrics, 0
regressed, 25 stable. **Pristine** — exactly as the spec's
acceptance criteria expect for a strictly-additive surface no
hot-path bench exercises; the pre-existing P2 `*.bump_s`
hardware-staleness drift did not trip this run. **carry-on**: no
baseline update, no ratify entry (Iron Law — nothing intentionally
moved a metric).
**Milestone-loop-recur tidy: resolved.** Architect drift cleared
(the `[high]` is fixed code, the two `[medium]` are doc-honest, the
`[low]` is roadmap-tracked); bench pristine carry-on. The loop/recur
milestone is audit-clean; the only remaining pipeline step before
full close is `fieldtest` (user-visible Form-A surface).
## Stats
bench/orchestrator-stats/2026-05-17-iter-loop-recur.tidy.json