//! `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"]); } }