//! 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 { 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 = 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 = 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) -> 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 = Vec::new(); let mut on_runtime: Vec = Vec::new(); let mut emitted = 0; let mut configurations = 0; for w in reg.all() { let id = w.id(); let defaults: HashMap = w .parameters() .into_iter() .map(|p| (p.name, p.default)) .collect(); let inputs: HashMap = 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 = 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:?}"); }