use crate::{
GRAPH_STATE,
compile::{
ir::{Arg, Arm, Atom, Body, NodeCall, Step, Subject, Tail, Var},
names::{join_name, node_fn_name, pair_name, var_name},
},
node,
};
use std::fmt;
use steel::{parser::ast::ExprKind, steel_vm::engine::Engine};
const UNFIRED: &str = "'%gantz-unfired";
#[derive(Clone, Debug)]
pub(crate) enum Sexp {
A(String),
L(Vec<Sexp>),
}
pub(crate) struct Cx<'a> {
pub path: &'a [node::Id],
}
impl fmt::Display for Sexp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Sexp::A(s) => write!(f, "{s}"),
Sexp::L(items) => {
write!(f, "(")?;
for (i, item) in items.iter().enumerate() {
if i > 0 {
write!(f, " ")?;
}
write!(f, "{item}")?;
}
write!(f, ")")
}
}
}
}
pub(crate) fn a(s: impl Into<String>) -> Sexp {
Sexp::A(s.into())
}
pub(crate) fn l(items: impl IntoIterator<Item = Sexp>) -> Sexp {
Sexp::L(items.into_iter().collect())
}
pub(crate) fn unit() -> Sexp {
a("'()")
}
fn atom_sexp(atom: &Atom) -> Sexp {
match atom {
Atom::Var(v) => a(var_name(v)),
Atom::Unit => unit(),
Atom::Unfired => a(UNFIRED),
}
}
fn arg_sexp(arg: &Arg) -> Sexp {
match arg {
Arg::One(atom) => atom_sexp(atom),
Arg::List(atoms) => l(std::iter::once(a("list")).chain(atoms.iter().map(atom_sexp))),
}
}
fn define_sexps(dst: &[Var], value: Sexp) -> Vec<Sexp> {
match dst {
[] => vec![value],
[v] => vec![l([a("define"), a(var_name(v)), value])],
_ => {
let tmp = format!("%vals-{}", var_name(&dst[0]));
let mut stmts = vec![l([a("define"), a(tmp.clone()), value])];
for (k, v) in dst.iter().enumerate() {
stmts.push(l([
a("define"),
a(var_name(v)),
l([a("list-ref"), a(tmp.clone()), a(k.to_string())]),
]));
}
stmts
}
}
}
fn call_sexp(cx: &Cx, call: &NodeCall) -> Sexp {
let inputs = node::Conns::try_from_iter(call.args.iter().map(Option::is_some))
.expect("validated NodeCall arg count exceeds Conns::MAX");
let node_path: Vec<node::Id> = cx.path.iter().copied().chain([call.node]).collect();
let fn_name = node_fn_name(&node_path, &inputs, &call.outputs);
let mut items = vec![a(fn_name)];
items.extend(call.args.iter().flatten().map(arg_sexp));
if call.stateful {
items.push(a("state"));
}
let call_expr = Sexp::L(items);
if call.stateful {
state_wrapped(call.node, call_expr)
} else {
call_expr
}
}
fn state_wrapped(node: node::Id, call_expr: Sexp) -> Sexp {
let key = a(format!("'{node}"));
l([
a("let"),
l([l([
a("state"),
l([a("hash-ref"), a(GRAPH_STATE), key.clone()]),
])]),
l([
a("let"),
l([l([a("results"), call_expr])]),
l([
a("let"),
l([
l([a("output"), l([a("car"), a("results")])]),
l([a("newstate"), l([a("car"), l([a("cdr"), a("results")])])]),
]),
l([
a("set!"),
a(GRAPH_STATE),
l([a("hash-insert"), a(GRAPH_STATE), key, a("newstate")]),
]),
a("output"),
]),
]),
])
}
pub(crate) fn body_sexps(cx: &Cx, body: &Body) -> Vec<Sexp> {
let mut stmts = Vec::with_capacity(body.steps.len() + 1);
for step in &body.steps {
match step {
Step::Node { dst, call } => {
stmts.extend(define_sexps(dst, call_sexp(cx, call)));
}
Step::DelayRead { node } => {
let var = Var::Output {
node: *node,
output: 0,
};
stmts.push(l([
a("define"),
a(var_name(&var)),
l([a("hash-ref"), a(GRAPH_STATE), a(format!("'{node}"))]),
]));
}
Step::DelayWrite { node, arg } => {
stmts.push(l([
a("set!"),
a(GRAPH_STATE),
l([
a("hash-insert"),
a(GRAPH_STATE),
a(format!("'{node}")),
arg_sexp(arg),
]),
]));
}
Step::Join(join) => {
let mut sig = vec![a(join_name(join.id))];
sig.extend(join.params.iter().map(|p| a(var_name(p))));
let mut items = vec![a("define"), Sexp::L(sig)];
items.extend(body_sexps(cx, &join.body));
stmts.push(Sexp::L(items));
}
Step::Branch { subject, dst, arms } => {
let pair = match subject {
Subject::Call(call) => {
let pair = pair_name(call.node);
stmts.push(l([a("define"), a(pair.clone()), call_sexp(cx, call)]));
pair
}
Subject::PreBound { node } => pair_name(*node),
};
stmts.extend(define_sexps(dst, dispatch_sexp(cx, &pair, arms)));
}
}
}
stmts.push(tail_sexp(&body.tail));
stmts
}
fn dispatch_sexp(cx: &Cx, pair: &str, arms: &[Arm]) -> Sexp {
let ix_expr = || l([a("list-ref"), a(pair), a("0")]);
let mut expr = unit();
for (pos, arm) in arms.iter().enumerate().rev() {
let arm_expr = arm_sexp(cx, pair, arm);
if pos + 1 == arms.len() {
expr = arm_expr;
} else {
let test = l([a("="), a(arm.ix.to_string()), ix_expr()]);
expr = l([a("if"), test, arm_expr, expr]);
}
}
expr
}
fn arm_sexp(cx: &Cx, pair: &str, arm: &Arm) -> Sexp {
let value = || l([a("list-ref"), a(pair), a("1")]);
let binds = match arm.binds.as_slice() {
[] => vec![],
binds => define_sexps(binds, value()),
};
let body = body_sexps(cx, &arm.body);
match (binds.is_empty(), arm.body.steps.is_empty()) {
(true, true) => body.into_iter().next().expect("body yields a tail expr"),
_ => {
let mut items = vec![a("let"), l([])];
items.extend(binds);
items.extend(body);
Sexp::L(items)
}
}
}
fn tail_sexp(tail: &Tail) -> Sexp {
match tail {
Tail::Ret(atoms) => match atoms.as_slice() {
[] => unit(),
[atom] => atom_sexp(atom),
atoms => l(std::iter::once(a("list")).chain(atoms.iter().map(atom_sexp))),
},
Tail::Jump { join, args } => {
l(std::iter::once(a(join_name(*join))).chain(args.iter().map(atom_sexp)))
}
}
}
pub(crate) fn level_result_sexp(
outlets: &[crate::compile::lower::OutletVal],
patterns: &[node::Conns],
) -> Sexp {
let atom = |o: &crate::compile::lower::OutletVal| match o.atom {
Some(ref atom) => atom_sexp(atom),
None => unit(),
};
let shaped = |active: Vec<&crate::compile::lower::OutletVal>| match active.as_slice() {
[] => unit(),
[o] => atom(o),
os => l(std::iter::once(a("list")).chain(os.iter().map(|o| atom(o)))),
};
if patterns.len() < 2 {
return shaped(outlets.iter().collect());
}
let mut base: u128 = 0;
let mut terms: Vec<Sexp> = Vec::new();
for (i, o) in outlets.iter().enumerate() {
match (o.atom, o.conditional) {
(None, _) => {}
(Some(_), false) => base += 1 << i,
(Some(atom), true) => terms.push(l([
a("if"),
l([a("equal?"), atom_sexp(&atom), a(UNFIRED)]),
a("0"),
a((1u128 << i).to_string()),
])),
}
}
let sig_expr = match (base, terms.len()) {
(b, 0) => a(b.to_string()),
(0, 1) => terms.pop().unwrap(),
(b, _) => {
let mut items = vec![a("+"), a(b.to_string())];
items.extend(terms);
Sexp::L(items)
}
};
let signature = |conns: &node::Conns| -> u128 {
(0..outlets.len())
.filter(|&i| conns.get(i).unwrap_or(false))
.map(|i| 1u128 << i)
.sum()
};
let pattern_value = |conns: &node::Conns| -> Sexp {
let active: Vec<&crate::compile::lower::OutletVal> = outlets
.iter()
.enumerate()
.filter_map(|(i, o)| conns.get(i).unwrap_or(false).then_some(o))
.collect();
shaped(active)
};
let last = patterns.len() - 1;
let mut expr = l([
a("list"),
a(last.to_string()),
pattern_value(&patterns[last]),
]);
for k in (0..last).rev() {
expr = l([
a("if"),
l([
a("="),
a("%gantz-sig"),
a(signature(&patterns[k]).to_string()),
]),
l([a("list"), a(k.to_string()), pattern_value(&patterns[k])]),
expr,
]);
}
l([a("let"), l([l([a("%gantz-sig"), sig_expr])]), expr])
}
pub(crate) fn fn_def(name: &str, params: &[String], stmts: Vec<Sexp>) -> ExprKind {
let mut sig = vec![a(name)];
sig.extend(params.iter().map(|p| a(p.clone())));
let mut items = vec![a("define"), Sexp::L(sig)];
items.extend(stmts);
parse_one(&Sexp::L(items).to_string())
}
pub(crate) fn parse_one(src: &str) -> ExprKind {
Engine::emit_ast(src)
.unwrap_or_else(|e| panic!("emitted Steel failed to parse: {e}\n{src}"))
.into_iter()
.next()
.expect("emitted Steel parsed to no expression")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compile::ir::{self, Join};
use steel::SteelVal;
fn out(node: node::Id, output: usize) -> Var {
Var::Output { node, output }
}
fn call(node: node::Id, args: Vec<Option<Arg>>, outputs: &str) -> NodeCall {
NodeCall {
node,
args,
outputs: outputs.parse().unwrap(),
stateful: false,
}
}
fn run(node_fns: &[&str], body: &Body) -> SteelVal {
ir::validate(body, 1, &[]).unwrap();
let cx = Cx { path: &[] };
let mut src = node_fns.join(" ");
let mut items = vec![a("define"), l([a("test-fn")])];
items.extend(body_sexps(&cx, body));
src.push_str(&format!(" {}", Sexp::L(items)));
let mut vm = Engine::new_base();
vm.run(src).unwrap();
vm.run("(test-fn)".to_string())
.unwrap()
.into_iter()
.next()
.unwrap()
}
#[test]
fn linear_chain() {
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![Atom::Var(out(1, 0))]),
};
let result = run(
&[
"(define (node-fn-0-o1) 21)",
"(define (node-fn-1-i1-o1 input0) (* 2 input0))",
],
&body,
);
assert_eq!(result, SteelVal::IntV(42));
}
#[test]
fn multi_output_destructure() {
let body = Body {
steps: vec![
Step::Node {
dst: vec![out(0, 0), out(0, 1)],
call: call(0, vec![], "11"),
},
Step::Node {
dst: vec![out(1, 0)],
call: call(
1,
vec![
Some(Arg::One(Atom::Var(out(0, 0)))),
Some(Arg::One(Atom::Var(out(0, 1)))),
],
"1",
),
},
],
tail: Tail::Ret(vec![Atom::Var(out(1, 0))]),
};
let result = run(
&[
"(define (node-fn-0-o11) (list 40 2))",
"(define (node-fn-1-i11-o1 input0 input1) (+ input0 input1))",
],
&body,
);
assert_eq!(result, SteelVal::IntV(42));
}
#[test]
fn list_arg() {
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::List(vec![
Atom::Var(out(0, 0)),
Atom::Var(out(0, 0)),
]))],
"1",
),
},
],
tail: Tail::Ret(vec![Atom::Var(out(1, 0))]),
};
let result = run(
&[
"(define (node-fn-0-o1) 21)",
"(define (node-fn-1-i1-o1 input0) (+ (list-ref input0 0) (list-ref input0 1)))",
],
&body,
);
assert_eq!(result, SteelVal::IntV(42));
}
#[test]
fn branch_join_export() {
let param = Var::Input { node: 2, input: 0 };
let export = out(2, 0);
let arm = |ix: usize, output: usize| Arm {
ix,
binds: vec![out(1, output)],
body: Body {
steps: vec![],
tail: Tail::Jump {
join: 2,
args: vec![Atom::Var(out(1, output))],
},
},
};
let body = Body {
steps: vec![
Step::Node {
dst: vec![out(0, 0)],
call: call(0, vec![], "1"),
},
Step::Join(Join {
id: 2,
params: vec![param],
rec: false,
body: Body {
steps: vec![Step::Node {
dst: vec![out(2, 0)],
call: call(2, vec![Some(Arg::One(Atom::Var(param)))], "1"),
}],
tail: Tail::Ret(vec![Atom::Var(out(2, 0))]),
},
}),
Step::Branch {
subject: Subject::Call(call(
1,
vec![Some(Arg::One(Atom::Var(out(0, 0))))],
"11",
)),
dst: vec![export],
arms: vec![arm(0, 0), arm(1, 1)],
},
],
tail: Tail::Ret(vec![Atom::Var(export)]),
};
let node_fns = [
"(define (node-fn-0-o1) 20)",
"(define (node-fn-1-i1-o11 input0)
(if (< input0 10) (list 0 input0) (list 1 input0)))",
"(define (node-fn-2-i1-o1 input0) (+ input0 1))",
];
let result = run(&node_fns, &body);
assert_eq!(result, SteelVal::IntV(21));
}
#[test]
fn rec_join_loop() {
let acc = Var::Input { node: 1, input: 0 };
let n = Var::Input { node: 1, input: 1 };
let body = Body {
steps: vec![Step::Join(Join {
id: 1,
params: vec![acc, n],
rec: true,
body: Body {
steps: vec![Step::Branch {
subject: Subject::Call(call(
1,
vec![Some(Arg::One(Atom::Var(acc))), Some(Arg::One(Atom::Var(n)))],
"11",
)),
dst: vec![out(1, 0)],
arms: vec![
Arm {
ix: 0,
binds: vec![out(9, 0), out(9, 1)],
body: Body {
steps: vec![],
tail: Tail::Jump {
join: 1,
args: vec![Atom::Var(out(9, 0)), Atom::Var(out(9, 1))],
},
},
},
Arm {
ix: 1,
binds: vec![out(9, 0)],
body: Body {
steps: vec![],
tail: Tail::Ret(vec![Atom::Var(out(9, 0))]),
},
},
],
}],
tail: Tail::Ret(vec![Atom::Var(out(1, 0))]),
},
})],
tail: Tail::Jump {
join: 1,
args: vec![Atom::Unit, Atom::Unit],
},
};
let node_fns = ["(define (node-fn-1-i11-o11 input0 input1)
(let ((acc (if (number? input0) input0 0))
(n (if (number? input1) input1 100000)))
(if (= n 0)
(list 1 acc)
(list 0 (list (+ acc 1) (- n 1))))))"];
let result = run(&node_fns, &body);
assert_eq!(result, SteelVal::IntV(100000));
}
}