use crate::node;
use std::collections::{BTreeMap, BTreeSet};
pub(crate) type JoinId = node::Id;
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub(crate) enum Atom {
Var(Var),
Unit,
Unfired,
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub(crate) enum Var {
Output { node: node::Id, output: usize },
Input { node: node::Id, input: usize },
Result { node: node::Id },
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum Arg {
One(Atom),
List(Vec<Atom>),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct NodeCall {
pub node: node::Id,
pub args: Vec<Option<Arg>>,
pub outputs: node::Conns,
pub stateful: bool,
}
#[derive(Debug)]
pub(crate) enum Subject {
Call(NodeCall),
PreBound { node: node::Id },
}
#[derive(Debug)]
pub(crate) enum Step {
Node { dst: Vec<Var>, call: NodeCall },
DelayRead { node: node::Id },
DelayWrite { node: node::Id, arg: Arg },
Join(Join),
Branch {
subject: Subject,
dst: Vec<Var>,
arms: Vec<Arm>,
},
}
#[derive(Debug)]
pub(crate) struct Arm {
pub ix: usize,
pub binds: Vec<Var>,
pub body: Body,
}
#[derive(Debug)]
pub(crate) struct Join {
pub id: JoinId,
pub params: Vec<Var>,
pub rec: bool,
pub body: Body,
}
#[derive(Debug)]
pub(crate) struct Body {
pub steps: Vec<Step>,
pub tail: Tail,
}
#[derive(Debug)]
pub(crate) enum Tail {
Ret(Vec<Atom>),
Jump { join: JoinId, args: Vec<Atom> },
}
#[derive(Debug, Eq, PartialEq, thiserror::Error)]
pub(crate) enum Invalid {
#[error("variable {0:?} referenced before binding")]
UnboundVar(Var),
#[error("variable {0:?} bound more than once in scope")]
Rebound(Var),
#[error("jump to unknown or out-of-scope join {0}")]
UnknownJoin(JoinId),
#[error("join {0} defined more than once in scope")]
DuplicateJoin(JoinId),
#[error("jump to join {join} with {got} args, expected {expected}")]
JumpArity {
join: JoinId,
expected: usize,
got: usize,
},
#[error("branch on node {node}: arm {arm} yields {got} values, expected {expected}")]
ExportArity {
node: node::Id,
arm: usize,
expected: usize,
got: usize,
},
#[error("branch on node {0}: duplicate arm index {1}")]
DuplicateArm(node::Id, usize),
}
#[derive(Clone, Copy)]
struct JoinSig {
arity: usize,
yields: Option<usize>,
}
#[derive(Clone, Default)]
struct Scope {
vars: BTreeSet<Var>,
joins: BTreeMap<JoinId, JoinSig>,
}
impl Scope {
fn bind(&mut self, var: Var) -> Result<(), Invalid> {
if !self.vars.insert(var) {
return Err(Invalid::Rebound(var));
}
Ok(())
}
fn check_atom(&self, atom: &Atom) -> Result<(), Invalid> {
match atom {
Atom::Unit | Atom::Unfired => Ok(()),
Atom::Var(v) => self
.vars
.contains(v)
.then_some(())
.ok_or(Invalid::UnboundVar(*v)),
}
}
fn check_arg(&self, arg: &Arg) -> Result<(), Invalid> {
match arg {
Arg::One(a) => self.check_atom(a),
Arg::List(atoms) => atoms.iter().try_for_each(|a| self.check_atom(a)),
}
}
fn check_call(&self, call: &NodeCall) -> Result<(), Invalid> {
call.args
.iter()
.flatten()
.try_for_each(|arg| self.check_arg(arg))
}
}
pub(crate) fn validate(body: &Body, yields: usize, pre_bound: &[Var]) -> Result<(), Invalid> {
let mut scope = Scope::default();
for &v in pre_bound {
scope.bind(v)?;
}
let got = validate_body(body, &mut scope)?;
if got.is_some_and(|got| got != yields) {
return Err(Invalid::ExportArity {
node: node::Id::MAX,
arm: 0,
expected: yields,
got: got.unwrap(),
});
}
Ok(())
}
fn validate_body(body: &Body, scope: &mut Scope) -> Result<Option<usize>, Invalid> {
for step in &body.steps {
match step {
Step::Node { dst, call } => {
scope.check_call(call)?;
for &v in dst {
scope.bind(v)?;
}
}
Step::DelayRead { node } => {
scope.bind(Var::Output {
node: *node,
output: 0,
})?;
}
Step::DelayWrite { node: _, arg } => {
scope.check_arg(arg)?;
}
Step::Join(join) => {
if scope.joins.contains_key(&join.id) {
return Err(Invalid::DuplicateJoin(join.id));
}
let mut inner = scope.clone();
for &p in &join.params {
inner.bind(p)?;
}
if join.rec {
inner.joins.insert(
join.id,
JoinSig {
arity: join.params.len(),
yields: None,
},
);
}
let yields = validate_body(&join.body, &mut inner)?;
scope.joins.insert(
join.id,
JoinSig {
arity: join.params.len(),
yields,
},
);
}
Step::Branch { subject, dst, arms } => {
let node = match subject {
Subject::Call(call) => {
scope.check_call(call)?;
call.node
}
Subject::PreBound { node } => {
let pair = Var::Result { node: *node };
if !scope.vars.contains(&pair) {
return Err(Invalid::UnboundVar(pair));
}
*node
}
};
let mut seen = BTreeSet::new();
for arm in arms {
if !seen.insert(arm.ix) {
return Err(Invalid::DuplicateArm(node, arm.ix));
}
let mut inner = scope.clone();
for &b in &arm.binds {
inner.bind(b)?;
}
let yields = validate_body(&arm.body, &mut inner)?;
if yields.is_some_and(|got| got != dst.len()) {
return Err(Invalid::ExportArity {
node,
arm: arm.ix,
expected: dst.len(),
got: yields.unwrap(),
});
}
}
for &v in dst {
scope.bind(v)?;
}
}
}
}
match &body.tail {
Tail::Ret(atoms) => {
for a in atoms {
scope.check_atom(a)?;
}
Ok(Some(atoms.len()))
}
Tail::Jump { join, args } => {
for a in args {
scope.check_atom(a)?;
}
let sig = scope.joins.get(join).ok_or(Invalid::UnknownJoin(*join))?;
if sig.arity != args.len() {
return Err(Invalid::JumpArity {
join: *join,
expected: sig.arity,
got: args.len(),
});
}
Ok(sig.yields)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn out(node: node::Id, output: usize) -> Var {
Var::Output { node, output }
}
fn call(node: node::Id, args: Vec<Option<Arg>>, n_outputs: usize) -> NodeCall {
NodeCall {
node,
args,
outputs: node::Conns::connected(n_outputs).unwrap(),
stateful: false,
}
}
#[test]
fn linear_chain_validates() {
let body = Body {
steps: vec![
Step::Node {
dst: vec![out(0, 0)],
call: call(0, vec![], 1),
},
Step::Node {
dst: vec![out(1, 0)],
call: call(1, vec![Some(Arg::One(Atom::Var(out(0, 0))))], 1),
},
],
tail: Tail::Ret(vec![]),
};
validate(&body, 0, &[]).unwrap();
}
#[test]
fn unbound_var_rejected() {
let body = Body {
steps: vec![Step::Node {
dst: vec![out(1, 0)],
call: call(1, vec![Some(Arg::One(Atom::Var(out(0, 0))))], 1),
}],
tail: Tail::Ret(vec![]),
};
assert_eq!(validate(&body, 0, &[]), Err(Invalid::UnboundVar(out(0, 0))));
}
#[test]
fn rebound_var_rejected() {
let body = Body {
steps: vec![
Step::Node {
dst: vec![out(0, 0)],
call: call(0, vec![], 1),
},
Step::Node {
dst: vec![out(0, 0)],
call: call(0, vec![], 1),
},
],
tail: Tail::Ret(vec![]),
};
assert_eq!(validate(&body, 0, &[]), Err(Invalid::Rebound(out(0, 0))));
}
#[test]
fn branch_with_join_validates() {
let param = Var::Input { node: 3, input: 0 };
let join = Join {
id: 3,
params: vec![param],
rec: false,
body: Body {
steps: vec![Step::Node {
dst: vec![out(3, 0)],
call: call(3, vec![Some(Arg::One(Atom::Var(param)))], 1),
}],
tail: Tail::Ret(vec![]),
},
};
let arm = |ix: usize| Arm {
ix,
binds: vec![out(1, ix)],
body: Body {
steps: vec![],
tail: Tail::Jump {
join: 3,
args: vec![Atom::Var(out(1, ix))],
},
},
};
let body = Body {
steps: vec![
Step::Join(join),
Step::Branch {
subject: Subject::Call(call(1, vec![], 2)),
dst: vec![],
arms: vec![arm(0), arm(1)],
},
],
tail: Tail::Ret(vec![]),
};
validate(&body, 0, &[]).unwrap();
}
#[test]
fn export_arity_mismatch_rejected() {
let body = Body {
steps: vec![Step::Branch {
subject: Subject::Call(call(1, vec![], 2)),
dst: vec![out(9, 0)],
arms: vec![Arm {
ix: 0,
binds: vec![out(1, 0)],
body: Body {
steps: vec![],
tail: Tail::Ret(vec![]),
},
}],
}],
tail: Tail::Ret(vec![]),
};
assert_eq!(
validate(&body, 0, &[]),
Err(Invalid::ExportArity {
node: 1,
arm: 0,
expected: 1,
got: 0,
})
);
}
#[test]
fn sibling_arms_bind_same_vars() {
let arm = |ix: usize| Arm {
ix,
binds: vec![out(1, 0)],
body: Body {
steps: vec![],
tail: Tail::Ret(vec![Atom::Var(out(1, 0))]),
},
};
let body = Body {
steps: vec![Step::Branch {
subject: Subject::Call(call(1, vec![], 1)),
dst: vec![out(1, 0)],
arms: vec![arm(0), arm(1)],
}],
tail: Tail::Ret(vec![]),
};
validate(&body, 0, &[]).unwrap();
}
#[test]
fn jump_to_undefined_join_rejected() {
let body = Body {
steps: vec![],
tail: Tail::Jump {
join: 7,
args: vec![],
},
};
assert_eq!(validate(&body, 0, &[]), Err(Invalid::UnknownJoin(7)));
}
#[test]
fn rec_join_self_jump() {
let rec_join = |rec: bool| Body {
steps: vec![Step::Join(Join {
id: 5,
params: vec![],
rec,
body: Body {
steps: vec![],
tail: Tail::Jump {
join: 5,
args: vec![],
},
},
})],
tail: Tail::Ret(vec![]),
};
validate(&rec_join(true), 0, &[]).unwrap();
assert_eq!(
validate(&rec_join(false), 0, &[]),
Err(Invalid::UnknownJoin(5))
);
}
}