iter it.1: loop/recur additive — Term::Loop/Term::Recur/LoopBinder end-to-end
Iteration-discipline milestone, 1 of 3. Adds named loop + recur as strictly-additive first-class AST nodes: parse/print/prose/serde/ round-trip/schema lockstep, typecheck (binder typing + recur arity/type unification via loop_stack threaded as mut.2's mut_scope_stack; recur-tail-position via verify_loop_body), codegen (loop-header + one phi per binder; recur back-edge br with a NEW parallel block_terminated setter; lambda-boundary loop_frames save/restore). Four Recur* CheckError variants. Strictly additive: zero deletions touch tail-app/tail-do or the seven existing block_terminated sites — this is what makes the destructive it.3 safe. recur synth = fresh metavar (resolves the plan's flagged Type::unit() risk). loop_counter->55, loop_in_lambda->49, four negatives fire, tail-app byte-identical, cargo test --workspace green. Specfda9b78, plan7381a42.
This commit is contained in:
@@ -201,6 +201,17 @@ fn walk(t: &Term, out: &mut NonEscapeSet) {
|
||||
walk(body, out);
|
||||
}
|
||||
Term::Assign { value, .. } => walk(value, out),
|
||||
Term::Loop { binders, body } => {
|
||||
for b in binders {
|
||||
walk(&b.init, out);
|
||||
}
|
||||
walk(body, out);
|
||||
}
|
||||
Term::Recur { args } => {
|
||||
for a in args {
|
||||
walk(a, out);
|
||||
}
|
||||
}
|
||||
Term::Lit { .. } | Term::Var { .. } => {}
|
||||
}
|
||||
}
|
||||
@@ -395,6 +406,15 @@ fn escapes(t: &Term, tainted: &BTreeSet<String>, in_tail: bool) -> bool {
|
||||
escapes(body, tainted, in_tail)
|
||||
}
|
||||
Term::Assign { value, .. } => escapes(value, tainted, false),
|
||||
Term::Loop { binders, body } => {
|
||||
for b in binders {
|
||||
if escapes(&b.init, tainted, false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
escapes(body, tainted, in_tail)
|
||||
}
|
||||
Term::Recur { args } => args.iter().any(|a| escapes(a, tainted, false)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -524,6 +544,26 @@ fn collect_free_vars(t: &Term, bound: &mut BTreeSet<String>, out: &mut BTreeSet<
|
||||
}
|
||||
collect_free_vars(value, bound, out);
|
||||
}
|
||||
// Iter it.1: loop binder names bind inside the body and later
|
||||
// binder inits, mirroring `Term::Mut`. Recur args are uses.
|
||||
Term::Loop { binders, body } => {
|
||||
let mut newly: Vec<String> = Vec::new();
|
||||
for b in binders {
|
||||
collect_free_vars(&b.init, bound, out);
|
||||
if bound.insert(b.name.clone()) {
|
||||
newly.push(b.name.clone());
|
||||
}
|
||||
}
|
||||
collect_free_vars(body, bound, out);
|
||||
for n in newly {
|
||||
bound.remove(&n);
|
||||
}
|
||||
}
|
||||
Term::Recur { args } => {
|
||||
for a in args {
|
||||
collect_free_vars(a, bound, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -144,6 +144,12 @@ impl<'a> Emitter<'a> {
|
||||
let saved_mut_allocas = std::mem::take(&mut self.mut_var_allocas);
|
||||
let saved_pending_allocas = std::mem::take(&mut self.pending_entry_allocas);
|
||||
let saved_entry_marker = self.entry_block_end_marker.take();
|
||||
// Iter it.1: a `loop` inside a lambda body scopes its header /
|
||||
// phis to the closure's own body, not the outer fn's. Save and
|
||||
// reset `loop_frames` (the exact mut.3 analogue to
|
||||
// `mut_var_allocas` above) so a `Term::Recur` inside the thunk
|
||||
// cannot back-edge to an outer fn's loop header.
|
||||
let saved_loop_frames = std::mem::take(&mut self.loop_frames);
|
||||
// Iter 18d.4 fix: a lambda thunk is its own fn frame for
|
||||
// param-mode lookup. The outer fn's params are not in scope
|
||||
// inside the thunk; the thunk's own params are pushed below
|
||||
@@ -258,6 +264,7 @@ impl<'a> Emitter<'a> {
|
||||
self.mut_var_allocas = saved_mut_allocas;
|
||||
self.pending_entry_allocas = saved_pending_allocas;
|
||||
self.entry_block_end_marker = saved_entry_marker;
|
||||
self.loop_frames = saved_loop_frames;
|
||||
|
||||
// 3. Emit allocation + capture filling + closure-pair packing
|
||||
// in the OUTER body. Captures use 8 bytes each; closure-pair
|
||||
@@ -505,6 +512,27 @@ impl<'a> Emitter<'a> {
|
||||
}
|
||||
Self::collect_captures(value, bound, captures, captures_set, builtins, top_level);
|
||||
}
|
||||
// Iter it.1: loop binder names bind inside the body and
|
||||
// later binder inits — they are NOT captures, mirroring
|
||||
// the `Term::Mut` arm above. Recur args are uses.
|
||||
Term::Loop { binders, body } => {
|
||||
let mut newly_bound: Vec<String> = Vec::new();
|
||||
for b in binders {
|
||||
Self::collect_captures(&b.init, bound, captures, captures_set, builtins, top_level);
|
||||
if bound.insert(b.name.clone()) {
|
||||
newly_bound.push(b.name.clone());
|
||||
}
|
||||
}
|
||||
Self::collect_captures(body, bound, captures, captures_set, builtins, top_level);
|
||||
for n in newly_bound {
|
||||
bound.remove(&n);
|
||||
}
|
||||
}
|
||||
Term::Recur { args } => {
|
||||
for a in args {
|
||||
Self::collect_captures(a, bound, captures, captures_set, builtins, top_level);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -735,6 +735,28 @@ struct Emitter<'a> {
|
||||
/// at fn-body emission. Used to splice `pending_entry_allocas`
|
||||
/// into the entry block once body lowering completes.
|
||||
entry_block_end_marker: Option<usize>,
|
||||
/// Iter it.1: stack of in-flight `Term::Loop` frames. Innermost
|
||||
/// last. `Term::Recur` consults the innermost frame for the
|
||||
/// header label and records its back-edge incoming values; the
|
||||
/// `Term::Loop` arm patches each phi's incoming list once the
|
||||
/// body (and all its recur sites) has been lowered. Saved /
|
||||
/// reset / restored across the lambda boundary (a `loop` inside
|
||||
/// a closure scopes its header to the closure's own body), the
|
||||
/// exact mut.3 analogue to `mut_var_allocas`.
|
||||
loop_frames: Vec<LoopFrame>,
|
||||
}
|
||||
|
||||
/// Iter it.1: one in-flight `Term::Loop` being lowered. `phis`
|
||||
/// carries, per binder, `(phi_ssa, llvm_ty, placeholder)` — the
|
||||
/// placeholder is a unique token emitted in place of the phi's
|
||||
/// back-edge incoming list and string-replaced once `recur_edges`
|
||||
/// is complete. `recur_edges` collects `(pred_block_label,
|
||||
/// [arg_ssa,...])` one entry per `Term::Recur` reached in the body.
|
||||
#[derive(Debug, Clone)]
|
||||
struct LoopFrame {
|
||||
header: String,
|
||||
phis: Vec<(String, String, String)>,
|
||||
recur_edges: Vec<(String, Vec<String>)>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -855,6 +877,7 @@ impl<'a> Emitter<'a> {
|
||||
mut_var_allocas: BTreeMap::new(),
|
||||
pending_entry_allocas: String::new(),
|
||||
entry_block_end_marker: None,
|
||||
loop_frames: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1060,6 +1083,7 @@ impl<'a> Emitter<'a> {
|
||||
self.mut_var_allocas.clear();
|
||||
self.pending_entry_allocas.clear();
|
||||
self.entry_block_end_marker = None;
|
||||
self.loop_frames.clear();
|
||||
for (i, pname) in f.params.iter().enumerate() {
|
||||
let mode = param_modes.get(i).copied().unwrap_or(ParamMode::Implicit);
|
||||
self.current_param_modes.insert(pname.clone(), mode);
|
||||
@@ -1837,6 +1861,141 @@ impl<'a> Emitter<'a> {
|
||||
));
|
||||
Ok(("0".into(), "i8".into()))
|
||||
}
|
||||
// Iter it.1: a `Term::Loop` lowers to a header block with
|
||||
// one `phi` per binder. Binder inits flow in from the
|
||||
// predecessor block; every enclosed `Term::Recur` adds a
|
||||
// back-edge incoming value. The phi's back-edge incoming
|
||||
// list is emitted as a unique placeholder token and
|
||||
// string-replaced once the body (and all its recur sites)
|
||||
// has been lowered — phi instructions must syntactically
|
||||
// precede the body, but the recur predecessors are only
|
||||
// known after lowering it.
|
||||
Term::Loop { binders, body } => {
|
||||
// 1. Lower each binder init in the CURRENT block.
|
||||
let pred = self.current_block.clone();
|
||||
// Per binder: (name, ail_ty, init_ssa, llvm_ty).
|
||||
let mut init_ssa: Vec<(String, Type, String, String)> = Vec::new();
|
||||
for b in binders {
|
||||
let lty = llvm_type(&b.ty)?;
|
||||
let (v, vty) = self.lower_term(&b.init)?;
|
||||
if vty != lty {
|
||||
return Err(CodegenError::Internal(format!(
|
||||
"Term::Loop binder `{}`: init LLVM type {vty} != declared {lty}",
|
||||
b.name
|
||||
)));
|
||||
}
|
||||
init_ssa.push((b.name.clone(), b.ty.clone(), v, lty));
|
||||
}
|
||||
let id = self.fresh_id();
|
||||
let header = format!("loop.header.{id}");
|
||||
self.body.push_str(&format!(" br label %{header}\n"));
|
||||
self.start_block(&header);
|
||||
|
||||
// 2. One phi per binder. Each phi's back-edge incoming
|
||||
// list is a placeholder, patched in step 4. Bind the
|
||||
// phi SSA into `self.locals` so a `Term::Var` for the
|
||||
// binder name inside the body resolves to it (a loop
|
||||
// binder shadows like a let binding).
|
||||
let mut phis: Vec<(String, String, String)> = Vec::new();
|
||||
let mut saved_locals: Vec<(String, Option<(String, String, String, Type)>)> =
|
||||
Vec::new();
|
||||
for (idx, (name, ail_ty, iv, lty)) in init_ssa.iter().enumerate() {
|
||||
let p = self.fresh_ssa();
|
||||
let placeholder = format!("/*RECUR_EDGES.{id}.{idx}*/");
|
||||
self.body.push_str(&format!(
|
||||
" {p} = phi {lty} [ {iv}, %{pred} ]{placeholder}\n"
|
||||
));
|
||||
let prior = self
|
||||
.locals
|
||||
.iter()
|
||||
.rposition(|(n, _, _, _)| n == name)
|
||||
.map(|pos| self.locals.remove(pos));
|
||||
self.locals.push((
|
||||
name.clone(),
|
||||
p.clone(),
|
||||
lty.clone(),
|
||||
ail_ty.clone(),
|
||||
));
|
||||
saved_locals.push((name.clone(), prior));
|
||||
phis.push((p.clone(), lty.clone(), placeholder));
|
||||
}
|
||||
|
||||
// 3. Lower the body inside this loop frame.
|
||||
self.loop_frames.push(LoopFrame {
|
||||
header: header.clone(),
|
||||
phis: phis.clone(),
|
||||
recur_edges: Vec::new(),
|
||||
});
|
||||
let body_result = self.lower_term(body);
|
||||
let frame = self
|
||||
.loop_frames
|
||||
.pop()
|
||||
.expect("loop frame pushed above must still be on the stack");
|
||||
|
||||
// Restore the shadowed locals unconditionally so an
|
||||
// error during body lowering does not leak the binder
|
||||
// bindings into the outer scope.
|
||||
for (name, prior) in saved_locals.into_iter().rev() {
|
||||
if let Some(pos) = self.locals.iter().rposition(|(n, _, _, _)| n == &name) {
|
||||
self.locals.remove(pos);
|
||||
}
|
||||
if let Some(p) = prior {
|
||||
self.locals.push(p);
|
||||
}
|
||||
}
|
||||
let (body_v, body_ty) = body_result?;
|
||||
|
||||
// 4. Patch each phi placeholder with the collected
|
||||
// back-edge incomings (one `[ arg, %pred ]` per
|
||||
// recur site). A loop whose only exit is `recur`
|
||||
// has zero non-recur predecessors after the entry
|
||||
// edge — that is fine; the phi still lists the
|
||||
// entry edge plus the recur edges.
|
||||
for (binder_idx, (_p, _lty, placeholder)) in frame.phis.iter().enumerate() {
|
||||
let mut edges = String::new();
|
||||
for (pred_lbl, args) in &frame.recur_edges {
|
||||
let arg = args.get(binder_idx).ok_or_else(|| {
|
||||
CodegenError::Internal(format!(
|
||||
"recur edge from %{pred_lbl} missing arg #{binder_idx} \
|
||||
— typecheck guarantees arity"
|
||||
))
|
||||
})?;
|
||||
edges.push_str(&format!(", [ {arg}, %{pred_lbl} ]"));
|
||||
}
|
||||
self.body = self.body.replacen(placeholder.as_str(), &edges, 1);
|
||||
}
|
||||
|
||||
// 5. The loop's value is the body value on the
|
||||
// non-recur exit path. If every path recurred, the
|
||||
// body block is terminated and the value is unused.
|
||||
Ok((body_v, body_ty))
|
||||
}
|
||||
// Iter it.1: a `Term::Recur` is a back-edge `br` to the
|
||||
// innermost enclosing loop header. It records its argument
|
||||
// SSA values + the current block label so the loop arm can
|
||||
// patch the header phis. `block_terminated = true` is a
|
||||
// NEW, parallel setter (it.1 additive constraint — it does
|
||||
// NOT touch the seven existing tail-driven setters).
|
||||
Term::Recur { args } => {
|
||||
let mut arg_ssa: Vec<String> = Vec::with_capacity(args.len());
|
||||
for a in args {
|
||||
let (v, _ty) = self.lower_term(a)?;
|
||||
arg_ssa.push(v);
|
||||
}
|
||||
let from = self.current_block.clone();
|
||||
let frame = self.loop_frames.last_mut().ok_or_else(|| {
|
||||
CodegenError::Internal(
|
||||
"Term::Recur without an enclosing loop frame — \
|
||||
typecheck (RecurOutsideLoop) guarantees this cannot happen"
|
||||
.into(),
|
||||
)
|
||||
})?;
|
||||
let header = frame.header.clone();
|
||||
frame.recur_edges.push((from, arg_ssa));
|
||||
self.body.push_str(&format!(" br label %{header}\n"));
|
||||
self.block_terminated = true;
|
||||
Ok(("0".into(), "i8".into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3093,6 +3252,11 @@ impl<'a> Emitter<'a> {
|
||||
// is exhaustive on Term so the arms must exist.
|
||||
Term::Mut { body, .. } => self.synth_with_extras(body, extras),
|
||||
Term::Assign { .. } => Ok(Type::unit()),
|
||||
// Iter it.1: a loop's static type is its body's static
|
||||
// type (spec §"Data model"); `Term::Recur` does not fall
|
||||
// through (its synth value is never consumed).
|
||||
Term::Loop { binders: _, body } => self.synth_with_extras(body, extras),
|
||||
Term::Recur { .. } => Ok(Type::unit()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user