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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user