353 lines
14 KiB
Rust
353 lines
14 KiB
Rust
//! No widget's T0 path and no program flowRS generates calls the platform's maths library.
|
|
//!
|
|
//! The browser's wasm and a native program link different `sin`, `exp`, `atan2`, `powf`
|
|
//! (musl's port against Apple's or glibc's libm), so both tiers compute every transcendental
|
|
//! function through flowrs-math, one text (see crates/flowrs-math). This test holds that, two
|
|
//! ways, with `flowrs_core::codegen::platform_maths::scan` (a token scanner that aliases do not
|
|
//! fool; its head says how):
|
|
//!
|
|
//! 1. the source of every crate a widget's executor or a generated program runs, test code
|
|
//! aside: method calls, paths, intrinsics, `extern` declarations, renamings and the
|
|
//! generators' string templates;
|
|
//! 2. what the generator emits: every widget at its defaults with its module-scope items,
|
|
//! every primitive alone, and every corpus graph's whole program.
|
|
//!
|
|
//! What remains is named, not hidden: a crate whose arithmetic follows the CPU (tract, the
|
|
//! ONNX runtime) is reported by name, and the places it is reached must be exactly the ones
|
|
//! listed here, each with why it cannot be one text (the tiers census and FORM's tier-parity
|
|
//! row say the same for a graph that uses them).
|
|
//!
|
|
//! It prints the census of what it scanned (`--nocapture`).
|
|
|
|
use flowrs_core::codegen::platform_maths::{Finding, scan, test_modules};
|
|
use flowrs_core::graph::widget::{CodegenContext, builtin_registry};
|
|
use std::collections::{BTreeMap, HashMap};
|
|
use std::path::{Path, PathBuf};
|
|
|
|
/// Every crate whose code runs at T0 (the executors and what they call) or in a generated
|
|
/// program, and why it is here.
|
|
const CRATES: &[(&str, &str)] = &[
|
|
(
|
|
"flowrs-core",
|
|
"the executors (T0), the shared kernels and the code generator (T1)",
|
|
),
|
|
("flowrs-math", "the one implementation itself"),
|
|
(
|
|
"flowrs-dsp",
|
|
"the dsp.* designers and transforms at both tiers; vendored into exports",
|
|
),
|
|
(
|
|
"flowrs-neuro",
|
|
"the neuro.* executors; its kernel is pasted into programs",
|
|
),
|
|
(
|
|
"flowrs-ml",
|
|
"ml.* training and inference at both tiers (on the libm crate, which both link)",
|
|
),
|
|
(
|
|
"flowrs-record",
|
|
"record.* at both tiers; vendored into exports",
|
|
),
|
|
("flowrs-sequence", "test sequences under record.*; vendored"),
|
|
("flowrs-tdms", "data.tdms reads at both tiers"),
|
|
(
|
|
"flowrs-capture",
|
|
"capture.open and its decoders at both tiers",
|
|
),
|
|
("flowrs-decode", "the protocol decoders capture.* runs"),
|
|
(
|
|
"flowrs-transport",
|
|
"instrument.* command building at both tiers; vendored",
|
|
),
|
|
("flowrs-instruments", "instrument.supply's descriptors"),
|
|
(
|
|
"flowrs-connect",
|
|
"cloud.*, mavlink.* and ros.* nodes at both tiers",
|
|
),
|
|
("flowrs-live", "live.* streams and their decoding"),
|
|
("flowrs-schemas", "the signal value both tiers carry"),
|
|
("flowrs-iface", "units and clocks the unit.* nodes convert"),
|
|
("flowrs-boards", "the board registry the MCU targets bind"),
|
|
(
|
|
"flowrs-device",
|
|
"a Linux board's IO in generated programs; vendored",
|
|
),
|
|
(
|
|
"flowrs-hal",
|
|
"the firmware HAL generated programs build on; vendored",
|
|
),
|
|
];
|
|
|
|
/// The places a crate whose arithmetic is not one text at both tiers is reached, by file: tract,
|
|
/// the ONNX runtime the neural.* and ml.* model nodes run, picks its matrix kernels by the CPU
|
|
/// it runs on (NEON, AVX, wasm SIMD or generic), so a model gives the browser and a native
|
|
/// program last-bit differences, which no flowRS text can remove short of replacing the runtime.
|
|
/// The census (`graph::tiers::not_bit_identical`) and FORM's tier-parity row name the nodes.
|
|
/// Some of these files hold tract's `Tensor` only as a container for numbers flowRS computed
|
|
/// (vision.rs's image tensors, ml.rs's elementwise maps): the per-widget check below tells
|
|
/// those from the ones that run a model.
|
|
const RUNTIMES: &[(&str, &str)] = &[
|
|
("flowrs-core/src/graph/onnx_runtime.rs", "tract_onnx"),
|
|
("flowrs-core/src/graph/widgets/ml.rs", "tract_onnx"),
|
|
("flowrs-core/src/graph/widgets/neural.rs", "tract_onnx"),
|
|
("flowrs-core/src/graph/widgets/vision.rs", "tract_onnx"),
|
|
("flowrs-ml/src/onnx_infer.rs", "tract_onnx"),
|
|
("flowrs-neuro/src/analysis/iclabel_net.rs", "tract_onnx"),
|
|
("flowrs-neuro/src/analysis_widgets.rs", "tract_onnx"),
|
|
];
|
|
|
|
fn crates_dir() -> PathBuf {
|
|
Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
.parent()
|
|
.unwrap()
|
|
.to_path_buf()
|
|
}
|
|
|
|
/// The `.rs` files under `dir`, test modules declared out of line (`#[cfg(test)] mod tests;`)
|
|
/// left out.
|
|
fn sources(dir: &Path) -> Vec<PathBuf> {
|
|
let mut all = Vec::new();
|
|
let mut stack = vec![dir.to_path_buf()];
|
|
while let Some(d) = stack.pop() {
|
|
for e in std::fs::read_dir(&d).unwrap() {
|
|
let p = e.unwrap().path();
|
|
if p.is_dir() {
|
|
stack.push(p);
|
|
} else if p.extension().is_some_and(|x| x == "rs") {
|
|
all.push(p);
|
|
}
|
|
}
|
|
}
|
|
all.sort();
|
|
let mut tests: Vec<PathBuf> = Vec::new();
|
|
for p in &all {
|
|
let text = std::fs::read_to_string(p).unwrap();
|
|
let stem = p.file_stem().unwrap().to_string_lossy().into_owned();
|
|
let parent = p.parent().unwrap();
|
|
// A module `m` declared in `x.rs` lives in `x/m.rs`; in `mod.rs` or `lib.rs`, beside it.
|
|
let base = if matches!(stem.as_str(), "mod" | "lib" | "main") {
|
|
parent.to_path_buf()
|
|
} else {
|
|
parent.join(&stem)
|
|
};
|
|
for m in test_modules(&text) {
|
|
tests.push(base.join(format!("{m}.rs")));
|
|
tests.push(base.join(&m).join("mod.rs"));
|
|
}
|
|
}
|
|
all.retain(|p| !tests.contains(p));
|
|
all
|
|
}
|
|
|
|
fn relative(p: &Path) -> String {
|
|
p.strip_prefix(crates_dir())
|
|
.unwrap()
|
|
.to_string_lossy()
|
|
.into_owned()
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "reads flowrs-neuro's source, which is not in this snapshot"]
|
|
fn no_source_a_tier_runs_calls_the_platforms_maths() {
|
|
let mut wrong: Vec<String> = Vec::new();
|
|
let mut runtimes: Vec<(String, String)> = Vec::new();
|
|
let mut census: BTreeMap<&str, (usize, usize)> = BTreeMap::new();
|
|
for (name, why) in CRATES {
|
|
let src = crates_dir().join(name).join("src");
|
|
assert!(src.is_dir(), "{name} ({why}) has no src/");
|
|
let files = sources(&src);
|
|
let mut lines = 0;
|
|
for f in &files {
|
|
let text = std::fs::read_to_string(f).unwrap();
|
|
lines += text.lines().count();
|
|
for Finding {
|
|
line,
|
|
rule,
|
|
what,
|
|
in_string,
|
|
} in scan(&text)
|
|
{
|
|
if rule == "crate" {
|
|
runtimes.push((relative(f), what));
|
|
} else {
|
|
wrong.push(format!(
|
|
"{}:{line}: {rule} `{what}`{}",
|
|
relative(f),
|
|
if in_string {
|
|
" (in a generated program's text)"
|
|
} else {
|
|
""
|
|
}
|
|
));
|
|
}
|
|
}
|
|
}
|
|
census.insert(name, (files.len(), lines));
|
|
}
|
|
for (name, (files, lines)) in &census {
|
|
eprintln!("scanned {name}: {files} files, {lines} lines");
|
|
}
|
|
assert!(
|
|
wrong.is_empty(),
|
|
"{} calls into the platform's maths library, where a tier runs (route each through \
|
|
flowrs_math):\n{}",
|
|
wrong.len(),
|
|
wrong.join("\n")
|
|
);
|
|
runtimes.sort();
|
|
runtimes.dedup();
|
|
let want: Vec<(String, String)> = RUNTIMES
|
|
.iter()
|
|
.map(|(f, c)| (f.to_string(), c.to_string()))
|
|
.collect();
|
|
assert_eq!(
|
|
runtimes, want,
|
|
"the places a CPU-dependent runtime is reached changed: state each here, and in the census"
|
|
);
|
|
// Every crate that computes with flowrs-math runs at a tier, so it is scanned: one that
|
|
// took the dependency and was left off the list would go unread. (flowrs-critic takes it
|
|
// for the module's text, to measure a program without it; it computes nothing.)
|
|
for e in std::fs::read_dir(crates_dir()).unwrap() {
|
|
let dir = e.unwrap().path();
|
|
let name = dir.file_name().unwrap().to_string_lossy().into_owned();
|
|
let manifest = std::fs::read_to_string(dir.join("Cargo.toml")).unwrap_or_default();
|
|
if manifest.contains("flowrs-math = { path") && name != "flowrs-critic" {
|
|
assert!(
|
|
CRATES.iter().any(|(c, _)| *c == name),
|
|
"{name} computes with flowrs-math, so a tier runs it, but it is not scanned"
|
|
);
|
|
}
|
|
}
|
|
// Not vacuous: the scan read the crates, and the ones that compute the most are among them.
|
|
let core = census["flowrs-core"];
|
|
assert!(core.0 > 120 && core.1 > 100_000, "{core:?}");
|
|
assert!(census["flowrs-dsp"].0 >= 6 && census["flowrs-neuro"].0 >= 20);
|
|
}
|
|
|
|
fn ctx(params: HashMap<String, String>) -> CodegenContext {
|
|
CodegenContext {
|
|
var_names: HashMap::new(),
|
|
imports: vec![],
|
|
state_fields: vec![],
|
|
profile: flowrs_core::graph::Profile::scientific(),
|
|
params,
|
|
}
|
|
}
|
|
|
|
/// Whether an emission runs or loads a model through tract (see [`RUNTIMES`]): it loads an
|
|
/// ONNX model, or runs the model it is given (which may be one). tract's `Tensor` alone is a
|
|
/// container (`ml.relu` maps T0's own closure over its data), and a model a widget builds or
|
|
/// trains itself is flowrs-ml's (`ml.train`), the same text at both tiers.
|
|
fn runs_model(w: &dyn flowrs_core::graph::widget::Widget, text: &str) -> bool {
|
|
text.contains("into_runnable")
|
|
|| (w.inputs().iter().any(|p| p.name == "model") && text.contains(".run_flat("))
|
|
}
|
|
|
|
#[test]
|
|
fn no_generated_program_calls_the_platforms_maths() {
|
|
let reg = builtin_registry();
|
|
let mut wrong: Vec<String> = Vec::new();
|
|
let mut on_runtime: Vec<String> = Vec::new();
|
|
let mut emitted = 0;
|
|
let mut configurations = 0;
|
|
for w in reg.all() {
|
|
let id = w.id();
|
|
let defaults: HashMap<String, String> = w
|
|
.parameters()
|
|
.into_iter()
|
|
.map(|p| (p.name, p.default))
|
|
.collect();
|
|
let inputs: HashMap<String, String> = w
|
|
.inputs()
|
|
.into_iter()
|
|
.map(|p| (p.name.clone(), format!("in_{}", p.name)))
|
|
.collect();
|
|
// Every configuration an enumerated parameter names, not the defaults alone: a filter's
|
|
// family, a window's type, a waveform, a detector's head (which refuses at its default).
|
|
let mut configs = vec![defaults.clone()];
|
|
for p in w.parameters() {
|
|
for choice in &p.choices {
|
|
let mut c = defaults.clone();
|
|
c.insert(p.name.clone(), choice.clone());
|
|
configs.push(c);
|
|
}
|
|
}
|
|
let mut text = String::new();
|
|
for params in configs {
|
|
let r = w.codegen(&ctx(params), inputs.clone());
|
|
for part in [&r.init, &Some(r.process.clone()), &r.cleanup]
|
|
.into_iter()
|
|
.flatten()
|
|
{
|
|
text.push_str(part);
|
|
text.push('\n');
|
|
}
|
|
configurations += 1;
|
|
}
|
|
for item in w.codegen_items() {
|
|
text.push_str(item);
|
|
text.push('\n');
|
|
}
|
|
// A widget's emission is a body, not a file: wrap it so its strings and items lex.
|
|
let found = scan(&format!("fn __emitted() {{\n{text}\n}}"));
|
|
if runs_model(w, &text) {
|
|
on_runtime.push(id.clone());
|
|
}
|
|
for f in found.into_iter().filter(|f| f.rule != "crate") {
|
|
wrong.push(format!("{id}: {f}"));
|
|
}
|
|
emitted += 1;
|
|
}
|
|
// Every primitive, placed alone.
|
|
let mut primitives = 0;
|
|
for nt in flowrs_core::graph::nodes::init_node_types() {
|
|
if reg.get(&nt.name).is_some() {
|
|
continue;
|
|
}
|
|
let mut g = flowrs_core::graph::Graph::new();
|
|
g.add_node(&nt.name, 0.0, 0.0);
|
|
let Ok(src) = g.to_rust() else { continue };
|
|
for f in scan(&src).into_iter().filter(|f| f.rule != "crate") {
|
|
wrong.push(format!("{} (alone): {f}", nt.name));
|
|
}
|
|
primitives += 1;
|
|
}
|
|
// Every corpus graph's whole program.
|
|
let mut programs = 0;
|
|
for e in flowrs_core::graph::corpus::entries() {
|
|
let Ok(parsed) = e.parse() else { continue };
|
|
let Ok(src) = parsed.graph.to_rust() else {
|
|
continue;
|
|
};
|
|
for f in scan(&src).into_iter().filter(|f| f.rule != "crate") {
|
|
wrong.push(format!("corpus {}: {f}", e.slug));
|
|
}
|
|
programs += 1;
|
|
}
|
|
eprintln!(
|
|
"scanned the emissions of {emitted} widgets, {primitives} primitives alone and {programs} \
|
|
corpus programs ({configurations} widget configurations); {} widgets reach a CPU-dependent \
|
|
runtime: {on_runtime:?}",
|
|
on_runtime.len()
|
|
);
|
|
assert!(
|
|
wrong.is_empty(),
|
|
"generated code calls the platform's maths library:\n{}",
|
|
wrong.join("\n")
|
|
);
|
|
assert!(emitted > 250 && primitives > 50 && programs >= 10);
|
|
// The model nodes, and only they, run tract; the census marks exactly these.
|
|
let mut marked: Vec<String> = reg
|
|
.all()
|
|
.into_iter()
|
|
.filter(|w| w.not_bit_identical().is_some())
|
|
.map(|w| w.id())
|
|
.collect();
|
|
marked.sort();
|
|
on_runtime.sort();
|
|
assert_eq!(
|
|
on_runtime, marked,
|
|
"the widgets that run a model through tract and the ones the census marks differ"
|
|
);
|
|
assert!(on_runtime.len() >= 5, "{on_runtime:?}");
|
|
}
|