refactor(aura-core): split Scalar into a tag-free Cell + ScalarKind

Motivation
----------
`Scalar` was a tagged enum (I64/F64/Bool/Ts), so every scalar value
physically carried its own kind tag. But the kind is already known from
the schema/port/column the value flows through (C7: the type is a
property of the column, not of the value — the hot path is already
columnar `Column<T>`, and `AnyColumn::get` *reconstructs* the tag from
the column on the way out). The per-value tag was therefore redundant
with the kind the surrounding context already holds.

That redundancy had three costs:

  * It baked an implicit `match` (a branch) into every function that read
    a Scalar payload — even where the caller statically knew the type.
    The tag could never be exploited away.
  * Size: a tagged enum is tag + payload = 16 bytes (f64/i64 alignment),
    twice the 8 bytes the value needs. A `Column<Scalar>` would be double
    the memory and half the cache utilisation.
  * It is the shared root of several downstream papercuts we keep hitting
    — the lossy f64 manifest field, the `unreachable!` panic on a
    non-numeric param, the serde-tag question — all symptoms of "the type
    is baked into the value".

Change
------
Introduce `Cell`: a type-erased 64-bit word (`struct Cell(u64)`) that is
not readable without external type context. It is constructed per base
type (`from_i64/from_f64/from_bool/from_ts`) and read only by naming the
type at the call site (`i64()/f64()/bool()/ts()`) — each a branch-free
bit-cast. The hot path resolves the kind once at the boundary (from the
schema) and then reads natively, with no per-value branch. `Cell` knows
nothing of `Scalar` or `ScalarKind`; the dependency is strictly one-way,
and it lives in its own `cell.rs` (more is planned on top of it).

`Scalar` becomes `struct { kind: ScalarKind, cell: Cell }` — the
self-describing form for the dynamic boundaries (builder binding,
serialization, rendering), built on top of `Cell`. Its `as_*` accessors
now `debug_assert` the kind and return the native value (free in
release); calling the wrong accessor is a caller bug, not a checked
`Option`. The variant constructors `Scalar::I64(..)` become associated
fns `Scalar::i64(..)`.

`PartialEq` is hand-written (not derived) to preserve the former enum's
value semantics: kinds must match, then native payloads compare, so f64
keeps IEEE-754 behaviour (`NaN != NaN`, `+0.0 == -0.0`) and a kind
mismatch is never equal even when the raw words coincide. A fixture
(`scalar_eq_is_value_not_bitwise`) pins exactly the cases where bit- and
value-equality diverge, so it can't silently regress. `Cell`'s own
`Eq`/`Hash` stay bitwise — correct for a raw word.

The change is behaviour-preserving: Scalar's observable behaviour is
identical to the pre-Cell enum (the value-equality fixture proves it);
only the internal representation changed. The ~440 call sites across the
workspace are a mechanical constructor rename plus ~12 destructuring
sites (match-arms / `let`-patterns) rewritten to `kind()` + `as_*`.

Verified: cargo build --workspace --all-targets, cargo clippy --workspace
--all-targets -- -D warnings, cargo test --workspace — all green.
This commit is contained in:
2026-06-16 12:12:52 +02:00
parent b188773fb8
commit cd3d1ca9ed
28 changed files with 573 additions and 429 deletions
+49 -49
View File
@@ -827,16 +827,16 @@ mod tests {
ParamSpec { name: "scale".into(), kind: ScalarKind::F64 },
];
let axes = vec![
("scale".to_string(), vec![Scalar::F64(0.5)]),
("sma_cross.fast".to_string(), vec![Scalar::I64(2), Scalar::I64(3)]),
("sma_cross.slow".to_string(), vec![Scalar::I64(4), Scalar::I64(5)]),
("scale".to_string(), vec![Scalar::f64(0.5)]),
("sma_cross.fast".to_string(), vec![Scalar::i64(2), Scalar::i64(3)]),
("sma_cross.slow".to_string(), vec![Scalar::i64(4), Scalar::i64(5)]),
];
assert_eq!(
resolve_axes(&space, &axes),
Ok(vec![
vec![Scalar::I64(2), Scalar::I64(3)],
vec![Scalar::I64(4), Scalar::I64(5)],
vec![Scalar::F64(0.5)],
vec![Scalar::i64(2), Scalar::i64(3)],
vec![Scalar::i64(4), Scalar::i64(5)],
vec![Scalar::f64(0.5)],
]),
);
}
@@ -857,7 +857,7 @@ mod tests {
ParamSpec { name: "b".into(), kind: ScalarKind::I64 },
];
assert_eq!(
resolve_axes(&space, &[("a".to_string(), vec![Scalar::I64(1)])]),
resolve_axes(&space, &[("a".to_string(), vec![Scalar::i64(1)])]),
Err(BindError::MissingKnob("b".to_string())),
);
}
@@ -867,7 +867,7 @@ mod tests {
// a MIXED-kind axis: the second element mismatches; per-element check must
// catch it (a first-element-only check would pass it through to a panic).
let space = vec![ParamSpec { name: "scale".into(), kind: ScalarKind::F64 }];
let axes = vec![("scale".to_string(), vec![Scalar::F64(0.5), Scalar::I64(1)])];
let axes = vec![("scale".to_string(), vec![Scalar::f64(0.5), Scalar::i64(1)])];
assert_eq!(
resolve_axes(&space, &axes),
Err(BindError::KindMismatch {
@@ -907,16 +907,16 @@ mod tests {
let named = resolve_axes(
&space,
&[
("sma_cross.fast.length".to_string(), vec![Scalar::I64(2), Scalar::I64(3)]),
("sma_cross.slow.length".to_string(), vec![Scalar::I64(4), Scalar::I64(5)]),
("exposure.scale".to_string(), vec![Scalar::F64(0.5)]),
("sma_cross.fast.length".to_string(), vec![Scalar::i64(2), Scalar::i64(3)]),
("sma_cross.slow.length".to_string(), vec![Scalar::i64(4), Scalar::i64(5)]),
("exposure.scale".to_string(), vec![Scalar::f64(0.5)]),
],
)
.expect("named axes resolve");
let positional = vec![
vec![Scalar::I64(2), Scalar::I64(3)],
vec![Scalar::I64(4), Scalar::I64(5)],
vec![Scalar::F64(0.5)],
vec![Scalar::i64(2), Scalar::i64(3)],
vec![Scalar::i64(4), Scalar::i64(5)],
vec![Scalar::f64(0.5)],
];
let named_pts = GridSpace::new(&space, named).expect("named grid").points();
let pos_pts = GridSpace::new(&space, positional).expect("positional grid").points();
@@ -932,13 +932,13 @@ mod tests {
ParamSpec { name: "scale".into(), kind: ScalarKind::F64 },
];
let bound = vec![
("scale".to_string(), Scalar::F64(0.5)),
("sma_cross.fast".to_string(), Scalar::I64(2)),
("sma_cross.slow".to_string(), Scalar::I64(4)),
("scale".to_string(), Scalar::f64(0.5)),
("sma_cross.fast".to_string(), Scalar::i64(2)),
("sma_cross.slow".to_string(), Scalar::i64(4)),
];
assert_eq!(
resolve(&space, &bound),
Ok(vec![Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5)]),
Ok(vec![Scalar::i64(2), Scalar::i64(4), Scalar::f64(0.5)]),
);
}
@@ -946,7 +946,7 @@ mod tests {
fn resolve_unknown_knob() {
let space = vec![ParamSpec { name: "scale".into(), kind: ScalarKind::F64 }];
assert_eq!(
resolve(&space, &[("nope".to_string(), Scalar::F64(0.5))]),
resolve(&space, &[("nope".to_string(), Scalar::f64(0.5))]),
Err(BindError::UnknownKnob("nope".to_string())),
);
}
@@ -958,7 +958,7 @@ mod tests {
ParamSpec { name: "b".into(), kind: ScalarKind::I64 },
];
assert_eq!(
resolve(&space, &[("a".to_string(), Scalar::I64(1))]),
resolve(&space, &[("a".to_string(), Scalar::i64(1))]),
Err(BindError::MissingKnob("b".to_string())),
);
}
@@ -967,7 +967,7 @@ mod tests {
fn resolve_kind_mismatch() {
let space = vec![ParamSpec { name: "scale".into(), kind: ScalarKind::F64 }];
assert_eq!(
resolve(&space, &[("scale".to_string(), Scalar::I64(2))]),
resolve(&space, &[("scale".to_string(), Scalar::i64(2))]),
Err(BindError::KindMismatch {
knob: "scale".to_string(),
expected: ScalarKind::F64,
@@ -982,7 +982,7 @@ mod tests {
assert_eq!(
resolve(
&space,
&[("a".to_string(), Scalar::I64(1)), ("a".to_string(), Scalar::I64(2))],
&[("a".to_string(), Scalar::i64(1)), ("a".to_string(), Scalar::i64(2))],
),
Err(BindError::DuplicateBinding("a".to_string())),
);
@@ -993,8 +993,8 @@ mod tests {
// Phase-1 (unknown) wins over Phase-2 (kind mismatch)
let space = vec![ParamSpec { name: "scale".into(), kind: ScalarKind::F64 }];
let bound = vec![
("typo".to_string(), Scalar::I64(9)),
("scale".to_string(), Scalar::I64(2)),
("typo".to_string(), Scalar::i64(9)),
("scale".to_string(), Scalar::i64(2)),
];
assert_eq!(resolve(&space, &bound), Err(BindError::UnknownKnob("typo".to_string())));
}
@@ -1004,8 +1004,8 @@ mod tests {
// intra-binding: check a (unknown) precedes check d (duplicate)
let space = vec![ParamSpec { name: "a".into(), kind: ScalarKind::I64 }];
let bound = vec![
("typo".to_string(), Scalar::I64(1)),
("typo".to_string(), Scalar::I64(2)),
("typo".to_string(), Scalar::i64(1)),
("typo".to_string(), Scalar::i64(2)),
];
assert_eq!(resolve(&space, &bound), Err(BindError::UnknownKnob("typo".to_string())));
}
@@ -1027,7 +1027,7 @@ mod tests {
let (bp2, pos_eq, pos_ex) = composite_sma_cross_harness();
let mut positional = bp2
.bootstrap_with_params(vec![Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5)])
.bootstrap_with_params(vec![Scalar::i64(2), Scalar::i64(4), Scalar::f64(0.5)])
.expect("positional bootstrap");
positional.run(vec![Box::new(VecSource::new(synthetic_prices()))]);
let pos_eq_v = pos_eq.try_iter().collect::<Vec<_>>();
@@ -1066,7 +1066,7 @@ mod tests {
if a.is_empty() || b.is_empty() {
return None;
}
self.out[0] = Scalar::F64(a[0] + b[0]);
self.out[0] = Scalar::f64(a[0] + b[0]);
Some(&self.out)
}
}
@@ -1084,7 +1084,7 @@ mod tests {
if w.is_empty() {
return None;
}
self.out[0] = Scalar::F64(w[0]);
self.out[0] = Scalar::f64(w[0]);
Some(&self.out)
}
}
@@ -1116,7 +1116,7 @@ mod tests {
PrimitiveBuilder::new(
"Pass1",
NodeSchema { inputs: vec![f64_any()], output: out_v(), params: vec![] },
|_| Box::new(Pass1 { out: [Scalar::F64(0.0)] }),
|_| Box::new(Pass1 { out: [Scalar::f64(0.0)] }),
)
.into()
}
@@ -1124,7 +1124,7 @@ mod tests {
PrimitiveBuilder::new(
"Join2",
NodeSchema { inputs: vec![f64_any(), f64_any()], output: out_v(), params: vec![] },
|_| Box::new(Join2 { out: [Scalar::F64(0.0)] }),
|_| Box::new(Join2 { out: [Scalar::f64(0.0)] }),
)
.into()
}
@@ -1236,7 +1236,7 @@ mod tests {
// distinct node names -> fan-in distinguishable -> compiles (the two SMA
// length params are supplied so the only thing under test is the fan-in)
let bp = sma_cross_under_root(true);
assert!(bp.compile_with_params(&[Scalar::I64(2), Scalar::I64(4)]).is_ok());
assert!(bp.compile_with_params(&[Scalar::i64(2), Scalar::i64(4)]).is_ok());
}
#[test]
@@ -1745,7 +1745,7 @@ mod tests {
// (b) the same graph authored as a composite blueprint, compiled
let (bp, comp_eq, comp_ex) = composite_sma_cross_harness();
let mut composed = bp
.bootstrap_with_params(vec![Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5)])
.bootstrap_with_params(vec![Scalar::i64(2), Scalar::i64(4), Scalar::f64(0.5)])
.expect("composite blueprint compiles");
composed.run(vec![Box::new(VecSource::new(prices))]);
@@ -1808,7 +1808,7 @@ mod tests {
vec![], // output
);
let mut h = bp
.bootstrap_with_params(vec![Scalar::I64(2), Scalar::I64(4)])
.bootstrap_with_params(vec![Scalar::i64(2), Scalar::i64(4)])
.expect("multi-output composite bootstraps");
h.run(vec![Box::new(VecSource::new(prices))]);
@@ -1825,13 +1825,13 @@ mod tests {
fn injecting_a_different_vector_changes_the_run() {
let prices = synthetic_prices();
let (bp, eq, _ex) = composite_sma_cross_harness();
let mut a = bp.bootstrap_with_params(vec![Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5)])
let mut a = bp.bootstrap_with_params(vec![Scalar::i64(2), Scalar::i64(4), Scalar::f64(0.5)])
.expect("compiles");
a.run(vec![Box::new(VecSource::new(prices.clone()))]);
let a_eq = eq.try_iter().collect::<Vec<_>>();
let (bp2, eq2, _ex2) = composite_sma_cross_harness();
let mut b = bp2.bootstrap_with_params(vec![Scalar::I64(5), Scalar::I64(20), Scalar::F64(1.0)])
let mut b = bp2.bootstrap_with_params(vec![Scalar::i64(5), Scalar::i64(20), Scalar::f64(1.0)])
.expect("compiles");
b.run(vec![Box::new(VecSource::new(prices))]);
let b_eq = eq2.try_iter().collect::<Vec<_>>();
@@ -1844,7 +1844,7 @@ mod tests {
fn wrong_kind_is_a_param_kind_mismatch() {
let (bp, _eq, _ex) = composite_sma_cross_harness();
// slot 0 is I64 (an SMA length); inject F64 there
let err = bp.bootstrap_with_params(vec![Scalar::F64(2.0), Scalar::I64(4), Scalar::F64(0.5)])
let err = bp.bootstrap_with_params(vec![Scalar::f64(2.0), Scalar::i64(4), Scalar::f64(0.5)])
.unwrap_err();
assert!(matches!(err, CompileError::ParamKindMismatch { slot: 0, .. }));
}
@@ -1853,13 +1853,13 @@ mod tests {
fn wrong_arity_is_a_param_arity_error() {
let (short, _e1, _x1) = composite_sma_cross_harness();
assert!(matches!(
short.bootstrap_with_params(vec![Scalar::I64(2)]).unwrap_err(),
short.bootstrap_with_params(vec![Scalar::i64(2)]).unwrap_err(),
CompileError::ParamArity { expected: 3, got: 1 }
));
let (long, _e2, _x2) = composite_sma_cross_harness();
assert!(matches!(
long.bootstrap_with_params(
vec![Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5), Scalar::F64(0.0)]
vec![Scalar::i64(2), Scalar::i64(4), Scalar::f64(0.5), Scalar::f64(0.0)]
).unwrap_err(),
CompileError::ParamArity { expected: 3, got: 4 }
));
@@ -1869,11 +1869,11 @@ mod tests {
fn same_vector_bootstraps_identically() {
let prices = synthetic_prices();
let (bp, eq, _ex) = composite_sma_cross_harness();
let mut a = bp.bootstrap_with_params(vec![Scalar::I64(3), Scalar::I64(9), Scalar::F64(0.7)])
let mut a = bp.bootstrap_with_params(vec![Scalar::i64(3), Scalar::i64(9), Scalar::f64(0.7)])
.expect("compiles");
a.run(vec![Box::new(VecSource::new(prices.clone()))]);
let (bp2, eq2, _ex2) = composite_sma_cross_harness();
let mut b = bp2.bootstrap_with_params(vec![Scalar::I64(3), Scalar::I64(9), Scalar::F64(0.7)])
let mut b = bp2.bootstrap_with_params(vec![Scalar::i64(3), Scalar::i64(9), Scalar::f64(0.7)])
.expect("compiles");
b.run(vec![Box::new(VecSource::new(prices))]);
assert_eq!(eq.try_iter().collect::<Vec<_>>(), eq2.try_iter().collect::<Vec<_>>());
@@ -1896,7 +1896,7 @@ mod tests {
// the same blueprint, actually compiled to its flat node array; each flat
// node's own declared params, concatenated in flat-node order
let flat = bp.compile_with_params(&[Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5)]).expect("harness compiles");
let flat = bp.compile_with_params(&[Scalar::i64(2), Scalar::i64(4), Scalar::f64(0.5)]).expect("harness compiles");
let from_flat: Vec<ParamSpec> =
flat.signatures.iter().flat_map(|s| s.params.clone()).collect();
@@ -1981,7 +1981,7 @@ mod tests {
let strat = Composite::new(
"sma2_entry",
vec![
Sma::builder().named("bias").bind("length", Scalar::I64(2)).into(),
Sma::builder().named("bias").bind("length", Scalar::i64(2)).into(),
Exposure::builder().named("exp").into(),
],
vec![], // edges — irrelevant to param_space()
@@ -2055,7 +2055,7 @@ mod tests {
// the same blueprint, compiled to its flat node array; each flat node's
// own declared params, concatenated in flat-node order
let flat = bp.compile_with_params(&[Scalar::I64(2), Scalar::I64(4), Scalar::F64(1.0), Scalar::F64(-1.0)]).expect("nested composite compiles");
let flat = bp.compile_with_params(&[Scalar::i64(2), Scalar::i64(4), Scalar::f64(1.0), Scalar::f64(-1.0)]).expect("nested composite compiles");
let from_flat: Vec<ParamSpec> =
flat.signatures.iter().flat_map(|s| s.params.clone()).collect();
@@ -2190,7 +2190,7 @@ mod tests {
vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }], source: Some(ScalarKind::F64) }],
vec![],
);
let err = root.compile_with_params(&[Scalar::I64(3)]);
let err = root.compile_with_params(&[Scalar::i64(3)]);
// kind fault caught pre-build (no panic) — same variant bootstrap would give
assert!(matches!(
err,
@@ -2209,7 +2209,7 @@ mod tests {
);
// the Ok arm holds a FlatGraph (not Debug), so assert via the Err arm.
assert_eq!(
root.compile_with_params(&[Scalar::I64(3)]).err(),
root.compile_with_params(&[Scalar::i64(3)]).err(),
Some(CompileError::UnboundRootRole { role: 0 })
);
}
@@ -2262,7 +2262,7 @@ mod tests {
// two params declared (one per SMA); supply both so the only failure under
// test is the duplicate path (green today, DuplicateParamPath after).
assert_eq!(
bp.compile_with_params(&[Scalar::I64(2), Scalar::I64(3)]).err(),
bp.compile_with_params(&[Scalar::i64(2), Scalar::i64(3)]).err(),
Some(CompileError::DuplicateParamPath("dup.sma.length".to_string()))
);
}
@@ -2280,7 +2280,7 @@ mod tests {
let paramless_sma = PrimitiveBuilder::new(
"Pass1",
NodeSchema { inputs: vec![f64_any()], output: out_v(), params: vec![] },
|_| Box::new(Pass1 { out: [Scalar::F64(0.0)] }),
|_| Box::new(Pass1 { out: [Scalar::f64(0.0)] }),
)
.named("sma");
let asym = Composite::new(
@@ -2312,7 +2312,7 @@ mod tests {
bp.param_space().into_iter().map(|p| p.name).collect::<Vec<_>>(),
["asym.sma.length"]
);
assert!(bp.compile_with_params(&[Scalar::I64(2)]).is_ok());
assert!(bp.compile_with_params(&[Scalar::i64(2)]).is_ok());
}
#[test]