Built alone from an empty CARGO_HOME, its tests passed, and flowrsc checked, ran and exported a document from the conformance corpus; the export built alone. Co-Authored-By: Claude Opus 5.5 <[email protected]>
1357 lines
47 KiB
Rust
1357 lines
47 KiB
Rust
//! The published document specification (`spec/flowrs-document-2.md`, its schema and its
|
|
//! conformance corpus) held to the code that reads documents and graph texts. Version 1.0.0's
|
|
//! prose and schema stay as they were published (`spec::PREVIOUS`).
|
|
//!
|
|
//! A specification is only worth publishing if it describes the reader people actually run,
|
|
//! so each claim it makes is read back here against that reader:
|
|
//!
|
|
//! - the committed schema is `spec::schema()`, and is itself a draft 2020-12 schema;
|
|
//! - a document that sets every member (written from the types' own struct literals, so a
|
|
//! field added to any of them does not compile here until it is given a value) writes
|
|
//! exactly the members the schema declares, validates, and is the committed
|
|
//! `every-member.flowrs`;
|
|
//! - for every member the schema declares, dropping it or giving it a value of the wrong type
|
|
//! leaves the schema and `Document::from_json` agreeing: both accept, or both refuse, and a
|
|
//! member is required in the schema exactly when the reader needs it;
|
|
//! - the conformance corpus is read as its manifest says, and every document and graph text
|
|
//! in the repository conforms (texts round-trip through the printer);
|
|
//! - the prose's tables are the schema's members, types and required flags, its modes and
|
|
//! authors the code's, its escapes, keywords, punctuation and modifiers the lexer's and the
|
|
//! parser's, its older spellings the ones read, its fingerprint the one computed, and its
|
|
//! examples parse, round-trip, or fail where it says.
|
|
//!
|
|
//! `FLOWRS_SPEC_BLESS=1` rewrites the three generated files (the schema, `every-member.flowrs`
|
|
//! and `functions.flowrs`) instead of comparing them.
|
|
|
|
use flowrs_core::graph::document::{
|
|
AppSpec, AppTurn, CardPlace, Document, FORMAT, FunctionDoc, LoadError, PanelLayout,
|
|
};
|
|
use flowrs_core::graph::dsl;
|
|
use flowrs_core::graph::ir::{Group, GroupId, Node, NodeId, Wire, WireId, WireMode};
|
|
use flowrs_core::graph::provenance::{Author, NodeOrigin};
|
|
use flowrs_core::graph::spec;
|
|
use serde_json::{Value, json};
|
|
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
|
use std::path::{Path, PathBuf};
|
|
|
|
fn root() -> PathBuf {
|
|
Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
.parent()
|
|
.and_then(|p| p.parent())
|
|
.unwrap()
|
|
.to_path_buf()
|
|
}
|
|
|
|
fn read(rel: &str) -> String {
|
|
std::fs::read_to_string(root().join(rel)).unwrap_or_else(|e| panic!("{rel}: {e}"))
|
|
}
|
|
|
|
fn bless() -> bool {
|
|
std::env::var("FLOWRS_SPEC_BLESS").is_ok_and(|v| v == "1")
|
|
}
|
|
|
|
/// `path` holds `text`, or, blessing, is made to.
|
|
fn holds(rel: &str, text: &str) {
|
|
if bless() {
|
|
std::fs::write(root().join(rel), text).unwrap();
|
|
return;
|
|
}
|
|
assert!(
|
|
read(rel) == text,
|
|
"{rel} is not what the code generates; rerun with FLOWRS_SPEC_BLESS=1 and review the diff"
|
|
);
|
|
}
|
|
|
|
fn schema() -> Value {
|
|
serde_json::from_str(&spec::schema()).unwrap()
|
|
}
|
|
|
|
fn validator() -> jsonschema::Validator {
|
|
jsonschema::options()
|
|
.build(&schema())
|
|
.expect("the schema compiles")
|
|
}
|
|
|
|
fn conforms(v: &jsonschema::Validator, text: &str) -> bool {
|
|
serde_json::from_str::<Value>(text).is_ok_and(|x| v.is_valid(&x))
|
|
}
|
|
|
|
/// What the reader does with `text`: `accepts`, `malformed` or `too_new`.
|
|
fn reader(text: &str) -> &'static str {
|
|
match Document::from_json(text) {
|
|
Ok(_) => "accepts",
|
|
Err(LoadError::Malformed(_)) => "malformed",
|
|
Err(LoadError::TooNew { .. }) => "too_new",
|
|
}
|
|
}
|
|
|
|
/// Whether `text` is the canonical form: what the reader read, written again, is the same
|
|
/// bytes, and every array is in id order.
|
|
fn canonical(text: &str) -> bool {
|
|
let Ok(d) = Document::from_json(text) else {
|
|
return false;
|
|
};
|
|
let sorted = |ids: Vec<u64>| ids.windows(2).all(|w| w[0] < w[1]);
|
|
d.to_json() == text.trim_end()
|
|
&& sorted(d.nodes.iter().map(|n| n.id.0).collect())
|
|
&& sorted(d.wires.iter().map(|w| w.id.0).collect())
|
|
&& sorted(d.groups.iter().map(|g| g.id.0).collect())
|
|
}
|
|
|
|
// -------------------------------------------------------------------------------------
|
|
// The document that sets every member.
|
|
// -------------------------------------------------------------------------------------
|
|
|
|
/// Every member of every object, each holding something other than its default, so each is
|
|
/// written. Struct literals with no `..`: a field added to any of these types fails to
|
|
/// compile here until it has a value, and then the schema must declare it.
|
|
fn every_member() -> Document {
|
|
let node = |id: u64,
|
|
type_id: &str,
|
|
props: &[(&str, &str)],
|
|
position: (f32, f32),
|
|
group: Option<u64>,
|
|
origin: NodeOrigin| Node {
|
|
id: NodeId(id),
|
|
type_id: type_id.into(),
|
|
properties: props
|
|
.iter()
|
|
.map(|(k, v)| (k.to_string(), v.to_string()))
|
|
.collect::<HashMap<_, _>>(),
|
|
position,
|
|
group: group.map(GroupId),
|
|
origin,
|
|
};
|
|
let wire = |id: u64, from: u64, fp: &str, to: u64, tp: &str, mode, feedback, origin| Wire {
|
|
id: WireId(id),
|
|
from_node: NodeId(from),
|
|
from_port: fp.into(),
|
|
to_node: NodeId(to),
|
|
to_port: tp.into(),
|
|
mode,
|
|
feedback,
|
|
origin,
|
|
};
|
|
Document {
|
|
format: FORMAT,
|
|
generator: "spec/conformance: every member".into(),
|
|
nodes: vec![
|
|
node(
|
|
0,
|
|
"FloatLiteral",
|
|
&[("value", "2"), ("unit", "V")],
|
|
(10.0, 20.0),
|
|
Some(1),
|
|
NodeOrigin {
|
|
node: Author::Human,
|
|
params: BTreeMap::from([
|
|
("unit".to_string(), Author::Unknown),
|
|
(
|
|
"value".to_string(),
|
|
Author::agent("anthropic/claude-example", "c1"),
|
|
),
|
|
]),
|
|
inputs: BTreeMap::from([("in".to_string(), Author::Mixed)]),
|
|
},
|
|
),
|
|
node(
|
|
1,
|
|
"math.add",
|
|
&[],
|
|
(260.0, 20.0),
|
|
None,
|
|
NodeOrigin {
|
|
node: Author::Imported {
|
|
dialect: "comfyui".into(),
|
|
},
|
|
params: BTreeMap::new(),
|
|
inputs: BTreeMap::new(),
|
|
},
|
|
),
|
|
node(
|
|
2,
|
|
"io.print",
|
|
&[("label", "sum")],
|
|
(520.0, 20.0),
|
|
Some(0),
|
|
NodeOrigin::default(),
|
|
),
|
|
node(
|
|
3,
|
|
"Call",
|
|
&[("fn", "scale@1"), ("k", "3")],
|
|
(260.0, 140.0),
|
|
None,
|
|
NodeOrigin::default(),
|
|
),
|
|
],
|
|
wires: vec![
|
|
wire(0, 0, "value", 1, "a", WireMode::Move, false, Author::Human),
|
|
wire(
|
|
1,
|
|
1,
|
|
"sum",
|
|
1,
|
|
"b",
|
|
WireMode::Copy,
|
|
true,
|
|
Author::agent("anthropic/claude-example", "c2"),
|
|
),
|
|
wire(
|
|
2,
|
|
1,
|
|
"sum",
|
|
2,
|
|
"value",
|
|
WireMode::Borrow,
|
|
false,
|
|
Author::Unknown,
|
|
),
|
|
wire(
|
|
3,
|
|
0,
|
|
"value",
|
|
2,
|
|
"value",
|
|
WireMode::BorrowMut,
|
|
false,
|
|
Author::Unknown,
|
|
),
|
|
wire(
|
|
4,
|
|
0,
|
|
"value",
|
|
2,
|
|
"value",
|
|
WireMode::Clone,
|
|
false,
|
|
Author::Unknown,
|
|
),
|
|
wire(
|
|
5,
|
|
0,
|
|
"value",
|
|
3,
|
|
"x",
|
|
WireMode::Copy,
|
|
false,
|
|
Author::Unknown,
|
|
),
|
|
],
|
|
groups: vec![
|
|
Group {
|
|
id: GroupId(0),
|
|
name: "Outer".into(),
|
|
parent: None,
|
|
position: (0.0, 0.0),
|
|
},
|
|
Group {
|
|
id: GroupId(1),
|
|
name: "Inner".into(),
|
|
parent: Some(GroupId(0)),
|
|
position: (40.0, 60.0),
|
|
},
|
|
],
|
|
functions: vec![FunctionDoc {
|
|
name: "scale".into(),
|
|
version: 1,
|
|
nodes: vec![
|
|
node(
|
|
0,
|
|
"FunctionInput",
|
|
&[("name", "x"), ("type", "f64")],
|
|
(0.0, 0.0),
|
|
None,
|
|
NodeOrigin::default(),
|
|
),
|
|
node(
|
|
1,
|
|
"FunctionInput",
|
|
&[
|
|
("default", "2"),
|
|
("index", "1"),
|
|
("name", "k"),
|
|
("type", "f64"),
|
|
],
|
|
(0.0, 120.0),
|
|
None,
|
|
NodeOrigin::default(),
|
|
),
|
|
node(
|
|
2,
|
|
"math.mul",
|
|
&[],
|
|
(220.0, 60.0),
|
|
Some(0),
|
|
NodeOrigin::default(),
|
|
),
|
|
node(
|
|
3,
|
|
"FunctionOutput",
|
|
&[("name", "y"), ("type", "f64")],
|
|
(440.0, 60.0),
|
|
None,
|
|
NodeOrigin::default(),
|
|
),
|
|
],
|
|
wires: vec![
|
|
wire(
|
|
0,
|
|
0,
|
|
"value",
|
|
2,
|
|
"a",
|
|
WireMode::Move,
|
|
false,
|
|
Author::Unknown,
|
|
),
|
|
wire(
|
|
1,
|
|
1,
|
|
"value",
|
|
2,
|
|
"b",
|
|
WireMode::Move,
|
|
false,
|
|
Author::Unknown,
|
|
),
|
|
wire(
|
|
2,
|
|
2,
|
|
"product",
|
|
3,
|
|
"value",
|
|
WireMode::Move,
|
|
false,
|
|
Author::Unknown,
|
|
),
|
|
],
|
|
groups: vec![Group {
|
|
id: GroupId(0),
|
|
name: "Body".into(),
|
|
parent: None,
|
|
position: (220.0, 60.0),
|
|
}],
|
|
}],
|
|
widgets: BTreeMap::from([("math.add".to_string(), "0123456789abcdef".to_string())]),
|
|
app: Some(AppSpec {
|
|
version: "1.0".into(),
|
|
library: "flowrs-app@1".into(),
|
|
source: "root = Stack([total])\ntotal = Metric(\"Sum\", n1.sum)".into(),
|
|
skin: "paper".into(),
|
|
panel: PanelLayout {
|
|
tabs: vec!["Main".into(), "Detail".into()],
|
|
cards: vec![CardPlace {
|
|
card: "ctl:0".into(),
|
|
col: 1,
|
|
row: 2,
|
|
tab: Some(1),
|
|
}],
|
|
},
|
|
title: "Add a number to itself".into(),
|
|
description: "Every member of the document, set.".into(),
|
|
steps: 40,
|
|
turns: vec![AppTurn {
|
|
request: "show the sum as a reading".into(),
|
|
checks: vec!["shows n1.sum as reading".into()],
|
|
}],
|
|
prefer: vec!["R06=Scope".into()],
|
|
}),
|
|
}
|
|
}
|
|
|
|
/// One member as the schema sees it in a document: which object it is in (the `$defs` name,
|
|
/// `#` for the document, `<def>.<member>` for a map), its name (`*` for a map's entries), its
|
|
/// schema, whether that object requires it, and where the object is (a JSON pointer).
|
|
#[derive(Clone, Debug)]
|
|
struct Site {
|
|
def: String,
|
|
key: String,
|
|
schema: Value,
|
|
required: bool,
|
|
at: String,
|
|
}
|
|
|
|
fn escape(k: &str) -> String {
|
|
k.replace('~', "~0").replace('/', "~1")
|
|
}
|
|
|
|
/// Walk `v` alongside its schema `s`, listing every member it holds.
|
|
fn walk(root: &Value, s: &Value, def: &str, v: &Value, at: &str, out: &mut Vec<Site>) {
|
|
if let Some(r) = s.get("$ref").and_then(Value::as_str) {
|
|
let name = r.trim_start_matches("#/$defs/");
|
|
return walk(root, &root["$defs"][name], name, v, at, out);
|
|
}
|
|
if let Some(branches) = s.get("oneOf").and_then(Value::as_array) {
|
|
if v.is_null() {
|
|
return;
|
|
}
|
|
let branch = branches
|
|
.iter()
|
|
.find(|b| match v.get("by") {
|
|
Some(by) => b["properties"]["by"]["const"] == *by,
|
|
None => b.get("type") != Some(&json!("null")),
|
|
})
|
|
.unwrap_or_else(|| panic!("{def} at {at}: no branch of the schema fits {v}"));
|
|
return walk(root, branch, def, v, at, out);
|
|
}
|
|
match v {
|
|
Value::Object(o) => {
|
|
if let Some(props) = s.get("properties").and_then(Value::as_object) {
|
|
let required: Vec<&str> = s["required"]
|
|
.as_array()
|
|
.map(|r| r.iter().filter_map(Value::as_str).collect())
|
|
.unwrap_or_default();
|
|
for (k, x) in o {
|
|
let ps = props
|
|
.get(k)
|
|
.unwrap_or_else(|| panic!("{def}: `{k}` is written but not declared"));
|
|
out.push(Site {
|
|
def: def.into(),
|
|
key: k.clone(),
|
|
schema: ps.clone(),
|
|
required: required.contains(&k.as_str()),
|
|
at: at.into(),
|
|
});
|
|
walk(
|
|
root,
|
|
ps,
|
|
&format!("{def}.{k}"),
|
|
x,
|
|
&format!("{at}/{}", escape(k)),
|
|
out,
|
|
);
|
|
}
|
|
} else if let Some(ap) = s.get("additionalProperties").filter(|a| a.is_object()) {
|
|
for (k, x) in o {
|
|
out.push(Site {
|
|
def: def.into(),
|
|
key: "*".into(),
|
|
schema: ap.clone(),
|
|
required: false,
|
|
at: format!("{at}/{}", escape(k)),
|
|
});
|
|
walk(root, ap, def, x, &format!("{at}/{}", escape(k)), out);
|
|
}
|
|
}
|
|
}
|
|
Value::Array(a) => {
|
|
if let Some(items) = s.get("items").filter(|i| i.is_object()) {
|
|
for (i, x) in a.iter().enumerate() {
|
|
walk(root, items, def, x, &format!("{at}/{i}"), out);
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
/// Every member the schema declares, as [`walk`] names them.
|
|
fn declared(root: &Value) -> BTreeSet<(String, String)> {
|
|
let mut out = BTreeSet::new();
|
|
let object = |def: &str, s: &Value, out: &mut BTreeSet<(String, String)>| {
|
|
let mut shapes = vec![s];
|
|
if let Some(b) = s.get("oneOf").and_then(Value::as_array) {
|
|
shapes.extend(b.iter());
|
|
}
|
|
for shape in shapes {
|
|
for (k, p) in shape
|
|
.get("properties")
|
|
.and_then(Value::as_object)
|
|
.into_iter()
|
|
.flatten()
|
|
{
|
|
out.insert((def.to_string(), k.clone()));
|
|
if p.get("additionalProperties").is_some_and(Value::is_object) {
|
|
out.insert((format!("{def}.{k}"), "*".into()));
|
|
}
|
|
}
|
|
}
|
|
};
|
|
object("#", root, &mut out);
|
|
for (name, s) in root["$defs"].as_object().unwrap() {
|
|
object(name, s, &mut out);
|
|
}
|
|
out
|
|
}
|
|
|
|
/// A value of a type the member does not take.
|
|
fn wrong(root: &Value, s: &Value) -> Value {
|
|
if let Some(r) = s.get("$ref").and_then(Value::as_str) {
|
|
return wrong(root, &root["$defs"][r.trim_start_matches("#/$defs/")]);
|
|
}
|
|
if let Some(b) = s.get("oneOf").and_then(Value::as_array) {
|
|
// An author (objects) takes no number; an optional id (null or an id) no string.
|
|
return if b.iter().any(|x| x.get("type") == Some(&json!("object"))) {
|
|
json!(7)
|
|
} else {
|
|
json!("x")
|
|
};
|
|
}
|
|
match s.get("const") {
|
|
Some(Value::Number(_)) => return json!("1"),
|
|
Some(Value::String(_)) => return json!(12345),
|
|
_ => {}
|
|
}
|
|
if s.get("enum").is_some() {
|
|
return json!(7);
|
|
}
|
|
match s["type"].as_str() {
|
|
Some("string") => json!(12345),
|
|
Some("integer") | Some("number") => json!("x"),
|
|
Some("boolean") => json!("yes"),
|
|
Some("array") => json!({}),
|
|
// Not an array: serde reads a struct from an array of its members (its sequence form),
|
|
// a leniency the prose states and the corpus shows (object-as-array.flowrs).
|
|
Some("object") => json!("x"),
|
|
other => panic!("no wrong value for a schema of type {other:?}: {s}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn the_committed_schema_is_the_one_the_code_generates_and_a_2020_12_schema() {
|
|
holds(spec::SCHEMA_FILE, &spec::schema());
|
|
let s = schema();
|
|
jsonschema::meta::validate(&s).expect("the schema meets draft 2020-12's own meta-schema");
|
|
assert_eq!(s["$schema"], "https://json-schema.org/draft/2020-12/schema");
|
|
assert_eq!(s["$id"], spec::SCHEMA_URL);
|
|
assert!(
|
|
s["$comment"]
|
|
.as_str()
|
|
.unwrap()
|
|
.contains(&format!("specification {}", spec::SPEC_VERSION))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_document_with_every_member_writes_exactly_what_the_schema_declares() {
|
|
let doc = every_member();
|
|
let mut text = doc.to_json();
|
|
text.push('\n');
|
|
holds("spec/conformance/flowrs/every-member.flowrs", &text);
|
|
let value: Value = serde_json::from_str(&text).unwrap();
|
|
let v = validator();
|
|
let errors: Vec<String> = v
|
|
.iter_errors(&value)
|
|
.map(|e| format!("{e} at {}", e.instance_path()))
|
|
.collect();
|
|
assert!(errors.is_empty(), "{errors:#?}");
|
|
assert_eq!(reader(&text), "accepts");
|
|
assert!(canonical(&text), "the document is not in canonical form");
|
|
|
|
let root = schema();
|
|
let mut sites = Vec::new();
|
|
walk(&root, &root, "#", &value, "", &mut sites);
|
|
let seen: BTreeSet<(String, String)> = sites
|
|
.iter()
|
|
.map(|s| (s.def.clone(), s.key.clone()))
|
|
.collect();
|
|
let want = declared(&root);
|
|
let unwritten: Vec<_> = want.difference(&seen).collect();
|
|
assert!(
|
|
unwritten.is_empty(),
|
|
"declared but never written by the document that sets every member: {unwritten:?}"
|
|
);
|
|
let undeclared: Vec<_> = seen.difference(&want).collect();
|
|
assert!(
|
|
undeclared.is_empty(),
|
|
"written but not declared: {undeclared:?}"
|
|
);
|
|
// Each kind of author appears, and each mode.
|
|
for a in spec::authors() {
|
|
let (by, _) = spec::author_shape(&a);
|
|
assert!(
|
|
text.contains(&format!("\"by\": \"{by}\"")),
|
|
"no {by} author"
|
|
);
|
|
}
|
|
for m in spec::WIRE_MODES {
|
|
assert!(text.contains(&format!("\"mode\": \"{}\"", spec::mode_name(m))));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn the_schema_and_the_reader_agree_on_every_member_dropped_or_mistyped() {
|
|
let root = schema();
|
|
let v = validator();
|
|
let value: Value = serde_json::from_str(&every_member().to_json()).unwrap();
|
|
let mut sites = Vec::new();
|
|
walk(&root, &root, "#", &value, "", &mut sites);
|
|
let mut done = BTreeSet::new();
|
|
let mut checked = 0;
|
|
let mut problems = Vec::new();
|
|
for s in &sites {
|
|
if !done.insert((s.def.clone(), s.key.clone())) {
|
|
continue;
|
|
}
|
|
let mut cases: Vec<(&str, Value, bool)> = Vec::new();
|
|
if s.key != "*" {
|
|
let mut dropped = value.clone();
|
|
dropped
|
|
.pointer_mut(&s.at)
|
|
.and_then(Value::as_object_mut)
|
|
.unwrap()
|
|
.remove(&s.key);
|
|
cases.push(("dropped", dropped, !s.required));
|
|
}
|
|
let mut mistyped = value.clone();
|
|
let slot = if s.key == "*" {
|
|
mistyped.pointer_mut(&s.at).unwrap()
|
|
} else {
|
|
mistyped
|
|
.pointer_mut(&format!("{}/{}", s.at, escape(&s.key)))
|
|
.unwrap()
|
|
};
|
|
*slot = wrong(&root, &s.schema);
|
|
cases.push(("mistyped", mistyped, false));
|
|
for (what, doc, readable) in cases {
|
|
checked += 1;
|
|
let text = serde_json::to_string(&doc).unwrap();
|
|
let (by_reader, by_schema) = (reader(&text) == "accepts", v.is_valid(&doc));
|
|
if by_reader != readable || by_schema != readable {
|
|
problems.push(format!(
|
|
"{}.{} {what}: the reader {}, the schema {}, the prose says {}",
|
|
s.def,
|
|
s.key,
|
|
if by_reader { "accepts" } else { "refuses" },
|
|
if by_schema { "accepts" } else { "refuses" },
|
|
if readable { "readable" } else { "refused" },
|
|
));
|
|
}
|
|
}
|
|
}
|
|
assert!(problems.is_empty(), "{}", problems.join("\n"));
|
|
assert!(checked >= 60, "only {checked} cases");
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
struct Manifest {
|
|
spec: String,
|
|
flowrs: Vec<DocCase>,
|
|
fdsl: Vec<TextCase>,
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
struct DocCase {
|
|
file: String,
|
|
reader: String,
|
|
schema: bool,
|
|
#[serde(default)]
|
|
canonical: Option<bool>,
|
|
why: String,
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
struct TextCase {
|
|
file: String,
|
|
parses: bool,
|
|
#[serde(default)]
|
|
error: Option<String>,
|
|
why: String,
|
|
}
|
|
|
|
fn manifest() -> Manifest {
|
|
serde_json::from_str(&read("spec/conformance/manifest.json")).unwrap()
|
|
}
|
|
|
|
/// The files under `dir` with extension `ext`, sorted, from the repository's root.
|
|
fn listed(dir: &str, ext: &str) -> Vec<String> {
|
|
let mut out: Vec<String> = std::fs::read_dir(root().join(dir))
|
|
.unwrap()
|
|
.filter_map(|e| e.ok())
|
|
.map(|e| e.path())
|
|
.filter(|p| p.extension().is_some_and(|x| x == ext))
|
|
.map(|p| {
|
|
p.strip_prefix(root().join("spec/conformance"))
|
|
.unwrap()
|
|
.to_string_lossy()
|
|
.into_owned()
|
|
})
|
|
.collect();
|
|
out.sort();
|
|
out
|
|
}
|
|
|
|
#[test]
|
|
fn the_conformance_documents_are_read_as_the_manifest_says() {
|
|
let m = manifest();
|
|
assert_eq!(m.spec, spec::PROSE_FILE);
|
|
let mut files: Vec<String> = m.flowrs.iter().map(|c| c.file.clone()).collect();
|
|
files.sort();
|
|
assert_eq!(
|
|
files,
|
|
listed("spec/conformance/flowrs", "flowrs"),
|
|
"every corpus document is in the manifest, once"
|
|
);
|
|
let v = validator();
|
|
let mut problems = Vec::new();
|
|
for c in &m.flowrs {
|
|
assert!(!c.why.trim().is_empty(), "{}: no why", c.file);
|
|
let text = read(&format!("spec/conformance/{}", c.file));
|
|
let got = reader(&text);
|
|
if got != c.reader {
|
|
problems.push(format!("{}: the reader {got}, not {}", c.file, c.reader));
|
|
}
|
|
if conforms(&v, &text) != c.schema {
|
|
problems.push(format!(
|
|
"{}: the schema {}",
|
|
c.file,
|
|
if c.schema { "refuses it" } else { "accepts it" }
|
|
));
|
|
}
|
|
match (c.reader.as_str(), c.canonical) {
|
|
("accepts", Some(want)) => {
|
|
if canonical(&text) != want {
|
|
problems.push(format!("{}: canonical is not {want}", c.file));
|
|
}
|
|
}
|
|
("accepts", None) => problems.push(format!("{}: canonical is not stated", c.file)),
|
|
(_, Some(_)) => problems.push(format!("{}: canonical of a refused file", c.file)),
|
|
_ => {}
|
|
}
|
|
}
|
|
assert!(problems.is_empty(), "{}", problems.join("\n"));
|
|
// Each disagreement the schema cannot help is in the corpus: a document the schema accepts
|
|
// and the reader refuses, and one the reader accepts and the schema refuses.
|
|
assert!(m.flowrs.iter().any(|c| c.schema && c.reader != "accepts"));
|
|
assert!(m.flowrs.iter().any(|c| !c.schema && c.reader == "accepts"));
|
|
}
|
|
|
|
/// The first error's place in `text`, or `None` when it parses.
|
|
fn first_error(text: &str) -> Option<String> {
|
|
dsl::parse_graph(text)
|
|
.err()
|
|
.map(|e| format!("{}:{}", e[0].line, e[0].col))
|
|
}
|
|
|
|
/// Graph text → graph → text → graph is the identity on what the graph computes, with and
|
|
/// without the printer's type comments.
|
|
fn round_trips(text: &str) -> Result<(), String> {
|
|
let g = dsl::parse_graph(text).map_err(|e| format!("{e:?}"))?.graph;
|
|
for printed in [
|
|
dsl::print(&g, flowrs_core::graph::widget::builtin_registry()),
|
|
dsl::print_plain(&g),
|
|
] {
|
|
let back = dsl::parse_graph(&printed)
|
|
.map_err(|e| format!("the printed text does not parse: {e:?}\n{printed}"))?
|
|
.graph;
|
|
if dsl::semantics(&back) != dsl::semantics(&g) {
|
|
return Err(format!(
|
|
"the printed text computes something else:\n{printed}"
|
|
));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn the_conformance_texts_are_read_as_the_manifest_says() {
|
|
let m = manifest();
|
|
let mut files: Vec<String> = m.fdsl.iter().map(|c| c.file.clone()).collect();
|
|
files.sort();
|
|
assert_eq!(files, listed("spec/conformance/fdsl", "fdsl"));
|
|
let mut problems = Vec::new();
|
|
for c in &m.fdsl {
|
|
assert!(!c.why.trim().is_empty(), "{}: no why", c.file);
|
|
let text = read(&format!("spec/conformance/{}", c.file));
|
|
match (c.parses, first_error(&text), &c.error) {
|
|
(true, None, None) => {
|
|
if let Err(e) = round_trips(&text) {
|
|
problems.push(format!("{}: {e}", c.file));
|
|
}
|
|
}
|
|
(false, Some(at), Some(want)) if &at == want => {}
|
|
(want, got, at) => problems.push(format!(
|
|
"{}: the manifest says parses={want} {at:?}, the parser {got:?}",
|
|
c.file
|
|
)),
|
|
}
|
|
}
|
|
assert!(problems.is_empty(), "{}", problems.join("\n"));
|
|
// The text of every form carries an app, and the app is read.
|
|
let every = dsl::parse_graph(&read("spec/conformance/fdsl/every-form.fdsl")).unwrap();
|
|
assert!(every.app.unwrap().contains("Heading(\"Every statement\")"));
|
|
assert!(every.graph.groups.len() >= 3 && every.graph.wires.values().any(|w| w.feedback));
|
|
}
|
|
|
|
/// Every `.flowrs` and `.fdsl` file in the repository but the conformance corpus (which holds
|
|
/// refused ones on purpose), and the generated and vendored trees.
|
|
fn repository_files(ext: &str) -> Vec<PathBuf> {
|
|
const SKIP: &[&str] = &[
|
|
"target",
|
|
"node_modules",
|
|
".git",
|
|
"dist",
|
|
"test-results",
|
|
"conformance",
|
|
"vendor",
|
|
];
|
|
fn go(dir: &Path, ext: &str, out: &mut Vec<PathBuf>) {
|
|
let Ok(entries) = std::fs::read_dir(dir) else {
|
|
return;
|
|
};
|
|
for e in entries.flatten() {
|
|
let p = e.path();
|
|
let name = e.file_name().to_string_lossy().into_owned();
|
|
if p.is_dir() {
|
|
if !SKIP.contains(&name.as_str()) {
|
|
go(&p, ext, out);
|
|
}
|
|
} else if p.extension().is_some_and(|x| x == ext) {
|
|
out.push(p);
|
|
}
|
|
}
|
|
}
|
|
let mut out = Vec::new();
|
|
go(&root(), ext, &mut out);
|
|
out.sort();
|
|
out
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "counts the documents of flowRS's whole repository, most of which are not here"]
|
|
fn every_document_in_the_repository_conforms() {
|
|
let v = validator();
|
|
let files = repository_files("flowrs");
|
|
let mut problems = Vec::new();
|
|
for f in &files {
|
|
let text = std::fs::read_to_string(f).unwrap();
|
|
let rel = f.strip_prefix(root()).unwrap().display().to_string();
|
|
if reader(&text) != "accepts" {
|
|
problems.push(format!("{rel}: the reader {}", reader(&text)));
|
|
continue;
|
|
}
|
|
let value: Value = serde_json::from_str(&text).unwrap();
|
|
for e in v.iter_errors(&value) {
|
|
problems.push(format!("{rel}: {e} at {}", e.instance_path()));
|
|
}
|
|
}
|
|
assert!(problems.is_empty(), "{}", problems.join("\n"));
|
|
assert!(files.len() >= 40, "only {} documents found", files.len());
|
|
}
|
|
|
|
#[test]
|
|
fn every_graph_text_in_the_repository_parses_and_round_trips() {
|
|
let files = repository_files("fdsl");
|
|
let mut problems = Vec::new();
|
|
for f in &files {
|
|
let rel = f.strip_prefix(root()).unwrap().display().to_string();
|
|
if let Err(e) = round_trips(&std::fs::read_to_string(f).unwrap()) {
|
|
problems.push(format!("{rel}: {e}"));
|
|
}
|
|
}
|
|
assert!(problems.is_empty(), "{}", problems.join("\n"));
|
|
assert!(files.len() >= 40, "only {} texts found", files.len());
|
|
}
|
|
|
|
// -------------------------------------------------------------------------------------
|
|
// The prose.
|
|
// -------------------------------------------------------------------------------------
|
|
|
|
fn prose() -> String {
|
|
read(spec::PROSE_FILE)
|
|
}
|
|
|
|
/// The rows of the table after the line `<!-- {marker} -->`, its header and rule left out,
|
|
/// each a list of its cells.
|
|
fn table(md: &str, marker: &str) -> Vec<Vec<String>> {
|
|
let tag = format!("<!-- {marker} -->");
|
|
let at = md
|
|
.find(&tag)
|
|
.unwrap_or_else(|| panic!("the prose has no {tag}"));
|
|
md[at + tag.len()..]
|
|
.lines()
|
|
.skip_while(|l| l.trim().is_empty())
|
|
.take_while(|l| l.trim_start().starts_with('|'))
|
|
.skip(2)
|
|
.map(|l| {
|
|
let l = l.trim().trim_start_matches('|').trim_end_matches('|');
|
|
l.split('|').map(|c| c.trim().to_string()).collect()
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn unquote(cell: &str) -> String {
|
|
cell.trim().trim_matches('`').to_string()
|
|
}
|
|
|
|
/// A member's type as the prose's tables write it.
|
|
fn type_word(s: &Value) -> String {
|
|
if let Some(r) = s.get("$ref").and_then(Value::as_str) {
|
|
return r.trim_start_matches("#/$defs/").to_string();
|
|
}
|
|
if let Some(c) = s.get("const") {
|
|
return c.to_string();
|
|
}
|
|
if s.get("enum").is_some() {
|
|
return "enum".into();
|
|
}
|
|
if let Some(b) = s.get("oneOf").and_then(Value::as_array) {
|
|
let other = b
|
|
.iter()
|
|
.find(|x| x.get("type") != Some(&json!("null")))
|
|
.unwrap();
|
|
return format!("{} or null", type_word(other));
|
|
}
|
|
match s["type"].as_str().unwrap_or_default() {
|
|
"integer" if s["maximum"] == json!(u32::MAX) => "u32".into(),
|
|
"array" => format!("array of {}", type_word(&s["items"])),
|
|
"object" if s["additionalProperties"].is_object() => {
|
|
format!("map of {}", type_word(&s["additionalProperties"]))
|
|
}
|
|
t => t.to_string(),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn the_prose_tables_are_the_schemas_members_types_and_required_flags() {
|
|
let md = prose();
|
|
let root = schema();
|
|
let mut covered = BTreeSet::new();
|
|
for (marker, def) in [
|
|
("schema: #", "#"),
|
|
("schema: #/$defs/node", "node"),
|
|
("schema: #/$defs/wire", "wire"),
|
|
("schema: #/$defs/group", "group"),
|
|
("schema: #/$defs/function", "function"),
|
|
("schema: #/$defs/node_origin", "node_origin"),
|
|
("schema: #/$defs/app", "app"),
|
|
("schema: #/$defs/app_turn", "app_turn"),
|
|
("schema: #/$defs/panel", "panel"),
|
|
("schema: #/$defs/card", "card"),
|
|
] {
|
|
let s = if def == "#" {
|
|
&root
|
|
} else {
|
|
&root["$defs"][def]
|
|
};
|
|
let props = s["properties"].as_object().unwrap();
|
|
let required: Vec<&str> = s["required"]
|
|
.as_array()
|
|
.map(|r| r.iter().filter_map(Value::as_str).collect())
|
|
.unwrap_or_default();
|
|
let rows = table(&md, marker);
|
|
let names: Vec<String> = rows.iter().map(|r| unquote(&r[0])).collect();
|
|
// The schema's order, which is the canonical form's.
|
|
let want: Vec<String> = props.keys().cloned().collect();
|
|
let mut sorted_names = names.clone();
|
|
sorted_names.sort();
|
|
let mut sorted_want = want.clone();
|
|
sorted_want.sort();
|
|
assert_eq!(sorted_names, sorted_want, "{def}: the members");
|
|
for r in &rows {
|
|
let name = unquote(&r[0]);
|
|
assert_eq!(r[1], type_word(&props[&name]), "{def}.{name}: the type");
|
|
assert_eq!(
|
|
r[2] == "yes",
|
|
required.contains(&name.as_str()),
|
|
"{def}.{name}: required"
|
|
);
|
|
assert!(r[2] == "yes" || r[2] == "no", "{def}.{name}: {:?}", r[2]);
|
|
assert!(!r[3].is_empty(), "{def}.{name}: no meaning");
|
|
}
|
|
covered.insert(def.to_string());
|
|
}
|
|
// Every object the schema defines is either tabled or one of the shapes stated in words.
|
|
for (name, s) in root["$defs"].as_object().unwrap() {
|
|
if s.get("properties").is_some() {
|
|
assert!(covered.contains(name), "no table for {name}");
|
|
}
|
|
}
|
|
// The modes, as the code spells and means them.
|
|
let modes = table(&md, "modes");
|
|
assert_eq!(modes.len(), spec::WIRE_MODES.len());
|
|
for (row, m) in modes.iter().zip(spec::WIRE_MODES) {
|
|
assert_eq!(unquote(&row[0]), spec::mode_name(m));
|
|
assert_eq!(row[1], spec::mode_meaning(m));
|
|
}
|
|
let modes_in_schema: Vec<&str> = root["$defs"]["wire"]["properties"]["mode"]["enum"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.filter_map(Value::as_str)
|
|
.collect();
|
|
assert_eq!(
|
|
modes_in_schema,
|
|
spec::WIRE_MODES
|
|
.iter()
|
|
.map(|m| spec::mode_name(*m))
|
|
.collect::<Vec<_>>()
|
|
);
|
|
// The authors: each tag and the members it carries.
|
|
let authors = table(&md, "authors");
|
|
assert_eq!(authors.len(), spec::authors().len());
|
|
for (row, a) in authors.iter().zip(spec::authors()) {
|
|
let (by, members) = spec::author_shape(&a);
|
|
assert_eq!(unquote(&row[0]), by);
|
|
let listed: Vec<String> = if row[1] == "none" {
|
|
Vec::new()
|
|
} else {
|
|
row[1].split(',').map(unquote).collect()
|
|
};
|
|
assert_eq!(listed, members, "{by}");
|
|
}
|
|
// The skins.
|
|
assert_eq!(
|
|
root["$defs"]["app"]["properties"]["skin"]["enum"],
|
|
json!(spec::SKINS)
|
|
);
|
|
for skin in spec::SKINS {
|
|
assert!(md.contains(&format!("`\"{skin}\"`")), "{skin}");
|
|
}
|
|
}
|
|
|
|
/// The fenced blocks of the prose: (info string, contents).
|
|
fn blocks(md: &str) -> Vec<(String, String)> {
|
|
let mut out = Vec::new();
|
|
let mut lines = md.lines();
|
|
while let Some(l) = lines.next() {
|
|
if let Some(info) = l.strip_prefix("```") {
|
|
let mut body = String::new();
|
|
for b in lines.by_ref() {
|
|
if b.starts_with("```") {
|
|
break;
|
|
}
|
|
body.push_str(b);
|
|
body.push('\n');
|
|
}
|
|
out.push((info.trim().to_string(), body));
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
#[test]
|
|
fn the_prose_examples_load_parse_round_trip_or_fail_where_it_says() {
|
|
let md = prose();
|
|
let v = validator();
|
|
let mut documents = Vec::new();
|
|
let mut texts = Vec::new();
|
|
let mut refused = 0;
|
|
for (info, body) in blocks(&md) {
|
|
let words: Vec<&str> = info.split_whitespace().collect();
|
|
match words.as_slice() {
|
|
["json", "flowrs"] => {
|
|
assert_eq!(reader(&body), "accepts", "{body}");
|
|
assert!(conforms(&v, &body), "the schema refuses the example");
|
|
assert!(canonical(&body), "the example is not canonical");
|
|
documents.push(body);
|
|
}
|
|
["fdsl"] => {
|
|
round_trips(&body).unwrap_or_else(|e| panic!("{e}\n{body}"));
|
|
texts.push(body);
|
|
}
|
|
["fdsl", "refused", at] => {
|
|
assert_eq!(first_error(&body).as_deref(), Some(*at), "{body}");
|
|
refused += 1;
|
|
}
|
|
["ebnf"] => {}
|
|
other => panic!("a block the test does not read: {other:?}"),
|
|
}
|
|
}
|
|
assert!(!documents.is_empty() && texts.len() >= 2 && refused >= 3);
|
|
// The document example is the corpus's, and the first text is the same graph.
|
|
assert_eq!(
|
|
documents[0],
|
|
read("spec/conformance/flowrs/add-two-numbers.flowrs")
|
|
);
|
|
let doc = Document::from_json(&documents[0]).unwrap().into_graph();
|
|
let text = dsl::parse_graph(&texts[0]).unwrap().graph;
|
|
assert_eq!(dsl::semantics(&doc), dsl::semantics(&text));
|
|
assert_eq!(texts[0], read("spec/conformance/fdsl/add.fdsl"));
|
|
}
|
|
|
|
#[test]
|
|
fn the_prose_escapes_keywords_punctuation_and_modifiers_are_the_languages() {
|
|
use flowrs_core::graph::dsl::lex::{Tok, lex};
|
|
let md = prose();
|
|
// Keywords: the parser's list, in its order.
|
|
let tag = "<!-- keywords -->";
|
|
let line = md[md.find(tag).unwrap() + tag.len()..]
|
|
.lines()
|
|
.find(|l| !l.trim().is_empty())
|
|
.unwrap();
|
|
let keywords: Vec<String> = line.split_whitespace().map(unquote).collect();
|
|
assert_eq!(keywords, dsl::parse::KEYWORDS);
|
|
// The contextual words: not reserved (a node may still be called by one), and each begins
|
|
// the statement the grammar gives it.
|
|
let tag = "<!-- contextual -->";
|
|
let line = md[md.find(tag).unwrap() + tag.len()..]
|
|
.lines()
|
|
.find(|l| !l.trim().is_empty())
|
|
.unwrap();
|
|
let contextual: Vec<String> = line.split_whitespace().map(unquote).collect();
|
|
assert_eq!(contextual, ["fn", "out"]);
|
|
for w in &contextual {
|
|
assert!(
|
|
!dsl::parse::KEYWORDS.contains(&w.as_str()),
|
|
"{w} is reserved"
|
|
);
|
|
assert!(
|
|
dsl::parse_graph(&format!(
|
|
"let {w} = FloatLiteral {{ value: 1 }};
|
|
let n1 = io.print(value: {w}.value);"
|
|
))
|
|
.is_ok(),
|
|
"{w} cannot be a handle"
|
|
);
|
|
}
|
|
let f = dsl::parse_graph("fn f@1(x: f64) -> (y: f64) {\n out y = x.value;\n}").unwrap();
|
|
assert_eq!(f.graph.functions.len(), 1);
|
|
// Escapes: exactly the listed ones decode, each to what the prose says.
|
|
let rows = table(&md, "escapes");
|
|
let means = |w: &str| -> Option<char> {
|
|
Some(match w {
|
|
"line feed" => '\n',
|
|
"tab" => '\t',
|
|
"carriage return" => '\r',
|
|
"the NUL character" => '\0',
|
|
"a backslash" => '\\',
|
|
"a double quote" => '"',
|
|
"a single quote" => '\'',
|
|
_ => return None,
|
|
})
|
|
};
|
|
let mut listed = BTreeSet::new();
|
|
for r in &rows {
|
|
let esc = unquote(&r[0]);
|
|
let c = esc.chars().nth(1).unwrap();
|
|
listed.insert(c);
|
|
if c == 'u' {
|
|
assert_eq!(
|
|
lex(r#""\u{e9}""#).unwrap()[0].tok,
|
|
Tok::Str("é".into()),
|
|
"\\u{{…}}"
|
|
);
|
|
continue;
|
|
}
|
|
let want = means(&r[1]).unwrap_or_else(|| panic!("{:?} means nothing known", r[1]));
|
|
assert_eq!(
|
|
lex(&format!("\"\\{c}\"")).unwrap()[0].tok,
|
|
Tok::Str(want.to_string()),
|
|
"{esc}"
|
|
);
|
|
}
|
|
for c in (0x20u8..0x7f).map(char::from) {
|
|
let ok = lex(&format!("\"a\\{c}b\"")).is_ok();
|
|
assert_eq!(ok, listed.contains(&c) && c != 'u', "the escape \\{c}");
|
|
}
|
|
// Punctuation: each listed one is a token; every other ASCII symbol but those that open a
|
|
// string, a comment, a number or a name is an error.
|
|
let bullet = md.lines().find(|l| l.contains("**Punctuation**")).unwrap();
|
|
let punct: Vec<String> = bullet
|
|
.split('`')
|
|
.skip(1)
|
|
.step_by(2)
|
|
.map(str::to_string)
|
|
.collect();
|
|
assert_eq!(punct.len(), 15, "{punct:?}");
|
|
for p in &punct {
|
|
let t = lex(p).unwrap();
|
|
assert_eq!(t.len(), 2, "{p} is one token");
|
|
}
|
|
for c in (0x21u8..0x7f).map(char::from) {
|
|
if c.is_ascii_alphanumeric() || ['"', '_', '-', '/'].contains(&c) {
|
|
continue;
|
|
}
|
|
let one = punct.iter().any(|p| p == &c.to_string());
|
|
assert_eq!(lex(&c.to_string()).is_ok(), one, "the character {c}");
|
|
}
|
|
// Modifiers: the grammar's, and no other word.
|
|
let ebnf = blocks(&md)
|
|
.into_iter()
|
|
.find(|(i, _)| i == "ebnf")
|
|
.unwrap()
|
|
.1;
|
|
let rule = ebnf
|
|
.lines()
|
|
.find(|l| l.trim_start().starts_with("modifier"))
|
|
.unwrap();
|
|
let modifiers: Vec<&str> = rule.split('"').skip(1).step_by(2).collect();
|
|
assert_eq!(modifiers, ["clone", "copy", "prev"]);
|
|
let with = |m: &str| {
|
|
format!("let n0 = FloatLiteral {{ value: 1 }};\nlet n1 = io.print(value: n0.value.{m}());")
|
|
};
|
|
for m in &modifiers {
|
|
assert!(dsl::parse_graph(&with(m)).is_ok(), ".{m}()");
|
|
}
|
|
for m in ["borrow", "move", "next", "mut", "last", "hold", "ref"] {
|
|
assert!(dsl::parse_graph(&with(m)).is_err(), ".{m}() parses");
|
|
}
|
|
// Every terminal of the grammar is a keyword, a modifier, `_`, `mut`, or punctuation.
|
|
for t in ebnf.split('"').skip(1).step_by(2) {
|
|
let known = keywords.iter().any(|k| k == t)
|
|
|| contextual.iter().any(|k| k == t)
|
|
|| modifiers.contains(&t)
|
|
|| t == "_"
|
|
|| t == "mut"
|
|
|| punct.iter().any(|p| p == t);
|
|
assert!(known, "the grammar's terminal {t:?}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn the_older_spellings_are_the_ones_read() {
|
|
use flowrs_core::graph::schedule::legacy_spellings;
|
|
let rows = table(&prose(), "legacy");
|
|
let code = legacy_spellings();
|
|
assert_eq!(rows.len(), code.len());
|
|
for (row, (old, base, sched)) in rows.iter().zip(&code) {
|
|
let quoted = |cell: &str| -> Vec<String> {
|
|
cell.split('`')
|
|
.skip(1)
|
|
.step_by(2)
|
|
.map(str::to_string)
|
|
.collect()
|
|
};
|
|
assert_eq!(quoted(&row[0]), [*old]);
|
|
assert_eq!(quoted(&row[1]), [*base, "schedule", sched.as_str()]);
|
|
let text = format!(
|
|
r#"{{"format": 1, "nodes": [{{"id": 0, "type_id": "{old}", "properties": {{}}}}], "wires": []}}"#
|
|
);
|
|
let g = flowrs_core::graph::document::load(&text).unwrap();
|
|
let n = &g.nodes[&NodeId(0)];
|
|
assert_eq!(n.type_id, *base);
|
|
assert_eq!(n.properties["schedule"], sched.as_str());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn the_fingerprint_is_the_one_the_prose_states() {
|
|
let md = prose();
|
|
const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
|
|
const PRIME: u64 = 0x0000_0100_0000_01b3;
|
|
assert!(md.contains("`0xcbf29ce484222325`") && md.contains("`0x100000001b3`"));
|
|
// Written from the prose, not from flowrs-core's function.
|
|
let fnv = |parts: &[String]| {
|
|
let mut h = OFFSET;
|
|
for p in parts {
|
|
for b in p.bytes().chain([0xffu8]) {
|
|
h ^= u64::from(b);
|
|
h = h.wrapping_mul(PRIME);
|
|
}
|
|
}
|
|
format!("{h:016x}")
|
|
};
|
|
let reg = flowrs_core::graph::widget::WidgetRegistry::with_builtins();
|
|
let mut checked = 0;
|
|
for w in reg.all() {
|
|
let mut parts = vec![w.id()];
|
|
for (kind, ports) in [("in", w.inputs()), ("out", w.outputs())] {
|
|
parts.push(kind.into());
|
|
for p in ports {
|
|
parts.push(p.name.clone());
|
|
parts.push(p.semantic_type.clone());
|
|
parts.push(if p.variadic { "variadic" } else { "single" }.into());
|
|
}
|
|
}
|
|
for p in w.parameters() {
|
|
parts.push(p.name.clone());
|
|
parts.push(p.ty.clone());
|
|
parts.push(p.default.clone());
|
|
}
|
|
assert_eq!(
|
|
fnv(&parts),
|
|
flowrs_core::graph::widget::fingerprint(w),
|
|
"{}",
|
|
w.id()
|
|
);
|
|
checked += 1;
|
|
}
|
|
assert!(checked > 100, "{checked} node types");
|
|
}
|
|
|
|
/// The prose's function example, saved, is the corpus's format-2 document; and it computes what
|
|
/// the prose says it does.
|
|
#[test]
|
|
fn the_prose_function_example_saved_is_the_corpus_document() {
|
|
let text = blocks(&prose())
|
|
.into_iter()
|
|
.find(|(i, b)| i == "fdsl" && b.contains("fn calibrate@1"))
|
|
.expect("the prose has a function example")
|
|
.1;
|
|
assert_eq!(text, read("spec/conformance/fdsl/function.fdsl"));
|
|
let g = dsl::parse_graph(&text).unwrap().graph;
|
|
let mut doc = Document::from_graph(&g);
|
|
doc.generator = "spec/conformance: a function and its calls".into();
|
|
assert_eq!(doc.format, FORMAT);
|
|
let mut json = doc.to_json();
|
|
json.push('\n');
|
|
holds("spec/conformance/flowrs/functions.flowrs", &json);
|
|
let back = Document::from_json(&json).unwrap().into_graph();
|
|
assert_eq!(dsl::semantics(&back), dsl::semantics(&g));
|
|
let mut ex =
|
|
flowrs_core::graph::GraphExecutor::builtin(flowrs_core::graph::runtime::Runtime::new());
|
|
let steps = ex.run(&back).unwrap();
|
|
let at = |id: u64| match steps
|
|
.iter()
|
|
.find(|s| s.node_id == NodeId(id))
|
|
.unwrap()
|
|
.outputs["volts"]
|
|
{
|
|
flowrs_core::graph::Value::Float(f) => f,
|
|
ref other => panic!("{other:?}"),
|
|
};
|
|
assert!(
|
|
(at(1) - 3.0).abs() < 1e-12 && (at(2) - 2.9).abs() < 1e-12,
|
|
"{} {}",
|
|
at(1),
|
|
at(2)
|
|
);
|
|
}
|
|
|
|
/// The prose's table of function node types is the code's: their names, the properties a
|
|
/// boundary node reads, and its one port.
|
|
#[test]
|
|
fn the_prose_function_nodes_are_the_codes() {
|
|
use flowrs_core::graph::function as f;
|
|
let rows = table(&prose(), "function nodes");
|
|
let types: Vec<String> = rows.iter().map(|r| unquote(&r[0])).collect();
|
|
assert_eq!(types, [f::INPUT, f::OUTPUT, f::CALL]);
|
|
let props = |cell: &str| -> Vec<String> {
|
|
cell.split('`')
|
|
.skip(1)
|
|
.step_by(2)
|
|
.map(str::to_string)
|
|
.collect()
|
|
};
|
|
assert_eq!(props(&rows[0][1]), f::BOUNDARY_KEYS);
|
|
assert_eq!(
|
|
props(&rows[1][1]),
|
|
f::BOUNDARY_KEYS
|
|
.iter()
|
|
.filter(|k| **k != f::DEFAULT)
|
|
.map(|k| k.to_string())
|
|
.collect::<Vec<_>>()
|
|
);
|
|
assert_eq!(props(&rows[2][1]), [f::FN]);
|
|
assert_eq!(props(&rows[0][2]), [f::PORT]);
|
|
assert_eq!(props(&rows[1][2]), [f::PORT]);
|
|
assert!(prose().contains(&format!("more than {} deep", f::MAX_DEPTH)));
|
|
}
|
|
|
|
/// Version 1.0.0 stays as it was published: its prose and its schema are in the repository,
|
|
/// the schema still a 2020-12 schema at its own address, and every format-1 document of the
|
|
/// corpus that it accepted this version's schema accepts too.
|
|
#[test]
|
|
fn the_previous_version_is_kept_as_published() {
|
|
for p in spec::PREVIOUS {
|
|
let prose = read(p.prose);
|
|
assert!(prose.contains(&format!("Specification version {}", p.version)));
|
|
let schema: Value = serde_json::from_str(&read(p.schema)).unwrap();
|
|
jsonschema::meta::validate(&schema).expect("a 2020-12 schema");
|
|
assert_eq!(schema["$id"], p.url);
|
|
let old = jsonschema::options().build(&schema).unwrap();
|
|
let new = validator();
|
|
for c in manifest().flowrs {
|
|
let text = read(&format!("spec/conformance/{}", c.file));
|
|
if conforms(&old, &text) {
|
|
assert!(
|
|
conforms(&new, &text),
|
|
"{}: 1.0.0 accepted it, 2.0.0 does not",
|
|
c.file
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|