refactor(aura-engine): fold resolve/resolve_axes into one generic resolve_into
resolve (single Scalar per slot) and resolve_axes (a Vec<Scalar> axis per slot) were the same two-phase named-binding resolution against param_space(), duplicated almost verbatim — the live instance of the "two raise-sites for one invariant" hazard (cf. #45/#53). A change to binding precedence or error ordering had to touch both in lockstep. Collapse the shared skeleton into resolve_into<T>, parameterized by two closures: claim_ok (phase-1 claim-time gate, axis-only EmptyAxis, before the duplicate check) and kind_ok (phase-2 per-slot kind gate). The error total order now lives in exactly one place. Behaviour-preserving: same error for same input, all 231 tests green (the resolve/bind precedence tests pin it).
This commit is contained in:
@@ -359,12 +359,29 @@ impl SweepBinder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve named sweep axes to a positional `Vec<Vec<Scalar>>` in `param_space()`
|
||||
/// slot order. (Body filled in Step 3.)
|
||||
fn resolve_axes(space: &[ParamSpec], axes: &[(String, Vec<Scalar>)]) -> Result<Vec<Vec<Scalar>>, BindError> {
|
||||
// Phase 1 — per axis in input order: claim slots by exact name (a, b, c, d).
|
||||
let mut claimed: Vec<Option<Vec<Scalar>>> = vec![None; space.len()];
|
||||
for (name, values) in axes {
|
||||
/// The shared two-phase named-binding resolution against `param_space()`, generic
|
||||
/// over the per-slot payload `T` (one `Scalar` for [`resolve`], a `Vec<Scalar>`
|
||||
/// axis for [`resolve_axes`]). The two callers differ only in the per-element work
|
||||
/// at two pinned points; the error TOTAL ORDER lives here, once, so it cannot drift
|
||||
/// between the scalar and axis paths (the #45/#53 "two raise-sites for one
|
||||
/// invariant" hazard).
|
||||
///
|
||||
/// `claim_ok` runs inside the phase-1 `[idx]` arm BEFORE the duplicate check (an
|
||||
/// axis rejects an empty value list there); `kind_ok` is the phase-2 per-slot kind
|
||||
/// gate (an axis loops every element, first offender in axis order winning). Both
|
||||
/// return `Some(err)` to abort with that error, `None` to continue. The order of
|
||||
/// arms — phase 1 per input `UnknownKnob → claim_ok → DuplicateBinding`, then phase
|
||||
/// 2 per slot `MissingKnob → kind_ok` — is the contract pinned by the resolve/bind
|
||||
/// tests; do not reorder.
|
||||
fn resolve_into<T: Clone>(
|
||||
space: &[ParamSpec],
|
||||
bindings: &[(String, T)],
|
||||
claim_ok: impl Fn(&str, &T) -> Option<BindError>,
|
||||
kind_ok: impl Fn(&ParamSpec, &T) -> Option<BindError>,
|
||||
) -> Result<Vec<T>, BindError> {
|
||||
// Phase 1 — per binding in input order: claim slots by exact name.
|
||||
let mut claimed: Vec<Option<T>> = vec![None; space.len()];
|
||||
for (name, value) in bindings {
|
||||
let matches: Vec<usize> = space
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -374,39 +391,55 @@ fn resolve_axes(space: &[ParamSpec], axes: &[(String, Vec<Scalar>)]) -> Result<V
|
||||
match matches.as_slice() {
|
||||
[] => return Err(BindError::UnknownKnob(name.clone())), // a
|
||||
[idx] => {
|
||||
if values.is_empty() {
|
||||
return Err(BindError::EmptyAxis(name.clone())); // c
|
||||
if let Some(err) = claim_ok(name, value) {
|
||||
return Err(err); // c (axis-only: EmptyAxis), before duplicate
|
||||
}
|
||||
if claimed[*idx].is_some() {
|
||||
return Err(BindError::DuplicateBinding(name.clone())); // d
|
||||
}
|
||||
claimed[*idx] = Some(values.clone());
|
||||
claimed[*idx] = Some(value.clone());
|
||||
}
|
||||
_ => unreachable!("param_space() is injective — checked before resolve"),
|
||||
}
|
||||
}
|
||||
// Phase 2 — slot walk in param_space() order: completeness (e) + per-element kind (f).
|
||||
// Phase 2 — slot walk in param_space() order: completeness (e) + kind (f).
|
||||
let mut ordered = Vec::with_capacity(space.len());
|
||||
for (i, p) in space.iter().enumerate() {
|
||||
match &claimed[i] {
|
||||
None => return Err(BindError::MissingKnob(p.name.clone())), // e
|
||||
Some(values) => {
|
||||
for v in values {
|
||||
if v.kind() != p.kind {
|
||||
return Err(BindError::KindMismatch {
|
||||
knob: p.name.clone(),
|
||||
expected: p.kind,
|
||||
got: v.kind(),
|
||||
}); // f — first offending element in axis order
|
||||
}
|
||||
Some(value) => {
|
||||
if let Some(err) = kind_ok(p, value) {
|
||||
return Err(err); // f
|
||||
}
|
||||
ordered.push(values.clone());
|
||||
ordered.push(value.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ordered)
|
||||
}
|
||||
|
||||
/// The phase-2 kind gate for a single scalar: mismatch against the slot kind.
|
||||
fn scalar_kind_err(p: &ParamSpec, v: Scalar) -> Option<BindError> {
|
||||
(v.kind() != p.kind).then(|| BindError::KindMismatch {
|
||||
knob: p.name.clone(),
|
||||
expected: p.kind,
|
||||
got: v.kind(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Resolve named sweep axes to a positional `Vec<Vec<Scalar>>` in `param_space()`
|
||||
/// slot order. Thin caller over [`resolve_into`]: an axis rejects an empty value
|
||||
/// list at claim time (`EmptyAxis`, before the duplicate check) and kind-checks
|
||||
/// every element (first offender in axis order).
|
||||
fn resolve_axes(space: &[ParamSpec], axes: &[(String, Vec<Scalar>)]) -> Result<Vec<Vec<Scalar>>, BindError> {
|
||||
resolve_into(
|
||||
space,
|
||||
axes,
|
||||
|name, values| values.is_empty().then(|| BindError::EmptyAxis(name.to_string())),
|
||||
|p, values| values.iter().find_map(|v| scalar_kind_err(p, *v)),
|
||||
)
|
||||
}
|
||||
|
||||
/// Structural validation (param-value-independent): the `param_space()` name
|
||||
/// projection is the by-name knob address space (C12/C19) and must be injective —
|
||||
/// a duplicated path is a knob no binding can select alone. The first duplicate in
|
||||
@@ -424,46 +457,15 @@ fn check_param_namespace_injective(space: &[ParamSpec]) -> Result<(), CompileErr
|
||||
}
|
||||
|
||||
/// Resolve named bindings to a positional `Vec<Scalar>` in `param_space()` slot
|
||||
/// order. (Body filled in Step 3.)
|
||||
/// order. Thin caller over [`resolve_into`]: a single scalar has no claim-time
|
||||
/// rejection and kind-checks the one value.
|
||||
fn resolve(space: &[ParamSpec], bound: &[(String, Scalar)]) -> Result<Vec<Scalar>, BindError> {
|
||||
// Phase 1 — per binding in input order: claim slots by exact name (a, b, d).
|
||||
let mut claimed: Vec<Option<Scalar>> = vec![None; space.len()];
|
||||
for (name, value) in bound {
|
||||
let matches: Vec<usize> = space
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, p)| p.name == *name)
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
match matches.as_slice() {
|
||||
[] => return Err(BindError::UnknownKnob(name.clone())), // a
|
||||
[idx] => {
|
||||
if claimed[*idx].is_some() {
|
||||
return Err(BindError::DuplicateBinding(name.clone())); // d
|
||||
}
|
||||
claimed[*idx] = Some(*value);
|
||||
}
|
||||
_ => unreachable!("param_space() is injective — checked before resolve"),
|
||||
}
|
||||
}
|
||||
// Phase 2 — slot walk in param_space() order (e, f).
|
||||
let mut point = Vec::with_capacity(space.len());
|
||||
for (i, p) in space.iter().enumerate() {
|
||||
match claimed[i] {
|
||||
None => return Err(BindError::MissingKnob(p.name.clone())), // e
|
||||
Some(v) => {
|
||||
if v.kind() != p.kind {
|
||||
return Err(BindError::KindMismatch {
|
||||
knob: p.name.clone(),
|
||||
expected: p.kind,
|
||||
got: v.kind(),
|
||||
}); // f
|
||||
}
|
||||
point.push(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(point)
|
||||
resolve_into(
|
||||
space,
|
||||
bound,
|
||||
|_name, _value| None,
|
||||
|p, value| scalar_kind_err(p, *value),
|
||||
)
|
||||
}
|
||||
|
||||
/// A construction-phase fault, caught before the flat graph reaches
|
||||
|
||||
Reference in New Issue
Block a user