flowrs-engine/crates/flowrs-core/tests/graph_functions.rs
flowRS 2b6f30d348 flowRS engine, cut from flowRS f907e0756a7c
Built alone from an empty CARGO_HOME, its tests passed, and flowrsc checked, ran and exported a
document from the conformance corpus; the export built alone.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-10 08:54:33 -05:00

232 lines
7.7 KiB
Rust

//! Graph functions at both tiers: every case runs in the interpreter and as one compiled
//! program ([`tier_check::run_graphs`]), and every call's outputs, every port probed after
//! them and what the program printed must agree.
//!
//! The cases cover what a call does at T1 that no widget does: inputs bound by name (a
//! function whose inputs are not interchangeable), a constant and a default at the call site,
//! several outputs, no outputs (a function that only prints), a number crossing into a port
//! of another numeric type in both directions, text in and out, calls three and five deep
//! (the station of LabCraft's examples, `graph/corpus/station_call_tree.fdsl`), and a function
//! called inside a loop's body.
use flowrs_core::graph::dsl;
use flowrs_core::graph::tier_check::{GraphCase, run_graphs};
use flowrs_core::graph::{Graph, NodeId};
fn graph(text: &str) -> Graph {
dsl::parse_graph(text)
.unwrap_or_else(|e| panic!("{e:?}\n{text}"))
.graph
}
/// Every output of every node, probed.
fn case(name: &str, text: &str) -> GraphCase {
let g = graph(text);
let mut probes = Vec::new();
let mut ids: Vec<NodeId> = g.nodes.keys().copied().collect();
ids.sort();
for id in ids {
let n = &g.nodes[&id];
let outs: Vec<String> = match flowrs_core::graph::function::ports(&g, n) {
Some((_, o)) => o.into_iter().map(|(p, _)| p).collect(),
None => flowrs_core::graph::nodes::init_node_types()
.iter()
.find(|t| t.name == n.type_id)
.map(|t| t.outputs.iter().map(|p| p.name.clone()).collect())
.unwrap_or_default(),
};
for p in outs {
probes.push((id, p));
}
}
GraphCase {
name: name.into(),
graph: g,
probes,
files: Vec::new(),
console: true,
}
}
const ORDER: &str = r#"
fn diff@1(a: f64, b: f64 = 1) -> (d: f64) {
let n0 = math.sub(a: a.value, b: b.value);
out d = n0.diff;
}
let n0 = FloatLiteral { value: 10 };
let n1 = FloatLiteral { value: 3 };
let n2 = diff@1(a: n0.value, b: n1.value);
let n3 = diff@1(a: n1.value) { b: 0.5 };
let n4 = diff@1(a: n2.d);
let n5 = io.print(value: n4.d);
"#;
const TREE: &str = r#"
fn scale@1(x: f64, k: f64) -> (y: f64) {
let n0 = math.mul(a: x.value, b: k.value);
out y = n0.product;
}
fn affine@1(x: f64, k: f64, c: f64 = 0) -> (y: f64) {
let n0 = scale@1(x: x.value, k: k.value);
let n1 = math.add(a: n0.y, b: c.value);
out y = n1.sum;
}
fn both@1(x: f64) -> (lo: f64, hi: f64) {
let n0 = affine@1(x: x.value.copy()) { k: 2 };
let n1 = affine@1(x: x.value.copy()) { k: 3, c: 1 };
out lo = n0.y;
out hi = n1.y;
}
let n0 = FloatLiteral { value: 5 };
let n1 = both@1(x: n0.value);
let n2 = math.sub(a: n1.hi, b: n1.lo);
let n3 = io.print(value: n2.diff);
"#;
const PRINTS: &str = r#"
fn report@1(v: f64, label: String = "reading") {
let n0 = io.print(value: label.value);
let n1 = io.print(value: v.value);
}
let n0 = FloatLiteral { value: 2.5 };
let n1 = report@1(v: n0.value.copy());
let n2 = report@1(v: n0.value.copy()) { label: "again" };
"#;
const NUMBERS: &str = r#"
fn twice@1(x: f64) -> (y: f64) {
let n0 = math.mul(a: x.value) { b: 2 };
out y = n0.product;
}
fn whole@1(x: f64) -> (n: i64) {
out n = x.value;
}
fn doubled@1(n: i64) -> (m: i64) {
let n0 = math.mul(a: n.value) { b: 2 };
out m = n0.product;
}
let n0 = IntLiteral { value: 3 };
let n1 = twice@1(x: n0.value);
let n2 = FloatLiteral { value: 2.7 };
let n3 = whole@1(x: n2.value.copy());
let n4 = io.print(value: n3.n);
let n5 = doubled@1(n: n2.value.copy());
"#;
const TEXT: &str = r#"
fn shout@1(s: String) -> (loud: String, length: i64) {
let n0 = text.case(text: s.value.clone()) { mode: upper };
let n1 = text.length(text: s.value.clone());
out loud = n0.out;
out length = n1.length;
}
let n0 = StringLiteral { value: "volts" };
let n1 = shout@1(s: n0.value);
let n2 = io.print(value: n1.loud);
"#;
fn station() -> &'static str {
flowrs_core::graph::corpus::get("station_call_tree")
.expect("the station is in the corpus")
.text
}
#[test]
fn every_call_computes_at_t0_what_its_program_computes() {
let cases = vec![
case("inputs bound by name, a constant and a default", ORDER),
case("three levels, two outputs, constants and defaults", TREE),
case("a function that only prints", PRINTS),
case("numbers crossing into other numeric ports", NUMBERS),
case("text in and out", TEXT),
case("the station call tree, five deep", station()),
];
let dir = std::env::temp_dir().join("flowrs_graph_functions");
let _ = std::fs::remove_dir_all(&dir);
let report = run_graphs(&cases, &dir).unwrap_or_else(|e| panic!("{e}"));
eprintln!(
"{} cases, {} comparisons, skipped {:?}, faulted {:?}",
cases.len(),
report.compared,
report.skipped,
report.faulted
);
assert!(report.skipped.is_empty(), "{:?}", report.skipped);
assert!(report.faulted.is_empty(), "{:?}", report.faulted);
assert!(
report.disagreements.is_empty(),
"the interpreter and the program disagree:\n {}",
report.disagreements.join("\n ")
);
// Every call output above, and the ports read after them.
assert!(
report.compared >= 30,
"only {} comparisons",
report.compared
);
}
/// The station's numbers, as the corpus claims them, at T0 (the corpus test holds the
/// program to them too).
#[test]
fn the_station_passes_both_steps_at_t0() {
let g = graph(station());
let mut ex =
flowrs_core::graph::GraphExecutor::builtin(flowrs_core::graph::runtime::Runtime::new());
let steps = ex.run(&g).unwrap();
assert!(ex.unexecuted().is_empty(), "{:?}", ex.unexecuted());
let call = steps.iter().find(|s| s.node_id == NodeId(1)).unwrap();
let f = |p: &str| match &call.outputs[p] {
flowrs_core::graph::Value::Float(f) => *f,
other => panic!("{other:?}"),
};
assert!((f("v1") - 3.275).abs() < 1e-12, "{}", f("v1"));
assert!((f("v2") - 4.975).abs() < 1e-12, "{}", f("v2"));
assert!(matches!(
call.outputs["passed"],
flowrs_core::graph::Value::Bool(true)
));
// Five functions deep, each defined once.
assert_eq!(g.functions.len(), 5);
assert_eq!(
flowrs_core::graph::function::call_depth(&g.functions, "station@1"),
Some(5)
);
// A tighter load fails the first step: 3.3 - 4 * 0.05 = 3.1 is in its limits, but the
// reading is 0.2 V from its target; at 5 A it is 0.25 V off, and the step fails.
let heavy = station().replace("value: 0.5, unit", "value: 5, unit");
let mut ex =
flowrs_core::graph::GraphExecutor::builtin(flowrs_core::graph::runtime::Runtime::new());
let steps = ex.run(&graph(&heavy)).unwrap();
let call = steps.iter().find(|s| s.node_id == NodeId(1)).unwrap();
assert!(matches!(
call.outputs["passed"],
flowrs_core::graph::Value::Bool(false)
));
}
/// The program calls each function where the graph calls it, and defines each one once:
/// the station's program holds five `fn` items and seven call sites.
#[test]
fn the_station_program_defines_each_function_once() {
let p = graph(station()).to_rust().unwrap();
for name in [
"scpi_query_f64_v1",
"dmm_driver_read_dc_volts_v1",
"settle_v1",
"run_step_v1",
"station_v1",
] {
assert_eq!(p.matches(&format!("fn {name}(")).count(), 1, "{name}\n{p}");
}
assert_eq!(
p.matches("run_step_v1(").count(),
3,
"defined once, called twice"
);
assert_eq!(
p.matches("settle_v1(").count(),
2,
"defined once, called once"
);
}