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:
2026-07-17 23:18:00 +02:00
parent 17e125a90a
commit f3daded514
5 changed files with 244 additions and 43 deletions
+143 -6
View File
@@ -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 }]);
}
} }
+1 -1
View File
@@ -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(),
+4 -6
View File
@@ -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,
+1 -1
View File
@@ -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::{
+95 -29
View File
@@ -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"
);
} }