//! `When` — the clock-enable driver `f64 × bool -> f64`: forwards `value` //! verbatim when `gate` is true, and is **quiet** (`eval -> None`, the //! `Resample` precedent) when it is false. //! //! The single clock-enable standard cell of the RTL metaphor (#281): gating //! belongs in the *stream*, not in per-node gated variants. Because the run //! loop is freshness-gated, every existing stateful reducer composes gated, //! unmodified: `SMA(When(x, gate), N)` IS the gated SMA — under //! `Firing::Any` a consumer holds its last forwarded value across quiet //! cycles and its state advances per *firing*, not per cycle. A quiet //! `When` feeding a `Firing::Barrier(N)` group means the group does not //! fire that cycle — the quiet member is not at the current timestamp, //! which is the correct reading of the barrier contract (deliberately //! pinned by an engine test). //! //! Two inputs (`value: F64`, `gate: Bool`, each `Firing::Any` — the enable //! is level-sensitive: the gate *level* is sampled whenever the cell fires, //! however long ago it was driven). One `f64` output, no params, stateless. //! Emits `None` while either leg is cold (warm-up), and on every gate-false //! cycle (the quiet cycle that IS the semantics). use aura_core::{Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, PortSpec, PrimitiveBuilder, ScalarKind}; /// Clock-enable forwarder: `value` iff `gate`, else quiet (`None`). A false /// gate is a quiet cycle, not a held or zero output — downstream freshness /// does not advance. pub struct When { out: [Cell; 1], } impl When { /// Build a `When` node. pub fn new() -> Self { Self { out: [Cell::from_f64(0.0)] } } /// The param-generic recipe for a blueprint primitive: paramless, builds /// through `When::new`. pub fn builder() -> PrimitiveBuilder { PrimitiveBuilder::new( "When", NodeSchema { inputs: vec![ PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "value".into() }, PortSpec { kind: ScalarKind::Bool, firing: Firing::Any, name: "gate".into() }, ], output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }], params: vec![], doc: "emits the input only on cycles where the condition input is true", }, |_| Box::new(When::new()), ) } } impl Default for When { fn default() -> Self { Self::new() } } impl Node for When { fn lookbacks(&self) -> Vec { vec![1, 1] } fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> { let value = ctx.f64_in(0); let gate = ctx.bool_in(1); if value.is_empty() || gate.is_empty() { return None; // cold warm-up (C8) — both legs required } if !gate[0] { return None; // gate low -> QUIET cycle (the semantics, not warm-up) } self.out[0] = Cell::from_f64(value[0]); Some(&self.out) } fn label(&self) -> String { "When".to_string() } } #[cfg(test)] mod tests { use super::*; use aura_core::{AnyColumn, Scalar, Timestamp}; fn gate_inputs() -> Vec { vec![ AnyColumn::with_capacity(ScalarKind::F64, 1), AnyColumn::with_capacity(ScalarKind::Bool, 1), ] } #[test] fn when_forwards_on_true_and_is_quiet_on_false() { // PROPERTY: gate true -> the value passes through verbatim (bit-equal); // gate false -> a QUIET cycle (None), never a held or zero output. let mut node = When::new(); let mut inputs = gate_inputs(); inputs[0].push(Scalar::f64(42.5)).unwrap(); inputs[1].push(Scalar::bool(true)).unwrap(); assert_eq!( node.eval(Ctx::new(&inputs, Timestamp(0))), Some([Cell::from_f64(42.5)].as_slice()) ); inputs[0].push(Scalar::f64(7.0)).unwrap(); inputs[1].push(Scalar::bool(false)).unwrap(); assert_eq!(node.eval(Ctx::new(&inputs, Timestamp(0))), None); inputs[0].push(Scalar::f64(-3.25)).unwrap(); inputs[1].push(Scalar::bool(true)).unwrap(); assert_eq!( node.eval(Ctx::new(&inputs, Timestamp(0))), Some([Cell::from_f64(-3.25)].as_slice()) ); } #[test] fn when_is_none_while_either_leg_is_cold() { // Cold warm-up is distinct from the quiet cycle: before both legs have // seen a value the cell is cold (C8), regardless of the gate. let mut node = When::new(); let mut inputs = gate_inputs(); assert_eq!(node.eval(Ctx::new(&inputs, Timestamp(0))), None); // both cold inputs[0].push(Scalar::f64(1.0)).unwrap(); assert_eq!(node.eval(Ctx::new(&inputs, Timestamp(0))), None); // gate cold } #[test] fn input_slots_are_named_value_gate() { let names: Vec = When::builder().schema().inputs.iter().map(|p| p.name.clone()).collect(); assert_eq!(names, ["value", "gate"]); } }