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:
@@ -563,7 +563,7 @@ fn resolve_ranges(space: &[ParamSpec], ranges: &[(String, ParamRange)]) -> Resul
|
||||
/// `param_space()` order is reported (the order is deterministic). Single source of
|
||||
/// duplicate detection; called from `compile_with_params` and from both binders
|
||||
/// before name resolution.
|
||||
fn check_param_namespace_injective(space: &[ParamSpec]) -> Result<(), CompileError> {
|
||||
pub(crate) fn check_param_namespace_injective(space: &[ParamSpec]) -> Result<(), CompileError> {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for p in space {
|
||||
if !seen.insert(p.name.as_str()) {
|
||||
@@ -620,12 +620,34 @@ pub enum CompileError {
|
||||
DoubleWiredPort { node: usize, slot: usize },
|
||||
}
|
||||
|
||||
/// The per-edge kind predicate, shared by `validate_wiring` (holistic) and the
|
||||
/// eager `connect` op (`construction.rs`) — one check, two cadences (no second
|
||||
/// validator). Looks the producer field + consumer slot up by index and rejects
|
||||
/// a kind mismatch with the SAME variant bootstrap uses, so existing
|
||||
/// compiled-graph tests stay green.
|
||||
pub(crate) fn edge_kind_check(
|
||||
from: &NodeSchema,
|
||||
from_field: usize,
|
||||
to: &NodeSchema,
|
||||
slot: usize,
|
||||
) -> Result<(), CompileError> {
|
||||
let f = from.output.get(from_field).ok_or(CompileError::BadInteriorIndex)?;
|
||||
let s = to.inputs.get(slot).ok_or(CompileError::BadInteriorIndex)?;
|
||||
if f.kind != s.kind {
|
||||
return Err(CompileError::Bootstrap(BootstrapError::KindMismatch {
|
||||
producer: f.kind,
|
||||
consumer: s.kind,
|
||||
}));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Pre-build structural validation via `signature()` (no node constructed): every
|
||||
/// edge's producer field and consumer slot are in range and kind-matched; every
|
||||
/// output re-export and role target is in range and kind-consistent. Recurses into
|
||||
/// nested composites so the checks hold at every level. This is what lets `compile`
|
||||
/// reject a wiring fault before any build closure fires.
|
||||
fn validate_wiring(
|
||||
pub(crate) fn validate_wiring(
|
||||
nodes: &[BlueprintNode],
|
||||
edges: &[Edge],
|
||||
roles: &[Role],
|
||||
@@ -638,14 +660,7 @@ fn validate_wiring(
|
||||
for e in edges {
|
||||
let from = nodes.get(e.from).ok_or(CompileError::BadInteriorIndex)?.signature();
|
||||
let to = nodes.get(e.to).ok_or(CompileError::BadInteriorIndex)?.signature();
|
||||
let f = from.output.get(e.from_field).ok_or(CompileError::BadInteriorIndex)?;
|
||||
let s = to.inputs.get(e.slot).ok_or(CompileError::BadInteriorIndex)?;
|
||||
if f.kind != s.kind {
|
||||
return Err(CompileError::Bootstrap(BootstrapError::KindMismatch {
|
||||
producer: f.kind,
|
||||
consumer: s.kind,
|
||||
}));
|
||||
}
|
||||
edge_kind_check(&from, e.from_field, &to, e.slot)?;
|
||||
}
|
||||
// roles: every target in range, and all targets of one role share a kind
|
||||
// (RoleKindMismatch — the existing variant, today read off built schema()).
|
||||
@@ -926,6 +941,34 @@ mod tests {
|
||||
use aura_std::{Bias, Ema, Recorder, SimBroker, Sma, Sub};
|
||||
use std::sync::mpsc;
|
||||
|
||||
#[test]
|
||||
fn edge_kind_check_accepts_match_and_rejects_mismatch() {
|
||||
use super::edge_kind_check;
|
||||
use aura_core::{FieldSpec, NodeSchema, PortSpec, ScalarKind};
|
||||
// producer: one f64 output field; consumer: slot 0 is f64, slot 1 is bool.
|
||||
let from = NodeSchema {
|
||||
inputs: vec![],
|
||||
output: vec![FieldSpec { name: "v".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
};
|
||||
let to = NodeSchema {
|
||||
inputs: vec![
|
||||
PortSpec { kind: ScalarKind::F64, firing: aura_core::Firing::Any, name: "a".into() },
|
||||
PortSpec { kind: ScalarKind::Bool, firing: aura_core::Firing::Any, name: "b".into() },
|
||||
],
|
||||
output: vec![],
|
||||
params: vec![],
|
||||
};
|
||||
assert!(edge_kind_check(&from, 0, &to, 0).is_ok());
|
||||
assert_eq!(
|
||||
edge_kind_check(&from, 0, &to, 1),
|
||||
Err(CompileError::Bootstrap(BootstrapError::KindMismatch {
|
||||
producer: ScalarKind::F64,
|
||||
consumer: ScalarKind::Bool,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
/// Build + bootstrap + run + drain + summarize one swept point into a
|
||||
/// `RunReport`, using a fresh harness per point (disjoint runs, C1). A free
|
||||
/// `fn` (Copy + Sync) so it serves both as the `sweep`/binder closure and as a
|
||||
|
||||
@@ -162,17 +162,14 @@ impl GraphBuilder {
|
||||
.schemas
|
||||
.get(p.node)
|
||||
.ok_or(BuildError::BadHandle { node: p.node })?;
|
||||
let m: Vec<usize> = schema
|
||||
.inputs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, port)| port.name == p.name)
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
match m.as_slice() {
|
||||
[i] => Ok((p.node, *i)),
|
||||
[] => Err(BuildError::UnknownInPort { node: p.node, name: p.name.to_string() }),
|
||||
_ => Err(BuildError::AmbiguousInPort { node: p.node, name: p.name.to_string() }),
|
||||
match resolve_input_slot(schema, p.name) {
|
||||
Ok(i) => Ok((p.node, i)),
|
||||
Err(PortResolveError::Unknown) => {
|
||||
Err(BuildError::UnknownInPort { node: p.node, name: p.name.to_string() })
|
||||
}
|
||||
Err(PortResolveError::Ambiguous) => {
|
||||
Err(BuildError::AmbiguousInPort { node: p.node, name: p.name.to_string() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,21 +180,59 @@ impl GraphBuilder {
|
||||
.schemas
|
||||
.get(p.node)
|
||||
.ok_or(BuildError::BadHandle { node: p.node })?;
|
||||
let m: Vec<usize> = schema
|
||||
.output
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, f)| f.name == p.name)
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
match m.as_slice() {
|
||||
[i] => Ok(*i),
|
||||
[] => Err(BuildError::UnknownOutPort { node: p.node, name: p.name.to_string() }),
|
||||
_ => Err(BuildError::AmbiguousOutPort { node: p.node, name: p.name.to_string() }),
|
||||
match resolve_output_field(schema, p.name) {
|
||||
Ok(i) => Ok(i),
|
||||
Err(PortResolveError::Unknown) => {
|
||||
Err(BuildError::UnknownOutPort { node: p.node, name: p.name.to_string() })
|
||||
}
|
||||
Err(PortResolveError::Ambiguous) => {
|
||||
Err(BuildError::AmbiguousOutPort { node: p.node, name: p.name.to_string() })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A name→index resolution outcome, shared by `GraphBuilder` (mapped to
|
||||
/// `BuildError`) and `GraphSession` (mapped to `OpError`). Over `&str`, so it
|
||||
/// serves both `&'static str` literals and runtime document names.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum PortResolveError {
|
||||
Unknown,
|
||||
Ambiguous,
|
||||
}
|
||||
|
||||
/// Resolve an input-port name to its slot index by exactly-one-match.
|
||||
pub(crate) fn resolve_input_slot(schema: &NodeSchema, name: &str) -> Result<usize, PortResolveError> {
|
||||
let m: Vec<usize> = schema
|
||||
.inputs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, port)| port.name == name)
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
match m.as_slice() {
|
||||
[i] => Ok(*i),
|
||||
[] => Err(PortResolveError::Unknown),
|
||||
_ => Err(PortResolveError::Ambiguous),
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve an output-field name to its field index by exactly-one-match.
|
||||
pub(crate) fn resolve_output_field(schema: &NodeSchema, name: &str) -> Result<usize, PortResolveError> {
|
||||
let m: Vec<usize> = schema
|
||||
.output
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, f)| f.name == name)
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
match m.as_slice() {
|
||||
[i] => Ok(*i),
|
||||
[] => Err(PortResolveError::Unknown),
|
||||
_ => Err(PortResolveError::Ambiguous),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{BuildError, GraphBuilder};
|
||||
@@ -297,6 +332,19 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_port_resolution_matches_exactly_one() {
|
||||
use super::{resolve_input_slot, resolve_output_field, PortResolveError};
|
||||
use crate::BlueprintNode;
|
||||
use aura_std::Sub;
|
||||
let schema = BlueprintNode::from(Sub::builder()).signature(); // lhs, rhs -> value
|
||||
assert_eq!(resolve_input_slot(&schema, "lhs"), Ok(0));
|
||||
assert_eq!(resolve_input_slot(&schema, "rhs"), Ok(1));
|
||||
assert_eq!(resolve_input_slot(&schema, "nope"), Err(PortResolveError::Unknown));
|
||||
assert_eq!(resolve_output_field(&schema, "value"), Ok(0));
|
||||
assert_eq!(resolve_output_field(&schema, "nope"), Err(PortResolveError::Unknown));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_in_port_is_reported_at_build() {
|
||||
let mut g = GraphBuilder::new("x");
|
||||
|
||||
Reference in New Issue
Block a user