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]>
232 lines
7.7 KiB
Rust
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"
|
|
);
|
|
}
|