From 550895d5fddb0c41eb32daf086265bc2d943bc52 Mon Sep 17 00:00:00 2001 From: Brummel Date: Wed, 17 Jun 2026 16:44:11 +0200 Subject: [PATCH] =?UTF-8?q?feat(aura-std):=20EqConst=20=E2=80=94=20i64->bo?= =?UTF-8?q?ol=20equality=20gate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stateless gate, out = (value == target): the i64->bool comparator the session-breakout strategy needs to turn Session's bars_since_open into isBar3/isBar5Close (two instances bound to 3 and 5). The operator is topology (a concrete node type), only target is a knob (C11/C12). Build-step 1 of milestone 'Strategy node vocabulary I'. closes #84 --- crates/aura-std/src/eqconst.rs | 105 +++++++++++++++++++++++++++++++++ crates/aura-std/src/lib.rs | 2 + 2 files changed, 107 insertions(+) create mode 100644 crates/aura-std/src/eqconst.rs diff --git a/crates/aura-std/src/eqconst.rs b/crates/aura-std/src/eqconst.rs new file mode 100644 index 0000000..d6a638d --- /dev/null +++ b/crates/aura-std/src/eqconst.rs @@ -0,0 +1,105 @@ +//! `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); + } +} diff --git a/crates/aura-std/src/lib.rs b/crates/aura-std/src/lib.rs index 7126eae..0c05500 100644 --- a/crates/aura-std/src/lib.rs +++ b/crates/aura-std/src/lib.rs @@ -17,6 +17,7 @@ mod add; mod ema; +mod eqconst; mod exposure; mod lincomb; mod recorder; @@ -25,6 +26,7 @@ mod sma; mod sub; pub use add::Add; pub use ema::Ema; +pub use eqconst::EqConst; pub use exposure::Exposure; pub use lincomb::LinComb; pub use recorder::Recorder;