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,
|
||||
};
|
||||
|
||||
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::{GridSpace, ParamRange, RandomSpace, RunReport, SweepFamily};
|
||||
|
||||
@@ -326,7 +326,7 @@ impl Composite {
|
||||
// structural validation, all pre-build (no node constructed):
|
||||
let space = self.param_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)?;
|
||||
|
||||
if point.len() != space.len() {
|
||||
@@ -345,6 +345,7 @@ impl Composite {
|
||||
let mut flat_nodes: Vec<Box<dyn Node>> = Vec::new();
|
||||
let mut flat_signatures: Vec<NodeSchema> = Vec::new();
|
||||
let mut flat_edges: Vec<Edge> = Vec::new();
|
||||
let mut flat_taps: Vec<FlatTap> = Vec::new();
|
||||
let mut cursor = 0usize;
|
||||
|
||||
let lowerings = lower_items(
|
||||
@@ -354,6 +355,7 @@ impl Composite {
|
||||
&mut flat_nodes,
|
||||
&mut flat_signatures,
|
||||
&mut flat_edges,
|
||||
&mut flat_taps,
|
||||
)?;
|
||||
|
||||
for e in &self.edges {
|
||||
@@ -373,12 +375,20 @@ impl Composite {
|
||||
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 {
|
||||
nodes: flat_nodes,
|
||||
signatures: flat_signatures,
|
||||
sources: flat_sources,
|
||||
edges: flat_edges,
|
||||
taps: Vec::new(),
|
||||
taps: flat_taps,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -853,6 +863,9 @@ pub enum CompileError {
|
||||
DoubleWiredPort { node: usize, slot: usize },
|
||||
/// A gang table failed structural validation at a minting boundary.
|
||||
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`
|
||||
@@ -901,6 +914,7 @@ pub(crate) fn validate_wiring(
|
||||
edges: &[Edge],
|
||||
roles: &[Role],
|
||||
output: &[OutField],
|
||||
taps: &[Tap],
|
||||
) -> Result<(), CompileError> {
|
||||
// edges: index-range + producer/consumer kind match. The kind-mismatch variant
|
||||
// is the SAME one bootstrap returns today (Bootstrap(KindMismatch)), just raised
|
||||
@@ -932,12 +946,20 @@ pub(crate) fn validate_wiring(
|
||||
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
|
||||
check_ports_connected(nodes, edges, roles)?;
|
||||
// recurse into nested composites
|
||||
for item in nodes {
|
||||
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(())
|
||||
@@ -1158,6 +1180,7 @@ fn lower_items(
|
||||
flat_nodes: &mut Vec<Box<dyn Node>>,
|
||||
flat_signatures: &mut Vec<NodeSchema>,
|
||||
flat_edges: &mut Vec<Edge>,
|
||||
flat_taps: &mut Vec<FlatTap>,
|
||||
) -> Result<Vec<ItemLowering>, CompileError> {
|
||||
let mut lowerings = Vec::with_capacity(items.len());
|
||||
for item in items {
|
||||
@@ -1181,6 +1204,7 @@ fn lower_items(
|
||||
flat_nodes,
|
||||
flat_signatures,
|
||||
flat_edges,
|
||||
flat_taps,
|
||||
)?);
|
||||
}
|
||||
}
|
||||
@@ -1197,6 +1221,7 @@ fn inline_composite(
|
||||
flat_nodes: &mut Vec<Box<dyn Node>>,
|
||||
flat_signatures: &mut Vec<NodeSchema>,
|
||||
flat_edges: &mut Vec<Edge>,
|
||||
flat_taps: &mut Vec<FlatTap>,
|
||||
) -> Result<ItemLowering, CompileError> {
|
||||
// `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.
|
||||
@@ -1207,11 +1232,12 @@ fn inline_composite(
|
||||
// `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
|
||||
// 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();
|
||||
|
||||
// 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 fe in rewrite_edge(e, &interior, flat_signatures)? {
|
||||
flat_edges.push(fe);
|
||||
@@ -1257,6 +1283,15 @@ fn inline_composite(
|
||||
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 })
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
fn slot_kind(t: Target, flat_signatures: &[NodeSchema]) -> Result<ScalarKind, CompileError> {
|
||||
flat_signatures[t.node]
|
||||
@@ -3624,4 +3688,77 @@ mod tests {
|
||||
.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 } }]);
|
||||
}
|
||||
|
||||
/// 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)
|
||||
.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 {
|
||||
node: self.ids[node].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
|
||||
/// producer `(node, output-field)`. The output-side twin of `SourceSpec` — its
|
||||
/// `name` is meant to survive compile and be load-bearing for a future
|
||||
/// by-name binding step (like `SourceSpec.role`, #275), planned as `bind_tap`
|
||||
/// in a later slice of #282. This slice only carries the type; `compile`
|
||||
/// resolves no `Tap`s yet and always emits an empty `taps` list — the
|
||||
/// lowering remap that would populate it from a blueprint `Tap` lands with
|
||||
/// `bind_tap`.
|
||||
/// `name` survives compile and is load-bearing for by-name binding (like
|
||||
/// `SourceSpec.role`, #275). `compile` resolves each blueprint `Tap` into a
|
||||
/// `FlatTap` through the same lowering remap that hoists edges/`OutField`
|
||||
/// re-exports (interior-composite taps hoist to the root list).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct FlatTap {
|
||||
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 graph_model::model_to_json;
|
||||
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,
|
||||
};
|
||||
pub use report::{
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
//! End-to-end coverage for the `FlatTap` / `FlatGraph.taps` plumbing (#282,
|
||||
//! "thread all literal sites"): a `Composite` can declare a `Tap` (via
|
||||
//! `with_taps`), but this slice adds only the flat-graph *carrier* — no
|
||||
//! resolution step exists yet. These tests pin the two halves of that
|
||||
//! contract at the public `aura_engine` boundary: `compile()` never surfaces
|
||||
//! a declared `Tap` into the flat graph it hands to `Harness::bootstrap`
|
||||
//! (`compile_never_resolves_declared_taps_yet`), and the field's presence is
|
||||
//! otherwise perfectly transparent to compilation of the rest of the graph
|
||||
//! (`tapped_composite_compiles_identically_to_untapped_besides_the_taps_list`)
|
||||
//! — a future resolving slice (`bind_tap`) is expected to flip the first of
|
||||
//! 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.
|
||||
//! End-to-end coverage for the `FlatTap` / `FlatGraph.taps` plumbing (#282):
|
||||
//! a `Composite` can declare a `Tap` (via `with_taps`), and `compile()` now
|
||||
//! resolves it into the flat graph it hands to `Harness::bootstrap`. These
|
||||
//! tests pin the two halves of that contract at the public `aura_engine`
|
||||
//! boundary: `compile()` surfaces a declared `Tap` into `FlatGraph.taps`,
|
||||
//! resolved to its flat producer `(node, field)`
|
||||
//! (`compile_resolves_a_declared_tap_at_the_public_boundary`), and doing so
|
||||
//! is otherwise transparent to compilation of the rest of the graph — the
|
||||
//! same node/edge structure, just an added taps entry
|
||||
//! (`tapped_composite_compiles_identically_to_untapped_besides_the_taps_list`).
|
||||
|
||||
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;
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// Property: **`compile()` resolves no `Tap`s in this slice.** A composite
|
||||
/// declaring a valid `Tap` (naming a real interior producer, `node: 0, field:
|
||||
/// 0`) still compiles to a `FlatGraph` whose `taps` list is empty — the
|
||||
/// declaration is carried on the `Composite` only; nothing yet lowers it into
|
||||
/// the flat `FlatTap` the run loop could someday read. If a later change
|
||||
/// 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.
|
||||
/// Property: **`compile()` resolves a declared `Tap` at the public
|
||||
/// boundary.** A composite declaring a valid `Tap` (naming a real interior
|
||||
/// producer, `node: 0, field: 0`) compiles to a `FlatGraph` whose `taps` list
|
||||
/// carries the resolved `FlatTap` — name preserved, wire resolved to the flat
|
||||
/// producer index.
|
||||
#[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 flat = tapped.compile().expect("a tap-bearing composite still compiles");
|
||||
assert!(flat.taps.is_empty(), "compile() must not resolve declared Taps in this slice: {:?}", flat.taps);
|
||||
let flat = tapped.compile().expect("a tap-bearing composite compiles");
|
||||
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
|
||||
/// changes nothing about the flattened node/edge structure. Compiling the
|
||||
/// same composite with and without a `Tap` declaration must produce
|
||||
/// structurally identical flat graphs (same node count, same edges); the
|
||||
/// only difference tap-bearing scaffolding is *allowed* to make is the (here,
|
||||
/// still-empty) `taps` list itself.
|
||||
/// only difference tap-bearing scaffolding is allowed to make is the `taps`
|
||||
/// list itself (empty vs. carrying the one resolved entry).
|
||||
#[test]
|
||||
fn tapped_composite_compiles_identically_to_untapped_besides_the_taps_list() {
|
||||
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.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