refactor(std): std_vocabulary_roster! macro — one source for the vocabulary roster
The three hand-kept copies of the 22-key zero-arg roster in aura-std/vocabulary.rs (std_vocabulary match arms, std_vocabulary_types list, the test's inline array) collapse into one private declarative macro invoked once with the "TypeId" => Type pairs — the resolver and the enumerable list now agree by construction (the #160 failure mode: a node resolvable but silently absent from graph introspect --vocabulary, or vice versa). Byte-preserving: same fn signatures, same 22 ids, same order; lib.rs re-export and every consumer untouched; the vocabulary stays a closed compiled-in set (invariant 9 / C24 — a compile-time expansion, no registry). Adding a zero-arg node is now one roster line plus the conscious count-pin bumps. Tests: the two unit tests reshaped (round-trip iterates the generated list with PrimitiveBuilder::label() as the independent oracle; the shape test keeps the count pin as deliberate friction). The E2E phase added one cross-boundary pin beyond the plan — kept: graph_introspect_vocabulary_lists_exactly_the_closed_roster_count checks the same count through Env::type_ids() and the CLI print loop across the real process boundary, which no roster-internal test can see. Accepted residual (documented at the roster site, recorded on #160): a new zero-arg node never rostered at all stays unguarded — no enumeration of zero-arg builders exists — and fails safe in both directions (clean UnknownNodeType on load; merely absent from --vocabulary). Verification: cargo build clean; cargo test --workspace 885 passed / 0 failed (884 baseline + 1 new e2e); clippy --all-targets -D warnings clean; cargo doc --no-deps 0 warnings. closes #160, refs #180
This commit is contained in:
@@ -24,92 +24,76 @@ use crate::{
|
||||
};
|
||||
use aura_core::PrimitiveBuilder;
|
||||
|
||||
/// 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 {
|
||||
"Add" => Add::builder(),
|
||||
"And" => And::builder(),
|
||||
"Bias" => Bias::builder(),
|
||||
"CarryCost" => CarryCost::builder(),
|
||||
"ConstantCost" => ConstantCost::builder(),
|
||||
"Delay" => Delay::builder(),
|
||||
"EMA" => Ema::builder(),
|
||||
"EqConst" => EqConst::builder(),
|
||||
"FixedStop" => FixedStop::builder(),
|
||||
"Gt" => Gt::builder(),
|
||||
"Latch" => Latch::builder(),
|
||||
"LongOnly" => LongOnly::builder(),
|
||||
"Mul" => Mul::builder(),
|
||||
"PositionManagement" => PositionManagement::builder(),
|
||||
"Resample" => Resample::builder(),
|
||||
"RollingMax" => RollingMax::builder(),
|
||||
"RollingMin" => RollingMin::builder(),
|
||||
"Sizer" => Sizer::builder(),
|
||||
"SMA" => Sma::builder(),
|
||||
"Sqrt" => Sqrt::builder(),
|
||||
"Sub" => Sub::builder(),
|
||||
"VolSlippageCost" => VolSlippageCost::builder(),
|
||||
_ => return None,
|
||||
})
|
||||
/// 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 bump in the tests). 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),+]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// 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. Kept in
|
||||
/// lockstep with the `std_vocabulary` match arms by maintainer discipline — a
|
||||
/// type added there must be added here. The reverse (an arm with no entry here)
|
||||
/// is not test-enforced and cannot be, from a `fn` resolver; it fails safe (the
|
||||
/// type stays load-resolvable, only unlisted; #160).
|
||||
pub fn std_vocabulary_types() -> &'static [&'static str] {
|
||||
&[
|
||||
"Add", "And", "Bias", "CarryCost", "ConstantCost", "Delay", "EMA",
|
||||
"EqConst", "FixedStop", "Gt", "Latch", "LongOnly", "Mul",
|
||||
"PositionManagement", "Resample", "RollingMax", "RollingMin", "Sizer",
|
||||
"SMA", "Sqrt", "Sub", "VolSlippageCost",
|
||||
]
|
||||
std_vocabulary_roster! {
|
||||
"Add" => Add,
|
||||
"And" => And,
|
||||
"Bias" => Bias,
|
||||
"CarryCost" => CarryCost,
|
||||
"ConstantCost" => ConstantCost,
|
||||
"Delay" => Delay,
|
||||
"EMA" => Ema,
|
||||
"EqConst" => EqConst,
|
||||
"FixedStop" => FixedStop,
|
||||
"Gt" => Gt,
|
||||
"Latch" => Latch,
|
||||
"LongOnly" => LongOnly,
|
||||
"Mul" => Mul,
|
||||
"PositionManagement" => PositionManagement,
|
||||
"Resample" => Resample,
|
||||
"RollingMax" => RollingMax,
|
||||
"RollingMin" => RollingMin,
|
||||
"Sizer" => Sizer,
|
||||
"SMA" => Sma,
|
||||
"Sqrt" => Sqrt,
|
||||
"Sub" => Sub,
|
||||
"VolSlippageCost" => VolSlippageCost,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::std_vocabulary;
|
||||
use super::{std_vocabulary, std_vocabulary_types};
|
||||
|
||||
/// Every key in the closed vocabulary resolves to a builder that carries
|
||||
/// that exact type label back, and only the zero-arg keys do. The label
|
||||
/// round-trip is the property: a typo in any match key (the silent-
|
||||
/// unresolvable failure this resolver guards against) breaks it, as does a
|
||||
/// key mapped to the wrong builder; construction-arg nodes and sinks stay
|
||||
/// absent so the loader fails cleanly rather than guessing.
|
||||
/// 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() {
|
||||
// every zero-arg key round-trips: lookup -> builder -> same type label.
|
||||
// (this list is the resolver's whole vocabulary; a missing/typo'd arm
|
||||
// fails the lookup, a mis-mapped arm fails the label assertion.)
|
||||
for type_id in [
|
||||
"Add",
|
||||
"And",
|
||||
"Bias",
|
||||
"CarryCost",
|
||||
"ConstantCost",
|
||||
"Delay",
|
||||
"EMA",
|
||||
"EqConst",
|
||||
"FixedStop",
|
||||
"Gt",
|
||||
"Latch",
|
||||
"LongOnly",
|
||||
"Mul",
|
||||
"PositionManagement",
|
||||
"Resample",
|
||||
"RollingMax",
|
||||
"RollingMin",
|
||||
"Sizer",
|
||||
"SMA",
|
||||
"Sqrt",
|
||||
"Sub",
|
||||
"VolSlippageCost",
|
||||
] {
|
||||
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!(
|
||||
@@ -124,28 +108,21 @@ mod tests {
|
||||
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() {
|
||||
use super::std_vocabulary_types;
|
||||
// every listed type resolves to a builder carrying that exact label
|
||||
for &type_id in std_vocabulary_types() {
|
||||
let builder = std_vocabulary(type_id)
|
||||
.unwrap_or_else(|| panic!("{type_id} listed but unresolvable"));
|
||||
assert_eq!(
|
||||
builder.label(),
|
||||
type_id,
|
||||
"listed type must round-trip to its label"
|
||||
);
|
||||
}
|
||||
// the list is the WHOLE resolvable set: a couple of known non-members stay out
|
||||
// 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 list at exactly 22 entries, so a dropped or added
|
||||
// list entry trips this; with the round-trip loop above, the list is
|
||||
// exactly the 22 listed resolvable keys. The reverse drift — a resolver
|
||||
// arm with no list entry — is not catchable from a `fn(&str)` resolver
|
||||
// (not enumerable); it fails safe (load-resolvable, only unlisted; #160).
|
||||
// count guard: pins the roster at exactly 22 entries
|
||||
assert_eq!(std_vocabulary_types().len(), 22);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user