Skip to main content

datum/graph/
mod.rs

1//! Typed graph, shape, port, and junction primitives.
2//!
3//! This module contains Datum's Akka-like graph layer: typed public ports, the
4//! consuming [`TypedGraphBuilder`] subset for static Rust graphs, the mutable
5//! runtime-validated [`GraphBuilder`] for dynamic/cyclic/erased interop, fused
6//! acyclic fast paths, and a queued fused interpreter for cyclic feedback
7//! topologies. Invalid wiring and illegal stage operations fail
8//! deterministically.
9
10use std::{
11    any::{Any, TypeId, type_name},
12    collections::{HashMap, HashSet, VecDeque},
13    fmt,
14    marker::PhantomData,
15    sync::{
16        Arc, Mutex, OnceLock,
17        atomic::{AtomicUsize, Ordering},
18    },
19    thread,
20};
21
22#[cfg(test)]
23use std::sync::mpsc;
24
25use crate::{
26    actor::{Actor, ActorProcessingErr, ActorRef},
27    stream::{OverflowStrategy, StreamError, StreamResult},
28};
29
30type DatumValue = Box<dyn DatumElement>;
31type StageMapFn = dyn Fn(DatumValue) -> StreamResult<DatumValue> + Send + Sync;
32type StageTypedMapFn = dyn Any + Send + Sync;
33type StageZipFn = dyn Fn(DatumValue, DatumValue) -> StreamResult<DatumValue> + Send + Sync;
34type StageUnzipFn = dyn Fn(DatumValue) -> (DatumValue, DatumValue) + Send + Sync;
35/// Opaque container for a typed split function `Arc<dyn Fn(In) -> (Out0, Out1)>`.
36/// Down-cast at plan time — same pattern as [`StageTypedMapFn`].
37type StageTypedUnzipFn = dyn Any + Send + Sync;
38type StageCompareFn = dyn Fn(&DatumValue, &DatumValue) -> std::cmp::Ordering + Send + Sync;
39type StageSequenceFn = dyn Fn(&DatumValue) -> u64 + Send + Sync;
40/// Opaque container for a typed sequence-extractor `Arc<dyn Fn(&T) -> u64>`.
41/// Down-cast at plan time.
42type StageTypedSequenceFn = dyn Any + Send + Sync;
43type StageSnapshotFn = dyn Fn(&[&DatumValue]) -> DatumValue + Send + Sync;
44/// Opaque container for a typed snapshot builder `Arc<dyn Fn(&[Option<T>]) -> Vec<T>>`.
45/// Down-cast at plan time.
46type StageTypedSnapshotFn = dyn Any + Send + Sync;
47type StagePartitionFn = dyn Fn(&DatumValue) -> usize + Send + Sync;
48/// Opaque container for a typed partitioner `Arc<dyn Fn(&T) -> usize>`.
49/// Down-cast at plan time.
50type StageTypedPartitionFn = dyn Any + Send + Sync;
51
52#[derive(Clone)]
53struct StageMapFns {
54    erased: Arc<StageMapFn>,
55    typed: Arc<StageTypedMapFn>,
56    scalar_i64: Option<Arc<StageTypedMapFn>>,
57    scalar_finish: Option<Arc<StageTypedMapFn>>,
58}
59
60/// Optional typed metadata attached to an `Opaque` stage so the typed **cyclic**
61/// executor can run it without `DatumValue` boxing or `GraphStageLogic`.
62///
63/// Built-in `Buffer`/`TakeWhile` stages surface this through
64/// [`GraphStage::typed_cyclic_op`]; every other stage returns `None` and the
65/// cyclic typed planner falls back to the erased queued interpreter. The erased
66/// executor never reads this — it keeps running the stage's `GraphStageLogic`.
67#[derive(Clone)]
68pub(super) enum TypedCyclicOp {
69    /// A `Buffer` stage. In the synchronous fused executor a buffer drains one
70    /// element per step and therefore never overflows — it is a pass-through.
71    BufferPassthrough,
72    /// A `TakeWhile` stage carrying its typed predicate
73    /// `Arc<dyn Fn(&T) -> bool + Send + Sync>` (down-cast at plan time, same
74    /// pattern as [`StageTypedMapFn`]).
75    TakeWhile(Arc<dyn Any + Send + Sync>),
76}
77
78impl fmt::Debug for TypedCyclicOp {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        match self {
81            Self::BufferPassthrough => f.write_str("BufferPassthrough"),
82            Self::TakeWhile(_) => f.write_str("TakeWhile(..)"),
83        }
84    }
85}
86
87pub(crate) trait DatumElement: Any + Send {
88    fn clone_box(&self) -> DatumValue;
89    fn into_any(self: Box<Self>) -> Box<dyn Any + Send>;
90    fn as_any_ref(&self) -> &dyn Any;
91}
92
93impl<T> DatumElement for T
94where
95    T: Any + Clone + Send,
96{
97    fn clone_box(&self) -> DatumValue {
98        Box::new(self.clone())
99    }
100
101    fn into_any(self: Box<Self>) -> Box<dyn Any + Send> {
102        self
103    }
104
105    fn as_any_ref(&self) -> &dyn Any {
106        self
107    }
108}
109
110fn datum<T>(value: T) -> DatumValue
111where
112    T: Clone + Send + 'static,
113{
114    Box::new(value)
115}
116
117fn downcast_datum<T, S>(
118    value: DatumValue,
119    operation: &'static str,
120    port: impl FnOnce() -> S,
121) -> StreamResult<T>
122where
123    T: Send + 'static,
124    S: Into<String>,
125{
126    // `port` is only evaluated on the cold error path so the success path
127    // (the per-element fused hot path) allocates nothing for diagnostics. It
128    // returns `impl Into<String>` so callers can hand back a `&'static str`
129    // without an eager `to_owned()`.
130    value
131        .into_any()
132        .downcast::<T>()
133        .map(|value| *value)
134        .map_err(|_| StreamError::InvalidPortOperation {
135            operation,
136            port: port().into(),
137            reason: format!("element type did not match {}", type_name::<T>()),
138        })
139}
140
141static NEXT_PORT_ID: AtomicUsize = AtomicUsize::new(1);
142
143fn next_port_id() -> PortId {
144    PortId(NEXT_PORT_ID.fetch_add(1, Ordering::Relaxed))
145}
146
147fn next_port_id_block(count: usize) -> PortId {
148    debug_assert!(count > 0);
149    PortId(NEXT_PORT_ID.fetch_add(count, Ordering::Relaxed))
150}
151
152/// A process-global unique identifier for a port, drawn from a single atomic
153/// counter. Stages allocate contiguous id blocks; ids are not graph-local and
154/// do not start at zero.
155#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
156pub struct PortId(usize);
157
158impl PortId {
159    #[must_use]
160    pub const fn as_usize(self) -> usize {
161        self.0
162    }
163
164    const fn offset(self, offset: usize) -> Self {
165        Self(self.0 + offset)
166    }
167}
168
169/// Whether a port is an inlet (accepts elements) or an outlet (emits them).
170#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
171pub enum PortKind {
172    Inlet,
173    Outlet,
174}
175
176macro_rules! shared_name {
177    ($fn_name:ident, $value:literal) => {
178        fn $fn_name() -> Arc<str> {
179            static NAME: OnceLock<Arc<str>> = OnceLock::new();
180            Arc::clone(NAME.get_or_init(|| Arc::from($value)))
181        }
182    };
183}
184
185shared_name!(identity_stage_name, "Identity");
186shared_name!(identity_inlet_name, "Identity.in");
187shared_name!(identity_outlet_name, "Identity.out");
188shared_name!(map_stage_name, "Map");
189shared_name!(map_inlet_name, "Map.in");
190shared_name!(map_outlet_name, "Map.out");
191shared_name!(broadcast_stage_name, "Broadcast");
192shared_name!(broadcast_inlet_name, "Broadcast.in");
193shared_name!(balance_stage_name, "Balance");
194shared_name!(balance_inlet_name, "Balance.in");
195shared_name!(merge_stage_name, "Merge");
196shared_name!(merge_outlet_name, "Merge.out");
197shared_name!(merge_preferred_stage_name, "MergePreferred");
198shared_name!(merge_preferred_preferred_name, "MergePreferred.preferred");
199shared_name!(merge_preferred_outlet_name, "MergePreferred.out");
200shared_name!(merge_prioritized_stage_name, "MergePrioritized");
201shared_name!(merge_prioritized_outlet_name, "MergePrioritized.out");
202shared_name!(concat_stage_name, "Concat");
203shared_name!(concat_outlet_name, "Concat.out");
204shared_name!(or_else_stage_name, "OrElse");
205shared_name!(or_else_primary_name, "OrElse.primary");
206shared_name!(or_else_secondary_name, "OrElse.secondary");
207shared_name!(or_else_outlet_name, "OrElse.out");
208shared_name!(interleave_stage_name, "Interleave");
209shared_name!(interleave_outlet_name, "Interleave.out");
210shared_name!(zip_stage_name, "Zip");
211shared_name!(zip_in0_name, "Zip.in0");
212shared_name!(zip_in1_name, "Zip.in1");
213shared_name!(zip_outlet_name, "Zip.out");
214shared_name!(async_boundary_stage_name, "AsyncBoundary");
215shared_name!(async_boundary_inlet_name, "AsyncBoundary.in");
216shared_name!(async_boundary_outlet_name, "AsyncBoundary.out");
217
218mod builder;
219mod executor;
220mod junctions;
221mod metrics;
222mod ports;
223mod shapes;
224mod stage;
225mod typed_builder;
226mod wire;
227
228#[cfg(test)]
229use self::executor::BoundaryCountExecutor;
230use self::{
231    builder::StageRecord,
232    stage::{StageKind, StageTimerMailbox, StageTimerRuntime},
233};
234
235pub use self::{
236    builder::{
237        AsyncBoundaryExecutionConfig, FusedExecutionConfig, FusedExecutionReport,
238        FusedExecutorTier, FusedNodeAttributes, FusedPlanReport, FusedSegment, FusedTerminalReport,
239        FusedTierChange, FusedTierChangeReason, Graph, GraphBlueprint, GraphBuilder, GraphDsl,
240        ImportedGraph, PartialGraph,
241    },
242    junctions::{
243        AsyncBoundary, Balance, Broadcast, Buffer, Concat, Identity, Interleave, MapStage, Merge,
244        MergeLatest, MergePreferred, MergePrioritized, MergeSequence, MergeSorted, OrElse,
245        Partition, TakeWhile, Unzip, UnzipWith, Zip,
246    },
247    metrics::{FusedNodeMetrics, metrics_disabled_path_probe, metrics_no_site_probe},
248    ports::{AnyInlet, AnyOutlet, Inlet, Outlet, PortRef},
249    shapes::{
250        BidiShape, FanInShape, FanOutShape, FanOutShape2, FlowShape, MergePreferredShape,
251        PortAllocator, Shape, SinkShape, SourceShape, ZipShape,
252    },
253    stage::{
254        AsyncCallback, GraphStage, GraphStageLogic, InHandler, OutHandler, StageSpec, TimerHandler,
255    },
256    typed_builder::{OpenInlet, OpenOutlet, TypedGraphBuilder},
257    wire::{
258        AutoInletEndpoint, AutoOutletEndpoint, InletCursor, OutletCursor, WireDsl, WirePair,
259        WireSpec,
260    },
261};
262
263#[cfg(test)]
264mod tests {
265    use super::executor::ExecutorMode;
266    use super::*;
267    use crate::{Attribute, AttributeError, Attributes, Fusion, KernelBackend, MetricsLevel};
268
269    fn segment_indices<S: Shape>(graph: &GraphBlueprint<S>) -> Vec<Vec<usize>> {
270        graph
271            .segments()
272            .iter()
273            .map(|segment| segment.stage_indices().to_vec())
274            .collect()
275    }
276
277    fn assert_same_flow_blueprint_and_typed_tier<In, Out>(
278        current: &GraphBlueprint<FlowShape<In, Out>>,
279        typed: &GraphBlueprint<FlowShape<In, Out>>,
280        input: Vec<In>,
281    ) where
282        In: Clone + Send + 'static,
283        Out: Send + PartialEq + fmt::Debug + 'static,
284    {
285        assert_eq!(current.stage_count(), typed.stage_count());
286        assert_eq!(current.edge_count(), typed.edge_count());
287        assert_eq!(segment_indices(current), segment_indices(typed));
288
289        let config = FusedExecutionConfig {
290            event_limit: 5_000_000,
291        };
292        let current_auto = current
293            .run_with_input_report(input.clone(), config)
294            .unwrap();
295        let typed_auto = typed.run_with_input_report(input.clone(), config).unwrap();
296        assert_eq!(current_auto, typed_auto);
297
298        let current_typed = current
299            .run_with_input_report_mode(input.clone(), config, ExecutorMode::TypedOnly)
300            .unwrap();
301        let typed_typed = typed
302            .run_with_input_report_mode(input, config, ExecutorMode::TypedOnly)
303            .unwrap();
304        assert_eq!(current_typed, typed_typed);
305        assert_eq!(current_auto, current_typed);
306        assert_eq!(typed_auto, typed_typed);
307    }
308
309    #[test]
310    fn graph_dsl_builds_broadcast_zip_flow() {
311        let graph = GraphDsl::try_create(|builder| {
312            let broadcast = builder.add(Broadcast::<i32>::new(2));
313            let zip = builder.add(Zip::<i32, i32>::new());
314
315            builder.connect(broadcast.outlet(0)?, zip.in0())?;
316            builder.connect(broadcast.outlet(1)?, zip.in1())?;
317
318            Ok(FlowShape::new(broadcast.inlet(), zip.outlet()))
319        })
320        .unwrap();
321
322        assert_eq!(graph.stage_count(), 2);
323        assert_eq!(graph.edge_count(), 2);
324        assert_eq!(graph.shape().inlets().len(), 1);
325        assert_eq!(graph.shape().outlets().len(), 1);
326        assert_eq!(
327            graph.run_with_input([1, 2, 3]).unwrap(),
328            vec![(1, 1), (2, 2), (3, 3)]
329        );
330    }
331
332    #[test]
333    fn consuming_builder_matches_current_linear_blueprint_and_tier() {
334        let current = GraphDsl::try_create(|builder| {
335            let first = builder.add(Identity::<u64>::new());
336            let second = builder.add(MapStage::new(|item: u64| item + 1));
337
338            builder.connect(first.outlet(), second.inlet())?;
339
340            Ok(FlowShape::new(first.inlet(), second.outlet()))
341        })
342        .unwrap();
343
344        let typed = GraphDsl::typed_try_create(|builder| {
345            let (builder, input, first_out) = builder.add_flow(Identity::<u64>::new());
346            let (builder, second_in, output) =
347                builder.add_flow(MapStage::new(|item: u64| item + 1));
348
349            let builder = builder.connect(first_out, second_in)?;
350
351            Ok((
352                builder,
353                FlowShape::new(input.into_port(), output.into_port()),
354            ))
355        })
356        .unwrap();
357
358        assert_same_flow_blueprint_and_typed_tier(&current, &typed, (0_u64..16).collect());
359    }
360
361    #[test]
362    fn consuming_builder_matches_current_broadcast_zip_blueprint_and_tier() {
363        let current = GraphDsl::try_create(|builder| {
364            let broadcast = builder.add(Broadcast::<u64>::new(2));
365            let zip = builder.add(Zip::<u64, u64>::new());
366
367            builder.connect(broadcast.outlet(0)?, zip.in0())?;
368            builder.connect(broadcast.outlet(1)?, zip.in1())?;
369
370            Ok(FlowShape::new(broadcast.inlet(), zip.outlet()))
371        })
372        .unwrap();
373
374        let typed = GraphDsl::typed_try_create(|builder| {
375            let (builder, input, [left_out, right_out]) = builder.add_broadcast2::<u64>();
376            let (builder, left_in, right_in, output) = builder.add_zip::<u64, u64>();
377
378            let builder = builder.connect(left_out, left_in)?;
379            let builder = builder.connect(right_out, right_in)?;
380
381            Ok((
382                builder,
383                FlowShape::new(input.into_port(), output.into_port()),
384            ))
385        })
386        .unwrap();
387
388        assert_same_flow_blueprint_and_typed_tier(&current, &typed, (0_u64..16).collect());
389    }
390
391    #[test]
392    fn consuming_builder_matches_current_balance_merge_blueprint_and_tier() {
393        let current = GraphDsl::try_create(|builder| {
394            let balance = builder.add(Balance::<u64>::new(2));
395            let merge = builder.add(Merge::<u64>::new(2));
396
397            builder.connect(balance.outlet(0)?, merge.inlet(0)?)?;
398            builder.connect(balance.outlet(1)?, merge.inlet(1)?)?;
399
400            Ok(FlowShape::new(balance.inlet(), merge.outlet()))
401        })
402        .unwrap();
403
404        let typed = GraphDsl::typed_try_create(|builder| {
405            let (builder, input, [left_out, right_out]) = builder.add_balance2::<u64>();
406            let (builder, [left_in, right_in], output) = builder.add_merge2::<u64>();
407
408            let builder = builder.connect(left_out, left_in)?;
409            let builder = builder.connect(right_out, right_in)?;
410
411            Ok((
412                builder,
413                FlowShape::new(input.into_port(), output.into_port()),
414            ))
415        })
416        .unwrap();
417
418        assert_same_flow_blueprint_and_typed_tier(&current, &typed, (0_u64..16).collect());
419    }
420
421    #[test]
422    fn graph_dsl_zip_slots_follow_inlet_ids() {
423        let graph = GraphDsl::try_create(|builder| {
424            let broadcast = builder.add(Broadcast::<i32>::new(2));
425            let identity = builder.add(Identity::<i32>::new());
426            let zip = builder.add(Zip::<i32, i32>::new());
427
428            builder.connect(broadcast.outlet(0)?, identity.inlet())?;
429            builder.connect(identity.outlet(), zip.in1())?;
430            builder.connect(broadcast.outlet(1)?, zip.in0())?;
431
432            Ok(FlowShape::new(broadcast.inlet(), zip.outlet()))
433        })
434        .unwrap();
435
436        assert_eq!(
437            graph.run_with_input([1, 2, 3]).unwrap(),
438            vec![(1, 1), (2, 2), (3, 3)]
439        );
440    }
441
442    #[test]
443    fn graph_dsl_zip_buffers_skewed_inlet_arrivals() {
444        // Regression test for per-inlet Zip buffering. This asymmetric topology
445        // delivers TWO elements to zip.in0 for every ONE that reaches zip.in1
446        // within a single fused cycle:
447        //
448        //   in -> Broadcast A(2)
449        //          A.out0 -> Broadcast B(2) -> Merge(2).out -> zip.in0   (two arrivals)
450        //          A.out1 ----------------------------------> zip.in1   (one arrival)
451        //
452        // The fused executor drains A.out0 depth-first, so both Merge emissions
453        // hit zip.in0 before A.out1 ever feeds zip.in1. With single-slot Zip
454        // state this errors ("second element before its pair"); bounded
455        // per-inlet buffering must instead queue the surplus and pair in FIFO
456        // arrival order without error. Neither Broadcast matches the fused
457        // Broadcast(2)->Zip fast path (A feeds two different stages; B feeds a
458        // Merge), so this exercises the general buffered Zip path.
459        let graph = GraphDsl::try_create(|builder| {
460            let fan = builder.add(Broadcast::<i32>::new(2));
461            let doubler = builder.add(Broadcast::<i32>::new(2));
462            let merge = builder.add(Merge::<i32>::new(2));
463            let zip = builder.add(Zip::<i32, i32>::new());
464
465            builder.connect(fan.outlet(0)?, doubler.inlet())?;
466            builder.connect(doubler.outlet(0)?, merge.inlet(0)?)?;
467            builder.connect(doubler.outlet(1)?, merge.inlet(1)?)?;
468            builder.connect(merge.outlet(), zip.in0())?;
469            builder.connect(fan.outlet(1)?, zip.in1())?;
470
471            Ok(FlowShape::new(fan.inlet(), zip.outlet()))
472        })
473        .unwrap();
474
475        // in0 accumulates two per item while in1 gets one, so FIFO pairing runs
476        // in0 one item ahead: 10 pairs with 10, then the leftover 10 pairs with
477        // 20. The point is that this succeeds (no error) and pairs in order.
478        assert_eq!(
479            graph.run_with_input([10, 20]).unwrap(),
480            vec![(10, 10), (10, 20)]
481        );
482    }
483
484    #[test]
485    fn graph_dsl_runs_merge_preferred_buffered_feedback_cycle() {
486        let graph = GraphDsl::try_create(|builder| {
487            let merge = builder.add(MergePreferred::<i32>::new(1));
488            let broadcast = builder.add(Broadcast::<i32>::new(2));
489            let buffer = builder.add(Buffer::<i32>::new(8, OverflowStrategy::Backpressure));
490            let positive = builder.add(TakeWhile::<i32>::new(|item| *item > 0));
491            let decrement = builder.add(MapStage::new(|item: i32| item - 1));
492
493            builder.connect(merge.outlet(), broadcast.inlet())?;
494            builder.connect(broadcast.outlet(1)?, buffer.inlet())?;
495            builder.connect(buffer.outlet(), positive.inlet())?;
496            builder.connect(positive.outlet(), decrement.inlet())?;
497            builder.connect(decrement.outlet(), merge.preferred())?;
498
499            Ok(FlowShape::new(merge.secondary(0)?, broadcast.outlet(0)?))
500        })
501        .unwrap();
502
503        assert_eq!(graph.run_with_input([3]).unwrap(), vec![3, 2, 1, 0]);
504    }
505
506    #[test]
507    fn graph_dsl_runs_buffered_merge_feedback_cycle() {
508        let graph = GraphDsl::try_create(|builder| {
509            let merge = builder.add(Merge::<i32>::new(2));
510            let broadcast = builder.add(Broadcast::<i32>::new(2));
511            let buffer = builder.add(Buffer::<i32>::new(8, OverflowStrategy::Backpressure));
512            let positive = builder.add(TakeWhile::<i32>::new(|item| *item > 0));
513            let decrement = builder.add(MapStage::new(|item: i32| item - 1));
514
515            builder.connect(merge.outlet(), broadcast.inlet())?;
516            builder.connect(broadcast.outlet(1)?, buffer.inlet())?;
517            builder.connect(buffer.outlet(), positive.inlet())?;
518            builder.connect(positive.outlet(), decrement.inlet())?;
519            builder.connect(decrement.outlet(), merge.inlet(1)?)?;
520
521            Ok(FlowShape::new(merge.inlet(0)?, broadcast.outlet(0)?))
522        })
523        .unwrap();
524
525        assert_eq!(graph.run_with_input([3]).unwrap(), vec![3, 2, 1, 0]);
526    }
527
528    #[test]
529    fn graph_dsl_unbuffered_merge_cycle_surfaces_event_limit() {
530        let graph = GraphDsl::try_create(|builder| {
531            let merge = builder.add(Merge::<i32>::new(2));
532            let broadcast = builder.add(Broadcast::<i32>::new(2));
533
534            builder.connect(merge.outlet(), broadcast.inlet())?;
535            builder.connect(broadcast.outlet(1)?, merge.inlet(1)?)?;
536
537            Ok(FlowShape::new(merge.inlet(0)?, broadcast.outlet(0)?))
538        })
539        .unwrap();
540
541        let result = graph.run_with_input_report([1], FusedExecutionConfig { event_limit: 256 });
542
543        assert_eq!(result, Err(StreamError::EventLimitExceeded { limit: 256 }));
544    }
545
546    #[test]
547    fn partial_graph_can_be_imported_with_its_shape() {
548        let partial = GraphDsl::partial(|builder| {
549            let first = builder.add(Identity::<i32>::new());
550            let second = builder.add_named(Identity::<i32>::new(), "partial.tail");
551            builder.connect(first.outlet(), second.inlet())?;
552            Ok(FlowShape::new(first.inlet(), second.outlet()))
553        })
554        .named("partial.identity");
555
556        let graph = GraphDsl::try_create(|builder| {
557            let imported = builder.import(&partial)?;
558            let after = builder.add(Identity::<i32>::new());
559            builder.connect(imported.outlet(), after.inlet())?;
560            Ok(FlowShape::new(imported.inlet(), after.outlet()))
561        })
562        .unwrap()
563        .named("outer.graph");
564
565        assert_eq!(graph.run_with_input([1, 2, 3]).unwrap(), vec![1, 2, 3]);
566        assert_eq!(graph.attributes().name(), Some("outer.graph"));
567    }
568
569    #[test]
570    fn graph_attributes_follow_innermost_wins_order() {
571        let graph = GraphDsl::create(|builder| {
572            builder.add_with_attributes(
573                Identity::<i32>::new(),
574                Attributes::named("stage-outer").and(Attributes::named("stage-inner")),
575            )
576        })
577        .unwrap()
578        .add_attributes(Attributes::dispatcher("graph-outer"))
579        .add_attributes(Attributes::dispatcher("graph-inner"));
580
581        assert_eq!(graph.attributes().dispatcher_hint(), Some("graph-inner"));
582        assert_eq!(
583            graph.stages[0].spec.attributes().name(),
584            Some("stage-inner")
585        );
586    }
587
588    #[test]
589    fn effective_attributes_are_closest_wins_with_partial_stage_override() {
590        let graph = GraphDsl::try_create(|builder| {
591            let first =
592                builder.add_with_attributes(Identity::<i32>::new(), Attributes::batch_size(64));
593            let second = builder.add(Identity::<i32>::new());
594            builder.connect(first.outlet(), second.inlet())?;
595            Ok(FlowShape::new(first.inlet(), second.outlet()))
596        })
597        .unwrap()
598        .add_attributes(Attributes::named("graph-node"))
599        .add_attributes(Attributes::metrics_level(MetricsLevel::Counts))
600        .add_attributes(Attributes::batch_size(16));
601
602        let report = graph
603            .run_with_input_report([1, 2], FusedExecutionConfig::default())
604            .unwrap();
605
606        let first = &report.plan.stage_attributes[0].attributes;
607        assert_eq!(first.name(), Some("graph-node"));
608        assert_eq!(first.metrics_level_hint(), Some(MetricsLevel::Counts));
609        assert_eq!(first.batch_size_hint(), Some(64));
610
611        let second = &report.plan.stage_attributes[1].attributes;
612        assert_eq!(second.name(), Some("graph-node"));
613        assert_eq!(second.metrics_level_hint(), Some(MetricsLevel::Counts));
614        assert_eq!(second.batch_size_hint(), Some(16));
615    }
616
617    #[test]
618    fn execution_report_exposes_full_attribute_vocabulary() {
619        let full_bag = Attributes::named("full-node")
620            .and(Attributes::input_buffer(2, 8))
621            .and(Attributes::dispatcher("graph-dispatcher"))
622            .and(Attributes::batch_size(128))
623            .and(Attributes::parallelism(4))
624            .and(Attributes::fusion(Fusion::Auto))
625            .and(Attributes::metrics_level(MetricsLevel::Timing))
626            .and(Attributes::kernel_backend(KernelBackend::Python));
627
628        let graph = GraphDsl::create(|builder| {
629            builder.add_with_attributes(Identity::<i32>::new(), full_bag)
630        })
631        .unwrap();
632
633        let report = graph
634            .run_with_input_report([7], FusedExecutionConfig::default())
635            .unwrap();
636        let attrs = &report.plan.stage_attributes[0].attributes;
637
638        assert_eq!(report.plan.selected_tier, FusedExecutorTier::TypedLinear);
639        assert_eq!(attrs.name(), Some("full-node"));
640        assert_eq!(attrs.input_buffer_hint(), Some((2, 8)));
641        assert_eq!(attrs.dispatcher_hint(), Some("graph-dispatcher"));
642        assert_eq!(attrs.batch_size_hint(), Some(128));
643        assert_eq!(attrs.parallelism_hint(), Some(4));
644        assert_eq!(attrs.fusion_hint(), Some(Fusion::Auto));
645        assert_eq!(attrs.metrics_level_hint(), Some(MetricsLevel::Timing));
646        assert_eq!(attrs.kernel_backend_hint(), Some(KernelBackend::Python));
647        assert_eq!(attrs.attribute_list().len(), 8);
648    }
649
650    #[test]
651    fn attribute_validation_rejects_zero_buffer_and_parallelism() {
652        assert_eq!(
653            Attributes::try_input_buffer(0, 8),
654            Err(AttributeError::ZeroInputBuffer)
655        );
656        assert_eq!(
657            Attributes::try_parallelism(0),
658            Err(AttributeError::ZeroParallelism)
659        );
660    }
661
662    #[test]
663    fn typed_graph_builder_accepts_stage_attributes() {
664        let graph = GraphDsl::typed_try_create(|builder| {
665            let (builder, inlet, outlet) = builder
666                .add_flow_with_attributes(Identity::<i32>::new(), Attributes::batch_size(32));
667            Ok((
668                builder,
669                FlowShape::new(inlet.into_port(), outlet.into_port()),
670            ))
671        })
672        .unwrap()
673        .add_attributes(Attributes::metrics_level(MetricsLevel::Counts));
674
675        let report = graph
676            .run_with_input_report([1], FusedExecutionConfig::default())
677            .unwrap();
678        let attrs = &report.plan.stage_attributes[0].attributes;
679
680        assert_eq!(attrs.batch_size_hint(), Some(32));
681        assert_eq!(attrs.metrics_level_hint(), Some(MetricsLevel::Counts));
682    }
683
684    #[test]
685    fn metrics_off_reports_an_empty_snapshot() {
686        let graph = GraphDsl::create(|builder| builder.add(Identity::<i32>::new())).unwrap();
687        let report = graph
688            .run_with_input_report([1, 2, 3], FusedExecutionConfig::default())
689            .unwrap();
690
691        assert!(report.node_metrics.is_empty());
692        assert!(report.metrics_snapshot().is_empty());
693    }
694
695    #[test]
696    fn linear_metrics_are_exact_and_levels_are_monotonic() {
697        fn run(level: MetricsLevel) -> Vec<FusedNodeMetrics> {
698            let graph = GraphDsl::try_create(|builder| {
699                let first = builder.add(Identity::<i32>::new());
700                let second = builder.add(MapStage::new(|item: i32| item + 1));
701                builder.connect(first.outlet(), second.inlet())?;
702                Ok(FlowShape::new(first.inlet(), second.outlet()))
703            })
704            .unwrap()
705            .add_attributes(Attributes::metrics_level(level));
706
707            graph
708                .run_with_input_report([1, 2, 3, 4], FusedExecutionConfig::default())
709                .unwrap()
710                .node_metrics
711        }
712
713        let counts = run(MetricsLevel::Counts);
714        let timing = run(MetricsLevel::Timing);
715        let stalls = run(MetricsLevel::Stalls);
716        for snapshot in [&counts, &timing, &stalls] {
717            assert_eq!(snapshot.len(), 2);
718            assert!(
719                snapshot
720                    .iter()
721                    .all(|node| node.elements_in == 4 && node.elements_out == 4)
722            );
723            assert!(snapshot.iter().all(|node| {
724                node.batches_in == 0
725                    && node.batches_out == 0
726                    && node.rows_in == 0
727                    && node.rows_out == 0
728            }));
729        }
730        assert!(counts.iter().all(|node| node.processing_time.is_zero()));
731        assert!(timing.iter().all(|node| !node.processing_time.is_zero()));
732        assert!(stalls.iter().all(|node| !node.processing_time.is_zero()));
733        assert!(
734            timing
735                .iter()
736                .all(|node| node.backpressure_stall_time.is_zero())
737        );
738    }
739
740    #[test]
741    fn junction_metrics_count_each_stage_boundary_exactly() {
742        let graph = GraphDsl::try_create(|builder| {
743            let broadcast = builder.add(Broadcast::<i32>::new(2));
744            let zip = builder.add(Zip::<i32, i32>::new());
745            builder.connect(broadcast.outlet(0)?, zip.in0())?;
746            builder.connect(broadcast.outlet(1)?, zip.in1())?;
747            Ok(FlowShape::new(broadcast.inlet(), zip.outlet()))
748        })
749        .unwrap()
750        .add_attributes(Attributes::metrics_level(MetricsLevel::Counts));
751
752        let report = graph
753            .run_with_input_report([1, 2, 3], FusedExecutionConfig::default())
754            .unwrap();
755        assert_eq!(
756            report.plan.selected_tier,
757            FusedExecutorTier::TypedAcyclicJunction
758        );
759        assert_eq!(report.node_metrics.len(), 2);
760        assert_eq!(report.node_metrics[0].elements_in, 3);
761        assert_eq!(report.node_metrics[0].elements_out, 6);
762        assert_eq!(report.node_metrics[1].elements_in, 6);
763        assert_eq!(report.node_metrics[1].elements_out, 3);
764    }
765
766    #[test]
767    fn stalls_level_observes_bounded_zip_wait_and_queue_depth() {
768        let graph = GraphDsl::create(|builder| builder.add(Zip::<i32, i32>::new()))
769            .unwrap()
770            .add_attributes(Attributes::metrics_level(MetricsLevel::Stalls));
771
772        let report = graph
773            .run_zip_report(vec![1], vec![2], FusedExecutionConfig::default())
774            .unwrap();
775        assert_eq!(report.node_metrics.len(), 1);
776        let metrics = &report.node_metrics[0];
777        assert_eq!(metrics.elements_in, 2);
778        assert_eq!(metrics.elements_out, 1);
779        assert!(!metrics.processing_time.is_zero());
780        assert!(!metrics.backpressure_stall_time.is_zero());
781        assert_eq!(metrics.queue_depth, Some(1));
782    }
783
784    #[test]
785    fn typed_and_mutable_builders_report_equivalent_metrics() {
786        let current = GraphDsl::create(|builder| builder.add(Identity::<i32>::new()))
787            .unwrap()
788            .add_attributes(Attributes::metrics_level(MetricsLevel::Counts));
789        let typed = GraphDsl::typed_try_create(|builder| {
790            let (builder, inlet, outlet) = builder.add_flow(Identity::<i32>::new());
791            Ok((
792                builder,
793                FlowShape::new(inlet.into_port(), outlet.into_port()),
794            ))
795        })
796        .unwrap()
797        .add_attributes(Attributes::metrics_level(MetricsLevel::Counts));
798
799        let current = current
800            .run_with_input_report([1, 2], FusedExecutionConfig::default())
801            .unwrap();
802        let typed = typed
803            .run_with_input_report([1, 2], FusedExecutionConfig::default())
804            .unwrap();
805        assert_eq!(current.node_metrics, typed.node_metrics);
806    }
807
808    #[test]
809    fn fusion_erased_only_forces_erased_tier_and_reports_reason() {
810        let graph = GraphDsl::create(|builder| builder.add(Identity::<i32>::new()))
811            .unwrap()
812            .add_attributes(Attributes::fusion(Fusion::ErasedOnly));
813
814        let report = graph
815            .run_with_input_report([1, 2], FusedExecutionConfig::default())
816            .unwrap();
817
818        assert_eq!(report.output, vec![1, 2]);
819        assert_eq!(report.plan.selected_tier, FusedExecutorTier::Erased);
820        assert_eq!(report.plan.tier_changes.len(), 1);
821        let change = &report.plan.tier_changes[0];
822        assert_eq!(
823            change.requested_attribute,
824            Some(Attribute::Fusion(Fusion::ErasedOnly))
825        );
826        assert_eq!(
827            change.reason,
828            FusedTierChangeReason::FusionErasedOnlyRequested
829        );
830        assert_eq!(change.selected_tier, FusedExecutorTier::Erased);
831        assert!(!change.affected_nodes.is_empty());
832    }
833
834    #[test]
835    fn graph_dsl_rejects_invalid_and_incomplete_wiring() {
836        let graph = GraphDsl::try_create(|builder| {
837            let first = builder.add(Identity::<i32>::new());
838            let second = builder.add(Identity::<i32>::new());
839            let third = builder.add(Identity::<i32>::new());
840
841            builder.connect(first.outlet(), second.inlet())?;
842            let duplicate = builder.connect(first.outlet(), third.inlet());
843            assert!(matches!(duplicate, Err(StreamError::GraphValidation(_))));
844
845            Ok(FlowShape::new(first.inlet(), second.outlet()))
846        });
847
848        assert!(matches!(graph, Err(StreamError::GraphValidation(_))));
849
850        let graph = GraphDsl::create(|builder| {
851            let broadcast = builder.add(Broadcast::<i32>::new(2));
852            SourceShape::new(broadcast.outlet(0).unwrap())
853        });
854        assert!(matches!(graph, Err(StreamError::GraphValidation(_))));
855    }
856
857    #[test]
858    fn graph_dsl_rejects_erased_type_mismatch() {
859        let graph = GraphDsl::try_create(|builder| {
860            let left = builder.add(Identity::<i32>::new());
861            let right = builder.add(Identity::<u64>::new());
862            let mismatch = builder.connect_any(left.outlet().erase(), right.inlet().erase());
863            assert!(matches!(mismatch, Err(StreamError::GraphValidation(_))));
864            Ok(FlowShape::new(left.inlet(), right.outlet()))
865        });
866
867        assert!(matches!(graph, Err(StreamError::GraphValidation(_))));
868    }
869
870    #[test]
871    fn graph_dsl_rejects_result_ports_with_spoofed_metadata() {
872        let graph = GraphDsl::create(|builder| {
873            let shape = builder.add(Identity::<i32>::new());
874            FlowShape::new(
875                Inlet::<u64>::with_id(shape.inlet().id(), "Identity.in"),
876                shape.outlet(),
877            )
878        });
879
880        assert!(matches!(graph, Err(StreamError::GraphValidation(_))));
881    }
882
883    #[test]
884    fn allocated_ports_do_not_collide_with_manual_ports() {
885        let manual = Inlet::<i32>::new("manual");
886        let graph = GraphDsl::try_create(|builder| {
887            let shape = builder.add(Identity::<i32>::new());
888            assert_ne!(manual.id(), shape.inlet().id());
889            Ok(shape)
890        });
891
892        assert!(graph.is_ok());
893    }
894
895    #[test]
896    fn identity_ports_keep_static_names_and_unique_ids() {
897        let graph = GraphDsl::try_create(|builder| {
898            let first = builder.add(Identity::<i32>::new());
899            let second = builder.add(Identity::<i32>::new());
900
901            assert_eq!(first.inlet().name(), "Identity.in");
902            assert_eq!(first.outlet().name(), "Identity.out");
903            assert_eq!(second.inlet().name(), "Identity.in");
904            assert_eq!(second.outlet().name(), "Identity.out");
905            assert_ne!(first.inlet().id(), first.outlet().id());
906            assert_ne!(first.outlet().id(), second.inlet().id());
907            assert_ne!(second.inlet().id(), second.outlet().id());
908
909            builder.connect(first.outlet(), second.inlet())?;
910            Ok(FlowShape::new(first.inlet(), second.outlet()))
911        });
912
913        assert!(graph.is_ok());
914    }
915
916    #[test]
917    fn graph_stage_logic_checks_port_operations() {
918        let shape = FlowShape::new(Inlet::<i32>::new("in"), Outlet::<i32>::new("out"));
919        let inlet = shape.inlet();
920        let outlet = shape.outlet();
921        let mut logic = GraphStageLogic::new(&shape);
922
923        assert!(matches!(
924            logic.grab(&inlet),
925            Err(StreamError::InvalidPortOperation { .. })
926        ));
927        logic.pull(&inlet).unwrap();
928        assert!(logic.has_been_pulled(&inlet));
929        assert!(matches!(
930            logic.pull(&inlet),
931            Err(StreamError::InvalidPortOperation { .. })
932        ));
933        logic.offer(&inlet, 41).unwrap();
934        assert!(!logic.has_been_pulled(&inlet));
935        assert_eq!(logic.grab(&inlet).unwrap(), 41);
936
937        assert!(matches!(
938            logic.push(&outlet, 1),
939            Err(StreamError::InvalidPortOperation { .. })
940        ));
941        logic.request(&outlet).unwrap();
942        assert!(logic.is_available(&outlet));
943        logic.push(&outlet, 42).unwrap();
944        assert!(!logic.is_available(&outlet));
945        logic.complete(&outlet).unwrap();
946        assert!(logic.is_closed(&outlet));
947        assert!(matches!(
948            logic.request(&outlet),
949            Err(StreamError::InvalidPortOperation { .. })
950        ));
951    }
952
953    #[test]
954    fn fused_execution_enforces_event_limit() {
955        let graph = GraphDsl::create(|builder| builder.add(Identity::<i32>::new())).unwrap();
956
957        let result = graph.run_with_input_report([1, 2], FusedExecutionConfig { event_limit: 1 });
958
959        assert_eq!(result, Err(StreamError::EventLimitExceeded { limit: 1 }));
960    }
961
962    #[test]
963    fn async_boundary_splits_fused_segments() {
964        let graph = GraphDsl::try_create(|builder| {
965            let first = builder.add(Identity::<i32>::new());
966            let boundary = builder.add(AsyncBoundary::<i32>::new());
967            let second = builder.add(MapStage::new(|item: i32| item + 1));
968
969            builder.connect(first.outlet(), boundary.inlet())?;
970            builder.connect(boundary.outlet(), second.inlet())?;
971
972            Ok(FlowShape::new(first.inlet(), second.outlet()))
973        })
974        .unwrap();
975
976        assert_eq!(graph.segments().len(), 3);
977        let report = graph
978            .run_with_input_report([1, 2], FusedExecutionConfig::default())
979            .unwrap();
980        assert_eq!(report.output, vec![2, 3]);
981        assert_eq!(report.async_boundary_crossings, 2);
982    }
983
984    #[test]
985    fn async_boundary_count_path_uses_ractor_handoff_segments() {
986        let graph = GraphDsl::try_create(|builder| {
987            let first = builder.add(MapStage::new(|item: i32| item + 1));
988            let boundary = builder.add(AsyncBoundary::<i32>::new());
989            let second = builder.add(MapStage::new(|item: i32| item * 2));
990
991            builder.connect(first.outlet(), boundary.inlet())?;
992            builder.connect(boundary.outlet(), second.inlet())?;
993
994            Ok(FlowShape::new(first.inlet(), second.outlet()))
995        })
996        .unwrap();
997
998        let report = graph
999            .run_async_boundary_count_with_input_report(
1000                [1, 2, 3],
1001                AsyncBoundaryExecutionConfig {
1002                    fused: FusedExecutionConfig::default(),
1003                    buffer_size: 2,
1004                },
1005            )
1006            .unwrap();
1007
1008        assert_eq!(report.result, 3);
1009        assert_eq!(report.async_boundary_crossings, 3);
1010        assert_eq!(report.events, 18);
1011    }
1012
1013    #[test]
1014    fn threaded_async_boundary_baseline_matches_ractor_count_path() {
1015        let graph = GraphDsl::try_create(|builder| {
1016            let first = builder.add(MapStage::new(|item: i32| item + 1));
1017            let boundary = builder.add(AsyncBoundary::<i32>::new());
1018            let second = builder.add(MapStage::new(|item: i32| item * 2));
1019
1020            builder.connect(first.outlet(), boundary.inlet())?;
1021            builder.connect(boundary.outlet(), second.inlet())?;
1022
1023            Ok(FlowShape::new(first.inlet(), second.outlet()))
1024        })
1025        .unwrap();
1026
1027        let config = AsyncBoundaryExecutionConfig {
1028            fused: FusedExecutionConfig::default(),
1029            buffer_size: 2,
1030        };
1031        let ractor_report = graph
1032            .run_async_boundary_count_with_input_report([1, 2, 3], config)
1033            .unwrap();
1034        let threaded_report = BoundaryCountExecutor::Threaded
1035            .run_count(
1036                [1, 2, 3],
1037                graph.typed_linear_async_segments().unwrap(),
1038                config,
1039            )
1040            .unwrap();
1041
1042        assert_eq!(threaded_report.result, ractor_report.result);
1043        assert_eq!(threaded_report.events, ractor_report.events);
1044        assert_eq!(
1045            threaded_report.async_boundary_crossings,
1046            ractor_report.async_boundary_crossings
1047        );
1048    }
1049
1050    #[test]
1051    fn ractor_async_boundary_rejects_zero_buffer_size() {
1052        let graph = GraphDsl::try_create(|builder| {
1053            let first = builder.add(MapStage::new(|item: i32| item + 1));
1054            let boundary = builder.add(AsyncBoundary::<i32>::new());
1055            let second = builder.add(MapStage::new(|item: i32| item * 2));
1056
1057            builder.connect(first.outlet(), boundary.inlet())?;
1058            builder.connect(boundary.outlet(), second.inlet())?;
1059
1060            Ok(FlowShape::new(first.inlet(), second.outlet()))
1061        })
1062        .unwrap();
1063
1064        let result = BoundaryCountExecutor::Ractor.run_count(
1065            [1, 2, 3],
1066            graph.typed_linear_async_segments().unwrap(),
1067            AsyncBoundaryExecutionConfig {
1068                fused: FusedExecutionConfig::default(),
1069                buffer_size: 0,
1070            },
1071        );
1072
1073        assert!(matches!(
1074            result,
1075            Err(StreamError::GraphValidation(message)) if message.contains("buffer_size")
1076        ));
1077    }
1078
1079    #[test]
1080    fn ractor_async_boundary_streams_input_without_eager_collection() {
1081        let graph = GraphDsl::try_create(|builder| {
1082            let first = builder.add(MapStage::new(|item: i32| item + 1));
1083            let boundary = builder.add(AsyncBoundary::<i32>::new());
1084            let second = builder.add(MapStage::new(|item: i32| item * 2));
1085
1086            builder.connect(first.outlet(), boundary.inlet())?;
1087            builder.connect(boundary.outlet(), second.inlet())?;
1088
1089            Ok(FlowShape::new(first.inlet(), second.outlet()))
1090        })
1091        .unwrap();
1092
1093        let result = BoundaryCountExecutor::Ractor.run_count(
1094            std::iter::repeat(1_i32),
1095            graph.typed_linear_async_segments().unwrap(),
1096            AsyncBoundaryExecutionConfig {
1097                fused: FusedExecutionConfig { event_limit: 4 },
1098                buffer_size: 1,
1099            },
1100        );
1101
1102        assert_eq!(result, Err(StreamError::EventLimitExceeded { limit: 4 }));
1103    }
1104
1105    #[test]
1106    fn ractor_async_boundary_runs_inside_existing_tokio_runtime() {
1107        let graph = GraphDsl::try_create(|builder| {
1108            let first = builder.add(MapStage::new(|item: i32| item + 1));
1109            let boundary = builder.add(AsyncBoundary::<i32>::new());
1110            let second = builder.add(MapStage::new(|item: i32| item * 2));
1111
1112            builder.connect(first.outlet(), boundary.inlet())?;
1113            builder.connect(boundary.outlet(), second.inlet())?;
1114
1115            Ok(FlowShape::new(first.inlet(), second.outlet()))
1116        })
1117        .unwrap();
1118
1119        let runtime = tokio::runtime::Builder::new_current_thread()
1120            .build()
1121            .unwrap();
1122        let report = runtime
1123            .block_on(async {
1124                BoundaryCountExecutor::Ractor.run_count(
1125                    [1, 2, 3],
1126                    graph.typed_linear_async_segments().unwrap(),
1127                    AsyncBoundaryExecutionConfig {
1128                        fused: FusedExecutionConfig::default(),
1129                        buffer_size: 2,
1130                    },
1131                )
1132            })
1133            .unwrap();
1134
1135        assert_eq!(report.result, 3);
1136        assert_eq!(report.async_boundary_crossings, 3);
1137        assert_eq!(report.events, 18);
1138    }
1139
1140    #[test]
1141    fn balance_merge_round_robins_through_junctions() {
1142        let graph = GraphDsl::try_create(|builder| {
1143            let balance = builder.add(Balance::<i32>::new(2));
1144            let merge = builder.add(Merge::<i32>::new(2));
1145
1146            builder.connect(balance.outlet(0)?, merge.inlet(0)?)?;
1147            builder.connect(balance.outlet(1)?, merge.inlet(1)?)?;
1148
1149            Ok(FlowShape::new(balance.inlet(), merge.outlet()))
1150        })
1151        .unwrap();
1152
1153        assert_eq!(graph.run_with_input(0..6).unwrap(), vec![0, 1, 2, 3, 4, 5]);
1154    }
1155
1156    #[test]
1157    fn prioritized_merge_uses_weighted_schedule() {
1158        let graph =
1159            GraphDsl::create(|builder| builder.add(MergePrioritized::<i32>::new(vec![2, 1])))
1160                .unwrap();
1161
1162        assert_eq!(
1163            graph
1164                .run_fan_in(vec![vec![1, 2, 3, 4], vec![100, 101]])
1165                .unwrap(),
1166            vec![1, 2, 100, 3, 4, 101]
1167        );
1168    }
1169
1170    #[test]
1171    fn fused_execution_supports_count_and_fold_sinks() {
1172        let graph = GraphDsl::try_create(|builder| {
1173            let first = builder.add(MapStage::new(|item: u64| item + 1));
1174            let second = builder.add(MapStage::new(|item: u64| item * 2));
1175
1176            builder.connect(first.outlet(), second.inlet())?;
1177
1178            Ok(FlowShape::new(first.inlet(), second.outlet()))
1179        })
1180        .unwrap();
1181
1182        assert_eq!(graph.run_count_with_input(0..4).unwrap(), 4);
1183        assert_eq!(
1184            graph
1185                .run_fold_with_input(0..4, 0, |acc, item| acc + item)
1186                .unwrap(),
1187            20
1188        );
1189    }
1190
1191    #[test]
1192    fn typed_linear_fast_path_runs_same_type_chains() {
1193        let graph = GraphDsl::try_create(|builder| {
1194            let first = builder.add(MapStage::new(|item: u64| item + 1));
1195            let second = builder.add(MapStage::new(|item: u64| item * 2));
1196
1197            builder.connect(first.outlet(), second.inlet())?;
1198
1199            Ok(FlowShape::new(first.inlet(), second.outlet()))
1200        })
1201        .unwrap();
1202
1203        let report = graph
1204            .run_typed_linear_with_input_report([1, 2, 3], FusedExecutionConfig::default())
1205            .unwrap();
1206        assert_eq!(report.output, vec![4, 6, 8]);
1207        assert_eq!(report.events, 12);
1208
1209        assert_eq!(graph.run_typed_linear_count_with_input(0..4).unwrap(), 4);
1210        assert_eq!(
1211            graph
1212                .run_typed_linear_fold_with_input(0..4, 0, |acc, item| acc + item)
1213                .unwrap(),
1214            20
1215        );
1216    }
1217
1218    #[test]
1219    fn typed_linear_fast_path_rejects_junction_graphs() {
1220        let graph = GraphDsl::try_create(|builder| {
1221            let balance = builder.add(Balance::<i32>::new(2));
1222            let merge = builder.add(Merge::<i32>::new(2));
1223
1224            builder.connect(balance.outlet(0)?, merge.inlet(0)?)?;
1225            builder.connect(balance.outlet(1)?, merge.inlet(1)?)?;
1226
1227            Ok(FlowShape::new(balance.inlet(), merge.outlet()))
1228        })
1229        .unwrap();
1230
1231        assert!(matches!(
1232            graph.run_typed_linear_count_with_input([1, 2, 3]),
1233            Err(StreamError::GraphValidation(_))
1234        ));
1235    }
1236
1237    #[test]
1238    fn merge_preferred_drains_preferred_before_secondaries() {
1239        let graph = GraphDsl::create(|builder| builder.add(MergePreferred::<i32>::new(2))).unwrap();
1240
1241        assert_eq!(
1242            graph
1243                .run_merge_preferred(vec![1, 2, 3], vec![vec![100, 101], vec![200]])
1244                .unwrap(),
1245            vec![1, 2, 3, 100, 200, 101]
1246        );
1247    }
1248
1249    #[test]
1250    fn merge_waits_for_all_inputs_to_complete() {
1251        let graph = GraphDsl::create(|builder| builder.add(Merge::<i32>::new(2))).unwrap();
1252
1253        assert_eq!(
1254            graph.run_fan_in(vec![vec![], vec![10, 20]]).unwrap(),
1255            vec![10, 20]
1256        );
1257        assert_eq!(graph.run_fan_in(vec![vec![1], vec![]]).unwrap(), vec![1]);
1258    }
1259
1260    #[test]
1261    fn merge_sorted_drains_remaining_input_after_peer_completes() {
1262        let graph = GraphDsl::try_create(|builder| {
1263            let unzip = builder.add(Unzip::<i32, i32>::new());
1264            let merge = builder.add(MergeSorted::<i32>::new());
1265
1266            builder.connect(unzip.out0(), merge.inlet(0)?)?;
1267            builder.connect(unzip.out1(), merge.inlet(1)?)?;
1268
1269            Ok(FlowShape::new(unzip.inlet(), merge.outlet()))
1270        })
1271        .unwrap();
1272
1273        assert_eq!(
1274            graph.run_with_input([(1, 2), (4, 3), (6, 5)]).unwrap(),
1275            vec![1, 2, 3, 4, 5, 6]
1276        );
1277    }
1278
1279    #[test]
1280    fn merge_sequence_reorders_adversarial_arrivals_by_sequence_number() {
1281        let graph = GraphDsl::try_create(|builder| {
1282            let unzip = builder.add(Unzip::<u64, u64>::new());
1283            let merge = builder.add(MergeSequence::<u64>::new(2, |item| *item));
1284
1285            builder.connect(unzip.out0(), merge.inlet(0)?)?;
1286            builder.connect(unzip.out1(), merge.inlet(1)?)?;
1287
1288            Ok(FlowShape::new(unzip.inlet(), merge.outlet()))
1289        })
1290        .unwrap();
1291
1292        assert_eq!(
1293            graph.run_with_input([(0, 1), (2, 3), (4, 5)]).unwrap(),
1294            vec![0, 1, 2, 3, 4, 5]
1295        );
1296    }
1297
1298    #[test]
1299    fn merge_latest_emits_with_last_seen_peer_and_honors_eager_complete() {
1300        let graph = GraphDsl::try_create(|builder| {
1301            let unzip = builder.add(Unzip::<i32, i32>::new());
1302            let merge = builder.add(MergeLatest::<i32>::new(2, false));
1303
1304            builder.connect(unzip.out0(), merge.inlet(0)?)?;
1305            builder.connect(unzip.out1(), merge.inlet(1)?)?;
1306
1307            Ok(FlowShape::new(unzip.inlet(), merge.outlet()))
1308        })
1309        .unwrap();
1310
1311        assert_eq!(
1312            graph.run_with_input([(1, 10), (2, 11)]).unwrap(),
1313            vec![vec![1, 10], vec![2, 10], vec![2, 11]]
1314        );
1315    }
1316
1317    #[test]
1318    fn zip_completes_when_any_input_completes() {
1319        let graph = GraphDsl::create(|builder| builder.add(Zip::<i32, i32>::new())).unwrap();
1320
1321        assert_eq!(
1322            graph.run_zip(vec![1, 2, 3], vec![10]).unwrap(),
1323            vec![(1, 10)]
1324        );
1325        assert_eq!(graph.run_zip(vec![1], vec![10, 20]).unwrap(), vec![(1, 10)]);
1326        assert_eq!(
1327            graph.run_zip(vec![], vec![10, 20]).unwrap(),
1328            Vec::<(i32, i32)>::new()
1329        );
1330    }
1331
1332    #[test]
1333    fn concat_drains_inputs_in_declared_order() {
1334        let graph = GraphDsl::create(|builder| builder.add(Concat::<i32>::new(3))).unwrap();
1335
1336        assert_eq!(
1337            graph
1338                .run_concat(vec![vec![1, 2], vec![], vec![3, 4]])
1339                .unwrap(),
1340            vec![1, 2, 3, 4]
1341        );
1342    }
1343
1344    #[test]
1345    fn or_else_switches_only_if_primary_is_empty() {
1346        let graph = GraphDsl::create(|builder| builder.add(OrElse::<i32>::new())).unwrap();
1347
1348        assert_eq!(
1349            graph.run_or_else(vec![], vec![10, 20]).unwrap(),
1350            vec![10, 20]
1351        );
1352        assert_eq!(
1353            graph.run_or_else(vec![1, 2], vec![10, 20]).unwrap(),
1354            vec![1, 2]
1355        );
1356    }
1357
1358    #[test]
1359    fn or_else_secondary_first_dropped_when_primary_emits() {
1360        let graph = GraphDsl::create(|builder| builder.add(OrElse::<i32>::new())).unwrap();
1361
1362        assert_eq!(
1363            graph
1364                .run_or_else_secondary_first(vec![1, 2], vec![10, 20])
1365                .unwrap(),
1366            vec![1, 2]
1367        );
1368    }
1369
1370    #[test]
1371    fn or_else_secondary_first_flushed_when_primary_empty() {
1372        let graph = GraphDsl::create(|builder| builder.add(OrElse::<i32>::new())).unwrap();
1373
1374        assert_eq!(
1375            graph
1376                .run_or_else_secondary_first(vec![], vec![10, 20])
1377                .unwrap(),
1378            vec![10, 20]
1379        );
1380    }
1381
1382    #[test]
1383    fn or_else_secondary_closed_then_primary_empty_drains_buffer() {
1384        let graph = GraphDsl::create(|builder| builder.add(OrElse::<i32>::new())).unwrap();
1385
1386        assert_eq!(
1387            graph
1388                .run_or_else_secondary_closed_first(vec![10, 20])
1389                .unwrap(),
1390            vec![10, 20]
1391        );
1392    }
1393
1394    #[test]
1395    fn interleave_cycles_segment_sized_chunks() {
1396        let graph = GraphDsl::create(|builder| builder.add(Interleave::<i32>::new(3, 2))).unwrap();
1397
1398        assert_eq!(
1399            graph
1400                .run_interleave(vec![vec![1, 2, 3], vec![10, 11, 12], vec![20]], 2, false)
1401                .unwrap(),
1402            vec![1, 2, 10, 11, 20, 3, 12]
1403        );
1404    }
1405
1406    #[test]
1407    fn interleave_eager_close_stops_when_any_input_completes() {
1408        let graph = GraphDsl::create(|builder| {
1409            builder.add(Interleave::<i32>::new_with_eager_close(2, 1, true))
1410        })
1411        .unwrap();
1412
1413        assert_eq!(
1414            graph
1415                .run_interleave(vec![vec![1, 2], vec![]], 1, true)
1416                .unwrap(),
1417            Vec::<i32>::new()
1418        );
1419        assert_eq!(
1420            graph
1421                .run_interleave(vec![vec![1], vec![10, 11]], 1, true)
1422                .unwrap(),
1423            vec![1, 10]
1424        );
1425    }
1426
1427    #[test]
1428    fn partition_routes_only_live_outlets_after_peer_cancels() {
1429        let graph = GraphDsl::try_create(|builder| {
1430            let partition = builder.add(Partition::<i32>::new(2, |item| (*item % 2) as usize));
1431            let merge = builder.add(Merge::<i32>::new(2));
1432
1433            builder.connect(partition.outlet(0)?, merge.inlet(0)?)?;
1434            builder.connect(partition.outlet(1)?, merge.inlet(1)?)?;
1435
1436            Ok(FlowShape::new(partition.inlet(), merge.outlet()))
1437        })
1438        .unwrap();
1439
1440        assert_eq!(
1441            graph.run_with_input([0, 1, 2, 3]).unwrap(),
1442            vec![0, 1, 2, 3]
1443        );
1444    }
1445
1446    #[test]
1447    fn unzip_with_keeps_live_outlet_running_after_peer_finishes() {
1448        let graph = GraphDsl::try_create(|builder| {
1449            let unzip = builder.add(UnzipWith::<i32, i32, i32>::new(|item| (item, item * 10)));
1450            let zip = builder.add(Zip::<i32, i32>::new());
1451
1452            builder.connect(unzip.out0(), zip.in0())?;
1453            builder.connect(unzip.out1(), zip.in1())?;
1454
1455            Ok(FlowShape::new(unzip.inlet(), zip.outlet()))
1456        })
1457        .unwrap();
1458
1459        assert_eq!(
1460            graph.run_with_input([1, 2, 3]).unwrap(),
1461            vec![(1, 10), (2, 20), (3, 30)]
1462        );
1463    }
1464
1465    // --- Regression tests pinning WP-17a review-comment fixes ---
1466
1467    // Fix 1a: UnzipFanInFastPath preserves target inlet order for MergeSorted.
1468    // out0 is wired to inlet(1) and out1 to inlet(0) — values must still be
1469    // sorted correctly regardless of swapped wiring.
1470    #[test]
1471    fn unzip_merge_sorted_swapped_inlets_still_sorted() {
1472        let graph_normal = GraphDsl::try_create(|builder| {
1473            let unzip = builder.add(Unzip::<i32, i32>::new());
1474            let merge = builder.add(MergeSorted::<i32>::new());
1475            builder.connect(unzip.out0(), merge.inlet(0)?)?;
1476            builder.connect(unzip.out1(), merge.inlet(1)?)?;
1477            Ok(FlowShape::new(unzip.inlet(), merge.outlet()))
1478        })
1479        .unwrap();
1480
1481        // Swapped: out0 → inlet(1), out1 → inlet(0).
1482        let graph_swapped = GraphDsl::try_create(|builder| {
1483            let unzip = builder.add(Unzip::<i32, i32>::new());
1484            let merge = builder.add(MergeSorted::<i32>::new());
1485            builder.connect(unzip.out0(), merge.inlet(1)?)?;
1486            builder.connect(unzip.out1(), merge.inlet(0)?)?;
1487            Ok(FlowShape::new(unzip.inlet(), merge.outlet()))
1488        })
1489        .unwrap();
1490
1491        let input = vec![(1, 2), (4, 3), (6, 5)];
1492        let expected = vec![1, 2, 3, 4, 5, 6];
1493        assert_eq!(
1494            graph_normal.run_with_input(input.clone()).unwrap(),
1495            expected
1496        );
1497        assert_eq!(graph_swapped.run_with_input(input).unwrap(), expected);
1498    }
1499
1500    // Fix 1b: UnzipFanInFastPath preserves target inlet order for MergeLatest.
1501    // With swapped wiring the snapshot elements must reflect which inlet each
1502    // value was routed to, producing the same result as the non-swapped graph.
1503    #[test]
1504    fn unzip_merge_latest_swapped_inlets_correct_snapshot_order() {
1505        // Normal wiring: out0 → inlet(0), out1 → inlet(1).
1506        // Input (1, 10): latest = [Some(1), Some(10)] → snapshot [1, 10].
1507        let graph_normal = GraphDsl::try_create(|builder| {
1508            let unzip = builder.add(Unzip::<i32, i32>::new());
1509            let merge = builder.add(MergeLatest::<i32>::new(2, false));
1510            builder.connect(unzip.out0(), merge.inlet(0)?)?;
1511            builder.connect(unzip.out1(), merge.inlet(1)?)?;
1512            Ok(FlowShape::new(unzip.inlet(), merge.outlet()))
1513        })
1514        .unwrap();
1515
1516        // Swapped: out0 → inlet(1), out1 → inlet(0).
1517        // Input (1, 10): out0=1 goes to inlet(1), out1=10 goes to inlet(0).
1518        // latest = [Some(10), Some(1)] → snapshot [10, 1].
1519        let graph_swapped = GraphDsl::try_create(|builder| {
1520            let unzip = builder.add(Unzip::<i32, i32>::new());
1521            let merge = builder.add(MergeLatest::<i32>::new(2, false));
1522            builder.connect(unzip.out0(), merge.inlet(1)?)?;
1523            builder.connect(unzip.out1(), merge.inlet(0)?)?;
1524            Ok(FlowShape::new(unzip.inlet(), merge.outlet()))
1525        })
1526        .unwrap();
1527
1528        assert_eq!(
1529            graph_normal.run_with_input([(1, 10)]).unwrap(),
1530            vec![vec![1, 10]]
1531        );
1532        // Swapped wiring routes values to opposite slots; snapshot order differs.
1533        assert_eq!(
1534            graph_swapped.run_with_input([(1, 10)]).unwrap(),
1535            vec![vec![10, 1]]
1536        );
1537    }
1538
1539    // Fix 2: MergeSequence fails on a sequence gap when all inputs complete.
1540    // The GraphStage logic errors if `pending` holds items but next_sequence
1541    // is not among them — the fused executor must mirror that behavior.
1542    #[test]
1543    fn merge_sequence_fails_on_gap_at_completion() {
1544        // Sequences 1 and 2 arrive (via the unzip split of pair (1, 2)), but
1545        // sequence 0 never does.  When the upstream completes both inlets,
1546        // next_sequence is still 0, there is no sequence 0 in pending → error.
1547        let graph = GraphDsl::try_create(|builder| {
1548            let unzip = builder.add(Unzip::<u64, u64>::new());
1549            let merge = builder.add(MergeSequence::<u64>::new(2, |item| *item));
1550            builder.connect(unzip.out0(), merge.inlet(0)?)?;
1551            builder.connect(unzip.out1(), merge.inlet(1)?)?;
1552            Ok(FlowShape::new(unzip.inlet(), merge.outlet()))
1553        })
1554        .unwrap();
1555
1556        let result = graph.run_with_input([(1u64, 2u64)]);
1557        assert!(
1558            matches!(result, Err(StreamError::Failed(ref msg)) if msg.contains("expected sequence")),
1559            "expected a sequence-gap error, got: {result:?}"
1560        );
1561    }
1562
1563    // Fix 3: MergeLatest honors eager_complete — stream ends as soon as any
1564    // inlet completes when eager_complete = true.
1565    #[test]
1566    fn merge_latest_eager_complete_closes_on_first_inlet_done() {
1567        // With eager_complete = true, the stage should complete when the first
1568        // input stream finishes (here the single-item upstream drains first).
1569        // The non-eager variant of the same graph would keep running until both
1570        // inlets complete.
1571        let graph_eager = GraphDsl::try_create(|builder| {
1572            let unzip = builder.add(Unzip::<i32, i32>::new());
1573            let merge = builder.add(MergeLatest::<i32>::new(2, true));
1574            builder.connect(unzip.out0(), merge.inlet(0)?)?;
1575            builder.connect(unzip.out1(), merge.inlet(1)?)?;
1576            Ok(FlowShape::new(unzip.inlet(), merge.outlet()))
1577        })
1578        .unwrap();
1579
1580        let graph_non_eager = GraphDsl::try_create(|builder| {
1581            let unzip = builder.add(Unzip::<i32, i32>::new());
1582            let merge = builder.add(MergeLatest::<i32>::new(2, false));
1583            builder.connect(unzip.out0(), merge.inlet(0)?)?;
1584            builder.connect(unzip.out1(), merge.inlet(1)?)?;
1585            Ok(FlowShape::new(unzip.inlet(), merge.outlet()))
1586        })
1587        .unwrap();
1588
1589        // Single item: after this item both outlets close simultaneously (the
1590        // Unzip upstream completes), so both eager and non-eager complete after
1591        // emitting the first snapshot.
1592        let result_eager = graph_eager.run_with_input([(1i32, 10i32)]).unwrap();
1593        let result_non_eager = graph_non_eager.run_with_input([(1i32, 10i32)]).unwrap();
1594
1595        // Both should produce at least one snapshot.
1596        assert!(!result_eager.is_empty(), "eager graph produced no output");
1597        assert!(
1598            !result_non_eager.is_empty(),
1599            "non-eager graph produced no output"
1600        );
1601
1602        // Produce the same result for a balanced input — the eager flag only
1603        // changes behavior when ONE inlet finishes before the other.
1604        assert_eq!(result_eager, result_non_eager);
1605    }
1606
1607    #[test]
1608    fn scalar_builtin_graph_matches_erased_oracle_and_uses_typed_linear() {
1609        let graph = GraphDsl::try_create(|builder| {
1610            let add = builder.add(MapStage::map_add(7));
1611            let subtract = builder.add(MapStage::map_subtract(3));
1612            builder.connect(add.outlet(), subtract.inlet())?;
1613            Ok(FlowShape::new(add.inlet(), subtract.outlet()))
1614        })
1615        .unwrap();
1616        let input = -600_i64..600;
1617        let auto = graph
1618            .run_with_input_report_mode(
1619                input.clone(),
1620                FusedExecutionConfig::default(),
1621                super::executor::ExecutorMode::Auto,
1622            )
1623            .unwrap();
1624        let erased = graph
1625            .run_with_input_report_mode(
1626                input,
1627                FusedExecutionConfig::default(),
1628                super::executor::ExecutorMode::ErasedOnly,
1629            )
1630            .unwrap();
1631        assert_eq!(auto.output, erased.output);
1632        assert_eq!(auto.plan.selected_tier, FusedExecutorTier::TypedLinear);
1633    }
1634
1635    #[test]
1636    fn scalar_builtin_graph_replays_whole_segment_at_first_failure() {
1637        let graph = GraphDsl::try_create(|builder| {
1638            let add = builder.add(MapStage::map_add(1));
1639            let subtract = builder.add(MapStage::map_subtract(10));
1640            builder.connect(add.outlet(), subtract.inlet())?;
1641            Ok(FlowShape::new(add.inlet(), subtract.outlet()))
1642        })
1643        .unwrap();
1644        let observed = Arc::new(Mutex::new(Vec::new()));
1645        let folded = Arc::clone(&observed);
1646        let result = graph.run_fold_with_input_report(
1647            [0, i64::MIN + 5, i64::MAX],
1648            (),
1649            move |(), value| {
1650                folded.lock().expect("fold output poisoned").push(value);
1651            },
1652            FusedExecutionConfig::default(),
1653        );
1654        assert_eq!(
1655            result,
1656            Err(StreamError::Failed(format!(
1657                "integer overflow in map_subtract: {} - 10",
1658                i64::MIN + 6
1659            )))
1660        );
1661        assert_eq!(*observed.lock().unwrap(), vec![-9]);
1662
1663        let erased = graph.run_with_input_report_mode(
1664            [0, i64::MIN + 5, i64::MAX],
1665            FusedExecutionConfig::default(),
1666            super::executor::ExecutorMode::ErasedOnly,
1667        );
1668        assert_eq!(
1669            erased.map(|report| report.output),
1670            result.map(|_| Vec::new())
1671        );
1672    }
1673
1674    #[test]
1675    fn scalar_builtin_graph_multiply_stays_checked_scalar() {
1676        let graph = GraphDsl::create(|builder| builder.add(MapStage::map_multiply(2))).unwrap();
1677        assert_eq!(
1678            graph.run_with_input([2_i64, i64::MAX]),
1679            Err(StreamError::Failed(format!(
1680                "integer overflow in map_multiply: {} * 2",
1681                i64::MAX
1682            )))
1683        );
1684    }
1685}