//! `EqConst` — a stateless `i64 -> bool` equality gate: `out = (value == target)`. //! //! The `i64 -> bool` analogue of a relational comparator (`Gt` is the `f64 -> bool` //! cousin). Its purpose in the session-breakout strategy is to turn `Session`'s //! `bars_since_open: i64` into per-bar booleans (`isBar3`, `isBar5Close`) via two //! instances bound to `3` and `5`. //! //! The **operator is topology** — `EqConst` is a concrete node type, not an op //! selected by a swept param. Only `target` is a knob (C11/C12); the `==` is fixed //! by the type. (`!=`, `<`, `>` would each be their own node type, never a param.) //! One `i64` input (`Firing::Any`), one `bool` output, allocation-free on the hot //! path (the single-cell output buffer is sized once at construction, C7). use aura_core::{ Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder, ScalarKind, }; /// Stateless `i64 -> bool` equality gate: emits `value == target` each cycle. /// Emits `None` until its input is present (warm-up filter, C8). pub struct EqConst { target: i64, out: [Cell; 1], } impl EqConst { /// Build an equality gate that fires `true` exactly when its input equals /// `target`. pub fn new(target: i64) -> Self { Self { target, out: [Cell::from_bool(false)] } } /// The param-generic recipe for a blueprint primitive: declares `target` and /// builds through `EqConst::new` (the slice is kind-checked before `build` /// runs, so the typed read is total). pub fn builder() -> PrimitiveBuilder { PrimitiveBuilder::new( "EqConst", NodeSchema { inputs: vec![PortSpec { kind: ScalarKind::I64, firing: Firing::Any, name: "value".into() }], output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::Bool }], params: vec![ParamSpec { name: "target".into(), kind: ScalarKind::I64 }], }, |p| Box::new(EqConst::new(p[0].i64())), ) } } impl Node for EqConst { fn lookbacks(&self) -> Vec { vec![1] } fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> { let w = ctx.i64_in(0); if w.is_empty() { return None; // not yet warmed up (C8 filter) } self.out[0] = Cell::from_bool(w[0] == self.target); Some(&self.out) } fn label(&self) -> String { format!("EqConst({})", self.target) } } #[cfg(test)] mod tests { use super::*; use aura_core::{AnyColumn, Scalar, Timestamp}; #[test] fn eqconst_gates_on_equality_with_target() { // The core property: out == (value == target). Bound to target=3, the gate // fires true exactly on the matching input and false on every mismatch — // this is what makes `bars_since_open == 3` the isBar3 boolean. let node_for_depth = EqConst::new(3); // size the input column from the node's lookback, as bootstrap will at wiring let mut inputs = vec![AnyColumn::with_capacity(ScalarKind::I64, node_for_depth.lookbacks()[0])]; let mut node = node_for_depth; let feed = [2_i64, 3, 5]; let expect = [Some(false), Some(true), Some(false)]; for (v, want) in feed.iter().zip(expect) { inputs[0].push(Scalar::i64(*v)).unwrap(); let got = node.eval(Ctx::new(&inputs, Timestamp(0))); match want { Some(b) => assert_eq!(got, Some([Cell::from_bool(b)].as_slice())), None => assert_eq!(got, None), } } } #[test] fn eqconst_is_none_until_input_present() { // a warm-up / empty-input cycle filters (C8) — like every other stateless node let mut node = EqConst::new(3); let inputs = vec![AnyColumn::with_capacity(ScalarKind::I64, 1)]; assert_eq!(node.eval(Ctx::new(&inputs, Timestamp(0))), None); } }