1268 lines
40 KiB
Rust
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
|
|
);
|
|
}
|