Files
Aura/crates/aura-std/src/and.rs
T
Brummel 657bdf5c22 feat(aura-std): And — bool->bool conjunction
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
2026-06-17 16:57:15 +02:00

127 lines
4.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `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<usize> {
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"]);
}
}