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. Spec fda9b78, plan 7381a42.
This commit is contained in:
2026-05-15 14:38:55 +02:00
parent 7381a4233b
commit 96db54d15d
38 changed files with 1891 additions and 32 deletions
+61
View File
@@ -537,6 +537,19 @@ pub enum Term {
name: String,
value: Box<Term>,
},
/// Named loop head. The only repetition form besides structural
/// recursion. Iter it.1. `recur` re-enters the lexically
/// nearest enclosing `Loop`, rebinding `binders` positionally.
Loop {
binders: Vec<LoopBinder>,
body: Box<Term>,
},
/// Backward jump to the lexically nearest enclosing `Loop`.
/// Must be in tail position of that loop's body. Iter it.1.
Recur {
#[serde(default)]
args: Vec<Term>,
},
}
/// One arm of a [`Term::Match`].
@@ -580,6 +593,17 @@ pub struct MutVar {
pub init: Term,
}
/// One named, typed, initialised binder of a [`Term::Loop`].
/// Mirrors [`MutVar`] (DD-2): no `PartialEq` — codegen maps the
/// binder name to an SSA value, never compares binders.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoopBinder {
pub name: String,
#[serde(rename = "type")]
pub ty: Type,
pub init: Box<Term>,
}
/// A match pattern.
///
/// The JSON discriminator is the `p` field. Patterns are linear: each
@@ -946,4 +970,41 @@ mod tests {
other => panic!("variant mismatch: {other:?}"),
}
}
/// Iter it.1: round-trip a `Term::Loop` through JSON. Pins the
/// canonical-form shape and the `LoopBinder.ty` serde rename
/// (`"type"`); a future rename or a `skip_serializing_if` on the
/// binder fields would break the content-addressed identity this
/// pin protects.
#[test]
fn term_loop_round_trips_through_json() {
let t = Term::Loop {
binders: vec![LoopBinder {
name: "i".into(),
ty: Type::int(),
init: Box::new(Term::Lit {
lit: Literal::Int { value: 0 },
}),
}],
body: Box::new(Term::Recur {
args: vec![Term::Var { name: "i".into() }],
}),
};
let j = serde_json::to_string(&t).expect("serialise");
assert!(j.contains(r#""t":"loop""#));
assert!(j.contains(r#""type":"#)); // LoopBinder.ty serde rename
let back: Term = serde_json::from_str(&j).expect("deserialise");
assert_eq!(serde_json::to_string(&back).expect("re-serialise"), j);
}
/// Iter it.1: a zero-arg `Term::Recur` round-trips. `args` carries
/// `#[serde(default)]` so an absent `args` key still deserialises;
/// the empty vector serialises explicitly.
#[test]
fn term_recur_empty_args_round_trips() {
let t = Term::Recur { args: vec![] };
let j = serde_json::to_string(&t).expect("serialise");
let back: Term = serde_json::from_str(&j).expect("deserialise");
assert_eq!(serde_json::to_string(&back).expect("re-serialise"), j);
}
}