flowrs-engine/crates/flowrs-core/tests/primitive_tiers.rs
flowRS fd958c0547 flowRS engine, cut from flowRS 3451b065
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-02 19:04:39 -05:00

1268 lines
40 KiB
Rust

//! The language primitives at both tiers: the executor (T0) against the compiled program
//! (T1), port by port, on graphs that compile.
//!
//! `tier_differential.rs` does this for widgets, one widget at a time. A primitive is not
//! a widget — it is an executor arm and a codegen arm — and several only mean something
//! wired (a `Fold` over a `Range`), so here the unit is a graph:
//! [`tier_check::run_graphs`] runs it, compiles it with a probe after every port named
//! below, and compares. Where the program panics (an `Unwrap` of nothing, an overflow in a
//! closure body) the executor must have refused, and vice versa.
//!
//! Every graph below is written to COMPILE, which is a constraint worth knowing: codegen
//! binds an `IntLiteral` as `7i64` and a `Control` as its bare text (`2`, inferred), so a
//! `usize` index comes from a `Control`; `VecNew` is the one `let mut` collection, so the
//! in-place nodes (`VecPop`, `Sort`, `FieldSet`) start from it.
//!
//! `every_primitive_is_compared_or_explained` holds the list honest: a primitive that no
//! case exercises, and that `NOT_COMPARED` does not explain, fails it.
use flowrs_core::graph::primitives::{LOOPS, PRIMITIVES};
use flowrs_core::graph::tier_check::{GraphCase, run_graphs};
use flowrs_core::graph::{Graph, NodeId, WireMode};
/// Primitives whose effect is what the program prints, compared through the console in
/// a case that compares it: the printers, statements, and the loops and loop control that
/// decide how many times the statements in a body print.
const SAID_ON_THE_CONSOLE: &[&str] = &[
"Print",
"PrintLn",
"CodeBlockNode",
"Break",
"Continue",
"ForLoop",
"WhileLoop",
"Loop",
"LoopNode",
];
/// Primitives no case below compares, and why.
const NOT_COMPARED: &[(&str, &str)] = &[(
"Indicator",
"display-only: T0 hands its input to the front panel, the program prints it; it has \
no output port the program binds, and its console line is a known divergence",
)];
struct G {
g: Graph,
probes: Vec<(NodeId, String)>,
files: Vec<(String, Vec<u8>)>,
console: bool,
}
impl G {
fn new() -> Self {
G {
g: Graph::new(),
probes: Vec::new(),
files: Vec::new(),
console: false,
}
}
fn n(&mut self, ty: &str, props: &[(&str, &str)]) -> NodeId {
let id = self.g.add_node(ty, 0.0, 0.0);
let node = self.g.get_node_mut(id).unwrap();
for (k, v) in props {
node.properties.insert((*k).into(), (*v).into());
}
id
}
fn lit(&mut self, ty: &str, v: &str) -> NodeId {
self.n(ty, &[("value", v)])
}
fn int(&mut self, v: i64) -> NodeId {
self.lit("IntLiteral", &v.to_string())
}
fn w(&mut self, from: NodeId, fp: &str, to: NodeId, tp: &str) {
self.g.add_wire(from, fp, to, tp, WireMode::Copy).unwrap();
}
/// A wire in `Clone` mode: codegen reads `x.clone()`, so a field can be read out of a
/// struct that is used again afterwards.
fn wc(&mut self, from: NodeId, fp: &str, to: NodeId, tp: &str) {
self.g.add_wire(from, fp, to, tp, WireMode::Clone).unwrap();
}
fn p(&mut self, n: NodeId, port: &str) {
self.probes.push((n, port.into()));
}
/// `ty` with `inputs` wired in, its `out` port probed.
fn op(
&mut self,
ty: &str,
props: &[(&str, &str)],
inputs: &[(NodeId, &str, &str)],
out: &str,
) -> NodeId {
let n = self.n(ty, props);
for (from, fp, tp) in inputs {
self.w(*from, fp, n, tp);
}
if !out.is_empty() {
self.p(n, out);
}
n
}
/// A `Vec<i64>` the program can mutate: `VecNew` and one `VecPush` per item.
fn vec(&mut self, items: &[i64]) -> (NodeId, &'static str) {
let mut at = (self.n("VecNew", &[]), "vec");
for i in items {
let item = self.int(*i);
let push = self.n("VecPush", &[]);
self.w(at.0, at.1, push, "vec");
self.w(item, "value", push, "item");
at = (push, "vec");
}
at
}
fn case(self, name: &str) -> GraphCase {
GraphCase {
name: name.into(),
graph: self.g,
probes: self.probes,
files: self.files,
console: self.console,
}
}
}
fn cases() -> Vec<GraphCase> {
let mut out = Vec::new();
// ---- logic ------------------------------------------------------------------------
let mut g = G::new();
let t = g.lit("BoolLiteral", "true");
let f = g.lit("BoolLiteral", "false");
g.op(
"And",
&[],
&[(t, "value", "a"), (f, "value", "b")],
"result",
);
g.op("Or", &[], &[(t, "value", "a"), (f, "value", "b")], "result");
g.op("And", &[], &[], "result");
g.op("Not", &[], &[(t, "value", "input")], "result");
g.op("Not", &[], &[], "result");
// `!` on an integer is bitwise in Rust.
let five = g.int(5);
g.op("Not", &[], &[(five, "value", "input")], "result");
out.push(g.case("logic"));
// ---- Vec --------------------------------------------------------------------------
let mut g = G::new();
let (v, port) = g.vec(&[1, 2]);
let pop = g.op("VecPop", &[], &[(v, port, "vec")], "item");
g.p(pop, "vec");
out.push(g.case("pop"));
// Popping an empty Vec is `None`; the push after it types the Vec.
let mut g = G::new();
let v = g.n("VecNew", &[]);
let pop = g.op("VecPop", &[], &[(v, "vec", "vec")], "item");
let one = g.int(1);
g.op(
"VecPush",
&[],
&[(pop, "vec", "vec"), (one, "value", "item")],
"vec",
);
out.push(g.case("pop empty"));
let mut g = G::new();
let arr = g.n("Control", &[("widget", "array"), ("value", "1, 4, 9")]);
let two = g.n("Control", &[("value", "2")]);
let far = g.n("Control", &[("value", "5")]);
let at2 = g.op(
"VecGet",
&[],
&[(arr, "value", "vec"), (two, "value", "index")],
"item",
);
let past = g.op(
"VecGet",
&[],
&[(arr, "value", "vec"), (far, "value", "index")],
"item",
);
g.op("VecGet", &[], &[(arr, "value", "vec")], "item");
g.op("IsSome", &[], &[(at2, "item", "input")], "result");
g.op("IsNone", &[], &[(past, "item", "input")], "result");
g.op("Unwrap", &[], &[(at2, "item", "input")], "value");
// `unwrap_or` on an `Option<&f64>` takes a `&f64`: the default goes through Borrow.
let half = g.lit("FloatLiteral", "0.5");
let r = g.op("Borrow", &[], &[(half, "value", "input")], "reference");
g.p(r, "output");
g.op(
"UnwrapOr",
&[],
&[(past, "item", "input"), (r, "reference", "default")],
"value",
);
g.op(
"UnwrapOr",
&[],
&[(at2, "item", "input"), (r, "reference", "default")],
"value",
);
g.op("UnwrapOr", &[], &[], "value");
g.op("IsSome", &[], &[], "result");
out.push(g.case("get and options"));
// Unwrapping nothing panics in the program, so T0 must refuse.
let mut g = G::new();
let arr = g.n("Control", &[("widget", "array"), ("value", "1, 4, 9")]);
let far = g.n("Control", &[("value", "7")]);
let past = g.op(
"VecGet",
&[],
&[(arr, "value", "vec"), (far, "value", "index")],
"item",
);
g.op("Unwrap", &[], &[(past, "item", "input")], "value");
out.push(g.case("unwrap of nothing panics"));
// ---- HashMap ----------------------------------------------------------------------
let mut g = G::new();
let m = g.n("HashMapNew", &[]);
let a = g.lit("StringLiteral", "a");
let (one, two) = (g.int(1), g.int(2));
let i1 = g.op(
"HashMapInsert",
&[],
&[
(m, "map", "map"),
(a, "value", "key"),
(one, "value", "value"),
],
"old",
);
let i2 = g.op(
"HashMapInsert",
&[],
&[
(i1, "map", "map"),
(a, "value", "key"),
(two, "value", "value"),
],
"old",
);
g.op(
"HashMapGet",
&[],
&[(i2, "map", "map"), (a, "value", "key")],
"value",
);
let zz = g.lit("StringLiteral", "zz");
g.op(
"HashMapGet",
&[],
&[(i2, "map", "map"), (zz, "value", "key")],
"value",
);
out.push(g.case("hashmap"));
// ---- iterators and closure bodies -------------------------------------------------
let mut g = G::new();
let (lo, hi) = (g.int(1), g.int(6));
let range = g.op(
"Range",
&[],
&[(lo, "value", "start"), (hi, "value", "end")],
"",
);
let sq = g.op(
"Map",
&[("src", "x * x")],
&[(range, "iterator", "iterator")],
"",
);
g.op(
"Collect",
&[],
&[(sq, "iterator", "iterator")],
"collection",
);
out.push(g.case("range map collect"));
let mut g = G::new();
let (lo, hi) = (g.int(1), g.int(6));
let range = g.op(
"Range",
&[],
&[(lo, "value", "start"), (hi, "value", "end")],
"",
);
let even = g.op(
"Filter",
&[("src", "*x % 2 == 0")],
&[(range, "iterator", "iterator")],
"",
);
g.op(
"Collect",
&[],
&[(even, "iterator", "iterator")],
"collection",
);
out.push(g.case("filter"));
let mut g = G::new();
let (lo, hi, init) = (g.int(1), g.int(6), g.int(100));
let range = g.op(
"Range",
&[],
&[(lo, "value", "start"), (hi, "value", "end")],
"",
);
g.op(
"Fold",
&[("src", "acc + x")],
&[(range, "iterator", "iterator"), (init, "value", "init")],
"result",
);
out.push(g.case("fold"));
let mut g = G::new();
let (lo, hi, init) = (g.int(0), g.int(5), g.int(0));
let range = g.op(
"Range",
&[],
&[(lo, "value", "start"), (hi, "value", "end")],
"",
);
// A body with a block, an `if`, a cast and a method: still the small language.
g.op(
"Fold",
&[(
"src",
"{ let y = if x % 2 == 0 { x * 10 } else { x }; acc + (y as f64).sqrt() as i64 }",
)],
&[(range, "iterator", "iterator"), (init, "value", "init")],
"result",
);
out.push(g.case("fold with a block body"));
let mut g = G::new();
let arr = g.n("Control", &[("widget", "array"), ("value", "1, 4, 9")]);
let it = g.op("IntoIter", &[], &[(arr, "value", "collection")], "");
let same = g.op("Map", &[], &[(it, "iterator", "iterator")], "");
g.op(
"Collect",
&[],
&[(same, "iterator", "iterator")],
"collection",
);
let empty = g.op("IntoIter", &[], &[], "");
g.op(
"Collect",
&[],
&[(empty, "iterator", "iterator")],
"collection",
);
g.op("Collect", &[], &[], "collection");
out.push(g.case("into_iter, identity map, empty"));
// Overflow in a closure body: a debug build panics, so T0 refuses.
let mut g = G::new();
let lo = g.int(i64::MAX - 1);
let hi = g.int(i64::MAX);
let range = g.op(
"Range",
&[],
&[(lo, "value", "start"), (hi, "value", "end")],
"",
);
let over = g.op(
"Map",
&[("src", "x + 2")],
&[(range, "iterator", "iterator")],
"",
);
g.op(
"Collect",
&[],
&[(over, "iterator", "iterator")],
"collection",
);
out.push(g.case("overflow panics"));
// ---- console ------------------------------------------------------------------------
let mut g = G::new();
g.console = true;
let hi_s = g.lit("StringLiteral", "hi");
g.op("Print", &[], &[(hi_s, "value", "input")], "");
let n = g.int(42);
g.op("Print", &[], &[(n, "value", "input")], "");
g.op("Print", &[], &[], "");
let x = g.lit("FloatLiteral", "2.5");
g.op("PrintLn", &[], &[(x, "value", "input")], "");
let b = g.lit("BoolLiteral", "true");
g.op("Print", &[], &[(b, "value", "input")], "");
out.push(g.case("print"));
// ---- files ----------------------------------------------------------------------------
let mut g = G::new();
g.files = vec![
("data.txt".into(), b"hello\nworld".to_vec()),
("bad.dat".into(), vec![0xff, 0xfe, 0x00]),
];
let path = g.lit("StringLiteral", "data.txt");
let ok = g.op("ReadFile", &[], &[(path, "value", "path")], "contents");
g.op("IsOk", &[], &[(ok, "contents", "input")], "result");
let bad_path = g.lit("StringLiteral", "bad.dat");
let bad = g.op("ReadFile", &[], &[(bad_path, "value", "path")], "contents");
g.op("IsErr", &[], &[(bad, "contents", "input")], "result");
g.op("IsOk", &[], &[(bad, "contents", "input")], "result");
out.push(g.case("read_file"));
// ---- ownership ------------------------------------------------------------------------
let mut g = G::new();
let seven = g.int(7);
let c = g.op("Copy", &[], &[(seven, "value", "input")], "output1");
g.p(c, "output2");
let c0 = g.op("Copy", &[], &[], "output1");
g.p(c0, "output2");
let three = g.int(3);
let b = g.op("Borrow", &[], &[(three, "value", "input")], "reference");
g.p(b, "output");
// `&mut` needs a `let mut` binding: VecNew is one. The push after types the Vec.
let v = g.n("VecNew", &[]);
let bm = g.op("BorrowMut", &[], &[(v, "vec", "input")], "reference");
g.p(bm, "output");
let one = g.int(1);
g.op(
"VecPush",
&[],
&[(bm, "output", "vec"), (one, "value", "item")],
"vec",
);
let gone = g.int(9);
g.op("Drop", &[], &[(gone, "value", "input")], "");
g.op("Drop", &[], &[], "");
out.push(g.case("ownership"));
// ---- structs ----------------------------------------------------------------------------
let mut g = G::new();
let s = g.n(
"StructNew",
&[
("type", "Point"),
("x", "3"),
("y", "2.5"),
("name", "pt"),
("on", "true"),
],
);
g.op(
"FieldGet",
&[("field", "x")],
&[(s, "instance", "instance")],
"field",
);
g.op(
"FieldGet",
&[("field", "y")],
&[(s, "instance", "instance")],
"field",
);
g.op(
"FieldGet",
&[("field", "on")],
&[(s, "instance", "instance")],
"field",
);
let name = g.n("FieldGet", &[("field", "name")]);
g.wc(s, "instance", name, "instance");
g.p(name, "field");
let nine = g.int(9);
let set = g.op(
"FieldSet",
&[("field", "x")],
&[(s, "instance", "instance"), (nine, "value", "value")],
"",
);
g.op(
"FieldGet",
&[("field", "x")],
&[(set, "instance", "instance")],
"field",
);
out.push(g.case("struct"));
let mut g = G::new();
let sp = g.n("Sprite", &[("x", "5"), ("vy", "-1.5")]);
for f in ["x", "w", "vy"] {
g.op(
"FieldGet",
&[("field", f)],
&[(sp, "sprite", "instance")],
"field",
);
}
let color = g.n("FieldGet", &[("field", "color")]);
g.wc(sp, "sprite", color, "instance");
g.p(color, "field");
let pd = g.n("Paddle", &[("speed", "4")]);
for f in ["h", "speed"] {
g.op(
"FieldGet",
&[("field", f)],
&[(pd, "paddle", "instance")],
"field",
);
}
out.push(g.case("sprite and paddle"));
// ---- strings ----------------------------------------------------------------------------
let mut g = G::new();
let s = g.n("StringNew", &[]);
g.p(s, "string");
let p1 = g.op("StringPush", &[], &[(s, "string", "string")], "string");
g.op("StringPush", &[], &[(p1, "string", "string")], "string");
// Unwired, the push lands on a temporary and `String::new()` is passed on.
g.op("StringPush", &[], &[], "string");
let word = g.lit("StringLiteral", "héllo");
g.op("StringLen", &[], &[(word, "value", "string")], "length");
let arr = g.n("Control", &[("widget", "array"), ("value", "1, 4, 9")]);
g.op("StringLen", &[], &[(arr, "value", "string")], "length");
g.op("StringLen", &[], &[], "length");
let ab = g.lit("StringLiteral", "ab");
let seven = g.int(7);
g.op(
"StringConcat",
&[],
&[(ab, "value", "a"), (seven, "value", "b")],
"result",
);
let two = g.lit("FloatLiteral", "2.0");
let t = g.lit("BoolLiteral", "true");
g.op(
"StringConcat",
&[],
&[(two, "value", "a"), (t, "value", "b")],
"result",
);
g.op("StringConcat", &[], &[], "result");
let csv = g.lit("StringLiteral", "a,b,,c");
let comma = g.lit("StringLiteral", ",");
let parts = g.op(
"StringSplit",
&[],
&[(csv, "value", "string"), (comma, "value", "pattern")],
"",
);
g.op(
"Collect",
&[],
&[(parts, "parts", "iterator")],
"collection",
);
let none = g.op("StringSplit", &[], &[], "");
g.op("Collect", &[], &[(none, "parts", "iterator")], "collection");
out.push(g.case("strings"));
// ---- match ------------------------------------------------------------------------------
let mut g = G::new();
let seven = g.int(7);
let arms = "0 => \"zero\", 1..=9 => \"digit\", _ => \"many\"";
g.op(
"Match",
&[("src", arms)],
&[(seven, "value", "value")],
"result",
);
let neg = g.int(-4);
g.op(
"MatchNode",
&[("src", "n if n < 0 => -n, n => n * 10")],
&[(neg, "value", "value")],
"result",
);
g.op("Match", &[], &[(seven, "value", "value")], "result");
let b = g.lit("StringLiteral", "b");
g.op(
"Match",
&[("src", "\"a\" => 1, \"b\" | \"c\" => 2, _ => 0")],
&[(b, "value", "value")],
"result",
);
out.push(g.case("match"));
// ---- algorithms -------------------------------------------------------------------------
let mut g = G::new();
let (v, port) = g.vec(&[3, 1, 2]);
let sorted = g.op("Sort", &[], &[(v, port, "vec")], "vec");
g.op("Reverse", &[], &[(sorted, "vec", "vec")], "vec");
g.op("Sort", &[], &[], "vec");
out.push(g.case("sort and reverse"));
let mut g = G::new();
let mut at = (g.n("VecNew", &[]), "vec");
for w in ["pear", "apple", "fig"] {
let item = g.lit("StringLiteral", w);
at = (
g.op(
"VecPush",
&[],
&[(at.0, at.1, "vec"), (item, "value", "item")],
"",
),
"vec",
);
}
g.op("Sort", &[], &[(at.0, at.1, "vec")], "vec");
out.push(g.case("sort strings"));
let mut g = G::new();
let (v, port) = g.vec(&[1, 3, 5]);
let (three, four) = (g.int(3), g.int(4));
let hit = g.op(
"BinarySearch",
&[],
&[(v, port, "vec"), (three, "value", "target")],
"result",
);
let miss = g.op(
"BinarySearch",
&[],
&[(v, port, "vec"), (four, "value", "target")],
"result",
);
g.op("IsOk", &[], &[(hit, "result", "input")], "result");
g.op("IsErr", &[], &[(miss, "result", "input")], "result");
out.push(g.case("binary_search"));
// ---- code blocks ------------------------------------------------------------------------
let mut g = G::new();
let four = g.int(4);
g.op(
"CodeBlock",
&[("src", "let y = input * 2; y + 1")],
&[(four, "value", "inputs")],
"outputs",
);
g.op(
"CodeBlock",
&[("src", "if input > 3 { \"big\" } else { \"small\" }")],
&[(four, "value", "inputs")],
"outputs",
);
g.op("CodeBlock", &[], &[], "outputs");
out.push(g.case("code block"));
// ---- hand-written float maths: the editor evaluates it with flowrs-math, and the program
// takes it from there too (the generator renames `x.sin()` to `x.flowrs_sin()`): left as
// written, the program's `sin` would be the platform's, which differs from flowrs-math's
// (musl's) on a few percent of inputs. Each block returns 200 values, every one compared:
// a sum would round most last-bit differences away.
let mut g = G::new();
let x0 = g.lit("FloatLiteral", "0.5");
for (f, scale) in [
("X.sin()", 7.31),
("X.cos()", 7.31),
("X.tan()", 0.731),
("(X / 300.0).asin()", 1.0),
("(X / 300.0).acos()", 1.0),
("X.atan()", 0.731),
("(X / 9.0).exp()", 0.731),
("X.ln()", 13.7),
("X.log10()", 13.7),
("X.log2()", 13.7),
("X.powf(0.37)", 13.7),
("X.powi(7)", 0.0731),
("X.atan2(3.0)", 0.731),
("X.hypot(2.5)", 0.731),
("X.cbrt()", 13.7),
] {
let values: Vec<String> = (0..200)
.map(|i| f.replace('X', &format!("(input + {i}.0 * {scale:?})")))
.collect();
g.op(
"CodeBlock",
&[("src", &format!("vec![{}]", values.join(", ")))],
&[(x0, "value", "inputs")],
"outputs",
);
}
out.push(g.case("hand-written maths"));
let mut g = G::new();
let arr = g.n("Control", &[("widget", "array"), ("value", "1, 2")]);
g.op(
"CodeBlock",
&[("src", "input[5]")],
&[(arr, "value", "inputs")],
"outputs",
);
out.push(g.case("index out of bounds panics"));
// ---- the editor's primitives, on inputs both tiers type the same way --------------------
let mut g = G::new();
let (a, b) = (g.int(-7), g.int(3));
for op in ["Add", "Subtract", "Multiply", "Divide", "Modulo"] {
g.op(op, &[], &[(a, "value", "a"), (b, "value", "b")], "result");
}
for op in [
"Equal",
"NotEqual",
"LessThan",
"LessOrEqual",
"GreaterThan",
"GreaterOrEqual",
] {
g.op(op, &[], &[(a, "value", "a"), (b, "value", "b")], "result");
}
let x = g.lit("FloatLiteral", "2.25");
g.op("Negate", &[], &[(x, "value", "input")], "result");
for op in ["Sin", "Cos", "Sqrt", "Abs"] {
g.op(op, &[], &[(x, "value", "input")], "result");
}
let s = g.lit("StringLiteral", "abc");
g.op("Clone", &[], &[(s, "value", "input")], "output");
let t = g.lit("BoolLiteral", "true");
g.op(
"IfThenElse",
&[],
&[
(t, "value", "condition"),
(a, "value", "then_value"),
(b, "value", "else_value"),
],
"result",
);
let five = g.n("Control", &[("value", "5")]);
g.p(five, "value");
let frac = g.n("Control", &[("value", "1.5")]);
g.p(frac, "value");
out.push(g.case("editor scalars"));
// ---- a FloatLiteral with no Rust literal: NaN and the infinities -------------------
// `{:?}` of them is `NaN`, `inf`, `-inf`, which the generator pasted as `NaNf64` (no
// such literal: the program did not compile); it names the constants now, with the sign
// the editor parsed, and a Control's array does the same.
let mut g = G::new();
for v in ["NaN", "inf", "-inf", "infinity", "-NaN"] {
let x = g.lit("FloatLiteral", v);
g.p(x, "value");
g.op("Abs", &[], &[(x, "value", "input")], "result");
g.op("Sin", &[], &[(x, "value", "input")], "result");
}
let arr = g.n(
"Control",
&[("widget", "array"), ("value", "1, NaN, inf, -inf")],
);
g.p(arr, "value");
out.push(g.case("non-finite literals"));
let mut g = G::new();
let arr = g.n("Control", &[("widget", "array"), ("value", "1, 4, 9, 0.5")]);
g.p(arr, "value");
for op in ["MapSin", "MapCos", "MapSquare", "MapSqrt"] {
g.op(op, &[], &[(arr, "value", "input")], "values");
}
let k = g.lit("FloatLiteral", "-2.0");
g.op(
"MapScale",
&[],
&[(arr, "value", "input"), (k, "value", "factor")],
"values",
);
for op in ["Mean", "Sum", "Min", "Max", "Std", "Count"] {
g.op(op, &[], &[(arr, "value", "input")], "result");
}
let (lo, hi, n) = (
g.lit("FloatLiteral", "0.0"),
g.lit("FloatLiteral", "1.0"),
g.int(5),
);
g.op(
"Linspace",
&[],
&[
(lo, "value", "start"),
(hi, "value", "end"),
(n, "value", "count"),
],
"values",
);
let (v, port) = g.vec(&[4, 5, 6]);
g.op("VecLen", &[], &[(v, port, "vec")], "length");
g.p(v, port);
out.push(g.case("editor series"));
// ---- io.print: `{:?}` by std at both tiers -----------------------------------------
let mut g = G::new();
g.console = true;
let n = g.int(-7);
g.op("io.print", &[], &[(n, "value", "value")], "");
let x = g.lit("FloatLiteral", "2.5");
g.op("io.print", &[("label", "x")], &[(x, "value", "value")], "");
let quoted = g.lit("StringLiteral", "hi \"there\"\tok");
g.op("io.print", &[], &[(quoted, "value", "value")], "");
let arr = g.n("Control", &[("widget", "array"), ("value", "1, 4.5, 9")]);
g.op(
"io.print",
&[("label", "arr")],
&[(arr, "value", "value")],
"",
);
let t = g.lit("BoolLiteral", "true");
g.op("io.print", &[], &[(t, "value", "value")], "");
out.push(g.case("io.print"));
// ---- in place: a binding changed by one node is changed for every later reader ------
// `v0.push(1)` and then `v0.len()` through the FIRST wire: the program reads the
// pushed Vec, and so must T0 (it read the Vec as `VecNew` made it, and said 0).
let mut g = G::new();
let v = g.n("VecNew", &[]);
let one = g.int(1);
g.op(
"VecPush",
&[],
&[(v, "vec", "vec"), (one, "value", "item")],
"",
);
g.op("VecLen", &[], &[(v, "vec", "vec")], "length");
let m = g.n("HashMapNew", &[]);
let (a, two) = (g.lit("StringLiteral", "a"), g.int(2));
g.op(
"HashMapInsert",
&[],
&[
(m, "map", "map"),
(a, "value", "key"),
(two, "value", "value"),
],
"",
);
g.op(
"HashMapGet",
&[],
&[(m, "map", "map"), (a, "value", "key")],
"value",
);
let w = g.n("VecNew", &[]);
let mut at = (w, "vec");
for i in [3, 1, 2] {
let item = g.int(i);
at = (
g.op(
"VecPush",
&[],
&[(at.0, at.1, "vec"), (item, "value", "item")],
"",
),
"vec",
);
}
g.op("Sort", &[], &[(at.0, at.1, "vec")], "");
let first = g.n("Control", &[("value", "0")]);
g.op(
"VecGet",
&[],
&[(w, "vec", "vec"), (first, "value", "index")],
"item",
);
out.push(g.case("in place, read through the first wire"));
// ---- standard input and the run's own files ----------------------------------------
// Both tiers read an empty standard input (a Playground run's, a check's): `Ok("")`.
let mut g = G::new();
g.op("ReadLine", &[], &[], "line");
g.op("ReadLine", &[], &[], "line");
let path = g.lit("StringLiteral", "primexec_written.txt");
let text = g.lit("StringLiteral", "written by the run");
g.op(
"WriteFile",
&[],
&[(path, "value", "path"), (text, "value", "contents")],
"result",
);
// Created after the write, so it reads after it in both tiers' order.
let back = g.op("ReadFile", &[], &[(path, "value", "path")], "contents");
g.op("IsOk", &[], &[(back, "contents", "input")], "result");
let empty_path = g.lit("StringLiteral", "primexec_empty.txt");
g.op("WriteFile", &[], &[(empty_path, "value", "path")], "result");
g.op(
"ReadFile",
&[],
&[(empty_path, "value", "path")],
"contents",
);
out.push(g.case("stdin and written files"));
// ---- threads and channels ----------------------------------------------------------
let mut g = G::new();
let t = g.op(
"Spawn",
&[("src", "let v = vec![1, 2, 3]; v.len() * 14")],
&[],
"",
);
g.op("Join", &[], &[(t, "handle", "handle")], "result");
let t2 = g.op("Spawn", &[("src", "6 * 7")], &[], "");
g.op("Join", &[], &[(t2, "handle", "handle")], "result");
// A thread that panics: the program's `join` is `Err`, and nothing else stops.
let bad = g.op(
"Spawn",
&[("src", "if true { panic!(\"in the thread\") } 1")],
&[],
"",
);
let joined = g.op("Join", &[], &[(bad, "handle", "handle")], "result");
g.op("IsErr", &[], &[(joined, "result", "input")], "result");
out.push(g.case("spawn and join"));
let mut g = G::new();
let ch = g.n("ChannelNew", &[]);
let (five, seven) = (g.int(5), g.int(7));
g.op(
"Send",
&[],
&[(ch, "sender", "sender"), (five, "value", "value")],
"result",
);
g.op(
"Send",
&[],
&[(ch, "sender", "sender"), (seven, "value", "value")],
"result",
);
g.op("Recv", &[], &[(ch, "receiver", "receiver")], "value");
g.op("Recv", &[], &[(ch, "receiver", "receiver")], "value");
out.push(g.case("channel"));
// ---- code in the program's scope ---------------------------------------------------
let mut g = G::new();
g.console = true;
g.n(
"CodeBlockNode",
&[(
"src",
"let k = 3;\nlet mut total = 0;\nfor i in 0..4 { total += i * k; }\nprintln!(\"total {total}\");",
)],
);
// What the block declared is in scope for the code after it.
g.op("CodeBlock", &[("src", "k * 2")], &[], "outputs");
let (lo, hi) = (g.int(0), g.int(3));
let range = g.op(
"Range",
&[],
&[(lo, "value", "start"), (hi, "value", "end")],
"",
);
let scaled = g.op(
"Map",
&[("src", "x * k")],
&[(range, "iterator", "iterator")],
"",
);
g.op(
"Collect",
&[],
&[(scaled, "iterator", "iterator")],
"collection",
);
// Formatting is std's: width, fill, alignment, sign, zero padding, precision, `{:?}`,
// hex, a named argument and a captured name.
g.n(
"CodeBlockNode",
&[(
"src",
"let w = 4;\nprintln!(\"[{:>6.2}|{:<4}|{:+}|{:08.3}|{:?}|{:x}|{:*^7}|{w}|{n:>3}]\", 3.14159, \"ab\", 5, -2.5, \"q\", 255, \"mid\", n = 9);\nprint!(\"{:?} {:e} {}\", vec![1.5, 2.0], 1234.5, true);\nprintln!();",
)],
);
out.push(g.case("code in the program's scope"));
// ---- loops: the body runs once per pass ----------------------------------------------
let mut g = G::new();
g.console = true;
let (lo, hi) = (g.int(0), g.int(3));
let range = g.op(
"Range",
&[],
&[(lo, "value", "start"), (hi, "value", "end")],
"",
);
let body = g.n("CodeBlockNode", &[("src", "println!(\"for {}\", x);")]);
let l = g.n("ForLoop", &[]);
g.w(range, "iterator", l, "iterator");
g.w(body, "outputs", l, "body");
// Accumulating into a Vec made outside the loop, read after it.
let acc = g.n("VecNew", &[]);
let (lo2, hi2) = (g.int(1), g.int(4));
let range2 = g.op(
"Range",
&[],
&[(lo2, "value", "start"), (hi2, "value", "end")],
"",
);
let tenx = g.n("CodeBlock", &[("src", "x * 10")]);
let push = g.n("VecPush", &[]);
g.w(acc, "vec", push, "vec");
g.w(tenx, "outputs", push, "item");
let l2 = g.n("ForLoop", &[]);
g.w(range2, "iterator", l2, "iterator");
g.w(push, "vec", l2, "body");
let len = g.op("VecLen", &[], &[(acc, "vec", "vec")], "length");
let _ = len;
let sum = g.n("Fold", &[("src", "acc + x")]);
let zero = g.int(0);
let it = g.op("IntoIter", &[], &[(acc, "vec", "collection")], "");
g.w(it, "iterator", sum, "iterator");
g.w(zero, "value", sum, "init");
g.p(sum, "result");
out.push(g.case("for loops"));
let mut g = G::new();
g.console = true;
// `let mut i = 0;` in main, then `while i < 3 { … i += 1; }`.
g.n("CodeBlockNode", &[("src", "let mut i = 0;")]);
let wbody = g.n(
"CodeBlockNode",
&[("src", "println!(\"while {i}\");\ni += 1;")],
);
let wl = g.n("WhileLoop", &[("src", "i < 3")]);
g.w(wbody, "outputs", wl, "body");
// `loop { …; break; }` with a Break node after a statement.
let once = g.n("CodeBlockNode", &[("src", "println!(\"loop once\");")]);
let brk = g.n("Break", &[]);
g.w(once, "outputs", brk, "after");
let lp = g.n("Loop", &[]);
g.w(brk, "signal", lp, "body");
// `continue` in code, and a Continue node that skips what follows it.
let (lo, hi) = (g.int(0), g.int(5));
let range = g.op(
"Range",
&[],
&[(lo, "value", "start"), (hi, "value", "end")],
"",
);
let odd = g.n(
"CodeBlockNode",
&[("src", "if x % 2 == 0 { continue; }\nprintln!(\"odd {x}\");")],
);
let cont = g.n("Continue", &[]);
g.w(odd, "outputs", cont, "after");
let never = g.n("CodeBlockNode", &[("src", "println!(\"never\");")]);
g.w(cont, "signal", never, "after");
let fl = g.n("ForLoop", &[]);
g.w(range, "iterator", fl, "iterator");
g.w(never, "outputs", fl, "body");
// A `loop` in code that counts down and breaks.
g.n(
"CodeBlockNode",
&[(
"src",
"let mut n = 3;\nloop { if n == 0 { break; } print!(\"{n} \"); n -= 1; }\nlet mut j = 0;\nwhile j < 2 { j += 1; }\nprintln!(\"j={j}\");",
)],
);
out.push(g.case("while, loop, break and continue"));
let mut g = G::new();
g.console = true;
// LoopNode: the iterator collected, the body over `&item`, the Vec on `result`.
let (lo, hi) = (g.int(2), g.int(5));
let range = g.op(
"Range",
&[],
&[(lo, "value", "start"), (hi, "value", "end")],
"",
);
let lbody = g.n("CodeBlockNode", &[("src", "println!(\"ln {}\", x * 2);")]);
let ln = g.n("LoopNode", &[]);
g.w(range, "iterator", ln, "iterator");
g.w(lbody, "outputs", ln, "body");
g.p(ln, "result");
// A loop in a loop's body: the inner body runs once per inner pass, per outer pass.
let (a, b) = (g.int(0), g.int(2));
let outer_range = g.op(
"Range",
&[],
&[(a, "value", "start"), (b, "value", "end")],
"",
);
let (c, d) = (g.int(10), g.int(12));
let inner_range = g.op(
"Range",
&[],
&[(c, "value", "start"), (d, "value", "end")],
"",
);
let ibody = g.n("CodeBlockNode", &[("src", "print!(\"{x} \");")]);
let inner = g.n("ForLoop", &[]);
g.w(inner_range, "iterator", inner, "iterator");
g.w(ibody, "outputs", inner, "body");
let outer = g.n("ForLoop", &[]);
g.w(outer_range, "iterator", outer, "iterator");
g.w(inner, "done", outer, "body");
let nl = g.n("CodeBlockNode", &[("src", "println!();")]);
let _ = nl;
out.push(g.case("loop node and nested loops"));
// A body that panics on its third pass: what it printed before stays printed, and
// T0 refuses where the program panics.
let mut g = G::new();
g.console = true;
let (lo, hi) = (g.int(0), g.int(5));
let range = g.op(
"Range",
&[],
&[(lo, "value", "start"), (hi, "value", "end")],
"",
);
let pbody = g.n(
"CodeBlockNode",
&[(
"src",
"println!(\"pass {x}\");\nassert!(x < 2, \"x is {x}\");",
)],
);
let pl = g.n("ForLoop", &[]);
g.w(range, "iterator", pl, "iterator");
g.w(pbody, "outputs", pl, "body");
out.push(g.case("a loop body that panics"));
let mut g = G::new();
g.console = true;
let n = g.int(42);
g.op("PrintLn", &[], &[(n, "value", "input")], "");
let s = g.lit("StringLiteral", "done");
g.op("PrintLn", &[], &[(s, "value", "input")], "");
out.push(g.case("println"));
out
}
#[test]
fn every_primitive_computes_what_its_program_computes() {
let cases = cases();
let dir = std::env::temp_dir().join("flowrs_primitive_tiers");
let _ = std::fs::remove_dir_all(&dir);
let report = run_graphs(&cases, &dir).unwrap_or_else(|e| panic!("{e}"));
eprintln!(
"{} cases, {} comparisons, skipped: {:?}",
cases.len(),
report.compared,
report.skipped
);
assert!(report.skipped.is_empty(), "{:?}", report.skipped);
// Only the cases written to panic may: a panic ends a program early, so one in any
// other case would quietly drop every comparison after it.
assert_eq!(
report.faulted,
[
"unwrap of nothing panics",
"overflow panics",
"index out of bounds panics",
"a loop body that panics"
],
);
assert!(
report.compared >= 150,
"only {} comparisons were made",
report.compared
);
assert!(
report.disagreements.is_empty(),
"{} of {} comparisons disagree between the executor and the compiled program:\n {}",
report.disagreements.len(),
report.compared,
report.disagreements.join("\n ")
);
}
/// Does `from`'s output reach a probed port in `c` — the port itself, or one downstream?
///
/// An iterator (`Range`, `Map`) or a struct (`StructNew`) has no program-side rendering
/// T0 can match, so it is compared through what reads it: a `Collect`, a `FieldGet`. A
/// node whose value reaches no probe is not compared at all.
fn feeds_a_probe(c: &GraphCase, from: NodeId) -> bool {
let mut seen = std::collections::HashSet::new();
let mut stack = vec![from];
while let Some(n) = stack.pop() {
if !seen.insert(n) {
continue;
}
if c.probes.iter().any(|(p, _)| *p == n) {
return true;
}
stack.extend(
c.graph
.wires
.values()
.filter(|w| w.from_node == n)
.map(|w| w.to_node),
);
}
false
}
/// Each primitive is compared by some case — its value reaches a probe, or (with no
/// output port) the console or the program running at all — or `NOT_COMPARED` says why.
#[test]
fn every_primitive_is_compared_or_explained() {
let cases = cases();
let mut missing = Vec::new();
for t in PRIMITIVES.iter().chain(LOOPS) {
let compared = cases.iter().any(|c| {
c.graph.nodes.values().any(|n| {
n.type_id == *t
&& (feeds_a_probe(c, n.id)
|| (c.console && SAID_ON_THE_CONSOLE.contains(t))
|| *t == "Drop")
})
});
let explained = NOT_COMPARED.iter().any(|(x, _)| x == t);
match (compared, explained) {
(false, false) => missing.push(format!("{t}: no case compares it")),
(true, true) => missing.push(format!("{t}: compared, so remove it from NOT_COMPARED")),
_ => {}
}
}
assert!(missing.is_empty(), "{}", missing.join("\n"));
}
/// The harness must be able to fail. A refusal paired with a panic agrees; a port the
/// executor fills and the program never binds (an `Indicator` feeds the front panel, the
/// program only prints it) is reported, not skipped.
#[test]
fn the_differential_reports_what_it_cannot_match() {
// `x / acc` with init 0 divides by zero on the first item: the program panics, the
// executor refuses, and that is agreement.
let mut g = G::new();
let (lo, hi, zero) = (g.int(1), g.int(3), g.int(0));
let range = g.op(
"Range",
&[],
&[(lo, "value", "start"), (hi, "value", "end")],
"",
);
g.op(
"Fold",
&[("src", "x / acc")],
&[(range, "iterator", "iterator"), (zero, "value", "init")],
"result",
);
let agree = g.case("refusal and panic");
let mut g = G::new();
let one = g.int(1);
g.op("Indicator", &[], &[(one, "value", "input")], "input");
let unbound = g.case("indicator");
let dir = std::env::temp_dir().join("flowrs_primitive_tiers_self_test");
let _ = std::fs::remove_dir_all(&dir);
let report = run_graphs(&[agree, unbound], &dir).unwrap_or_else(|e| panic!("{e}"));
assert_eq!(report.disagreements.len(), 1, "{:?}", report.disagreements);
assert!(
report.disagreements[0].contains("indicator")
&& report.disagreements[0].contains("printed no value"),
"{:?}",
report.disagreements
);
}