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:
2026-06-29 18:38:34 +02:00
parent a5b887a955
commit ea1ca32dbd
5 changed files with 241 additions and 37 deletions
+1 -1
View File
@@ -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};
+94
View File
@@ -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)]