a8b1ba45c5
The data plane reaches non-scalar structural config (closes #271, the final Data-authorability-boundary item). Op::Add gains args (string pairs, applied via try_args after resolve and before the bind loop — no pending builder ever enters a graph; an arg-bearing type without args refuses as MissingArg). OpError::BadArg carries the precise ArgOpError; the CLI renders it as prose naming the arg, its kind, and the kind's closed hint (exit 1, #175 content-fault class). Serde: PrimitiveData.args (skip-if-empty — args-free documents stay byte-identical, golden-pinned, so every existing content id is stable per C18) with a data-driven format version: the writer emits 1 for args-free documents and 2 when any primitive recursively carries args; the loader accepts 1..=2. The old v2-refusal pin flips deliberately to v3 (named contract change). Identity JSON keeps args — they are structural, and the Rust configured() path and the args op-script bridge to the same identity id (pinned). Roster: Session / LinComb / CostSum enter (33 -> 36, both count pins; the LinComb-rejection assertion flips to the resolved side). introspect --node shows arg rows (name, kind, hint) plus the pending note; OP_REFERENCE's add row teaches the args form. Registry comment de-staled (LinComb is now roster-reachable). Sequencing note: this commit lands op seam and roster together so no intermediate tree state exposes a pending builder.
156 lines
6.9 KiB
Rust
156 lines
6.9 KiB
Rust
//! The closed, compiled-in dispatch from a serialized node **type identity** to
|
|
//! its node-crate 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 the node crates); a loader
|
|
//! takes it as an injected resolver, and a project `cdylib` later supplies its
|
|
//! own (C16) through the same seam.
|
|
//!
|
|
//! Scope (#155, extended #271): the roster resolves any type whose factory is
|
|
//! `Type::builder()` with zero RUST-side arguments — this now includes
|
|
//! arg-bearing types (`Session`, `LinComb`, `CostSum`), whose `builder()` is
|
|
//! itself zero-arg and returns a *pending* `PrimitiveBuilder`, configured
|
|
//! through the typed `args` channel (`try_args`) at the `add` op, before any
|
|
//! bind. `SimBroker(pip_size)` stays out of scope: it is a plain scalar Rust
|
|
//! parameter, not a structural construction arg — a different (unaddressed)
|
|
//! gap. The recording sinks (`Recorder`/`GatedRecorder`/`SeriesReducer`, which
|
|
//! capture an `mpsc::Sender`) stay deliberately absent — an absent `type_id`
|
|
//! yields `None`, so the loader fails cleanly (`LoadError::UnknownNodeType`)
|
|
//! rather than guessing.
|
|
|
|
use aura_backtest::PositionManagement;
|
|
use aura_market::{Resample, Session, SessionFrankfurt};
|
|
use aura_std::{
|
|
Abs, Add, And, Const, CumSum, Delay, Div, Ema, EqConst, Gt, Latch, LinComb, Max, Min, Mul,
|
|
RollingMax, RollingMin, Scale, Select, Sign, Sma, Sqrt, Sub, When,
|
|
};
|
|
use aura_strategy::{
|
|
Bias, CarryCost, ConstantCost, CostSum, FixedStop, LongOnly, Sizer, VolSlippageCost,
|
|
};
|
|
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 node
|
|
/// 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,
|
|
"CostSum" => CostSum,
|
|
"CumSum" => CumSum,
|
|
"Delay" => Delay,
|
|
"Div" => Div,
|
|
"EMA" => Ema,
|
|
"EqConst" => EqConst,
|
|
"FixedStop" => FixedStop,
|
|
"Gt" => Gt,
|
|
"Latch" => Latch,
|
|
"LinComb" => LinComb,
|
|
"LongOnly" => LongOnly,
|
|
"Max" => Max,
|
|
"Min" => Min,
|
|
"Mul" => Mul,
|
|
"PositionManagement" => PositionManagement,
|
|
"Resample" => Resample,
|
|
"RollingMax" => RollingMax,
|
|
"RollingMin" => RollingMin,
|
|
"Scale" => Scale,
|
|
"Select" => Select,
|
|
"Session" => Session,
|
|
"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; an unresolvable id and a sink stay absent so the
|
|
/// loader fails cleanly rather than guessing. `LinComb` — an arg-bearing
|
|
/// type, #271 — now resolves to a PENDING builder (its own `.label()`
|
|
/// still round-trips; it just is not yet constructible without
|
|
/// `try_args`).
|
|
#[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 and a sink are absent; LinComb is now resolved (#271)
|
|
assert!(std_vocabulary("nope").is_none());
|
|
assert!(std_vocabulary("LinComb").is_some());
|
|
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`). #271: `Session`/`LinComb`/`CostSum` moved IN
|
|
/// (arg-bearing types now have zero-arg `builder()` factories); the count
|
|
/// pin moves 33 -> 36.
|
|
#[test]
|
|
fn std_vocabulary_types_lists_exactly_the_resolvable_keys() {
|
|
// known non-members stay out of the list
|
|
assert!(std_vocabulary_types().contains(&"LinComb")); // now arg-bearing-reachable (#271)
|
|
assert!(!std_vocabulary_types().contains(&"Recorder")); // sink
|
|
assert!(!std_vocabulary_types().contains(&"nope"));
|
|
// count guard: pins the roster at exactly 36 entries
|
|
assert_eq!(std_vocabulary_types().len(), 36);
|
|
}
|
|
}
|