313 lines
13 KiB
Rust
313 lines
13 KiB
Rust
//! A widget whose generated program panics must say so before it is run, and a widget
|
|
//! that runs a substitute for the model or device it is named after must not be counted as
|
|
//! shipping.
|
|
//!
|
|
//! A seam is codegen that emits `unimplemented!` at declared defaults, in process or
|
|
//! init. The program compiles and then panics on the first run. That was the deliberate
|
|
//! choice over fabricating APIs that do not exist (`AudioBuffer::stft_magnitude`, a
|
|
//! one-call candle `txt2img`). The set is empty: every builtin emits a real call.
|
|
//!
|
|
//! A stand-in is the other way not to ship: it compiles AND runs, but what runs is a
|
|
//! deterministic substitute — seeded noise for Stable Diffusion, a byte histogram for
|
|
//! CLIP, a word rotation for a language model. Neither the seam scan nor the T0 census
|
|
//! sees it, so it declares itself through `Widget::stand_in`.
|
|
//!
|
|
//! The live census is [`flowrs_core::graph::widget::seam_widget_ids`],
|
|
//! [`flowrs_core::graph::widget::stand_in_widget_ids`] and
|
|
//! [`flowrs_core::graph::widget::shipping_widget_count`], not this comment.
|
|
//! [`the_launch_census_matches_the_registry`] locks the numbers those functions return.
|
|
|
|
use flowrs_core::Graph;
|
|
use flowrs_core::graph::diagnostics::check;
|
|
|
|
#[test]
|
|
fn a_widget_that_emits_no_seam_does_not_warn() {
|
|
// `gen.txt2img` used to be a seam. It is a stand-in now — no seam, and not shipping
|
|
// either — and `image.grayscale` always shipped. Neither generates a seam.
|
|
for id in ["gen.txt2img", "image.grayscale"] {
|
|
let mut g = Graph::new();
|
|
g.add_node(id, 0.0, 0.0);
|
|
assert!(
|
|
!check(&g)
|
|
.iter()
|
|
.any(|d| d.message.contains("generates a seam")),
|
|
"{id} emits a real implementation and must not be flagged",
|
|
);
|
|
}
|
|
}
|
|
|
|
/// The set is computed from what widgets emit, not from a hand-kept list — a list would
|
|
/// drift the moment a seam was filled in.
|
|
#[test]
|
|
fn the_seam_set_matches_what_the_widgets_actually_emit() {
|
|
let seams = flowrs_core::graph::widget::seam_widget_ids();
|
|
|
|
for id in seams {
|
|
let mut g = Graph::new();
|
|
g.add_node(id, 0.0, 0.0);
|
|
let src = g.to_rust().unwrap_or_default();
|
|
assert!(
|
|
src.contains("unimplemented!"),
|
|
"`{id}` is listed as a seam but its program has none",
|
|
);
|
|
}
|
|
|
|
// And nothing outside the set emits one.
|
|
let registry = flowrs_core::graph::widget::builtin_registry();
|
|
for w in registry.all() {
|
|
let id = w.id();
|
|
if seams.contains(&id) {
|
|
continue;
|
|
}
|
|
let mut g = Graph::new();
|
|
g.add_node(&id, 0.0, 0.0);
|
|
let Ok(src) = g.to_rust() else { continue };
|
|
assert!(
|
|
!src.contains("unimplemented!"),
|
|
"`{id}` emits a seam but is not listed, so no warning would be raised",
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Launch census. Quoted by the site from library code; this test is what stops the
|
|
/// numbers drifting from what codegen actually emits and what the widgets declare.
|
|
///
|
|
/// Shipping is total builtins minus seams minus stand-ins. A seam is a widget whose
|
|
/// codegen emits `unimplemented!` (see [`the_seam_set_matches_what_the_widgets_actually_emit`]);
|
|
/// a stand-in is a widget whose `stand_in()` names the model or device it does not reach.
|
|
/// Counted from the builtin registry: 262 widgets, 0 seams, 16 stand-ins, 246 shipping.
|
|
// +3 (2026-10-18): live.serial, live.rtt, live.symbol — shipping, no stand-in.
|
|
// +10 (2026-10-18): capture.open and nine decode.* — shipping, no stand-in.
|
|
// +1 (2026-10-19): live.biosignal — shipping, no stand-in.
|
|
// +1 (2026-10-20): decode.harp — shipping, no stand-in.
|
|
// +2 (2026-10-20): unit.convert, clock.convert — shipping, no stand-in.
|
|
// -7 stand-ins (2026-10-20, work/mcuio): the UART, I2C, SPI, ADC and timer widgets lower
|
|
// to flowrs-hal on Embassy and RTIC and run as firmware on the F4 and RP2040 twins.
|
|
// +1 (2026-10-21, work/partition): imu.complementary — shipping, no stand-in.
|
|
// +3 (2026-10-22, work/ros2edge): imu.decode, ros.publish, ros.subscribe — shipping, no
|
|
// stand-in (the ROS endpoints publish and subscribe for real in the generated ROS 2 node).
|
|
// +4 (2026-10-28, work/seamsg): mavlink.telemetry, mavlink.heartbeat, mavlink.setpoint,
|
|
// mavlink.command — shipping, no stand-in (the generated program talks to the vehicle).
|
|
// +6 (2026-10-29, work/mcudsp): embedded.adc.capture, dsp.samples, dsp.fir, dsp.biquad,
|
|
// dsp.moving, dsp.decimate — shipping, no stand-in (fixed-size frames, lowered to the MCU).
|
|
// +1 (2026-10-29, work/teststation): instrument.station — shipping, no stand-in (the editor
|
|
// runs it on simulated instruments; the generated program drives the real ones).
|
|
const LAUNCH_BUILTIN_WIDGETS: usize = 273;
|
|
const LAUNCH_SEAM_WIDGETS: usize = 0;
|
|
const LAUNCH_STAND_IN_WIDGETS: usize = 16;
|
|
const LAUNCH_SHIPPING_WIDGETS: usize =
|
|
LAUNCH_BUILTIN_WIDGETS - LAUNCH_SEAM_WIDGETS - LAUNCH_STAND_IN_WIDGETS;
|
|
|
|
/// Seam ids in the order [`WidgetRegistry::seam_ids`](flowrs_core::graph::widget::WidgetRegistry::seam_ids)
|
|
/// returns them. The count above is `LAUNCH_SEAM_IDS.len()`; both have to move together.
|
|
const LAUNCH_SEAM_IDS: &[&str] = &[];
|
|
|
|
/// Stand-in ids, sorted, as [`WidgetRegistry::stand_in_ids`](flowrs_core::graph::widget::WidgetRegistry::stand_in_ids)
|
|
/// returns them. Models: the generative pillar, whose ten widgets run `gen_runtime`
|
|
/// substitutes at both tiers, and `ai.complete`, whose generated program answers with a
|
|
/// word rotation. Devices: `audio.record` and `microphone` return a synthetic 1 kHz tone,
|
|
/// `audio.play` only counts samples, and `video.open` / `video.write` move raw frames
|
|
/// through flowRS's own container instead of a codec. `neural.detect` and `neural.segment`
|
|
/// left this list when they began decoding the model they are given, and the seven
|
|
/// embedded peripherals (UART read and write, I2C read and write, SPI, ADC, the timer) when
|
|
/// Embassy and RTIC began lowering them to flowrs-hal: no embedded widget is on it now
|
|
/// ([`no_embedded_widget_is_a_stand_in`]).
|
|
const LAUNCH_STAND_IN_IDS: &[&str] = &[
|
|
"ai.complete",
|
|
"audio.play",
|
|
"audio.record",
|
|
"gen.caption",
|
|
"gen.clip_image",
|
|
"gen.clip_text",
|
|
"gen.img2img",
|
|
"gen.inpaint",
|
|
"gen.txt2img",
|
|
"gen.upscale",
|
|
"gen.vae_decode",
|
|
"gen.vae_encode",
|
|
"gen.whisper",
|
|
"microphone",
|
|
"video.open",
|
|
"video.write",
|
|
];
|
|
|
|
/// Every embedded widget reaches real hardware on the MCU targets, so none is a stand-in.
|
|
/// Pinned at zero: the plan's blocker was seven of eleven declared stand-ins (twelve
|
|
/// embedded widgets since the ADC capture).
|
|
#[test]
|
|
fn no_embedded_widget_is_a_stand_in() {
|
|
let registry = flowrs_core::graph::widget::builtin_registry();
|
|
let embedded: Vec<String> = registry
|
|
.all()
|
|
.iter()
|
|
.map(|w| w.id())
|
|
.filter(|id| id.starts_with("embedded."))
|
|
.collect();
|
|
assert_eq!(embedded.len(), 12, "{embedded:?}");
|
|
let stand_ins: Vec<&String> = embedded
|
|
.iter()
|
|
.filter(|id| flowrs_core::graph::widget::stand_in_widget_ids().contains(*id))
|
|
.collect();
|
|
assert_eq!(stand_ins.len(), 0, "embedded stand-ins: {stand_ins:?}");
|
|
for id in &embedded {
|
|
assert!(
|
|
flowrs_core::codegen::mcu::SURFACE.contains(&id.as_str()),
|
|
"{id} is not on the MCU surface"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn the_launch_census_matches_the_registry() {
|
|
let registry = flowrs_core::graph::widget::builtin_registry();
|
|
let seams = flowrs_core::graph::widget::seam_widget_ids();
|
|
let stand_ins = flowrs_core::graph::widget::stand_in_widget_ids();
|
|
let shipping = flowrs_core::graph::widget::shipping_widget_count();
|
|
|
|
assert_eq!(registry.len(), registry.all().len());
|
|
assert_eq!(registry.shipping_count(), shipping);
|
|
|
|
// A stand-in is never counted as shipping: the shipping widgets are exactly those in
|
|
// neither set, and the three numbers account for every builtin once.
|
|
let shipped: Vec<String> = registry
|
|
.all()
|
|
.iter()
|
|
.map(|w| w.id())
|
|
.filter(|id| !seams.contains(id) && !stand_ins.contains(id))
|
|
.collect();
|
|
assert_eq!(
|
|
shipping,
|
|
shipped.len(),
|
|
"shipping_widget_count() counts a stand-in or a seam as shipping"
|
|
);
|
|
for id in stand_ins {
|
|
assert!(
|
|
!shipped.contains(id),
|
|
"{id} is a stand-in and counted as shipping"
|
|
);
|
|
}
|
|
let overlap = seams.intersection(stand_ins).count();
|
|
assert_eq!(
|
|
shipping + seams.len() + stand_ins.len() - overlap,
|
|
registry.len()
|
|
);
|
|
|
|
let listed = registry.seam_ids();
|
|
let mut cached: Vec<_> = seams.iter().cloned().collect();
|
|
cached.sort();
|
|
assert_eq!(
|
|
listed, cached,
|
|
"seam_ids() must be seam_widget_ids(), sorted"
|
|
);
|
|
assert!(
|
|
listed.windows(2).all(|w| w[0] < w[1]),
|
|
"seam ids are sorted"
|
|
);
|
|
assert_eq!(LAUNCH_SEAM_IDS.len(), LAUNCH_SEAM_WIDGETS);
|
|
assert_eq!(
|
|
listed.iter().map(String::as_str).collect::<Vec<_>>(),
|
|
LAUNCH_SEAM_IDS,
|
|
"seam ids changed"
|
|
);
|
|
|
|
let standing = registry.stand_in_ids();
|
|
assert!(
|
|
standing.windows(2).all(|w| w[0] < w[1]),
|
|
"stand-in ids are sorted"
|
|
);
|
|
assert_eq!(LAUNCH_STAND_IN_IDS.len(), LAUNCH_STAND_IN_WIDGETS);
|
|
assert_eq!(
|
|
standing.iter().map(String::as_str).collect::<Vec<_>>(),
|
|
LAUNCH_STAND_IN_IDS,
|
|
"stand-in ids changed. A widget that now runs the real model drops its \
|
|
`stand_in()`; one that runs a substitute must declare it"
|
|
);
|
|
// Each names the real thing it stands in for, which is what the editor's badge says.
|
|
for w in registry.all() {
|
|
if let Some(real) = w.stand_in() {
|
|
assert!(
|
|
!real.trim().is_empty(),
|
|
"{} declares a stand-in for nothing",
|
|
w.id()
|
|
);
|
|
}
|
|
}
|
|
|
|
let total = registry.len();
|
|
assert_eq!(
|
|
(total, seams.len(), stand_ins.len(), shipping),
|
|
(
|
|
LAUNCH_BUILTIN_WIDGETS,
|
|
LAUNCH_SEAM_WIDGETS,
|
|
LAUNCH_STAND_IN_WIDGETS,
|
|
LAUNCH_SHIPPING_WIDGETS
|
|
),
|
|
"launch census changed: total={total} seams={listed:?} stand-ins={standing:?} \
|
|
shipping={shipping}. Update LAUNCH_* only when codegen really gained or lost a \
|
|
seam, a widget stopped or started standing in, or a widget was added",
|
|
);
|
|
}
|
|
|
|
/// The substitute runtimes, and what each stands in for. A widget whose program calls one
|
|
/// computes the substitute, whatever its name says, so it must declare a stand-in; the
|
|
/// census would otherwise count it as shipping. `host_pcm`'s WAV codec and the embedded
|
|
/// board's latches are not in the list: a WAV file is real, and the board is what the
|
|
/// Host target simulates for widgets the MCU targets lower for real (`embedded.rs`
|
|
/// holds that split to `codegen::mcu::SURFACE`).
|
|
const SUBSTITUTES: &[&str] = &[
|
|
"gen_runtime::",
|
|
"llm_local::",
|
|
"host_pcm::capture",
|
|
"host_pcm::playback",
|
|
"video_io::",
|
|
];
|
|
|
|
#[test]
|
|
fn every_widget_that_runs_a_substitute_declares_a_stand_in() {
|
|
use flowrs_core::graph::widget::CodegenContext;
|
|
use std::collections::HashMap;
|
|
let registry = flowrs_core::graph::widget::builtin_registry();
|
|
let mut caught = 0usize;
|
|
for w in registry.all() {
|
|
let ctx = CodegenContext {
|
|
var_names: HashMap::new(),
|
|
imports: vec![],
|
|
state_fields: vec![],
|
|
profile: flowrs_core::graph::Profile::scientific(),
|
|
params: w
|
|
.parameters()
|
|
.into_iter()
|
|
.map(|p| (p.name, p.default))
|
|
.collect(),
|
|
};
|
|
let inputs = w
|
|
.inputs()
|
|
.into_iter()
|
|
.map(|p| (p.name.clone(), p.name))
|
|
.collect();
|
|
let process = w.codegen(&ctx, inputs).process;
|
|
if let Some(sub) = SUBSTITUTES.iter().find(|s| process.contains(*s)) {
|
|
caught += 1;
|
|
assert!(
|
|
w.stand_in().is_some(),
|
|
"{} calls `{sub}` but does not declare a stand-in",
|
|
w.id()
|
|
);
|
|
}
|
|
}
|
|
// The scan has something to hold: the generative ten, ai.complete, the two audio
|
|
// stand-ins, the microphone and the video pair.
|
|
assert_eq!(caught, 16, "the substitute scan found {caught} widgets");
|
|
}
|
|
|
|
/// A widget that used to be a seam must still build. A seam, when one exists, is a
|
|
/// warning, not an error — refusing the build would block the person filling it in.
|
|
#[test]
|
|
fn a_seam_does_not_block_the_build() {
|
|
let mut g = Graph::new();
|
|
g.add_node("gen.txt2img", 0.0, 0.0);
|
|
assert!(
|
|
flowrs_core::graph::diagnostics::is_buildable(&g),
|
|
"a seam is a warning, not an error",
|
|
);
|
|
}
|