//! 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)>, 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` 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 { 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 = (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 ); }