//! `Select` — the stateless 2:1 mux `bool × f64 × f64 -> f64`: forwards //! `then` when `cond` is true, `otherwise` when it is false. //! //! The missing RTL standard cell (#281): the generic consumer of the Bool //! plane back into F64. Until it existed, conditional values required //! arithmetic contortions; with it, `count_true` composes as //! `CumSum(Select(cond, 1.0, 0.0))` — no Counter primitive needed. //! //! Three inputs (`cond: Bool`, `then: F64`, `otherwise: F64`, each //! `Firing::Any`), one `f64` output, no params, stateless. Emits `None` //! until **all three** legs are warm — the mux reads both data inputs, as //! [`Gt`](crate::Gt) requires both of its legs. use aura_core::{Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, PortSpec, PrimitiveBuilder, ScalarKind}; /// Stateless 2:1 mux: `out = if cond { then } else { otherwise }`, decided /// fresh each fired cycle. Emits `None` until all three legs are warm (C8). pub struct Select { out: [Cell; 1], } impl Select { /// Build a `Select` node. pub fn new() -> Self { Self { out: [Cell::from_f64(0.0)] } } /// The param-generic recipe for a blueprint primitive: paramless, builds /// through `Select::new`. pub fn builder() -> PrimitiveBuilder { PrimitiveBuilder::new( "Select", NodeSchema { inputs: vec![ PortSpec { kind: ScalarKind::Bool, firing: Firing::Any, name: "cond".into() }, PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "then".into() }, PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "otherwise".into() }, ], output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }], params: vec![], doc: "chooses between two inputs by a boolean selector", }, |_| Box::new(Select::new()), ) } } impl Default for Select { fn default() -> Self { Self::new() } } impl Node for Select { fn lookbacks(&self) -> Vec { vec![1, 1, 1] } fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> { let cond = ctx.bool_in(0); let then = ctx.f64_in(1); let otherwise = ctx.f64_in(2); if cond.is_empty() || then.is_empty() || otherwise.is_empty() { return None; // all three legs required (C8 warm-up gate) } let c = cond.get(0).unwrap_or(false); self.out[0] = Cell::from_f64(if c { then[0] } else { otherwise[0] }); Some(&self.out) } fn label(&self) -> String { "Select".to_string() } } #[cfg(test)] mod tests { use super::*; use aura_core::{AnyColumn, Scalar, Timestamp}; fn mux_inputs() -> Vec { vec![ AnyColumn::with_capacity(ScalarKind::Bool, 1), AnyColumn::with_capacity(ScalarKind::F64, 1), AnyColumn::with_capacity(ScalarKind::F64, 1), ] } #[test] fn select_muxes_per_cycle_by_cond() { // PROPERTY: out = if cond { then } else { otherwise }, re-decided each // cycle (stateless) — the 2:1 mux, both branches exercised plus a flip. let mut node = Select::new(); let mut inputs = mux_inputs(); let cases = [((true, 1.0, 9.0), 1.0), ((false, 1.0, 9.0), 9.0), ((true, 5.0, 7.0), 5.0)]; for ((c, t, o), want) in cases { inputs[0].push(Scalar::bool(c)).unwrap(); inputs[1].push(Scalar::f64(t)).unwrap(); inputs[2].push(Scalar::f64(o)).unwrap(); let got = node.eval(Ctx::new(&inputs, Timestamp(0))); assert_eq!(got, Some([Cell::from_f64(want)].as_slice())); } } #[test] fn select_is_none_until_all_three_legs_warm() { // The mux reads BOTH data inputs: a cold otherwise-leg gates the output // even while cond is true and then is present. let mut node = Select::new(); let mut inputs = mux_inputs(); inputs[0].push(Scalar::bool(true)).unwrap(); inputs[1].push(Scalar::f64(1.0)).unwrap(); assert_eq!(node.eval(Ctx::new(&inputs, Timestamp(0))), None); inputs[2].push(Scalar::f64(9.0)).unwrap(); assert_eq!( node.eval(Ctx::new(&inputs, Timestamp(0))), Some([Cell::from_f64(1.0)].as_slice()) ); } #[test] fn input_slots_are_named_cond_then_otherwise() { let names: Vec = Select::builder().schema().inputs.iter().map(|p| p.name.clone()).collect(); assert_eq!(names, ["cond", "then", "otherwise"]); } }