a32dc38d18
C29 compile/unit seam, task 3 of the self-description plan: every aura-std NodeSchema literal threads its one-line doc. Four texts were corrected against the actual eval/finalize semantics rather than taken from the plan table verbatim: Delay is a lag-N register (not one-step), GatedRecorder flushes an ungated final row at finalize, Latch is a level-sensitive set/reset register (captures no input), SeriesReducer emits its single summary row at finalize (not per cycle). Gate: cargo build -p aura-std --lib clean; full std test run follows at the all-crates gate once strategy/backtest/engine thread their sites (the earlier per-crate test gate was unsatisfiable in isolation -- std's dev-deps pull crates whose sites belong to later tasks). refs #316
128 lines
4.5 KiB
Rust
128 lines
4.5 KiB
Rust
//! `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![],
|
||
doc: "logical AND of two boolean series",
|
||
},
|
||
|_| 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"]);
|
||
}
|
||
}
|