feat(engine): compile resolves and hoists declared taps (#282)
The compile core. resolve_tap_wire resolves a blueprint Tap's interior
{node, field} through the lowering table exactly as OutField re-exports and
edges resolve — a leaf to its remapped index (output-arity checked), a nested
composite through its output remap. Taps are terminal (not re-exported through
the boundary): a flat_taps accumulator threads through the lowering recursion,
so an interior composite's taps hoist to the root FlatGraph.taps with remapped
wires — the case the CLI single-run wrapper relies on. validate_wiring gains a
tap leg (threaded at all three call sites incl. GraphSession::finish) and
CompileError::TapWireOutOfRange names a bad wire. Root-resolve, out-of-arity,
and nested-hoist are pinned green.
refs #282
This commit is contained in:
@@ -18,7 +18,7 @@ use aura_core::{
|
|||||||
Cell, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder, Scalar, ScalarKind,
|
Cell, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder, Scalar, ScalarKind,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::harness::{BootstrapError, Edge, FlatGraph, Harness, SourceSpec, Target};
|
use crate::harness::{BootstrapError, Edge, FlatGraph, FlatTap, Harness, SourceSpec, Target};
|
||||||
use crate::sweep::sweep;
|
use crate::sweep::sweep;
|
||||||
use crate::{GridSpace, ParamRange, RandomSpace, RunReport, SweepFamily};
|
use crate::{GridSpace, ParamRange, RandomSpace, RunReport, SweepFamily};
|
||||||
|
|
||||||
@@ -326,7 +326,7 @@ impl Composite {
|
|||||||
// structural validation, all pre-build (no node constructed):
|
// structural validation, all pre-build (no node constructed):
|
||||||
let space = self.param_space();
|
let space = self.param_space();
|
||||||
check_param_namespace_injective(&space)?;
|
check_param_namespace_injective(&space)?;
|
||||||
validate_wiring(&self.nodes, &self.edges, &self.input_roles, &self.output)?;
|
validate_wiring(&self.nodes, &self.edges, &self.input_roles, &self.output, &self.taps)?;
|
||||||
check_root_roles_bound(&self.input_roles)?;
|
check_root_roles_bound(&self.input_roles)?;
|
||||||
|
|
||||||
if point.len() != space.len() {
|
if point.len() != space.len() {
|
||||||
@@ -345,6 +345,7 @@ impl Composite {
|
|||||||
let mut flat_nodes: Vec<Box<dyn Node>> = Vec::new();
|
let mut flat_nodes: Vec<Box<dyn Node>> = Vec::new();
|
||||||
let mut flat_signatures: Vec<NodeSchema> = Vec::new();
|
let mut flat_signatures: Vec<NodeSchema> = Vec::new();
|
||||||
let mut flat_edges: Vec<Edge> = Vec::new();
|
let mut flat_edges: Vec<Edge> = Vec::new();
|
||||||
|
let mut flat_taps: Vec<FlatTap> = Vec::new();
|
||||||
let mut cursor = 0usize;
|
let mut cursor = 0usize;
|
||||||
|
|
||||||
let lowerings = lower_items(
|
let lowerings = lower_items(
|
||||||
@@ -354,6 +355,7 @@ impl Composite {
|
|||||||
&mut flat_nodes,
|
&mut flat_nodes,
|
||||||
&mut flat_signatures,
|
&mut flat_signatures,
|
||||||
&mut flat_edges,
|
&mut flat_edges,
|
||||||
|
&mut flat_taps,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
for e in &self.edges {
|
for e in &self.edges {
|
||||||
@@ -373,12 +375,20 @@ impl Composite {
|
|||||||
flat_sources.push(SourceSpec::named(role.name.clone(), kind, targets));
|
flat_sources.push(SourceSpec::named(role.name.clone(), kind, targets));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the root's own declared taps resolve through the root `lowerings`, same
|
||||||
|
// as any nested composite's — the root frame IS a composite frame, just
|
||||||
|
// the outermost one, so it hoists into the same accumulator.
|
||||||
|
for tap in &self.taps {
|
||||||
|
let (node, field) = resolve_tap_wire(&tap.from, &tap.name, &lowerings, &flat_signatures)?;
|
||||||
|
flat_taps.push(FlatTap { name: tap.name.clone(), node, field });
|
||||||
|
}
|
||||||
|
|
||||||
Ok(FlatGraph {
|
Ok(FlatGraph {
|
||||||
nodes: flat_nodes,
|
nodes: flat_nodes,
|
||||||
signatures: flat_signatures,
|
signatures: flat_signatures,
|
||||||
sources: flat_sources,
|
sources: flat_sources,
|
||||||
edges: flat_edges,
|
edges: flat_edges,
|
||||||
taps: Vec::new(),
|
taps: flat_taps,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -853,6 +863,9 @@ pub enum CompileError {
|
|||||||
DoubleWiredPort { node: usize, slot: usize },
|
DoubleWiredPort { node: usize, slot: usize },
|
||||||
/// A gang table failed structural validation at a minting boundary.
|
/// A gang table failed structural validation at a minting boundary.
|
||||||
BadGang(GangFault),
|
BadGang(GangFault),
|
||||||
|
/// A declared tap's wire references a non-existent interior node, or a field
|
||||||
|
/// beyond that producer's output arity.
|
||||||
|
TapWireOutOfRange { tap: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The typed detail of a `CompileError::BadGang` (one arm per `check_gangs`
|
/// The typed detail of a `CompileError::BadGang` (one arm per `check_gangs`
|
||||||
@@ -901,6 +914,7 @@ pub(crate) fn validate_wiring(
|
|||||||
edges: &[Edge],
|
edges: &[Edge],
|
||||||
roles: &[Role],
|
roles: &[Role],
|
||||||
output: &[OutField],
|
output: &[OutField],
|
||||||
|
taps: &[Tap],
|
||||||
) -> Result<(), CompileError> {
|
) -> Result<(), CompileError> {
|
||||||
// edges: index-range + producer/consumer kind match. The kind-mismatch variant
|
// edges: index-range + producer/consumer kind match. The kind-mismatch variant
|
||||||
// is the SAME one bootstrap returns today (Bootstrap(KindMismatch)), just raised
|
// is the SAME one bootstrap returns today (Bootstrap(KindMismatch)), just raised
|
||||||
@@ -932,12 +946,20 @@ pub(crate) fn validate_wiring(
|
|||||||
return Err(CompileError::OutputPortOutOfRange);
|
return Err(CompileError::OutputPortOutOfRange);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// taps: each declared tap's node index in range (structural leg only — deep
|
||||||
|
// field-arity is validated at resolve time by `resolve_tap_wire`, mirroring
|
||||||
|
// how the output leg above pins the node index).
|
||||||
|
for tap in taps {
|
||||||
|
if tap.from.node >= nodes.len() {
|
||||||
|
return Err(CompileError::TapWireOutOfRange { tap: tap.name.clone() });
|
||||||
|
}
|
||||||
|
}
|
||||||
// wiring totality: every interior input slot covered by exactly one wiring act
|
// wiring totality: every interior input slot covered by exactly one wiring act
|
||||||
check_ports_connected(nodes, edges, roles)?;
|
check_ports_connected(nodes, edges, roles)?;
|
||||||
// recurse into nested composites
|
// recurse into nested composites
|
||||||
for item in nodes {
|
for item in nodes {
|
||||||
if let BlueprintNode::Composite(c) = item {
|
if let BlueprintNode::Composite(c) = item {
|
||||||
validate_wiring(c.nodes(), c.edges(), c.input_roles(), c.output())?;
|
validate_wiring(c.nodes(), c.edges(), c.input_roles(), c.output(), c.taps())?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1158,6 +1180,7 @@ fn lower_items(
|
|||||||
flat_nodes: &mut Vec<Box<dyn Node>>,
|
flat_nodes: &mut Vec<Box<dyn Node>>,
|
||||||
flat_signatures: &mut Vec<NodeSchema>,
|
flat_signatures: &mut Vec<NodeSchema>,
|
||||||
flat_edges: &mut Vec<Edge>,
|
flat_edges: &mut Vec<Edge>,
|
||||||
|
flat_taps: &mut Vec<FlatTap>,
|
||||||
) -> Result<Vec<ItemLowering>, CompileError> {
|
) -> Result<Vec<ItemLowering>, CompileError> {
|
||||||
let mut lowerings = Vec::with_capacity(items.len());
|
let mut lowerings = Vec::with_capacity(items.len());
|
||||||
for item in items {
|
for item in items {
|
||||||
@@ -1181,6 +1204,7 @@ fn lower_items(
|
|||||||
flat_nodes,
|
flat_nodes,
|
||||||
flat_signatures,
|
flat_signatures,
|
||||||
flat_edges,
|
flat_edges,
|
||||||
|
flat_taps,
|
||||||
)?);
|
)?);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1197,6 +1221,7 @@ fn inline_composite(
|
|||||||
flat_nodes: &mut Vec<Box<dyn Node>>,
|
flat_nodes: &mut Vec<Box<dyn Node>>,
|
||||||
flat_signatures: &mut Vec<NodeSchema>,
|
flat_signatures: &mut Vec<NodeSchema>,
|
||||||
flat_edges: &mut Vec<Edge>,
|
flat_edges: &mut Vec<Edge>,
|
||||||
|
flat_taps: &mut Vec<FlatTap>,
|
||||||
) -> Result<ItemLowering, CompileError> {
|
) -> Result<ItemLowering, CompileError> {
|
||||||
// `name` is the non-load-bearing render symbol (#13); it dissolves at inline
|
// `name` is the non-load-bearing render symbol (#13); it dissolves at inline
|
||||||
// (C23 — the boundary does not reach the flat graph), so it is not destructured.
|
// (C23 — the boundary does not reach the flat graph), so it is not destructured.
|
||||||
@@ -1207,11 +1232,12 @@ fn inline_composite(
|
|||||||
// `gangs` is an authoring-time gate only (checked at `with_gangs`); it has
|
// `gangs` is an authoring-time gate only (checked at `with_gangs`); it has
|
||||||
// no runtime representation in the flat graph, same as `name`. `doc` is
|
// no runtime representation in the flat graph, same as `name`. `doc` is
|
||||||
// the prose twin of `name` (#125) and dissolves alongside it.
|
// the prose twin of `name` (#125) and dissolves alongside it.
|
||||||
let Composite { name: _, doc: _, nodes, edges, input_roles, output, taps: _, gangs: _ } = c;
|
let Composite { name: _, doc: _, nodes, edges, input_roles, output, taps, gangs: _ } = c;
|
||||||
let item_count = nodes.len();
|
let item_count = nodes.len();
|
||||||
|
|
||||||
// recursively lower interior items, then rewrite interior edges through them
|
// recursively lower interior items, then rewrite interior edges through them
|
||||||
let interior = lower_items(nodes, point, cursor, flat_nodes, flat_signatures, flat_edges)?;
|
let interior = lower_items(nodes, point, cursor, flat_nodes, flat_signatures, flat_edges, flat_taps)?;
|
||||||
|
|
||||||
for e in &edges {
|
for e in &edges {
|
||||||
for fe in rewrite_edge(e, &interior, flat_signatures)? {
|
for fe in rewrite_edge(e, &interior, flat_signatures)? {
|
||||||
flat_edges.push(fe);
|
flat_edges.push(fe);
|
||||||
@@ -1257,6 +1283,15 @@ fn inline_composite(
|
|||||||
roles.push(flat_targets);
|
roles.push(flat_targets);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this composite's own declared taps hoist into the flat, bottom-up
|
||||||
|
// accumulator now — a tap is terminal (no re-export through the boundary,
|
||||||
|
// unlike `output`), so it resolves through THIS level's freshly-lowered
|
||||||
|
// `interior` and is pushed once, never touched again by an enclosing level.
|
||||||
|
for tap in &taps {
|
||||||
|
let (node, field) = resolve_tap_wire(&tap.from, &tap.name, &interior, flat_signatures)?;
|
||||||
|
flat_taps.push(FlatTap { name: tap.name.clone(), node, field });
|
||||||
|
}
|
||||||
|
|
||||||
Ok(ItemLowering::Composite { output: out, roles })
|
Ok(ItemLowering::Composite { output: out, roles })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1305,6 +1340,35 @@ fn resolve_target(t: &Target, lowerings: &[ItemLowering]) -> Result<Vec<Target>,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve a declared tap's interior `{node, field}` into a flat producer
|
||||||
|
/// `(node, field)` through the lowering table — a Leaf resolves to its remapped
|
||||||
|
/// index with an output-arity check; a nested Composite resolves through its
|
||||||
|
/// `output` re-export remap. Mirrors the `OutField` resolution loop in
|
||||||
|
/// `inline_composite`. Taps do NOT re-export through the boundary (a tap is
|
||||||
|
/// terminal) — this is used both for a composite's own taps (resolved through its
|
||||||
|
/// freshly-lowered interior) and for the root's own taps (resolved through the
|
||||||
|
/// root `lowerings`).
|
||||||
|
fn resolve_tap_wire(
|
||||||
|
w: &TapWire,
|
||||||
|
name: &str,
|
||||||
|
lowerings: &[ItemLowering],
|
||||||
|
flat_signatures: &[NodeSchema],
|
||||||
|
) -> Result<(usize, usize), CompileError> {
|
||||||
|
let err = || CompileError::TapWireOutOfRange { tap: name.to_string() };
|
||||||
|
if w.node >= lowerings.len() {
|
||||||
|
return Err(err());
|
||||||
|
}
|
||||||
|
match &lowerings[w.node] {
|
||||||
|
ItemLowering::Leaf { index } => {
|
||||||
|
if w.field >= flat_signatures[*index].output.len() {
|
||||||
|
return Err(err());
|
||||||
|
}
|
||||||
|
Ok((*index, w.field))
|
||||||
|
}
|
||||||
|
ItemLowering::Composite { output, .. } => output.get(w.field).copied().ok_or_else(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The declared scalar kind of a flat node's input slot (for role kind-checking).
|
/// The declared scalar kind of a flat node's input slot (for role kind-checking).
|
||||||
fn slot_kind(t: Target, flat_signatures: &[NodeSchema]) -> Result<ScalarKind, CompileError> {
|
fn slot_kind(t: Target, flat_signatures: &[NodeSchema]) -> Result<ScalarKind, CompileError> {
|
||||||
flat_signatures[t.node]
|
flat_signatures[t.node]
|
||||||
@@ -3624,4 +3688,77 @@ mod tests {
|
|||||||
.with_taps(vec![Tap { name: "p_long".into(), from: TapWire { node: 3, field: 0 } }]);
|
.with_taps(vec![Tap { name: "p_long".into(), from: TapWire { node: 3, field: 0 } }]);
|
||||||
assert_eq!(tapped.taps(), &[Tap { name: "p_long".into(), from: TapWire { node: 3, field: 0 } }]);
|
assert_eq!(tapped.taps(), &[Tap { name: "p_long".into(), from: TapWire { node: 3, field: 0 } }]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A tap on a root leaf producer resolves to that producer's flat (node, field) —
|
||||||
|
/// the property `compile` must give a declared tap a load-bearing flat identity.
|
||||||
|
#[test]
|
||||||
|
fn compile_resolves_a_root_tap_to_flat_indices() {
|
||||||
|
let bp = Composite::new(
|
||||||
|
"m",
|
||||||
|
vec![Sub::builder().into()],
|
||||||
|
vec![],
|
||||||
|
vec![Role {
|
||||||
|
name: "a".into(),
|
||||||
|
targets: vec![Target { node: 0, slot: 0 }, Target { node: 0, slot: 1 }],
|
||||||
|
source: Some(ScalarKind::F64),
|
||||||
|
}],
|
||||||
|
vec![],
|
||||||
|
)
|
||||||
|
.with_taps(vec![Tap { name: "d".into(), from: TapWire { node: 0, field: 0 } }]);
|
||||||
|
let flat = bp.compile_with_params(&[]).expect("compiles");
|
||||||
|
assert_eq!(flat.taps, vec![crate::harness::FlatTap { name: "d".into(), node: 0, field: 0 }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A tap wire naming a field beyond its producer's output arity is rejected —
|
||||||
|
/// `compile` must not silently resolve a tap into a bogus flat index.
|
||||||
|
#[test]
|
||||||
|
fn compile_rejects_a_tap_wire_out_of_output_arity() {
|
||||||
|
let bp = Composite::new(
|
||||||
|
"m",
|
||||||
|
vec![Sub::builder().into()],
|
||||||
|
vec![],
|
||||||
|
vec![Role {
|
||||||
|
name: "a".into(),
|
||||||
|
targets: vec![Target { node: 0, slot: 0 }, Target { node: 0, slot: 1 }],
|
||||||
|
source: Some(ScalarKind::F64),
|
||||||
|
}],
|
||||||
|
vec![],
|
||||||
|
)
|
||||||
|
.with_taps(vec![Tap { name: "bad".into(), from: TapWire { node: 0, field: 9 } }]); // Sub has 1 output field
|
||||||
|
assert!(matches!(bp.compile_with_params(&[]), Err(CompileError::TapWireOutOfRange { .. })));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A tap declared on a NESTED composite hoists to the root `FlatGraph.taps`
|
||||||
|
/// with its wire remapped through the inline offset — the case the CLI
|
||||||
|
/// wrapper needs (a tap named deep in an authored blueprint must still surface
|
||||||
|
/// as a single flat, by-name-bindable measurement point at the root).
|
||||||
|
#[test]
|
||||||
|
fn compile_hoists_an_interior_composite_tap_to_the_root() {
|
||||||
|
let inner = Composite::new(
|
||||||
|
"inner",
|
||||||
|
vec![Sub::builder().into()],
|
||||||
|
vec![],
|
||||||
|
vec![Role {
|
||||||
|
name: "x".into(),
|
||||||
|
targets: vec![Target { node: 0, slot: 0 }, Target { node: 0, slot: 1 }],
|
||||||
|
source: None,
|
||||||
|
}],
|
||||||
|
vec![OutField { node: 0, field: 0, name: "o".into() }],
|
||||||
|
)
|
||||||
|
.with_taps(vec![Tap { name: "inner_d".into(), from: TapWire { node: 0, field: 0 } }]);
|
||||||
|
let root = Composite::new(
|
||||||
|
"root",
|
||||||
|
vec![BlueprintNode::Composite(inner)],
|
||||||
|
vec![],
|
||||||
|
vec![Role {
|
||||||
|
name: "a".into(),
|
||||||
|
targets: vec![Target { node: 0, slot: 0 }],
|
||||||
|
source: Some(ScalarKind::F64),
|
||||||
|
}],
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
let flat = root.compile_with_params(&[]).expect("compiles");
|
||||||
|
// the inner Sub lowered to flat node 0; the hoisted tap points at it
|
||||||
|
assert_eq!(flat.taps, vec![crate::harness::FlatTap { name: "inner_d".into(), node: 0, field: 0 }]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -399,7 +399,7 @@ impl<'v> GraphSession<'v> {
|
|||||||
.with_gangs(self.gangs)
|
.with_gangs(self.gangs)
|
||||||
.map_err(OpError::Incomplete)?;
|
.map_err(OpError::Incomplete)?;
|
||||||
check_param_namespace_injective(&c.param_space()).map_err(OpError::Incomplete)?;
|
check_param_namespace_injective(&c.param_space()).map_err(OpError::Incomplete)?;
|
||||||
validate_wiring(c.nodes(), c.edges(), c.input_roles(), c.output()).map_err(|e| match e {
|
validate_wiring(c.nodes(), c.edges(), c.input_roles(), c.output(), c.taps()).map_err(|e| match e {
|
||||||
CompileError::UnconnectedPort { node, slot } => OpError::UnconnectedPort {
|
CompileError::UnconnectedPort { node, slot } => OpError::UnconnectedPort {
|
||||||
node: self.ids[node].clone(),
|
node: self.ids[node].clone(),
|
||||||
slot: self.schemas[node].inputs[slot].name.clone(),
|
slot: self.schemas[node].inputs[slot].name.clone(),
|
||||||
|
|||||||
@@ -288,12 +288,10 @@ impl SyntheticSpec {
|
|||||||
|
|
||||||
/// A resolved measurement tap in the flat graph: a name paired with a flat
|
/// A resolved measurement tap in the flat graph: a name paired with a flat
|
||||||
/// producer `(node, output-field)`. The output-side twin of `SourceSpec` — its
|
/// producer `(node, output-field)`. The output-side twin of `SourceSpec` — its
|
||||||
/// `name` is meant to survive compile and be load-bearing for a future
|
/// `name` survives compile and is load-bearing for by-name binding (like
|
||||||
/// by-name binding step (like `SourceSpec.role`, #275), planned as `bind_tap`
|
/// `SourceSpec.role`, #275). `compile` resolves each blueprint `Tap` into a
|
||||||
/// in a later slice of #282. This slice only carries the type; `compile`
|
/// `FlatTap` through the same lowering remap that hoists edges/`OutField`
|
||||||
/// resolves no `Tap`s yet and always emits an empty `taps` list — the
|
/// re-exports (interior-composite taps hoist to the root list).
|
||||||
/// lowering remap that would populate it from a blueprint `Tap` lands with
|
|
||||||
/// `bind_tap`.
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct FlatTap {
|
pub struct FlatTap {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ pub use builder::{BuildError, GraphBuilder, InPort, NodeHandle, OutPort, RoleHan
|
|||||||
pub use construction::{replay, GraphSession, Op, OpError};
|
pub use construction::{replay, GraphSession, Op, OpError};
|
||||||
pub use graph_model::model_to_json;
|
pub use graph_model::model_to_json;
|
||||||
pub use harness::{
|
pub use harness::{
|
||||||
window_of, bind_sources, BootstrapError, Edge, FlatGraph, Harness, Source, SourceBindError,
|
window_of, bind_sources, BootstrapError, Edge, FlatGraph, FlatTap, Harness, Source, SourceBindError,
|
||||||
SourceSpec, SplitMix64, SyntheticSpec, Target, VecSource,
|
SourceSpec, SplitMix64, SyntheticSpec, Target, VecSource,
|
||||||
};
|
};
|
||||||
pub use report::{
|
pub use report::{
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
//! End-to-end coverage for the `FlatTap` / `FlatGraph.taps` plumbing (#282,
|
//! End-to-end coverage for the `FlatTap` / `FlatGraph.taps` plumbing (#282):
|
||||||
//! "thread all literal sites"): a `Composite` can declare a `Tap` (via
|
//! a `Composite` can declare a `Tap` (via `with_taps`), and `compile()` now
|
||||||
//! `with_taps`), but this slice adds only the flat-graph *carrier* — no
|
//! resolves it into the flat graph it hands to `Harness::bootstrap`. These
|
||||||
//! resolution step exists yet. These tests pin the two halves of that
|
//! tests pin the two halves of that contract at the public `aura_engine`
|
||||||
//! contract at the public `aura_engine` boundary: `compile()` never surfaces
|
//! boundary: `compile()` surfaces a declared `Tap` into `FlatGraph.taps`,
|
||||||
//! a declared `Tap` into the flat graph it hands to `Harness::bootstrap`
|
//! resolved to its flat producer `(node, field)`
|
||||||
//! (`compile_never_resolves_declared_taps_yet`), and the field's presence is
|
//! (`compile_resolves_a_declared_tap_at_the_public_boundary`), and doing so
|
||||||
//! otherwise perfectly transparent to compilation of the rest of the graph
|
//! is otherwise transparent to compilation of the rest of the graph — the
|
||||||
//! (`tapped_composite_compiles_identically_to_untapped_besides_the_taps_list`)
|
//! same node/edge structure, just an added taps entry
|
||||||
//! — a future resolving slice (`bind_tap`) is expected to flip the first of
|
//! (`tapped_composite_compiles_identically_to_untapped_besides_the_taps_list`).
|
||||||
//! these; until it does, a regression that started eagerly resolving taps,
|
|
||||||
//! or one that let a `Tap` perturb node/edge flattening, would be caught
|
|
||||||
//! here.
|
|
||||||
|
|
||||||
use aura_core::ScalarKind;
|
use aura_core::ScalarKind;
|
||||||
use aura_engine::{Composite, OutField, Role, Tap, TapWire, Target};
|
use aura_engine::{BlueprintNode, CompileError, Composite, FlatTap, OutField, Role, Tap, TapWire, Target};
|
||||||
use aura_std::Sub;
|
use aura_std::Sub;
|
||||||
|
|
||||||
/// A trivial one-node composite with a single declared output (`Sub`'s
|
/// A trivial one-node composite with a single declared output (`Sub`'s
|
||||||
@@ -35,28 +32,29 @@ fn leaf(taps: Vec<Tap>) -> Composite {
|
|||||||
.with_taps(taps)
|
.with_taps(taps)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Property: **`compile()` resolves no `Tap`s in this slice.** A composite
|
/// Property: **`compile()` resolves a declared `Tap` at the public
|
||||||
/// declaring a valid `Tap` (naming a real interior producer, `node: 0, field:
|
/// boundary.** A composite declaring a valid `Tap` (naming a real interior
|
||||||
/// 0`) still compiles to a `FlatGraph` whose `taps` list is empty — the
|
/// producer, `node: 0, field: 0`) compiles to a `FlatGraph` whose `taps` list
|
||||||
/// declaration is carried on the `Composite` only; nothing yet lowers it into
|
/// carries the resolved `FlatTap` — name preserved, wire resolved to the flat
|
||||||
/// the flat `FlatTap` the run loop could someday read. If a later change
|
/// producer index.
|
||||||
/// started eagerly resolving `Tap`s into `FlatGraph.taps` inside `compile()`
|
|
||||||
/// without an explicit opt-in (`bind_tap`), this test would flip green->red,
|
|
||||||
/// flagging the design-changing move for the ledger instead of it happening
|
|
||||||
/// silently.
|
|
||||||
#[test]
|
#[test]
|
||||||
fn compile_never_resolves_declared_taps_yet() {
|
fn compile_resolves_a_declared_tap_at_the_public_boundary() {
|
||||||
let tapped = leaf(vec![Tap { name: "spread".into(), from: TapWire { node: 0, field: 0 } }]);
|
let tapped = leaf(vec![Tap { name: "spread".into(), from: TapWire { node: 0, field: 0 } }]);
|
||||||
let flat = tapped.compile().expect("a tap-bearing composite still compiles");
|
let flat = tapped.compile().expect("a tap-bearing composite compiles");
|
||||||
assert!(flat.taps.is_empty(), "compile() must not resolve declared Taps in this slice: {:?}", flat.taps);
|
assert_eq!(
|
||||||
|
flat.taps,
|
||||||
|
vec![FlatTap { name: "spread".into(), node: 0, field: 0 }],
|
||||||
|
"compile() must resolve the declared Tap into FlatGraph.taps: {:?}",
|
||||||
|
flat.taps
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Property: **a declared `Tap` is otherwise inert at compile time** — it
|
/// Property: **a declared `Tap` is otherwise inert at compile time** — it
|
||||||
/// changes nothing about the flattened node/edge structure. Compiling the
|
/// changes nothing about the flattened node/edge structure. Compiling the
|
||||||
/// same composite with and without a `Tap` declaration must produce
|
/// same composite with and without a `Tap` declaration must produce
|
||||||
/// structurally identical flat graphs (same node count, same edges); the
|
/// structurally identical flat graphs (same node count, same edges); the
|
||||||
/// only difference tap-bearing scaffolding is *allowed* to make is the (here,
|
/// only difference tap-bearing scaffolding is allowed to make is the `taps`
|
||||||
/// still-empty) `taps` list itself.
|
/// list itself (empty vs. carrying the one resolved entry).
|
||||||
#[test]
|
#[test]
|
||||||
fn tapped_composite_compiles_identically_to_untapped_besides_the_taps_list() {
|
fn tapped_composite_compiles_identically_to_untapped_besides_the_taps_list() {
|
||||||
let untapped = leaf(vec![]).compile().expect("untapped compiles");
|
let untapped = leaf(vec![]).compile().expect("untapped compiles");
|
||||||
@@ -66,5 +64,73 @@ fn tapped_composite_compiles_identically_to_untapped_besides_the_taps_list() {
|
|||||||
|
|
||||||
assert_eq!(untapped.nodes.len(), tapped.nodes.len(), "tap declaration must not add/remove flat nodes");
|
assert_eq!(untapped.nodes.len(), tapped.nodes.len(), "tap declaration must not add/remove flat nodes");
|
||||||
assert_eq!(untapped.edges, tapped.edges, "tap declaration must not perturb flat edges");
|
assert_eq!(untapped.edges, tapped.edges, "tap declaration must not perturb flat edges");
|
||||||
assert_eq!(untapped.taps, tapped.taps, "both compile to the same (empty) taps list in this slice");
|
assert!(untapped.taps.is_empty(), "the untapped composite compiles to an empty taps list");
|
||||||
|
assert_eq!(
|
||||||
|
tapped.taps,
|
||||||
|
vec![FlatTap { name: "spread".into(), node: 0, field: 0 }],
|
||||||
|
"the tapped composite compiles to the one resolved entry"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Property: **the `validate_wiring` structural leg rejects a tap whose wire
|
||||||
|
/// names a node index beyond the composite's own node count**, before any
|
||||||
|
/// lowering or field-arity resolution ever runs — mirroring how the sibling
|
||||||
|
/// `output` leg raises `OutputPortOutOfRange` for the same shape of mistake.
|
||||||
|
/// This is the early, cheap structural gate (`tap.from.node >= nodes.len()`)
|
||||||
|
/// added alongside tap resolution in this iteration; without it, a
|
||||||
|
/// wildly-out-of-range tap wire would only be caught by luck at whichever
|
||||||
|
/// later stage happens to index into a lowering table, not deterministically
|
||||||
|
/// at compile's first validation pass.
|
||||||
|
#[test]
|
||||||
|
fn compile_rejects_a_tap_wire_node_out_of_range() {
|
||||||
|
let tapped = leaf(vec![Tap { name: "ghost".into(), from: TapWire { node: 1, field: 0 } }]);
|
||||||
|
match tapped.compile() {
|
||||||
|
Err(CompileError::TapWireOutOfRange { tap }) => assert_eq!(tap, "ghost"),
|
||||||
|
other => panic!("expected TapWireOutOfRange{{tap: \"ghost\"}}, got {:?}", other.map(|_| ())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Property: **a `Tap` declared on a nested composite hoists all the way to
|
||||||
|
/// the root `FlatGraph.taps`, with its wire remapped through the inline
|
||||||
|
/// offset** accumulated by every flat node lowered ahead of it — not just
|
||||||
|
/// carried through unresolved, and not resolved to its interior-local index.
|
||||||
|
/// The root here lowers a plain leaf node first (flat index 0) followed by
|
||||||
|
/// the tapped composite (whose interior `Sub` therefore lands at flat index
|
||||||
|
/// 1, not 0), so a remap that silently stayed at the interior-local index —
|
||||||
|
/// or hoisted at all — is caught by asserting the hoisted `FlatTap` points at
|
||||||
|
/// index 1. This is the exact shape the by-name tap-binding CLI wrapper
|
||||||
|
/// needs: a tap declared arbitrarily deep in an authored blueprint must
|
||||||
|
/// surface as one flat, addressable measurement point at the root.
|
||||||
|
#[test]
|
||||||
|
fn compile_hoists_a_nested_composite_tap_to_the_root_with_remapped_index() {
|
||||||
|
let inner = Composite::new(
|
||||||
|
"inner",
|
||||||
|
vec![Sub::builder().into()],
|
||||||
|
vec![],
|
||||||
|
vec![Role { name: "x".into(), targets: vec![Target { node: 0, slot: 0 }, Target { node: 0, slot: 1 }], source: None }],
|
||||||
|
vec![OutField { node: 0, field: 0, name: "o".into() }],
|
||||||
|
)
|
||||||
|
.with_taps(vec![Tap { name: "inner_d".into(), from: TapWire { node: 0, field: 0 } }]);
|
||||||
|
|
||||||
|
let root = Composite::new(
|
||||||
|
"root",
|
||||||
|
vec![Sub::builder().into(), BlueprintNode::Composite(inner)],
|
||||||
|
vec![],
|
||||||
|
vec![
|
||||||
|
Role {
|
||||||
|
name: "lead".into(),
|
||||||
|
targets: vec![Target { node: 0, slot: 0 }, Target { node: 0, slot: 1 }],
|
||||||
|
source: Some(ScalarKind::F64),
|
||||||
|
},
|
||||||
|
Role { name: "a".into(), targets: vec![Target { node: 1, slot: 0 }], source: Some(ScalarKind::F64) },
|
||||||
|
],
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
|
||||||
|
let flat = root.compile().expect("root with a nested tapped composite compiles");
|
||||||
|
assert_eq!(
|
||||||
|
flat.taps,
|
||||||
|
vec![FlatTap { name: "inner_d".into(), node: 1, field: 0 }],
|
||||||
|
"the inner tap must hoist to root FlatGraph.taps, remapped to the inline-offset flat node index"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user