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.
This commit is contained in:
2026-05-18 00:21:06 +02:00
parent 39380d361d
commit 2ee97943bd
13 changed files with 352 additions and 59 deletions
+14 -8
View File
@@ -714,14 +714,20 @@ struct Emitter<'a> {
/// dec'ing, because Implicit and Borrow do not carry the "caller
/// handed off ownership" signal that makes the dec safe.
current_param_modes: BTreeMap<String, ParamMode>,
/// Iter mut.3: per-fn map of mut-var name → (alloca SSA name,
/// AIL element type). Populated when entering a `Term::Mut`
/// block; entries removed (or restored to their prior binding)
/// when leaving the block. Consulted by the `Term::Var` arm of
/// `lower_term` before `self.locals`, matching the typecheck-
/// side mut-scope-stack precedence (`synth` in `ailang-check`
/// walks `mut_scope_stack` innermost-first before falling
/// through to locals/globals).
/// Per-fn map of name → (alloca SSA name, AIL element type) for
/// the two alloca-resident binding classes. Populated on entry
/// to a `Term::Mut` block (iter mut.3 — mut-vars) AND on entry
/// to a `Term::Loop` (iter loop-recur.3 — loop binders ride the
/// same alloca representation; the `Term::Loop` arm inserts each
/// binder here and `recur` stores into the slot). Entries are
/// removed (or restored to their prior binding) on block exit.
/// Consulted by the `Term::Var` arm of `lower_term` before
/// `self.locals` — the single load path serving both classes.
/// Note: only the mut-var half mirrors a typecheck-side
/// `mut_scope_stack`; loop binders are tracked typecheck-side in
/// the ordinary `locals` plus `loop_stack` (positional), not in
/// `mut_scope_stack` — the shared codegen map is a
/// representation detail, not a scope-precedence claim.
mut_var_allocas: BTreeMap<String, (String, Type)>,
/// loop-recur iter 3: stack of enclosing `Term::Loop` codegen
/// frames. Each frame is `(header_block_label, binder_slots)`