feat(0088): §A shared gate predicates — edge_kind_check, resolution helpers, try_bind
Iteration-1 §A of the construction service (#157): the surface-agnostic shared predicates the eager op-script path and the holistic finalize gates both call — no second validator (C24). - edge_kind_check (blueprint.rs): the per-edge producer/consumer kind check, lifted verbatim out of validate_wiring's edge loop into a pub(crate) fn that validate_wiring now calls — behaviour-preserving (same Bootstrap(KindMismatch) variant; the existing compile-time gate test stays green). - resolve_input_slot / resolve_output_field (builder.rs): the exactly-one-match name→index resolution extracted as pub(crate) fns over (&NodeSchema, &str), so the runtime-String op-script names share GraphBuilder's resolution; GraphBuilder delegates and maps to the unchanged BuildError variants. - try_bind + BindOpError (aura-core node.rs): the fallible Result twin of bind (bind keeps its panic contract and pinned messages verbatim; try_bind is a separate method reporting the same three conditions as values). - check_param_namespace_injective + validate_wiring widened to pub(crate) for the finalize-stage reuse by the upcoming GraphSession::finish (§B). Partial iteration: §B (the GraphSession/Op/replay surface + introspection) and the §A→§B integration land next (plan Tasks 4-8). compile_with_params / check_ports_connected are behaviourally unchanged. Full suite green; clippy clean. Plan correction folded in: Task 3's test originally referenced aura_std::Sma — infeasible inside aura-core (aura-std depends on aura-core; the reverse edge is a dependency cycle). Adapted to a local PrimitiveBuilder probe with identical assertions. refs #157
This commit is contained in:
@@ -43,7 +43,7 @@ pub use column::{Column, Window};
|
||||
pub use ctx::Ctx;
|
||||
pub use error::KindMismatch;
|
||||
pub use node::{
|
||||
zip_params, BoundParam, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec,
|
||||
zip_params, BindOpError, BoundParam, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec,
|
||||
PrimitiveBuilder,
|
||||
};
|
||||
pub use scalar::{Scalar, ScalarKind, Timestamp};
|
||||
|
||||
@@ -235,6 +235,65 @@ impl PrimitiveBuilder {
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// The fallible twin of [`bind`](Self::bind): same exactly-one-match + kind
|
||||
/// rule, returning [`BindOpError`] instead of panicking. Used by the op-script
|
||||
/// construction surface, where a bad param is rejected at the op, not a panic.
|
||||
pub fn try_bind(mut self, slot: &str, value: Scalar) -> Result<Self, BindOpError> {
|
||||
let matches: Vec<usize> = self
|
||||
.schema
|
||||
.params
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, p)| p.name == slot)
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
let pos = match matches.as_slice() {
|
||||
[pos] => *pos,
|
||||
[] => return Err(BindOpError::UnknownParam(slot.to_string())),
|
||||
_ => return Err(BindOpError::AmbiguousParam(slot.to_string())),
|
||||
};
|
||||
if value.kind() != self.schema.params[pos].kind {
|
||||
return Err(BindOpError::KindMismatch {
|
||||
param: slot.to_string(),
|
||||
expected: self.schema.params[pos].kind,
|
||||
got: value.kind(),
|
||||
});
|
||||
}
|
||||
let name = self.schema.params[pos].name.clone();
|
||||
let kind = self.schema.params[pos].kind;
|
||||
let mut orig = pos;
|
||||
let mut prior: Vec<usize> = self.bound.iter().map(|b| b.pos).collect();
|
||||
prior.sort_unstable();
|
||||
for p in prior {
|
||||
if p <= orig {
|
||||
orig += 1;
|
||||
}
|
||||
}
|
||||
self.bound.push(BoundParam { pos: orig, name, kind, value });
|
||||
self.schema.params.remove(pos);
|
||||
let inner = self.build;
|
||||
self.build = Box::new(move |open: &[Cell]| {
|
||||
let mut full = open.to_vec();
|
||||
full.insert(pos, value.cell());
|
||||
inner(&full)
|
||||
});
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// A fallible-bind fault — the `Result` twin of `bind`'s panics, for the
|
||||
/// data-level construction surface (`GraphSession`, #157). `bind` keeps its
|
||||
/// panic contract (and its pinned messages); `try_bind` reports the same three
|
||||
/// conditions as values.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum BindOpError {
|
||||
/// No still-open param has this name.
|
||||
UnknownParam(String),
|
||||
/// More than one still-open param has this name.
|
||||
AmbiguousParam(String),
|
||||
/// The value's kind does not equal the param's declared kind.
|
||||
KindMismatch { param: String, expected: ScalarKind, got: ScalarKind },
|
||||
}
|
||||
|
||||
/// A node's declared interface: its inputs (in order) and its output record — an
|
||||
@@ -550,6 +609,41 @@ mod tests {
|
||||
);
|
||||
let _ = b.bind("length", Scalar::f64(2.0)); // F64 value for an I64 slot
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_bind_ok_and_typed_errors() {
|
||||
use super::BindOpError;
|
||||
// A probe with one i64 param `length` (a real std node like `Sma` would
|
||||
// pull in aura-std, but aura-core cannot dev-depend on it without a
|
||||
// dev-cycle that compiles aura-core twice — incompatible `Scalar` types).
|
||||
let probe = || {
|
||||
PrimitiveBuilder::new(
|
||||
"Probe",
|
||||
NodeSchema {
|
||||
inputs: vec![],
|
||||
output: vec![],
|
||||
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
|
||||
},
|
||||
|_| Box::new(Bare),
|
||||
)
|
||||
};
|
||||
// ok: exact name, matching kind
|
||||
assert!(probe().try_bind("length", Scalar::i64(3)).is_ok());
|
||||
// unknown param name
|
||||
assert_eq!(
|
||||
probe().try_bind("nope", Scalar::i64(3)).err(),
|
||||
Some(BindOpError::UnknownParam("nope".into()))
|
||||
);
|
||||
// kind mismatch (f64 into an i64 param)
|
||||
assert_eq!(
|
||||
probe().try_bind("length", Scalar::f64(3.0)).err(),
|
||||
Some(BindOpError::KindMismatch {
|
||||
param: "length".into(),
|
||||
expected: ScalarKind::I64,
|
||||
got: ScalarKind::F64,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user