fix(codegen): replay loop binders in synth_with_extras (closes #47)

A `let`-bound `loop` whose body references one of its own loop binders on
the non-recur exit passed `ail check` but failed `ail build` with
`unknown variable`. The fieldtest surfaced this with an owned RawBuf loop
binder, but the trigger is element-type-independent.

Root cause: the Term::Loop arm of synth_with_extras (the type-replay walk
the let-lowering runs via synth_arg_type on every let value) descended
into the loop body WITHOUT adding the loop binders to `extras`, unlike the
Term::Let arm which pushes its binder. A loop binder referenced on the
non-recur exit then resolved to UnknownVar during the type replay.

Fix: clone `extras`, push each loop binder's (name, ty), and thread the
augmented extras into the recursive synth of the body — mirroring the
Term::Let arm exactly. General fix; a plain Int let-bound loop that
references its binder also builds now.

This restores check<->codegen agreement for the natural fill-loop (thread
an owned buffer of runtime length through a loop/recur, one RawBuf.set per
iteration) — the way to populate a buffer whose length is not a fixed set
of literal indices.

Surfaced by the raw-buf fieldtest (finding B2, docs/specs/0058).
RED-first: loop_let_bound_binder_reference_builds.
This commit is contained in:
2026-05-30 17:14:07 +02:00
parent 744ad41e47
commit db710b1a73
3 changed files with 58 additions and 1 deletions
+12 -1
View File
@@ -3387,7 +3387,18 @@ impl<'a> Emitter<'a> {
// `Term::Recur` does not fall through; a Unit stub is
// safe (recur transfers control and never produces a
// value at its own position).
Term::Loop { body, .. } => self.synth_with_extras(body, extras),
Term::Loop { binders, body } => {
// The loop's binders are in scope within `body` (and on
// the non-recur exit). Replay them into `extras` exactly
// as the `Term::Let` arm does for its single binder, so a
// `Term::Var` resolving to a loop binder synths instead of
// hitting `UnknownVar`.
let mut new_extras: Vec<(String, Type)> = extras.to_vec();
for b in binders {
new_extras.push((b.name.clone(), b.ty.clone()));
}
self.synth_with_extras(body, &new_extras)
}
Term::Recur { .. } => Ok(Type::unit()),
// raw-buf.4: Term::New is desugared to (app T.new …) before
// codegen (see lower_term's arm), so it never reaches the