use crate::{
GRAPH_STATE, ROOT_STATE,
compile::{
Block, Flow, FlowGraph, Meta, MetaGraph, NodeConns, RoseTree,
error::{CodegenError, InvalidInputIndex, TooManyConns},
},
node,
};
pub(crate) use node_fn::{node_fns, unique_node_confs};
use petgraph::{
graph::NodeIndex,
visit::{EdgeRef, IntoNodeReferences, NodeRef},
};
use std::collections::{BTreeSet, HashSet};
use steel::{parser::ast::ExprKind, steel_vm::engine::Engine};
mod node_fn;
#[derive(Clone, Debug)]
enum NodeFnCallArg {
Output(node::Id, node::Output),
DedicatedBinding,
}
const STATE: &str = "state";
const BRANCH_IX: &str = "branch-ix";
fn node_input_needs_binding(mg: &MetaGraph, n: node::Id, input: usize) -> bool {
let mut count = 0;
for e_ref in mg.edges_directed(n, petgraph::Incoming) {
for (edge, _kind) in e_ref.weight() {
let ix = edge.input.0 as usize;
count += (ix == input).then_some(1).unwrap_or(0);
if count > 1 {
return true;
}
}
}
false
}
fn node_fn_call(
node_path: &[node::Id],
inputs: &[Option<String>],
outputs: &node::Conns,
stateful: bool,
) -> Result<ExprKind, TooManyConns> {
let mut args: Vec<String> = inputs.iter().filter_map(Clone::clone).collect();
if stateful {
args.push(STATE.to_string());
}
let node_inputs = node::Conns::try_from_iter(inputs.iter().map(|arg| arg.is_some()))
.map_err(|_| TooManyConns(inputs.len()))?;
let node_fn_name = node_fn::name(&node_path, &node_inputs, outputs);
let node_fn_call_expr_str = format!("({node_fn_name} {})", args.join(" "));
Ok(Engine::emit_ast(&node_fn_call_expr_str)
.expect("failed to emit AST")
.into_iter()
.next()
.unwrap())
}
fn node_input_var(node_ix: node::Id, in_ix: usize) -> String {
format!("node-{}-i{}", node_ix, in_ix)
}
fn node_output_var(node_ix: node::Id, out_ix: usize) -> String {
format!("node-{}-o{}", node_ix, out_ix)
}
fn node_outputs_var(node_ix: node::Id) -> String {
format!("node-{node_ix}")
}
fn node_fn_call_args(
mg: &MetaGraph,
reachable: &HashSet<node::Id>,
n: node::Id,
inputs: &node::Conns,
) -> Result<Vec<Option<NodeFnCallArg>>, InvalidInputIndex> {
let mut args = vec![None; inputs.len()];
for e_ref in mg.edges_directed(n, petgraph::Incoming) {
for (edge, _kind) in e_ref.weight() {
let input_ix = edge.input.0 as usize;
let is_connected = inputs.get(input_ix).ok_or(InvalidInputIndex {
index: input_ix,
n_inputs: inputs.len(),
})?;
if is_connected && reachable.contains(&e_ref.source()) {
args[input_ix] = match args[input_ix] {
None => Some(NodeFnCallArg::Output(e_ref.source(), edge.output)),
Some(_) => Some(NodeFnCallArg::DedicatedBinding),
};
}
}
}
assert_eq!(
inputs.iter().filter(|&b| b).count(),
args.iter().filter(|opt| opt.is_some()).count(),
);
Ok(args)
}
fn eval_stmt(
mg: &MetaGraph,
reachable: &HashSet<node::Id>,
inlets: &BTreeSet<node::Id>,
outlets: &BTreeSet<node::Id>,
node_path: &[node::Id],
inputs: &node::Conns,
outputs: &node::Conns,
stateful: bool,
) -> Result<Option<ExprKind>, CodegenError> {
fn node_state_key(node_id: usize) -> ExprKind {
let key_str = format!("'{node_id}");
Engine::emit_ast(&key_str)
.expect("failed to emit AST")
.into_iter()
.next()
.unwrap()
}
fn wrap_node_fn_call_with_state(call_expr: &str, node_ix: usize) -> String {
const NEWSTATE: &str = "newstate";
const OUTPUT: &str = "output";
let key = node_state_key(node_ix);
format!(
"(let (({STATE} (hash-ref {GRAPH_STATE} {key})))
(let ((results {call_expr}))
(let (({OUTPUT} (car results)) ({NEWSTATE} (car (cdr results))))
(set! {GRAPH_STATE} (hash-insert {GRAPH_STATE} {key} {NEWSTATE}))
{OUTPUT})))"
)
}
fn create_define_expr(var_name: String, value_expr: ExprKind) -> ExprKind {
let s = format!("(define {var_name} {})", value_expr);
Engine::emit_ast(&s)
.expect("failed to emit AST")
.into_iter()
.next()
.unwrap()
}
let node_ix = *node_path.last().expect("node_path must not be empty");
if inlets.contains(&node_ix) {
let inlet_var = format!("inlet-{node_ix}");
let output_var = node_outputs_var(node_ix);
return Ok(Some(create_define_expr(
output_var,
Engine::emit_ast(&inlet_var)
.expect("failed to emit AST")
.into_iter()
.next()
.unwrap(),
)));
}
if outlets.contains(&node_ix) {
return Ok(None);
}
let input_args: Vec<_> = node_fn_call_args(mg, reachable, node_ix, inputs)?
.into_iter()
.enumerate()
.map(|(ix, src_opt)| {
src_opt.as_ref().map(|arg| match arg {
NodeFnCallArg::Output(n, out) => node_output_var(*n, out.0.into()),
NodeFnCallArg::DedicatedBinding => node_input_var(node_ix, ix),
})
})
.collect();
let node_fn_call_expr = node_fn_call(node_path, &input_args, outputs, stateful)?;
let mut node_fn_call_expr_str = format!("{node_fn_call_expr}");
if stateful {
node_fn_call_expr_str = wrap_node_fn_call_with_state(&node_fn_call_expr_str, node_ix);
};
let node_fn_call_expr = Engine::emit_ast(&node_fn_call_expr_str)
.expect("failed to emit AST")
.into_iter()
.next()
.unwrap();
let stmt = match outputs.len() {
0 => node_fn_call_expr,
_ => {
let output_var = node_outputs_var(node_ix);
let define_expr = create_define_expr(output_var, node_fn_call_expr);
define_expr
}
};
Ok(Some(stmt))
}
fn destructure_node_outputs_stmt(n: node::Id, outputs: node::Conns) -> ExprKind {
let vars: Vec<_> = outputs
.iter()
.enumerate()
.filter_map(|(ix, b)| b.then(|| node_output_var(n, ix)))
.collect();
let outputs_var = node_outputs_var(n);
let stmt = match vars.len() {
1 => format!("(define {} {outputs_var})", vars.join(" ")),
_ => format!("(define-values ({}) {outputs_var})", vars.join(" ")),
};
Engine::emit_ast(&stmt)
.expect("failed to emit AST")
.into_iter()
.next()
.unwrap()
}
fn destructure_node_branch_stmt(n: node::Id) -> ExprKind {
let outputs_var = node_outputs_var(n);
let stmt = format!("(define-values (branch-ix {outputs_var}) {outputs_var})");
Engine::emit_ast(&stmt)
.expect("failed to emit AST")
.into_iter()
.next()
.unwrap()
}
fn eval_fn(eval_fn_name: &str, stmts: Vec<ExprKind>) -> ExprKind {
let stmts_str = stmts
.iter()
.map(|stmt| stmt.to_string())
.chain(Some("'()".to_string()))
.collect::<Vec<_>>()
.join(" ");
let fn_def = format!(
"(define ({}) \
(define {GRAPH_STATE} {ROOT_STATE}) \
{stmts_str} \
(set! {ROOT_STATE} {GRAPH_STATE}))",
eval_fn_name
);
Engine::emit_ast(&fn_def)
.expect("Failed to emit AST for function")
.into_iter()
.next()
.unwrap()
}
pub(crate) fn path_string(path: &[node::Id]) -> String {
path.iter()
.map(|id| id.to_string())
.collect::<Vec<_>>()
.join(":")
}
pub fn pull_eval_fn_name(path: &[node::Id]) -> String {
format!("pull-fn-{}", path_string(path))
}
pub fn push_eval_fn_name(path: &[node::Id]) -> String {
format!("push-fn-{}", path_string(path))
}
fn node_outputs_that_need_bindings(
mg: &MetaGraph,
n: node::Id,
) -> BTreeSet<(node::Output, (node::Id, node::Input))> {
let mut need_bindings = BTreeSet::default();
for e_ref in mg.edges_directed(n, petgraph::Outgoing) {
for (edge, _kind) in e_ref.weight() {
if node_input_needs_binding(mg, e_ref.target(), edge.input.0 as usize) {
need_bindings.insert((edge.output, (e_ref.target(), edge.input)));
}
}
}
need_bindings
}
fn define_node_input_binding(
(src, src_out): (node::Id, node::Output),
(dst, dst_in): (node::Id, node::Input),
) -> ExprKind {
let s = format!(
"(define {} {})",
node_input_var(dst, dst_in.0 as usize),
node_output_var(src, src_out.0 as usize),
);
Engine::emit_ast(&s)
.expect("failed to emit AST")
.into_iter()
.next()
.unwrap()
}
fn define_necessary_node_input_bindings(
g: &MetaGraph,
n: node::Id,
) -> impl Iterator<Item = ExprKind> {
node_outputs_that_need_bindings(g, n)
.into_iter()
.map(move |(output, dst)| define_node_input_binding((n, output), dst))
}
pub(crate) fn eval_fn_block_stmts(
path: &[node::Id],
mg: &MetaGraph,
stateful: &BTreeSet<node::Id>,
inlets: &BTreeSet<node::Id>,
outlets: &BTreeSet<node::Id>,
reachable: &HashSet<node::Id>,
block: &Block,
) -> Result<Vec<ExprKind>, CodegenError> {
let mut stmts = Vec::new();
let mut iter = block.0.iter().peekable();
while let Some(conf) = iter.next() {
let node_path: Vec<_> = path.iter().copied().chain(Some(conf.id)).collect();
let stateful = stateful.contains(&conf.id);
let NodeConns { inputs, outputs } = conf.conns;
if let Some(stmt) = eval_stmt(
mg, reachable, inlets, outlets, &node_path, &inputs, &outputs, stateful,
)? {
stmts.push(stmt);
}
if iter.peek().is_none() {
continue;
}
stmts.push(destructure_node_outputs_stmt(conf.id, conf.conns.outputs));
stmts.extend(define_necessary_node_input_bindings(mg, conf.id));
}
Ok(stmts)
}
fn flow_graph_entry(fg: &FlowGraph) -> Option<NodeIndex<u32>> {
let mut iter = fg
.node_references()
.map(|n_ref| n_ref.id())
.filter(|&n| fg.edges_directed(n, petgraph::Incoming).next().is_none());
let entry = iter.next();
assert!(
iter.next().is_none(),
"flow graph should have only one entry"
);
entry
}
fn flow_graph_nodes(fg: &FlowGraph) -> HashSet<node::Id> {
let mut set = HashSet::new();
for n_ref in fg.node_references() {
set.extend(n_ref.weight().iter().map(|conf| conf.id));
}
set
}
fn flow_node_stmts(
path: &[node::Id],
flow_ix: NodeIndex,
mg: &MetaGraph,
stateful: &BTreeSet<node::Id>,
inlets: &BTreeSet<node::Id>,
outlets: &BTreeSet<node::Id>,
reachable: &HashSet<node::Id>,
fg: &FlowGraph,
) -> Result<Vec<ExprKind>, CodegenError> {
let block = &fg[flow_ix];
let mut stmts = eval_fn_block_stmts(path, mg, stateful, inlets, outlets, &reachable, block)?;
let mut edges: Vec<_> = fg
.edges_directed(flow_ix, petgraph::Outgoing)
.map(|e_ref| (*e_ref.weight(), e_ref.target()))
.collect();
edges.sort();
if edges.is_empty() {
return Ok(stmts);
} else if edges.len() == 1 {
let (_branch, dst) = edges.pop().unwrap();
let conf = *block.last().unwrap();
stmts.push(destructure_node_outputs_stmt(conf.id, conf.conns.outputs));
stmts.extend(define_necessary_node_input_bindings(mg, conf.id));
stmts.extend(flow_node_stmts(
path, dst, mg, stateful, inlets, outlets, reachable, fg,
)?);
return Ok(stmts);
}
let conf = *block.last().unwrap();
stmts.push(destructure_node_branch_stmt(conf.id));
stmts.extend(define_necessary_node_input_bindings(mg, conf.id));
let mut expr = "'()".to_string();
while let Some((branch, dst)) = edges.pop() {
let dst_stmts = flow_node_stmts(path, dst, mg, stateful, inlets, outlets, reachable, fg)?;
let dst_expr = format!(
"(begin {} {} '())",
destructure_node_outputs_stmt(conf.id, branch.conns),
dst_stmts
.into_iter()
.map(|expr| format!("{expr}"))
.collect::<Vec<_>>()
.join(" ")
);
expr = format!("(if (= {} {BRANCH_IX}) {dst_expr} {expr})", branch.ix);
}
let expr = Engine::emit_ast(&expr)
.expect("Failed to emit AST for function")
.into_iter()
.next()
.unwrap();
stmts.push(expr);
Ok(stmts)
}
pub fn eval_fn_body(
path: &[node::Id],
mg: &MetaGraph,
stateful: &BTreeSet<node::Id>,
inlets: &BTreeSet<node::Id>,
outlets: &BTreeSet<node::Id>,
fg: &FlowGraph,
) -> Result<Vec<ExprKind>, CodegenError> {
let Some(entry) = flow_graph_entry(fg) else {
return Ok(vec![]);
};
let reachable = flow_graph_nodes(fg);
flow_node_stmts(
path,
entry.id(),
mg,
stateful,
inlets,
outlets,
&reachable,
fg,
)
}
pub(crate) fn eval_fns_from_flow(
path: &[node::Id],
mg: &MetaGraph,
stateful: &BTreeSet<node::Id>,
inlets: &BTreeSet<node::Id>,
outlets: &BTreeSet<node::Id>,
flow: &Flow,
) -> Result<Vec<ExprKind>, CodegenError> {
let pull_fgs = flow.pull.iter().map(|(&(id, _conns), fg)| {
let node_path: Vec<_> = path.iter().copied().chain(Some(id)).collect();
let name = pull_eval_fn_name(&node_path);
(name, fg)
});
let push_fgs = flow.push.iter().map(|(&(id, _conns), fg)| {
let node_path: Vec<_> = path.iter().copied().chain(Some(id)).collect();
let name = push_eval_fn_name(&node_path);
(name, fg)
});
pull_fgs
.chain(push_fgs)
.map(|(name, fg)| {
let stmts = eval_fn_body(path, mg, stateful, inlets, outlets, fg)?;
Ok(eval_fn(&name, stmts))
})
.collect()
}
pub(crate) fn eval_fns(flow_tree: &RoseTree<(&Meta, Flow)>) -> Result<Vec<ExprKind>, CodegenError> {
let mut eval_fns = vec![];
flow_tree.try_visit(&[], &mut |path, (meta, flow)| -> Result<(), CodegenError> {
eval_fns.extend(eval_fns_from_flow(
path,
&meta.graph,
&meta.stateful,
&meta.inlets,
&meta.outlets,
flow,
)?);
Ok(())
})?;
Ok(eval_fns)
}