diff --git a/next/crates/wingfoil-next/examples/dual_mode.rs b/next/crates/wingfoil-next/examples/dual_mode.rs index e3212c46..3ef27766 100644 --- a/next/crates/wingfoil-next/examples/dual_mode.rs +++ b/next/crates/wingfoil-next/examples/dual_mode.rs @@ -4,57 +4,77 @@ //! `Op` semantics — and the compiled one gets the compiler's full //! optimization across node boundaries. //! +//! The wiring is a **split/recombine** DAG: a counter is split on parity into +//! two labelled branches which are then merged back into one stream — so this +//! also shows the macro deriving a non-linear graph (a shared apex node and a +//! recombine) for both engines: +//! +//! ```text +//! count (apex — shared node, once/cycle) +//! / \ +//! is_odd? is_even? (split on parity) +//! | | +//! "{i} is odd" "{i} is even" (format each branch) +//! \ / +//! merge (recombine — at most one fires/tick) +//! | +//! print +//! ``` +//! //! ```sh //! cargo run -p wingfoil-next --release --example dual_mode //! ``` -use std::time::{Duration, Instant}; +use std::time::Duration; use wingfoil::{NanoTime, RunFor, RunMode}; const HISTORICAL: RunMode = RunMode::HistoricalFrom(NanoTime::ZERO); -const CYCLES: u32 = 200_000; -const PERIOD: Duration = Duration::from_micros(1); +const CYCLES: u32 = 10; +const PERIOD: Duration = Duration::from_millis(10); -// One definition — a valid fluent wiring function. The macro parses it to -// derive the DAG and expands to a module: `evens_sum::wire` (this function, -// verbatim), `evens_sum::interpreted()` (built through wire), and -// `evens_sum::compiled()` (the monomorphized schedule derived from the same +// One definition — a valid fluent wiring function whose DAG is a +// split/recombine. The macro parses it to derive the DAG and expands to a +// module: `odds_evens::wire` (this function, verbatim), +// `odds_evens::interpreted()` (built through wire), and +// `odds_evens::compiled()` (the monomorphized schedule derived from the same // tokens). +// +// `count` is referenced three times, so it is a *shared* apex node: the +// interpreted engine runs it once per cycle and fans the tick out, and the +// compiled engine emits it once and feeds every reader from the same slot. +// `merge` is the recombine — since a number is either odd or even, at most +// one branch fires on any tick. wingfoil_next::graph! { - fn evens_sum(g: &GraphBuilder) -> Stream { + fn odds_evens(g: &GraphBuilder) -> Stream> { let count = g.ticker(PERIOD).count(); let is_even = count.map(|i| i.is_multiple_of(2)); - let sum = count.filter(&is_even).fold(0u64, |acc, v| *acc += v); - sum + let is_odd = is_even.map(|b| !b); + let odd_str = count.filter(&is_odd).map(|i| format!("{i} is odd")); + let even_str = count.filter(&is_even).map(|i| format!("{i} is even")); + let acc = odd_str.merge(&even_str).accumulate(); + acc } } fn main() { let run_for = RunFor::Cycles(CYCLES); - let t = Instant::now(); - let (mut runner, sum) = evens_sum::interpreted(); + // Interpreted: build the graph, run it, read the accumulated labels. + let (mut runner, acc) = odds_evens::interpreted(); runner.run(HISTORICAL, run_for).unwrap(); - let a: u64 = runner.value(sum); - let interp_time = t.elapsed(); + let interpreted = runner.value(acc); - let t = Instant::now(); - let (b,) = evens_sum::compiled(HISTORICAL, run_for).unwrap(); - let compiled_time = t.elapsed(); + // Compiled: the same tokens, monomorphized into a standalone runner. + let (compiled,) = odds_evens::compiled(HISTORICAL, run_for).unwrap(); - assert_eq!(a, b, "engines must agree"); - println!("sum of even counts over {CYCLES} cycles: {a} (both engines agree)"); - println!( - "interpreted: {interp_time:?} ({:.1} ns/cycle)", - interp_time.as_nanos() as f64 / CYCLES as f64 - ); - println!( - "compiled: {compiled_time:?} ({:.1} ns/cycle)", - compiled_time.as_nanos() as f64 / CYCLES as f64 - ); + assert_eq!(interpreted, compiled, "engines must agree"); + + for line in &interpreted { + println!("{line}"); + } println!( - "speedup: {:.1}x (run with --release for representative numbers)", - interp_time.as_secs_f64() / compiled_time.as_secs_f64() + "\n{} labels over {CYCLES} cycles — interpreted and compiled engines agree.", + interpreted.len() ); }