use crate::{
compile::{
Meta, MetaGraph,
error::LowerError,
ir::{Arg, Arm, Atom, Body, Join, NodeCall, Step, Subject, Tail, Var},
},
node,
};
use petgraph::visit::EdgeRef;
use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::fmt;
#[derive(Copy, Clone, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub(crate) struct NodeConf {
pub id: node::Id,
pub conns: NodeConns,
}
#[derive(Copy, Clone, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub(crate) struct NodeConns {
pub inputs: node::Conns,
pub outputs: node::Conns,
}
impl fmt::Debug for NodeConf {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({:?}: {:?})", self.id, self.conns)
}
}
impl fmt::Debug for NodeConns {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "([{}], [{}])", self.inputs, self.outputs)
}
}
pub(crate) struct Cx<'a> {
pub meta: &'a Meta,
pub extra_branches: BTreeMap<node::Id, Vec<node::Conns>>,
pub prebound: BTreeSet<node::Id>,
}
pub(crate) enum LevelSources {
Inlets(BTreeSet<node::Id>),
Eval {
push: Vec<(node::Id, node::Conns)>,
pull: Vec<(node::Id, node::Conns)>,
},
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct OutletVal {
pub atom: Option<Atom>,
pub conditional: bool,
}
pub(crate) struct LevelOut {
pub body: Body,
pub outlets: Vec<OutletVal>,
}
#[derive(Clone, Default)]
struct Env {
vals: BTreeMap<(node::Id, usize), Atom>,
inputs: BTreeMap<(node::Id, usize), Atom>,
}
impl<'a> Cx<'a> {
fn branches(&self, n: node::Id) -> Option<&Vec<node::Conns>> {
self.extra_branches.get(&n).or(self.meta.branches.get(&n))
}
}
pub(crate) fn level_reach_dag(
meta: &Meta,
sources: &LevelSources,
) -> Result<MetaGraph, LowerError> {
let conn1 = || node::Conns::connected(1).unwrap();
let (push, pull) = match sources {
LevelSources::Inlets(active) => {
let mut push: Vec<(node::Id, node::Conns)> =
active.iter().map(|&n| (n, conn1())).collect();
if active.len() == meta.inlets.len() {
let pull = meta.outlets.iter().map(|&n| (n, conn1())).collect();
(push, pull)
} else {
push.extend(static_sources(meta)?);
(push, vec![])
}
}
LevelSources::Eval { push, pull } => (push.clone(), pull.clone()),
};
let mut reach: HashSet<node::Id> = if meta.delays.is_empty() {
super::eval_order(&meta.graph, push, pull).collect()
} else {
let stripped = strip_delay_out_edges(&meta.graph, &meta.delays);
super::eval_order(&stripped, push, pull).collect()
};
for &d in &meta.delays {
let consumed = meta
.graph
.edges_directed(d, petgraph::Outgoing)
.any(|e_ref| reach.contains(&e_ref.target()));
if consumed {
reach.insert(d);
}
}
Ok(reachable_subgraph(&meta.graph, &reach))
}
fn reachable_subgraph(g: &MetaGraph, reachable: &HashSet<node::Id>) -> MetaGraph {
g.all_edges()
.filter(|(a, b, _)| reachable.contains(a) && reachable.contains(b))
.map(|(a, b, w)| (a, b, w.clone()))
.collect()
}
pub(crate) fn level_body(cx: &Cx, sources: &LevelSources) -> Result<LevelOut, LowerError> {
let meta = cx.meta;
let dag = level_reach_dag(meta, sources)?;
let mut env = Env::default();
if let LevelSources::Inlets(active) = sources {
for &i in active {
let var = Var::Output { node: i, output: 0 };
env.vals.insert((i, 0), Atom::Var(var));
}
}
for &n in &cx.prebound {
if cx.branches(n).is_none() {
let n_outputs = meta.outputs.get(&n).copied().unwrap_or(0);
for o in 0..n_outputs {
let var = Var::Output { node: n, output: o };
env.vals.insert((n, o), Atom::Var(var));
}
}
}
let pending: BTreeSet<node::Id> = dag
.nodes()
.filter(|n| !meta.inlets.contains(n) && !meta.outlets.contains(n))
.filter(|n| !cx.prebound.contains(n) || cx.branches(*n).is_some())
.collect();
let mut steps = Vec::new();
for &d in &meta.delays {
let consumed =
dag.contains_node(d) && dag.edges_directed(d, petgraph::Outgoing).next().is_some();
if consumed {
let var = Var::Output { node: d, output: 0 };
env.vals.insert((d, 0), Atom::Var(var));
steps.push(Step::DelayRead { node: d });
}
}
steps.extend(lower_steps(cx, &dag, pending, &mut env)?);
let mut outlets = Vec::with_capacity(meta.outlets.len());
for &o in &meta.outlets {
outlets.push(resolve_outlet(&dag, &env, o)?);
}
Ok(LevelOut {
body: Body {
steps,
tail: Tail::Ret(vec![]),
},
outlets,
})
}
fn strip_delay_out_edges(g: &MetaGraph, delays: &BTreeSet<node::Id>) -> MetaGraph {
let mut out = MetaGraph::default();
for n in g.nodes() {
out.add_node(n);
}
for (a, b, w) in g.all_edges() {
if !delays.contains(&a) {
out.add_edge(a, b, w.clone());
}
}
out
}
fn static_sources(meta: &Meta) -> Result<Vec<(node::Id, node::Conns)>, LowerError> {
use crate::compile::error::TooManyConns;
let mut sources = Vec::new();
for n in meta.graph.nodes() {
if meta.inlets.contains(&n) || meta.outlets.contains(&n) {
continue;
}
if meta
.graph
.edges_directed(n, petgraph::Incoming)
.next()
.is_some()
{
continue;
}
let n_out = meta.outputs.get(&n).copied().unwrap_or(0);
if n_out > 0 {
let conns = node::Conns::connected(n_out).map_err(|_| LowerError::Conns {
node: Some(n),
error: TooManyConns(n_out).into(),
})?;
sources.push((n, conns));
}
}
Ok(sources)
}
fn resolve_outlet(dag: &MetaGraph, env: &Env, o: node::Id) -> Result<OutletVal, LowerError> {
if !dag.contains_node(o) {
return Ok(OutletVal {
atom: None,
conditional: false,
});
}
if let Some(&atom) = env.inputs.get(&(o, 0)) {
return Ok(OutletVal {
atom: Some(atom),
conditional: true,
});
}
let atoms: Vec<Atom> = input_sources(dag, o, 0)
.into_iter()
.filter_map(|s| env.vals.get(&s).copied())
.collect();
match atoms.len() {
0 => Ok(OutletVal {
atom: None,
conditional: false,
}),
1 => Ok(OutletVal {
atom: Some(atoms[0]),
conditional: false,
}),
_ => Err(LowerError::MixedInputSources { node: o, input: 0 }),
}
}
pub(crate) fn collect_confs(body: &Body, confs: &mut BTreeSet<NodeConf>) {
fn conf(call: &NodeCall) -> NodeConf {
let inputs = node::Conns::try_from_iter(call.args.iter().map(Option::is_some))
.expect("arg count exceeds Conns::MAX");
NodeConf {
id: call.node,
conns: NodeConns {
inputs,
outputs: call.outputs,
},
}
}
for step in &body.steps {
match step {
Step::Node { call, .. } => {
confs.insert(conf(call));
}
Step::DelayRead { .. } | Step::DelayWrite { .. } => {}
Step::Join(join) => collect_confs(&join.body, confs),
Step::Branch { subject, arms, .. } => {
if let Subject::Call(call) = subject {
confs.insert(conf(call));
}
for arm in arms {
collect_confs(&arm.body, confs);
}
}
}
}
}
fn lower_steps(
cx: &Cx,
dag: &MetaGraph,
mut pending: BTreeSet<node::Id>,
env: &mut Env,
) -> Result<Vec<Step>, LowerError> {
let mut steps = Vec::new();
while let Some(n) = next_node(cx, dag, &pending) {
pending.remove(&n);
if cx.meta.delays.contains(&n) {
if let Some(arg) = resolve_input(dag, env, n, 0)? {
steps.push(Step::DelayWrite { node: n, arg });
}
} else if cx.branches(n).is_some() {
lower_branch(cx, dag, &mut pending, env, n, &mut steps)?;
} else {
let call = node_call(cx, dag, env, n)?;
let dst: Vec<Var> = call
.outputs
.iter()
.enumerate()
.filter_map(|(o, b)| b.then_some(Var::Output { node: n, output: o }))
.collect();
for &var in &dst {
let Var::Output { node, output } = var else {
unreachable!()
};
env.vals.insert((node, output), Atom::Var(var));
}
steps.push(Step::Node { dst, call });
}
}
Ok(steps)
}
fn next_node(cx: &Cx, dag: &MetaGraph, pending: &BTreeSet<node::Id>) -> Option<node::Id> {
let mut first_branch = None;
for &n in pending {
let ready = dag.edges_directed(n, petgraph::Incoming).all(|e_ref| {
!pending.contains(&e_ref.source()) || cx.meta.delays.contains(&e_ref.source())
});
if !ready {
continue;
}
if cx.branches(n).is_none() {
return Some(n);
}
if first_branch.is_none() {
first_branch = Some(n);
}
}
first_branch
}
fn node_call(cx: &Cx, dag: &MetaGraph, env: &Env, n: node::Id) -> Result<NodeCall, LowerError> {
let meta = cx.meta;
let n_inputs = meta.inputs.get(&n).copied().unwrap_or(0);
let mut args = Vec::with_capacity(n_inputs);
for i in 0..n_inputs {
args.push(resolve_input(dag, env, n, i)?);
}
Ok(NodeCall {
node: n,
args,
outputs: node_outputs(meta, dag, n)?,
stateful: meta.stateful.contains(&n),
})
}
fn resolve_input(
dag: &MetaGraph,
env: &Env,
n: node::Id,
i: usize,
) -> Result<Option<Arg>, LowerError> {
if let Some(&atom) = env.inputs.get(&(n, i)) {
return Ok(Some(Arg::One(atom)));
}
let atoms: Vec<Atom> = input_sources(dag, n, i)
.into_iter()
.filter_map(|s| env.vals.get(&s).copied())
.collect();
Ok(match atoms.len() {
0 => None,
1 => Some(Arg::One(atoms[0])),
_ => Some(Arg::List(atoms)),
})
}
fn input_sources(dag: &MetaGraph, n: node::Id, i: usize) -> Vec<(node::Id, usize)> {
let mut sources = Vec::new();
for e_ref in dag.edges_directed(n, petgraph::Incoming) {
for (edge, _kind) in e_ref.weight() {
if edge.input.0 as usize == i {
sources.push((e_ref.source(), edge.output.0 as usize));
}
}
}
sources.sort();
sources
}
fn node_outputs(meta: &Meta, dag: &MetaGraph, n: node::Id) -> Result<node::Conns, LowerError> {
use crate::compile::error::{InvalidOutputIndex, NodeConnsError, TooManyConns};
let conns_err = |error: NodeConnsError| LowerError::Conns {
node: Some(n),
error,
};
let n_outputs = meta.outputs.get(&n).copied().unwrap_or(0);
let mut outputs = node::Conns::unconnected(n_outputs)
.map_err(|_| conns_err(TooManyConns(n_outputs).into()))?;
for e_ref in dag.edges_directed(n, petgraph::Outgoing) {
for (edge, _kind) in e_ref.weight() {
let index = edge.output.0 as usize;
outputs
.set(index, true)
.map_err(|_| conns_err(InvalidOutputIndex { index, n_outputs }.into()))?;
}
}
Ok(outputs)
}
fn descendants(
cx: &Cx,
dag: &MetaGraph,
seeds: impl IntoIterator<Item = node::Id>,
within: &BTreeSet<node::Id>,
stop: &BTreeSet<node::Id>,
) -> BTreeSet<node::Id> {
let mut reached = BTreeSet::new();
let mut stack: Vec<node::Id> = seeds.into_iter().filter(|n| within.contains(n)).collect();
while let Some(n) = stack.pop() {
if !reached.insert(n) || cx.meta.delays.contains(&n) || stop.contains(&n) {
continue;
}
for e_ref in dag.edges_directed(n, petgraph::Outgoing) {
let t = e_ref.target();
if within.contains(&t) && !reached.contains(&t) {
stack.push(t);
}
}
}
reached
}
fn arm_seeds(
dag: &MetaGraph,
b: node::Id,
mask: &node::Conns,
within: &BTreeSet<node::Id>,
) -> Vec<node::Id> {
let mut seeds = Vec::new();
for e_ref in dag.edges_directed(b, petgraph::Outgoing) {
for (edge, _kind) in e_ref.weight() {
if mask.get(edge.output.0 as usize).unwrap_or(false) && within.contains(&e_ref.target())
{
seeds.push(e_ref.target());
}
}
}
seeds
}
fn unconditional_reach(
cx: &Cx,
dag: &MetaGraph,
seeds: impl IntoIterator<Item = node::Id>,
within: &BTreeSet<node::Id>,
) -> BTreeSet<node::Id> {
let mut reached = BTreeSet::new();
let mut stack: Vec<node::Id> = seeds.into_iter().filter(|n| within.contains(n)).collect();
while let Some(n) = stack.pop() {
if !reached.insert(n) || cx.meta.delays.contains(&n) {
continue;
}
match cx.branches(n) {
None => {
for e_ref in dag.edges_directed(n, petgraph::Outgoing) {
let t = e_ref.target();
if within.contains(&t) && !reached.contains(&t) {
stack.push(t);
}
}
}
Some(masks) => {
let mut arms = masks.iter().map(|mask| {
unconditional_reach(cx, dag, arm_seeds(dag, n, mask, within), within)
});
let mut shared = arms.next().unwrap_or_default();
for arm in arms {
shared = shared.intersection(&arm).copied().collect();
}
stack.extend(shared.into_iter().filter(|t| !reached.contains(t)));
}
}
}
reached
}
#[derive(Clone, Copy)]
struct Slot {
ret: SlotRet,
missing: Atom,
}
#[derive(Clone, Copy)]
enum SlotRet {
Param(Var),
Input {
consumer: (node::Id, usize),
source: (node::Id, usize),
},
Val((node::Id, usize)),
}
fn lower_branch(
cx: &Cx,
dag: &MetaGraph,
pending: &mut BTreeSet<node::Id>,
env: &mut Env,
b: node::Id,
steps: &mut Vec<Step>,
) -> Result<(), LowerError> {
let meta = cx.meta;
let arm_masks = cx.branches(b).expect("caller checked branching").clone();
let subject = if cx.prebound.contains(&b) {
Subject::PreBound { node: b }
} else {
Subject::Call(node_call(cx, dag, env, b)?)
};
let no_stop = BTreeSet::new();
let r_arms: Vec<BTreeSet<node::Id>> = arm_masks
.iter()
.map(|mask| descendants(cx, dag, arm_seeds(dag, b, mask, pending), pending, &no_stop))
.collect();
let r_all: BTreeSet<node::Id> = r_arms.iter().flatten().copied().collect();
let live: Vec<bool> = arm_masks
.iter()
.zip(&r_arms)
.map(|(mask, r)| {
if !r.is_empty() {
return true;
}
dag.edges_directed(b, petgraph::Outgoing).any(|e_ref| {
e_ref
.weight()
.iter()
.any(|(edge, _)| mask.get(edge.output.0 as usize).unwrap_or(false))
})
})
.collect();
let mut live_ucr = arm_masks
.iter()
.zip(&live)
.filter(|&(_, &l)| l)
.map(|(mask, _)| unconditional_reach(cx, dag, arm_seeds(dag, b, mask, pending), pending));
let mut cont_cand = live_ucr.next().unwrap_or_default();
for ucr in live_ucr {
cont_cand = cont_cand.intersection(&ucr).copied().collect();
}
let ext: BTreeSet<node::Id> = pending
.iter()
.copied()
.filter(|n| !r_all.contains(n) && *n != b)
.collect();
let ext_desc = descendants(cx, dag, ext.iter().copied(), pending, &no_stop);
if let Some(&bad) = r_all
.iter()
.find(|n| !cont_cand.contains(n) && ext_desc.contains(n))
{
return Err(LowerError::Entangled {
branch: b,
node: bad,
});
}
let deferred: BTreeSet<node::Id> = cont_cand.intersection(&ext_desc).copied().collect();
let arm_regions: Vec<BTreeSet<node::Id>> = arm_masks
.iter()
.map(|mask| {
descendants(
cx,
dag,
arm_seeds(dag, b, mask, pending),
pending,
&cont_cand,
)
.difference(&cont_cand)
.copied()
.collect()
})
.collect();
let in_armed: BTreeSet<node::Id> = arm_regions.iter().flatten().copied().collect();
let cont: BTreeSet<node::Id> = r_all
.iter()
.copied()
.filter(|n| !deferred.contains(n) && !in_armed.contains(n))
.collect();
let in_arms = |n: node::Id| arm_regions.iter().any(|r| r.contains(&n));
let mut consumer_inputs: BTreeSet<(node::Id, usize)> = BTreeSet::new();
for v in cont
.iter()
.filter(|v| !cx.meta.delays.contains(v))
.chain(arm_regions.iter().flatten())
.chain([&b])
{
for e_ref in dag.edges_directed(*v, petgraph::Outgoing) {
let t = e_ref.target();
if t == b || in_arms(t) {
continue;
}
for (edge, _kind) in e_ref.weight() {
consumer_inputs.insert((t, edge.input.0 as usize));
}
}
}
let mut params: BTreeMap<(node::Id, usize), Var> = BTreeMap::new();
let mut slots: BTreeMap<Var, Slot> = BTreeMap::new();
for &(t, i) in &consumer_inputs {
let sources = input_sources(dag, t, i);
let arm_s: Vec<(node::Id, usize)> = sources
.iter()
.copied()
.filter(|&(s, _)| s == b || in_arms(s))
.collect();
let cont_s: Vec<(node::Id, usize)> = sources
.iter()
.copied()
.filter(|&(s, _)| cont.contains(&s))
.collect();
let outlet = meta.outlets.contains(&t);
let missing = if outlet { Atom::Unfired } else { Atom::Unit };
let outside = !cont.contains(&t);
if !arm_s.is_empty() {
let lexical = sources.iter().any(|s| env.vals.contains_key(s));
if !cont_s.is_empty() || lexical || env.inputs.contains_key(&(t, i)) {
return Err(LowerError::MixedInputSources { node: t, input: i });
}
let param = Var::Input { node: t, input: i };
params.insert((t, i), param);
if outside {
slots.insert(
param,
Slot {
ret: SlotRet::Param(param),
missing,
},
);
}
} else if outside && !cont_s.is_empty() {
if outlet {
let lexical = sources.iter().any(|s| env.vals.contains_key(s));
if cont_s.len() > 1 || lexical || env.inputs.contains_key(&(t, i)) {
return Err(LowerError::MixedInputSources { node: t, input: i });
}
slots.insert(
Var::Input { node: t, input: i },
Slot {
ret: SlotRet::Input {
consumer: (t, i),
source: cont_s[0],
},
missing,
},
);
} else {
for (s, o) in cont_s {
let var = Var::Output { node: s, output: o };
slots.insert(
var,
Slot {
ret: SlotRet::Val((s, o)),
missing: Atom::Unit,
},
);
}
}
}
}
let export_vars: Vec<Var> = slots.keys().copied().collect();
let join_id = cont.first().copied().unwrap_or(b);
let join = if !cont.is_empty() || !slots.is_empty() {
let mut join_env = env.clone();
let mut param_vars: Vec<Var> = Vec::new();
for (&(n, i), ¶m) in ¶ms {
join_env.inputs.insert((n, i), Atom::Var(param));
param_vars.push(param);
}
let join_steps = lower_steps(cx, dag, cont.clone(), &mut join_env)?;
let ret = slots
.values()
.map(|slot| match slot.ret {
SlotRet::Param(p) => Atom::Var(p),
SlotRet::Input { consumer, source } => join_env
.inputs
.get(&consumer)
.or(join_env.vals.get(&source))
.copied()
.unwrap_or(slot.missing),
SlotRet::Val(source) => join_env.vals.get(&source).copied().unwrap_or(slot.missing),
})
.collect();
Some(Join {
id: join_id,
params: param_vars,
rec: false,
body: Body {
steps: join_steps,
tail: Tail::Ret(ret),
},
})
} else {
None
};
let mut arms = Vec::with_capacity(arm_masks.len());
for (k, mask) in arm_masks.iter().enumerate() {
let mut arm_env = env.clone();
let binds: Vec<Var> = mask
.iter()
.enumerate()
.filter_map(|(o, active)| active.then_some(Var::Output { node: b, output: o }))
.collect();
for &var in &binds {
let Var::Output { node, output } = var else {
unreachable!()
};
arm_env.vals.insert((node, output), Atom::Var(var));
}
let arm_steps = lower_steps(cx, dag, arm_regions[k].clone(), &mut arm_env)?;
let tail = if live[k] && join.is_some() {
let mut args = Vec::with_capacity(params.len());
for &(n, i) in params.keys() {
let missing = if meta.outlets.contains(&n) {
Atom::Unfired
} else {
Atom::Unit
};
args.push(arm_param_arg(
dag,
&arm_env,
&arm_regions[k],
b,
mask,
n,
i,
missing,
)?);
}
Tail::Jump {
join: join_id,
args,
}
} else {
Tail::Ret(slots.values().map(|slot| slot.missing).collect())
};
arms.push(Arm {
ix: k,
binds,
body: Body {
steps: arm_steps,
tail,
},
});
}
for n in r_all.iter() {
if !deferred.contains(n) {
pending.remove(n);
}
}
for &var in &export_vars {
match var {
Var::Output { node, output } => {
env.vals.insert((node, output), Atom::Var(var));
}
Var::Input { node, input } => {
env.inputs.insert((node, input), Atom::Var(var));
}
Var::Result { .. } => unreachable!("exports are output or input vars"),
}
}
steps.extend(join.map(Step::Join));
steps.push(Step::Branch {
subject,
dst: export_vars,
arms,
});
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn arm_param_arg(
dag: &MetaGraph,
arm_env: &Env,
arm_region: &BTreeSet<node::Id>,
b: node::Id,
mask: &node::Conns,
n: node::Id,
i: usize,
missing: Atom,
) -> Result<Atom, LowerError> {
if let Some(&atom) = arm_env.inputs.get(&(n, i)) {
let direct = input_sources(dag, n, i)
.into_iter()
.any(|s| arm_env.vals.contains_key(&s));
if direct {
return Err(LowerError::MixedInputSources { node: n, input: i });
}
return Ok(atom);
}
let mut atoms = Vec::new();
for (src, out) in input_sources(dag, n, i) {
let alive = if src == b {
mask.get(out).unwrap_or(false)
} else {
arm_region.contains(&src)
};
if !alive {
continue;
}
let &atom = arm_env
.vals
.get(&(src, out))
.ok_or(LowerError::Unresolved {
node: src,
output: out,
consumer: n,
})?;
atoms.push(atom);
}
match atoms.len() {
0 => Ok(missing),
1 => Ok(atoms[0]),
_ => Err(LowerError::MixedInputSources { node: n, input: i }),
}
}