From 657bdf5c22be402e6e6fdfe5ab4609da4f1b37d2 Mon Sep 17 00:00:00 2001 From: Brummel Date: Wed, 17 Jun 2026 16:57:15 +0200 Subject: [PATCH] =?UTF-8?q?feat(aura-std):=20And=20=E2=80=94=20bool->bool?= =?UTF-8?q?=20conjunction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stateless bool x bool -> bool, out = (a && b). Computes entry = breakout && isBar3 in the session-breakout strategy. The bool-input twin of Gt; seed of the logic family (Or/Not are separate node types, the operator is topology, never a swept param). Build-step 3 of milestone 'Strategy node vocabulary I'. closes #86 --- crates/aura-std/src/and.rs | 126 +++++++++++++++++++++++++++++++++++++ crates/aura-std/src/lib.rs | 2 + 2 files changed, 128 insertions(+) create mode 100644 crates/aura-std/src/and.rs diff --git a/crates/aura-std/src/and.rs b/crates/aura-std/src/and.rs new file mode 100644 index 0000000..810606d --- /dev/null +++ b/crates/aura-std/src/and.rs @@ -0,0 +1,126 @@ +//! `And` — a stateless `bool × bool -> bool` conjunction: `out = (a && b)`. +//! +//! The **bool-input twin** of `Gt` (which *emits* a bool from two `f64` legs); +//! `And` *consumes* two bools and ANDs them. Its purpose in the session-breakout +//! strategy is `entry = breakout && isBar3`: a fresh-15m-close breakout that +//! lands exactly on the third bar of the session. +//! +//! The **seed of the logic family** — `Or` and `Not` would each be their own +//! node type, never a swept op param (the operator is topology, like the +//! relational comparators). Two `bool` inputs (`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, PortSpec, PrimitiveBuilder, ScalarKind}; + +/// Stateless `bool × bool -> bool` conjunction: emits `a && b` each cycle. +/// Emits `None` until **both** inputs have a value (warm-up gate, C8). +pub struct And { + out: [Cell; 1], +} + +impl And { + /// Build an `And` node. + pub fn new() -> Self { + Self { out: [Cell::from_bool(false)] } + } + + /// The param-generic recipe for a blueprint primitive: paramless, builds + /// through `And::new`. + pub fn builder() -> PrimitiveBuilder { + PrimitiveBuilder::new( + "And", + NodeSchema { + inputs: vec![ + PortSpec { kind: ScalarKind::Bool, firing: Firing::Any, name: "a".into() }, + PortSpec { kind: ScalarKind::Bool, firing: Firing::Any, name: "b".into() }, + ], + output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::Bool }], + params: vec![], + }, + |_| Box::new(And::new()), + ) + } +} + +impl Default for And { + fn default() -> Self { + Self::new() + } +} + +impl Node for And { + fn lookbacks(&self) -> Vec { + vec![1, 1] + } + + fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> { + let a = ctx.bool_in(0); + let b = ctx.bool_in(1); + if a.is_empty() || b.is_empty() { + return None; // not yet warmed up — both legs required (C8 filter) + } + self.out[0] = Cell::from_bool(a[0] && b[0]); + Some(&self.out) + } + + fn label(&self) -> String { + "And".to_string() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use aura_core::{AnyColumn, Scalar, Timestamp}; + + #[test] + fn and_is_conjunction_once_both_inputs_present() { + // The core property: out == (a && b). The full truth table is pinned — + // true exactly when BOTH legs are true, false on every other row. This is + // what makes `entry = breakout && isBar3` fire only on a breakout that + // lands on the third bar. + let mut node = And::new(); + let mut inputs = vec![ + AnyColumn::with_capacity(ScalarKind::Bool, 1), + AnyColumn::with_capacity(ScalarKind::Bool, 1), + ]; + + let a_feed = [false, true, false, true]; + let b_feed = [false, false, true, true]; + // F,F T,F F,T T,T + let expect = [false, false, false, true]; + + for ((av, bv), want) in a_feed.iter().zip(b_feed.iter()).zip(expect) { + inputs[0].push(Scalar::bool(*av)).unwrap(); + inputs[1].push(Scalar::bool(*bv)).unwrap(); + let got = node.eval(Ctx::new(&inputs, Timestamp(0))); + assert_eq!(got, Some([Cell::from_bool(want)].as_slice())); + } + } + + #[test] + fn and_is_none_until_both_inputs_present() { + // Both-inputs warm-up gate (C8), like `Gt`: only one leg present -> None. + let mut node = And::new(); + let mut inputs = vec![ + AnyColumn::with_capacity(ScalarKind::Bool, 1), + AnyColumn::with_capacity(ScalarKind::Bool, 1), + ]; + + // only input 0 present -> None + inputs[0].push(Scalar::bool(true)).unwrap(); + assert_eq!(node.eval(Ctx::new(&inputs, Timestamp(0))), None); + + // both present -> the conjunction (true && true == true) + inputs[1].push(Scalar::bool(true)).unwrap(); + assert_eq!(node.eval(Ctx::new(&inputs, Timestamp(0))), Some([Cell::from_bool(true)].as_slice())); + } + + #[test] + fn input_slots_are_named_a_b() { + let a = And::builder(); + let names: Vec<&str> = a.schema().inputs.iter().map(|p| p.name.as_str()).collect(); + assert_eq!(names, ["a", "b"]); + } +} diff --git a/crates/aura-std/src/lib.rs b/crates/aura-std/src/lib.rs index c0d214d..ad8dfba 100644 --- a/crates/aura-std/src/lib.rs +++ b/crates/aura-std/src/lib.rs @@ -16,6 +16,7 @@ //! average — a worked producer node proving the `aura-core` `Node` contract. mod add; +mod and; mod ema; mod eqconst; mod exposure; @@ -26,6 +27,7 @@ mod sim_broker; mod sma; mod sub; pub use add::Add; +pub use and::And; pub use ema::Ema; pub use eqconst::EqConst; pub use exposure::Exposure;