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

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:?}");
}