test(engine): pin that a no-out-edge producer runs inertly (#282 substrate)
Current-behaviour pin for the declared-taps design: a producer node with a non-empty output and zero out-edges compiles, bootstraps, and runs — its per-cycle output silently discarded — observably identical to the same graph with the producer removed. This is the exact shape of an unbound declared tap (a study wire left un-recorded in a sweep), and it establishes that the engine already tolerates it: no DCE is needed for correctness, because check_ports_connected gates only input slots and the Kahn sort emits a no-out-edge node as a valid sink. The build-time-elision soundness the tap mechanism will rely on. refs #282
This commit is contained in:
@@ -1424,6 +1424,67 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_producer_with_no_out_edge_runs_inertly_identical_to_its_removal() {
|
||||
// CURRENT-BEHAVIOUR PIN (#282 substrate): a producer node with a
|
||||
// NON-EMPTY output and ZERO out-edges compiles, bootstraps, and runs —
|
||||
// its per-cycle Some(row) is simply discarded, disturbing nothing. This
|
||||
// is the exact shape of an *unbound declared tap*: the tap mechanism
|
||||
// will let such a producer be declared and left un-recorded in a sweep,
|
||||
// and this pins that the engine already tolerates it (no DCE needed for
|
||||
// correctness — check_ports_connected gates only input slots, and the
|
||||
// Kahn sort emits a no-out-edge node as a valid sink). The proof is
|
||||
// observable identity: the run's drained output is byte-for-byte the same
|
||||
// with the dangling producer present as with it removed.
|
||||
let prices = f64_stream(&[(1, 10.0), (2, 20.0), (3, 30.0)]);
|
||||
|
||||
// WITH a dangling producer: source fans out to Sma(1) [node 0, a real
|
||||
// producer that returns Some every warm cycle, wired to NOTHING] and to
|
||||
// a Recorder [node 1] fed directly by the source.
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let mut h_with = boot(
|
||||
vec![
|
||||
Box::new(Sma::new(1)),
|
||||
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)),
|
||||
],
|
||||
vec![Sma::builder().schema().clone(), recorder_sig(&[ScalarKind::F64], Firing::Any)],
|
||||
vec![SourceSpec::raw(
|
||||
ScalarKind::F64,
|
||||
vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }],
|
||||
)],
|
||||
vec![], // NO edge out of node 0 — the producer dangles
|
||||
)
|
||||
.expect("a no-out-edge producer is a valid graph");
|
||||
h_with.run(vec![Box::new(VecSource::new(prices.clone()))]);
|
||||
let with_dangling: Vec<(Timestamp, Vec<Scalar>)> = rx.try_iter().collect();
|
||||
|
||||
// WITHOUT it: just the Recorder, fed by the same source.
|
||||
let (tx2, rx2) = mpsc::channel();
|
||||
let mut h_without = boot(
|
||||
vec![Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx2))],
|
||||
vec![recorder_sig(&[ScalarKind::F64], Firing::Any)],
|
||||
vec![SourceSpec::raw(ScalarKind::F64, vec![Target { node: 0, slot: 0 }])],
|
||||
vec![],
|
||||
)
|
||||
.expect("valid");
|
||||
h_without.run(vec![Box::new(VecSource::new(prices))]);
|
||||
let without_dangling: Vec<(Timestamp, Vec<Scalar>)> = rx2.try_iter().collect();
|
||||
|
||||
assert_eq!(
|
||||
with_dangling,
|
||||
vec![
|
||||
(Timestamp(1), vec![Scalar::f64(10.0)]),
|
||||
(Timestamp(2), vec![Scalar::f64(20.0)]),
|
||||
(Timestamp(3), vec![Scalar::f64(30.0)]),
|
||||
],
|
||||
"the drained output is the source series; the dangling producer changes nothing"
|
||||
);
|
||||
assert_eq!(
|
||||
with_dangling, without_dangling,
|
||||
"a producer wired to nothing is observably inert — identical to its removal"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_a_and_b_or_combine_on_one_node() {
|
||||
// MixedSum @0 (in0,in1 barrier group 0; in2 as-of); node 1 = Recorder taps it.
|
||||
|
||||
Reference in New Issue
Block a user