refactor(aura-engine): Cell into the construction path — param-plane base/frontend split
Cell becomes the carrier of the construction path; Scalar narrows to the author/render boundaries. The validated/enumerated param point carries no redundant kind (it lives once, in the declared param-space); at the author edge the kind is a checksum — two independent sources, the typed value vs. the slot — so the self-describing Scalar stays there. The name->slot binding is dynamic (C23), so the check is necessarily a runtime one and the value must self-describe for it. Base/frontend split (the AnyColumn push/push_cell pattern one level up): compile_with_cells / bootstrap_with_cells are the kind-check-free base; compile_with_params / bootstrap_with_params are the frontend that adds only the per-value kind checksum, strips to cells (new Scalar::cell(), the partner of Scalar::from_cell), and delegates. lower_items loses its per-primitive kind-check; PrimitiveBuilder::build and the std node builders (sma/ema/exposure/lincomb) read cells. Boundary: construction -> Cell (PrimitiveBuilder::build, lower_items, GridSpace.axes/points, SweepPoint.params, the sweep closure). Author edges stay Scalar (GridSpace::new, bind, compile_with_params/bootstrap_with_params). walkforward chosen_params stays a self-describing Scalar report record (Option A — WalkForwardResult carries no space); the cell winner is reconstructed once at the WindowRun site via from_cell. injective-check moved ahead of the arity-check in the frontend to preserve the pre-split error order (DuplicateParamPath before ParamArity). The lossy i64->f64 param projection (scalar_as_f64 / scalar_as_param_f64) is deliberately untouched — a separate follow-up. Behaviour-preserving (C1): build --all-targets / test / clippy -D warnings / doc all clean across the workspace.
This commit is contained in:
@@ -74,8 +74,14 @@ pub struct ParamSpec {
|
||||
/// inverse of positional binding (C23): names are a derived view, not identity.
|
||||
/// `space` and `point` are co-indexed in `param_space()` slot order; a length
|
||||
/// mismatch (contract violation) truncates to the shorter, never panics.
|
||||
pub fn zip_params(space: &[ParamSpec], point: &[Scalar]) -> Vec<(String, Scalar)> {
|
||||
space.iter().zip(point).map(|(ps, v)| (ps.name.clone(), *v)).collect()
|
||||
///
|
||||
/// The point arrives as tag-free [`Cell`]s (C7): a *validated* param point no
|
||||
/// longer carries a per-value kind — that kind lives once, in the co-indexed
|
||||
/// `ParamSpec`. This is `AnyColumn::get` for the param plane: a bare cell meets
|
||||
/// the kind from its co-present schema and becomes a self-describing [`Scalar`]
|
||||
/// only here, at the render boundary.
|
||||
pub fn zip_params(space: &[ParamSpec], point: &[Cell]) -> Vec<(String, Scalar)> {
|
||||
space.iter().zip(point).map(|(ps, c)| (ps.name.clone(), Scalar::from_cell(ps.kind, *c))).collect()
|
||||
}
|
||||
|
||||
/// One param bound to a structural constant by [`PrimitiveBuilder::bind`] — a
|
||||
@@ -106,7 +112,7 @@ pub struct PrimitiveBuilder {
|
||||
// The build closure's type is exactly the recipe contract (a param slice in, a
|
||||
// boxed node out); a type alias would not clarify it.
|
||||
#[allow(clippy::type_complexity)]
|
||||
build: Box<dyn Fn(&[Scalar]) -> Box<dyn Node>>,
|
||||
build: Box<dyn Fn(&[Cell]) -> Box<dyn Node>>,
|
||||
}
|
||||
|
||||
impl PrimitiveBuilder {
|
||||
@@ -116,7 +122,7 @@ impl PrimitiveBuilder {
|
||||
pub fn new(
|
||||
name: &'static str,
|
||||
schema: NodeSchema,
|
||||
build: impl Fn(&[Scalar]) -> Box<dyn Node> + 'static,
|
||||
build: impl Fn(&[Cell]) -> Box<dyn Node> + 'static,
|
||||
) -> Self {
|
||||
Self { name, instance_name: None, schema, bound: Vec::new(), build: Box::new(build) }
|
||||
}
|
||||
@@ -162,9 +168,10 @@ impl PrimitiveBuilder {
|
||||
pub fn params(&self) -> &[ParamSpec] {
|
||||
&self.schema.params
|
||||
}
|
||||
/// Build a sized node from its param slice (the slice is kind-checked by the
|
||||
/// caller before this runs).
|
||||
pub fn build(&self, params: &[Scalar]) -> Box<dyn Node> {
|
||||
/// Build a sized node from its param slice — tag-free [`Cell`]s, already
|
||||
/// kind-checked against the declared slots by the caller (the param-plane
|
||||
/// frontend), so the closure reads each by its own declared kind.
|
||||
pub fn build(&self, params: &[Cell]) -> Box<dyn Node> {
|
||||
(self.build)(params)
|
||||
}
|
||||
/// The param-generic render label for the blueprint view (C22): just the node
|
||||
@@ -221,9 +228,9 @@ impl PrimitiveBuilder {
|
||||
self.bound.push(BoundParam { pos: orig, name, kind, value });
|
||||
self.schema.params.remove(pos); // [param_space side] shrink the declared surface
|
||||
let inner = self.build; // [value side] wrap the build closure
|
||||
self.build = Box::new(move |open: &[Scalar]| {
|
||||
self.build = Box::new(move |open: &[Cell]| {
|
||||
let mut full = open.to_vec();
|
||||
full.insert(pos, value); // re-splice at the slot's original position
|
||||
full.insert(pos, value.cell()); // bound value (kind-checked above) → Cell, at its original slot
|
||||
inner(&full)
|
||||
});
|
||||
self
|
||||
@@ -402,7 +409,7 @@ mod tests {
|
||||
|
||||
/// A node whose label echoes the param vector its constructor received — lets a
|
||||
/// test read back the positional vector `.build()` reconstructed after binds.
|
||||
struct Probe(Vec<Scalar>);
|
||||
struct Probe(Vec<Cell>);
|
||||
impl Node for Probe {
|
||||
fn lookbacks(&self) -> Vec<usize> {
|
||||
vec![]
|
||||
@@ -440,19 +447,19 @@ mod tests {
|
||||
bound.params().iter().map(|p| p.name.as_str()).collect::<Vec<_>>(),
|
||||
["c"], // only c remains open
|
||||
);
|
||||
let built = bound.build(&[Scalar::f64(9.0)]); // inject c
|
||||
let built = bound.build(&[Cell::from_f64(9.0)]); // inject c
|
||||
assert_eq!(
|
||||
built.label(),
|
||||
format!("{:?}", vec![Scalar::i64(7), Scalar::i64(8), Scalar::f64(9.0)]),
|
||||
format!("{:?}", vec![Cell::from_i64(7), Cell::from_i64(8), Cell::from_f64(9.0)]),
|
||||
);
|
||||
|
||||
// partial: bind only b; a and c stay open and keep their positions
|
||||
let built2 = probe3()
|
||||
.bind("b", Scalar::i64(8))
|
||||
.build(&[Scalar::i64(7), Scalar::f64(9.0)]); // a, c injected in order
|
||||
.build(&[Cell::from_i64(7), Cell::from_f64(9.0)]); // a, c injected in order
|
||||
assert_eq!(
|
||||
built2.label(),
|
||||
format!("{:?}", vec![Scalar::i64(7), Scalar::i64(8), Scalar::f64(9.0)]),
|
||||
format!("{:?}", vec![Cell::from_i64(7), Cell::from_i64(8), Cell::from_f64(9.0)]),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -534,7 +541,7 @@ mod tests {
|
||||
#[cfg(test)]
|
||||
mod zip_params_tests {
|
||||
use super::{zip_params, ParamSpec};
|
||||
use crate::{Scalar, ScalarKind};
|
||||
use crate::{Cell, Scalar, ScalarKind};
|
||||
|
||||
#[test]
|
||||
fn zip_params_pairs_names_with_values_in_slot_order() {
|
||||
@@ -542,7 +549,7 @@ mod zip_params_tests {
|
||||
ParamSpec { name: "fast".into(), kind: ScalarKind::I64 },
|
||||
ParamSpec { name: "scale".into(), kind: ScalarKind::F64 },
|
||||
];
|
||||
let point = vec![Scalar::i64(2), Scalar::f64(0.5)];
|
||||
let point = vec![Cell::from_i64(2), Cell::from_f64(0.5)];
|
||||
assert_eq!(
|
||||
zip_params(&space, &point),
|
||||
vec![
|
||||
@@ -558,9 +565,9 @@ mod zip_params_tests {
|
||||
ParamSpec { name: "a".into(), kind: ScalarKind::I64 },
|
||||
ParamSpec { name: "b".into(), kind: ScalarKind::I64 },
|
||||
];
|
||||
let point = vec![Scalar::i64(7), Scalar::i64(9)];
|
||||
let point = vec![Cell::from_i64(7), Cell::from_i64(9)];
|
||||
let hand: Vec<(String, Scalar)> =
|
||||
space.iter().zip(&point).map(|(ps, v)| (ps.name.clone(), *v)).collect();
|
||||
space.iter().zip(&point).map(|(ps, c)| (ps.name.clone(), Scalar::from_cell(ps.kind, *c))).collect();
|
||||
assert_eq!(zip_params(&space, &point), hand);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,27 @@ impl Scalar {
|
||||
Scalar { kind: ScalarKind::Timestamp, cell: Cell::from_ts(v) }
|
||||
}
|
||||
|
||||
/// Re-attach a `kind` to a tag-free [`Cell`] at a dynamic boundary — the
|
||||
/// inverse of the hot path's tag-stripping. Used where the kind comes from a
|
||||
/// co-present schema rather than from the value itself: a validated param
|
||||
/// point carries its values as bare cells (C7 — the kind lives once, in the
|
||||
/// co-indexed `ParamSpec`), and `zip_params` recombines the two into the
|
||||
/// self-describing `Scalar` only at the render boundary. The `Cell`'s bits are
|
||||
/// taken verbatim; correctness rests on `kind` actually describing them (the
|
||||
/// bootstrap's edge-time check, not a per-value tag).
|
||||
pub fn from_cell(kind: ScalarKind, cell: Cell) -> Self {
|
||||
Scalar { kind, cell }
|
||||
}
|
||||
|
||||
/// The bare [`Cell`], kind dropped — the strip side of the dynamic boundary
|
||||
/// (the inverse of [`from_cell`](Self::from_cell)). Used by the param-plane
|
||||
/// frontend to hand a validated value to the cell-carrying construction base,
|
||||
/// once the kind has been checked against the slot it no longer needs to ride
|
||||
/// along.
|
||||
pub fn cell(self) -> Cell {
|
||||
self.cell
|
||||
}
|
||||
|
||||
/// The kind tag of this scalar (a field read, no branch).
|
||||
pub fn kind(self) -> ScalarKind {
|
||||
self.kind
|
||||
@@ -176,6 +197,16 @@ mod tests {
|
||||
assert_eq!(Scalar::ts(Timestamp(7)).as_ts(), Timestamp(7));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_cell_and_cell_are_inverse() {
|
||||
// cell() strips the kind; from_cell re-attaches it — the dynamic-boundary
|
||||
// round trip the param-plane frontend/base split relies on (strip to hand a
|
||||
// validated value to the cell base, re-attach to render it).
|
||||
for s in [Scalar::i64(7), Scalar::f64(2.5), Scalar::bool(true), Scalar::ts(Timestamp(9))] {
|
||||
assert_eq!(Scalar::from_cell(s.kind(), s.cell()), s);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scalar_eq_is_value_not_bitwise() {
|
||||
// f64 follows IEEE-754 exactly as the former enum did: a NaN is never
|
||||
|
||||
Reference in New Issue
Block a user