c8dc57e2aa
Second half of the #281 clock-enable set. CumSum is the generic run-length accumulator with O(1) Neumaier-compensated state — the branching Neumaier variant, not Sma's plain Kahan, so it stays exact when a large running total absorbs a tiny sample (the standalone accumulator has no windowed-magnitude guarantee; module doc explains why the helper is not reused). When is the clock-enable driver: forwards value iff gate, else a quiet cycle (eval->None), so every stateful reducer composes gated unmodified — SMA(When(x,g),N) is the gated SMA. Rosters CumSum and When (both count pins 31->33) and ships two engine E2E fixtures: When forwarding/quiet-downstream through the real GraphBuilder->compile->run seam, and a quiet When leg suppressing a whole Resample Barrier(0) group (the deliberate stall semantics). refs #281
141 lines
6.2 KiB
Rust
141 lines
6.2 KiB
Rust
//! The closed, compiled-in dispatch from a serialized node **type identity** to
|
|
//! its `aura-std` builder factory — the load-side counterpart to the type label
|
|
//! a blueprint serializes (`PrimitiveBuilder::label`). This is **not** a dynamic
|
|
//! registry (domain invariant 9): it is a `match` the crate that *owns* the
|
|
//! nodes writes, over its own closed, compiled-in vocabulary (C24 — nodes are
|
|
//! referenced "by compiled-in type identity", never a by-name marketplace). The
|
|
//! engine cannot hold this table (it does not depend on `aura-std`); a loader
|
|
//! takes it as an injected resolver, and a project `cdylib` later supplies its
|
|
//! own (C16) through the same seam.
|
|
//!
|
|
//! Scope (#155): only the **zero-argument** `Type::builder()` factories are
|
|
//! resolvable. Builders that take structural construction arguments
|
|
//! (`LinComb(arity)`, `CostSum(n_costs)`, `SimBroker(pip_size)`, `Session(..)`)
|
|
//! and the recording sinks (`Recorder`/`GatedRecorder`/`SeriesReducer`, which
|
|
//! capture an `mpsc::Sender`) are deliberately absent — an absent `type_id`
|
|
//! yields `None`, so the loader fails cleanly (`LoadError::UnknownNodeType`)
|
|
//! rather than guessing. Serialising structural-axis construction args is a
|
|
//! later, additive extension (#156/C20).
|
|
|
|
use crate::{
|
|
Abs, Add, And, Bias, CarryCost, Const, ConstantCost, CumSum, Delay, Div, Ema, EqConst,
|
|
FixedStop, Gt, Latch, LongOnly, Max, Min, Mul, PositionManagement, Resample, RollingMax,
|
|
RollingMin, Scale, Select, SessionFrankfurt, Sign, Sizer, Sma, Sqrt, Sub, VolSlippageCost, When,
|
|
};
|
|
use aura_core::PrimitiveBuilder;
|
|
|
|
/// The single roster source (#160): one `"TypeId" => Type` line per zero-arg
|
|
/// node, expanded into BOTH the `std_vocabulary` match and the
|
|
/// `std_vocabulary_types` list — the two surfaces cannot drift apart, and
|
|
/// adding a zero-arg node to the vocabulary is exactly one line here (plus the
|
|
/// conscious count-pin bumps: the in-crate shape test and aura-cli's
|
|
/// cross-boundary `--vocabulary` count e2e). What this cannot guard: a new
|
|
/// zero-arg node never rostered at all — that fails safe (clean
|
|
/// `LoadError::UnknownNodeType` on load, merely absent from `--vocabulary`;
|
|
/// #160).
|
|
macro_rules! std_vocabulary_roster {
|
|
($($type_id:literal => $ty:ty),+ $(,)?) => {
|
|
/// Resolve a serialized node type identity (the `PrimitiveBuilder::label`,
|
|
/// e.g. `"SMA"`) to a fresh, unbound builder from the node's own factory.
|
|
/// Returns `None` for any identity outside the zero-arg `aura-std`
|
|
/// vocabulary.
|
|
pub fn std_vocabulary(type_id: &str) -> Option<PrimitiveBuilder> {
|
|
Some(match type_id {
|
|
$($type_id => <$ty>::builder(),)+
|
|
_ => return None,
|
|
})
|
|
}
|
|
|
|
/// The enumerable companion to [`std_vocabulary`]: the closed set of
|
|
/// zero-arg type identities the resolver answers. A `fn(&str)->Option<…>`
|
|
/// resolver cannot be listed, so build-free vocabulary introspection
|
|
/// (#157) reads this. Generated from the same roster as the match arms
|
|
/// (#160) — the two surfaces agree by construction.
|
|
pub fn std_vocabulary_types() -> &'static [&'static str] {
|
|
&[$($type_id),+]
|
|
}
|
|
};
|
|
}
|
|
|
|
std_vocabulary_roster! {
|
|
"Abs" => Abs,
|
|
"Add" => Add,
|
|
"And" => And,
|
|
"Bias" => Bias,
|
|
"CarryCost" => CarryCost,
|
|
"Const" => Const,
|
|
"ConstantCost" => ConstantCost,
|
|
"CumSum" => CumSum,
|
|
"Delay" => Delay,
|
|
"Div" => Div,
|
|
"EMA" => Ema,
|
|
"EqConst" => EqConst,
|
|
"FixedStop" => FixedStop,
|
|
"Gt" => Gt,
|
|
"Latch" => Latch,
|
|
"LongOnly" => LongOnly,
|
|
"Max" => Max,
|
|
"Min" => Min,
|
|
"Mul" => Mul,
|
|
"PositionManagement" => PositionManagement,
|
|
"Resample" => Resample,
|
|
"RollingMax" => RollingMax,
|
|
"RollingMin" => RollingMin,
|
|
"Scale" => Scale,
|
|
"Select" => Select,
|
|
"SessionFrankfurt" => SessionFrankfurt,
|
|
"Sign" => Sign,
|
|
"Sizer" => Sizer,
|
|
"SMA" => Sma,
|
|
"Sqrt" => Sqrt,
|
|
"Sub" => Sub,
|
|
"VolSlippageCost" => VolSlippageCost,
|
|
"When" => When,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{std_vocabulary, std_vocabulary_types};
|
|
|
|
/// Every rostered key resolves to a builder that carries that exact type
|
|
/// label back, and only the rostered keys do. The builder's own `label()`
|
|
/// is the independent oracle: a typo'd roster key fails the lookup-label
|
|
/// agreement even though the match arms and the list share the same
|
|
/// (mistyped) literal; construction-arg nodes and sinks stay absent so the
|
|
/// loader fails cleanly rather than guessing.
|
|
#[test]
|
|
fn std_vocabulary_resolves_known_and_rejects_unknown() {
|
|
for &type_id in std_vocabulary_types() {
|
|
let builder =
|
|
std_vocabulary(type_id).unwrap_or_else(|| panic!("{type_id} is in the vocabulary"));
|
|
assert_eq!(
|
|
builder.label(),
|
|
type_id,
|
|
"resolver key must round-trip to the same type label"
|
|
);
|
|
}
|
|
// an unknown id, a construction-arg node, and a sink are all absent
|
|
assert!(std_vocabulary("nope").is_none());
|
|
assert!(std_vocabulary("LinComb").is_none());
|
|
assert!(std_vocabulary("Recorder").is_none());
|
|
}
|
|
|
|
/// List <-> resolver agreement holds BY CONSTRUCTION since the #160 roster
|
|
/// macro (one source expands into both surfaces); what stays pinned here is
|
|
/// the roster's SHAPE. The count pin is deliberate friction: any roster
|
|
/// line added or dropped trips it, making a vocabulary change a conscious
|
|
/// act. The residual #160 drift — a new zero-arg node never rostered at
|
|
/// all — is not catchable here (no enumeration of zero-arg builders
|
|
/// exists); it fails safe (clean `UnknownNodeType` on load, merely absent
|
|
/// from `--vocabulary`).
|
|
#[test]
|
|
fn std_vocabulary_types_lists_exactly_the_resolvable_keys() {
|
|
// known non-members stay out of the list
|
|
assert!(!std_vocabulary_types().contains(&"LinComb")); // construction-arg node
|
|
assert!(!std_vocabulary_types().contains(&"Recorder")); // sink
|
|
assert!(!std_vocabulary_types().contains(&"nope"));
|
|
// count guard: pins the roster at exactly 33 entries
|
|
assert_eq!(std_vocabulary_types().len(), 33);
|
|
}
|
|
}
|