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
141 lines
5.0 KiB
Rust
141 lines
5.0 KiB
Rust
//! `When` — the clock-enable driver `f64 × bool -> f64`: forwards `value`
|
||
//! verbatim when `gate` is true, and is **quiet** (`eval -> None`, the
|
||
//! `Resample` precedent) when it is false.
|
||
//!
|
||
//! The single clock-enable standard cell of the RTL metaphor (#281): gating
|
||
//! belongs in the *stream*, not in per-node gated variants. Because the run
|
||
//! loop is freshness-gated, every existing stateful reducer composes gated,
|
||
//! unmodified: `SMA(When(x, gate), N)` IS the gated SMA — under
|
||
//! `Firing::Any` a consumer holds its last forwarded value across quiet
|
||
//! cycles and its state advances per *firing*, not per cycle. A quiet
|
||
//! `When` feeding a `Firing::Barrier(N)` group means the group does not
|
||
//! fire that cycle — the quiet member is not at the current timestamp,
|
||
//! which is the correct reading of the barrier contract (deliberately
|
||
//! pinned by an engine test).
|
||
//!
|
||
//! Two inputs (`value: F64`, `gate: Bool`, each `Firing::Any` — the enable
|
||
//! is level-sensitive: the gate *level* is sampled whenever the cell fires,
|
||
//! however long ago it was driven). One `f64` output, no params, stateless.
|
||
//! Emits `None` while either leg is cold (warm-up), and on every gate-false
|
||
//! cycle (the quiet cycle that IS the semantics).
|
||
|
||
use aura_core::{Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, PortSpec, PrimitiveBuilder, ScalarKind};
|
||
|
||
/// Clock-enable forwarder: `value` iff `gate`, else quiet (`None`). A false
|
||
/// gate is a quiet cycle, not a held or zero output — downstream freshness
|
||
/// does not advance.
|
||
pub struct When {
|
||
out: [Cell; 1],
|
||
}
|
||
|
||
impl When {
|
||
/// Build a `When` node.
|
||
pub fn new() -> Self {
|
||
Self { out: [Cell::from_f64(0.0)] }
|
||
}
|
||
|
||
/// The param-generic recipe for a blueprint primitive: paramless, builds
|
||
/// through `When::new`.
|
||
pub fn builder() -> PrimitiveBuilder {
|
||
PrimitiveBuilder::new(
|
||
"When",
|
||
NodeSchema {
|
||
inputs: vec![
|
||
PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "value".into() },
|
||
PortSpec { kind: ScalarKind::Bool, firing: Firing::Any, name: "gate".into() },
|
||
],
|
||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||
params: vec![],
|
||
doc: "emits the input only on cycles where the condition input is true",
|
||
},
|
||
|_| Box::new(When::new()),
|
||
)
|
||
}
|
||
}
|
||
|
||
impl Default for When {
|
||
fn default() -> Self {
|
||
Self::new()
|
||
}
|
||
}
|
||
|
||
impl Node for When {
|
||
fn lookbacks(&self) -> Vec<usize> {
|
||
vec![1, 1]
|
||
}
|
||
|
||
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> {
|
||
let value = ctx.f64_in(0);
|
||
let gate = ctx.bool_in(1);
|
||
if value.is_empty() || gate.is_empty() {
|
||
return None; // cold warm-up (C8) — both legs required
|
||
}
|
||
if !gate[0] {
|
||
return None; // gate low -> QUIET cycle (the semantics, not warm-up)
|
||
}
|
||
self.out[0] = Cell::from_f64(value[0]);
|
||
Some(&self.out)
|
||
}
|
||
|
||
fn label(&self) -> String {
|
||
"When".to_string()
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use aura_core::{AnyColumn, Scalar, Timestamp};
|
||
|
||
fn gate_inputs() -> Vec<AnyColumn> {
|
||
vec![
|
||
AnyColumn::with_capacity(ScalarKind::F64, 1),
|
||
AnyColumn::with_capacity(ScalarKind::Bool, 1),
|
||
]
|
||
}
|
||
|
||
#[test]
|
||
fn when_forwards_on_true_and_is_quiet_on_false() {
|
||
// PROPERTY: gate true -> the value passes through verbatim (bit-equal);
|
||
// gate false -> a QUIET cycle (None), never a held or zero output.
|
||
let mut node = When::new();
|
||
let mut inputs = gate_inputs();
|
||
|
||
inputs[0].push(Scalar::f64(42.5)).unwrap();
|
||
inputs[1].push(Scalar::bool(true)).unwrap();
|
||
assert_eq!(
|
||
node.eval(Ctx::new(&inputs, Timestamp(0))),
|
||
Some([Cell::from_f64(42.5)].as_slice())
|
||
);
|
||
|
||
inputs[0].push(Scalar::f64(7.0)).unwrap();
|
||
inputs[1].push(Scalar::bool(false)).unwrap();
|
||
assert_eq!(node.eval(Ctx::new(&inputs, Timestamp(0))), None);
|
||
|
||
inputs[0].push(Scalar::f64(-3.25)).unwrap();
|
||
inputs[1].push(Scalar::bool(true)).unwrap();
|
||
assert_eq!(
|
||
node.eval(Ctx::new(&inputs, Timestamp(0))),
|
||
Some([Cell::from_f64(-3.25)].as_slice())
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn when_is_none_while_either_leg_is_cold() {
|
||
// Cold warm-up is distinct from the quiet cycle: before both legs have
|
||
// seen a value the cell is cold (C8), regardless of the gate.
|
||
let mut node = When::new();
|
||
let mut inputs = gate_inputs();
|
||
assert_eq!(node.eval(Ctx::new(&inputs, Timestamp(0))), None); // both cold
|
||
inputs[0].push(Scalar::f64(1.0)).unwrap();
|
||
assert_eq!(node.eval(Ctx::new(&inputs, Timestamp(0))), None); // gate cold
|
||
}
|
||
|
||
#[test]
|
||
fn input_slots_are_named_value_gate() {
|
||
let names: Vec<String> =
|
||
When::builder().schema().inputs.iter().map(|p| p.name.clone()).collect();
|
||
assert_eq!(names, ["value", "gate"]);
|
||
}
|
||
}
|