Skip to main content

datum/graph/
executor.rs

1//! The fused graph executor and every `GraphBlueprint::run_*` method.
2//!
3//! Four execution tiers, dispatched by `ExecutorMode::Auto` in
4//! `run_with_input_report_mode` (this is where a graph picks its path):
5//!
6//! 1. **Typed-linear fast path** (`try_typed_flow_plan` / `TypedFlowPlan` /
7//!    `TypedLinearPlan`) — monomorphized Identity/Map/AsyncBoundary chains, no
8//!    per-element `DatumValue` boxing.
9//! 2. **Typed acyclic junction kernels**
10//!    (`try_build_typed_acyclic_junction_dispatch`) — Broadcast→Zip,
11//!    Balance→Merge, Partition→Merge, Unzip→Zip/MergeSorted, plus the typed
12//!    `MergeSequence`/`MergeLatest` plans.
13//! 3. **Typed cyclic feedback kernel**
14//!    (`try_build_typed_cyclic_feedback_dispatch`) — the `MergePreferred→Broadcast`
15//!    loop, consuming `TypedCyclicOp` from `Buffer`/`TakeWhile`.
16//! 4. **Erased `FusedExecutor`** — the `Box<dyn DatumElement>` event-stack
17//!    interpreter; the correctness oracle and the fallback for anything the
18//!    typed tiers don't cover. Cyclic graphs use a bounded event stack so an
19//!    unproductive cycle surfaces `EventLimitExceeded` instead of hanging.
20//!
21//! Typed kernels build fresh state per run (no caching, no `Mutex` on the run
22//! path) so blueprint reuse and concurrent runs are independent. These paths are
23//! benchmark-gated — see this module's `AGENTS.md`, `CLAUDE.md`, and
24//! `roadmap/benchmarks/graph.md` before refactoring any of them.
25
26use super::*;
27use crate::stream::async_boundary::{
28    AsyncBoundaryMessage as AsyncLinearMessage, RactorBoundaryCommand, RactorBoundarySourceActor,
29    RactorBoundarySourceState, ractor_boundary_runtime,
30};
31
32// ---------------------------------------------------------------------------
33// ExecutorMode — Phase 0 scaffolding (WP-18)
34// ---------------------------------------------------------------------------
35
36/// Selects which executor is used to run a fused graph.
37///
38/// This is an internal test/diagnostic hook — it is **not** part of the public
39/// API and must not be exposed through `pub use` or any user-facing surface.
40///
41/// * `Auto` — default; tries the typed flow plan first, falls back to the
42///   erased executor for unsupported graphs.
43/// * `ErasedOnly` — always runs the existing erased (`Box<dyn DatumElement>`)
44///   executor regardless of graph shape.
45/// * `TypedOnly` — always tries the typed flow plan; returns
46///   `StreamError::GraphValidation("typed executor does not support this graph
47///   shape")` when the plan cannot be built.  **Test/diagnostic only.**
48#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
49pub(crate) enum ExecutorMode {
50    /// Try typed first; fall back to erased for unsupported shapes.
51    #[default]
52    Auto,
53    /// Always use the erased executor.
54    ErasedOnly,
55    /// Always use typed; error if unsupported.  Test/diagnostic only.
56    TypedOnly,
57}
58
59impl<In, Out> GraphBlueprint<FlowShape<In, Out>>
60where
61    In: Clone + Send + 'static,
62    Out: Send + 'static,
63{
64    pub fn run_with_input<I>(&self, input: I) -> StreamResult<Vec<Out>>
65    where
66        I: IntoIterator<Item = In>,
67    {
68        Ok(self
69            .run_with_input_report(input, FusedExecutionConfig::default())?
70            .output)
71    }
72
73    pub fn run_with_input_report<I>(
74        &self,
75        input: I,
76        config: FusedExecutionConfig,
77    ) -> StreamResult<FusedExecutionReport<Out>>
78    where
79        I: IntoIterator<Item = In>,
80    {
81        self.run_with_input_report_mode(input, config, ExecutorMode::Auto)
82    }
83
84    pub(crate) fn run_with_input_report_mode<I>(
85        &self,
86        input: I,
87        config: FusedExecutionConfig,
88        mode: ExecutorMode,
89    ) -> StreamResult<FusedExecutionReport<Out>>
90    where
91        I: IntoIterator<Item = In>,
92    {
93        // --- Typed path (Auto / TypedOnly) ---
94        if mode != ExecutorMode::ErasedOnly {
95            // Phase 2: linear typed flow plan (Identity/Map/AsyncBoundary chains).
96            let linear_plan = try_typed_flow_plan::<In, Out>(
97                &self.stages,
98                &self.edges,
99                self.shape.inlet().id(),
100                self.shape.outlet().id(),
101            );
102            if let Some(plan) = linear_plan {
103                let input = input.into_iter();
104                let mut output = Vec::with_capacity(input.size_hint().0);
105                let mut events = 0usize;
106                let mut async_boundary_crossings = 0usize;
107                for item in input {
108                    let out =
109                        plan.run_item(item, config, &mut events, &mut async_boundary_crossings)?;
110                    output.push(out);
111                }
112                return Ok(FusedExecutionReport {
113                    output,
114                    events,
115                    async_boundary_crossings,
116                });
117            }
118
119            // Phase 4: typed acyclic junction kernels for deterministic
120            // two-stage fan-out/fan-in topologies. These replace the erased
121            // executor's hand-written DatumValue fast paths in Auto mode.
122            let inlet_id = self.shape.inlet().id();
123            let outlet_id = self.shape.outlet().id();
124            if let Some(runner) = try_build_typed_acyclic_junction_dispatch::<In, Out>(
125                &self.stages,
126                &self.edges,
127                inlet_id,
128                outlet_id,
129            ) {
130                let mut input_iter = input.into_iter();
131                let output = runner(&mut input_iter)?;
132                return Ok(FusedExecutionReport {
133                    output,
134                    events: 0,
135                    async_boundary_crossings: 0,
136                });
137            }
138
139            // Phase 3a: typed MergeSequence kernel (Unzip → MergeSequence topology).
140            let ms_plan = try_typed_merge_sequence_plan::<In, Out>(
141                &self.stages,
142                &self.edges,
143                self.shape.inlet().id(),
144                self.shape.outlet().id(),
145            );
146            if let Some(mut plan) = ms_plan {
147                let output = run_typed_merge_sequence(&mut plan, input)?;
148                return Ok(FusedExecutionReport {
149                    output,
150                    events: 0,
151                    async_boundary_crossings: 0,
152                });
153            }
154
155            // Phase 3b: typed MergeLatest kernel (Unzip → MergeLatest topology).
156            //
157            // Build a FRESH TypedMergeLatestPlan per run — no cached mutable state,
158            // no Mutex.  Blueprint reuse and concurrent runs are fully independent.
159            // The per-run cost (topology/type checks, a few Arc clones, one small
160            // MergeLatestCore alloc, and one Box for the output Vec) is negligible
161            // vs a 10k-element run.
162            //
163            // try_build_typed_merge_latest_dispatch checks topology and element
164            // TypeId without consuming input; if it returns Some(runner), we then
165            // consume input for the typed path.  Custom `T` not in the bounded
166            // 17-type set returns None → falls back to the erased executor (Auto)
167            // or errors (TypedOnly).
168            if let Some(runner) = try_build_typed_merge_latest_dispatch::<In, Out>(
169                &self.stages,
170                &self.edges,
171                inlet_id,
172                outlet_id,
173            ) {
174                let mut input_iter = input.into_iter();
175                let output = runner(&mut input_iter)?;
176                return Ok(FusedExecutionReport {
177                    output,
178                    events: 0,
179                    async_boundary_crossings: 0,
180                });
181            }
182
183            // Typed cyclic feedback kernel: the common MergePreferred → Broadcast
184            // loop. Replaces the erased queued interpreter for this shape.
185            if let Some(runner) = try_build_typed_cyclic_feedback_dispatch::<In, Out>(
186                &self.stages,
187                &self.edges,
188                inlet_id,
189                outlet_id,
190            ) {
191                let mut input_iter = input.into_iter();
192                return runner(&mut input_iter, config);
193            }
194
195            if mode == ExecutorMode::TypedOnly {
196                return Err(StreamError::GraphValidation(
197                    "typed executor does not support this graph shape".into(),
198                ));
199            }
200        }
201
202        // --- Erased fallback ---
203        let input = input.into_iter();
204        let mut executor = FusedExecutor::new(self, config);
205        let inlet = self.shape.inlet().id();
206        let outlet = self.shape.outlet().id();
207        let mut output = Vec::with_capacity(input.size_hint().0);
208
209        {
210            let mut output_sink = VecOutputSink {
211                output: &mut output,
212            };
213            executor.request(outlet, outlet, &mut output_sink)?;
214            executor.drain_timer_events_nonblocking(outlet, &mut output_sink)?;
215            for item in input {
216                executor.deliver(inlet, datum(item), outlet, &mut output_sink)?;
217                executor.drain_timer_events_nonblocking(outlet, &mut output_sink)?;
218            }
219            executor.complete(inlet, outlet, &mut output_sink)?;
220            executor.drain_timer_events_until_idle(outlet, &mut output_sink)?;
221        }
222
223        Ok(FusedExecutionReport {
224            output,
225            events: executor.events,
226            async_boundary_crossings: executor.async_boundary_crossings,
227        })
228    }
229
230    pub fn run_count_with_input<I>(&self, input: I) -> StreamResult<usize>
231    where
232        I: IntoIterator<Item = In>,
233    {
234        Ok(self
235            .run_count_with_input_report(input, FusedExecutionConfig::default())?
236            .result)
237    }
238
239    pub fn run_count_with_input_report<I>(
240        &self,
241        input: I,
242        config: FusedExecutionConfig,
243    ) -> StreamResult<FusedTerminalReport<usize>>
244    where
245        I: IntoIterator<Item = In>,
246    {
247        self.run_count_with_input_report_mode(input, config, ExecutorMode::Auto)
248    }
249
250    pub(crate) fn run_count_with_input_report_mode<I>(
251        &self,
252        input: I,
253        config: FusedExecutionConfig,
254        mode: ExecutorMode,
255    ) -> StreamResult<FusedTerminalReport<usize>>
256    where
257        I: IntoIterator<Item = In>,
258    {
259        // --- Typed path (Auto / TypedOnly) ---
260        if mode != ExecutorMode::ErasedOnly {
261            let plan = try_typed_flow_plan::<In, Out>(
262                &self.stages,
263                &self.edges,
264                self.shape.inlet().id(),
265                self.shape.outlet().id(),
266            );
267            if let Some(plan) = plan {
268                let mut count = 0usize;
269                let mut events = 0usize;
270                let mut async_boundary_crossings = 0usize;
271                for item in input {
272                    plan.run_item_count(item, config, &mut events, &mut async_boundary_crossings)?;
273                    count += 1;
274                }
275                return Ok(FusedTerminalReport {
276                    result: count,
277                    events,
278                    async_boundary_crossings,
279                });
280            } else if mode == ExecutorMode::TypedOnly {
281                return Err(StreamError::GraphValidation(
282                    "typed executor does not support this graph shape".into(),
283                ));
284            }
285        }
286
287        // --- Erased fallback ---
288        let mut executor = FusedExecutor::new(self, config);
289        let inlet = self.shape.inlet().id();
290        let outlet = self.shape.outlet().id();
291        let mut output_sink = CountOutputSink { count: 0 };
292
293        executor.request::<Out>(outlet, outlet, &mut output_sink)?;
294        executor.drain_timer_events_nonblocking::<Out>(outlet, &mut output_sink)?;
295        for item in input {
296            executor.deliver::<Out>(inlet, datum(item), outlet, &mut output_sink)?;
297            executor.drain_timer_events_nonblocking::<Out>(outlet, &mut output_sink)?;
298        }
299        executor.complete::<Out>(inlet, outlet, &mut output_sink)?;
300        executor.drain_timer_events_until_idle::<Out>(outlet, &mut output_sink)?;
301
302        Ok(FusedTerminalReport {
303            result: output_sink.count,
304            events: executor.events,
305            async_boundary_crossings: executor.async_boundary_crossings,
306        })
307    }
308
309    pub fn run_fold_with_input<I, Acc, F>(&self, input: I, zero: Acc, fold: F) -> StreamResult<Acc>
310    where
311        I: IntoIterator<Item = In>,
312        F: FnMut(Acc, Out) -> Acc,
313    {
314        Ok(self
315            .run_fold_with_input_report(input, zero, fold, FusedExecutionConfig::default())?
316            .result)
317    }
318
319    pub fn run_fold_with_input_report<I, Acc, F>(
320        &self,
321        input: I,
322        zero: Acc,
323        fold: F,
324        config: FusedExecutionConfig,
325    ) -> StreamResult<FusedTerminalReport<Acc>>
326    where
327        I: IntoIterator<Item = In>,
328        F: FnMut(Acc, Out) -> Acc,
329    {
330        self.run_fold_with_input_report_mode(input, zero, fold, config, ExecutorMode::Auto)
331    }
332
333    pub(crate) fn run_fold_with_input_report_mode<I, Acc, F>(
334        &self,
335        input: I,
336        zero: Acc,
337        mut fold: F,
338        config: FusedExecutionConfig,
339        mode: ExecutorMode,
340    ) -> StreamResult<FusedTerminalReport<Acc>>
341    where
342        I: IntoIterator<Item = In>,
343        F: FnMut(Acc, Out) -> Acc,
344    {
345        // --- Typed path (Auto / TypedOnly) ---
346        if mode != ExecutorMode::ErasedOnly {
347            let plan = try_typed_flow_plan::<In, Out>(
348                &self.stages,
349                &self.edges,
350                self.shape.inlet().id(),
351                self.shape.outlet().id(),
352            );
353            if let Some(plan) = plan {
354                let mut accumulator = zero;
355                let mut events = 0usize;
356                let mut async_boundary_crossings = 0usize;
357                for item in input {
358                    let out =
359                        plan.run_item(item, config, &mut events, &mut async_boundary_crossings)?;
360                    accumulator = fold(accumulator, out);
361                }
362                return Ok(FusedTerminalReport {
363                    result: accumulator,
364                    events,
365                    async_boundary_crossings,
366                });
367            } else if mode == ExecutorMode::TypedOnly {
368                return Err(StreamError::GraphValidation(
369                    "typed executor does not support this graph shape".into(),
370                ));
371            }
372        }
373
374        // --- Erased fallback ---
375        let mut executor = FusedExecutor::new(self, config);
376        let inlet = self.shape.inlet().id();
377        let outlet = self.shape.outlet().id();
378        let mut output_sink = FoldOutputSink {
379            accumulator: Some(zero),
380            fold,
381        };
382
383        executor.request(outlet, outlet, &mut output_sink)?;
384        executor.drain_timer_events_nonblocking(outlet, &mut output_sink)?;
385        for item in input {
386            executor.deliver(inlet, datum(item), outlet, &mut output_sink)?;
387            executor.drain_timer_events_nonblocking(outlet, &mut output_sink)?;
388        }
389        executor.complete(inlet, outlet, &mut output_sink)?;
390        executor.drain_timer_events_until_idle(outlet, &mut output_sink)?;
391
392        Ok(FusedTerminalReport {
393            result: output_sink.finish(),
394            events: executor.events,
395            async_boundary_crossings: executor.async_boundary_crossings,
396        })
397    }
398
399    // -- Phase 0/Phase 2 mode-aware entry points (pub(crate) / test-hook) ---
400
401    /// Like `run_with_input` but dispatches based on [`ExecutorMode`].
402    ///
403    /// * `Auto` — tries the typed flow plan; falls back to erased if unsupported.
404    /// * `ErasedOnly` — always uses the erased executor.
405    /// * `TypedOnly` — errors if the typed plan cannot be built.
406    ///
407    /// This method is `pub(crate)` and exercised in `#[cfg(test)]` modules.
408    #[cfg_attr(not(test), allow(dead_code))]
409    pub(crate) fn run_with_input_mode<I>(
410        &self,
411        input: I,
412        mode: ExecutorMode,
413    ) -> StreamResult<Vec<Out>>
414    where
415        I: IntoIterator<Item = In>,
416    {
417        Ok(self
418            .run_with_input_report_mode(input, FusedExecutionConfig::default(), mode)?
419            .output)
420    }
421
422    /// Like `run_count_with_input` but dispatches based on [`ExecutorMode`].
423    ///
424    /// `pub(crate)` test/diagnostic hook.
425    #[allow(dead_code)]
426    pub(crate) fn run_count_with_input_mode<I>(
427        &self,
428        input: I,
429        mode: ExecutorMode,
430    ) -> StreamResult<usize>
431    where
432        I: IntoIterator<Item = In>,
433    {
434        Ok(self
435            .run_count_with_input_report_mode(input, FusedExecutionConfig::default(), mode)?
436            .result)
437    }
438
439    /// Like `run_fold_with_input` but dispatches based on [`ExecutorMode`].
440    ///
441    /// `pub(crate)` test/diagnostic hook.
442    #[allow(dead_code)]
443    pub(crate) fn run_fold_with_input_mode<I, Acc, F>(
444        &self,
445        input: I,
446        zero: Acc,
447        fold: F,
448        mode: ExecutorMode,
449    ) -> StreamResult<Acc>
450    where
451        I: IntoIterator<Item = In>,
452        F: FnMut(Acc, Out) -> Acc,
453    {
454        Ok(self
455            .run_fold_with_input_report_mode(
456                input,
457                zero,
458                fold,
459                FusedExecutionConfig::default(),
460                mode,
461            )?
462            .result)
463    }
464}
465
466impl<T> GraphBlueprint<FlowShape<T, T>>
467where
468    T: Send + 'static,
469{
470    pub fn run_typed_linear_with_input<I>(&self, input: I) -> StreamResult<Vec<T>>
471    where
472        I: IntoIterator<Item = T>,
473    {
474        Ok(self
475            .run_typed_linear_with_input_report(input, FusedExecutionConfig::default())?
476            .output)
477    }
478
479    pub fn run_typed_linear_with_input_report<I>(
480        &self,
481        input: I,
482        config: FusedExecutionConfig,
483    ) -> StreamResult<FusedExecutionReport<T>>
484    where
485        I: IntoIterator<Item = T>,
486    {
487        let input = input.into_iter();
488        let plan = self.typed_linear_plan()?;
489        let mut output = Vec::with_capacity(input.size_hint().0);
490        let mut events = 0;
491        let mut async_boundary_crossings = 0;
492
493        for item in input {
494            let item = plan.run_item(item, config, &mut events, &mut async_boundary_crossings)?;
495            output.push(item);
496        }
497
498        Ok(FusedExecutionReport {
499            output,
500            events,
501            async_boundary_crossings,
502        })
503    }
504
505    pub fn run_typed_linear_count_with_input<I>(&self, input: I) -> StreamResult<usize>
506    where
507        I: IntoIterator<Item = T>,
508    {
509        Ok(self
510            .run_typed_linear_count_with_input_report(input, FusedExecutionConfig::default())?
511            .result)
512    }
513
514    pub fn run_typed_linear_count_with_input_report<I>(
515        &self,
516        input: I,
517        config: FusedExecutionConfig,
518    ) -> StreamResult<FusedTerminalReport<usize>>
519    where
520        I: IntoIterator<Item = T>,
521    {
522        let plan = self.typed_linear_plan()?;
523        let mut count = 0;
524        let mut events = 0;
525        let mut async_boundary_crossings = 0;
526
527        for item in input {
528            let _ = plan.run_item(item, config, &mut events, &mut async_boundary_crossings)?;
529            count += 1;
530        }
531
532        Ok(FusedTerminalReport {
533            result: count,
534            events,
535            async_boundary_crossings,
536        })
537    }
538
539    pub fn run_typed_linear_fold_with_input<I, Acc, F>(
540        &self,
541        input: I,
542        zero: Acc,
543        fold: F,
544    ) -> StreamResult<Acc>
545    where
546        I: IntoIterator<Item = T>,
547        F: FnMut(Acc, T) -> Acc,
548    {
549        Ok(self
550            .run_typed_linear_fold_with_input_report(
551                input,
552                zero,
553                fold,
554                FusedExecutionConfig::default(),
555            )?
556            .result)
557    }
558
559    pub fn run_typed_linear_fold_with_input_report<I, Acc, F>(
560        &self,
561        input: I,
562        zero: Acc,
563        mut fold: F,
564        config: FusedExecutionConfig,
565    ) -> StreamResult<FusedTerminalReport<Acc>>
566    where
567        I: IntoIterator<Item = T>,
568        F: FnMut(Acc, T) -> Acc,
569    {
570        let plan = self.typed_linear_plan()?;
571        let mut accumulator = zero;
572        let mut events = 0;
573        let mut async_boundary_crossings = 0;
574
575        for item in input {
576            let item = plan.run_item(item, config, &mut events, &mut async_boundary_crossings)?;
577            accumulator = fold(accumulator, item);
578        }
579
580        Ok(FusedTerminalReport {
581            result: accumulator,
582            events,
583            async_boundary_crossings,
584        })
585    }
586
587    /// Runs the internal async-boundary count path.
588    ///
589    /// The graph is still runtime-validated through the typed-linear plan, then
590    /// split at `AsyncBoundary` stages and connected with bounded handoff.
591    /// The concrete boundary executor is intentionally private so Datum does
592    /// not expose multiple public runtime backends.
593    pub fn run_async_boundary_count_with_input_report<I>(
594        &self,
595        input: I,
596        config: AsyncBoundaryExecutionConfig,
597    ) -> StreamResult<FusedTerminalReport<usize>>
598    where
599        I: IntoIterator<Item = T> + Send,
600        I::IntoIter: Send + 'static,
601    {
602        let segments = self.typed_linear_async_segments()?;
603        BoundaryCountExecutor::Ractor.run_count(input, segments, config)
604    }
605
606    fn typed_linear_plan(&self) -> StreamResult<TypedLinearPlan<T>> {
607        let graph_inlet = self.shape.inlet().id();
608        let graph_outlet = self.shape.outlet().id();
609        let type_id = TypeId::of::<T>();
610        let mut current_inlet = graph_inlet;
611        let mut seen = HashSet::new();
612        let mut steps = Vec::new();
613
614        loop {
615            let stage_index = self
616                .stages
617                .iter()
618                .position(|stage| {
619                    stage
620                        .spec
621                        .inlets
622                        .iter()
623                        .any(|inlet| inlet.id() == current_inlet)
624                })
625                .ok_or_else(|| {
626                    StreamError::GraphValidation(format!(
627                        "typed linear fast path could not find inlet {}",
628                        current_inlet.as_usize()
629                    ))
630                })?;
631            if !seen.insert(stage_index) {
632                return Err(StreamError::GraphValidation(
633                    "typed linear fast path does not support cycles".into(),
634                ));
635            }
636
637            let stage = &self.stages[stage_index];
638            if stage.spec.inlets.len() != 1 || stage.spec.outlets.len() != 1 {
639                return Err(StreamError::GraphValidation(format!(
640                    "typed linear fast path requires single-inlet single-outlet stages; {} has {} inlet(s) and {} outlet(s)",
641                    stage.spec.name(),
642                    stage.spec.inlets.len(),
643                    stage.spec.outlets.len()
644                )));
645            }
646            let inlet = &stage.spec.inlets[0];
647            let outlet = &stage.spec.outlets[0];
648            if inlet.type_id() != type_id || outlet.type_id() != type_id {
649                return Err(StreamError::GraphValidation(format!(
650                    "typed linear fast path requires every port to use {}",
651                    type_name::<T>()
652                )));
653            }
654
655            let step = match &stage.spec.kind {
656                StageKind::Identity | StageKind::Opaque => TypedLinearStep::Pass,
657                StageKind::AsyncBoundary => TypedLinearStep::AsyncBoundary,
658                StageKind::Map(map) => {
659                    let mapper = map
660                        .typed
661                        .as_ref()
662                        .downcast_ref::<Arc<dyn Fn(T) -> T + Send + Sync>>()
663                        .ok_or_else(|| {
664                            StreamError::GraphValidation(format!(
665                                "typed linear fast path could not downcast map stage {}",
666                                stage.spec.name()
667                            ))
668                        })?;
669                    TypedLinearStep::Map(Arc::clone(mapper))
670                }
671                _ => {
672                    return Err(StreamError::GraphValidation(format!(
673                        "typed linear fast path does not support {}",
674                        stage.spec.name()
675                    )));
676                }
677            };
678            steps.push(step);
679
680            if outlet.id() == graph_outlet {
681                break;
682            }
683            current_inlet = self
684                .edges
685                .iter()
686                .find_map(|edge| (edge.outlet == outlet.id()).then_some(edge.inlet))
687                .ok_or_else(|| {
688                    StreamError::GraphValidation(format!(
689                        "typed linear fast path could not follow outlet {}",
690                        outlet.id().as_usize()
691                    ))
692                })?;
693        }
694
695        if seen.len() != self.stages.len() {
696            return Err(StreamError::GraphValidation(
697                "typed linear fast path requires all stages to be on the result path".into(),
698            ));
699        }
700
701        Ok(TypedLinearPlan { steps })
702    }
703
704    pub(super) fn typed_linear_async_segments(&self) -> StreamResult<TypedLinearSegments<T>> {
705        let plan = self.typed_linear_plan()?;
706        let mut segments = Vec::new();
707        let mut current = Vec::new();
708
709        for step in plan.steps {
710            match step {
711                TypedLinearStep::AsyncBoundary => {
712                    segments.push(current);
713                    current = Vec::new();
714                }
715                step => current.push(step),
716            }
717        }
718        segments.push(current);
719
720        if segments.len() == 1 {
721            return Err(StreamError::GraphValidation(
722                "async boundary execution requires at least one AsyncBoundary stage".into(),
723            ));
724        }
725
726        Ok(TypedLinearSegments { segments })
727    }
728}
729
730pub(super) struct TypedLinearPlan<T> {
731    steps: Vec<TypedLinearStep<T>>,
732}
733
734pub(super) struct TypedLinearSegments<T> {
735    segments: Vec<Vec<TypedLinearStep<T>>>,
736}
737
738pub(super) enum TypedLinearStep<T> {
739    Pass,
740    Map(Arc<dyn Fn(T) -> T + Send + Sync>),
741    AsyncBoundary,
742}
743
744impl<T> Clone for TypedLinearStep<T> {
745    fn clone(&self) -> Self {
746        match self {
747            Self::Pass => Self::Pass,
748            Self::Map(mapper) => Self::Map(Arc::clone(mapper)),
749            Self::AsyncBoundary => Self::AsyncBoundary,
750        }
751    }
752}
753
754impl<T> TypedLinearPlan<T> {
755    fn run_item(
756        &self,
757        mut item: T,
758        config: FusedExecutionConfig,
759        events: &mut usize,
760        async_boundary_crossings: &mut usize,
761    ) -> StreamResult<T>
762    where
763        T: Send + 'static,
764    {
765        for step in &self.steps {
766            bump_fused_event(events, config)?;
767            match step {
768                TypedLinearStep::Pass => {}
769                TypedLinearStep::Map(mapper) => {
770                    item = mapper(item);
771                }
772                TypedLinearStep::AsyncBoundary => {
773                    *async_boundary_crossings += 1;
774                }
775            }
776            bump_fused_event(events, config)?;
777        }
778        Ok(item)
779    }
780}
781
782// ---------------------------------------------------------------------------
783// Phase 1 — Typed-port substrate (WP-18)
784// ---------------------------------------------------------------------------
785
786/// A typed storage cell for a single value associated with one port.
787///
788/// Used by typed junction kernels (Phase 3+) to avoid `DatumValue` boxing for
789/// per-port buffering.  Phase 2 linear kernels do not need per-port storage
790/// because values flow sequentially through the pipeline without buffering.
791// Phase 3+ will use TypedSlot actively; allow dead_code until then.
792#[allow(dead_code)]
793pub(crate) struct TypedSlot<T>(Option<T>);
794
795#[allow(dead_code)]
796impl<T> TypedSlot<T> {
797    pub(crate) fn empty() -> Self {
798        Self(None)
799    }
800
801    pub(crate) fn put(&mut self, value: T) {
802        self.0 = Some(value);
803    }
804
805    pub(crate) fn take(&mut self) -> Option<T> {
806        self.0.take()
807    }
808
809    pub(crate) fn is_some(&self) -> bool {
810        self.0.is_some()
811    }
812}
813
814/// Heterogeneous map from [`PortId`] to a type-erased [`TypedSlot<T>`].
815///
816/// Typed junction kernels (Phase 3+) register per-port slots here at plan
817/// time, then access them during execution without boxing the values.
818// Phase 3+ will use TypedPortRegistry actively; allow dead_code until then.
819#[allow(dead_code)]
820pub(crate) struct TypedPortRegistry {
821    slots: HashMap<PortId, Box<dyn Any + Send>>,
822}
823
824#[allow(dead_code)]
825impl TypedPortRegistry {
826    pub(crate) fn new() -> Self {
827        Self {
828            slots: HashMap::new(),
829        }
830    }
831
832    /// Register a typed slot for `port_id`.  Panics if a slot for that port
833    /// already exists (programming error — ports must be registered once).
834    pub(crate) fn register<T: Any + Send>(&mut self, port_id: PortId) {
835        let prev = self
836            .slots
837            .insert(port_id, Box::new(TypedSlot::<T>::empty()));
838        assert!(prev.is_none(), "port {port_id:?} registered twice");
839    }
840
841    /// Obtain a mutable reference to the typed slot for `port_id`.
842    ///
843    /// Returns `None` if the port was not registered or the stored type does
844    /// not match `T`.
845    pub(crate) fn get_mut<T: Any + Send>(&mut self, port_id: PortId) -> Option<&mut TypedSlot<T>> {
846        self.slots.get_mut(&port_id)?.downcast_mut::<TypedSlot<T>>()
847    }
848}
849
850/// Per-event kernel for a single stage in the typed-port executor.
851///
852/// A kernel receives a strongly-typed input value and produces a strongly-typed
853/// output — **no `DatumValue` boxing, no per-element downcast.**  Kernels are
854/// constructed once at plan time via a [`TypedStageFactory`] and reused across
855/// all items in the stream.
856// Phase 3+ will add typed junction kernels; allow dead_code until then.
857#[allow(dead_code)]
858pub(crate) trait TypedKernel<In, Out>: Send + Sync {
859    fn run(&self, input: In) -> Out;
860}
861
862/// Factory that constructs a [`TypedKernel<In, Out>`] for a specific stage at
863/// plan time.
864///
865/// Factories perform **safe `Any` downcasts** on the stage spec's stored
866/// type-erased functions once, during planning.  They return `None` when the
867/// stage is unsupported (falls back to the erased executor in `Auto` mode).
868// Phase 3+ will add typed junction factories; allow dead_code until then.
869#[allow(dead_code)]
870pub(crate) trait TypedStageFactory<In, Out>: Send + Sync {
871    fn try_build(&self, spec: &StageSpec) -> Option<Box<dyn TypedKernel<In, Out>>>;
872}
873
874// ---------------------------------------------------------------------------
875// Phase 2 — TypedFlowPlan<In, Out> (WP-18)
876// ---------------------------------------------------------------------------
877
878/// A typed, per-element step in a linear flow plan.
879///
880/// `In` here is the **intermediate** element type — all middle steps are typed
881/// as `In → In`.  Only the last step may produce a different type `Out`; that
882/// is handled separately in [`TypedFlowPlan`].
883enum TypedMiddleStep<T: 'static> {
884    /// Pass the value through unchanged.
885    Pass,
886    /// Apply a pure typed map function.
887    Map(Arc<dyn Fn(T) -> T + Send + Sync>),
888    /// Record an async-boundary crossing but do not move to a different thread.
889    AsyncBoundary,
890}
891
892/// The terminal step in a [`TypedFlowPlan`], which produces the final `Out`.
893///
894/// Two variants:
895///
896/// * `Map` — the last stage has a stored typed `Fn(In) → Out`.  Obtained by a
897///   plan-time `downcast_ref` on `StageMapFns::typed`.  Zero per-element
898///   allocations.
899/// * `Identity` — the last stage is a pass-through.  This variant is only
900///   constructed when `TypeId::of::<In>() == TypeId::of::<Out>()`, i.e. the
901///   types are the same at runtime.  The stored closure performs a single
902///   `Box<dyn Any + Send>::downcast::<Out>()` per element — one heap allocation
903///   — to safely reinterpret the `In` value as `Out` without `unsafe`.
904enum TypedLastStep<In: 'static, Out: 'static> {
905    /// Last stage is a typed map that directly produces `Out`.
906    Map(Arc<dyn Fn(In) -> Out + Send + Sync>),
907    /// Last stage is a pass-through; `In` and `Out` are the same type at
908    /// runtime (verified via [`TypeId`] at plan time).  One allocation per
909    /// element to perform the safe `In → Out` reinterpretation.
910    Identity(Arc<dyn Fn(In) -> Out + Send + Sync>),
911}
912
913/// A typed, fused execution plan for a linear `FlowShape<In, Out>` graph.
914///
915/// Built once at plan time by [`try_typed_flow_plan`]; reused across items.
916///
917/// **No `DatumValue` per-element boxing for Map graphs.**  For Identity-only
918/// graphs a single `Box<dyn Any + Send>` allocation per element converts the
919/// final `In` value to `Out` safely (see [`TypedLastStep::Identity`]).
920///
921/// For **count** sinks the plan short-circuits before the last step: no output
922/// value is produced and no allocation occurs.
923pub(crate) struct TypedFlowPlan<In: 'static, Out: 'static> {
924    /// Intermediate steps: `In → In`.
925    middle_steps: Vec<TypedMiddleStep<In>>,
926    /// Terminal step: `In → Out`.
927    last_step: TypedLastStep<In, Out>,
928    /// Total stage count (= `middle_steps.len() + 1`).
929    /// Retained for diagnostic use; not yet read in execution.
930    #[allow(dead_code)]
931    stage_count: usize,
932}
933
934impl<In: Send + 'static, Out: Send + 'static> TypedFlowPlan<In, Out> {
935    /// Run a single item through the typed plan, producing `Out`.
936    ///
937    /// Bumps the fused event counter **twice** per stage (before and after),
938    /// matching the erased executor's accounting.
939    pub(crate) fn run_item(
940        &self,
941        item: In,
942        config: FusedExecutionConfig,
943        events: &mut usize,
944        async_boundary_crossings: &mut usize,
945    ) -> StreamResult<Out> {
946        let mut val = item;
947        for step in &self.middle_steps {
948            bump_fused_event(events, config)?;
949            val = match step {
950                TypedMiddleStep::Pass => val,
951                TypedMiddleStep::Map(f) => f(val),
952                TypedMiddleStep::AsyncBoundary => {
953                    *async_boundary_crossings += 1;
954                    val
955                }
956            };
957            bump_fused_event(events, config)?;
958        }
959        // Terminal step.
960        bump_fused_event(events, config)?;
961        let out = match &self.last_step {
962            TypedLastStep::Map(f) => f(val),
963            TypedLastStep::Identity(f) => f(val),
964        };
965        bump_fused_event(events, config)?;
966        Ok(out)
967    }
968
969    /// Run a single item through all **middle** stages only, counting events
970    /// for the terminal stage but **not** producing an `Out` value.
971    ///
972    /// Used by `run_count_with_input_report` to avoid any allocation when the
973    /// last step is an identity pass-through (`TypedLastStep::Identity`).
974    /// Map-last graphs call this path too (the terminal map function is still
975    /// executed to maintain semantic equivalence with the erased path, but
976    /// its result is discarded — pure functions have no observable side effects).
977    pub(crate) fn run_item_count(
978        &self,
979        item: In,
980        config: FusedExecutionConfig,
981        events: &mut usize,
982        async_boundary_crossings: &mut usize,
983    ) -> StreamResult<()> {
984        let mut val = item;
985        for step in &self.middle_steps {
986            bump_fused_event(events, config)?;
987            val = match step {
988                TypedMiddleStep::Pass => val,
989                TypedMiddleStep::Map(f) => f(val),
990                TypedMiddleStep::AsyncBoundary => {
991                    *async_boundary_crossings += 1;
992                    val
993                }
994            };
995            bump_fused_event(events, config)?;
996        }
997        // Terminal stage: bump events and run (discard output).
998        bump_fused_event(events, config)?;
999        match &self.last_step {
1000            TypedLastStep::Map(f) => {
1001                let _ = f(val);
1002            }
1003            TypedLastStep::Identity(_) => {
1004                // Pass-through: no allocation needed.  Drop `val` in place.
1005                drop(val);
1006            }
1007        }
1008        bump_fused_event(events, config)?;
1009        Ok(())
1010    }
1011}
1012
1013/// Attempt to build a [`TypedFlowPlan<In, Out>`] for the given linear graph.
1014///
1015/// Returns `None` when the graph cannot be executed on the typed path.  This
1016/// happens for:
1017/// - Non-linear graphs (junctions, multi-inlet/outlet stages).
1018/// - Cyclic stage graphs.
1019/// - Graphs where not all stage port types match `In`/`Out` in a way that
1020///   allows safe plan-time downcasts.
1021/// - Stages with `StageKind::Opaque` (custom logic; always falls back to
1022///   erased).
1023///
1024/// On success, returns `Some(plan)` and the callers (`run_with_input_report`
1025/// etc.) skip the erased executor entirely.
1026pub(crate) fn try_typed_flow_plan<In, Out>(
1027    stages: &[super::builder::StageRecord],
1028    edges: &[super::builder::Edge],
1029    graph_inlet: PortId,
1030    graph_outlet: PortId,
1031) -> Option<TypedFlowPlan<In, Out>>
1032where
1033    In: Clone + Send + 'static,
1034    Out: Send + 'static,
1035{
1036    let in_type_id = TypeId::of::<In>();
1037    let out_type_id = TypeId::of::<Out>();
1038
1039    let mut current_inlet = graph_inlet;
1040    let mut seen = HashSet::new();
1041    // Collect (StageKind, outlet_type_id) pairs as we walk the chain.
1042    let mut stage_infos: Vec<(&StageKind, TypeId)> = Vec::new();
1043
1044    loop {
1045        let stage_index = stages.iter().position(|s| {
1046            s.spec
1047                .inlets
1048                .iter()
1049                .any(|inlet| inlet.id() == current_inlet)
1050        })?;
1051        if !seen.insert(stage_index) {
1052            // Cycle detected — not supported on typed path.
1053            return None;
1054        }
1055        let stage = &stages[stage_index];
1056        if stage.spec.inlets.len() != 1 || stage.spec.outlets.len() != 1 {
1057            // Non-linear stage (junction) — fall back to erased.
1058            return None;
1059        }
1060        let inlet = &stage.spec.inlets[0];
1061        let outlet = &stage.spec.outlets[0];
1062
1063        // All intermediate inlets must have type In.
1064        if inlet.type_id() != in_type_id {
1065            return None;
1066        }
1067
1068        stage_infos.push((&stage.spec.kind, outlet.type_id()));
1069
1070        if outlet.id() == graph_outlet {
1071            break;
1072        }
1073        // Follow the edge to the next inlet.
1074        current_inlet = edges
1075            .iter()
1076            .find_map(|e| (e.outlet == outlet.id()).then_some(e.inlet))?;
1077    }
1078
1079    if seen.len() != stages.len() {
1080        // Not all stages are on the result path (unreachable stages).
1081        return None;
1082    }
1083
1084    // Build the plan.  All stages except the last are `In → In`; the last
1085    // stage may produce a different `Out`.
1086    let (last_kind, last_outlet_type) = stage_infos.last()?;
1087    // The last outlet must have type `Out`.
1088    if *last_outlet_type != out_type_id {
1089        return None;
1090    }
1091
1092    let total = stage_infos.len();
1093    let mut middle_steps: Vec<TypedMiddleStep<In>> = Vec::with_capacity(total.saturating_sub(1));
1094
1095    for (kind, outlet_type) in &stage_infos[..total.saturating_sub(1)] {
1096        // Intermediate outlets must all have type `In`.
1097        if *outlet_type != in_type_id {
1098            return None;
1099        }
1100        let step = match kind {
1101            StageKind::Identity => TypedMiddleStep::Pass,
1102            // Opaque stages have custom GraphStageLogic — they can emit multiple
1103            // values, buffer, or do arbitrary things.  They are never safe to
1104            // treat as a pass-through on the typed path.
1105            StageKind::Opaque => return None,
1106            StageKind::AsyncBoundary => TypedMiddleStep::AsyncBoundary,
1107            StageKind::Map(map) => {
1108                // Downcast `typed` to `Arc<Fn(In) -> In>`.
1109                let f = map
1110                    .typed
1111                    .downcast_ref::<Arc<dyn Fn(In) -> In + Send + Sync>>()?;
1112                TypedMiddleStep::Map(Arc::clone(f))
1113            }
1114            _ => return None,
1115        };
1116        middle_steps.push(step);
1117    }
1118
1119    // Build the terminal step.
1120    let last_step: TypedLastStep<In, Out> = match last_kind {
1121        StageKind::Identity => {
1122            // Only valid when In = Out (same TypeId).
1123            if in_type_id != out_type_id {
1124                return None;
1125            }
1126            // Safe reinterpretation via Box<dyn Any + Send> downcast.
1127            // One allocation per element; no `unsafe`.
1128            TypedLastStep::Identity(Arc::new(|x: In| -> Out {
1129                let boxed: Box<dyn Any + Send> = Box::new(x);
1130                *boxed
1131                    .downcast::<Out>()
1132                    .expect("TypeId equality verified at plan time")
1133            }))
1134        }
1135        // Opaque stages always fall back to the erased executor.
1136        StageKind::Opaque => return None,
1137        StageKind::AsyncBoundary => {
1138            // Async boundary as last stage: same as Identity (sync pass-through
1139            // in the non-async fused executor).
1140            if in_type_id != out_type_id {
1141                return None;
1142            }
1143            TypedLastStep::Identity(Arc::new(|x: In| -> Out {
1144                let boxed: Box<dyn Any + Send> = Box::new(x);
1145                *boxed
1146                    .downcast::<Out>()
1147                    .expect("TypeId equality verified at plan time")
1148            }))
1149        }
1150        StageKind::Map(map) => {
1151            // Downcast `typed` to `Arc<Fn(In) -> Out>`.
1152            let f = map
1153                .typed
1154                .downcast_ref::<Arc<dyn Fn(In) -> Out + Send + Sync>>()?;
1155            TypedLastStep::Map(Arc::clone(f))
1156        }
1157        _ => return None,
1158    };
1159
1160    Some(TypedFlowPlan {
1161        middle_steps,
1162        last_step,
1163        stage_count: total,
1164    })
1165}
1166
1167// ---------------------------------------------------------------------------
1168// Phase 3a — Typed MergeSequence kernel (WP-18)
1169// ---------------------------------------------------------------------------
1170
1171/// Shared ordering core for `MergeSequence<T>`.
1172///
1173/// Holds all mutable state for sequence-ordering a fan-in: the next expected
1174/// sequence number, a pending buffer for out-of-order arrivals, and an output
1175/// queue for items whose sequence number has been resolved.
1176///
1177/// Used by the **typed** `MergeSequencePlan` execution path.  The erased
1178/// executor maintains equivalent state inline in [`StageState::MergeSequence`]
1179/// (shared semantics, forced equivalence via tests — see the `typed_vs_erased`
1180/// test suite).
1181pub(crate) struct MergeSequenceCore<T> {
1182    next_sequence: u64,
1183    /// Out-of-order items waiting for their sequence slot to open.
1184    pending: Vec<(u64, T)>,
1185    /// Items ready to emit, in sequence order.
1186    output_buffer: VecDeque<T>,
1187    /// Number of input ports that have completed.
1188    completed_count: usize,
1189    /// Total number of input ports.
1190    input_count: usize,
1191    /// Whether this stage has finished (all inputs done, all outputs drained).
1192    completed: bool,
1193}
1194
1195impl<T> MergeSequenceCore<T> {
1196    pub(crate) fn new(input_count: usize) -> Self {
1197        Self {
1198            next_sequence: 0,
1199            pending: Vec::new(),
1200            output_buffer: VecDeque::new(),
1201            completed_count: 0,
1202            input_count,
1203            completed: false,
1204        }
1205    }
1206
1207    /// Reset state for reuse across benchmark iterations.
1208    fn reset(&mut self) {
1209        self.next_sequence = 0;
1210        self.pending.clear();
1211        self.output_buffer.clear();
1212        self.completed_count = 0;
1213        self.completed = false;
1214    }
1215
1216    /// Push a typed value into the core with its extracted sequence number.
1217    ///
1218    /// On success, all newly-resolved items (whose sequence is now contiguous
1219    /// from `next_sequence`) are appended to `output_buffer`.  On duplicate
1220    /// sequence, returns `StreamError::Failed`.
1221    fn push_item(&mut self, seq: u64, val: T) -> StreamResult<()> {
1222        if seq == self.next_sequence {
1223            self.output_buffer.push_back(val);
1224            self.next_sequence += 1;
1225            // Drain any pending items that are now in sequence.
1226            while let Some(index) = self
1227                .pending
1228                .iter()
1229                .position(|(s, _)| *s == self.next_sequence)
1230            {
1231                let (_, item) = self.pending.remove(index);
1232                self.output_buffer.push_back(item);
1233                self.next_sequence += 1;
1234            }
1235        } else {
1236            if self.pending.iter().any(|(s, _)| *s == seq) {
1237                return Err(StreamError::Failed(format!(
1238                    "duplicate sequence {seq} on merge sequence"
1239                )));
1240            }
1241            self.pending.push((seq, val));
1242            self.pending.sort_by_key(|(s, _)| *s);
1243            // Check whether the new item resolves any contiguous run.
1244            while let Some(index) = self
1245                .pending
1246                .iter()
1247                .position(|(s, _)| *s == self.next_sequence)
1248            {
1249                let (_, item) = self.pending.remove(index);
1250                self.output_buffer.push_back(item);
1251                self.next_sequence += 1;
1252            }
1253        }
1254        Ok(())
1255    }
1256
1257    /// Signal that one input port has completed.
1258    ///
1259    /// Returns `true` when all inputs have completed *and* the output buffer
1260    /// is empty (the stage can now terminate cleanly).
1261    ///
1262    /// Returns `Err` when all inputs have completed but there are still items
1263    /// in `pending` that cannot be resolved — a sequence gap.
1264    fn on_inlet_complete(&mut self) -> StreamResult<bool> {
1265        self.completed_count += 1;
1266        if self.completed_count >= self.input_count && self.output_buffer.is_empty() {
1267            if !self.pending.is_empty() {
1268                return Err(StreamError::Failed(format!(
1269                    "expected sequence {}, but all input ports have pushed or are complete",
1270                    self.next_sequence,
1271                )));
1272            }
1273            self.completed = true;
1274            Ok(true)
1275        } else {
1276            Ok(false)
1277        }
1278    }
1279
1280    /// Drain all available output items into `out`.
1281    fn drain_into(&mut self, out: &mut Vec<T>) {
1282        out.extend(self.output_buffer.drain(..));
1283    }
1284}
1285
1286// ---------------------------------------------------------------------------
1287// Typed MergeSequence plan: Unzip<A,B> → MergeSequence<T> topology
1288// ---------------------------------------------------------------------------
1289
1290/// A typed, fused execution plan for a graph whose only stages are one
1291/// `Unzip`-like fan-out followed by one `MergeSequence` fan-in, where every
1292/// port shares the same element type `T` and `In` is the split input type.
1293///
1294/// Built once at plan time by [`try_typed_merge_sequence_plan`]; executed by
1295/// [`run_typed_merge_sequence`].
1296///
1297/// **No `DatumValue` boxing per element.**  The split and sequence-extractor
1298/// functions are called on strongly-typed values.
1299pub(crate) struct TypedMergeSequencePlan<In, T> {
1300    /// Splits one `In` value into `n` typed `T` values (one per MergeSequence
1301    /// inlet).  Currently only n=2 is supported (Unzip → MergeSequence(2)).
1302    splits: Vec<Arc<dyn Fn(In) -> T + Send + Sync>>,
1303    /// Extracts the sequence number from a `T` value.
1304    extract_sequence: Arc<dyn Fn(&T) -> u64 + Send + Sync>,
1305    /// Mutable ordering state, reset between runs.
1306    core: MergeSequenceCore<T>,
1307}
1308
1309impl<In: Clone + Send + 'static, T: Send + 'static> TypedMergeSequencePlan<In, T> {
1310    /// Push one input item, extract and order both split values, drain ready
1311    /// outputs into `out`.
1312    fn push_item(&mut self, item: In, out: &mut Vec<T>) -> StreamResult<()> {
1313        for split_fn in &self.splits {
1314            let val = split_fn(item.clone());
1315            let seq = (self.extract_sequence)(&val);
1316            self.core.push_item(seq, val)?;
1317        }
1318        self.core.drain_into(out);
1319        Ok(())
1320    }
1321
1322    /// Signal end-of-stream (all inputs exhausted) and check for gaps.
1323    ///
1324    /// Each split function simulates one logical inlet completing.
1325    fn finish(&mut self, out: &mut Vec<T>) -> StreamResult<()> {
1326        for _ in 0..self.splits.len() {
1327            self.core.on_inlet_complete()?;
1328        }
1329        self.core.drain_into(out);
1330        Ok(())
1331    }
1332
1333    /// Reset internal state so the plan can be reused across benchmark iterations.
1334    fn reset(&mut self) {
1335        self.core.reset();
1336    }
1337}
1338
1339/// Attempt to build a [`TypedMergeSequencePlan<In, T>`] for the given graph.
1340///
1341/// Succeeds when the graph has exactly these two stages (in topological order):
1342/// 1. A `StageKind::Unzip` with one inlet (type `In`) and `k` outlets (type `T`).
1343/// 2. A `StageKind::MergeSequence` with `k` inlets (type `T`) and one outlet
1344///    (type `T` = `Out`).
1345///
1346/// All `k` outlets of the Unzip must connect to the `k` inlets of the
1347/// MergeSequence, and all port types must match `T`.
1348///
1349/// Returns `None` for any other topology (falls back to erased executor).
1350pub(crate) fn try_typed_merge_sequence_plan<In, Out>(
1351    stages: &[super::builder::StageRecord],
1352    edges: &[super::builder::Edge],
1353    graph_inlet: PortId,
1354    graph_outlet: PortId,
1355) -> Option<TypedMergeSequencePlan<In, Out>>
1356where
1357    In: Clone + Send + 'static,
1358    Out: Send + 'static,
1359{
1360    // We require exactly two stages.
1361    if stages.len() != 2 {
1362        return None;
1363    }
1364
1365    let in_type_id = TypeId::of::<In>();
1366    let out_type_id = TypeId::of::<Out>();
1367
1368    // Find the Unzip stage (the one whose inlet id matches the graph inlet).
1369    let unzip_idx = stages
1370        .iter()
1371        .position(|s| s.spec.inlets.len() == 1 && s.spec.inlets[0].id() == graph_inlet)?;
1372    let unzip_stage = &stages[unzip_idx];
1373
1374    // Must be a StageKind::Unzip.
1375    let typed_split_any = match &unzip_stage.spec.kind {
1376        StageKind::Unzip { typed_split, .. } => Arc::clone(typed_split),
1377        _ => return None,
1378    };
1379
1380    // The unzip inlet must have type In.
1381    if unzip_stage.spec.inlets[0].type_id() != in_type_id {
1382        return None;
1383    }
1384
1385    // All unzip outlets must have type Out.
1386    let k = unzip_stage.spec.outlets.len();
1387    if k == 0 {
1388        return None;
1389    }
1390    for outlet in &unzip_stage.spec.outlets {
1391        if outlet.type_id() != out_type_id {
1392            return None;
1393        }
1394    }
1395
1396    // Find the MergeSequence stage.
1397    let ms_idx = 1 - unzip_idx;
1398    let ms_stage = &stages[ms_idx];
1399
1400    // Must be a StageKind::MergeSequence with k inlets and 1 outlet.
1401    let (ms_input_count, typed_extract_any) = match &ms_stage.spec.kind {
1402        StageKind::MergeSequence {
1403            input_count,
1404            typed_extract,
1405            ..
1406        } => (*input_count, Arc::clone(typed_extract)),
1407        _ => return None,
1408    };
1409
1410    if ms_stage.spec.inlets.len() != k || ms_stage.spec.outlets.len() != 1 {
1411        return None;
1412    }
1413    if ms_input_count != k {
1414        return None;
1415    }
1416    // All MergeSequence ports must have type Out.
1417    for inlet in &ms_stage.spec.inlets {
1418        if inlet.type_id() != out_type_id {
1419            return None;
1420        }
1421    }
1422    if ms_stage.spec.outlets[0].type_id() != out_type_id {
1423        return None;
1424    }
1425
1426    // MergeSequence outlet must be the graph outlet.
1427    if ms_stage.spec.outlets[0].id() != graph_outlet {
1428        return None;
1429    }
1430
1431    // Verify edge wiring: each Unzip outlet must connect to one MergeSequence inlet.
1432    // Build a set of (unzip_outlet_id → ms_inlet_id) edges and verify all k outlets
1433    // are connected.
1434    let unzip_outlet_ids: Vec<PortId> =
1435        unzip_stage.spec.outlets.iter().map(AnyOutlet::id).collect();
1436    let ms_inlet_ids: Vec<PortId> = ms_stage.spec.inlets.iter().map(AnyInlet::id).collect();
1437
1438    // For each Unzip outlet, find the edge and verify it leads to one of the MS inlets.
1439    let mut outlet_to_ms_inlet: Vec<Option<usize>> = vec![None; k];
1440    for edge in edges {
1441        if let Some(uo_idx) = unzip_outlet_ids.iter().position(|&id| id == edge.outlet) {
1442            // Unzip outlet must lead to an MS inlet, else the shape is unexpected.
1443            let mi_idx = ms_inlet_ids.iter().position(|&id| id == edge.inlet)?;
1444            outlet_to_ms_inlet[uo_idx] = Some(mi_idx);
1445        }
1446    }
1447    if outlet_to_ms_inlet.iter().any(|x| x.is_none()) {
1448        return None; // Not all Unzip outlets are wired to MergeSequence.
1449    }
1450
1451    // Down-cast typed_split: `Arc<StageTypedUnzipFn>` holds an
1452    // `Arc<dyn Fn(In) -> (Out0, Out1) + Send + Sync>` (for k=2) or
1453    // `Arc<dyn Fn(In) -> (Out, Out)>` etc.  We need per-outlet extractors.
1454    //
1455    // For k=2 (the only Unzip arity we support today): downcast to
1456    // `Arc<dyn Fn(In) -> (Out, Out) + Send + Sync>`.
1457    if k != 2 {
1458        // Only Unzip (2 outputs) is supported right now.
1459        return None;
1460    }
1461
1462    // Downcast typed_split to Arc<dyn Fn(In) -> (Out, Out) + Send + Sync>.
1463    let typed_split =
1464        typed_split_any.downcast_ref::<Arc<dyn Fn(In) -> (Out, Out) + Send + Sync>>()?;
1465    let typed_split = Arc::clone(typed_split);
1466
1467    // Downcast typed_extract to Arc<dyn Fn(&Out) -> u64 + Send + Sync>.
1468    let typed_extract =
1469        typed_extract_any.downcast_ref::<Arc<dyn Fn(&Out) -> u64 + Send + Sync>>()?;
1470    let typed_extract = Arc::clone(typed_extract);
1471
1472    // Build per-inlet split closures respecting the wiring order.
1473    // outlet_to_ms_inlet[0] = which MS inlet Unzip.out0 connects to.
1474    // outlet_to_ms_inlet[1] = which MS inlet Unzip.out1 connects to.
1475    //
1476    // We build `splits[ms_inlet_idx]` = the closure that extracts that value.
1477    #[allow(clippy::type_complexity)]
1478    let mut splits: Vec<Option<Arc<dyn Fn(In) -> Out + Send + Sync>>> = vec![None; k];
1479
1480    let split0 = Arc::clone(&typed_split);
1481    let split1 = Arc::clone(&typed_split);
1482
1483    let ms_idx_for_out0 = outlet_to_ms_inlet[0].unwrap();
1484    let ms_idx_for_out1 = outlet_to_ms_inlet[1].unwrap();
1485
1486    splits[ms_idx_for_out0] = Some(Arc::new(move |input: In| split0(input).0));
1487    splits[ms_idx_for_out1] = Some(Arc::new(move |input: In| split1(input).1));
1488
1489    // All slots must be filled.
1490    if splits.iter().any(|s| s.is_none()) {
1491        return None;
1492    }
1493    let splits: Vec<Arc<dyn Fn(In) -> Out + Send + Sync>> =
1494        splits.into_iter().map(|s| s.unwrap()).collect();
1495
1496    Some(TypedMergeSequencePlan {
1497        splits,
1498        extract_sequence: typed_extract,
1499        core: MergeSequenceCore::new(k),
1500    })
1501}
1502
1503/// Execute a [`TypedMergeSequencePlan`], collecting all outputs.
1504///
1505/// Called from [`run_with_input_report_mode`] when the typed plan is available.
1506pub(crate) fn run_typed_merge_sequence<In, T, I>(
1507    plan: &mut TypedMergeSequencePlan<In, T>,
1508    input: I,
1509) -> StreamResult<Vec<T>>
1510where
1511    In: Clone + Send + 'static,
1512    T: Send + 'static,
1513    I: IntoIterator<Item = In>,
1514{
1515    plan.reset();
1516    // Pre-allocate with a 2× hint (each input produces 2 outputs).
1517    let input = input.into_iter();
1518    let hint = input.size_hint().0;
1519    let mut output: Vec<T> = Vec::with_capacity(hint * plan.splits.len());
1520    for item in input {
1521        plan.push_item(item, &mut output)?;
1522    }
1523    plan.finish(&mut output)?;
1524    Ok(output)
1525}
1526
1527// ---------------------------------------------------------------------------
1528// Phase 3b — Typed MergeLatest kernel (WP-18)
1529// ---------------------------------------------------------------------------
1530
1531/// Shared latest-value core for `MergeLatest<T>`.
1532///
1533/// Holds per-inlet latest slots, a seen/completed count, and the pending-snapshot
1534/// queue.  The typed plan allocates `Vec<T>` snapshots directly without boxing.
1535///
1536/// Used by the **typed** `MergeLatestPlan` execution path.  The erased executor
1537/// maintains equivalent state inline in [`StageState::MergeLatest`] (shared
1538/// semantics, forced equivalence via typed-vs-erased tests).
1539pub(crate) struct MergeLatestCore<T> {
1540    /// Latest value seen on each inlet, `None` until first push.
1541    latest: Vec<Option<T>>,
1542    /// Number of inlets that have received at least one value.
1543    seen_count: usize,
1544    /// Number of inlets that have completed.
1545    completed_count: usize,
1546    /// Total number of input inlets.
1547    input_count: usize,
1548    /// Snapshots ready to emit (built when all inlets seen at least once).
1549    pending: VecDeque<Vec<T>>,
1550    /// Whether this stage has finished.
1551    completed: bool,
1552    /// When `true`, complete on the first inlet completion (Akka eager semantics).
1553    eager_complete: bool,
1554}
1555
1556impl<T: Clone> MergeLatestCore<T> {
1557    pub(crate) fn new(input_count: usize, eager_complete: bool) -> Self {
1558        Self {
1559            latest: vec![None; input_count],
1560            seen_count: 0,
1561            completed_count: 0,
1562            input_count,
1563            pending: VecDeque::new(),
1564            completed: false,
1565            eager_complete,
1566        }
1567    }
1568
1569    /// Reset state for reuse across benchmark iterations.
1570    fn reset(&mut self) {
1571        for slot in &mut self.latest {
1572            *slot = None;
1573        }
1574        self.seen_count = 0;
1575        self.completed_count = 0;
1576        self.pending.clear();
1577        self.completed = false;
1578    }
1579
1580    /// Push a value on `inlet_index`, update latest, and enqueue a snapshot if
1581    /// all inlets have been seen at least once.
1582    fn push_item(&mut self, inlet_index: usize, val: T) {
1583        if self.latest[inlet_index].is_none() {
1584            self.seen_count += 1;
1585        }
1586        self.latest[inlet_index] = Some(val);
1587        if self.seen_count >= self.input_count {
1588            // Build a snapshot without boxing: clone each slot directly.
1589            let snapshot: Vec<T> = self
1590                .latest
1591                .iter()
1592                .map(|s| s.clone().expect("merge-latest typed: slot seen but None"))
1593                .collect();
1594            self.pending.push_back(snapshot);
1595        }
1596    }
1597
1598    /// Signal that one inlet has completed.
1599    ///
1600    /// Returns `true` if the stage should now terminate (all inlets done, or
1601    /// `eager_complete` with no pending output).
1602    fn on_inlet_complete(&mut self) -> bool {
1603        self.completed_count += 1;
1604        let all_done = self.completed_count >= self.input_count;
1605        let eager_done = self.eager_complete && self.pending.is_empty();
1606        if all_done || eager_done {
1607            self.completed = true;
1608            true
1609        } else {
1610            false
1611        }
1612    }
1613
1614    /// Drain all pending snapshots into `out`.
1615    fn drain_into(&mut self, out: &mut Vec<Vec<T>>) {
1616        out.extend(self.pending.drain(..));
1617    }
1618}
1619
1620// ---------------------------------------------------------------------------
1621// Typed MergeLatest plan: Unzip<In> → MergeLatest<T> topology
1622// ---------------------------------------------------------------------------
1623
1624/// A typed, fused execution plan for a graph whose only stages are one
1625/// `Unzip`-like fan-out followed by one `MergeLatest` fan-in, where every
1626/// inlet shares the same element type `T` and the output is `Vec<T>`.
1627///
1628/// Built once at plan time by [`try_typed_merge_latest_plan`]; executed by
1629/// [`run_typed_merge_latest`].
1630///
1631/// **No `DatumValue` boxing per element.**  Only the final `Vec<T>` snapshot
1632/// allocation is unavoidable (it is the genuine output type).
1633pub(crate) struct TypedMergeLatestPlan<In, T> {
1634    /// Per-outlet split functions: `splits[i](input)` produces the value for
1635    /// MergeLatest inlet `i`.
1636    splits: Vec<Arc<dyn Fn(In) -> T + Send + Sync>>,
1637    /// Mutable state, reset between runs.
1638    core: MergeLatestCore<T>,
1639}
1640
1641impl<In: Clone + Send + 'static, T: Clone + Send + 'static> TypedMergeLatestPlan<In, T> {
1642    /// Push one input item, fan it out to all inlets, and drain ready snapshots.
1643    fn push_item(&mut self, item: In, out: &mut Vec<Vec<T>>) {
1644        for (idx, split_fn) in self.splits.iter().enumerate() {
1645            let val = split_fn(item.clone());
1646            self.core.push_item(idx, val);
1647        }
1648        self.core.drain_into(out);
1649    }
1650
1651    /// Signal end-of-stream (the upstream Unzip completed — all inlets complete
1652    /// simultaneously).
1653    fn finish(&mut self) -> bool {
1654        // The Unzip completes all its outlets at once; each outlet is one ML inlet.
1655        for _ in 0..self.splits.len() {
1656            if self.core.on_inlet_complete() {
1657                return true;
1658            }
1659        }
1660        true
1661    }
1662
1663    /// Reset internal state so the plan can be reused across benchmark iterations.
1664    fn reset(&mut self) {
1665        self.core.reset();
1666    }
1667}
1668
1669/// Attempt to build a [`TypedMergeLatestPlan<In, T>`] for the given graph.
1670///
1671/// Succeeds when the graph has exactly these two stages (in topological order):
1672/// 1. A `StageKind::Unzip` with one inlet (type `In`) and `k` outlets (type `T`).
1673/// 2. A `StageKind::MergeLatest` with `k` inlets (type `T`) and one outlet
1674///    (type `Vec<T>`).
1675///
1676/// All `k` outlets of the Unzip must connect to the `k` inlets of the
1677/// MergeLatest, and all port types must match.
1678///
1679/// **Type parameters**: `In` is the graph inlet type; `T` is the MergeLatest
1680/// element type (each inlet and each slot has type `T`; the output per snapshot
1681/// is `Vec<T>`).
1682///
1683/// Returns `None` for any other topology (falls back to erased executor).
1684pub(crate) fn try_typed_merge_latest_plan<In, T>(
1685    stages: &[super::builder::StageRecord],
1686    edges: &[super::builder::Edge],
1687    graph_inlet: PortId,
1688    graph_outlet: PortId,
1689) -> Option<TypedMergeLatestPlan<In, T>>
1690where
1691    In: Clone + Send + 'static,
1692    T: Clone + Send + 'static,
1693{
1694    // We require exactly two stages.
1695    if stages.len() != 2 {
1696        return None;
1697    }
1698
1699    let in_type_id = TypeId::of::<In>();
1700    let elem_type_id = TypeId::of::<T>();
1701    let vec_type_id = TypeId::of::<Vec<T>>();
1702
1703    // Find the Unzip stage (the one whose inlet id matches the graph inlet).
1704    let unzip_idx = stages
1705        .iter()
1706        .position(|s| s.spec.inlets.len() == 1 && s.spec.inlets[0].id() == graph_inlet)?;
1707    let unzip_stage = &stages[unzip_idx];
1708
1709    // Must be a StageKind::Unzip.
1710    let typed_split_any = match &unzip_stage.spec.kind {
1711        StageKind::Unzip { typed_split, .. } => Arc::clone(typed_split),
1712        _ => return None,
1713    };
1714
1715    // The unzip inlet must have type In.
1716    if unzip_stage.spec.inlets[0].type_id() != in_type_id {
1717        return None;
1718    }
1719
1720    // All unzip outlets must have type T.
1721    let k = unzip_stage.spec.outlets.len();
1722    if k == 0 {
1723        return None;
1724    }
1725    for outlet in &unzip_stage.spec.outlets {
1726        if outlet.type_id() != elem_type_id {
1727            return None;
1728        }
1729    }
1730
1731    // Find the MergeLatest stage.
1732    let ml_idx = 1 - unzip_idx;
1733    let ml_stage = &stages[ml_idx];
1734
1735    // Must be a StageKind::MergeLatest with k inlets and 1 outlet.
1736    let (ml_input_count, typed_snapshot_any) = match &ml_stage.spec.kind {
1737        StageKind::MergeLatest {
1738            input_count,
1739            typed_snapshot,
1740            ..
1741        } => (*input_count, Arc::clone(typed_snapshot)),
1742        _ => return None,
1743    };
1744
1745    if ml_stage.spec.inlets.len() != k || ml_stage.spec.outlets.len() != 1 {
1746        return None;
1747    }
1748    if ml_input_count != k {
1749        return None;
1750    }
1751    // All MergeLatest inlets must have type T.
1752    for inlet in &ml_stage.spec.inlets {
1753        if inlet.type_id() != elem_type_id {
1754            return None;
1755        }
1756    }
1757    // MergeLatest outlet must have type Vec<T>.
1758    if ml_stage.spec.outlets[0].type_id() != vec_type_id {
1759        return None;
1760    }
1761
1762    // MergeLatest outlet must be the graph outlet.
1763    if ml_stage.spec.outlets[0].id() != graph_outlet {
1764        return None;
1765    }
1766
1767    // Verify edge wiring: each Unzip outlet must connect to one MergeLatest inlet.
1768    let unzip_outlet_ids: Vec<PortId> =
1769        unzip_stage.spec.outlets.iter().map(AnyOutlet::id).collect();
1770    let ml_inlet_ids: Vec<PortId> = ml_stage.spec.inlets.iter().map(AnyInlet::id).collect();
1771
1772    let mut outlet_to_ml_inlet: Vec<Option<usize>> = vec![None; k];
1773    for edge in edges {
1774        if let Some(uo_idx) = unzip_outlet_ids.iter().position(|&id| id == edge.outlet) {
1775            // Unzip outlet must lead to an ML inlet, else the shape is unexpected.
1776            let mi_idx = ml_inlet_ids.iter().position(|&id| id == edge.inlet)?;
1777            outlet_to_ml_inlet[uo_idx] = Some(mi_idx);
1778        }
1779    }
1780    if outlet_to_ml_inlet.iter().any(|x| x.is_none()) {
1781        return None; // Not all Unzip outlets are wired to MergeLatest.
1782    }
1783
1784    // Only k=2 Unzip is supported (same as MergeSequence typed plan).
1785    if k != 2 {
1786        return None;
1787    }
1788
1789    // Downcast typed_split to Arc<dyn Fn(In) -> (T, T) + Send + Sync>.
1790    type SplitFn<A, B> = Arc<dyn Fn(A) -> (B, B) + Send + Sync>;
1791    let typed_split = typed_split_any.downcast_ref::<SplitFn<In, T>>()?;
1792    let typed_split = Arc::clone(typed_split);
1793
1794    // Verify typed_snapshot can be downcast (plan-time only; not called per-element).
1795    type SnapshotFn<U> = Arc<dyn Fn(&[Option<U>]) -> Vec<U> + Send + Sync>;
1796    typed_snapshot_any.downcast_ref::<SnapshotFn<T>>()?;
1797
1798    // Build per-inlet split closures respecting the wiring order.
1799    #[allow(clippy::type_complexity)]
1800    let mut splits: Vec<Option<Arc<dyn Fn(In) -> T + Send + Sync>>> = vec![None; k];
1801
1802    let split0 = Arc::clone(&typed_split);
1803    let split1 = Arc::clone(&typed_split);
1804
1805    let ml_idx_for_out0 = outlet_to_ml_inlet[0].unwrap();
1806    let ml_idx_for_out1 = outlet_to_ml_inlet[1].unwrap();
1807
1808    splits[ml_idx_for_out0] = Some(Arc::new(move |input: In| split0(input).0));
1809    splits[ml_idx_for_out1] = Some(Arc::new(move |input: In| split1(input).1));
1810
1811    if splits.iter().any(|s| s.is_none()) {
1812        return None;
1813    }
1814    let splits: Vec<Arc<dyn Fn(In) -> T + Send + Sync>> =
1815        splits.into_iter().map(|s| s.unwrap()).collect();
1816
1817    // Retrieve eager_complete from the MergeLatest stage kind.
1818    let eager_complete = match &ml_stage.spec.kind {
1819        StageKind::MergeLatest { eager_complete, .. } => *eager_complete,
1820        _ => return None,
1821    };
1822
1823    Some(TypedMergeLatestPlan {
1824        splits,
1825        core: MergeLatestCore::new(k, eager_complete),
1826    })
1827}
1828
1829// ---------------------------------------------------------------------------
1830// Phase 3b — per-run typed MergeLatest plan dispatcher
1831// ---------------------------------------------------------------------------
1832
1833/// Type alias for the boxed per-run MergeLatest runner returned by
1834/// [`try_build_typed_merge_latest_dispatch`].
1835///
1836/// The runner consumes a `&mut dyn Iterator<Item = In>` and returns
1837/// `StreamResult<Vec<Out>>`.  It is built fresh on every call to
1838/// [`run_with_input_report_mode`] — no mutable state is shared between runs.
1839type MergeLatestRunner<In, Out> =
1840    Box<dyn FnOnce(&mut dyn Iterator<Item = In>) -> StreamResult<Vec<Out>>>;
1841
1842/// Build a fresh one-shot typed MergeLatest runner for the given graph.
1843///
1844/// Inspects the stages to find a `MergeLatest` stage and reads its element
1845/// `TypeId`.  Then dispatches to the concrete `try_typed_merge_latest_plan`
1846/// instantiation for the matching type among the **bounded set** of 17 common
1847/// primitive/`String` types.
1848///
1849/// Returns `Some(runner)` if the topology matches and the element type is in
1850/// the supported set.  Returns `None` otherwise — callers fall back to the
1851/// erased executor (in `Auto` mode) or return a typed-unsupported error
1852/// (`TypedOnly`).
1853///
1854/// **Bounded set note**: covers all benchmark and typical user types.
1855/// A custom `T` not in this list falls back to the erased executor in `Auto`
1856/// mode; there is no silent data-loss.
1857///
1858/// **Blueprint independence**: each call builds a new [`TypedMergeLatestPlan`]
1859/// with its own [`MergeLatestCore`]; running the same blueprint concurrently
1860/// or sequentially produces independent, correct results.
1861pub(crate) fn try_build_typed_merge_latest_dispatch<In, Out>(
1862    stages: &[super::builder::StageRecord],
1863    edges: &[super::builder::Edge],
1864    inlet: PortId,
1865    outlet: PortId,
1866) -> Option<MergeLatestRunner<In, Out>>
1867where
1868    In: Clone + Send + 'static,
1869    Out: Send + 'static,
1870{
1871    // Read the element TypeId from the MergeLatest stage's inlet ports.
1872    let elem_type_id: TypeId = stages.iter().find_map(|s| {
1873        if let StageKind::MergeLatest { .. } = &s.spec.kind {
1874            s.spec.inlets.first().map(|i| i.type_id())
1875        } else {
1876            None
1877        }
1878    })?;
1879
1880    // Dispatch to the concrete instantiation for each supported element type.
1881    // The TypeId guard ensures we only call try_typed_merge_latest_plan when
1882    // T matches; the plan builder's own checks validate the full topology.
1883    //
1884    // The `Box<dyn Any>` round-trip for the output vector (`output` is a
1885    // `Vec<Vec<$T>>` which is the same as `Vec<Out>` when `Out == Vec<$T>`)
1886    // is a single allocation of the Vec header (24 bytes); no element data is
1887    // copied.  This is the minimum necessary to safely reinterpret the output
1888    // type without `unsafe` code.
1889    macro_rules! try_elem {
1890        ($($T:ty),*) => {
1891            $(
1892                if elem_type_id == TypeId::of::<$T>() {
1893                    let mut plan = try_typed_merge_latest_plan::<In, $T>(stages, edges, inlet, outlet)?;
1894                    // At this point Out == Vec<$T> (enforced by port TypeId checks
1895                    // inside try_typed_merge_latest_plan).
1896                    let runner: MergeLatestRunner<In, Out> = Box::new(
1897                        move |iter: &mut dyn Iterator<Item = In>| {
1898                            plan.reset();
1899                            let hint = iter.size_hint().0;
1900                            let mut output: Vec<Vec<$T>> = Vec::with_capacity(hint);
1901                            for item in iter {
1902                                plan.push_item(item, &mut output);
1903                            }
1904                            plan.finish();
1905                            // Vec<Vec<$T>> → Vec<Out>: safe because Out == Vec<$T>
1906                            // guaranteed by TypeId dispatch above and by
1907                            // try_typed_merge_latest_plan's port TypeId checks.
1908                            let boxed: Box<dyn Any + Send> = Box::new(output);
1909                            boxed
1910                                .downcast::<Vec<Out>>()
1911                                .map(|b| *b)
1912                                .map_err(|_| StreamError::Failed(
1913                                    "merge-latest typed runner: Out type mismatch".into()
1914                                ))
1915                        }
1916                    );
1917                    return Some(runner);
1918                }
1919            )*
1920        };
1921    }
1922
1923    try_elem!(
1924        u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64, bool, String
1925    );
1926
1927    // Element type not in the supported set → fall back to erased executor.
1928    None
1929}
1930
1931// ---------------------------------------------------------------------------
1932// Typed cyclic feedback kernel (WP-opt-cycles)
1933// ---------------------------------------------------------------------------
1934//
1935// The erased queued interpreter (`FusedExecutor` with `has_cycle == true`)
1936// flows one heap-boxed `DatumValue` per `FusedEvent` through an event stack and
1937// runs `Buffer`/`TakeWhile` via their full `GraphStageLogic` (Mutex + per-port
1938// handler HashMaps). Profiling the `cycle_merge_preferred_feedback_10k`
1939// benchmark showed ~35% of time in `HashMap`/`SipHash` and ~25% in
1940// `GraphStageLogic` machinery.
1941//
1942// This typed kernel monomorphizes the **common `MergePreferred` → `Broadcast`
1943// feedback shape** over a homogeneous element type `T` (= `In` = `Out`). It
1944// recognizes:
1945//
1946//   graph inlet ─▶ MergePreferred.secondary
1947//                  MergePreferred ─▶ Broadcast(2)
1948//                  Broadcast.out(a) ─▶ graph outlet            (output branch)
1949//                  Broadcast.out(b) ─▶ [typed feedback chain] ─▶ MergePreferred.preferred
1950//
1951// where the feedback chain is any sequence of single-in/single-out
1952// `Identity` / `Map` / `Buffer` / `TakeWhile` stages (all `T → T`). Because the
1953// feedback returns to the *preferred* inlet, the merge fully drains a value's
1954// feedback trajectory before consuming the next input — so the kernel processes
1955// one input element's trajectory to completion, then the next.
1956//
1957// Semantics preserved exactly vs the erased oracle (forced-equivalence tests in
1958// the module below): output ordering, permanent `TakeWhile` close across inputs,
1959// bounded memory (the pending queue holds ≤ 1 element), and `EventLimitExceeded`
1960// for unproductive cycles. Unsupported topologies/types return `None` and fall
1961// back to the erased interpreter in `Auto` mode.
1962
1963/// One stage of a recognized typed feedback chain. All steps are `T → T`.
1964enum CyclicFeedbackStep<T> {
1965    /// `Identity` or `Buffer` — pass the value through unchanged.
1966    Pass,
1967    /// `Map` — transform the value.
1968    Map(Arc<dyn Fn(T) -> T + Send + Sync>),
1969    /// `TakeWhile` — forward while the predicate holds; the first failure
1970    /// permanently closes the feedback path (matching Akka / the erased oracle).
1971    TakeWhile(Arc<dyn Fn(&T) -> bool + Send + Sync>),
1972}
1973
1974type CyclicFeedbackRunner<In, Out> = Box<
1975    dyn FnOnce(
1976        &mut dyn Iterator<Item = In>,
1977        FusedExecutionConfig,
1978    ) -> StreamResult<FusedExecutionReport<Out>>,
1979>;
1980
1981/// Run one value through the typed feedback chain.
1982///
1983/// Returns `Some(transformed)` if the value survives the chain (re-enters the
1984/// merge), or `None` if a `TakeWhile` predicate fails (which also permanently
1985/// closes the feedback path via `feedback_open`).
1986fn run_cyclic_feedback_chain<T>(
1987    steps: &[CyclicFeedbackStep<T>],
1988    mut value: T,
1989    feedback_open: &mut bool,
1990) -> Option<T> {
1991    for step in steps {
1992        match step {
1993            CyclicFeedbackStep::Pass => {}
1994            CyclicFeedbackStep::Map(f) => value = f(value),
1995            CyclicFeedbackStep::TakeWhile(predicate) => {
1996                if !predicate(&value) {
1997                    *feedback_open = false;
1998                    return None;
1999                }
2000            }
2001        }
2002    }
2003    Some(value)
2004}
2005
2006/// Attempt to build a typed cyclic feedback runner for the given graph.
2007///
2008/// Returns `None` (→ erased fallback in `Auto`) unless the graph is exactly the
2009/// `MergePreferred` → `Broadcast(2)` feedback shape documented above, with a
2010/// homogeneous element type `In` = `Out` that is recoverable on the typed path.
2011pub(crate) fn try_build_typed_cyclic_feedback_dispatch<In, Out>(
2012    stages: &[super::builder::StageRecord],
2013    edges: &[super::builder::Edge],
2014    graph_inlet: PortId,
2015    graph_outlet: PortId,
2016) -> Option<CyclicFeedbackRunner<In, Out>>
2017where
2018    In: Clone + Send + 'static,
2019    Out: Send + 'static,
2020{
2021    let in_type_id = TypeId::of::<In>();
2022    // The cycle is homogeneous: every internal edge carries `In`, and the graph
2023    // output is `In` too. Safe typed execution needs `In == Out`.
2024    if in_type_id != TypeId::of::<Out>() {
2025        return None;
2026    }
2027
2028    // edge helpers (linear topology, small graphs — linear scans are fine).
2029    let inlet_for_outlet = |outlet: PortId| {
2030        edges
2031            .iter()
2032            .find_map(|e| (e.outlet == outlet).then_some(e.inlet))
2033    };
2034    let stage_owning_inlet = |inlet: PortId| {
2035        stages
2036            .iter()
2037            .enumerate()
2038            .find(|(_, s)| s.spec.inlets.iter().any(|i| i.id() == inlet))
2039    };
2040
2041    // --- Merge stage M: owns the graph inlet (a secondary), feedback → preferred.
2042    let (merge_index, merge) = stage_owning_inlet(graph_inlet)?;
2043    if !matches!(merge.spec.kind, StageKind::MergePreferred) {
2044        return None;
2045    }
2046    // MergePreferred(1): inlets == [preferred, secondary0]; one outlet.
2047    if merge.spec.inlets.len() != 2 || merge.spec.outlets.len() != 1 {
2048        return None;
2049    }
2050    let preferred_inlet = merge.spec.inlets[0].id();
2051    // The graph input must arrive on the secondary, not the preferred inlet.
2052    if preferred_inlet == graph_inlet || merge.spec.inlets[1].id() != graph_inlet {
2053        return None;
2054    }
2055    if merge.spec.inlets.iter().any(|i| i.type_id() != in_type_id)
2056        || merge.spec.outlets[0].type_id() != in_type_id
2057    {
2058        return None;
2059    }
2060
2061    // --- Broadcast stage B: fed by M's outlet, fans out to 2 outlets.
2062    let broadcast_inlet = inlet_for_outlet(merge.spec.outlets[0].id())?;
2063    let (broadcast_index, broadcast) = stage_owning_inlet(broadcast_inlet)?;
2064    if !matches!(broadcast.spec.kind, StageKind::Broadcast) {
2065        return None;
2066    }
2067    if broadcast.spec.inlets.len() != 1 || broadcast.spec.outlets.len() != 2 {
2068        return None;
2069    }
2070    if broadcast.spec.inlets[0].type_id() != in_type_id
2071        || broadcast
2072            .spec
2073            .outlets
2074            .iter()
2075            .any(|o| o.type_id() != in_type_id)
2076    {
2077        return None;
2078    }
2079
2080    // One broadcast outlet is the output branch (→ graph outlet, directly); the
2081    // other is the feedback branch (→ chain → preferred).
2082    //
2083    // The erased oracle drains broadcast outlets in their declared order
2084    // (`outlets[0]` first via the LIFO event stack), so for a value `v` it emits
2085    // to whichever outlet is first *before* recursing the other. This typed
2086    // kernel emits `v` to the output, then recurses the feedback — i.e. it is
2087    // output-first. That matches the oracle only when the **output** branch is
2088    // `outlets[0]` and the feedback branch is `outlets[1]`. The feedback-first
2089    // orientation (output on the later outlet) produces a different interleaving
2090    // and falls back to the erased interpreter.
2091    if broadcast.spec.outlets[0].id() != graph_outlet
2092        || broadcast.spec.outlets[1].id() == graph_outlet
2093    {
2094        return None;
2095    }
2096    let feedback_outlet = broadcast.spec.outlets[1].id();
2097
2098    // --- Feedback chain: feedback_outlet ─▶ ... ─▶ preferred_inlet.
2099    let mut visited: HashSet<usize> = HashSet::new();
2100    visited.insert(merge_index);
2101    visited.insert(broadcast_index);
2102    let mut steps: Vec<CyclicFeedbackStep<In>> = Vec::new();
2103    let mut current_outlet = feedback_outlet;
2104    loop {
2105        let inlet = inlet_for_outlet(current_outlet)?;
2106        if inlet == preferred_inlet {
2107            break; // chain closes back at the merge's preferred inlet.
2108        }
2109        let (stage_index, stage) = stage_owning_inlet(inlet)?;
2110        if stage.spec.inlets.len() != 1 || stage.spec.outlets.len() != 1 {
2111            return None;
2112        }
2113        if stage.spec.inlets[0].type_id() != in_type_id
2114            || stage.spec.outlets[0].type_id() != in_type_id
2115        {
2116            return None;
2117        }
2118        if !visited.insert(stage_index) {
2119            return None; // unexpected revisit — bail to erased.
2120        }
2121        let step = match &stage.spec.kind {
2122            StageKind::Identity => CyclicFeedbackStep::Pass,
2123            StageKind::Map(map) => {
2124                let f = map
2125                    .typed
2126                    .downcast_ref::<Arc<dyn Fn(In) -> In + Send + Sync>>()?;
2127                CyclicFeedbackStep::Map(Arc::clone(f))
2128            }
2129            StageKind::Opaque => match stage.spec.typed_cyclic.as_ref()? {
2130                TypedCyclicOp::BufferPassthrough => CyclicFeedbackStep::Pass,
2131                TypedCyclicOp::TakeWhile(predicate) => {
2132                    let p = predicate.downcast_ref::<Arc<dyn Fn(&In) -> bool + Send + Sync>>()?;
2133                    CyclicFeedbackStep::TakeWhile(Arc::clone(p))
2134                }
2135            },
2136            _ => return None,
2137        };
2138        steps.push(step);
2139        current_outlet = stage.spec.outlets[0].id();
2140    }
2141
2142    // Every stage must be on the recognized cycle — no unreachable extras.
2143    if visited.len() != stages.len() {
2144        return None;
2145    }
2146
2147    let runner: CyclicFeedbackRunner<In, Out> = Box::new(
2148        move |iter: &mut dyn Iterator<Item = In>, config: FusedExecutionConfig| {
2149            let limit = config.event_limit;
2150            let mut output: Vec<In> = Vec::with_capacity(iter.size_hint().0);
2151            // Pending values waiting at the merge. Bounded to ≤ 1 element: each
2152            // step pops one and pushes at most one feedback value.
2153            let mut pending: VecDeque<In> = VecDeque::new();
2154            let mut feedback_open = true;
2155            let mut events: usize = 0;
2156
2157            for item in iter {
2158                pending.push_back(item);
2159                while let Some(value) = pending.pop_front() {
2160                    // Account one event for the merge/broadcast/output step;
2161                    // unproductive cycles surface `EventLimitExceeded`.
2162                    events += 1;
2163                    if events > limit {
2164                        return Err(StreamError::EventLimitExceeded { limit });
2165                    }
2166                    // Broadcast emits to the output branch...
2167                    output.push(value.clone());
2168                    // ...and to the feedback branch while it is still open.
2169                    if feedback_open {
2170                        events += steps.len();
2171                        if events > limit {
2172                            return Err(StreamError::EventLimitExceeded { limit });
2173                        }
2174                        if let Some(next) =
2175                            run_cyclic_feedback_chain(&steps, value, &mut feedback_open)
2176                        {
2177                            pending.push_back(next);
2178                        }
2179                    }
2180                }
2181            }
2182
2183            let output = downcast_output_vec::<In, Out>(output, "cyclic feedback")?;
2184            Ok(FusedExecutionReport {
2185                output,
2186                events,
2187                async_boundary_crossings: 0,
2188            })
2189        },
2190    );
2191    Some(runner)
2192}
2193
2194// ---------------------------------------------------------------------------
2195// Phase 4 — typed acyclic junction kernels (WP-P1)
2196// ---------------------------------------------------------------------------
2197
2198type AcyclicJunctionRunner<In, Out> =
2199    Box<dyn FnOnce(&mut dyn Iterator<Item = In>) -> StreamResult<Vec<Out>>>;
2200
2201fn downcast_output_vec<T, Out>(output: Vec<T>, context: &'static str) -> StreamResult<Vec<Out>>
2202where
2203    T: Send + 'static,
2204    Out: Send + 'static,
2205{
2206    let boxed: Box<dyn Any + Send> = Box::new(output);
2207    boxed
2208        .downcast::<Vec<Out>>()
2209        .map(|b| *b)
2210        .map_err(|_| StreamError::Failed(format!("{context} typed runner: output type mismatch")))
2211}
2212
2213fn stage_with_graph_inlet(
2214    stages: &[super::builder::StageRecord],
2215    graph_inlet: PortId,
2216) -> Option<(usize, &super::builder::StageRecord)> {
2217    stages.iter().enumerate().find(|(_, stage)| {
2218        stage
2219            .spec
2220            .inlets
2221            .iter()
2222            .any(|inlet| inlet.id() == graph_inlet)
2223    })
2224}
2225
2226fn other_stage(
2227    stages: &[super::builder::StageRecord],
2228    index: usize,
2229) -> Option<(usize, &super::builder::StageRecord)> {
2230    if stages.len() != 2 {
2231        return None;
2232    }
2233    let other = 1usize.checked_sub(index)?;
2234    stages.get(other).map(|stage| (other, stage))
2235}
2236
2237fn edge_target_index(
2238    edges: &[super::builder::Edge],
2239    outlet: PortId,
2240    inlets: &[AnyInlet],
2241) -> Option<usize> {
2242    let inlet_id = edges
2243        .iter()
2244        .find_map(|edge| (edge.outlet == outlet).then_some(edge.inlet))?;
2245    inlets.iter().position(|inlet| inlet.id() == inlet_id)
2246}
2247
2248fn outlets_cover_inlets(
2249    edges: &[super::builder::Edge],
2250    outlets: &[AnyOutlet],
2251    inlets: &[AnyInlet],
2252) -> Option<Vec<usize>> {
2253    if outlets.len() != inlets.len() {
2254        return None;
2255    }
2256    let mut seen = vec![false; inlets.len()];
2257    let mut mapping = Vec::with_capacity(outlets.len());
2258    for outlet in outlets {
2259        let inlet_index = edge_target_index(edges, outlet.id(), inlets)?;
2260        if seen[inlet_index] {
2261            return None;
2262        }
2263        seen[inlet_index] = true;
2264        mapping.push(inlet_index);
2265    }
2266    seen.iter().all(|item| *item).then_some(mapping)
2267}
2268
2269pub(crate) fn try_build_typed_acyclic_junction_dispatch<In, Out>(
2270    stages: &[super::builder::StageRecord],
2271    edges: &[super::builder::Edge],
2272    graph_inlet: PortId,
2273    graph_outlet: PortId,
2274) -> Option<AcyclicJunctionRunner<In, Out>>
2275where
2276    In: Clone + Send + 'static,
2277    Out: Send + 'static,
2278{
2279    if let Some(runner) =
2280        try_typed_broadcast_zip_runner::<In, Out>(stages, edges, graph_inlet, graph_outlet)
2281    {
2282        return Some(runner);
2283    }
2284    if let Some(runner) =
2285        try_typed_balance_merge_runner::<In, Out>(stages, edges, graph_inlet, graph_outlet)
2286    {
2287        return Some(runner);
2288    }
2289    if let Some(runner) =
2290        try_typed_partition_merge_runner::<In, Out>(stages, edges, graph_inlet, graph_outlet)
2291    {
2292        return Some(runner);
2293    }
2294    if let Some(runner) =
2295        try_build_typed_unzip_zip_dispatch::<In, Out>(stages, edges, graph_inlet, graph_outlet)
2296    {
2297        return Some(runner);
2298    }
2299    try_build_typed_merge_sorted_dispatch::<In, Out>(stages, edges, graph_inlet, graph_outlet)
2300}
2301
2302fn try_typed_broadcast_zip_runner<In, Out>(
2303    stages: &[super::builder::StageRecord],
2304    edges: &[super::builder::Edge],
2305    graph_inlet: PortId,
2306    graph_outlet: PortId,
2307) -> Option<AcyclicJunctionRunner<In, Out>>
2308where
2309    In: Clone + Send + 'static,
2310    Out: Send + 'static,
2311{
2312    if stages.len() != 2 || edges.len() != 2 {
2313        return None;
2314    }
2315    let in_type = TypeId::of::<In>();
2316    let pair_type = TypeId::of::<(In, In)>();
2317    if TypeId::of::<Out>() != pair_type {
2318        return None;
2319    }
2320
2321    let (broadcast_idx, broadcast_stage) = stage_with_graph_inlet(stages, graph_inlet)?;
2322    if !matches!(broadcast_stage.spec.kind, StageKind::Broadcast) {
2323        return None;
2324    }
2325    let (_, zip_stage) = other_stage(stages, broadcast_idx)?;
2326    if !matches!(zip_stage.spec.kind, StageKind::Zip(_)) {
2327        return None;
2328    }
2329
2330    if broadcast_stage.spec.inlets.len() != 1
2331        || broadcast_stage.spec.outlets.len() != 2
2332        || zip_stage.spec.inlets.len() != 2
2333        || zip_stage.spec.outlets.len() != 1
2334        || zip_stage.spec.outlets[0].id() != graph_outlet
2335        || zip_stage.spec.outlets[0].type_id() != pair_type
2336        || broadcast_stage.spec.inlets[0].type_id() != in_type
2337        || broadcast_stage
2338            .spec
2339            .outlets
2340            .iter()
2341            .any(|outlet| outlet.type_id() != in_type)
2342        || zip_stage
2343            .spec
2344            .inlets
2345            .iter()
2346            .any(|inlet| inlet.type_id() != in_type)
2347    {
2348        return None;
2349    }
2350    outlets_cover_inlets(edges, &broadcast_stage.spec.outlets, &zip_stage.spec.inlets)?;
2351
2352    Some(Box::new(|iter| {
2353        let mut output = Vec::with_capacity(iter.size_hint().0);
2354        for item in iter {
2355            output.push((item.clone(), item));
2356        }
2357        downcast_output_vec(output, "broadcast-zip")
2358    }))
2359}
2360
2361fn try_typed_balance_merge_runner<In, Out>(
2362    stages: &[super::builder::StageRecord],
2363    edges: &[super::builder::Edge],
2364    graph_inlet: PortId,
2365    graph_outlet: PortId,
2366) -> Option<AcyclicJunctionRunner<In, Out>>
2367where
2368    In: Clone + Send + 'static,
2369    Out: Send + 'static,
2370{
2371    if stages.len() != 2 || TypeId::of::<In>() != TypeId::of::<Out>() {
2372        return None;
2373    }
2374    let in_type = TypeId::of::<In>();
2375    let (balance_idx, balance_stage) = stage_with_graph_inlet(stages, graph_inlet)?;
2376    if !matches!(balance_stage.spec.kind, StageKind::Balance) {
2377        return None;
2378    }
2379    let (_, merge_stage) = other_stage(stages, balance_idx)?;
2380    if !matches!(merge_stage.spec.kind, StageKind::Merge) {
2381        return None;
2382    }
2383
2384    if balance_stage.spec.inlets.len() != 1
2385        || balance_stage.spec.outlets.is_empty()
2386        || merge_stage.spec.outlets.len() != 1
2387        || merge_stage.spec.outlets[0].id() != graph_outlet
2388        || edges.len() != balance_stage.spec.outlets.len()
2389        || balance_stage.spec.inlets[0].type_id() != in_type
2390        || merge_stage.spec.outlets[0].type_id() != in_type
2391        || balance_stage
2392            .spec
2393            .outlets
2394            .iter()
2395            .any(|outlet| outlet.type_id() != in_type)
2396        || merge_stage
2397            .spec
2398            .inlets
2399            .iter()
2400            .any(|inlet| inlet.type_id() != in_type)
2401    {
2402        return None;
2403    }
2404    outlets_cover_inlets(edges, &balance_stage.spec.outlets, &merge_stage.spec.inlets)?;
2405
2406    Some(Box::new(|iter| {
2407        let mut output = Vec::with_capacity(iter.size_hint().0);
2408        output.extend(iter);
2409        downcast_output_vec(output, "balance-merge")
2410    }))
2411}
2412
2413fn try_typed_partition_merge_runner<In, Out>(
2414    stages: &[super::builder::StageRecord],
2415    edges: &[super::builder::Edge],
2416    graph_inlet: PortId,
2417    graph_outlet: PortId,
2418) -> Option<AcyclicJunctionRunner<In, Out>>
2419where
2420    In: Clone + Send + 'static,
2421    Out: Send + 'static,
2422{
2423    if stages.len() != 2 || TypeId::of::<In>() != TypeId::of::<Out>() {
2424        return None;
2425    }
2426    let in_type = TypeId::of::<In>();
2427    let (partition_idx, partition_stage) = stage_with_graph_inlet(stages, graph_inlet)?;
2428    let (output_count, typed_partitioner) = match &partition_stage.spec.kind {
2429        StageKind::Partition {
2430            output_count,
2431            typed_partitioner,
2432            ..
2433        } => (*output_count, Arc::clone(typed_partitioner)),
2434        _ => return None,
2435    };
2436    let (_, merge_stage) = other_stage(stages, partition_idx)?;
2437    if !matches!(merge_stage.spec.kind, StageKind::Merge) {
2438        return None;
2439    }
2440
2441    if partition_stage.spec.inlets.len() != 1
2442        || partition_stage.spec.outlets.len() != output_count
2443        || merge_stage.spec.outlets.len() != 1
2444        || merge_stage.spec.outlets[0].id() != graph_outlet
2445        || merge_stage.spec.inlets.len() != output_count
2446        || edges.len() != output_count
2447        || partition_stage.spec.inlets[0].type_id() != in_type
2448        || merge_stage.spec.outlets[0].type_id() != in_type
2449        || partition_stage
2450            .spec
2451            .outlets
2452            .iter()
2453            .any(|outlet| outlet.type_id() != in_type)
2454        || merge_stage
2455            .spec
2456            .inlets
2457            .iter()
2458            .any(|inlet| inlet.type_id() != in_type)
2459    {
2460        return None;
2461    }
2462    outlets_cover_inlets(
2463        edges,
2464        &partition_stage.spec.outlets,
2465        &merge_stage.spec.inlets,
2466    )?;
2467
2468    let partitioner =
2469        typed_partitioner.downcast_ref::<Arc<dyn Fn(&In) -> usize + Send + Sync>>()?;
2470    let partitioner = Arc::clone(partitioner);
2471
2472    Some(Box::new(move |iter| {
2473        let mut output = Vec::with_capacity(iter.size_hint().0);
2474        for item in iter {
2475            let idx = partitioner(&item);
2476            if idx >= output_count {
2477                return Err(StreamError::Failed(format!(
2478                    "partitioner returned out-of-bounds index {idx} for {output_count} outputs"
2479                )));
2480            }
2481            output.push(item);
2482        }
2483        downcast_output_vec(output, "partition-merge")
2484    }))
2485}
2486
2487fn try_build_typed_unzip_zip_dispatch<In, Out>(
2488    stages: &[super::builder::StageRecord],
2489    edges: &[super::builder::Edge],
2490    graph_inlet: PortId,
2491    graph_outlet: PortId,
2492) -> Option<AcyclicJunctionRunner<In, Out>>
2493where
2494    In: Clone + Send + 'static,
2495    Out: Send + 'static,
2496{
2497    let elem_type_id = stages.iter().find_map(|stage| {
2498        if let StageKind::Zip(_) = stage.spec.kind {
2499            let [left, right] = stage.spec.inlets.as_slice() else {
2500                return None;
2501            };
2502            (left.type_id() == right.type_id()).then_some(left.type_id())
2503        } else {
2504            None
2505        }
2506    })?;
2507
2508    macro_rules! try_elem {
2509        ($($T:ty),*) => {
2510            $(
2511                if elem_type_id == TypeId::of::<$T>() {
2512                    return try_typed_unzip_zip_runner_same::<In, $T, Out>(
2513                        stages,
2514                        edges,
2515                        graph_inlet,
2516                        graph_outlet,
2517                    );
2518                }
2519            )*
2520        };
2521    }
2522
2523    try_elem!(
2524        u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64, bool, char,
2525        String
2526    );
2527    None
2528}
2529
2530fn try_typed_unzip_zip_runner_same<In, T, Out>(
2531    stages: &[super::builder::StageRecord],
2532    edges: &[super::builder::Edge],
2533    graph_inlet: PortId,
2534    graph_outlet: PortId,
2535) -> Option<AcyclicJunctionRunner<In, Out>>
2536where
2537    In: Clone + Send + 'static,
2538    T: Send + 'static,
2539    Out: Send + 'static,
2540{
2541    if stages.len() != 2 || edges.len() != 2 || TypeId::of::<Out>() != TypeId::of::<(T, T)>() {
2542        return None;
2543    }
2544    let in_type = TypeId::of::<In>();
2545    let elem_type = TypeId::of::<T>();
2546    let pair_type = TypeId::of::<(T, T)>();
2547
2548    let (unzip_idx, unzip_stage) = stage_with_graph_inlet(stages, graph_inlet)?;
2549    let typed_split = match &unzip_stage.spec.kind {
2550        StageKind::Unzip { typed_split, .. } => Arc::clone(typed_split),
2551        _ => return None,
2552    };
2553    let (_, zip_stage) = other_stage(stages, unzip_idx)?;
2554    if !matches!(zip_stage.spec.kind, StageKind::Zip(_)) {
2555        return None;
2556    }
2557
2558    if unzip_stage.spec.inlets.len() != 1
2559        || unzip_stage.spec.outlets.len() != 2
2560        || zip_stage.spec.inlets.len() != 2
2561        || zip_stage.spec.outlets.len() != 1
2562        || zip_stage.spec.outlets[0].id() != graph_outlet
2563        || unzip_stage.spec.inlets[0].type_id() != in_type
2564        || zip_stage.spec.outlets[0].type_id() != pair_type
2565        || unzip_stage
2566            .spec
2567            .outlets
2568            .iter()
2569            .any(|outlet| outlet.type_id() != elem_type)
2570        || zip_stage
2571            .spec
2572            .inlets
2573            .iter()
2574            .any(|inlet| inlet.type_id() != elem_type)
2575    {
2576        return None;
2577    }
2578    let mapping = outlets_cover_inlets(edges, &unzip_stage.spec.outlets, &zip_stage.spec.inlets)?;
2579    let out0_to_left = mapping.first().copied()? == 0;
2580
2581    let split = typed_split.downcast_ref::<Arc<dyn Fn(In) -> (T, T) + Send + Sync>>()?;
2582    let split = Arc::clone(split);
2583
2584    Some(Box::new(move |iter| {
2585        let mut output = Vec::with_capacity(iter.size_hint().0);
2586        for item in iter {
2587            let (left, right) = split(item);
2588            if out0_to_left {
2589                output.push((left, right));
2590            } else {
2591                output.push((right, left));
2592            }
2593        }
2594        downcast_output_vec(output, "unzip-zip")
2595    }))
2596}
2597
2598fn try_build_typed_merge_sorted_dispatch<In, Out>(
2599    stages: &[super::builder::StageRecord],
2600    edges: &[super::builder::Edge],
2601    graph_inlet: PortId,
2602    graph_outlet: PortId,
2603) -> Option<AcyclicJunctionRunner<In, Out>>
2604where
2605    In: Clone + Send + 'static,
2606    Out: Send + 'static,
2607{
2608    let elem_type_id = stages.iter().find_map(|stage| {
2609        if let StageKind::MergeSorted(_) = stage.spec.kind {
2610            let [left, right] = stage.spec.inlets.as_slice() else {
2611                return None;
2612            };
2613            (left.type_id() == right.type_id()).then_some(left.type_id())
2614        } else {
2615            None
2616        }
2617    })?;
2618
2619    macro_rules! try_elem {
2620        ($($T:ty),*) => {
2621            $(
2622                if elem_type_id == TypeId::of::<$T>() {
2623                    return try_typed_merge_sorted_runner::<In, $T, Out>(
2624                        stages,
2625                        edges,
2626                        graph_inlet,
2627                        graph_outlet,
2628                    );
2629                }
2630            )*
2631        };
2632    }
2633
2634    try_elem!(
2635        u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, bool, char, String
2636    );
2637    None
2638}
2639
2640fn try_typed_merge_sorted_runner<In, T, Out>(
2641    stages: &[super::builder::StageRecord],
2642    edges: &[super::builder::Edge],
2643    graph_inlet: PortId,
2644    graph_outlet: PortId,
2645) -> Option<AcyclicJunctionRunner<In, Out>>
2646where
2647    In: Clone + Send + 'static,
2648    T: Ord + Send + 'static,
2649    Out: Send + 'static,
2650{
2651    if stages.len() != 2 || edges.len() != 2 || TypeId::of::<Out>() != TypeId::of::<T>() {
2652        return None;
2653    }
2654    let in_type = TypeId::of::<In>();
2655    let elem_type = TypeId::of::<T>();
2656
2657    let (unzip_idx, unzip_stage) = stage_with_graph_inlet(stages, graph_inlet)?;
2658    let typed_split = match &unzip_stage.spec.kind {
2659        StageKind::Unzip { typed_split, .. } => Arc::clone(typed_split),
2660        _ => return None,
2661    };
2662    let (_, merge_stage) = other_stage(stages, unzip_idx)?;
2663    if !matches!(merge_stage.spec.kind, StageKind::MergeSorted(_)) {
2664        return None;
2665    }
2666
2667    if unzip_stage.spec.inlets.len() != 1
2668        || unzip_stage.spec.outlets.len() != 2
2669        || merge_stage.spec.inlets.len() != 2
2670        || merge_stage.spec.outlets.len() != 1
2671        || merge_stage.spec.outlets[0].id() != graph_outlet
2672        || unzip_stage.spec.inlets[0].type_id() != in_type
2673        || merge_stage.spec.outlets[0].type_id() != elem_type
2674        || unzip_stage
2675            .spec
2676            .outlets
2677            .iter()
2678            .any(|outlet| outlet.type_id() != elem_type)
2679        || merge_stage
2680            .spec
2681            .inlets
2682            .iter()
2683            .any(|inlet| inlet.type_id() != elem_type)
2684    {
2685        return None;
2686    }
2687    let mapping = outlets_cover_inlets(edges, &unzip_stage.spec.outlets, &merge_stage.spec.inlets)?;
2688    let out0_to_left = mapping.first().copied()? == 0;
2689
2690    let split = typed_split.downcast_ref::<Arc<dyn Fn(In) -> (T, T) + Send + Sync>>()?;
2691    let split = Arc::clone(split);
2692
2693    Some(Box::new(move |iter| {
2694        let mut left = VecDeque::new();
2695        let mut right = VecDeque::new();
2696        let mut output = Vec::with_capacity(iter.size_hint().0.saturating_mul(2));
2697        for item in iter {
2698            let (first, second) = split(item);
2699            if out0_to_left {
2700                left.push_back(first);
2701                right.push_back(second);
2702            } else {
2703                left.push_back(second);
2704                right.push_back(first);
2705            }
2706            drain_merge_sorted(&mut left, &mut right, false, false, &mut output);
2707        }
2708        drain_merge_sorted(&mut left, &mut right, true, true, &mut output);
2709        downcast_output_vec(output, "merge-sorted")
2710    }))
2711}
2712
2713fn drain_merge_sorted<T: Ord>(
2714    left: &mut VecDeque<T>,
2715    right: &mut VecDeque<T>,
2716    left_closed: bool,
2717    right_closed: bool,
2718    output: &mut Vec<T>,
2719) {
2720    loop {
2721        let next = match (left.front(), right.front()) {
2722            (Some(left_item), Some(right_item)) => {
2723                if left_item <= right_item {
2724                    left.pop_front()
2725                } else {
2726                    right.pop_front()
2727                }
2728            }
2729            (Some(_), None) if right_closed => left.pop_front(),
2730            (None, Some(_)) if left_closed => right.pop_front(),
2731            _ => None,
2732        };
2733        let Some(item) = next else {
2734            break;
2735        };
2736        output.push(item);
2737    }
2738}
2739
2740// ---------------------------------------------------------------------------
2741
2742pub(super) enum BoundaryCountExecutor {
2743    #[cfg(test)]
2744    Threaded,
2745    Ractor,
2746}
2747
2748impl BoundaryCountExecutor {
2749    pub(super) fn run_count<I, T>(
2750        &self,
2751        input: I,
2752        segments: TypedLinearSegments<T>,
2753        config: AsyncBoundaryExecutionConfig,
2754    ) -> StreamResult<FusedTerminalReport<usize>>
2755    where
2756        I: IntoIterator<Item = T> + Send,
2757        I::IntoIter: Send + 'static,
2758        T: Send + 'static,
2759    {
2760        match self {
2761            #[cfg(test)]
2762            Self::Threaded => run_threaded_async_linear_count(input, segments, config),
2763            Self::Ractor => run_ractor_async_linear_count(input, segments, config),
2764        }
2765    }
2766}
2767
2768#[cfg(test)]
2769mod tests {
2770    use super::*;
2771    use std::time::Duration;
2772
2773    #[derive(Default)]
2774    struct BufferedFlowState {
2775        queued: VecDeque<i32>,
2776        upstream_closed: bool,
2777        pull_calls: usize,
2778        finish_calls: usize,
2779    }
2780
2781    struct BufferedFlowOnPull {
2782        state: Arc<Mutex<BufferedFlowState>>,
2783    }
2784
2785    impl GraphStage for BufferedFlowOnPull {
2786        type Shape = FlowShape<i32, i32>;
2787
2788        fn name(&self) -> &str {
2789            "BufferedFlowOnPull"
2790        }
2791
2792        fn allocate_shape(&self, _allocator: &mut PortAllocator) -> Self::Shape {
2793            let first_id = next_port_id_block(2);
2794            FlowShape::new(
2795                Inlet::with_id(first_id, "buffered-flow.in"),
2796                Outlet::with_id(first_id.offset(1), "buffered-flow.out"),
2797            )
2798        }
2799
2800        fn stage_spec(&self, shape: &Self::Shape) -> StageSpec {
2801            StageSpec::opaque(self.name(), shape.inlets(), shape.outlets())
2802        }
2803
2804        fn create_logic(&self, shape: &Self::Shape) -> GraphStageLogic {
2805            struct In {
2806                state: Arc<Mutex<BufferedFlowState>>,
2807            }
2808
2809            impl InHandler for In {
2810                fn on_push(
2811                    &mut self,
2812                    logic: &mut GraphStageLogic,
2813                    inlet: AnyInlet,
2814                ) -> StreamResult<()> {
2815                    let value: i32 = logic.grab_datum(inlet.id()).and_then(|value| {
2816                        downcast_datum(value, "grab", || format!("inlet#{}", inlet.id().as_usize()))
2817                    })?;
2818                    self.state.lock().unwrap().queued.push_back(value);
2819                    Ok(())
2820                }
2821
2822                fn on_upstream_finish(
2823                    &mut self,
2824                    _logic: &mut GraphStageLogic,
2825                    _inlet: AnyInlet,
2826                ) -> StreamResult<()> {
2827                    self.state.lock().unwrap().upstream_closed = true;
2828                    Ok(())
2829                }
2830            }
2831
2832            struct Out {
2833                outlet: Outlet<i32>,
2834                state: Arc<Mutex<BufferedFlowState>>,
2835            }
2836
2837            impl OutHandler for Out {
2838                fn on_pull(
2839                    &mut self,
2840                    logic: &mut GraphStageLogic,
2841                    _outlet: AnyOutlet,
2842                ) -> StreamResult<()> {
2843                    let (next, upstream_closed) = {
2844                        let mut state = self.state.lock().unwrap();
2845                        state.pull_calls += 1;
2846                        (state.queued.pop_front(), state.upstream_closed)
2847                    };
2848                    if let Some(value) = next {
2849                        logic.emit(&self.outlet, value)
2850                    } else if upstream_closed {
2851                        logic.complete(&self.outlet)
2852                    } else {
2853                        Ok(())
2854                    }
2855                }
2856
2857                fn on_downstream_finish(
2858                    &mut self,
2859                    logic: &mut GraphStageLogic,
2860                    _outlet: AnyOutlet,
2861                ) -> StreamResult<()> {
2862                    self.state.lock().unwrap().finish_calls += 1;
2863                    logic.complete_stage()
2864                }
2865            }
2866
2867            let mut logic = GraphStageLogic::new(shape);
2868            logic
2869                .set_handler(
2870                    &shape.inlet(),
2871                    Box::new(In {
2872                        state: Arc::clone(&self.state),
2873                    }),
2874                )
2875                .unwrap();
2876            logic
2877                .set_out_handler(
2878                    &shape.outlet(),
2879                    Box::new(Out {
2880                        outlet: shape.outlet(),
2881                        state: Arc::clone(&self.state),
2882                    }),
2883                )
2884                .unwrap();
2885            logic
2886        }
2887    }
2888
2889    struct EmitMultipleThenFailOnPush;
2890
2891    impl GraphStage for EmitMultipleThenFailOnPush {
2892        type Shape = FlowShape<i32, i32>;
2893
2894        fn name(&self) -> &str {
2895            "EmitMultipleThenFailOnPush"
2896        }
2897
2898        fn allocate_shape(&self, _allocator: &mut PortAllocator) -> Self::Shape {
2899            let first_id = next_port_id_block(2);
2900            FlowShape::new(
2901                Inlet::with_id(first_id, "emit-fail.in"),
2902                Outlet::with_id(first_id.offset(1), "emit-fail.out"),
2903            )
2904        }
2905
2906        fn stage_spec(&self, shape: &Self::Shape) -> StageSpec {
2907            StageSpec::opaque(self.name(), shape.inlets(), shape.outlets())
2908        }
2909
2910        fn create_logic(&self, shape: &Self::Shape) -> GraphStageLogic {
2911            struct Handler {
2912                outlet: Outlet<i32>,
2913            }
2914
2915            impl InHandler for Handler {
2916                fn on_push(
2917                    &mut self,
2918                    logic: &mut GraphStageLogic,
2919                    _inlet: AnyInlet,
2920                ) -> StreamResult<()> {
2921                    logic.emit_multiple(&self.outlet, [1, 2])?;
2922                    Err(StreamError::Failed("emit_multiple boom".into()))
2923                }
2924            }
2925
2926            let mut logic = GraphStageLogic::new(shape);
2927            logic
2928                .set_handler(
2929                    &shape.inlet(),
2930                    Box::new(Handler {
2931                        outlet: shape.outlet(),
2932                    }),
2933                )
2934                .unwrap();
2935            logic
2936        }
2937    }
2938
2939    struct ReadNThenFailOnFinish;
2940
2941    struct EmitMultipleOnPush;
2942
2943    impl GraphStage for EmitMultipleOnPush {
2944        type Shape = FlowShape<i32, i32>;
2945
2946        fn name(&self) -> &str {
2947            "EmitMultipleOnPush"
2948        }
2949
2950        fn allocate_shape(&self, _allocator: &mut PortAllocator) -> Self::Shape {
2951            let first_id = next_port_id_block(2);
2952            FlowShape::new(
2953                Inlet::with_id(first_id, "emit-multiple.in"),
2954                Outlet::with_id(first_id.offset(1), "emit-multiple.out"),
2955            )
2956        }
2957
2958        fn stage_spec(&self, shape: &Self::Shape) -> StageSpec {
2959            StageSpec::opaque(self.name(), shape.inlets(), shape.outlets())
2960        }
2961
2962        fn create_logic(&self, shape: &Self::Shape) -> GraphStageLogic {
2963            struct Handler {
2964                outlet: Outlet<i32>,
2965            }
2966
2967            impl InHandler for Handler {
2968                fn on_push(
2969                    &mut self,
2970                    logic: &mut GraphStageLogic,
2971                    _inlet: AnyInlet,
2972                ) -> StreamResult<()> {
2973                    logic.emit_multiple(&self.outlet, [1, 2])
2974                }
2975            }
2976
2977            let mut logic = GraphStageLogic::new(shape);
2978            logic
2979                .set_handler(
2980                    &shape.inlet(),
2981                    Box::new(Handler {
2982                        outlet: shape.outlet(),
2983                    }),
2984                )
2985                .unwrap();
2986            logic
2987        }
2988    }
2989
2990    impl GraphStage for ReadNThenFailOnFinish {
2991        type Shape = FlowShape<i32, i32>;
2992
2993        fn name(&self) -> &str {
2994            "ReadNThenFailOnFinish"
2995        }
2996
2997        fn allocate_shape(&self, _allocator: &mut PortAllocator) -> Self::Shape {
2998            let first_id = next_port_id_block(2);
2999            FlowShape::new(
3000                Inlet::with_id(first_id, "read-n.in"),
3001                Outlet::with_id(first_id.offset(1), "read-n.out"),
3002            )
3003        }
3004
3005        fn stage_spec(&self, shape: &Self::Shape) -> StageSpec {
3006            StageSpec::opaque(self.name(), shape.inlets(), shape.outlets())
3007        }
3008
3009        fn create_logic(&self, shape: &Self::Shape) -> GraphStageLogic {
3010            struct Handler {
3011                inlet: Inlet<i32>,
3012                armed: bool,
3013            }
3014
3015            impl InHandler for Handler {
3016                fn on_push(
3017                    &mut self,
3018                    logic: &mut GraphStageLogic,
3019                    _inlet: AnyInlet,
3020                ) -> StreamResult<()> {
3021                    if !self.armed {
3022                        self.armed = true;
3023                        logic.read_n(&self.inlet, 2, |_values| {}, |_values| {})
3024                    } else {
3025                        Ok(())
3026                    }
3027                }
3028
3029                fn on_upstream_finish(
3030                    &mut self,
3031                    _logic: &mut GraphStageLogic,
3032                    _inlet: AnyInlet,
3033                ) -> StreamResult<()> {
3034                    Err(StreamError::Failed("read_n finish boom".into()))
3035                }
3036            }
3037
3038            let mut logic = GraphStageLogic::new(shape);
3039            logic
3040                .set_handler(
3041                    &shape.inlet(),
3042                    Box::new(Handler {
3043                        inlet: shape.inlet(),
3044                        armed: false,
3045                    }),
3046                )
3047                .unwrap();
3048            logic
3049        }
3050    }
3051
3052    fn single_opaque_stage_graph<G>(stage: G) -> GraphBlueprint<FlowShape<i32, i32>>
3053    where
3054        G: GraphStage<Shape = FlowShape<i32, i32>>,
3055    {
3056        GraphDsl::create(|builder| builder.add(stage)).unwrap()
3057    }
3058
3059    fn run_flow_with_timeout(
3060        graph: GraphBlueprint<FlowShape<i32, i32>>,
3061        input: Vec<i32>,
3062    ) -> StreamResult<Vec<i32>> {
3063        let (tx, rx) = mpsc::channel();
3064        thread::spawn(move || {
3065            tx.send(graph.run_with_input(input))
3066                .expect("test receiver is alive");
3067        });
3068        rx.recv_timeout(Duration::from_secs(2))
3069            .expect("graph run completed before timeout")
3070    }
3071
3072    #[derive(Default)]
3073    struct OneShotTimerChecks {
3074        inactive_before_schedule: bool,
3075        active_after_schedule: bool,
3076        inactive_on_timer: bool,
3077    }
3078
3079    struct OneShotTimerStage {
3080        checks: Arc<Mutex<OneShotTimerChecks>>,
3081    }
3082
3083    impl GraphStage for OneShotTimerStage {
3084        type Shape = FlowShape<i32, i32>;
3085
3086        fn name(&self) -> &str {
3087            "OneShotTimerStage"
3088        }
3089
3090        fn allocate_shape(&self, _allocator: &mut PortAllocator) -> Self::Shape {
3091            let first_id = next_port_id_block(2);
3092            FlowShape::new(
3093                Inlet::with_id(first_id, "one-shot-timer.in"),
3094                Outlet::with_id(first_id.offset(1), "one-shot-timer.out"),
3095            )
3096        }
3097
3098        fn stage_spec(&self, shape: &Self::Shape) -> StageSpec {
3099            StageSpec::opaque(self.name(), shape.inlets(), shape.outlets())
3100        }
3101
3102        fn create_logic(&self, shape: &Self::Shape) -> GraphStageLogic {
3103            struct Out {
3104                armed: bool,
3105                checks: Arc<Mutex<OneShotTimerChecks>>,
3106            }
3107
3108            impl OutHandler for Out {
3109                fn on_pull(
3110                    &mut self,
3111                    logic: &mut GraphStageLogic,
3112                    _outlet: AnyOutlet,
3113                ) -> StreamResult<()> {
3114                    if self.armed {
3115                        return Ok(());
3116                    }
3117                    self.armed = true;
3118                    {
3119                        let mut checks = self.checks.lock().unwrap();
3120                        checks.inactive_before_schedule = !logic.is_timer_active("once");
3121                    }
3122                    logic.schedule_once("once", Duration::from_millis(1))?;
3123                    self.checks.lock().unwrap().active_after_schedule =
3124                        logic.is_timer_active("once");
3125                    Ok(())
3126                }
3127            }
3128
3129            struct Timer {
3130                outlet: Outlet<i32>,
3131                checks: Arc<Mutex<OneShotTimerChecks>>,
3132            }
3133
3134            impl TimerHandler for Timer {
3135                fn on_timer(&mut self, logic: &mut GraphStageLogic, key: &str) -> StreamResult<()> {
3136                    assert_eq!(key, "once");
3137                    self.checks.lock().unwrap().inactive_on_timer = !logic.is_timer_active("once");
3138                    logic.push(&self.outlet, 42)?;
3139                    logic.complete(&self.outlet)
3140                }
3141            }
3142
3143            let mut logic = GraphStageLogic::new(shape);
3144            logic
3145                .set_out_handler(
3146                    &shape.outlet(),
3147                    Box::new(Out {
3148                        armed: false,
3149                        checks: Arc::clone(&self.checks),
3150                    }),
3151                )
3152                .unwrap();
3153            logic.set_timer_handler(Box::new(Timer {
3154                outlet: shape.outlet(),
3155                checks: Arc::clone(&self.checks),
3156            }));
3157            logic
3158        }
3159    }
3160
3161    #[derive(Default)]
3162    struct PeriodicTimerChecks {
3163        active_after_schedule: bool,
3164        active_on_first_tick: bool,
3165        inactive_after_cancel: bool,
3166        ticks: usize,
3167    }
3168
3169    struct PeriodicTimerStage {
3170        checks: Arc<Mutex<PeriodicTimerChecks>>,
3171    }
3172
3173    impl GraphStage for PeriodicTimerStage {
3174        type Shape = FlowShape<i32, i32>;
3175
3176        fn name(&self) -> &str {
3177            "PeriodicTimerStage"
3178        }
3179
3180        fn allocate_shape(&self, _allocator: &mut PortAllocator) -> Self::Shape {
3181            let first_id = next_port_id_block(2);
3182            FlowShape::new(
3183                Inlet::with_id(first_id, "periodic-timer.in"),
3184                Outlet::with_id(first_id.offset(1), "periodic-timer.out"),
3185            )
3186        }
3187
3188        fn stage_spec(&self, shape: &Self::Shape) -> StageSpec {
3189            StageSpec::opaque(self.name(), shape.inlets(), shape.outlets())
3190        }
3191
3192        fn create_logic(&self, shape: &Self::Shape) -> GraphStageLogic {
3193            struct Out {
3194                armed: bool,
3195                checks: Arc<Mutex<PeriodicTimerChecks>>,
3196            }
3197
3198            impl OutHandler for Out {
3199                fn on_pull(
3200                    &mut self,
3201                    logic: &mut GraphStageLogic,
3202                    _outlet: AnyOutlet,
3203                ) -> StreamResult<()> {
3204                    if self.armed {
3205                        return Ok(());
3206                    }
3207                    self.armed = true;
3208                    logic.schedule_periodically_with_initial_delay(
3209                        "periodic",
3210                        Duration::ZERO,
3211                        Duration::from_millis(2),
3212                    )?;
3213                    self.checks.lock().unwrap().active_after_schedule =
3214                        logic.is_timer_active("periodic");
3215                    Ok(())
3216                }
3217            }
3218
3219            struct Timer {
3220                outlet: Outlet<i32>,
3221                checks: Arc<Mutex<PeriodicTimerChecks>>,
3222            }
3223
3224            impl TimerHandler for Timer {
3225                fn on_timer(&mut self, logic: &mut GraphStageLogic, key: &str) -> StreamResult<()> {
3226                    assert_eq!(key, "periodic");
3227                    let tick = {
3228                        let mut checks = self.checks.lock().unwrap();
3229                        checks.ticks += 1;
3230                        if checks.ticks == 1 {
3231                            checks.active_on_first_tick = logic.is_timer_active("periodic");
3232                        }
3233                        checks.ticks
3234                    };
3235                    logic.push(&self.outlet, tick as i32)?;
3236                    if tick == 3 {
3237                        assert!(logic.cancel_timer("periodic"));
3238                        self.checks.lock().unwrap().inactive_after_cancel =
3239                            !logic.is_timer_active("periodic");
3240                        logic.complete(&self.outlet)?;
3241                    }
3242                    Ok(())
3243                }
3244            }
3245
3246            let mut logic = GraphStageLogic::new(shape);
3247            logic
3248                .set_out_handler(
3249                    &shape.outlet(),
3250                    Box::new(Out {
3251                        armed: false,
3252                        checks: Arc::clone(&self.checks),
3253                    }),
3254                )
3255                .unwrap();
3256            logic.set_timer_handler(Box::new(Timer {
3257                outlet: shape.outlet(),
3258                checks: Arc::clone(&self.checks),
3259            }));
3260            logic
3261        }
3262    }
3263
3264    struct CompletingTimerStage {
3265        fired: Arc<AtomicUsize>,
3266    }
3267
3268    impl GraphStage for CompletingTimerStage {
3269        type Shape = FlowShape<i32, i32>;
3270
3271        fn name(&self) -> &str {
3272            "CompletingTimerStage"
3273        }
3274
3275        fn allocate_shape(&self, _allocator: &mut PortAllocator) -> Self::Shape {
3276            let first_id = next_port_id_block(2);
3277            FlowShape::new(
3278                Inlet::with_id(first_id, "completing-timer.in"),
3279                Outlet::with_id(first_id.offset(1), "completing-timer.out"),
3280            )
3281        }
3282
3283        fn stage_spec(&self, shape: &Self::Shape) -> StageSpec {
3284            StageSpec::opaque(self.name(), shape.inlets(), shape.outlets())
3285        }
3286
3287        fn create_logic(&self, shape: &Self::Shape) -> GraphStageLogic {
3288            struct Out {
3289                outlet: Outlet<i32>,
3290            }
3291
3292            impl OutHandler for Out {
3293                fn on_pull(
3294                    &mut self,
3295                    logic: &mut GraphStageLogic,
3296                    _outlet: AnyOutlet,
3297                ) -> StreamResult<()> {
3298                    logic.schedule_once("late", Duration::from_millis(1))?;
3299                    logic.complete(&self.outlet)
3300                }
3301            }
3302
3303            struct Timer {
3304                fired: Arc<AtomicUsize>,
3305            }
3306
3307            impl TimerHandler for Timer {
3308                fn on_timer(
3309                    &mut self,
3310                    _logic: &mut GraphStageLogic,
3311                    _key: &str,
3312                ) -> StreamResult<()> {
3313                    self.fired.fetch_add(1, Ordering::SeqCst);
3314                    Ok(())
3315                }
3316            }
3317
3318            let mut logic = GraphStageLogic::new(shape);
3319            logic
3320                .set_out_handler(
3321                    &shape.outlet(),
3322                    Box::new(Out {
3323                        outlet: shape.outlet(),
3324                    }),
3325                )
3326                .unwrap();
3327            logic.set_timer_handler(Box::new(Timer {
3328                fired: Arc::clone(&self.fired),
3329            }));
3330            logic
3331        }
3332    }
3333
3334    struct FailingTimerStage {
3335        fired: Arc<AtomicUsize>,
3336    }
3337
3338    impl GraphStage for FailingTimerStage {
3339        type Shape = FlowShape<i32, i32>;
3340
3341        fn name(&self) -> &str {
3342            "FailingTimerStage"
3343        }
3344
3345        fn allocate_shape(&self, _allocator: &mut PortAllocator) -> Self::Shape {
3346            let first_id = next_port_id_block(2);
3347            FlowShape::new(
3348                Inlet::with_id(first_id, "failing-timer.in"),
3349                Outlet::with_id(first_id.offset(1), "failing-timer.out"),
3350            )
3351        }
3352
3353        fn stage_spec(&self, shape: &Self::Shape) -> StageSpec {
3354            StageSpec::opaque(self.name(), shape.inlets(), shape.outlets())
3355        }
3356
3357        fn create_logic(&self, shape: &Self::Shape) -> GraphStageLogic {
3358            struct Out;
3359
3360            impl OutHandler for Out {
3361                fn on_pull(
3362                    &mut self,
3363                    logic: &mut GraphStageLogic,
3364                    _outlet: AnyOutlet,
3365                ) -> StreamResult<()> {
3366                    logic.schedule_once("late", Duration::from_millis(1))?;
3367                    logic.fail_stage(StreamError::Failed("timer stage failed".into()))
3368                }
3369            }
3370
3371            struct Timer {
3372                fired: Arc<AtomicUsize>,
3373            }
3374
3375            impl TimerHandler for Timer {
3376                fn on_timer(
3377                    &mut self,
3378                    _logic: &mut GraphStageLogic,
3379                    _key: &str,
3380                ) -> StreamResult<()> {
3381                    self.fired.fetch_add(1, Ordering::SeqCst);
3382                    Ok(())
3383                }
3384            }
3385
3386            let mut logic = GraphStageLogic::new(shape);
3387            logic
3388                .set_out_handler(&shape.outlet(), Box::new(Out))
3389                .unwrap();
3390            logic.set_timer_handler(Box::new(Timer {
3391                fired: Arc::clone(&self.fired),
3392            }));
3393            logic
3394        }
3395    }
3396
3397    #[test]
3398    fn graph_stage_logic_one_shot_timer_fires_on_executor_thread() {
3399        let checks = Arc::new(Mutex::new(OneShotTimerChecks::default()));
3400        let graph = single_opaque_stage_graph(OneShotTimerStage {
3401            checks: Arc::clone(&checks),
3402        });
3403
3404        let output = run_flow_with_timeout(graph, Vec::new()).unwrap();
3405
3406        assert_eq!(output, vec![42]);
3407        let checks = checks.lock().unwrap();
3408        assert!(checks.inactive_before_schedule);
3409        assert!(checks.active_after_schedule);
3410        assert!(checks.inactive_on_timer);
3411    }
3412
3413    #[test]
3414    fn graph_stage_logic_periodic_timer_fires_until_cancelled() {
3415        let checks = Arc::new(Mutex::new(PeriodicTimerChecks::default()));
3416        let graph = single_opaque_stage_graph(PeriodicTimerStage {
3417            checks: Arc::clone(&checks),
3418        });
3419
3420        let output = run_flow_with_timeout(graph, Vec::new()).unwrap();
3421
3422        assert_eq!(output, vec![1, 2, 3]);
3423        let checks = checks.lock().unwrap();
3424        assert_eq!(checks.ticks, 3);
3425        assert!(checks.active_after_schedule);
3426        assert!(checks.active_on_first_tick);
3427        assert!(checks.inactive_after_cancel);
3428    }
3429
3430    #[test]
3431    fn graph_stage_logic_cancels_timers_when_stage_completes() {
3432        let fired = Arc::new(AtomicUsize::new(0));
3433        let graph = single_opaque_stage_graph(CompletingTimerStage {
3434            fired: Arc::clone(&fired),
3435        });
3436
3437        let output = run_flow_with_timeout(graph, Vec::new()).unwrap();
3438
3439        assert!(output.is_empty());
3440        assert_eq!(fired.load(Ordering::SeqCst), 0);
3441    }
3442
3443    #[test]
3444    fn graph_stage_logic_cancels_timers_when_stage_fails() {
3445        let fired = Arc::new(AtomicUsize::new(0));
3446        let graph = single_opaque_stage_graph(FailingTimerStage {
3447            fired: Arc::clone(&fired),
3448        });
3449
3450        let output = run_flow_with_timeout(graph, Vec::new()).unwrap();
3451
3452        assert!(output.is_empty());
3453        assert_eq!(fired.load(Ordering::SeqCst), 0);
3454    }
3455
3456    #[test]
3457    fn process_push_restores_handler_before_emit_multiple_error_propagates() {
3458        let graph = single_opaque_stage_graph(EmitMultipleThenFailOnPush);
3459        let inlet = graph.shape.inlet().id();
3460        let mut executor = FusedExecutor::new(&graph, FusedExecutionConfig::default());
3461
3462        let result = executor.process_stage(0, inlet, datum(10));
3463
3464        assert!(matches!(
3465            result,
3466            Err(StreamError::Failed(message)) if message == "emit_multiple boom"
3467        ));
3468        assert!(
3469            executor.opaque_logics[0]
3470                .as_mut()
3471                .unwrap()
3472                .get_in_handler_mut(inlet)
3473                .is_some()
3474        );
3475    }
3476
3477    #[test]
3478    fn process_completion_restores_handler_before_read_n_finish_error_propagates() {
3479        let graph = single_opaque_stage_graph(ReadNThenFailOnFinish);
3480        let inlet = graph.shape.inlet().id();
3481        let mut executor = FusedExecutor::new(&graph, FusedExecutionConfig::default());
3482
3483        executor.process_stage(0, inlet, datum(1)).unwrap();
3484        executor.process_stage(0, inlet, datum(2)).unwrap();
3485        let result = executor.process_completion(0, inlet);
3486
3487        assert!(matches!(
3488            result,
3489            Err(StreamError::Failed(message)) if message == "read_n finish boom"
3490        ));
3491        assert!(
3492            executor.opaque_logics[0]
3493                .as_mut()
3494                .unwrap()
3495                .get_in_handler_mut(inlet)
3496                .is_some()
3497        );
3498    }
3499
3500    #[test]
3501    fn opaque_request_drives_out_handler_for_buffered_output() {
3502        let state = Arc::new(Mutex::new(BufferedFlowState::default()));
3503        let graph = single_opaque_stage_graph(BufferedFlowOnPull {
3504            state: Arc::clone(&state),
3505        });
3506        let inlet = graph.shape.inlet().id();
3507        let outlet = graph.shape.outlet().id();
3508        let mut executor = FusedExecutor::new(&graph, FusedExecutionConfig::default());
3509        let mut output = Vec::<i32>::new();
3510        let mut output_sink = VecOutputSink {
3511            output: &mut output,
3512        };
3513
3514        executor
3515            .deliver(inlet, datum(7_i32), outlet, &mut output_sink)
3516            .unwrap();
3517        assert!(output_sink.output.is_empty());
3518
3519        executor.request(outlet, outlet, &mut output_sink).unwrap();
3520
3521        assert_eq!(&*output_sink.output, &[7]);
3522        let state = state.lock().unwrap();
3523        assert_eq!(state.pull_calls, 2);
3524        assert_eq!(state.finish_calls, 0);
3525    }
3526
3527    #[test]
3528    fn opaque_downstream_finish_before_first_demand_invokes_out_handler() {
3529        let state = Arc::new(Mutex::new(BufferedFlowState::default()));
3530        let graph = single_opaque_stage_graph(BufferedFlowOnPull {
3531            state: Arc::clone(&state),
3532        });
3533        let outlet = graph.shape.outlet().id();
3534        let mut executor = FusedExecutor::new(&graph, FusedExecutionConfig::default());
3535        let mut output = Vec::<i32>::new();
3536        let mut output_sink = VecOutputSink {
3537            output: &mut output,
3538        };
3539
3540        executor
3541            .downstream_finish(outlet, outlet, &mut output_sink)
3542            .unwrap();
3543        executor.request(outlet, outlet, &mut output_sink).unwrap();
3544
3545        assert!(output_sink.output.is_empty());
3546        let state = state.lock().unwrap();
3547        assert_eq!(state.pull_calls, 0);
3548        assert_eq!(state.finish_calls, 1);
3549    }
3550
3551    #[test]
3552    fn opaque_downstream_finish_drops_buffered_output_after_upstream_complete() {
3553        let state = Arc::new(Mutex::new(BufferedFlowState::default()));
3554        let graph = single_opaque_stage_graph(BufferedFlowOnPull {
3555            state: Arc::clone(&state),
3556        });
3557        let inlet = graph.shape.inlet().id();
3558        let outlet = graph.shape.outlet().id();
3559        let mut executor = FusedExecutor::new(&graph, FusedExecutionConfig::default());
3560        let mut output = Vec::<i32>::new();
3561        let mut output_sink = VecOutputSink {
3562            output: &mut output,
3563        };
3564
3565        executor
3566            .deliver(inlet, datum(11_i32), outlet, &mut output_sink)
3567            .unwrap();
3568        executor.complete(inlet, outlet, &mut output_sink).unwrap();
3569        executor
3570            .downstream_finish(outlet, outlet, &mut output_sink)
3571            .unwrap();
3572        executor.request(outlet, outlet, &mut output_sink).unwrap();
3573
3574        assert!(output_sink.output.is_empty());
3575        let state = state.lock().unwrap();
3576        assert_eq!(state.finish_calls, 1);
3577    }
3578
3579    #[test]
3580    fn broadcast_cancels_upstream_only_after_all_outlets_cancel() {
3581        let graph = GraphDsl::try_create(|builder| {
3582            let broadcast = builder.add(Broadcast::<i32>::new(2));
3583            let merge = builder.add(Merge::<i32>::new(2));
3584            builder.connect(broadcast.outlet(0)?, merge.inlet(0)?)?;
3585            builder.connect(broadcast.outlet(1)?, merge.inlet(1)?)?;
3586            Ok(FlowShape::new(broadcast.inlet(), merge.outlet()))
3587        })
3588        .unwrap();
3589
3590        let mut executor = FusedExecutor::new(&graph, FusedExecutionConfig::default());
3591        let broadcast_index = *executor
3592            .stage_by_inlet
3593            .get(&graph.shape.inlet().id())
3594            .unwrap();
3595        let first = graph.stages[broadcast_index].spec.outlets[0].id();
3596        let second = graph.stages[broadcast_index].spec.outlets[1].id();
3597
3598        let first_transition = executor
3599            .process_downstream_finish(broadcast_index, first)
3600            .unwrap();
3601        assert!(first_transition.cancelled_inlets.is_empty());
3602
3603        let second_transition = executor
3604            .process_downstream_finish(broadcast_index, second)
3605            .unwrap();
3606        assert_eq!(
3607            second_transition.cancelled_inlets,
3608            vec![graph.stages[broadcast_index].spec.inlets[0].id()]
3609        );
3610    }
3611
3612    #[test]
3613    fn downstream_finish_propagates_through_merge_and_broadcast() {
3614        let graph = GraphDsl::try_create(|builder| {
3615            let broadcast = builder.add(Broadcast::<i32>::new(2));
3616            let merge = builder.add(Merge::<i32>::new(2));
3617            builder.connect(broadcast.outlet(0)?, merge.inlet(0)?)?;
3618            builder.connect(broadcast.outlet(1)?, merge.inlet(1)?)?;
3619            Ok(FlowShape::new(broadcast.inlet(), merge.outlet()))
3620        })
3621        .unwrap();
3622
3623        let outlet = graph.shape.outlet().id();
3624        let mut executor = FusedExecutor::new(&graph, FusedExecutionConfig::default());
3625        let mut output = Vec::<i32>::new();
3626        let mut output_sink = VecOutputSink {
3627            output: &mut output,
3628        };
3629
3630        executor
3631            .downstream_finish(outlet, outlet, &mut output_sink)
3632            .unwrap();
3633
3634        let broadcast_index = *executor
3635            .stage_by_inlet
3636            .get(&graph.shape.inlet().id())
3637            .unwrap();
3638        let StageState::Broadcast {
3639            live_outlets,
3640            cancelled_outlets,
3641            ..
3642        } = &executor.stage_states[broadcast_index]
3643        else {
3644            panic!("expected broadcast state");
3645        };
3646        assert_eq!(*live_outlets, 0);
3647        assert_eq!(cancelled_outlets, &vec![true, true]);
3648    }
3649
3650    #[test]
3651    fn cyclic_graph_clears_pending_events_when_output_cancels() {
3652        struct CancelAfterFirst {
3653            emitted: usize,
3654        }
3655
3656        impl FusedOutputSink<i32> for CancelAfterFirst {
3657            fn emit(&mut self, _value: i32) -> StreamResult<()> {
3658                self.emitted += 1;
3659                Err(StreamError::Cancelled)
3660            }
3661        }
3662
3663        let graph = GraphDsl::try_create(|builder| {
3664            let merge = builder.add(MergePreferred::<i32>::new(1));
3665            let broadcast = builder.add(Broadcast::<i32>::new(2));
3666            let buffer = builder.add(Buffer::<i32>::new(8, OverflowStrategy::Backpressure));
3667            let positive = builder.add(TakeWhile::<i32>::new(|item| *item > 0));
3668            let decrement = builder.add(MapStage::new(|item: i32| item - 1));
3669
3670            builder.connect(merge.outlet(), broadcast.inlet())?;
3671            builder.connect(broadcast.outlet(1)?, buffer.inlet())?;
3672            builder.connect(buffer.outlet(), positive.inlet())?;
3673            builder.connect(positive.outlet(), decrement.inlet())?;
3674            builder.connect(decrement.outlet(), merge.preferred())?;
3675
3676            Ok(FlowShape::new(merge.secondary(0)?, broadcast.outlet(0)?))
3677        })
3678        .unwrap();
3679
3680        let graph_outlet = graph.shape.outlet().id();
3681        let graph_inlet = graph.shape.inlet().id();
3682        let mut executor = FusedExecutor::new(&graph, FusedExecutionConfig::default());
3683        let mut output = CancelAfterFirst { emitted: 0 };
3684
3685        executor
3686            .request(graph_outlet, graph_outlet, &mut output)
3687            .unwrap();
3688        let result = executor.deliver(graph_inlet, datum(1_i32), graph_outlet, &mut output);
3689
3690        assert_eq!(result, Err(StreamError::Cancelled));
3691        assert_eq!(output.emitted, 1);
3692        assert!(executor.event_stack.is_empty());
3693    }
3694
3695    // -- Typed cyclic feedback kernel (WP-opt-cycles) ------------------------
3696    //
3697    // The erased queued interpreter is the correctness oracle. Every supported
3698    // cyclic graph must produce identical output (and identical error) on
3699    // `ErasedOnly`, `TypedOnly`, and `Auto`. These tests force all three.
3700
3701    const CYCLE_LIMIT: FusedExecutionConfig = FusedExecutionConfig {
3702        event_limit: 5_000_000,
3703    };
3704
3705    /// Canonical `MergePreferred(1) → Broadcast(2)` feedback loop with a
3706    /// `Buffer → TakeWhile(> 0) → Map(- 1)` feedback chain (the benchmark shape).
3707    fn cyclic_feedback_i32(
3708        buffer_cap: usize,
3709        strategy: OverflowStrategy,
3710    ) -> GraphBlueprint<FlowShape<i32, i32>> {
3711        GraphDsl::try_create(|builder| {
3712            let merge = builder.add(MergePreferred::<i32>::new(1));
3713            let broadcast = builder.add(Broadcast::<i32>::new(2));
3714            let buffer = builder.add(Buffer::<i32>::new(buffer_cap, strategy));
3715            let positive = builder.add(TakeWhile::<i32>::new(|item| *item > 0));
3716            let decrement = builder.add(MapStage::new(|item: i32| item - 1));
3717
3718            builder.connect(merge.outlet(), broadcast.inlet())?;
3719            builder.connect(broadcast.outlet(1)?, buffer.inlet())?;
3720            builder.connect(buffer.outlet(), positive.inlet())?;
3721            builder.connect(positive.outlet(), decrement.inlet())?;
3722            builder.connect(decrement.outlet(), merge.preferred())?;
3723
3724            Ok(FlowShape::new(merge.secondary(0)?, broadcast.outlet(0)?))
3725        })
3726        .unwrap()
3727    }
3728
3729    /// Assert `ErasedOnly == TypedOnly == Auto` on the given input, and that the
3730    /// typed path was actually selected (i.e. `TypedOnly` did not error out).
3731    fn assert_cyclic_equiv_i32(graph: &GraphBlueprint<FlowShape<i32, i32>>, input: Vec<i32>) {
3732        let erased = graph
3733            .run_with_input_report_mode(input.clone(), CYCLE_LIMIT, ExecutorMode::ErasedOnly)
3734            .map(|r| r.output);
3735        let typed = graph
3736            .run_with_input_report_mode(input.clone(), CYCLE_LIMIT, ExecutorMode::TypedOnly)
3737            .map(|r| r.output);
3738        let auto = graph
3739            .run_with_input_report_mode(input.clone(), CYCLE_LIMIT, ExecutorMode::Auto)
3740            .map(|r| r.output);
3741        assert!(
3742            typed.is_ok(),
3743            "typed cyclic path was not selected for {input:?}: {typed:?}"
3744        );
3745        assert_eq!(erased, typed, "typed != erased for input {input:?}");
3746        assert_eq!(erased, auto, "auto != erased for input {input:?}");
3747    }
3748
3749    #[test]
3750    fn cyclic_typed_matches_erased_single_and_multi_input() {
3751        let graph = cyclic_feedback_i32(16, OverflowStrategy::Backpressure);
3752        for input in [
3753            vec![],
3754            vec![0],
3755            vec![1],
3756            vec![5],
3757            vec![100],
3758            vec![2, 5],
3759            vec![5, 2],
3760            vec![0, 3],
3761            vec![3, 0, 2],
3762            vec![1, 1, 1],
3763            vec![-1],
3764            vec![4, -3, 7],
3765        ] {
3766            assert_cyclic_equiv_i32(&graph, input);
3767        }
3768    }
3769
3770    #[test]
3771    fn cyclic_typed_matches_erased_across_buffer_configs() {
3772        // Buffer is a pass-through in the synchronous fused executor regardless
3773        // of capacity or overflow strategy; confirm against the oracle.
3774        for cap in [1usize, 2, 8, 64] {
3775            for strategy in [
3776                OverflowStrategy::Backpressure,
3777                OverflowStrategy::DropHead,
3778                OverflowStrategy::DropTail,
3779                OverflowStrategy::DropBuffer,
3780                OverflowStrategy::DropNew,
3781                OverflowStrategy::Fail,
3782            ] {
3783                let graph = cyclic_feedback_i32(cap, strategy);
3784                for input in [vec![6], vec![3, 0, 4], vec![1, 1]] {
3785                    assert_cyclic_equiv_i32(&graph, input);
3786                }
3787            }
3788        }
3789    }
3790
3791    #[test]
3792    fn cyclic_typed_falls_back_for_feedback_first_broadcast_orientation() {
3793        // Feedback on outlet(0), graph output on outlet(1). The erased oracle
3794        // drains outlet(0) (feedback) before outlet(1) (output), giving a
3795        // feedback-first interleaving the output-first typed kernel does not
3796        // reproduce. The planner must decline this orientation (TypedOnly errors)
3797        // and Auto must fall back to the erased interpreter.
3798        let graph = GraphDsl::try_create(|builder| {
3799            let merge = builder.add(MergePreferred::<i32>::new(1));
3800            let broadcast = builder.add(Broadcast::<i32>::new(2));
3801            let buffer = builder.add(Buffer::<i32>::new(4, OverflowStrategy::Backpressure));
3802            let positive = builder.add(TakeWhile::<i32>::new(|item| *item > 0));
3803            let decrement = builder.add(MapStage::new(|item: i32| item - 1));
3804
3805            builder.connect(merge.outlet(), broadcast.inlet())?;
3806            // Feedback on outlet(0); graph output on outlet(1).
3807            builder.connect(broadcast.outlet(0)?, buffer.inlet())?;
3808            builder.connect(buffer.outlet(), positive.inlet())?;
3809            builder.connect(positive.outlet(), decrement.inlet())?;
3810            builder.connect(decrement.outlet(), merge.preferred())?;
3811
3812            Ok(FlowShape::new(merge.secondary(0)?, broadcast.outlet(1)?))
3813        })
3814        .unwrap();
3815        for input in [vec![5], vec![2, 4], vec![0, 3]] {
3816            let typed = graph.run_with_input_report_mode(
3817                input.clone(),
3818                CYCLE_LIMIT,
3819                ExecutorMode::TypedOnly,
3820            );
3821            assert!(
3822                matches!(typed, Err(StreamError::GraphValidation(_))),
3823                "feedback-first orientation should not be typed-supported: {typed:?}"
3824            );
3825            let erased = graph
3826                .run_with_input_report_mode(input.clone(), CYCLE_LIMIT, ExecutorMode::ErasedOnly)
3827                .map(|r| r.output);
3828            let auto = graph
3829                .run_with_input_report_mode(input.clone(), CYCLE_LIMIT, ExecutorMode::Auto)
3830                .map(|r| r.output);
3831            assert_eq!(
3832                erased, auto,
3833                "auto must match erased on fallback for {input:?}"
3834            );
3835        }
3836    }
3837
3838    #[test]
3839    fn cyclic_typed_matches_erased_map_before_takewhile_and_identity() {
3840        // Map(-1) before TakeWhile(>= 0), plus an Identity stage in the chain.
3841        let graph = GraphDsl::try_create(|builder| {
3842            let merge = builder.add(MergePreferred::<i32>::new(1));
3843            let broadcast = builder.add(Broadcast::<i32>::new(2));
3844            let decrement = builder.add(MapStage::new(|item: i32| item - 1));
3845            let nonneg = builder.add(TakeWhile::<i32>::new(|item| *item >= 0));
3846            let passthrough = builder.add(Identity::<i32>::new());
3847
3848            builder.connect(merge.outlet(), broadcast.inlet())?;
3849            builder.connect(broadcast.outlet(1)?, decrement.inlet())?;
3850            builder.connect(decrement.outlet(), nonneg.inlet())?;
3851            builder.connect(nonneg.outlet(), passthrough.inlet())?;
3852            builder.connect(passthrough.outlet(), merge.preferred())?;
3853
3854            Ok(FlowShape::new(merge.secondary(0)?, broadcast.outlet(0)?))
3855        })
3856        .unwrap();
3857        for input in [vec![5], vec![0], vec![3, 1, 4], vec![10, 0]] {
3858            assert_cyclic_equiv_i32(&graph, input);
3859        }
3860    }
3861
3862    #[test]
3863    fn cyclic_typed_matches_erased_u64_elements() {
3864        let graph = GraphDsl::try_create(|builder| {
3865            let merge = builder.add(MergePreferred::<u64>::new(1));
3866            let broadcast = builder.add(Broadcast::<u64>::new(2));
3867            let buffer = builder.add(Buffer::<u64>::new(16, OverflowStrategy::Backpressure));
3868            let positive = builder.add(TakeWhile::<u64>::new(|item| *item > 0));
3869            let decrement = builder.add(MapStage::new(|item: u64| item - 1));
3870
3871            builder.connect(merge.outlet(), broadcast.inlet())?;
3872            builder.connect(broadcast.outlet(1)?, buffer.inlet())?;
3873            builder.connect(buffer.outlet(), positive.inlet())?;
3874            builder.connect(positive.outlet(), decrement.inlet())?;
3875            builder.connect(decrement.outlet(), merge.preferred())?;
3876
3877            Ok(FlowShape::new(merge.secondary(0)?, broadcast.outlet(0)?))
3878        })
3879        .unwrap();
3880        for input in [vec![0u64], vec![7u64], vec![3u64, 5], vec![10_000u64]] {
3881            let erased = graph
3882                .run_with_input_report_mode(input.clone(), CYCLE_LIMIT, ExecutorMode::ErasedOnly)
3883                .map(|r| r.output);
3884            let typed = graph
3885                .run_with_input_report_mode(input.clone(), CYCLE_LIMIT, ExecutorMode::TypedOnly)
3886                .map(|r| r.output);
3887            let auto = graph
3888                .run_with_input_report_mode(input.clone(), CYCLE_LIMIT, ExecutorMode::Auto)
3889                .map(|r| r.output);
3890            assert!(typed.is_ok(), "typed not selected for u64 input {input:?}");
3891            assert_eq!(erased, typed, "u64 typed != erased for {input:?}");
3892            assert_eq!(erased, auto, "u64 auto != erased for {input:?}");
3893        }
3894    }
3895
3896    #[test]
3897    fn cyclic_typed_unproductive_cycle_surfaces_event_limit_like_erased() {
3898        // No TakeWhile in the feedback chain → the cycle never terminates.
3899        // Both executors must surface `EventLimitExceeded { limit }`, not hang.
3900        let graph = GraphDsl::try_create(|builder| {
3901            let merge = builder.add(MergePreferred::<i32>::new(1));
3902            let broadcast = builder.add(Broadcast::<i32>::new(2));
3903            let buffer = builder.add(Buffer::<i32>::new(8, OverflowStrategy::Backpressure));
3904            let increment = builder.add(MapStage::new(|item: i32| item + 1));
3905
3906            builder.connect(merge.outlet(), broadcast.inlet())?;
3907            builder.connect(broadcast.outlet(1)?, buffer.inlet())?;
3908            builder.connect(buffer.outlet(), increment.inlet())?;
3909            builder.connect(increment.outlet(), merge.preferred())?;
3910
3911            Ok(FlowShape::new(merge.secondary(0)?, broadcast.outlet(0)?))
3912        })
3913        .unwrap();
3914        let config = FusedExecutionConfig { event_limit: 512 };
3915        let erased = graph.run_with_input_report_mode(vec![1], config, ExecutorMode::ErasedOnly);
3916        let typed = graph.run_with_input_report_mode(vec![1], config, ExecutorMode::TypedOnly);
3917        let auto = graph.run_with_input_report_mode(vec![1], config, ExecutorMode::Auto);
3918        assert_eq!(
3919            erased.map(|r| r.output),
3920            Err(StreamError::EventLimitExceeded { limit: 512 })
3921        );
3922        assert_eq!(
3923            typed.map(|r| r.output),
3924            Err(StreamError::EventLimitExceeded { limit: 512 })
3925        );
3926        assert_eq!(
3927            auto.map(|r| r.output),
3928            Err(StreamError::EventLimitExceeded { limit: 512 })
3929        );
3930    }
3931
3932    #[test]
3933    fn cyclic_typed_falls_back_for_plain_merge() {
3934        // Plain `Merge` (not `MergePreferred`) is not a recognized feedback
3935        // shape: `TypedOnly` errors and `Auto` falls back to the erased path.
3936        let graph = GraphDsl::try_create(|builder| {
3937            let merge = builder.add(Merge::<i32>::new(2));
3938            let broadcast = builder.add(Broadcast::<i32>::new(2));
3939            let buffer = builder.add(Buffer::<i32>::new(8, OverflowStrategy::Backpressure));
3940            let positive = builder.add(TakeWhile::<i32>::new(|item| *item > 0));
3941            let decrement = builder.add(MapStage::new(|item: i32| item - 1));
3942
3943            builder.connect(merge.outlet(), broadcast.inlet())?;
3944            builder.connect(broadcast.outlet(1)?, buffer.inlet())?;
3945            builder.connect(buffer.outlet(), positive.inlet())?;
3946            builder.connect(positive.outlet(), decrement.inlet())?;
3947            builder.connect(decrement.outlet(), merge.inlet(1)?)?;
3948
3949            Ok(FlowShape::new(merge.inlet(0)?, broadcast.outlet(0)?))
3950        })
3951        .unwrap();
3952        let typed = graph.run_with_input_report_mode(vec![3], CYCLE_LIMIT, ExecutorMode::TypedOnly);
3953        assert!(
3954            matches!(typed, Err(StreamError::GraphValidation(_))),
3955            "plain Merge cycle should not be typed-supported: {typed:?}"
3956        );
3957        let erased = graph
3958            .run_with_input_report_mode(vec![3], CYCLE_LIMIT, ExecutorMode::ErasedOnly)
3959            .map(|r| r.output);
3960        let auto = graph
3961            .run_with_input_report_mode(vec![3], CYCLE_LIMIT, ExecutorMode::Auto)
3962            .map(|r| r.output);
3963        assert_eq!(erased, auto, "auto must match erased on fallback");
3964    }
3965
3966    #[test]
3967    fn cyclic_typed_falls_back_for_custom_opaque_in_feedback() {
3968        // A custom opaque stage in the feedback chain is not typed-recoverable.
3969        let graph = GraphDsl::try_create(|builder| {
3970            let merge = builder.add(MergePreferred::<i32>::new(1));
3971            let broadcast = builder.add(Broadcast::<i32>::new(2));
3972            let positive = builder.add(TakeWhile::<i32>::new(|item| *item > 0));
3973            let decrement = builder.add(MapStage::new(|item: i32| item - 1));
3974            let custom = builder.add(BufferedFlowOnPull {
3975                state: Arc::new(Mutex::new(BufferedFlowState::default())),
3976            });
3977
3978            builder.connect(merge.outlet(), broadcast.inlet())?;
3979            builder.connect(broadcast.outlet(1)?, positive.inlet())?;
3980            builder.connect(positive.outlet(), decrement.inlet())?;
3981            builder.connect(decrement.outlet(), custom.inlet())?;
3982            builder.connect(custom.outlet(), merge.preferred())?;
3983
3984            Ok(FlowShape::new(merge.secondary(0)?, broadcast.outlet(0)?))
3985        })
3986        .unwrap();
3987        let typed = graph.run_with_input_report_mode(vec![4], CYCLE_LIMIT, ExecutorMode::TypedOnly);
3988        assert!(
3989            matches!(typed, Err(StreamError::GraphValidation(_))),
3990            "custom opaque feedback stage should fall back: {typed:?}"
3991        );
3992        let erased = graph
3993            .run_with_input_report_mode(vec![4], CYCLE_LIMIT, ExecutorMode::ErasedOnly)
3994            .map(|r| r.output);
3995        let auto = graph
3996            .run_with_input_report_mode(vec![4], CYCLE_LIMIT, ExecutorMode::Auto)
3997            .map(|r| r.output);
3998        assert_eq!(erased, auto, "auto must match erased on fallback");
3999    }
4000
4001    #[test]
4002    fn cyclic_typed_matches_erased_randomized() {
4003        // Deterministic pseudo-random inputs over the canonical graph; the typed
4004        // kernel must agree with the erased oracle on every sequence.
4005        let graph = cyclic_feedback_i32(16, OverflowStrategy::Backpressure);
4006        let mut state: u64 = 0x9E37_79B9_7F4A_7C15;
4007        let mut next = || {
4008            state ^= state << 13;
4009            state ^= state >> 7;
4010            state ^= state << 17;
4011            state
4012        };
4013        for _ in 0..200 {
4014            let len = (next() % 6) as usize;
4015            let input: Vec<i32> = (0..len).map(|_| (next() % 20) as i32 - 5).collect();
4016            assert_cyclic_equiv_i32(&graph, input);
4017        }
4018    }
4019
4020    #[test]
4021    fn partition_holds_routed_element_until_target_outlet_pulls() {
4022        let graph = GraphDsl::create(|builder| {
4023            builder.add(Partition::<i32>::new(2, |value| usize::from(*value >= 10)))
4024        })
4025        .unwrap();
4026
4027        let stage_index = 0usize;
4028        let inlet = graph.shape.inlet().id();
4029        let out0 = graph.shape.outlet(0).unwrap().id();
4030        let out1 = graph.shape.outlet(1).unwrap().id();
4031        let mut executor = FusedExecutor::new(&graph, FusedExecutionConfig::default());
4032
4033        executor.process_pull(stage_index, out0).unwrap();
4034        let transition = executor
4035            .process_stage(stage_index, inlet, datum(11_i32))
4036            .unwrap();
4037        assert!(matches!(transition.emissions, StageEmissions::None));
4038
4039        let pull_transition = executor.process_pull(stage_index, out1).unwrap();
4040        match pull_transition.emissions {
4041            StageEmissions::One(port, value) => {
4042                assert_eq!(port, out1);
4043                assert_eq!(
4044                    downcast_datum::<i32, _>(value, "emit", || "Partition.out1").unwrap(),
4045                    11
4046                );
4047            }
4048            _ => panic!("expected one pending partition emission"),
4049        }
4050    }
4051
4052    #[test]
4053    fn partition_cancels_upstream_only_after_all_outlets_cancel_when_not_eager() {
4054        let graph = GraphDsl::create(|builder| {
4055            builder.add(Partition::<i32>::new(2, |value| usize::from(*value >= 10)))
4056        })
4057        .unwrap();
4058
4059        let stage_index = 0usize;
4060        let out0 = graph.shape.outlet(0).unwrap().id();
4061        let out1 = graph.shape.outlet(1).unwrap().id();
4062        let mut executor = FusedExecutor::new(&graph, FusedExecutionConfig::default());
4063
4064        let first = executor
4065            .process_downstream_finish(stage_index, out0)
4066            .unwrap();
4067        assert!(first.cancelled_inlets.is_empty());
4068
4069        let second = executor
4070            .process_downstream_finish(stage_index, out1)
4071            .unwrap();
4072        assert_eq!(second.cancelled_inlets, vec![graph.shape.inlet().id()]);
4073    }
4074
4075    #[test]
4076    fn unzip_continues_emitting_to_live_outlet_after_peer_cancels() {
4077        let graph =
4078            GraphDsl::create(|builder| builder.add(Unzip::<i32, &'static str>::new())).unwrap();
4079
4080        let stage_index = 0usize;
4081        let inlet = graph.shape.inlet().id();
4082        let out0 = graph.shape.out0().id();
4083        let out1 = graph.shape.out1().id();
4084        let mut executor = FusedExecutor::new(&graph, FusedExecutionConfig::default());
4085
4086        executor.process_pull(stage_index, out0).unwrap();
4087        executor.process_pull(stage_index, out1).unwrap();
4088        let cancel = executor
4089            .process_downstream_finish(stage_index, out1)
4090            .unwrap();
4091        assert!(cancel.cancelled_inlets.is_empty());
4092
4093        let transition = executor
4094            .process_stage(stage_index, inlet, datum((7_i32, "seven")))
4095            .unwrap();
4096        match transition.emissions {
4097            StageEmissions::One(port, value) => {
4098                assert_eq!(port, out0);
4099                assert_eq!(
4100                    downcast_datum::<i32, _>(value, "emit", || "Unzip.out0").unwrap(),
4101                    7
4102                );
4103            }
4104            StageEmissions::Many(values) => {
4105                assert_eq!(values.len(), 1);
4106                assert_eq!(values[0].0, out0);
4107            }
4108            _ => panic!("expected emission to the remaining live unzip outlet"),
4109        }
4110    }
4111
4112    #[test]
4113    fn unzip_cancels_upstream_only_after_both_outlets_cancel() {
4114        let graph =
4115            GraphDsl::create(|builder| builder.add(Unzip::<i32, &'static str>::new())).unwrap();
4116
4117        let stage_index = 0usize;
4118        let out0 = graph.shape.out0().id();
4119        let out1 = graph.shape.out1().id();
4120        let mut executor = FusedExecutor::new(&graph, FusedExecutionConfig::default());
4121
4122        let first = executor
4123            .process_downstream_finish(stage_index, out0)
4124            .unwrap();
4125        assert!(first.cancelled_inlets.is_empty());
4126
4127        let second = executor
4128            .process_downstream_finish(stage_index, out1)
4129            .unwrap();
4130        assert_eq!(second.cancelled_inlets, vec![graph.shape.inlet().id()]);
4131    }
4132
4133    #[test]
4134    fn opaque_internal_outlet_repulls_after_first_emission() {
4135        let graph = GraphDsl::try_create(|builder| {
4136            let opaque = builder.add(EmitMultipleOnPush);
4137            let identity = builder.add(Identity::<i32>::new());
4138            builder.connect(opaque.outlet(), identity.inlet())?;
4139            Ok(FlowShape::new(opaque.inlet(), identity.outlet()))
4140        })
4141        .unwrap();
4142
4143        assert_eq!(graph.run_with_input([10]).unwrap(), vec![1, 2]);
4144    }
4145
4146    // -- Phase 0 (WP-18): ExecutorMode scaffolding tests -------------------
4147
4148    /// Verifies that `Auto` and `ErasedOnly` produce identical results on a
4149    /// representative junction graph (Broadcast→Merge) and that `TypedOnly`
4150    /// errors cleanly (junctions are not yet on the typed path).
4151    ///
4152    /// Graph topology:
4153    ///   in → Broadcast(2) → out0 → Merge(2).in0 → out
4154    ///                      → out1 → Merge(2).in1
4155    ///
4156    /// This exercises fan-out + fan-in junctions that cannot hit the typed
4157    /// linear fast path, so Auto falls back to the erased executor.
4158    #[test]
4159    fn executor_mode_auto_erased_identical_typed_errors() {
4160        let graph = GraphDsl::try_create(|builder| {
4161            let broadcast = builder.add(Broadcast::<i32>::new(2));
4162            let merge = builder.add(Merge::<i32>::new(2));
4163            builder.connect(broadcast.outlet(0)?, merge.inlet(0)?)?;
4164            builder.connect(broadcast.outlet(1)?, merge.inlet(1)?)?;
4165            Ok(FlowShape::new(broadcast.inlet(), merge.outlet()))
4166        })
4167        .unwrap();
4168
4169        let input = vec![1, 2, 3];
4170
4171        let auto_result = graph
4172            .run_with_input_mode(input.clone(), ExecutorMode::Auto)
4173            .unwrap();
4174        let erased_result = graph
4175            .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
4176            .unwrap();
4177
4178        // Auto and ErasedOnly must produce identical output (broadcast
4179        // duplicates each element onto both merge inlets; merge emits both).
4180        assert_eq!(auto_result, erased_result);
4181        // Each element is broadcast to both merge inlets, so output is 2× input.
4182        assert_eq!(auto_result.len(), input.len() * 2);
4183
4184        // TypedOnly must error because junctions are not on the typed path yet.
4185        let typed_result = graph.run_with_input_mode(input.clone(), ExecutorMode::TypedOnly);
4186        assert!(
4187            matches!(
4188                typed_result,
4189                Err(StreamError::GraphValidation(ref msg))
4190                if msg.contains("typed executor does not support this graph shape")
4191            ),
4192            "expected TypedOnly to error for junction graph, got: {typed_result:?}"
4193        );
4194    }
4195
4196    // -- Phase 2 (WP-18): typed-vs-erased equivalence tests ----------------
4197
4198    /// Builds an identity chain of `n` stages with the given type.
4199    fn identity_chain_bp(n: usize) -> GraphBlueprint<FlowShape<i64, i64>> {
4200        assert!(n >= 1);
4201        GraphDsl::try_create(|builder| {
4202            let first = builder.add(Identity::<i64>::new());
4203            let inlet = first.inlet();
4204            let mut outlet = first.outlet();
4205            for _ in 1..n {
4206                let next = builder.add(Identity::<i64>::new());
4207                builder.connect(outlet, next.inlet())?;
4208                outlet = next.outlet();
4209            }
4210            Ok(FlowShape::new(inlet, outlet))
4211        })
4212        .unwrap()
4213    }
4214
4215    /// Builds a map chain of `n` stages that double each value.
4216    fn map_chain_bp(n: usize) -> GraphBlueprint<FlowShape<i64, i64>> {
4217        assert!(n >= 1);
4218        GraphDsl::try_create(|builder| {
4219            let first = builder.add(MapStage::new(|x: i64| x.wrapping_mul(2)));
4220            let inlet = first.inlet();
4221            let mut outlet = first.outlet();
4222            for _ in 1..n {
4223                let next = builder.add(MapStage::new(|x: i64| x.wrapping_mul(2)));
4224                builder.connect(outlet, next.inlet())?;
4225                outlet = next.outlet();
4226            }
4227            Ok(FlowShape::new(inlet, outlet))
4228        })
4229        .unwrap()
4230    }
4231
4232    /// `ErasedOnly` and `TypedOnly` (via `run_with_input_mode`) produce
4233    /// identical output on a 5-stage identity graph.
4234    #[test]
4235    fn typed_erased_equivalence_identity_collect() {
4236        let graph = identity_chain_bp(5);
4237        let input: Vec<i64> = (0..20).collect();
4238
4239        let erased = graph
4240            .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
4241            .unwrap();
4242        let typed = graph
4243            .run_with_input_mode(input.clone(), ExecutorMode::TypedOnly)
4244            .unwrap();
4245
4246        assert_eq!(
4247            typed, erased,
4248            "typed and erased paths disagree on identity×5"
4249        );
4250    }
4251
4252    /// `ErasedOnly` and `TypedOnly` produce identical count on a 5-stage
4253    /// identity graph.
4254    #[test]
4255    fn typed_erased_equivalence_identity_count() {
4256        let graph = identity_chain_bp(5);
4257        let input: Vec<i64> = (0..20).collect();
4258        let config = FusedExecutionConfig::default();
4259
4260        let erased = graph
4261            .run_count_with_input_report_mode(input.clone(), config, ExecutorMode::ErasedOnly)
4262            .unwrap()
4263            .result;
4264        let typed = graph
4265            .run_count_with_input_report_mode(input.clone(), config, ExecutorMode::TypedOnly)
4266            .unwrap()
4267            .result;
4268
4269        assert_eq!(typed, erased, "typed and erased count differ on identity×5");
4270    }
4271
4272    /// `ErasedOnly` and `TypedOnly` produce identical fold result on a 5-stage
4273    /// map graph (each stage doubles the value).
4274    #[test]
4275    fn typed_erased_equivalence_map_fold() {
4276        let graph = map_chain_bp(5);
4277        let input: Vec<i64> = (1..=10).collect();
4278        let config = FusedExecutionConfig::default();
4279
4280        let erased = graph
4281            .run_fold_with_input_report_mode(
4282                input.clone(),
4283                0i64,
4284                |acc, x| acc.wrapping_add(x),
4285                config,
4286                ExecutorMode::ErasedOnly,
4287            )
4288            .unwrap()
4289            .result;
4290        let typed = graph
4291            .run_fold_with_input_report_mode(
4292                input.clone(),
4293                0i64,
4294                |acc, x| acc.wrapping_add(x),
4295                config,
4296                ExecutorMode::TypedOnly,
4297            )
4298            .unwrap()
4299            .result;
4300
4301        assert_eq!(typed, erased, "typed and erased fold differ on map×5");
4302    }
4303
4304    /// `ErasedOnly` and `TypedOnly` produce identical output on a 5-stage map
4305    /// graph.
4306    #[test]
4307    fn typed_erased_equivalence_map_collect() {
4308        let graph = map_chain_bp(5);
4309        let input: Vec<i64> = (0..20).collect();
4310
4311        let erased = graph
4312            .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
4313            .unwrap();
4314        let typed = graph
4315            .run_with_input_mode(input.clone(), ExecutorMode::TypedOnly)
4316            .unwrap();
4317
4318        assert_eq!(typed, erased, "typed and erased paths disagree on map×5");
4319    }
4320
4321    /// `TypedOnly` on a junction graph (Broadcast→Merge) returns a
4322    /// `GraphValidation` error (junctions not yet on the typed path).
4323    #[test]
4324    fn typed_only_errors_on_junction_graph() {
4325        let graph = GraphDsl::try_create(|builder| {
4326            let broadcast = builder.add(Broadcast::<i32>::new(2));
4327            let merge = builder.add(Merge::<i32>::new(2));
4328            builder.connect(broadcast.outlet(0)?, merge.inlet(0)?)?;
4329            builder.connect(broadcast.outlet(1)?, merge.inlet(1)?)?;
4330            Ok(FlowShape::new(broadcast.inlet(), merge.outlet()))
4331        })
4332        .unwrap();
4333
4334        let result = graph.run_with_input_mode(vec![1], ExecutorMode::TypedOnly);
4335        assert!(
4336            matches!(
4337                result,
4338                Err(StreamError::GraphValidation(ref msg))
4339                if msg.contains("typed executor does not support this graph shape")
4340            ),
4341            "expected TypedOnly to error on junction, got: {result:?}"
4342        );
4343    }
4344
4345    /// `Auto` on a junction graph falls back silently to the erased executor.
4346    #[test]
4347    fn auto_falls_back_silently_for_junction_graph() {
4348        let graph = GraphDsl::try_create(|builder| {
4349            let broadcast = builder.add(Broadcast::<i32>::new(2));
4350            let merge = builder.add(Merge::<i32>::new(2));
4351            builder.connect(broadcast.outlet(0)?, merge.inlet(0)?)?;
4352            builder.connect(broadcast.outlet(1)?, merge.inlet(1)?)?;
4353            Ok(FlowShape::new(broadcast.inlet(), merge.outlet()))
4354        })
4355        .unwrap();
4356
4357        let result = graph.run_with_input_mode(vec![1, 2, 3], ExecutorMode::Auto);
4358        assert!(result.is_ok(), "Auto should succeed (fallback to erased)");
4359        assert_eq!(result.unwrap().len(), 6); // broadcast × 2
4360    }
4361
4362    // -- Phase 3a (WP-18): typed MergeSequence equivalence tests ------------
4363
4364    /// Build the Unzip → MergeSequence graph used in the benchmark.
4365    fn merge_sequence_graph() -> GraphBlueprint<FlowShape<(u64, u64), u64>> {
4366        GraphDsl::try_create(|builder| {
4367            let unzip = builder.add(Unzip::<u64, u64>::new());
4368            let merge = builder.add(MergeSequence::<u64>::new(2, |item| *item));
4369            builder.connect(unzip.out0(), merge.inlet(0)?)?;
4370            builder.connect(unzip.out1(), merge.inlet(1)?)?;
4371            Ok(FlowShape::new(unzip.inlet(), merge.outlet()))
4372        })
4373        .unwrap()
4374    }
4375
4376    /// `TypedOnly` accepts the Unzip → MergeSequence topology (does not
4377    /// error with "typed executor does not support this graph shape").
4378    #[test]
4379    fn typed_only_accepts_merge_sequence_topology() {
4380        let graph = merge_sequence_graph();
4381        let result =
4382            graph.run_with_input_mode(vec![(0u64, 1u64), (2u64, 3u64)], ExecutorMode::TypedOnly);
4383        assert!(
4384            result.is_ok(),
4385            "TypedOnly should accept Unzip→MergeSequence topology, got: {result:?}"
4386        );
4387    }
4388
4389    /// `ErasedOnly` and `TypedOnly` produce identical output on an in-order
4390    /// Unzip → MergeSequence graph.
4391    #[test]
4392    fn typed_erased_equivalence_merge_sequence_in_order() {
4393        let graph = merge_sequence_graph();
4394        let input: Vec<(u64, u64)> = (0..10).step_by(2).map(|i| (i, i + 1)).collect();
4395
4396        let erased = graph
4397            .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
4398            .unwrap();
4399        let typed = graph
4400            .run_with_input_mode(input.clone(), ExecutorMode::TypedOnly)
4401            .unwrap();
4402
4403        assert_eq!(
4404            typed, erased,
4405            "typed and erased disagree on in-order merge_sequence"
4406        );
4407        // Verify the output is actually sorted.
4408        let expected: Vec<u64> = (0..10).collect();
4409        assert_eq!(typed, expected);
4410    }
4411
4412    /// `ErasedOnly` and `TypedOnly` produce identical output on an adversarial
4413    /// (out-of-order) Unzip → MergeSequence graph where pairs are (even, odd)
4414    /// with `even > odd - 1`, forcing out-of-order arrivals.
4415    #[test]
4416    fn typed_erased_equivalence_merge_sequence_out_of_order() {
4417        let graph = merge_sequence_graph();
4418        // Pairs (1,0), (3,2), (5,4): out0 gets larger sequence, out1 smaller.
4419        let input: Vec<(u64, u64)> = vec![(1, 0), (3, 2), (5, 4)];
4420
4421        let erased = graph
4422            .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
4423            .unwrap();
4424        let typed = graph
4425            .run_with_input_mode(input.clone(), ExecutorMode::TypedOnly)
4426            .unwrap();
4427
4428        assert_eq!(
4429            typed, erased,
4430            "typed and erased disagree on out-of-order merge_sequence"
4431        );
4432        // Output should be sorted.
4433        assert_eq!(typed, vec![0u64, 1, 2, 3, 4, 5]);
4434    }
4435
4436    /// Both `ErasedOnly` and `TypedOnly` fail with a sequence-gap error when
4437    /// the pending items can never be resolved at completion.
4438    ///
4439    /// This is the #78 regression: `merge_sequence_fails_on_gap_at_completion`.
4440    #[test]
4441    fn typed_erased_equivalence_merge_sequence_gap_failure() {
4442        // Sequences 1 and 2 arrive (via the unzip split of pair (1, 2)), but
4443        // sequence 0 never does.  Both paths must return an error.
4444        let graph = merge_sequence_graph();
4445        let input = vec![(1u64, 2u64)];
4446
4447        let erased = graph.run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly);
4448        let typed = graph.run_with_input_mode(input.clone(), ExecutorMode::TypedOnly);
4449
4450        assert!(
4451            matches!(&erased, Err(StreamError::Failed(msg)) if msg.contains("expected sequence")),
4452            "ErasedOnly should fail on gap: {erased:?}"
4453        );
4454        assert!(
4455            matches!(&typed, Err(StreamError::Failed(msg)) if msg.contains("expected sequence")),
4456            "TypedOnly should fail on gap: {typed:?}"
4457        );
4458    }
4459
4460    /// `ErasedOnly` and `TypedOnly` both complete cleanly with a single-item
4461    /// in-order input.
4462    #[test]
4463    fn typed_erased_equivalence_merge_sequence_completion() {
4464        let graph = merge_sequence_graph();
4465        let input = vec![(0u64, 1u64)];
4466
4467        let erased = graph
4468            .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
4469            .unwrap();
4470        let typed = graph
4471            .run_with_input_mode(input.clone(), ExecutorMode::TypedOnly)
4472            .unwrap();
4473
4474        assert_eq!(typed, erased, "typed and erased disagree on completion");
4475        assert_eq!(typed, vec![0u64, 1u64]);
4476    }
4477
4478    /// `Auto` selects the typed path for the Unzip → MergeSequence topology
4479    /// and produces the same result as `ErasedOnly`.
4480    #[test]
4481    fn auto_selects_typed_for_merge_sequence_topology() {
4482        let graph = merge_sequence_graph();
4483        let input: Vec<(u64, u64)> = (0..20).step_by(2).map(|i| (i, i + 1)).collect();
4484
4485        let auto_result = graph
4486            .run_with_input_mode(input.clone(), ExecutorMode::Auto)
4487            .unwrap();
4488        let erased_result = graph
4489            .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
4490            .unwrap();
4491
4492        assert_eq!(
4493            auto_result, erased_result,
4494            "Auto and ErasedOnly disagree on merge_sequence topology"
4495        );
4496    }
4497
4498    // -- Phase 3b (WP-18): typed MergeLatest equivalence tests ---------------
4499
4500    /// Build the Unzip → MergeLatest graph used in the benchmark.
4501    fn merge_latest_graph_exec() -> GraphBlueprint<FlowShape<(u64, u64), Vec<u64>>> {
4502        GraphDsl::try_create(|builder| {
4503            let unzip = builder.add(Unzip::<u64, u64>::new());
4504            let merge = builder.add(MergeLatest::<u64>::new(2, false));
4505            builder.connect(unzip.out0(), merge.inlet(0)?)?;
4506            builder.connect(unzip.out1(), merge.inlet(1)?)?;
4507            Ok(FlowShape::new(unzip.inlet(), merge.outlet()))
4508        })
4509        .unwrap()
4510    }
4511
4512    /// Build a Unzip → MergeLatest graph with `eager_complete = true`.
4513    fn merge_latest_eager_graph_exec() -> GraphBlueprint<FlowShape<(i32, i32), Vec<i32>>> {
4514        GraphDsl::try_create(|builder| {
4515            let unzip = builder.add(Unzip::<i32, i32>::new());
4516            let merge = builder.add(MergeLatest::<i32>::new(2, true));
4517            builder.connect(unzip.out0(), merge.inlet(0)?)?;
4518            builder.connect(unzip.out1(), merge.inlet(1)?)?;
4519            Ok(FlowShape::new(unzip.inlet(), merge.outlet()))
4520        })
4521        .unwrap()
4522    }
4523
4524    /// `TypedOnly` accepts the Unzip → MergeLatest topology.
4525    #[test]
4526    fn typed_only_accepts_merge_latest_topology() {
4527        let graph = merge_latest_graph_exec();
4528        let result =
4529            graph.run_with_input_mode(vec![(0u64, 1u64), (2u64, 3u64)], ExecutorMode::TypedOnly);
4530        assert!(
4531            result.is_ok(),
4532            "TypedOnly should accept Unzip→MergeLatest topology, got: {result:?}"
4533        );
4534    }
4535
4536    /// `ErasedOnly` and `TypedOnly` produce identical latest-snapshot sequences.
4537    #[test]
4538    fn typed_erased_equivalence_merge_latest_snapshot_ordering() {
4539        let graph = merge_latest_graph_exec();
4540        // Each pair (a, b) updates both inlets; after the first pair all inlets
4541        // are seen and every subsequent pair also produces a snapshot.
4542        let input: Vec<(u64, u64)> = (0..10).map(|i| (i, i + 100)).collect();
4543
4544        let erased = graph
4545            .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
4546            .unwrap();
4547        let typed = graph
4548            .run_with_input_mode(input.clone(), ExecutorMode::TypedOnly)
4549            .unwrap();
4550
4551        assert_eq!(
4552            typed, erased,
4553            "typed and erased disagree on snapshot ordering"
4554        );
4555        // All snapshots should have length 2 (one entry per inlet).
4556        assert!(
4557            typed.iter().all(|s| s.len() == 2),
4558            "snapshots must have len 2"
4559        );
4560    }
4561
4562    /// Partial-fill: first item sees only 1 of 2 inlets; no snapshot until both seen.
4563    #[test]
4564    fn typed_erased_equivalence_merge_latest_partial_fill() {
4565        // With a single input pair, both inlets get their first value simultaneously
4566        // (Unzip splits into two values), so exactly one snapshot is produced.
4567        let graph = merge_latest_graph_exec();
4568        let input = vec![(5u64, 42u64)];
4569
4570        let erased = graph
4571            .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
4572            .unwrap();
4573        let typed = graph
4574            .run_with_input_mode(input.clone(), ExecutorMode::TypedOnly)
4575            .unwrap();
4576
4577        assert_eq!(typed, erased, "typed and erased disagree on partial-fill");
4578        // Exactly one snapshot, containing [5, 42] in inlet order.
4579        assert_eq!(typed.len(), 1, "expected exactly one snapshot");
4580    }
4581
4582    /// Eager-complete: #78 regression must pass in BOTH `ErasedOnly` and `TypedOnly`.
4583    ///
4584    /// With `eager_complete = true`, the graph should complete as soon as any
4585    /// inlet finishes.  Since the Unzip stage completes all outlets simultaneously,
4586    /// both paths must produce the same result.
4587    #[test]
4588    fn typed_erased_equivalence_merge_latest_eager_complete() {
4589        let graph_eager = merge_latest_eager_graph_exec();
4590        let input = vec![(1i32, 10i32)];
4591
4592        let erased = graph_eager
4593            .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
4594            .unwrap();
4595        let typed = graph_eager
4596            .run_with_input_mode(input.clone(), ExecutorMode::TypedOnly)
4597            .unwrap();
4598
4599        assert_eq!(
4600            typed, erased,
4601            "typed and erased disagree on eager-complete behavior"
4602        );
4603        assert!(
4604            !typed.is_empty(),
4605            "eager-complete graph should produce at least one snapshot"
4606        );
4607    }
4608
4609    /// Completion: a single pair produces one snapshot; both paths complete cleanly.
4610    #[test]
4611    fn typed_erased_equivalence_merge_latest_completion() {
4612        let graph = merge_latest_graph_exec();
4613        let input = vec![(0u64, 1u64)];
4614
4615        let erased = graph
4616            .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
4617            .unwrap();
4618        let typed = graph
4619            .run_with_input_mode(input.clone(), ExecutorMode::TypedOnly)
4620            .unwrap();
4621
4622        assert_eq!(typed, erased, "typed and erased disagree on completion");
4623    }
4624
4625    /// `Auto` selects the typed path for the Unzip → MergeLatest topology and
4626    /// produces the same result as `ErasedOnly`.
4627    #[test]
4628    fn auto_selects_typed_for_merge_latest_topology() {
4629        let graph = merge_latest_graph_exec();
4630        let input: Vec<(u64, u64)> = (0..20).map(|i| (i, i + 1_000)).collect();
4631
4632        let auto_result = graph
4633            .run_with_input_mode(input.clone(), ExecutorMode::Auto)
4634            .unwrap();
4635        let erased_result = graph
4636            .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
4637            .unwrap();
4638
4639        assert_eq!(
4640            auto_result, erased_result,
4641            "Auto and ErasedOnly disagree on merge_latest topology"
4642        );
4643    }
4644
4645    // -- Phase 4 / WP-P1: typed acyclic junction equivalence tests ----------
4646
4647    fn broadcast_zip_graph_exec() -> GraphBlueprint<FlowShape<i64, (i64, i64)>> {
4648        GraphDsl::try_create(|builder| {
4649            let broadcast = builder.add(Broadcast::<i64>::new(2));
4650            let zip = builder.add(Zip::<i64, i64>::new());
4651            builder.connect(broadcast.outlet(0)?, zip.in0())?;
4652            builder.connect(broadcast.outlet(1)?, zip.in1())?;
4653            Ok(FlowShape::new(broadcast.inlet(), zip.outlet()))
4654        })
4655        .unwrap()
4656    }
4657
4658    fn balance_merge_graph_exec() -> GraphBlueprint<FlowShape<i64, i64>> {
4659        GraphDsl::try_create(|builder| {
4660            let balance = builder.add(Balance::<i64>::new(2));
4661            let merge = builder.add(Merge::<i64>::new(2));
4662            builder.connect(balance.outlet(0)?, merge.inlet(0)?)?;
4663            builder.connect(balance.outlet(1)?, merge.inlet(1)?)?;
4664            Ok(FlowShape::new(balance.inlet(), merge.outlet()))
4665        })
4666        .unwrap()
4667    }
4668
4669    fn partition_merge_graph_exec() -> GraphBlueprint<FlowShape<i64, i64>> {
4670        GraphDsl::try_create(|builder| {
4671            let partition = builder.add(Partition::<i64>::new(2, |item| {
4672                item.unsigned_abs() as usize % 2
4673            }));
4674            let merge = builder.add(Merge::<i64>::new(2));
4675            builder.connect(partition.outlet(0)?, merge.inlet(0)?)?;
4676            builder.connect(partition.outlet(1)?, merge.inlet(1)?)?;
4677            Ok(FlowShape::new(partition.inlet(), merge.outlet()))
4678        })
4679        .unwrap()
4680    }
4681
4682    fn unzip_zip_graph_exec() -> GraphBlueprint<FlowShape<i64, (i64, i64)>> {
4683        GraphDsl::try_create(|builder| {
4684            let unzip = builder.add(UnzipWith::<i64, i64, i64>::new(|item| (item, item + 10)));
4685            let zip = builder.add(Zip::<i64, i64>::new());
4686            builder.connect(unzip.out0(), zip.in0())?;
4687            builder.connect(unzip.out1(), zip.in1())?;
4688            Ok(FlowShape::new(unzip.inlet(), zip.outlet()))
4689        })
4690        .unwrap()
4691    }
4692
4693    fn merge_sorted_graph_exec() -> GraphBlueprint<FlowShape<(u64, u64), u64>> {
4694        GraphDsl::try_create(|builder| {
4695            let unzip = builder.add(Unzip::<u64, u64>::new());
4696            let merge = builder.add(MergeSorted::<u64>::new());
4697            builder.connect(unzip.out0(), merge.inlet(0)?)?;
4698            builder.connect(unzip.out1(), merge.inlet(1)?)?;
4699            Ok(FlowShape::new(unzip.inlet(), merge.outlet()))
4700        })
4701        .unwrap()
4702    }
4703
4704    #[test]
4705    fn typed_erased_equivalence_broadcast_zip() {
4706        let graph = broadcast_zip_graph_exec();
4707        let input: Vec<i64> = (-3..=3).collect();
4708
4709        let erased = graph
4710            .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
4711            .unwrap();
4712        let typed = graph
4713            .run_with_input_mode(input.clone(), ExecutorMode::TypedOnly)
4714            .unwrap();
4715        let auto = graph
4716            .run_with_input_mode(input, ExecutorMode::Auto)
4717            .unwrap();
4718
4719        assert_eq!(typed, erased, "typed and erased disagree on Broadcast->Zip");
4720        assert_eq!(
4721            auto, erased,
4722            "Auto and ErasedOnly disagree on Broadcast->Zip"
4723        );
4724        assert_eq!(typed.first().copied(), Some((-3, -3)));
4725    }
4726
4727    #[test]
4728    fn typed_erased_equivalence_balance_merge() {
4729        let graph = balance_merge_graph_exec();
4730        let input: Vec<i64> = (0..32).collect();
4731
4732        let erased = graph
4733            .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
4734            .unwrap();
4735        let typed = graph
4736            .run_with_input_mode(input.clone(), ExecutorMode::TypedOnly)
4737            .unwrap();
4738        let auto = graph
4739            .run_with_input_mode(input, ExecutorMode::Auto)
4740            .unwrap();
4741
4742        assert_eq!(typed, erased, "typed and erased disagree on Balance->Merge");
4743        assert_eq!(
4744            auto, erased,
4745            "Auto and ErasedOnly disagree on Balance->Merge"
4746        );
4747        assert_eq!(typed.len(), 32);
4748    }
4749
4750    #[test]
4751    fn typed_erased_equivalence_partition_merge() {
4752        let graph = partition_merge_graph_exec();
4753        let input: Vec<i64> = (-12..12).collect();
4754
4755        let erased = graph
4756            .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
4757            .unwrap();
4758        let typed = graph
4759            .run_with_input_mode(input.clone(), ExecutorMode::TypedOnly)
4760            .unwrap();
4761        let auto = graph
4762            .run_with_input_mode(input, ExecutorMode::Auto)
4763            .unwrap();
4764
4765        assert_eq!(
4766            typed, erased,
4767            "typed and erased disagree on Partition->Merge"
4768        );
4769        assert_eq!(
4770            auto, erased,
4771            "Auto and ErasedOnly disagree on Partition->Merge"
4772        );
4773    }
4774
4775    #[test]
4776    fn typed_erased_equivalence_partition_merge_error() {
4777        let graph = GraphDsl::try_create(|builder| {
4778            let partition = builder.add(Partition::<i64>::new(2, |_| 2));
4779            let merge = builder.add(Merge::<i64>::new(2));
4780            builder.connect(partition.outlet(0)?, merge.inlet(0)?)?;
4781            builder.connect(partition.outlet(1)?, merge.inlet(1)?)?;
4782            Ok(FlowShape::new(partition.inlet(), merge.outlet()))
4783        })
4784        .unwrap();
4785
4786        let erased = graph.run_with_input_mode(vec![7], ExecutorMode::ErasedOnly);
4787        let typed = graph.run_with_input_mode(vec![7], ExecutorMode::TypedOnly);
4788
4789        assert!(
4790            matches!(&erased, Err(StreamError::Failed(msg)) if msg.contains("out-of-bounds")),
4791            "ErasedOnly should fail on bad partitioner: {erased:?}"
4792        );
4793        assert!(
4794            matches!(&typed, Err(StreamError::Failed(msg)) if msg.contains("out-of-bounds")),
4795            "TypedOnly should fail on bad partitioner: {typed:?}"
4796        );
4797    }
4798
4799    #[test]
4800    fn typed_erased_equivalence_unzip_zip() {
4801        let graph = unzip_zip_graph_exec();
4802        let input: Vec<i64> = (0..16).collect();
4803
4804        let erased = graph
4805            .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
4806            .unwrap();
4807        let typed = graph
4808            .run_with_input_mode(input.clone(), ExecutorMode::TypedOnly)
4809            .unwrap();
4810        let auto = graph
4811            .run_with_input_mode(input, ExecutorMode::Auto)
4812            .unwrap();
4813
4814        assert_eq!(typed, erased, "typed and erased disagree on UnzipWith->Zip");
4815        assert_eq!(
4816            auto, erased,
4817            "Auto and ErasedOnly disagree on UnzipWith->Zip"
4818        );
4819        assert_eq!(typed[0], (0, 10));
4820    }
4821
4822    #[test]
4823    fn typed_erased_equivalence_merge_sorted() {
4824        let graph = merge_sorted_graph_exec();
4825        let input: Vec<(u64, u64)> = (0..20).step_by(2).map(|item| (item, item + 1)).collect();
4826
4827        let erased = graph
4828            .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
4829            .unwrap();
4830        let typed = graph
4831            .run_with_input_mode(input.clone(), ExecutorMode::TypedOnly)
4832            .unwrap();
4833        let auto = graph
4834            .run_with_input_mode(input, ExecutorMode::Auto)
4835            .unwrap();
4836
4837        assert_eq!(
4838            typed, erased,
4839            "typed and erased disagree on Unzip->MergeSorted"
4840        );
4841        assert_eq!(
4842            auto, erased,
4843            "Auto and ErasedOnly disagree on Unzip->MergeSorted"
4844        );
4845        assert_eq!(typed, (0..20).collect::<Vec<_>>());
4846    }
4847
4848    #[test]
4849    fn typed_erased_equivalence_prioritized_merge_helper() {
4850        let graph =
4851            GraphDsl::create(|builder| builder.add(MergePrioritized::<i64>::new(vec![2, 1])))
4852                .unwrap();
4853        let inputs = vec![vec![1, 2, 3, 4], vec![100, 101]];
4854
4855        let erased = graph
4856            .run_fan_in_report_mode(
4857                inputs.clone(),
4858                FusedExecutionConfig::default(),
4859                ExecutorMode::ErasedOnly,
4860            )
4861            .unwrap();
4862        let typed = graph
4863            .run_fan_in_report_mode(
4864                inputs.clone(),
4865                FusedExecutionConfig::default(),
4866                ExecutorMode::TypedOnly,
4867            )
4868            .unwrap();
4869        let auto = graph
4870            .run_fan_in_report_mode(inputs, FusedExecutionConfig::default(), ExecutorMode::Auto)
4871            .unwrap();
4872
4873        assert_eq!(
4874            typed, erased,
4875            "typed and erased disagree on MergePrioritized"
4876        );
4877        assert_eq!(
4878            auto, erased,
4879            "Auto and ErasedOnly disagree on MergePrioritized"
4880        );
4881        assert_eq!(typed.output, vec![1, 2, 100, 3, 4, 101]);
4882    }
4883
4884    #[test]
4885    fn typed_erased_equivalence_merge_preferred_helper() {
4886        let graph = GraphDsl::create(|builder| builder.add(MergePreferred::<i64>::new(2))).unwrap();
4887        let preferred = vec![1, 2, 3];
4888        let secondary = vec![vec![100, 101], vec![200, 201]];
4889
4890        let erased = graph
4891            .run_merge_preferred_report_mode(
4892                preferred.clone(),
4893                secondary.clone(),
4894                FusedExecutionConfig::default(),
4895                ExecutorMode::ErasedOnly,
4896            )
4897            .unwrap();
4898        let typed = graph
4899            .run_merge_preferred_report_mode(
4900                preferred.clone(),
4901                secondary.clone(),
4902                FusedExecutionConfig::default(),
4903                ExecutorMode::TypedOnly,
4904            )
4905            .unwrap();
4906        let auto = graph
4907            .run_merge_preferred_report_mode(
4908                preferred,
4909                secondary,
4910                FusedExecutionConfig::default(),
4911                ExecutorMode::Auto,
4912            )
4913            .unwrap();
4914
4915        assert_eq!(typed, erased, "typed and erased disagree on MergePreferred");
4916        assert_eq!(
4917            auto, erased,
4918            "Auto and ErasedOnly disagree on MergePreferred"
4919        );
4920        assert_eq!(typed.output, vec![1, 2, 3, 100, 200, 101, 201]);
4921    }
4922
4923    #[test]
4924    fn typed_erased_equivalence_concat_helper() {
4925        let graph = GraphDsl::create(|builder| builder.add(Concat::<i64>::new(3))).unwrap();
4926        let inputs = vec![vec![1, 2], vec![], vec![3, 4]];
4927
4928        let erased = graph
4929            .run_concat_report_mode(
4930                inputs.clone(),
4931                FusedExecutionConfig::default(),
4932                ExecutorMode::ErasedOnly,
4933            )
4934            .unwrap();
4935        let typed = graph
4936            .run_concat_report_mode(
4937                inputs.clone(),
4938                FusedExecutionConfig::default(),
4939                ExecutorMode::TypedOnly,
4940            )
4941            .unwrap();
4942        let auto = graph
4943            .run_concat_report_mode(inputs, FusedExecutionConfig::default(), ExecutorMode::Auto)
4944            .unwrap();
4945
4946        assert_eq!(typed, erased, "typed and erased disagree on Concat");
4947        assert_eq!(auto, erased, "Auto and ErasedOnly disagree on Concat");
4948        assert_eq!(typed.output, vec![1, 2, 3, 4]);
4949    }
4950
4951    #[test]
4952    fn typed_erased_equivalence_interleave_helper() {
4953        let graph = GraphDsl::create(|builder| builder.add(Interleave::<i64>::new(3, 2))).unwrap();
4954        let inputs = vec![vec![1, 2, 3], vec![10, 11, 12], vec![20]];
4955
4956        let erased = graph
4957            .run_interleave_report_mode(
4958                inputs.clone(),
4959                2,
4960                false,
4961                FusedExecutionConfig::default(),
4962                ExecutorMode::ErasedOnly,
4963            )
4964            .unwrap();
4965        let typed = graph
4966            .run_interleave_report_mode(
4967                inputs.clone(),
4968                2,
4969                false,
4970                FusedExecutionConfig::default(),
4971                ExecutorMode::TypedOnly,
4972            )
4973            .unwrap();
4974        let auto = graph
4975            .run_interleave_report_mode(
4976                inputs,
4977                2,
4978                false,
4979                FusedExecutionConfig::default(),
4980                ExecutorMode::Auto,
4981            )
4982            .unwrap();
4983
4984        assert_eq!(typed, erased, "typed and erased disagree on Interleave");
4985        assert_eq!(auto, erased, "Auto and ErasedOnly disagree on Interleave");
4986        assert_eq!(typed.output, vec![1, 2, 10, 11, 20, 3, 12]);
4987    }
4988
4989    #[test]
4990    fn typed_erased_equivalence_interleave_eager_close_helper() {
4991        let graph = GraphDsl::create(|builder| {
4992            builder.add(Interleave::<i64>::new_with_eager_close(2, 1, true))
4993        })
4994        .unwrap();
4995        let inputs = vec![vec![1], vec![10, 11]];
4996
4997        let erased = graph
4998            .run_interleave_report_mode(
4999                inputs.clone(),
5000                1,
5001                true,
5002                FusedExecutionConfig::default(),
5003                ExecutorMode::ErasedOnly,
5004            )
5005            .unwrap();
5006        let typed = graph
5007            .run_interleave_report_mode(
5008                inputs.clone(),
5009                1,
5010                true,
5011                FusedExecutionConfig::default(),
5012                ExecutorMode::TypedOnly,
5013            )
5014            .unwrap();
5015        let auto = graph
5016            .run_interleave_report_mode(
5017                inputs,
5018                1,
5019                true,
5020                FusedExecutionConfig::default(),
5021                ExecutorMode::Auto,
5022            )
5023            .unwrap();
5024
5025        assert_eq!(
5026            typed, erased,
5027            "typed and erased disagree on Interleave eager close"
5028        );
5029        assert_eq!(
5030            auto, erased,
5031            "Auto and ErasedOnly disagree on Interleave eager close"
5032        );
5033        assert_eq!(typed.output, vec![1, 10]);
5034    }
5035
5036    #[test]
5037    fn typed_erased_equivalence_helper_event_limit_failures() {
5038        let config = FusedExecutionConfig { event_limit: 1 };
5039
5040        let prioritized =
5041            GraphDsl::create(|builder| builder.add(MergePrioritized::<i64>::new(vec![2, 1])))
5042                .unwrap();
5043        let prioritized_inputs = vec![vec![1], vec![10]];
5044        let erased = prioritized.run_fan_in_report_mode(
5045            prioritized_inputs.clone(),
5046            config,
5047            ExecutorMode::ErasedOnly,
5048        );
5049        let typed = prioritized.run_fan_in_report_mode(
5050            prioritized_inputs.clone(),
5051            config,
5052            ExecutorMode::TypedOnly,
5053        );
5054        let auto =
5055            prioritized.run_fan_in_report_mode(prioritized_inputs, config, ExecutorMode::Auto);
5056        assert_eq!(typed, erased);
5057        assert_eq!(auto, erased);
5058
5059        let preferred =
5060            GraphDsl::create(|builder| builder.add(MergePreferred::<i64>::new(1))).unwrap();
5061        let erased = preferred.run_merge_preferred_report_mode(
5062            vec![1],
5063            vec![vec![10]],
5064            config,
5065            ExecutorMode::ErasedOnly,
5066        );
5067        let typed = preferred.run_merge_preferred_report_mode(
5068            vec![1],
5069            vec![vec![10]],
5070            config,
5071            ExecutorMode::TypedOnly,
5072        );
5073        let auto = preferred.run_merge_preferred_report_mode(
5074            vec![1],
5075            vec![vec![10]],
5076            config,
5077            ExecutorMode::Auto,
5078        );
5079        assert_eq!(typed, erased);
5080        assert_eq!(auto, erased);
5081
5082        let concat = GraphDsl::create(|builder| builder.add(Concat::<i64>::new(2))).unwrap();
5083        let concat_inputs = vec![vec![1], vec![10]];
5084        let erased =
5085            concat.run_concat_report_mode(concat_inputs.clone(), config, ExecutorMode::ErasedOnly);
5086        let typed =
5087            concat.run_concat_report_mode(concat_inputs.clone(), config, ExecutorMode::TypedOnly);
5088        let auto = concat.run_concat_report_mode(concat_inputs, config, ExecutorMode::Auto);
5089        assert_eq!(typed, erased);
5090        assert_eq!(auto, erased);
5091
5092        let interleave =
5093            GraphDsl::create(|builder| builder.add(Interleave::<i64>::new(2, 1))).unwrap();
5094        let interleave_inputs = vec![vec![1], vec![10]];
5095        let erased = interleave.run_interleave_report_mode(
5096            interleave_inputs.clone(),
5097            1,
5098            false,
5099            config,
5100            ExecutorMode::ErasedOnly,
5101        );
5102        let typed = interleave.run_interleave_report_mode(
5103            interleave_inputs.clone(),
5104            1,
5105            false,
5106            config,
5107            ExecutorMode::TypedOnly,
5108        );
5109        let auto = interleave.run_interleave_report_mode(
5110            interleave_inputs,
5111            1,
5112            false,
5113            config,
5114            ExecutorMode::Auto,
5115        );
5116        assert_eq!(typed, erased);
5117        assert_eq!(auto, erased);
5118    }
5119
5120    // -- Phase 3b blueprint-independence tests --------------------------------
5121
5122    /// Running the same MergeLatest blueprint twice (sequentially) produces
5123    /// independent, correct results — no shared mutable state between runs.
5124    ///
5125    /// Pins the fix that removed the `TypedPlanCache` / `MergeLatestRunnerCell`
5126    /// (which serialised concurrent reuse on a `Mutex` and shared one execution
5127    /// core across runs).  The typed path must build a fresh `MergeLatestCore`
5128    /// per call to `run_with_input_report_mode`.
5129    #[test]
5130    fn merge_latest_blueprint_sequential_reuse_is_independent() {
5131        let graph = merge_latest_graph_exec();
5132        let input_a: Vec<(u64, u64)> = (0..5).map(|i| (i, i + 100)).collect();
5133        let input_b: Vec<(u64, u64)> = (10..15).map(|i| (i, i + 200)).collect();
5134
5135        // Run the same blueprint with two different inputs.
5136        let result_a_typed = graph
5137            .run_with_input_mode(input_a.clone(), ExecutorMode::TypedOnly)
5138            .unwrap();
5139        let result_b_typed = graph
5140            .run_with_input_mode(input_b.clone(), ExecutorMode::TypedOnly)
5141            .unwrap();
5142
5143        // Each run should match its own erased reference.
5144        let result_a_erased = graph
5145            .run_with_input_mode(input_a, ExecutorMode::ErasedOnly)
5146            .unwrap();
5147        let result_b_erased = graph
5148            .run_with_input_mode(input_b, ExecutorMode::ErasedOnly)
5149            .unwrap();
5150
5151        assert_eq!(
5152            result_a_typed, result_a_erased,
5153            "sequential run A: typed and erased disagree"
5154        );
5155        assert_eq!(
5156            result_b_typed, result_b_erased,
5157            "sequential run B: typed and erased disagree"
5158        );
5159        // The two runs must NOT share state — run B must not contain run A's values.
5160        assert_ne!(
5161            result_a_typed, result_b_typed,
5162            "runs A and B should differ (different inputs)"
5163        );
5164    }
5165
5166    /// Running the same MergeLatest blueprint concurrently from two threads
5167    /// produces independent, correct results — no Mutex serialisation, no
5168    /// shared execution state.
5169    ///
5170    /// This is the concurrency variant of `merge_latest_blueprint_sequential_reuse_is_independent`.
5171    #[test]
5172    fn merge_latest_blueprint_concurrent_reuse_is_independent() {
5173        use std::sync::Arc as StdArc;
5174
5175        // Wrap in Arc so both threads can share the (immutable) blueprint.
5176        let graph = StdArc::new(merge_latest_graph_exec());
5177
5178        let input_a: Vec<(u64, u64)> = (0..50).map(|i| (i, i + 1_000)).collect();
5179        let input_b: Vec<(u64, u64)> = (100..150).map(|i| (i, i + 2_000)).collect();
5180
5181        let graph_a = StdArc::clone(&graph);
5182        let graph_b = StdArc::clone(&graph);
5183        let ia = input_a.clone();
5184        let ib = input_b.clone();
5185
5186        let handle_a =
5187            std::thread::spawn(move || graph_a.run_with_input_mode(ia, ExecutorMode::TypedOnly));
5188        let handle_b =
5189            std::thread::spawn(move || graph_b.run_with_input_mode(ib, ExecutorMode::TypedOnly));
5190
5191        let result_a = handle_a.join().expect("thread A panicked").unwrap();
5192        let result_b = handle_b.join().expect("thread B panicked").unwrap();
5193
5194        // Reference results from the erased executor.
5195        let ref_a = graph
5196            .run_with_input_mode(input_a, ExecutorMode::ErasedOnly)
5197            .unwrap();
5198        let ref_b = graph
5199            .run_with_input_mode(input_b, ExecutorMode::ErasedOnly)
5200            .unwrap();
5201
5202        assert_eq!(
5203            result_a, ref_a,
5204            "concurrent run A: typed and erased disagree"
5205        );
5206        assert_eq!(
5207            result_b, ref_b,
5208            "concurrent run B: typed and erased disagree"
5209        );
5210        // Sanity: the two runs must have produced different outputs.
5211        assert_ne!(result_a, result_b, "concurrent runs must be independent");
5212    }
5213}
5214
5215#[cfg(test)]
5216fn run_threaded_async_linear_count<I, T>(
5217    input: I,
5218    segments: TypedLinearSegments<T>,
5219    config: AsyncBoundaryExecutionConfig,
5220) -> StreamResult<FusedTerminalReport<usize>>
5221where
5222    I: IntoIterator<Item = T> + Send,
5223    I::IntoIter: Send,
5224    T: Send + 'static,
5225{
5226    let channels = segments.segments.len() + 1;
5227    let mut senders = Vec::with_capacity(channels);
5228    let mut receivers = Vec::with_capacity(channels);
5229    for _ in 0..channels {
5230        let (sender, receiver) = mpsc::sync_channel(config.buffer_size);
5231        senders.push(sender);
5232        receivers.push(Some(receiver));
5233    }
5234
5235    let first_sender = senders
5236        .first()
5237        .expect("at least one async segment channel")
5238        .clone();
5239    let mut final_receiver = Some(
5240        receivers
5241            .last_mut()
5242            .expect("at least one async segment channel")
5243            .take()
5244            .expect("final receiver is present"),
5245    );
5246    let events = AtomicUsize::new(0);
5247    let async_boundary_crossings = AtomicUsize::new(0);
5248
5249    let result = thread::scope(|scope| {
5250        let input = input.into_iter().map(Ok::<T, StreamError>);
5251        let source = scope.spawn(move || feed_threaded_async_linear_input(input, first_sender));
5252        let mut workers = Vec::with_capacity(segments.segments.len());
5253
5254        for (index, steps) in segments.segments.iter().enumerate() {
5255            let input = receivers[index].take().expect("worker receiver is present");
5256            let output = senders[index + 1].clone();
5257            let has_boundary_after = index + 1 < segments.segments.len();
5258            let events = &events;
5259            let async_boundary_crossings = &async_boundary_crossings;
5260            workers.push(scope.spawn(move || {
5261                run_threaded_async_linear_segment(
5262                    steps,
5263                    input,
5264                    output,
5265                    has_boundary_after,
5266                    events,
5267                    async_boundary_crossings,
5268                    config,
5269                )
5270            }));
5271        }
5272        drop(senders);
5273
5274        let final_rx = final_receiver.take().expect("final receiver present");
5275        let mut count = 0;
5276        let mut terminal_error = None;
5277        loop {
5278            match final_rx.recv() {
5279                Ok(AsyncLinearMessage::Item(_)) => count += 1,
5280                Ok(AsyncLinearMessage::Done) => break,
5281                Ok(AsyncLinearMessage::Failed(error)) => {
5282                    terminal_error = Some(error);
5283                    break;
5284                }
5285                Err(_) => {
5286                    terminal_error = Some(StreamError::AbruptTermination);
5287                    break;
5288                }
5289            }
5290        }
5291        drop(final_rx);
5292
5293        let mut worker_error = join_threaded_async_linear_worker(source)?;
5294        for worker in workers {
5295            if worker_error.is_none() {
5296                worker_error = join_threaded_async_linear_worker(worker)?;
5297            } else {
5298                let _ = join_threaded_async_linear_worker(worker);
5299            }
5300        }
5301
5302        match (terminal_error, worker_error) {
5303            (Some(error), _) if error != StreamError::AbruptTermination => return Err(error),
5304            (_, Some(error)) => return Err(error),
5305            (Some(error), None) => return Err(error),
5306            (None, None) => {}
5307        }
5308
5309        Ok(count)
5310    });
5311
5312    Ok(FusedTerminalReport {
5313        result: result?,
5314        events: events.load(Ordering::Relaxed),
5315        async_boundary_crossings: async_boundary_crossings.load(Ordering::Relaxed),
5316    })
5317}
5318
5319fn run_ractor_async_linear_count<I, T>(
5320    input: I,
5321    segments: TypedLinearSegments<T>,
5322    config: AsyncBoundaryExecutionConfig,
5323) -> StreamResult<FusedTerminalReport<usize>>
5324where
5325    I: IntoIterator<Item = T> + Send,
5326    I::IntoIter: Send + 'static,
5327    T: Send + 'static,
5328{
5329    if config.buffer_size == 0 {
5330        return Err(StreamError::GraphValidation(
5331            "ractor async boundary execution requires buffer_size greater than zero".into(),
5332        ));
5333    }
5334
5335    let input = input.into_iter().map(Ok::<T, StreamError>);
5336    let runtime = ractor_boundary_runtime()?;
5337    if tokio::runtime::Handle::try_current().is_ok() {
5338        thread::scope(|scope| {
5339            let handle = scope.spawn(move || {
5340                runtime.block_on(run_ractor_async_linear_count_on_runtime(
5341                    input, segments, config,
5342                ))
5343            });
5344            handle.join().map_err(|_| {
5345                StreamError::Failed("ractor async boundary runtime thread panicked".into())
5346            })?
5347        })
5348    } else {
5349        runtime.block_on(run_ractor_async_linear_count_on_runtime(
5350            input, segments, config,
5351        ))
5352    }
5353}
5354
5355async fn run_ractor_async_linear_count_on_runtime<I, T>(
5356    input: I,
5357    segments: TypedLinearSegments<T>,
5358    config: AsyncBoundaryExecutionConfig,
5359) -> StreamResult<FusedTerminalReport<usize>>
5360where
5361    I: Iterator<Item = StreamResult<T>> + Send + 'static,
5362    T: Send + 'static,
5363{
5364    let channels = segments.segments.len() + 1;
5365    let mut senders = Vec::with_capacity(channels);
5366    let mut receivers = Vec::with_capacity(channels);
5367    for _ in 0..channels {
5368        let (sender, receiver) = ractor::concurrency::mpsc_bounded(config.buffer_size);
5369        senders.push(sender);
5370        receivers.push(Some(receiver));
5371    }
5372
5373    let first_sender = senders
5374        .first()
5375        .expect("at least one async segment channel")
5376        .clone();
5377    let mut final_receiver = receivers
5378        .last_mut()
5379        .expect("at least one async segment channel")
5380        .take()
5381        .expect("final receiver is present");
5382    let events = Arc::new(AtomicUsize::new(0));
5383    let async_boundary_crossings = Arc::new(AtomicUsize::new(0));
5384
5385    let (source_ref, source_handle) = Actor::spawn(
5386        None,
5387        RactorBoundarySourceActor::<I, T>::new(),
5388        RactorBoundarySourceState {
5389            input: Some(input),
5390            output: first_sender,
5391        },
5392    )
5393    .await
5394    .map_err(ractor_spawn_error)?;
5395
5396    let mut actors = Vec::with_capacity(segments.segments.len() + 1);
5397    actors.push((source_ref, source_handle));
5398
5399    for (index, steps) in segments.segments.into_iter().enumerate() {
5400        let input = receivers[index].take().expect("worker receiver is present");
5401        let output = senders[index + 1].clone();
5402        let has_boundary_after = index + 1 < channels - 1;
5403        let (worker_ref, worker_handle) = match Actor::spawn(
5404            None,
5405            RactorLinearSegmentActor::<T>::new(),
5406            RactorLinearSegmentState {
5407                steps,
5408                input,
5409                output,
5410                has_boundary_after,
5411                events: Arc::clone(&events),
5412                async_boundary_crossings: Arc::clone(&async_boundary_crossings),
5413                config,
5414            },
5415        )
5416        .await
5417        {
5418            Ok(actor) => actor,
5419            Err(error) => {
5420                let error = ractor_spawn_error(error);
5421                stop_ractor_async_linear_actors(&actors);
5422                let _ = join_ractor_async_linear_actors(actors).await;
5423                return Err(error);
5424            }
5425        };
5426        actors.push((worker_ref, worker_handle));
5427    }
5428    drop(senders);
5429
5430    let mut start_error = None;
5431    for (actor, _) in &actors {
5432        if actor.send_message(RactorBoundaryCommand::Run).is_err() {
5433            start_error = Some(StreamError::AbruptTermination);
5434            break;
5435        }
5436    }
5437    if let Some(error) = start_error {
5438        stop_ractor_async_linear_actors(&actors);
5439        let _ = join_ractor_async_linear_actors(actors).await;
5440        return Err(error);
5441    }
5442
5443    let mut count = 0;
5444    let mut terminal_error = None;
5445    loop {
5446        match final_receiver.recv().await {
5447            Some(AsyncLinearMessage::Item(_)) => count += 1,
5448            Some(AsyncLinearMessage::Done) => break,
5449            Some(AsyncLinearMessage::Failed(error)) => {
5450                terminal_error = Some(error);
5451                break;
5452            }
5453            None => {
5454                terminal_error = Some(StreamError::AbruptTermination);
5455                break;
5456            }
5457        }
5458    }
5459    drop(final_receiver);
5460
5461    stop_ractor_async_linear_actors(&actors);
5462    let actor_error = join_ractor_async_linear_actors(actors).await;
5463
5464    match (terminal_error, actor_error) {
5465        (Some(error), _) if error != StreamError::AbruptTermination => return Err(error),
5466        (_, Some(error)) => return Err(error),
5467        (Some(error), None) => return Err(error),
5468        (None, None) => {}
5469    }
5470
5471    Ok(FusedTerminalReport {
5472        result: count,
5473        events: events.load(Ordering::Relaxed),
5474        async_boundary_crossings: async_boundary_crossings.load(Ordering::Relaxed),
5475    })
5476}
5477
5478struct RactorLinearSegmentActor<T> {
5479    _marker: PhantomData<fn() -> T>,
5480}
5481
5482impl<T> RactorLinearSegmentActor<T> {
5483    fn new() -> Self {
5484        Self {
5485            _marker: PhantomData,
5486        }
5487    }
5488}
5489
5490struct RactorLinearSegmentState<T> {
5491    steps: Vec<TypedLinearStep<T>>,
5492    input: ractor::concurrency::MpscReceiver<AsyncLinearMessage<T>>,
5493    output: ractor::concurrency::MpscSender<AsyncLinearMessage<T>>,
5494    has_boundary_after: bool,
5495    events: Arc<AtomicUsize>,
5496    async_boundary_crossings: Arc<AtomicUsize>,
5497    config: AsyncBoundaryExecutionConfig,
5498}
5499
5500impl<T> Actor for RactorLinearSegmentActor<T>
5501where
5502    T: Send + 'static,
5503{
5504    type Msg = RactorBoundaryCommand;
5505    type State = RactorLinearSegmentState<T>;
5506    type Arguments = RactorLinearSegmentState<T>;
5507
5508    async fn pre_start(
5509        &self,
5510        _myself: ActorRef<Self::Msg>,
5511        args: Self::Arguments,
5512    ) -> Result<Self::State, ActorProcessingErr> {
5513        Ok(args)
5514    }
5515
5516    async fn handle(
5517        &self,
5518        myself: ActorRef<Self::Msg>,
5519        message: Self::Msg,
5520        state: &mut Self::State,
5521    ) -> Result<(), ActorProcessingErr> {
5522        match message {
5523            RactorBoundaryCommand::Run => {
5524                run_ractor_async_linear_segment(state)
5525                    .await
5526                    .map_err(actor_processing_error)?;
5527                myself.stop(None);
5528            }
5529        }
5530        Ok(())
5531    }
5532}
5533
5534async fn run_ractor_async_linear_segment<T>(
5535    state: &mut RactorLinearSegmentState<T>,
5536) -> StreamResult<()>
5537where
5538    T: Send + 'static,
5539{
5540    loop {
5541        match state.input.recv().await {
5542            Some(AsyncLinearMessage::Item(item)) => {
5543                let result =
5544                    run_async_linear_item(item, &state.steps, &state.events, state.config.fused)
5545                        .and_then(|item| {
5546                            if state.has_boundary_after {
5547                                bump_fused_event_atomic(&state.events, state.config.fused)?;
5548                                state
5549                                    .async_boundary_crossings
5550                                    .fetch_add(1, Ordering::Relaxed);
5551                                bump_fused_event_atomic(&state.events, state.config.fused)?;
5552                            }
5553                            Ok(item)
5554                        });
5555
5556                match result {
5557                    Ok(item) => state
5558                        .output
5559                        .send(AsyncLinearMessage::Item(item))
5560                        .await
5561                        .map_err(|_| StreamError::AbruptTermination)?,
5562                    Err(error) => {
5563                        let _ = state
5564                            .output
5565                            .send(AsyncLinearMessage::Failed(error.clone()))
5566                            .await;
5567                        return Err(error);
5568                    }
5569                }
5570            }
5571            Some(AsyncLinearMessage::Done) => {
5572                state
5573                    .output
5574                    .send(AsyncLinearMessage::Done)
5575                    .await
5576                    .map_err(|_| StreamError::AbruptTermination)?;
5577                return Ok(());
5578            }
5579            Some(AsyncLinearMessage::Failed(error)) => {
5580                let _ = state
5581                    .output
5582                    .send(AsyncLinearMessage::Failed(error.clone()))
5583                    .await;
5584                return Err(error);
5585            }
5586            None => return Err(StreamError::AbruptTermination),
5587        }
5588    }
5589}
5590
5591async fn join_ractor_async_linear_actor(
5592    handle: ractor::concurrency::JoinHandle<()>,
5593) -> StreamResult<()> {
5594    handle.await.map_err(|error| {
5595        StreamError::Failed(format!("ractor async boundary actor task failed: {error}"))
5596    })
5597}
5598
5599fn stop_ractor_async_linear_actors(
5600    actors: &[(
5601        ActorRef<RactorBoundaryCommand>,
5602        ractor::concurrency::JoinHandle<()>,
5603    )],
5604) {
5605    for (actor, _) in actors {
5606        actor.stop(None);
5607    }
5608}
5609
5610#[cfg_attr(not(test), allow(dead_code))]
5611async fn join_ractor_async_linear_actors(
5612    actors: Vec<(
5613        ActorRef<RactorBoundaryCommand>,
5614        ractor::concurrency::JoinHandle<()>,
5615    )>,
5616) -> Option<StreamError> {
5617    let mut actor_error = None;
5618    for (_, handle) in actors {
5619        let result = join_ractor_async_linear_actor(handle).await;
5620        if actor_error.is_some() {
5621            continue;
5622        }
5623        if let Err(error) = result {
5624            actor_error = Some(error);
5625        }
5626    }
5627    actor_error
5628}
5629
5630fn ractor_spawn_error(error: ractor::SpawnErr) -> StreamError {
5631    StreamError::Failed(format!(
5632        "ractor async boundary actor failed to spawn: {error}"
5633    ))
5634}
5635
5636fn actor_processing_error(error: StreamError) -> ActorProcessingErr {
5637    Box::new(error)
5638}
5639
5640#[cfg(test)]
5641fn feed_threaded_async_linear_input<I, T>(
5642    input: I,
5643    output: mpsc::SyncSender<AsyncLinearMessage<T>>,
5644) -> StreamResult<()>
5645where
5646    I: IntoIterator<Item = StreamResult<T>>,
5647{
5648    for item in input {
5649        match item {
5650            Ok(item) => output
5651                .send(AsyncLinearMessage::Item(item))
5652                .map_err(|_| StreamError::AbruptTermination)?,
5653            Err(error) => {
5654                let _ = output.send(AsyncLinearMessage::Failed(error.clone()));
5655                return Err(error);
5656            }
5657        }
5658    }
5659    output
5660        .send(AsyncLinearMessage::Done)
5661        .map_err(|_| StreamError::AbruptTermination)
5662}
5663
5664#[cfg(test)]
5665fn run_threaded_async_linear_segment<T>(
5666    steps: &[TypedLinearStep<T>],
5667    input: mpsc::Receiver<AsyncLinearMessage<T>>,
5668    output: mpsc::SyncSender<AsyncLinearMessage<T>>,
5669    has_boundary_after: bool,
5670    events: &AtomicUsize,
5671    async_boundary_crossings: &AtomicUsize,
5672    config: AsyncBoundaryExecutionConfig,
5673) -> StreamResult<()>
5674where
5675    T: Send + 'static,
5676{
5677    loop {
5678        match input.recv().map_err(|_| StreamError::AbruptTermination)? {
5679            AsyncLinearMessage::Item(item) => {
5680                let result =
5681                    run_async_linear_item(item, steps, events, config.fused).and_then(|item| {
5682                        if has_boundary_after {
5683                            bump_fused_event_atomic(events, config.fused)?;
5684                            async_boundary_crossings.fetch_add(1, Ordering::Relaxed);
5685                            bump_fused_event_atomic(events, config.fused)?;
5686                        }
5687                        output
5688                            .send(AsyncLinearMessage::Item(item))
5689                            .map_err(|_| StreamError::AbruptTermination)
5690                    });
5691                if let Err(error) = result {
5692                    let _ = output.send(AsyncLinearMessage::Failed(error.clone()));
5693                    return Err(error);
5694                }
5695            }
5696            AsyncLinearMessage::Done => {
5697                output
5698                    .send(AsyncLinearMessage::Done)
5699                    .map_err(|_| StreamError::AbruptTermination)?;
5700                return Ok(());
5701            }
5702            AsyncLinearMessage::Failed(error) => {
5703                let _ = output.send(AsyncLinearMessage::Failed(error.clone()));
5704                return Err(error);
5705            }
5706        }
5707    }
5708}
5709
5710fn run_async_linear_item<T>(
5711    mut item: T,
5712    steps: &[TypedLinearStep<T>],
5713    events: &AtomicUsize,
5714    config: FusedExecutionConfig,
5715) -> StreamResult<T>
5716where
5717    T: Send + 'static,
5718{
5719    for step in steps {
5720        bump_fused_event_atomic(events, config)?;
5721        match step {
5722            TypedLinearStep::Pass => {}
5723            TypedLinearStep::Map(mapper) => {
5724                item = mapper(item);
5725            }
5726            TypedLinearStep::AsyncBoundary => {
5727                return Err(StreamError::GraphValidation(
5728                    "async boundary execution expects pre-split linear segments".into(),
5729                ));
5730            }
5731        }
5732        bump_fused_event_atomic(events, config)?;
5733    }
5734    Ok(item)
5735}
5736
5737#[cfg(test)]
5738fn join_threaded_async_linear_worker(
5739    handle: thread::ScopedJoinHandle<'_, StreamResult<()>>,
5740) -> StreamResult<Option<StreamError>> {
5741    match handle.join() {
5742        Ok(Ok(())) => Ok(None),
5743        Ok(Err(error)) => Ok(Some(error)),
5744        Err(_) => Err(StreamError::Failed("async boundary worker panicked".into())),
5745    }
5746}
5747
5748fn bump_fused_event_atomic(events: &AtomicUsize, config: FusedExecutionConfig) -> StreamResult<()> {
5749    let events = events.fetch_add(1, Ordering::Relaxed) + 1;
5750    if events > config.event_limit {
5751        return Err(StreamError::EventLimitExceeded {
5752            limit: config.event_limit,
5753        });
5754    }
5755    Ok(())
5756}
5757
5758fn add_typed_helper_events(
5759    events: &mut usize,
5760    config: FusedExecutionConfig,
5761    count: usize,
5762) -> StreamResult<()> {
5763    let next = events
5764        .checked_add(count)
5765        .ok_or_else(|| StreamError::Failed("typed helper event count overflow".into()))?;
5766    if next > config.event_limit {
5767        return Err(StreamError::EventLimitExceeded {
5768            limit: config.event_limit,
5769        });
5770    }
5771    *events = next;
5772    Ok(())
5773}
5774
5775fn typed_fan_in_success_events(output_len: usize, input_count: usize) -> StreamResult<usize> {
5776    output_len
5777        .checked_mul(3)
5778        .and_then(|events| events.checked_add(input_count))
5779        .and_then(|events| events.checked_add(1))
5780        .ok_or_else(|| StreamError::Failed("typed helper event count overflow".into()))
5781}
5782
5783fn direct_single_fan_in_stage<'a, T>(
5784    stages: &'a [super::builder::StageRecord],
5785    edges: &[super::builder::Edge],
5786    shape_inlets: &[AnyInlet],
5787    shape_outlet: &AnyOutlet,
5788) -> Option<&'a super::builder::StageRecord>
5789where
5790    T: 'static,
5791{
5792    if stages.len() != 1 || !edges.is_empty() {
5793        return None;
5794    }
5795    let stage = stages.first()?;
5796    let element_type = TypeId::of::<T>();
5797    if stage.spec.inlets.len() != shape_inlets.len()
5798        || stage.spec.outlets.len() != 1
5799        || stage.spec.outlets[0].id() != shape_outlet.id()
5800        || stage.spec.outlets[0].type_id() != element_type
5801        || shape_outlet.type_id() != element_type
5802        || stage
5803            .spec
5804            .inlets
5805            .iter()
5806            .map(AnyInlet::id)
5807            .ne(shape_inlets.iter().map(AnyInlet::id))
5808        || stage
5809            .spec
5810            .inlets
5811            .iter()
5812            .any(|inlet| inlet.type_id() != element_type)
5813        || shape_inlets
5814            .iter()
5815            .any(|inlet| inlet.type_id() != element_type)
5816    {
5817        return None;
5818    }
5819    Some(stage)
5820}
5821
5822fn run_typed_scheduled_fan_in<T>(
5823    inputs: Vec<Vec<T>>,
5824    schedule: &[usize],
5825    config: FusedExecutionConfig,
5826) -> StreamResult<FusedExecutionReport<T>>
5827where
5828    T: Send + 'static,
5829{
5830    let output_capacity = inputs.iter().map(Vec::len).sum();
5831    let events = typed_fan_in_success_events(output_capacity, inputs.len())?;
5832    let mut checked_events = 0;
5833    add_typed_helper_events(&mut checked_events, config, events)?;
5834
5835    let mut queues: Vec<_> = inputs.into_iter().map(Vec::into_iter).collect();
5836    let mut schedule_index = 0;
5837    let mut output = Vec::with_capacity(output_capacity);
5838    while output.len() < output_capacity {
5839        let input_index = next_scheduled_input(&queues, schedule, &mut schedule_index)
5840            .ok_or_else(|| StreamError::GraphValidation("no runnable fan-in input".into()))?;
5841        let item = queues[input_index]
5842            .next()
5843            .expect("scheduled input had an item");
5844        output.push(item);
5845    }
5846
5847    Ok(FusedExecutionReport {
5848        output,
5849        events,
5850        async_boundary_crossings: 0,
5851    })
5852}
5853
5854fn run_typed_concat<T>(
5855    inputs: Vec<Vec<T>>,
5856    config: FusedExecutionConfig,
5857) -> StreamResult<FusedExecutionReport<T>>
5858where
5859    T: Send + 'static,
5860{
5861    let output_capacity = inputs.iter().map(Vec::len).sum();
5862    let events = typed_fan_in_success_events(output_capacity, inputs.len())?;
5863    let mut checked_events = 0;
5864    add_typed_helper_events(&mut checked_events, config, events)?;
5865
5866    let mut output = Vec::with_capacity(output_capacity);
5867    for input in inputs {
5868        output.extend(input);
5869    }
5870
5871    Ok(FusedExecutionReport {
5872        output,
5873        events,
5874        async_boundary_crossings: 0,
5875    })
5876}
5877
5878fn run_typed_interleave<T>(
5879    inputs: Vec<Vec<T>>,
5880    segment_size: usize,
5881    eager_close: bool,
5882    config: FusedExecutionConfig,
5883) -> StreamResult<FusedExecutionReport<T>>
5884where
5885    T: Send + 'static,
5886{
5887    let output_capacity = inputs.iter().map(Vec::len).sum();
5888    let mut events = 0;
5889    if !eager_close {
5890        let total_events = typed_fan_in_success_events(output_capacity, inputs.len())?;
5891        add_typed_helper_events(&mut events, config, total_events)?;
5892    }
5893
5894    let mut queues: Vec<_> = inputs.into_iter().map(Vec::into_iter).collect();
5895    let mut completed = vec![false; queues.len()];
5896    let mut output = Vec::with_capacity(output_capacity);
5897
5898    for (index, queue) in queues.iter().enumerate() {
5899        if queue.len() == 0 {
5900            completed[index] = true;
5901            if eager_close {
5902                add_typed_helper_events(&mut events, config, 2)?;
5903                return Ok(FusedExecutionReport {
5904                    output,
5905                    events,
5906                    async_boundary_crossings: 0,
5907                });
5908            }
5909        }
5910    }
5911
5912    let mut current = 0usize;
5913    while completed.iter().any(|done| !done) {
5914        if completed[current] {
5915            current = next_open_index(&completed, current)
5916                .ok_or_else(|| StreamError::GraphValidation("no open interleave input".into()))?;
5917            continue;
5918        }
5919
5920        let mut emitted = 0usize;
5921        while emitted < segment_size {
5922            match queues[current].next() {
5923                Some(item) => {
5924                    if eager_close {
5925                        add_typed_helper_events(&mut events, config, 3)?;
5926                    }
5927                    output.push(item);
5928                    emitted += 1;
5929                }
5930                None => {
5931                    completed[current] = true;
5932                    if eager_close {
5933                        add_typed_helper_events(&mut events, config, 2)?;
5934                        return Ok(FusedExecutionReport {
5935                            output,
5936                            events,
5937                            async_boundary_crossings: 0,
5938                        });
5939                    }
5940                    break;
5941                }
5942            }
5943        }
5944
5945        if completed.iter().all(|done| *done) {
5946            break;
5947        }
5948        current = next_open_index(&completed, current)
5949            .ok_or_else(|| StreamError::GraphValidation("no open interleave input".into()))?;
5950    }
5951
5952    Ok(FusedExecutionReport {
5953        output,
5954        events,
5955        async_boundary_crossings: 0,
5956    })
5957}
5958
5959impl<T> GraphBlueprint<FanInShape<T, T>>
5960where
5961    T: Clone + Send + 'static,
5962{
5963    pub fn run_fan_in(&self, inputs: Vec<Vec<T>>) -> StreamResult<Vec<T>> {
5964        Ok(self
5965            .run_fan_in_report(inputs, FusedExecutionConfig::default())?
5966            .output)
5967    }
5968
5969    pub fn run_fan_in_report(
5970        &self,
5971        inputs: Vec<Vec<T>>,
5972        config: FusedExecutionConfig,
5973    ) -> StreamResult<FusedExecutionReport<T>> {
5974        self.run_fan_in_report_mode(inputs, config, ExecutorMode::Auto)
5975    }
5976
5977    pub(crate) fn run_fan_in_report_mode(
5978        &self,
5979        inputs: Vec<Vec<T>>,
5980        config: FusedExecutionConfig,
5981        mode: ExecutorMode,
5982    ) -> StreamResult<FusedExecutionReport<T>> {
5983        if inputs.len() != self.shape.inlet_count() {
5984            return Err(StreamError::GraphValidation(format!(
5985                "expected {} input streams, got {}",
5986                self.shape.inlet_count(),
5987                inputs.len()
5988            )));
5989        }
5990
5991        if mode != ExecutorMode::ErasedOnly {
5992            if let Some(schedule) = self.typed_prioritized_fan_in_schedule() {
5993                return run_typed_scheduled_fan_in(inputs, &schedule, config);
5994            }
5995            if mode == ExecutorMode::TypedOnly {
5996                return Err(StreamError::GraphValidation(
5997                    "typed executor does not support this graph shape".into(),
5998                ));
5999            }
6000        }
6001
6002        self.run_fan_in_report_erased(inputs, config)
6003    }
6004
6005    fn typed_prioritized_fan_in_schedule(&self) -> Option<Vec<usize>> {
6006        let shape_inlets = self.shape.inlets();
6007        let shape_outlet = self.shape.outlet().erase();
6008        let stage = direct_single_fan_in_stage::<T>(
6009            &self.stages,
6010            &self.edges,
6011            &shape_inlets,
6012            &shape_outlet,
6013        )?;
6014        let StageKind::MergePrioritized { weights } = &stage.spec.kind else {
6015            return None;
6016        };
6017        if weights.len() != self.shape.inlet_count() || weights.contains(&0) {
6018            return None;
6019        }
6020        Some(
6021            weights
6022                .iter()
6023                .enumerate()
6024                .flat_map(|(index, weight)| std::iter::repeat_n(index, *weight))
6025                .collect(),
6026        )
6027    }
6028
6029    fn run_fan_in_report_erased(
6030        &self,
6031        inputs: Vec<Vec<T>>,
6032        config: FusedExecutionConfig,
6033    ) -> StreamResult<FusedExecutionReport<T>> {
6034        let output_capacity = inputs.iter().map(Vec::len).sum();
6035        let mut queues: Vec<_> = inputs.into_iter().map(Vec::into_iter).collect();
6036        let schedule = self.fan_in_schedule();
6037        let mut schedule_index = 0;
6038        let outlet = self.shape.outlet().id();
6039        let mut executor = FusedExecutor::new(self, config);
6040        let mut output = Vec::with_capacity(output_capacity);
6041        let mut completed = vec![false; queues.len()];
6042
6043        {
6044            let mut output_sink = VecOutputSink {
6045                output: &mut output,
6046            };
6047            for (index, queue) in queues.iter().enumerate() {
6048                if queue.len() == 0 {
6049                    executor.complete(self.shape.inlet(index)?.id(), outlet, &mut output_sink)?;
6050                    completed[index] = true;
6051                }
6052            }
6053            while queues.iter().any(|queue| queue.len() > 0) {
6054                let input_index = next_scheduled_input(&queues, &schedule, &mut schedule_index)
6055                    .ok_or_else(|| {
6056                        StreamError::GraphValidation("no runnable fan-in input".into())
6057                    })?;
6058                let item = queues[input_index]
6059                    .next()
6060                    .expect("scheduled input had an item");
6061                executor.deliver(
6062                    self.shape.inlet(input_index)?.id(),
6063                    datum(item),
6064                    outlet,
6065                    &mut output_sink,
6066                )?;
6067                if queues[input_index].len() == 0 && !completed[input_index] {
6068                    executor.complete(
6069                        self.shape.inlet(input_index)?.id(),
6070                        outlet,
6071                        &mut output_sink,
6072                    )?;
6073                    completed[input_index] = true;
6074                }
6075            }
6076        }
6077
6078        Ok(FusedExecutionReport {
6079            output,
6080            events: executor.events,
6081            async_boundary_crossings: executor.async_boundary_crossings,
6082        })
6083    }
6084
6085    fn fan_in_schedule(&self) -> Vec<usize> {
6086        self.stages
6087            .iter()
6088            .find_map(|stage| match &stage.spec.kind {
6089                StageKind::MergePrioritized { weights }
6090                    if weights.len() == self.shape.inlet_count()
6091                        && stage.spec.outlets.len() == 1
6092                        && stage.spec.outlets[0].id() == self.shape.outlet().id()
6093                        && stage.spec.inlets.iter().map(AnyInlet::id).eq(self
6094                            .shape
6095                            .inlets()
6096                            .iter()
6097                            .map(|inlet| inlet.id())) =>
6098                {
6099                    Some(
6100                        weights
6101                            .iter()
6102                            .enumerate()
6103                            .flat_map(|(index, weight)| std::iter::repeat_n(index, *weight))
6104                            .collect(),
6105                    )
6106                }
6107                _ => None,
6108            })
6109            .unwrap_or_else(|| (0..self.shape.inlet_count()).collect())
6110    }
6111
6112    pub fn run_concat(&self, inputs: Vec<Vec<T>>) -> StreamResult<Vec<T>> {
6113        Ok(self
6114            .run_concat_report(inputs, FusedExecutionConfig::default())?
6115            .output)
6116    }
6117
6118    pub fn run_concat_report(
6119        &self,
6120        inputs: Vec<Vec<T>>,
6121        config: FusedExecutionConfig,
6122    ) -> StreamResult<FusedExecutionReport<T>> {
6123        self.run_concat_report_mode(inputs, config, ExecutorMode::Auto)
6124    }
6125
6126    pub(crate) fn run_concat_report_mode(
6127        &self,
6128        inputs: Vec<Vec<T>>,
6129        config: FusedExecutionConfig,
6130        mode: ExecutorMode,
6131    ) -> StreamResult<FusedExecutionReport<T>> {
6132        if inputs.len() != self.shape.inlet_count() {
6133            return Err(StreamError::GraphValidation(format!(
6134                "expected {} input streams, got {}",
6135                self.shape.inlet_count(),
6136                inputs.len()
6137            )));
6138        }
6139
6140        if mode != ExecutorMode::ErasedOnly {
6141            if self.typed_concat_supported() {
6142                return run_typed_concat(inputs, config);
6143            }
6144            if mode == ExecutorMode::TypedOnly {
6145                return Err(StreamError::GraphValidation(
6146                    "typed executor does not support this graph shape".into(),
6147                ));
6148            }
6149        }
6150
6151        self.run_concat_report_erased(inputs, config)
6152    }
6153
6154    fn typed_concat_supported(&self) -> bool {
6155        let shape_inlets = self.shape.inlets();
6156        let shape_outlet = self.shape.outlet().erase();
6157        direct_single_fan_in_stage::<T>(&self.stages, &self.edges, &shape_inlets, &shape_outlet)
6158            .is_some_and(|stage| matches!(&stage.spec.kind, StageKind::Concat))
6159    }
6160
6161    fn run_concat_report_erased(
6162        &self,
6163        inputs: Vec<Vec<T>>,
6164        config: FusedExecutionConfig,
6165    ) -> StreamResult<FusedExecutionReport<T>> {
6166        let output_capacity = inputs.iter().map(Vec::len).sum();
6167        let mut queues: Vec<_> = inputs.into_iter().map(Vec::into_iter).collect();
6168        let outlet = self.shape.outlet().id();
6169        let mut executor = FusedExecutor::new(self, config);
6170        let mut output = Vec::with_capacity(output_capacity);
6171
6172        {
6173            let mut output_sink = VecOutputSink {
6174                output: &mut output,
6175            };
6176            for (index, queue) in queues.iter_mut().enumerate() {
6177                for item in queue.by_ref() {
6178                    executor.deliver(
6179                        self.shape.inlet(index)?.id(),
6180                        datum(item),
6181                        outlet,
6182                        &mut output_sink,
6183                    )?;
6184                }
6185                executor.complete(self.shape.inlet(index)?.id(), outlet, &mut output_sink)?;
6186            }
6187        }
6188
6189        Ok(FusedExecutionReport {
6190            output,
6191            events: executor.events,
6192            async_boundary_crossings: executor.async_boundary_crossings,
6193        })
6194    }
6195
6196    pub fn run_or_else(&self, primary: Vec<T>, secondary: Vec<T>) -> StreamResult<Vec<T>> {
6197        Ok(self
6198            .run_or_else_report(primary, secondary, FusedExecutionConfig::default())?
6199            .output)
6200    }
6201
6202    pub fn run_or_else_report(
6203        &self,
6204        primary: Vec<T>,
6205        secondary: Vec<T>,
6206        config: FusedExecutionConfig,
6207    ) -> StreamResult<FusedExecutionReport<T>> {
6208        if self.shape.inlet_count() != 2 {
6209            return Err(StreamError::GraphValidation(format!(
6210                "or-else helper expected 2 inlets, got {}",
6211                self.shape.inlet_count()
6212            )));
6213        }
6214
6215        let primary = primary.into_iter();
6216        let secondary = secondary.into_iter();
6217        let primary_inlet = self.shape.inlet(0)?.id();
6218        let secondary_inlet = self.shape.inlet(1)?.id();
6219        let outlet = self.shape.outlet().id();
6220        let mut executor = FusedExecutor::new(self, config);
6221        let mut output = Vec::new();
6222        let mut primary_emitted = false;
6223
6224        {
6225            let mut output_sink = VecOutputSink {
6226                output: &mut output,
6227            };
6228            for item in primary {
6229                primary_emitted = true;
6230                executor.deliver(primary_inlet, datum(item), outlet, &mut output_sink)?;
6231            }
6232            executor.complete(primary_inlet, outlet, &mut output_sink)?;
6233
6234            if !primary_emitted {
6235                for item in secondary {
6236                    executor.deliver(secondary_inlet, datum(item), outlet, &mut output_sink)?;
6237                }
6238            }
6239            executor.complete(secondary_inlet, outlet, &mut output_sink)?;
6240        }
6241
6242        Ok(FusedExecutionReport {
6243            output,
6244            events: executor.events,
6245            async_boundary_crossings: executor.async_boundary_crossings,
6246        })
6247    }
6248
6249    pub fn run_or_else_secondary_first(
6250        &self,
6251        primary: Vec<T>,
6252        secondary: Vec<T>,
6253    ) -> StreamResult<Vec<T>> {
6254        Ok(self
6255            .run_or_else_secondary_first_report(
6256                primary,
6257                secondary,
6258                FusedExecutionConfig::default(),
6259            )?
6260            .output)
6261    }
6262
6263    pub fn run_or_else_secondary_first_report(
6264        &self,
6265        primary: Vec<T>,
6266        secondary: Vec<T>,
6267        config: FusedExecutionConfig,
6268    ) -> StreamResult<FusedExecutionReport<T>> {
6269        if self.shape.inlet_count() != 2 {
6270            return Err(StreamError::GraphValidation(format!(
6271                "or-else helper expected 2 inlets, got {}",
6272                self.shape.inlet_count()
6273            )));
6274        }
6275
6276        let primary_inlet = self.shape.inlet(0)?.id();
6277        let secondary_inlet = self.shape.inlet(1)?.id();
6278        let outlet = self.shape.outlet().id();
6279        let mut executor = FusedExecutor::new(self, config);
6280        let mut output = Vec::new();
6281
6282        {
6283            let mut output_sink = VecOutputSink {
6284                output: &mut output,
6285            };
6286            for item in secondary {
6287                executor.deliver(secondary_inlet, datum(item), outlet, &mut output_sink)?;
6288            }
6289            for item in primary {
6290                executor.deliver(primary_inlet, datum(item), outlet, &mut output_sink)?;
6291            }
6292            executor.complete(primary_inlet, outlet, &mut output_sink)?;
6293            executor.complete(secondary_inlet, outlet, &mut output_sink)?;
6294        }
6295
6296        Ok(FusedExecutionReport {
6297            output,
6298            events: executor.events,
6299            async_boundary_crossings: executor.async_boundary_crossings,
6300        })
6301    }
6302
6303    pub fn run_or_else_secondary_closed_first(&self, secondary: Vec<T>) -> StreamResult<Vec<T>> {
6304        if self.shape.inlet_count() != 2 {
6305            return Err(StreamError::GraphValidation(format!(
6306                "or-else helper expected 2 inlets, got {}",
6307                self.shape.inlet_count()
6308            )));
6309        }
6310
6311        let primary_inlet = self.shape.inlet(0)?.id();
6312        let secondary_inlet = self.shape.inlet(1)?.id();
6313        let outlet = self.shape.outlet().id();
6314        let mut executor = FusedExecutor::new(self, FusedExecutionConfig::default());
6315        let mut output = Vec::new();
6316
6317        {
6318            let mut output_sink = VecOutputSink {
6319                output: &mut output,
6320            };
6321            for item in secondary {
6322                executor.deliver(secondary_inlet, datum(item), outlet, &mut output_sink)?;
6323            }
6324            executor.complete(secondary_inlet, outlet, &mut output_sink)?;
6325            executor.complete(primary_inlet, outlet, &mut output_sink)?;
6326        }
6327
6328        Ok(output)
6329    }
6330
6331    pub fn run_interleave(
6332        &self,
6333        inputs: Vec<Vec<T>>,
6334        segment_size: usize,
6335        eager_close: bool,
6336    ) -> StreamResult<Vec<T>> {
6337        Ok(self
6338            .run_interleave_report(
6339                inputs,
6340                segment_size,
6341                eager_close,
6342                FusedExecutionConfig::default(),
6343            )?
6344            .output)
6345    }
6346
6347    pub fn run_interleave_report(
6348        &self,
6349        inputs: Vec<Vec<T>>,
6350        segment_size: usize,
6351        eager_close: bool,
6352        config: FusedExecutionConfig,
6353    ) -> StreamResult<FusedExecutionReport<T>> {
6354        self.run_interleave_report_mode(
6355            inputs,
6356            segment_size,
6357            eager_close,
6358            config,
6359            ExecutorMode::Auto,
6360        )
6361    }
6362
6363    pub(crate) fn run_interleave_report_mode(
6364        &self,
6365        inputs: Vec<Vec<T>>,
6366        segment_size: usize,
6367        eager_close: bool,
6368        config: FusedExecutionConfig,
6369        mode: ExecutorMode,
6370    ) -> StreamResult<FusedExecutionReport<T>> {
6371        if inputs.len() != self.shape.inlet_count() {
6372            return Err(StreamError::GraphValidation(format!(
6373                "expected {} input streams, got {}",
6374                self.shape.inlet_count(),
6375                inputs.len()
6376            )));
6377        }
6378        if segment_size == 0 {
6379            return Err(StreamError::GraphValidation(
6380                "interleave segment size must be greater than zero".into(),
6381            ));
6382        }
6383
6384        if mode != ExecutorMode::ErasedOnly {
6385            if self.typed_interleave_supported(segment_size, eager_close) {
6386                return run_typed_interleave(inputs, segment_size, eager_close, config);
6387            }
6388            if mode == ExecutorMode::TypedOnly {
6389                return Err(StreamError::GraphValidation(
6390                    "typed executor does not support this graph shape".into(),
6391                ));
6392            }
6393        }
6394
6395        self.run_interleave_report_erased(inputs, segment_size, eager_close, config)
6396    }
6397
6398    fn typed_interleave_supported(&self, segment_size: usize, eager_close: bool) -> bool {
6399        let shape_inlets = self.shape.inlets();
6400        let shape_outlet = self.shape.outlet().erase();
6401        direct_single_fan_in_stage::<T>(&self.stages, &self.edges, &shape_inlets, &shape_outlet)
6402            .is_some_and(|stage| {
6403                matches!(
6404                    &stage.spec.kind,
6405                    StageKind::Interleave {
6406                        segment_size: stage_segment_size,
6407                        eager_close: stage_eager_close,
6408                    } if *stage_segment_size == segment_size && *stage_eager_close == eager_close
6409                )
6410            })
6411    }
6412
6413    fn run_interleave_report_erased(
6414        &self,
6415        inputs: Vec<Vec<T>>,
6416        segment_size: usize,
6417        eager_close: bool,
6418        config: FusedExecutionConfig,
6419    ) -> StreamResult<FusedExecutionReport<T>> {
6420        let output_capacity = inputs.iter().map(Vec::len).sum();
6421        let mut queues: Vec<_> = inputs.into_iter().map(Vec::into_iter).collect();
6422        let mut completed = vec![false; queues.len()];
6423        let outlet = self.shape.outlet().id();
6424        let mut executor = FusedExecutor::new(self, config);
6425        let mut output = Vec::with_capacity(output_capacity);
6426
6427        {
6428            let mut output_sink = VecOutputSink {
6429                output: &mut output,
6430            };
6431            for (index, queue) in queues.iter().enumerate() {
6432                if queue.len() == 0 {
6433                    executor.complete(self.shape.inlet(index)?.id(), outlet, &mut output_sink)?;
6434                    completed[index] = true;
6435                    if eager_close {
6436                        return Ok(FusedExecutionReport {
6437                            output,
6438                            events: executor.events,
6439                            async_boundary_crossings: executor.async_boundary_crossings,
6440                        });
6441                    }
6442                }
6443            }
6444
6445            let mut current = 0usize;
6446            while completed.iter().any(|done| !done) {
6447                if completed[current] {
6448                    current = next_open_index(&completed, current).ok_or_else(|| {
6449                        StreamError::GraphValidation("no open interleave input".into())
6450                    })?;
6451                    continue;
6452                }
6453
6454                let mut emitted = 0usize;
6455                while emitted < segment_size {
6456                    match queues[current].next() {
6457                        Some(item) => {
6458                            executor.deliver(
6459                                self.shape.inlet(current)?.id(),
6460                                datum(item),
6461                                outlet,
6462                                &mut output_sink,
6463                            )?;
6464                            emitted += 1;
6465                        }
6466                        None => {
6467                            executor.complete(
6468                                self.shape.inlet(current)?.id(),
6469                                outlet,
6470                                &mut output_sink,
6471                            )?;
6472                            completed[current] = true;
6473                            if eager_close {
6474                                return Ok(FusedExecutionReport {
6475                                    output,
6476                                    events: executor.events,
6477                                    async_boundary_crossings: executor.async_boundary_crossings,
6478                                });
6479                            }
6480                            break;
6481                        }
6482                    }
6483                }
6484
6485                if completed.iter().all(|done| *done) {
6486                    break;
6487                }
6488                current = next_open_index(&completed, current).ok_or_else(|| {
6489                    StreamError::GraphValidation("no open interleave input".into())
6490                })?;
6491            }
6492        }
6493
6494        Ok(FusedExecutionReport {
6495            output,
6496            events: executor.events,
6497            async_boundary_crossings: executor.async_boundary_crossings,
6498        })
6499    }
6500}
6501
6502impl<T> GraphBlueprint<MergePreferredShape<T>>
6503where
6504    T: Clone + Send + 'static,
6505{
6506    pub fn run_merge_preferred(
6507        &self,
6508        preferred: Vec<T>,
6509        secondary_inputs: Vec<Vec<T>>,
6510    ) -> StreamResult<Vec<T>> {
6511        Ok(self
6512            .run_merge_preferred_report(
6513                preferred,
6514                secondary_inputs,
6515                FusedExecutionConfig::default(),
6516            )?
6517            .output)
6518    }
6519
6520    pub fn run_merge_preferred_report(
6521        &self,
6522        preferred: Vec<T>,
6523        secondary_inputs: Vec<Vec<T>>,
6524        config: FusedExecutionConfig,
6525    ) -> StreamResult<FusedExecutionReport<T>> {
6526        self.run_merge_preferred_report_mode(
6527            preferred,
6528            secondary_inputs,
6529            config,
6530            ExecutorMode::Auto,
6531        )
6532    }
6533
6534    pub(crate) fn run_merge_preferred_report_mode(
6535        &self,
6536        preferred: Vec<T>,
6537        secondary_inputs: Vec<Vec<T>>,
6538        config: FusedExecutionConfig,
6539        mode: ExecutorMode,
6540    ) -> StreamResult<FusedExecutionReport<T>> {
6541        if secondary_inputs.len() != self.shape.secondary_count() {
6542            return Err(StreamError::GraphValidation(format!(
6543                "expected {} secondary input streams, got {}",
6544                self.shape.secondary_count(),
6545                secondary_inputs.len()
6546            )));
6547        }
6548
6549        if mode != ExecutorMode::ErasedOnly {
6550            if self.typed_merge_preferred_supported() {
6551                return run_typed_merge_preferred(preferred, secondary_inputs, config);
6552            }
6553            if mode == ExecutorMode::TypedOnly {
6554                return Err(StreamError::GraphValidation(
6555                    "typed executor does not support this graph shape".into(),
6556                ));
6557            }
6558        }
6559
6560        self.run_merge_preferred_report_erased(preferred, secondary_inputs, config)
6561    }
6562
6563    fn typed_merge_preferred_supported(&self) -> bool {
6564        let shape_inlets = self.shape.inlets();
6565        let shape_outlet = self.shape.outlet().erase();
6566        direct_single_fan_in_stage::<T>(&self.stages, &self.edges, &shape_inlets, &shape_outlet)
6567            .is_some_and(|stage| matches!(&stage.spec.kind, StageKind::MergePreferred))
6568    }
6569
6570    fn run_merge_preferred_report_erased(
6571        &self,
6572        preferred: Vec<T>,
6573        secondary_inputs: Vec<Vec<T>>,
6574        config: FusedExecutionConfig,
6575    ) -> StreamResult<FusedExecutionReport<T>> {
6576        let output_capacity =
6577            preferred.len() + secondary_inputs.iter().map(Vec::len).sum::<usize>();
6578        let mut preferred = preferred.into_iter();
6579        let mut secondary: Vec<_> = secondary_inputs.into_iter().map(Vec::into_iter).collect();
6580        let secondary_schedule = (0..secondary.len()).collect::<Vec<_>>();
6581        let mut secondary_index = 0;
6582        let outlet = self.shape.outlet().id();
6583        let preferred_inlet = self.shape.preferred().id();
6584        let mut executor = FusedExecutor::new(self, config);
6585        let mut output = Vec::with_capacity(output_capacity);
6586        let mut preferred_completed = false;
6587        let mut secondary_completed = vec![false; secondary.len()];
6588
6589        {
6590            let mut output_sink = VecOutputSink {
6591                output: &mut output,
6592            };
6593            if preferred.len() == 0 {
6594                executor.complete(preferred_inlet, outlet, &mut output_sink)?;
6595                preferred_completed = true;
6596            }
6597            for (index, queue) in secondary.iter().enumerate() {
6598                if queue.len() == 0 {
6599                    executor.complete(
6600                        self.shape.secondary(index)?.id(),
6601                        outlet,
6602                        &mut output_sink,
6603                    )?;
6604                    secondary_completed[index] = true;
6605                }
6606            }
6607            while preferred.len() > 0 || secondary.iter().any(|queue| queue.len() > 0) {
6608                if let Some(item) = preferred.next() {
6609                    executor.deliver(preferred_inlet, datum(item), outlet, &mut output_sink)?;
6610                    if preferred.len() == 0 && !preferred_completed {
6611                        executor.complete(preferred_inlet, outlet, &mut output_sink)?;
6612                        preferred_completed = true;
6613                    }
6614                    continue;
6615                }
6616
6617                let input_index =
6618                    next_scheduled_input(&secondary, &secondary_schedule, &mut secondary_index)
6619                        .ok_or_else(|| {
6620                            StreamError::GraphValidation("no runnable secondary input".into())
6621                        })?;
6622                let item = secondary[input_index]
6623                    .next()
6624                    .expect("scheduled secondary input had an item");
6625                executor.deliver(
6626                    self.shape.secondary(input_index)?.id(),
6627                    datum(item),
6628                    outlet,
6629                    &mut output_sink,
6630                )?;
6631                if secondary[input_index].len() == 0 && !secondary_completed[input_index] {
6632                    executor.complete(
6633                        self.shape.secondary(input_index)?.id(),
6634                        outlet,
6635                        &mut output_sink,
6636                    )?;
6637                    secondary_completed[input_index] = true;
6638                }
6639            }
6640        }
6641
6642        Ok(FusedExecutionReport {
6643            output,
6644            events: executor.events,
6645            async_boundary_crossings: executor.async_boundary_crossings,
6646        })
6647    }
6648}
6649
6650fn run_typed_merge_preferred<T>(
6651    preferred: Vec<T>,
6652    secondary_inputs: Vec<Vec<T>>,
6653    config: FusedExecutionConfig,
6654) -> StreamResult<FusedExecutionReport<T>>
6655where
6656    T: Send + 'static,
6657{
6658    let output_capacity = preferred.len() + secondary_inputs.iter().map(Vec::len).sum::<usize>();
6659    let input_count = secondary_inputs.len() + 1;
6660    let events = typed_fan_in_success_events(output_capacity, input_count)?;
6661    let mut checked_events = 0;
6662    add_typed_helper_events(&mut checked_events, config, events)?;
6663
6664    let mut output = Vec::with_capacity(output_capacity);
6665    output.extend(preferred);
6666
6667    let mut secondary: Vec<_> = secondary_inputs.into_iter().map(Vec::into_iter).collect();
6668    let secondary_schedule = (0..secondary.len()).collect::<Vec<_>>();
6669    let mut secondary_index = 0;
6670    while output.len() < output_capacity {
6671        let input_index =
6672            next_scheduled_input(&secondary, &secondary_schedule, &mut secondary_index)
6673                .ok_or_else(|| {
6674                    StreamError::GraphValidation("no runnable secondary input".into())
6675                })?;
6676        let item = secondary[input_index]
6677            .next()
6678            .expect("scheduled secondary input had an item");
6679        output.push(item);
6680    }
6681
6682    Ok(FusedExecutionReport {
6683        output,
6684        events,
6685        async_boundary_crossings: 0,
6686    })
6687}
6688
6689fn next_scheduled_input<I>(
6690    queues: &[I],
6691    schedule: &[usize],
6692    schedule_index: &mut usize,
6693) -> Option<usize>
6694where
6695    I: ExactSizeIterator,
6696{
6697    if schedule.is_empty() {
6698        return queues.iter().position(|queue| queue.len() > 0);
6699    }
6700
6701    for _ in 0..schedule.len() {
6702        let index = schedule[*schedule_index % schedule.len()];
6703        *schedule_index += 1;
6704        if queues.get(index).is_some_and(|queue| queue.len() > 0) {
6705            return Some(index);
6706        }
6707    }
6708
6709    queues.iter().position(|queue| queue.len() > 0)
6710}
6711
6712fn next_open_index(completed: &[bool], current: usize) -> Option<usize> {
6713    if completed.is_empty() {
6714        return None;
6715    }
6716    let len = completed.len();
6717    for offset in 1..=len {
6718        let index = (current + offset) % len;
6719        if !completed[index] {
6720            return Some(index);
6721        }
6722    }
6723    None
6724}
6725
6726// ---------------------------------------------------------------------------
6727// Output-sink plumbing — pub(crate) so Phase 1+ typed executors can reuse
6728// ---------------------------------------------------------------------------
6729
6730/// Typed output receiver used by the fused executor.
6731///
6732/// `pub(crate)` so the upcoming typed-port executor (Phase 1+) can reuse the
6733/// same sink abstractions without duplicating the erased-path infrastructure.
6734pub(crate) trait FusedOutputSink<Out> {
6735    fn emit(&mut self, value: Out) -> StreamResult<()>;
6736}
6737
6738pub(crate) struct VecOutputSink<'a, Out> {
6739    pub(crate) output: &'a mut Vec<Out>,
6740}
6741
6742impl<Out> FusedOutputSink<Out> for VecOutputSink<'_, Out> {
6743    fn emit(&mut self, value: Out) -> StreamResult<()> {
6744        self.output.push(value);
6745        Ok(())
6746    }
6747}
6748
6749pub(crate) struct CountOutputSink {
6750    pub(crate) count: usize,
6751}
6752
6753impl<Out> FusedOutputSink<Out> for CountOutputSink {
6754    fn emit(&mut self, _value: Out) -> StreamResult<()> {
6755        self.count += 1;
6756        Ok(())
6757    }
6758}
6759
6760pub(crate) struct FoldOutputSink<Acc, F> {
6761    pub(crate) accumulator: Option<Acc>,
6762    pub(crate) fold: F,
6763}
6764
6765impl<Out, Acc, F> FusedOutputSink<Out> for FoldOutputSink<Acc, F>
6766where
6767    F: FnMut(Acc, Out) -> Acc,
6768{
6769    fn emit(&mut self, value: Out) -> StreamResult<()> {
6770        let accumulator = self
6771            .accumulator
6772            .take()
6773            .expect("fold accumulator is present while executor is running");
6774        self.accumulator = Some((self.fold)(accumulator, value));
6775        Ok(())
6776    }
6777}
6778
6779impl<Acc, F> FoldOutputSink<Acc, F> {
6780    pub(crate) fn finish(mut self) -> Acc {
6781        self.accumulator
6782            .take()
6783            .expect("fold accumulator is present after executor finishes")
6784    }
6785}
6786
6787// ---------------------------------------------------------------------------
6788// Graph-index scaffolding — pub(crate) for Phase 1+ typed executor reuse
6789// ---------------------------------------------------------------------------
6790
6791/// Fused graph executor over erased (`Box<dyn DatumElement>`) values.
6792///
6793/// The struct itself and its edge/stage-lookup maps are `pub(crate)` so the
6794/// typed-port executor introduced in Phase 1+ can reuse the graph-index
6795/// infrastructure (edge maps, stage-index maps) without duplicating it.
6796/// The per-element hot path remains encapsulated — only the structural pieces
6797/// are exposed.
6798#[derive(Debug)]
6799pub(crate) struct FusedExecutor<'a, S: Shape> {
6800    graph: &'a GraphBlueprint<S>,
6801    /// Outlet → connected inlet (internal graph edge lookup).
6802    pub(crate) edge_by_outlet: HashMap<PortId, PortId>,
6803    /// Inlet → connected outlet (internal graph edge lookup).
6804    pub(crate) edge_by_inlet: HashMap<PortId, PortId>,
6805    /// Inlet port → owning stage index.
6806    pub(crate) stage_by_inlet: HashMap<PortId, usize>,
6807    /// Outlet port → owning stage index.
6808    pub(crate) stage_by_outlet: HashMap<PortId, usize>,
6809    stage_states: Vec<StageState>,
6810    pub(crate) opaque_logics: Vec<Option<GraphStageLogic>>,
6811    timer_mailbox: Arc<StageTimerMailbox>,
6812    timers_enabled: bool,
6813    config: FusedExecutionConfig,
6814    pub(crate) events: usize,
6815    pub(crate) async_boundary_crossings: usize,
6816    cancelled_outlets: HashSet<PortId>,
6817    event_stack: Vec<FusedEvent>,
6818    draining: bool,
6819    has_cycle: bool,
6820}
6821
6822#[derive(Debug)]
6823enum StageState {
6824    Stateless,
6825    Broadcast {
6826        cancelled_outlets: Vec<bool>,
6827        live_outlets: usize,
6828    },
6829    Balance {
6830        next: usize,
6831        cancelled_outlets: Vec<bool>,
6832        live_outlets: usize,
6833    },
6834    Merge {
6835        open_inputs: usize,
6836        eager_complete: bool,
6837        completed: bool,
6838    },
6839    OrElse {
6840        primary_emitted: bool,
6841        buffer: VecDeque<DatumValue>,
6842        primary_closed: bool,
6843        secondary_closed: bool,
6844        completed: bool,
6845    },
6846    Zip {
6847        left_inlet: PortId,
6848        right_inlet: PortId,
6849        left: VecDeque<DatumValue>,
6850        right: VecDeque<DatumValue>,
6851        left_pending_complete: bool,
6852        right_pending_complete: bool,
6853        completed: bool,
6854    },
6855    Unzip {
6856        fast_path: Option<UnzipFanInFastPath>,
6857        zip_fast_path: Option<UnzipZipFastPath>,
6858        demand: [bool; 2],
6859        cancelled: [bool; 2],
6860        upstream_closed: bool,
6861    },
6862    MergeSorted {
6863        left: VecDeque<DatumValue>,
6864        right: VecDeque<DatumValue>,
6865        left_closed: bool,
6866        right_closed: bool,
6867        pending: VecDeque<DatumValue>,
6868        completed: bool,
6869    },
6870    MergeSequence {
6871        next_sequence: u64,
6872        pending: Vec<(u64, DatumValue)>,
6873        completed_count: usize,
6874        output_buffer: VecDeque<DatumValue>,
6875        completed: bool,
6876    },
6877    MergeLatest {
6878        latest: Vec<Option<DatumValue>>,
6879        seen_count: usize,
6880        completed_count: usize,
6881        pending: VecDeque<DatumValue>,
6882        completed: bool,
6883    },
6884    Partition {
6885        pending: Option<(usize, DatumValue)>,
6886        upstream_closed: bool,
6887        demand: Vec<bool>,
6888        cancelled: Vec<bool>,
6889        output_count: usize,
6890        completed: bool,
6891    },
6892}
6893
6894#[derive(Clone, Copy, Debug)]
6895struct UnzipFanInFastPath {
6896    fan_in_stage_index: usize,
6897    /// Which inlet indices of the fan-in stage the two Unzip outlets connect to.
6898    /// `target_inlet_indices[0]` is the index of the inlet connected to `out0`,
6899    /// and `target_inlet_indices[1]` is the index connected to `out1`.
6900    target_inlet_indices: [usize; 2],
6901}
6902
6903#[derive(Clone, Copy, Debug)]
6904struct UnzipZipFastPath {
6905    zip_stage_index: usize,
6906}
6907
6908enum StageEmissions {
6909    None,
6910    One(PortId, DatumValue),
6911    Two((PortId, DatumValue), (PortId, DatumValue)),
6912    Many(Vec<(PortId, DatumValue)>),
6913}
6914
6915struct StageTransition {
6916    emissions: StageEmissions,
6917    completed_outlets: Vec<PortId>,
6918    cancelled_inlets: Vec<PortId>,
6919}
6920
6921impl StageTransition {
6922    fn none() -> Self {
6923        Self {
6924            emissions: StageEmissions::None,
6925            completed_outlets: Vec::new(),
6926            cancelled_inlets: Vec::new(),
6927        }
6928    }
6929
6930    fn emit(emissions: StageEmissions) -> Self {
6931        Self {
6932            emissions,
6933            completed_outlets: Vec::new(),
6934            cancelled_inlets: Vec::new(),
6935        }
6936    }
6937
6938    fn with_completion(mut self, completed_outlets: Vec<PortId>) -> Self {
6939        self.completed_outlets = completed_outlets;
6940        self
6941    }
6942
6943    fn with_cancellations(mut self, cancelled_inlets: Vec<PortId>) -> Self {
6944        self.cancelled_inlets = cancelled_inlets;
6945        self
6946    }
6947}
6948
6949#[derive(Debug)]
6950enum FusedEvent {
6951    Deliver { inlet: PortId, value: DatumValue },
6952    CompleteInlet { inlet: PortId },
6953    Request { outlet: PortId },
6954    DownstreamFinish { outlet: PortId },
6955    Emit { outlet: PortId, value: DatumValue },
6956    CompleteOutlet { outlet: PortId },
6957    CancelInlet { inlet: PortId },
6958}
6959
6960fn graph_has_cycle<S: Shape>(
6961    graph: &GraphBlueprint<S>,
6962    stage_by_inlet: &HashMap<PortId, usize>,
6963    stage_by_outlet: &HashMap<PortId, usize>,
6964) -> bool {
6965    let mut adjacency = vec![Vec::new(); graph.stages.len()];
6966    for edge in &graph.edges {
6967        let Some(from) = stage_by_outlet.get(&edge.outlet).copied() else {
6968            continue;
6969        };
6970        let Some(to) = stage_by_inlet.get(&edge.inlet).copied() else {
6971            continue;
6972        };
6973        adjacency[from].push(to);
6974    }
6975
6976    #[derive(Clone, Copy, PartialEq, Eq)]
6977    enum Visit {
6978        New,
6979        Active,
6980        Done,
6981    }
6982
6983    fn visit(node: usize, adjacency: &[Vec<usize>], marks: &mut [Visit]) -> bool {
6984        marks[node] = Visit::Active;
6985        for &next in &adjacency[node] {
6986            if marks[next] == Visit::Active {
6987                return true;
6988            }
6989            if marks[next] == Visit::New && visit(next, adjacency, marks) {
6990                return true;
6991            }
6992        }
6993        marks[node] = Visit::Done;
6994        false
6995    }
6996
6997    let mut marks = vec![Visit::New; graph.stages.len()];
6998    for node in 0..graph.stages.len() {
6999        if marks[node] == Visit::New && visit(node, &adjacency, &mut marks) {
7000            return true;
7001        }
7002    }
7003    false
7004}
7005
7006impl<'a, S: Shape> FusedExecutor<'a, S> {
7007    fn is_outlet_cancelled(&self, outlet: PortId) -> bool {
7008        !self.cancelled_outlets.is_empty() && self.cancelled_outlets.contains(&outlet)
7009    }
7010
7011    fn new(graph: &'a GraphBlueprint<S>, config: FusedExecutionConfig) -> Self {
7012        let edge_by_outlet = graph
7013            .edges
7014            .iter()
7015            .map(|edge| (edge.outlet, edge.inlet))
7016            .collect();
7017        let edge_by_inlet = graph
7018            .edges
7019            .iter()
7020            .map(|edge| (edge.inlet, edge.outlet))
7021            .collect();
7022        let mut stage_by_inlet = HashMap::new();
7023        let mut stage_by_outlet = HashMap::new();
7024        for (index, stage) in graph.stages.iter().enumerate() {
7025            for inlet in &stage.spec.inlets {
7026                stage_by_inlet.insert(inlet.id(), index);
7027            }
7028            for outlet in &stage.spec.outlets {
7029                stage_by_outlet.insert(outlet.id(), index);
7030            }
7031        }
7032
7033        let timer_mailbox = Arc::new(StageTimerMailbox::default());
7034        let timer_materializer = Arc::new(OnceLock::new());
7035        let mut opaque_logics: Vec<_> = graph
7036            .stages
7037            .iter()
7038            .map(|stage| stage.logic_factory.as_ref().map(|factory| factory()))
7039            .collect();
7040        for (stage_index, logic) in opaque_logics.iter_mut().enumerate() {
7041            if let Some(logic) = logic {
7042                logic.attach_timer_runtime(StageTimerRuntime::new(
7043                    stage_index,
7044                    Arc::clone(&timer_mailbox),
7045                    Arc::clone(&timer_materializer),
7046                ));
7047            }
7048        }
7049        let timers_enabled = opaque_logics.iter().any(|logic| {
7050            logic
7051                .as_ref()
7052                .is_some_and(GraphStageLogic::has_timer_handler)
7053        });
7054
7055        let stage_states = graph
7056            .stages
7057            .iter()
7058            .map(|stage| match stage.spec.kind {
7059                StageKind::Broadcast => StageState::Broadcast {
7060                    cancelled_outlets: vec![false; stage.spec.outlets.len()],
7061                    live_outlets: stage.spec.outlets.len(),
7062                },
7063                StageKind::Balance => StageState::Balance {
7064                    next: 0,
7065                    cancelled_outlets: vec![false; stage.spec.outlets.len()],
7066                    live_outlets: stage.spec.outlets.len(),
7067                },
7068                StageKind::Merge => StageState::Merge {
7069                    open_inputs: stage.spec.inlets.len(),
7070                    eager_complete: false,
7071                    completed: false,
7072                },
7073                StageKind::MergePreferred => StageState::Merge {
7074                    open_inputs: stage.spec.inlets.len(),
7075                    eager_complete: false,
7076                    completed: false,
7077                },
7078                StageKind::MergePrioritized { .. } => StageState::Merge {
7079                    open_inputs: stage.spec.inlets.len(),
7080                    eager_complete: false,
7081                    completed: false,
7082                },
7083                StageKind::Concat => StageState::Merge {
7084                    open_inputs: stage.spec.inlets.len(),
7085                    eager_complete: false,
7086                    completed: false,
7087                },
7088                StageKind::Interleave { eager_close, .. } => StageState::Merge {
7089                    open_inputs: stage.spec.inlets.len(),
7090                    eager_complete: eager_close,
7091                    completed: false,
7092                },
7093                StageKind::OrElse { primary_inlet: _ } => StageState::OrElse {
7094                    primary_emitted: false,
7095                    buffer: VecDeque::new(),
7096                    primary_closed: false,
7097                    secondary_closed: false,
7098                    completed: false,
7099                },
7100                StageKind::Zip(_) => {
7101                    if let [left, right] = stage.spec.inlets.as_slice() {
7102                        StageState::Zip {
7103                            left_inlet: left.id(),
7104                            right_inlet: right.id(),
7105                            left: VecDeque::new(),
7106                            right: VecDeque::new(),
7107                            left_pending_complete: false,
7108                            right_pending_complete: false,
7109                            completed: false,
7110                        }
7111                    } else {
7112                        StageState::Stateless
7113                    }
7114                }
7115                StageKind::Unzip { .. } => StageState::Unzip {
7116                    fast_path: unzip_fan_in_fast_path(
7117                        stage,
7118                        graph,
7119                        &edge_by_outlet,
7120                        &stage_by_inlet,
7121                    ),
7122                    zip_fast_path: unzip_zip_fast_path(
7123                        stage,
7124                        graph,
7125                        &edge_by_outlet,
7126                        &stage_by_inlet,
7127                    ),
7128                    demand: [false; 2],
7129                    cancelled: [false; 2],
7130                    upstream_closed: false,
7131                },
7132                StageKind::MergeSorted(_) => StageState::MergeSorted {
7133                    left: VecDeque::new(),
7134                    right: VecDeque::new(),
7135                    left_closed: false,
7136                    right_closed: false,
7137                    pending: VecDeque::new(),
7138                    completed: false,
7139                },
7140                StageKind::MergeSequence { .. } => StageState::MergeSequence {
7141                    next_sequence: 0,
7142                    pending: Vec::new(),
7143                    completed_count: 0,
7144                    output_buffer: VecDeque::new(),
7145                    completed: false,
7146                },
7147                StageKind::MergeLatest { input_count, .. } => {
7148                    let mut latest = Vec::with_capacity(input_count);
7149                    for _ in 0..input_count {
7150                        latest.push(None);
7151                    }
7152                    StageState::MergeLatest {
7153                        latest,
7154                        seen_count: 0,
7155                        completed_count: 0,
7156                        pending: VecDeque::new(),
7157                        completed: false,
7158                    }
7159                }
7160                StageKind::Partition { output_count, .. } => StageState::Partition {
7161                    pending: None,
7162                    upstream_closed: false,
7163                    demand: vec![false; output_count],
7164                    cancelled: vec![false; output_count],
7165                    output_count,
7166                    completed: false,
7167                },
7168                _ => StageState::Stateless,
7169            })
7170            .collect();
7171
7172        let has_cycle =
7173            !graph.edges.is_empty() && graph_has_cycle(graph, &stage_by_inlet, &stage_by_outlet);
7174
7175        let mut executor = Self {
7176            graph,
7177            edge_by_outlet,
7178            edge_by_inlet,
7179            stage_by_inlet,
7180            stage_by_outlet,
7181            stage_states,
7182            opaque_logics,
7183            timer_mailbox,
7184            timers_enabled,
7185            config,
7186            events: 0,
7187            async_boundary_crossings: 0,
7188            cancelled_outlets: HashSet::new(),
7189            event_stack: Vec::new(),
7190            draining: false,
7191            has_cycle,
7192        };
7193        executor.prime_connected_demands();
7194        executor
7195    }
7196
7197    fn deliver<Out>(
7198        &mut self,
7199        inlet: PortId,
7200        value: DatumValue,
7201        graph_outlet: PortId,
7202        output: &mut impl FusedOutputSink<Out>,
7203    ) -> StreamResult<()>
7204    where
7205        Out: Send + 'static,
7206    {
7207        if !self.has_cycle {
7208            return self.process_deliver_direct(inlet, value, graph_outlet, output);
7209        }
7210        self.schedule_event(FusedEvent::Deliver { inlet, value });
7211        self.drain_events(graph_outlet, output)
7212    }
7213
7214    fn complete<Out>(
7215        &mut self,
7216        inlet: PortId,
7217        graph_outlet: PortId,
7218        output: &mut impl FusedOutputSink<Out>,
7219    ) -> StreamResult<()>
7220    where
7221        Out: Send + 'static,
7222    {
7223        if !self.has_cycle {
7224            return self.process_complete_inlet_direct(inlet, graph_outlet, output);
7225        }
7226        self.schedule_event(FusedEvent::CompleteInlet { inlet });
7227        self.drain_events(graph_outlet, output)
7228    }
7229
7230    fn request<Out>(
7231        &mut self,
7232        outlet: PortId,
7233        graph_outlet: PortId,
7234        output: &mut impl FusedOutputSink<Out>,
7235    ) -> StreamResult<()>
7236    where
7237        Out: Send + 'static,
7238    {
7239        if !self.has_cycle {
7240            return self.process_request_direct(outlet, graph_outlet, output);
7241        }
7242        self.schedule_event(FusedEvent::Request { outlet });
7243        self.drain_events(graph_outlet, output)
7244    }
7245
7246    #[cfg_attr(not(test), allow(dead_code))]
7247    fn downstream_finish<Out>(
7248        &mut self,
7249        outlet: PortId,
7250        graph_outlet: PortId,
7251        output: &mut impl FusedOutputSink<Out>,
7252    ) -> StreamResult<()>
7253    where
7254        Out: Send + 'static,
7255    {
7256        if !self.has_cycle {
7257            return self.process_downstream_finish_direct(outlet, graph_outlet, output);
7258        }
7259        self.schedule_event(FusedEvent::DownstreamFinish { outlet });
7260        self.drain_events(graph_outlet, output)
7261    }
7262
7263    fn schedule_event(&mut self, event: FusedEvent) {
7264        self.event_stack.push(event);
7265    }
7266
7267    fn schedule_transition(&mut self, transition: StageTransition) {
7268        for inlet in transition.cancelled_inlets.into_iter().rev() {
7269            self.schedule_event(FusedEvent::CancelInlet { inlet });
7270        }
7271        for outlet in transition.completed_outlets.into_iter().rev() {
7272            if !self.is_outlet_cancelled(outlet) {
7273                self.schedule_event(FusedEvent::CompleteOutlet { outlet });
7274            }
7275        }
7276        self.schedule_emissions_in_order(transition.emissions);
7277    }
7278
7279    fn schedule_emissions_in_order(&mut self, emissions: StageEmissions) {
7280        match emissions {
7281            StageEmissions::None => {}
7282            StageEmissions::One(outlet, value) => {
7283                self.schedule_event(FusedEvent::Emit { outlet, value });
7284            }
7285            StageEmissions::Two((first_outlet, first_value), (second_outlet, second_value)) => {
7286                self.schedule_event(FusedEvent::Emit {
7287                    outlet: second_outlet,
7288                    value: second_value,
7289                });
7290                self.schedule_event(FusedEvent::Emit {
7291                    outlet: first_outlet,
7292                    value: first_value,
7293                });
7294            }
7295            StageEmissions::Many(emissions) => {
7296                for (outlet, value) in emissions.into_iter().rev() {
7297                    self.schedule_event(FusedEvent::Emit { outlet, value });
7298                }
7299            }
7300        }
7301    }
7302
7303    fn drain_events<Out>(
7304        &mut self,
7305        graph_outlet: PortId,
7306        output: &mut impl FusedOutputSink<Out>,
7307    ) -> StreamResult<()>
7308    where
7309        Out: Send + 'static,
7310    {
7311        if self.draining {
7312            return Ok(());
7313        }
7314
7315        self.draining = true;
7316        let result = (|| {
7317            while let Some(event) = self.event_stack.pop() {
7318                self.process_event(event, graph_outlet, output)?;
7319            }
7320            Ok(())
7321        })();
7322        self.draining = false;
7323        if result.is_err() {
7324            self.event_stack.clear();
7325        }
7326        result
7327    }
7328
7329    fn drain_timer_events_nonblocking<Out>(
7330        &mut self,
7331        graph_outlet: PortId,
7332        output: &mut impl FusedOutputSink<Out>,
7333    ) -> StreamResult<()>
7334    where
7335        Out: Send + 'static,
7336    {
7337        if !self.timers_enabled {
7338            return Ok(());
7339        }
7340        if !self.timer_mailbox.has_pending() {
7341            return Ok(());
7342        }
7343        while let Some(stage_index) = self.timer_mailbox.pop() {
7344            self.process_timer_event(stage_index, graph_outlet, output)?;
7345        }
7346        Ok(())
7347    }
7348
7349    fn drain_timer_events_until_idle<Out>(
7350        &mut self,
7351        graph_outlet: PortId,
7352        output: &mut impl FusedOutputSink<Out>,
7353    ) -> StreamResult<()>
7354    where
7355        Out: Send + 'static,
7356    {
7357        if !self.timers_enabled {
7358            return Ok(());
7359        }
7360        self.drain_timer_events_nonblocking(graph_outlet, output)?;
7361        while self.has_timer_work() || self.timer_mailbox.has_pending() {
7362            let stage_index = self.timer_mailbox.wait();
7363            self.process_timer_event(stage_index, graph_outlet, output)?;
7364            self.drain_timer_events_nonblocking(graph_outlet, output)?;
7365        }
7366        Ok(())
7367    }
7368
7369    fn has_timer_work(&self) -> bool {
7370        self.opaque_logics
7371            .iter()
7372            .any(|logic| logic.as_ref().is_some_and(GraphStageLogic::has_timer_work))
7373    }
7374
7375    fn process_timer_event<Out>(
7376        &mut self,
7377        stage_index: usize,
7378        graph_outlet: PortId,
7379        output: &mut impl FusedOutputSink<Out>,
7380    ) -> StreamResult<()>
7381    where
7382        Out: Send + 'static,
7383    {
7384        let Some(transition) = self.process_stage_timer(stage_index)? else {
7385            return Ok(());
7386        };
7387        self.bump_event()?;
7388        if self.has_cycle {
7389            self.process_transition(transition);
7390            self.drain_events(graph_outlet, output)
7391        } else {
7392            self.process_transition_direct(transition, graph_outlet, output)
7393        }
7394    }
7395
7396    fn process_stage_timer(&mut self, stage_index: usize) -> StreamResult<Option<StageTransition>> {
7397        let Some(stage) = self.graph.stages.get(stage_index) else {
7398            return Ok(None);
7399        };
7400        if !matches!(stage.spec.kind, StageKind::Opaque) {
7401            return Ok(None);
7402        }
7403        let Some(logic) = self
7404            .opaque_logics
7405            .get_mut(stage_index)
7406            .and_then(|logic| logic.as_mut())
7407        else {
7408            return Ok(None);
7409        };
7410
7411        logic.drain_async_callbacks();
7412        let Some(key) = logic.pop_timer_event() else {
7413            return Ok(None);
7414        };
7415
7416        let mut handler = logic.take_timer_handler();
7417        let result = if let Some(ref mut handler) = handler {
7418            handler.on_timer(logic, &key)
7419        } else {
7420            Ok(())
7421        };
7422        if let Some(handler) = handler {
7423            logic.restore_timer_handler(handler);
7424        }
7425        if result.is_err() {
7426            logic.cancel_all_timers();
7427        }
7428        result?;
7429
7430        self.collect_opaque_emissions(stage, stage_index).map(Some)
7431    }
7432
7433    fn process_event<Out>(
7434        &mut self,
7435        event: FusedEvent,
7436        graph_outlet: PortId,
7437        output: &mut impl FusedOutputSink<Out>,
7438    ) -> StreamResult<()>
7439    where
7440        Out: Send + 'static,
7441    {
7442        match event {
7443            FusedEvent::Deliver { inlet, value } => {
7444                self.bump_event()?;
7445                let stage_index = *self.stage_by_inlet.get(&inlet).ok_or_else(|| {
7446                    StreamError::GraphValidation(format!(
7447                        "inlet {} has no owning stage",
7448                        inlet.as_usize()
7449                    ))
7450                })?;
7451                let transition = self.process_stage(stage_index, inlet, value)?;
7452                self.process_transition(transition);
7453                Ok(())
7454            }
7455            FusedEvent::CompleteInlet { inlet } => {
7456                self.bump_event()?;
7457                let stage_index = *self.stage_by_inlet.get(&inlet).ok_or_else(|| {
7458                    StreamError::GraphValidation(format!(
7459                        "inlet {} has no owning stage",
7460                        inlet.as_usize()
7461                    ))
7462                })?;
7463                let transition = self.process_completion(stage_index, inlet)?;
7464                self.process_transition(transition);
7465                Ok(())
7466            }
7467            FusedEvent::Request { outlet } => {
7468                if self.is_outlet_cancelled(outlet) {
7469                    return Ok(());
7470                }
7471                self.bump_event()?;
7472                let Some(stage_index) = self.stage_by_outlet.get(&outlet).copied() else {
7473                    return Ok(());
7474                };
7475                let transition = self.process_pull(stage_index, outlet)?;
7476                self.process_transition(transition);
7477                Ok(())
7478            }
7479            FusedEvent::DownstreamFinish { outlet } => {
7480                if !self.cancelled_outlets.insert(outlet) {
7481                    return Ok(());
7482                }
7483                self.bump_event()?;
7484                let stage_index = *self.stage_by_outlet.get(&outlet).ok_or_else(|| {
7485                    StreamError::GraphValidation(format!(
7486                        "outlet {} has no owning stage",
7487                        outlet.as_usize()
7488                    ))
7489                })?;
7490                let transition = self.process_downstream_finish(stage_index, outlet)?;
7491                self.process_transition(transition);
7492                Ok(())
7493            }
7494            FusedEvent::Emit { outlet, value } => {
7495                self.process_emit_cyclic(outlet, value, graph_outlet, output)
7496            }
7497            FusedEvent::CompleteOutlet { outlet } => {
7498                self.process_complete_outlet_cyclic(outlet, graph_outlet, output)
7499            }
7500            FusedEvent::CancelInlet { inlet } => {
7501                if let Some(outlet) = self.edge_by_inlet.get(&inlet).copied() {
7502                    self.schedule_event(FusedEvent::DownstreamFinish { outlet });
7503                }
7504                Ok(())
7505            }
7506        }
7507    }
7508
7509    fn process_transition(&mut self, transition: StageTransition) {
7510        self.schedule_transition(transition);
7511    }
7512
7513    fn process_transition_direct<Out>(
7514        &mut self,
7515        transition: StageTransition,
7516        graph_outlet: PortId,
7517        output: &mut impl FusedOutputSink<Out>,
7518    ) -> StreamResult<()>
7519    where
7520        Out: Send + 'static,
7521    {
7522        self.process_emissions_direct(transition.emissions, graph_outlet, output)?;
7523        for outlet in transition.completed_outlets {
7524            if !self.is_outlet_cancelled(outlet) {
7525                self.process_complete_outlet_direct(outlet, graph_outlet, output)?;
7526            }
7527        }
7528        for inlet in transition.cancelled_inlets {
7529            self.process_cancel_inlet_direct(inlet, graph_outlet, output)?;
7530        }
7531        Ok(())
7532    }
7533
7534    fn process_emissions_direct<Out>(
7535        &mut self,
7536        emissions: StageEmissions,
7537        graph_outlet: PortId,
7538        output: &mut impl FusedOutputSink<Out>,
7539    ) -> StreamResult<()>
7540    where
7541        Out: Send + 'static,
7542    {
7543        match emissions {
7544            StageEmissions::None => Ok(()),
7545            StageEmissions::One(outlet, value) => {
7546                self.process_emit_direct(outlet, value, graph_outlet, output)
7547            }
7548            StageEmissions::Two((first_outlet, first_value), (second_outlet, second_value)) => {
7549                self.process_emit_direct(first_outlet, first_value, graph_outlet, output)?;
7550                self.process_emit_direct(second_outlet, second_value, graph_outlet, output)
7551            }
7552            StageEmissions::Many(emissions) => {
7553                for (outlet, value) in emissions {
7554                    self.process_emit_direct(outlet, value, graph_outlet, output)?;
7555                }
7556                Ok(())
7557            }
7558        }
7559    }
7560
7561    fn process_deliver_direct<Out>(
7562        &mut self,
7563        inlet: PortId,
7564        value: DatumValue,
7565        graph_outlet: PortId,
7566        output: &mut impl FusedOutputSink<Out>,
7567    ) -> StreamResult<()>
7568    where
7569        Out: Send + 'static,
7570    {
7571        self.bump_event()?;
7572        let stage_index = *self.stage_by_inlet.get(&inlet).ok_or_else(|| {
7573            StreamError::GraphValidation(format!("inlet {} has no owning stage", inlet.as_usize()))
7574        })?;
7575        let transition = self.process_stage(stage_index, inlet, value)?;
7576        self.process_transition_direct(transition, graph_outlet, output)
7577    }
7578
7579    fn process_complete_inlet_direct<Out>(
7580        &mut self,
7581        inlet: PortId,
7582        graph_outlet: PortId,
7583        output: &mut impl FusedOutputSink<Out>,
7584    ) -> StreamResult<()>
7585    where
7586        Out: Send + 'static,
7587    {
7588        self.bump_event()?;
7589        let stage_index = *self.stage_by_inlet.get(&inlet).ok_or_else(|| {
7590            StreamError::GraphValidation(format!("inlet {} has no owning stage", inlet.as_usize()))
7591        })?;
7592        let transition = self.process_completion(stage_index, inlet)?;
7593        self.process_transition_direct(transition, graph_outlet, output)
7594    }
7595
7596    fn process_request_direct<Out>(
7597        &mut self,
7598        outlet: PortId,
7599        graph_outlet: PortId,
7600        output: &mut impl FusedOutputSink<Out>,
7601    ) -> StreamResult<()>
7602    where
7603        Out: Send + 'static,
7604    {
7605        if self.is_outlet_cancelled(outlet) {
7606            return Ok(());
7607        }
7608        self.bump_event()?;
7609        let Some(stage_index) = self.stage_by_outlet.get(&outlet).copied() else {
7610            return Ok(());
7611        };
7612        let transition = self.process_pull(stage_index, outlet)?;
7613        self.process_transition_direct(transition, graph_outlet, output)
7614    }
7615
7616    fn process_downstream_finish_direct<Out>(
7617        &mut self,
7618        outlet: PortId,
7619        graph_outlet: PortId,
7620        output: &mut impl FusedOutputSink<Out>,
7621    ) -> StreamResult<()>
7622    where
7623        Out: Send + 'static,
7624    {
7625        if !self.cancelled_outlets.insert(outlet) {
7626            return Ok(());
7627        }
7628        self.bump_event()?;
7629        let stage_index = *self.stage_by_outlet.get(&outlet).ok_or_else(|| {
7630            StreamError::GraphValidation(format!(
7631                "outlet {} has no owning stage",
7632                outlet.as_usize()
7633            ))
7634        })?;
7635        let transition = self.process_downstream_finish(stage_index, outlet)?;
7636        self.process_transition_direct(transition, graph_outlet, output)
7637    }
7638
7639    fn process_cancel_inlet_direct<Out>(
7640        &mut self,
7641        inlet: PortId,
7642        graph_outlet: PortId,
7643        output: &mut impl FusedOutputSink<Out>,
7644    ) -> StreamResult<()>
7645    where
7646        Out: Send + 'static,
7647    {
7648        if let Some(outlet) = self.edge_by_inlet.get(&inlet).copied() {
7649            self.process_downstream_finish_direct(outlet, graph_outlet, output)
7650        } else {
7651            Ok(())
7652        }
7653    }
7654
7655    fn process_emit_direct<Out>(
7656        &mut self,
7657        outlet: PortId,
7658        value: DatumValue,
7659        graph_outlet: PortId,
7660        output: &mut impl FusedOutputSink<Out>,
7661    ) -> StreamResult<()>
7662    where
7663        Out: Send + 'static,
7664    {
7665        self.bump_event()?;
7666        if self.is_outlet_cancelled(outlet) {
7667            return Ok(());
7668        }
7669        if outlet == graph_outlet {
7670            output.emit(downcast_datum(value, "emit", || {
7671                format!("outlet#{}", outlet.as_usize())
7672            })?)?;
7673            return self.process_request_direct(outlet, graph_outlet, output);
7674        }
7675
7676        let Some(inlet) = self.edge_by_outlet.get(&outlet).copied() else {
7677            return Err(StreamError::GraphValidation(format!(
7678                "outlet {} is neither connected nor graph output",
7679                outlet.as_usize()
7680            )));
7681        };
7682        let should_repull = self
7683            .stage_by_outlet
7684            .get(&outlet)
7685            .copied()
7686            .is_some_and(|stage_index| {
7687                matches!(
7688                    self.graph.stages[stage_index].spec.kind,
7689                    StageKind::Opaque | StageKind::Unzip { .. } | StageKind::Partition { .. }
7690                )
7691            });
7692        self.process_deliver_direct(inlet, value, graph_outlet, output)?;
7693        if should_repull {
7694            self.process_request_direct(outlet, graph_outlet, output)?;
7695        }
7696        Ok(())
7697    }
7698
7699    fn process_emit_cyclic<Out>(
7700        &mut self,
7701        outlet: PortId,
7702        value: DatumValue,
7703        graph_outlet: PortId,
7704        output: &mut impl FusedOutputSink<Out>,
7705    ) -> StreamResult<()>
7706    where
7707        Out: Send + 'static,
7708    {
7709        self.bump_event()?;
7710        if self.is_outlet_cancelled(outlet) {
7711            return Ok(());
7712        }
7713        if outlet == graph_outlet {
7714            output.emit(downcast_datum(value, "emit", || {
7715                format!("outlet#{}", outlet.as_usize())
7716            })?)?;
7717            self.schedule_event(FusedEvent::Request { outlet });
7718            return Ok(());
7719        }
7720
7721        let Some(inlet) = self.edge_by_outlet.get(&outlet).copied() else {
7722            return Err(StreamError::GraphValidation(format!(
7723                "outlet {} is neither connected nor graph output",
7724                outlet.as_usize()
7725            )));
7726        };
7727        let should_repull = self
7728            .stage_by_outlet
7729            .get(&outlet)
7730            .copied()
7731            .is_some_and(|stage_index| {
7732                matches!(
7733                    self.graph.stages[stage_index].spec.kind,
7734                    StageKind::Opaque | StageKind::Unzip { .. } | StageKind::Partition { .. }
7735                )
7736            });
7737        if should_repull {
7738            self.schedule_event(FusedEvent::Request { outlet });
7739        }
7740        self.schedule_event(FusedEvent::Deliver { inlet, value });
7741        Ok(())
7742    }
7743
7744    fn process_complete_outlet_direct<Out>(
7745        &mut self,
7746        outlet: PortId,
7747        graph_outlet: PortId,
7748        output: &mut impl FusedOutputSink<Out>,
7749    ) -> StreamResult<()>
7750    where
7751        Out: Send + 'static,
7752    {
7753        self.bump_event()?;
7754        if outlet == graph_outlet {
7755            return Ok(());
7756        }
7757        let Some(inlet) = self.edge_by_outlet.get(&outlet).copied() else {
7758            return Err(StreamError::GraphValidation(format!(
7759                "outlet {} is neither connected nor graph output",
7760                outlet.as_usize()
7761            )));
7762        };
7763        self.process_complete_inlet_direct(inlet, graph_outlet, output)
7764    }
7765
7766    fn process_complete_outlet_cyclic<Out>(
7767        &mut self,
7768        outlet: PortId,
7769        graph_outlet: PortId,
7770        _output: &mut impl FusedOutputSink<Out>,
7771    ) -> StreamResult<()>
7772    where
7773        Out: Send + 'static,
7774    {
7775        self.bump_event()?;
7776        if outlet == graph_outlet {
7777            return Ok(());
7778        }
7779        let Some(inlet) = self.edge_by_outlet.get(&outlet).copied() else {
7780            return Err(StreamError::GraphValidation(format!(
7781                "outlet {} is neither connected nor graph output",
7782                outlet.as_usize()
7783            )));
7784        };
7785        self.schedule_event(FusedEvent::CompleteInlet { inlet });
7786        Ok(())
7787    }
7788
7789    fn process_stage(
7790        &mut self,
7791        stage_index: usize,
7792        inlet: PortId,
7793        value: DatumValue,
7794    ) -> StreamResult<StageTransition> {
7795        let stage = &self.graph.stages[stage_index];
7796        match &stage.spec.kind {
7797            StageKind::Identity => Ok(StageTransition::emit(StageEmissions::One(
7798                single_outlet(stage)?,
7799                value,
7800            ))),
7801            StageKind::Opaque => {
7802                if let Some(logic) = self
7803                    .opaque_logics
7804                    .get_mut(stage_index)
7805                    .and_then(|l| l.as_mut())
7806                {
7807                    logic.drain_async_callbacks();
7808                    let inlet_ref = stage.spec.inlets.iter().find(|i| i.id() == inlet).cloned();
7809                    if let Some(inlet_ref) = inlet_ref {
7810                        logic.offer_datum(inlet, value)?;
7811                        let mut handler = logic.take_in_handler(inlet);
7812                        let result = if let Some(ref mut handler) = handler {
7813                            let inlet_any = inlet_ref;
7814                            handler.on_push(logic, inlet_any)
7815                        } else {
7816                            Ok(())
7817                        };
7818                        if let Some(handler) = handler {
7819                            logic.restore_in_handler(inlet, handler);
7820                        }
7821                        if result.is_err() {
7822                            logic.cancel_all_timers();
7823                        }
7824                        result?;
7825                    }
7826                    self.collect_opaque_emissions(stage, stage_index)
7827                } else {
7828                    Ok(StageTransition::emit(StageEmissions::One(
7829                        single_outlet(stage)?,
7830                        value,
7831                    )))
7832                }
7833            }
7834            StageKind::Map(map) => Ok(StageTransition::emit(StageEmissions::One(
7835                single_outlet(stage)?,
7836                (map.erased)(value)?,
7837            ))),
7838            StageKind::AsyncBoundary => {
7839                self.async_boundary_crossings += 1;
7840                Ok(StageTransition::emit(StageEmissions::One(
7841                    single_outlet(stage)?,
7842                    value,
7843                )))
7844            }
7845            StageKind::Broadcast => {
7846                broadcast_emissions(&stage.spec.outlets, value).map(StageTransition::emit)
7847            }
7848            StageKind::Balance => {
7849                let outlets = &stage.spec.outlets;
7850                if outlets.is_empty() {
7851                    return Err(StreamError::GraphValidation(
7852                        "balance has no outlets".into(),
7853                    ));
7854                }
7855                let StageState::Balance {
7856                    next,
7857                    cancelled_outlets,
7858                    live_outlets,
7859                } = &mut self.stage_states[stage_index]
7860                else {
7861                    return Err(StreamError::GraphValidation(
7862                        "balance state is missing".into(),
7863                    ));
7864                };
7865                if *live_outlets == 0 {
7866                    return Ok(StageTransition::none());
7867                }
7868                let mut selected = None;
7869                for offset in 0..outlets.len() {
7870                    let index = (*next + offset) % outlets.len();
7871                    if !cancelled_outlets[index] {
7872                        selected = Some(index);
7873                        break;
7874                    }
7875                }
7876                let index = selected.ok_or_else(|| {
7877                    StreamError::GraphValidation("balance has no live outlets".into())
7878                })?;
7879                let outlet = outlets[index].id();
7880                *next = (index + 1) % outlets.len();
7881                Ok(StageTransition::emit(StageEmissions::One(outlet, value)))
7882            }
7883            StageKind::Merge | StageKind::MergePreferred | StageKind::MergePrioritized { .. } => {
7884                let StageState::Merge { completed, .. } = &self.stage_states[stage_index] else {
7885                    return Err(StreamError::GraphValidation(
7886                        "merge state is missing".into(),
7887                    ));
7888                };
7889                if *completed {
7890                    return Ok(StageTransition::none());
7891                }
7892                Ok(StageTransition::emit(StageEmissions::One(
7893                    single_outlet(stage)?,
7894                    value,
7895                )))
7896            }
7897            StageKind::Concat | StageKind::Interleave { .. } => {
7898                let StageState::Merge { completed, .. } = &self.stage_states[stage_index] else {
7899                    return Err(StreamError::GraphValidation(
7900                        "fan-in state is missing".into(),
7901                    ));
7902                };
7903                if *completed {
7904                    return Ok(StageTransition::none());
7905                }
7906                Ok(StageTransition::emit(StageEmissions::One(
7907                    single_outlet(stage)?,
7908                    value,
7909                )))
7910            }
7911            StageKind::OrElse { primary_inlet } => {
7912                let StageState::OrElse {
7913                    primary_emitted,
7914                    buffer,
7915                    primary_closed,
7916                    completed,
7917                    ..
7918                } = &mut self.stage_states[stage_index]
7919                else {
7920                    return Err(StreamError::GraphValidation(
7921                        "or-else state is missing".into(),
7922                    ));
7923                };
7924                if *completed {
7925                    return Ok(StageTransition::none());
7926                }
7927                if inlet == *primary_inlet {
7928                    *primary_emitted = true;
7929                    buffer.clear();
7930                    Ok(StageTransition::emit(StageEmissions::One(
7931                        single_outlet(stage)?,
7932                        value,
7933                    )))
7934                } else if *primary_emitted {
7935                    Ok(StageTransition::none())
7936                } else if *primary_closed {
7937                    Ok(StageTransition::emit(StageEmissions::One(
7938                        single_outlet(stage)?,
7939                        value,
7940                    )))
7941                } else {
7942                    buffer.push_back(value);
7943                    Ok(StageTransition::none())
7944                }
7945            }
7946            StageKind::Zip(zip) => {
7947                if stage.spec.inlets.len() != 2 {
7948                    return Err(StreamError::GraphValidation(format!(
7949                        "zip stage {} expected 2 inlets, got {}",
7950                        stage.spec.name(),
7951                        stage.spec.inlets.len()
7952                    )));
7953                }
7954
7955                let ready = {
7956                    let StageState::Zip {
7957                        left_inlet,
7958                        right_inlet,
7959                        left,
7960                        right,
7961                        left_pending_complete,
7962                        right_pending_complete,
7963                        completed,
7964                    } = &mut self.stage_states[stage_index]
7965                    else {
7966                        return Err(StreamError::GraphValidation("zip state is missing".into()));
7967                    };
7968
7969                    if *completed {
7970                        return Ok(StageTransition::none());
7971                    }
7972
7973                    // Bounded per-inlet buffering: an inlet may receive several
7974                    // elements before its pair arrives (e.g. an asymmetric upstream
7975                    // topology), so queue them and pair in arrival order.
7976                    if inlet == *left_inlet {
7977                        left.push_back(value);
7978                    } else if inlet == *right_inlet {
7979                        right.push_back(value);
7980                    } else {
7981                        return Err(StreamError::GraphValidation(format!(
7982                            "zip inlet {} is not part of the stage",
7983                            inlet.as_usize()
7984                        )));
7985                    }
7986
7987                    match (left.front().is_some(), right.front().is_some()) {
7988                        (true, true) => {
7989                            let left_item =
7990                                left.pop_front().expect("zip left buffer had an element");
7991                            let right_item =
7992                                right.pop_front().expect("zip right buffer had an element");
7993                            let should_complete = (*left_pending_complete && left.is_empty())
7994                                || (*right_pending_complete && right.is_empty());
7995                            Some((left_item, right_item, should_complete))
7996                        }
7997                        _ => None,
7998                    }
7999                };
8000
8001                if let Some((left, right, should_complete)) = ready {
8002                    let outlet = single_outlet(stage)?;
8003                    if should_complete {
8004                        let StageState::Zip { completed, .. } = &mut self.stage_states[stage_index]
8005                        else {
8006                            return Err(StreamError::GraphValidation(
8007                                "zip state is missing".into(),
8008                            ));
8009                        };
8010                        *completed = true;
8011                    }
8012                    let mut transition =
8013                        StageTransition::emit(StageEmissions::One(outlet, zip(left, right)?));
8014                    if should_complete {
8015                        transition.completed_outlets.push(outlet);
8016                    }
8017                    Ok(transition)
8018                } else {
8019                    Ok(StageTransition::none())
8020                }
8021            }
8022            StageKind::Unzip { split, .. } => {
8023                let (fan_in, zip_fast) = match &self.stage_states[stage_index] {
8024                    StageState::Unzip {
8025                        fast_path,
8026                        zip_fast_path,
8027                        ..
8028                    } => (*fast_path, *zip_fast_path),
8029                    _ => (None, None),
8030                };
8031                if let Some(zip_fast) = zip_fast {
8032                    let (out0_val, out1_val) = split(value);
8033                    let zip_stage = &self.graph.stages[zip_fast.zip_stage_index];
8034                    let StageKind::Zip(zip) = &zip_stage.spec.kind else {
8035                        return Err(StreamError::GraphValidation(
8036                            "unzip-zip fast path references a non-zip stage".into(),
8037                        ));
8038                    };
8039                    let outlet = single_outlet(zip_stage)?;
8040                    let zipped = zip(out0_val, out1_val)?;
8041                    return Ok(StageTransition::emit(StageEmissions::One(outlet, zipped)));
8042                }
8043                if let Some(fast_path) = fan_in {
8044                    let (out0_val, out1_val) = split(value);
8045                    let target = &self.graph.stages[fast_path.fan_in_stage_index];
8046                    match &target.spec.kind {
8047                        StageKind::MergeSorted(compare) => {
8048                            let result = {
8049                                let StageState::MergeSorted {
8050                                    left,
8051                                    right,
8052                                    left_closed,
8053                                    right_closed,
8054                                    pending,
8055                                    completed,
8056                                } = &mut self.stage_states[fast_path.fan_in_stage_index]
8057                                else {
8058                                    return Err(StreamError::GraphValidation(
8059                                        "merge-sorted state is missing".into(),
8060                                    ));
8061                                };
8062                                if *completed {
8063                                    return Ok(StageTransition::none());
8064                                }
8065                                // Route each Unzip output to the correct left/right queue
8066                                // based on which inlet index each outlet connects to.
8067                                if fast_path.target_inlet_indices[0] == 0 {
8068                                    left.push_back(out0_val);
8069                                    right.push_back(out1_val);
8070                                } else {
8071                                    left.push_back(out1_val);
8072                                    right.push_back(out0_val);
8073                                }
8074
8075                                loop {
8076                                    let next = match (left.front(), right.front()) {
8077                                        (Some(l), Some(r)) => {
8078                                            if compare(l, r) != std::cmp::Ordering::Greater {
8079                                                left.pop_front()
8080                                            } else {
8081                                                right.pop_front()
8082                                            }
8083                                        }
8084                                        (Some(_), None) if *right_closed => left.pop_front(),
8085                                        (None, Some(_)) if *left_closed => right.pop_front(),
8086                                        _ => break,
8087                                    };
8088                                    if let Some(val) = next {
8089                                        pending.push_back(val);
8090                                    } else {
8091                                        break;
8092                                    }
8093                                }
8094
8095                                if let Some(output) = pending.pop_front() {
8096                                    let outlet = single_outlet(target)?;
8097                                    let all_done = *left_closed
8098                                        && *right_closed
8099                                        && left.is_empty()
8100                                        && right.is_empty()
8101                                        && pending.is_empty();
8102                                    if all_done {
8103                                        *completed = true;
8104                                        StageTransition::emit(StageEmissions::One(outlet, output))
8105                                            .with_completion(vec![outlet])
8106                                    } else {
8107                                        StageTransition::emit(StageEmissions::One(outlet, output))
8108                                    }
8109                                } else {
8110                                    StageTransition::none()
8111                                }
8112                            };
8113                            Ok(result)
8114                        }
8115                        StageKind::MergeSequence {
8116                            extract_sequence,
8117                            input_count,
8118                            ..
8119                        } => {
8120                            let result = {
8121                                let StageState::MergeSequence {
8122                                    next_sequence,
8123                                    pending,
8124                                    completed_count,
8125                                    output_buffer,
8126                                    completed,
8127                                } = &mut self.stage_states[fast_path.fan_in_stage_index]
8128                                else {
8129                                    return Err(StreamError::GraphValidation(
8130                                        "merge-sequence state is missing".into(),
8131                                    ));
8132                                };
8133                                if *completed {
8134                                    return Ok(StageTransition::none());
8135                                }
8136                                for val in [out0_val, out1_val] {
8137                                    let seq = extract_sequence(&val);
8138                                    if seq == *next_sequence {
8139                                        output_buffer.push_back(val);
8140                                        *next_sequence += 1;
8141                                        while let Some(index) =
8142                                            pending.iter().position(|(s, _)| *s == *next_sequence)
8143                                        {
8144                                            let (_, item) = pending.remove(index);
8145                                            output_buffer.push_back(item);
8146                                            *next_sequence += 1;
8147                                        }
8148                                    } else {
8149                                        if pending.iter().any(|(s, _)| *s == seq) {
8150                                            return Err(StreamError::Failed(format!(
8151                                                "duplicate sequence {seq} on merge sequence"
8152                                            )));
8153                                        }
8154                                        pending.push((seq, val));
8155                                        pending.sort_by_key(|(s, _)| *s);
8156                                        while let Some(index) =
8157                                            pending.iter().position(|(s, _)| *s == *next_sequence)
8158                                        {
8159                                            let (_, item) = pending.remove(index);
8160                                            output_buffer.push_back(item);
8161                                            *next_sequence += 1;
8162                                        }
8163                                    }
8164                                }
8165
8166                                if !output_buffer.is_empty() {
8167                                    let outlet = single_outlet(target)?;
8168                                    let all_done = *completed_count >= *input_count;
8169                                    let emissions: Vec<_> =
8170                                        output_buffer.drain(..).map(|v| (outlet, v)).collect();
8171                                    if all_done {
8172                                        *completed = true;
8173                                        StageTransition::emit(StageEmissions::Many(emissions))
8174                                            .with_completion(vec![outlet])
8175                                    } else {
8176                                        StageTransition::emit(StageEmissions::Many(emissions))
8177                                    }
8178                                } else {
8179                                    StageTransition::none()
8180                                }
8181                            };
8182                            Ok(result)
8183                        }
8184                        StageKind::MergeLatest {
8185                            input_count,
8186                            build_snapshot,
8187                            ..
8188                        } => {
8189                            let result = {
8190                                let StageState::MergeLatest {
8191                                    latest,
8192                                    seen_count,
8193                                    completed_count,
8194                                    pending,
8195                                    completed,
8196                                } = &mut self.stage_states[fast_path.fan_in_stage_index]
8197                                else {
8198                                    return Err(StreamError::GraphValidation(
8199                                        "merge-latest state is missing".into(),
8200                                    ));
8201                                };
8202                                if *completed {
8203                                    return Ok(StageTransition::none());
8204                                }
8205                                let inlets = &target.spec.inlets;
8206                                for (idx, val) in fast_path
8207                                    .target_inlet_indices
8208                                    .into_iter()
8209                                    .zip([out0_val, out1_val])
8210                                {
8211                                    if idx < inlets.len() && latest[idx].is_none() {
8212                                        *seen_count += 1;
8213                                    }
8214                                    latest[idx] = Some(val);
8215                                    if *seen_count >= *input_count {
8216                                        let values: Vec<&DatumValue> =
8217                                            latest.iter().filter_map(|v| v.as_ref()).collect();
8218                                        let snapshot = build_snapshot(&values);
8219                                        pending.push_back(snapshot);
8220                                    }
8221                                }
8222
8223                                if !pending.is_empty() {
8224                                    let outlet = single_outlet(target)?;
8225                                    let all_done = *completed_count >= *input_count;
8226                                    let emissions: Vec<_> =
8227                                        pending.drain(..).map(|v| (outlet, v)).collect();
8228                                    if all_done {
8229                                        *completed = true;
8230                                        StageTransition::emit(StageEmissions::Many(emissions))
8231                                            .with_completion(vec![outlet])
8232                                    } else {
8233                                        StageTransition::emit(StageEmissions::Many(emissions))
8234                                    }
8235                                } else {
8236                                    StageTransition::none()
8237                                }
8238                            };
8239                            Ok(result)
8240                        }
8241                        _ => {
8242                            let transition = {
8243                                let StageState::Unzip { cancelled, .. } =
8244                                    &self.stage_states[stage_index]
8245                                else {
8246                                    return Err(StreamError::GraphValidation(
8247                                        "unzip state is missing".into(),
8248                                    ));
8249                                };
8250                                let out0 = stage.spec.outlets.first().map(AnyOutlet::id);
8251                                let out1 = stage.spec.outlets.get(1).map(AnyOutlet::id);
8252                                let live0 = out0.is_some() && !cancelled[0];
8253                                let live1 = out1.is_some() && !cancelled[1];
8254                                if live0 && live1 {
8255                                    StageTransition::emit(StageEmissions::Two(
8256                                        (out0.unwrap(), out0_val),
8257                                        (out1.unwrap(), out1_val),
8258                                    ))
8259                                } else if live0 {
8260                                    StageTransition::emit(StageEmissions::One(
8261                                        out0.unwrap(),
8262                                        out0_val,
8263                                    ))
8264                                } else if live1 {
8265                                    StageTransition::emit(StageEmissions::One(
8266                                        out1.unwrap(),
8267                                        out1_val,
8268                                    ))
8269                                } else {
8270                                    StageTransition::none()
8271                                }
8272                            };
8273                            Ok(transition)
8274                        }
8275                    }
8276                } else {
8277                    let transition = {
8278                        let StageState::Unzip { cancelled, .. } = &self.stage_states[stage_index]
8279                        else {
8280                            return Err(StreamError::GraphValidation(
8281                                "unzip state is missing".into(),
8282                            ));
8283                        };
8284                        let (out0_val, out1_val) = split(value);
8285                        let out0 = stage.spec.outlets.first().map(AnyOutlet::id);
8286                        let out1 = stage.spec.outlets.get(1).map(AnyOutlet::id);
8287                        let live0 = out0.is_some() && !cancelled[0];
8288                        let live1 = out1.is_some() && !cancelled[1];
8289                        if live0 && live1 {
8290                            StageTransition::emit(StageEmissions::Two(
8291                                (out0.unwrap(), out0_val),
8292                                (out1.unwrap(), out1_val),
8293                            ))
8294                        } else if live0 {
8295                            StageTransition::emit(StageEmissions::One(out0.unwrap(), out0_val))
8296                        } else if live1 {
8297                            StageTransition::emit(StageEmissions::One(out1.unwrap(), out1_val))
8298                        } else {
8299                            StageTransition::none()
8300                        }
8301                    };
8302                    Ok(transition)
8303                }
8304            }
8305            StageKind::MergeSorted(compare) => {
8306                let result = {
8307                    let StageState::MergeSorted {
8308                        left,
8309                        right,
8310                        left_closed,
8311                        right_closed,
8312                        pending,
8313                        completed,
8314                    } = &mut self.stage_states[stage_index]
8315                    else {
8316                        return Err(StreamError::GraphValidation(
8317                            "merge-sorted state is missing".into(),
8318                        ));
8319                    };
8320                    if *completed {
8321                        return Ok(StageTransition::none());
8322                    }
8323                    let is_left = stage.spec.inlets.first().is_some_and(|i| i.id() == inlet);
8324                    if is_left {
8325                        left.push_back(value);
8326                    } else {
8327                        right.push_back(value);
8328                    }
8329
8330                    loop {
8331                        let next = match (left.front(), right.front()) {
8332                            (Some(l), Some(r)) => {
8333                                if compare(l, r) != std::cmp::Ordering::Greater {
8334                                    left.pop_front()
8335                                } else {
8336                                    right.pop_front()
8337                                }
8338                            }
8339                            (Some(_), None) if *right_closed => left.pop_front(),
8340                            (None, Some(_)) if *left_closed => right.pop_front(),
8341                            _ => break,
8342                        };
8343                        if let Some(val) = next {
8344                            pending.push_back(val);
8345                        } else {
8346                            break;
8347                        }
8348                    }
8349
8350                    if let Some(output) = pending.pop_front() {
8351                        let outlet = single_outlet(stage)?;
8352                        let all_done = *left_closed
8353                            && *right_closed
8354                            && left.is_empty()
8355                            && right.is_empty()
8356                            && pending.is_empty();
8357                        if all_done {
8358                            *completed = true;
8359                            StageTransition::emit(StageEmissions::One(outlet, output))
8360                                .with_completion(vec![outlet])
8361                        } else {
8362                            StageTransition::emit(StageEmissions::One(outlet, output))
8363                        }
8364                    } else {
8365                        StageTransition::none()
8366                    }
8367                };
8368                Ok(result)
8369            }
8370            StageKind::MergeSequence {
8371                input_count,
8372                extract_sequence,
8373                ..
8374            } => {
8375                let result = {
8376                    let StageState::MergeSequence {
8377                        next_sequence,
8378                        pending,
8379                        completed_count,
8380                        output_buffer,
8381                        completed,
8382                    } = &mut self.stage_states[stage_index]
8383                    else {
8384                        return Err(StreamError::GraphValidation(
8385                            "merge-sequence state is missing".into(),
8386                        ));
8387                    };
8388                    if *completed {
8389                        return Ok(StageTransition::none());
8390                    }
8391                    let seq = extract_sequence(&value);
8392                    if seq == *next_sequence {
8393                        output_buffer.push_back(value);
8394                        *next_sequence += 1;
8395                        while let Some(index) =
8396                            pending.iter().position(|(s, _)| *s == *next_sequence)
8397                        {
8398                            let (_, item) = pending.remove(index);
8399                            output_buffer.push_back(item);
8400                            *next_sequence += 1;
8401                        }
8402                    } else {
8403                        if pending.iter().any(|(s, _)| *s == seq) {
8404                            return Err(StreamError::Failed(format!(
8405                                "duplicate sequence {seq} on merge sequence"
8406                            )));
8407                        }
8408                        pending.push((seq, value));
8409                        pending.sort_by_key(|(s, _)| *s);
8410                        while let Some(index) =
8411                            pending.iter().position(|(s, _)| *s == *next_sequence)
8412                        {
8413                            let (_, item) = pending.remove(index);
8414                            output_buffer.push_back(item);
8415                            *next_sequence += 1;
8416                        }
8417                    }
8418
8419                    if !output_buffer.is_empty() {
8420                        let outlet = single_outlet(stage)?;
8421                        let all_done = *completed_count >= *input_count;
8422                        let emissions: Vec<_> =
8423                            output_buffer.drain(..).map(|v| (outlet, v)).collect();
8424                        if all_done {
8425                            *completed = true;
8426                            StageTransition::emit(StageEmissions::Many(emissions))
8427                                .with_completion(vec![outlet])
8428                        } else {
8429                            StageTransition::emit(StageEmissions::Many(emissions))
8430                        }
8431                    } else {
8432                        StageTransition::none()
8433                    }
8434                };
8435                Ok(result)
8436            }
8437            StageKind::MergeLatest {
8438                input_count,
8439                build_snapshot,
8440                ..
8441            } => {
8442                let result = {
8443                    let StageState::MergeLatest {
8444                        latest,
8445                        seen_count,
8446                        completed_count,
8447                        pending,
8448                        completed,
8449                    } = &mut self.stage_states[stage_index]
8450                    else {
8451                        return Err(StreamError::GraphValidation(
8452                            "merge-latest state is missing".into(),
8453                        ));
8454                    };
8455                    if *completed {
8456                        return Ok(StageTransition::none());
8457                    }
8458                    let inlet_index = stage
8459                        .spec
8460                        .inlets
8461                        .iter()
8462                        .position(|i| i.id() == inlet)
8463                        .ok_or_else(|| {
8464                            StreamError::GraphValidation(format!(
8465                                "merge-latest inlet {} not part of stage",
8466                                inlet.as_usize()
8467                            ))
8468                        })?;
8469                    if latest[inlet_index].is_none() {
8470                        *seen_count += 1;
8471                    }
8472                    latest[inlet_index] = Some(value);
8473                    if *seen_count >= *input_count {
8474                        let values: Vec<&DatumValue> =
8475                            latest.iter().filter_map(|v| v.as_ref()).collect();
8476                        let snapshot = build_snapshot(&values);
8477                        pending.push_back(snapshot);
8478                    }
8479
8480                    if !pending.is_empty() {
8481                        let outlet = single_outlet(stage)?;
8482                        let all_done = *completed_count >= *input_count;
8483                        let emissions: Vec<_> = pending.drain(..).map(|v| (outlet, v)).collect();
8484                        if all_done {
8485                            *completed = true;
8486                            StageTransition::emit(StageEmissions::Many(emissions))
8487                                .with_completion(vec![outlet])
8488                        } else {
8489                            StageTransition::emit(StageEmissions::Many(emissions))
8490                        }
8491                    } else {
8492                        StageTransition::none()
8493                    }
8494                };
8495                Ok(result)
8496            }
8497            StageKind::Partition {
8498                output_count,
8499                partitioner,
8500                ..
8501            } => {
8502                let result = {
8503                    let StageState::Partition {
8504                        pending,
8505                        upstream_closed: _,
8506                        demand,
8507                        cancelled,
8508                        output_count: _,
8509                        completed,
8510                        ..
8511                    } = &mut self.stage_states[stage_index]
8512                    else {
8513                        return Err(StreamError::GraphValidation(
8514                            "partition state is missing".into(),
8515                        ));
8516                    };
8517                    if *completed {
8518                        return Ok(StageTransition::none());
8519                    }
8520                    let idx = partitioner(&value);
8521                    if idx >= *output_count {
8522                        return Err(StreamError::Failed(format!(
8523                            "partitioner returned out-of-bounds index {idx} for {output_count} outputs"
8524                        )));
8525                    }
8526                    if cancelled[idx] {
8527                        return Ok(StageTransition::none());
8528                    }
8529                    if demand[idx] {
8530                        demand[idx] = false;
8531                        let outlet = stage.spec.outlets[idx].id();
8532                        StageTransition::emit(StageEmissions::One(outlet, value))
8533                    } else {
8534                        *pending = Some((idx, value));
8535                        StageTransition::none()
8536                    }
8537                };
8538                Ok(result)
8539            }
8540        }
8541    }
8542
8543    fn process_completion(
8544        &mut self,
8545        stage_index: usize,
8546        inlet: PortId,
8547    ) -> StreamResult<StageTransition> {
8548        let stage = &self.graph.stages[stage_index];
8549        match &stage.spec.kind {
8550            StageKind::Identity | StageKind::Map(_) | StageKind::AsyncBoundary => {
8551                Ok(StageTransition::emit(StageEmissions::None)
8552                    .with_completion(vec![single_outlet(stage)?]))
8553            }
8554            StageKind::Opaque => {
8555                if let Some(logic) = self
8556                    .opaque_logics
8557                    .get_mut(stage_index)
8558                    .and_then(|l| l.as_mut())
8559                {
8560                    logic.drain_async_callbacks();
8561                    logic.complete_inlet_by_id(inlet)?;
8562                    let inlet_ref = stage.spec.inlets.iter().find(|i| i.id() == inlet).cloned();
8563                    if let Some(inlet_ref) = inlet_ref {
8564                        let mut handler = logic.take_in_handler(inlet);
8565                        let result = if let Some(ref mut handler) = handler {
8566                            let inlet_any = inlet_ref;
8567                            handler.on_upstream_finish(logic, inlet_any)
8568                        } else {
8569                            Ok(())
8570                        };
8571                        if let Some(handler) = handler {
8572                            logic.restore_in_handler(inlet, handler);
8573                        }
8574                        if result.is_err() {
8575                            logic.cancel_all_timers();
8576                        }
8577                        result?;
8578                    }
8579                    self.collect_opaque_emissions(stage, stage_index)
8580                } else {
8581                    Ok(StageTransition::emit(StageEmissions::None)
8582                        .with_completion(vec![single_outlet(stage)?]))
8583                }
8584            }
8585            StageKind::Broadcast | StageKind::Balance => {
8586                Ok(StageTransition::emit(StageEmissions::None)
8587                    .with_completion(stage.spec.outlets.iter().map(AnyOutlet::id).collect()))
8588            }
8589            StageKind::Merge | StageKind::MergePreferred | StageKind::MergePrioritized { .. } => {
8590                let StageState::Merge {
8591                    open_inputs,
8592                    eager_complete,
8593                    completed,
8594                } = &mut self.stage_states[stage_index]
8595                else {
8596                    return Err(StreamError::GraphValidation(
8597                        "merge state is missing".into(),
8598                    ));
8599                };
8600
8601                if *completed {
8602                    return Ok(StageTransition::none());
8603                }
8604                if *open_inputs == 0 {
8605                    return Ok(StageTransition::none());
8606                }
8607                *open_inputs -= 1;
8608                if *eager_complete || *open_inputs == 0 {
8609                    *completed = true;
8610                    Ok(StageTransition::emit(StageEmissions::None)
8611                        .with_completion(vec![single_outlet(stage)?]))
8612                } else {
8613                    Ok(StageTransition::none())
8614                }
8615            }
8616            StageKind::Concat | StageKind::Interleave { .. } => {
8617                let StageState::Merge {
8618                    open_inputs,
8619                    eager_complete,
8620                    completed,
8621                } = &mut self.stage_states[stage_index]
8622                else {
8623                    return Err(StreamError::GraphValidation(
8624                        "fan-in state is missing".into(),
8625                    ));
8626                };
8627
8628                if *completed {
8629                    return Ok(StageTransition::none());
8630                }
8631                if *open_inputs == 0 {
8632                    return Ok(StageTransition::none());
8633                }
8634                *open_inputs -= 1;
8635                if *eager_complete || *open_inputs == 0 {
8636                    *completed = true;
8637                    Ok(StageTransition::emit(StageEmissions::None)
8638                        .with_completion(vec![single_outlet(stage)?]))
8639                } else {
8640                    Ok(StageTransition::none())
8641                }
8642            }
8643            StageKind::OrElse { primary_inlet } => {
8644                let StageState::OrElse {
8645                    primary_emitted,
8646                    buffer,
8647                    primary_closed,
8648                    secondary_closed,
8649                    completed,
8650                    ..
8651                } = &mut self.stage_states[stage_index]
8652                else {
8653                    return Err(StreamError::GraphValidation(
8654                        "or-else state is missing".into(),
8655                    ));
8656                };
8657                if *completed {
8658                    return Ok(StageTransition::none());
8659                }
8660                if inlet == *primary_inlet {
8661                    *primary_closed = true;
8662                    if *primary_emitted {
8663                        *completed = true;
8664                        buffer.clear();
8665                        Ok(StageTransition::emit(StageEmissions::None)
8666                            .with_completion(vec![single_outlet(stage)?]))
8667                    } else {
8668                        let outlet = single_outlet(stage)?;
8669                        let emissions: Vec<_> = buffer.drain(..).map(|v| (outlet, v)).collect();
8670                        if *secondary_closed {
8671                            *completed = true;
8672                            let transition = if emissions.is_empty() {
8673                                StageTransition::emit(StageEmissions::None)
8674                            } else {
8675                                StageTransition::emit(StageEmissions::Many(emissions))
8676                            };
8677                            Ok(transition.with_completion(vec![outlet]))
8678                        } else {
8679                            if emissions.is_empty() {
8680                                Ok(StageTransition::none())
8681                            } else {
8682                                Ok(StageTransition::emit(StageEmissions::Many(emissions)))
8683                            }
8684                        }
8685                    }
8686                } else {
8687                    *secondary_closed = true;
8688                    if *primary_closed && !*primary_emitted {
8689                        let outlet = single_outlet(stage)?;
8690                        let emissions: Vec<_> = buffer.drain(..).map(|v| (outlet, v)).collect();
8691                        *completed = true;
8692                        if emissions.is_empty() {
8693                            Ok(StageTransition::emit(StageEmissions::None)
8694                                .with_completion(vec![outlet]))
8695                        } else {
8696                            Ok(StageTransition::emit(StageEmissions::Many(emissions))
8697                                .with_completion(vec![outlet]))
8698                        }
8699                    } else {
8700                        Ok(StageTransition::none())
8701                    }
8702                }
8703            }
8704            StageKind::Zip(_) => {
8705                let StageState::Zip {
8706                    left_inlet,
8707                    right_inlet,
8708                    left,
8709                    right,
8710                    left_pending_complete,
8711                    right_pending_complete,
8712                    completed,
8713                } = &mut self.stage_states[stage_index]
8714                else {
8715                    return Err(StreamError::GraphValidation("zip state is missing".into()));
8716                };
8717                if *completed {
8718                    return Ok(StageTransition::none());
8719                }
8720                let finishes_left = inlet == *left_inlet;
8721                let finishes_right = inlet == *right_inlet;
8722                if (finishes_left && left.is_empty()) || (finishes_right && right.is_empty()) {
8723                    *completed = true;
8724                    Ok(StageTransition::emit(StageEmissions::None)
8725                        .with_completion(vec![single_outlet(stage)?]))
8726                } else {
8727                    if finishes_left {
8728                        *left_pending_complete = true;
8729                    }
8730                    if finishes_right {
8731                        *right_pending_complete = true;
8732                    }
8733                    Ok(StageTransition::none())
8734                }
8735            }
8736            StageKind::Unzip { .. } => {
8737                let (fan_in, zip_fast) = match &self.stage_states[stage_index] {
8738                    StageState::Unzip {
8739                        fast_path,
8740                        zip_fast_path,
8741                        ..
8742                    } => (*fast_path, *zip_fast_path),
8743                    _ => (None, None),
8744                };
8745                if let Some(zip_fast) = zip_fast {
8746                    let StageState::Unzip {
8747                        upstream_closed, ..
8748                    } = &mut self.stage_states[stage_index]
8749                    else {
8750                        return Err(StreamError::GraphValidation(
8751                            "unzip state is missing".into(),
8752                        ));
8753                    };
8754                    *upstream_closed = true;
8755                    let zip_stage = &self.graph.stages[zip_fast.zip_stage_index];
8756                    return Ok(StageTransition::emit(StageEmissions::None)
8757                        .with_completion(vec![single_outlet(zip_stage)?]));
8758                }
8759                if let Some(fast_path) = fan_in {
8760                    let StageState::Unzip {
8761                        upstream_closed, ..
8762                    } = &mut self.stage_states[stage_index]
8763                    else {
8764                        return Err(StreamError::GraphValidation(
8765                            "unzip state is missing".into(),
8766                        ));
8767                    };
8768                    *upstream_closed = true;
8769                    let target_stage = &self.graph.stages[fast_path.fan_in_stage_index];
8770                    // Only notify the two inlets that the Unzip outlets are wired to —
8771                    // do not call process_completion on unrelated inlets of the fan-in
8772                    // stage (e.g. a third inlet fed by a separate source).
8773                    let target_inlets = [
8774                        target_stage.spec.inlets[fast_path.target_inlet_indices[0]].id(),
8775                        target_stage.spec.inlets[fast_path.target_inlet_indices[1]].id(),
8776                    ];
8777                    let mut combined = StageTransition::none();
8778                    for target_inlet in target_inlets {
8779                        let t =
8780                            self.process_completion(fast_path.fan_in_stage_index, target_inlet)?;
8781                        combined.emissions = merge_emissions(combined.emissions, t.emissions);
8782                        combined.completed_outlets.extend(t.completed_outlets);
8783                        combined.cancelled_inlets.extend(t.cancelled_inlets);
8784                    }
8785                    Ok(combined)
8786                } else {
8787                    let StageState::Unzip {
8788                        upstream_closed, ..
8789                    } = &mut self.stage_states[stage_index]
8790                    else {
8791                        return Err(StreamError::GraphValidation(
8792                            "unzip state is missing".into(),
8793                        ));
8794                    };
8795                    *upstream_closed = true;
8796                    Ok(StageTransition::emit(StageEmissions::None)
8797                        .with_completion(stage.spec.outlets.iter().map(AnyOutlet::id).collect()))
8798                }
8799            }
8800            StageKind::MergeSorted(compare) => {
8801                let result = {
8802                    let StageState::MergeSorted {
8803                        left,
8804                        right,
8805                        left_closed,
8806                        right_closed,
8807                        pending,
8808                        completed,
8809                    } = &mut self.stage_states[stage_index]
8810                    else {
8811                        return Err(StreamError::GraphValidation(
8812                            "merge-sorted state is missing".into(),
8813                        ));
8814                    };
8815                    if *completed {
8816                        return Ok(StageTransition::none());
8817                    }
8818                    let is_left = stage.spec.inlets.first().is_some_and(|i| i.id() == inlet);
8819                    if is_left {
8820                        *left_closed = true;
8821                    } else {
8822                        *right_closed = true;
8823                    }
8824
8825                    loop {
8826                        let next = match (left.front(), right.front()) {
8827                            (Some(l), Some(r)) => {
8828                                if compare(l, r) != std::cmp::Ordering::Greater {
8829                                    left.pop_front()
8830                                } else {
8831                                    right.pop_front()
8832                                }
8833                            }
8834                            (Some(_), None) if *right_closed => left.pop_front(),
8835                            (None, Some(_)) if *left_closed => right.pop_front(),
8836                            _ => break,
8837                        };
8838                        if let Some(val) = next {
8839                            pending.push_back(val);
8840                        } else {
8841                            break;
8842                        }
8843                    }
8844
8845                    if pending.is_empty() {
8846                        let all_done =
8847                            *left_closed && *right_closed && left.is_empty() && right.is_empty();
8848                        if all_done {
8849                            *completed = true;
8850                            StageTransition::emit(StageEmissions::None)
8851                                .with_completion(vec![single_outlet(stage)?])
8852                        } else {
8853                            StageTransition::none()
8854                        }
8855                    } else {
8856                        let outlet = single_outlet(stage)?;
8857                        let emissions: Vec<_> = pending.drain(..).map(|v| (outlet, v)).collect();
8858                        let all_done =
8859                            *left_closed && *right_closed && left.is_empty() && right.is_empty();
8860                        if all_done {
8861                            *completed = true;
8862                            StageTransition::emit(StageEmissions::Many(emissions))
8863                                .with_completion(vec![outlet])
8864                        } else {
8865                            StageTransition::emit(StageEmissions::Many(emissions))
8866                        }
8867                    }
8868                };
8869                Ok(result)
8870            }
8871            StageKind::MergeSequence { input_count, .. } => {
8872                let result = {
8873                    let StageState::MergeSequence {
8874                        next_sequence,
8875                        pending,
8876                        completed_count,
8877                        output_buffer,
8878                        completed,
8879                    } = &mut self.stage_states[stage_index]
8880                    else {
8881                        return Err(StreamError::GraphValidation(
8882                            "merge-sequence state is missing".into(),
8883                        ));
8884                    };
8885                    if *completed {
8886                        return Ok(StageTransition::none());
8887                    }
8888                    *completed_count += 1;
8889                    if *completed_count >= *input_count && output_buffer.is_empty() {
8890                        if !pending.is_empty() {
8891                            // All inputs have completed but there are buffered elements
8892                            // whose sequence numbers do not include `next_sequence`.
8893                            // This is a gap — fail exactly as the GraphStage logic does.
8894                            return Err(StreamError::Failed(format!(
8895                                "expected sequence {next_sequence}, but all input ports have pushed or are complete",
8896                            )));
8897                        }
8898                        *completed = true;
8899                        StageTransition::emit(StageEmissions::None)
8900                            .with_completion(vec![single_outlet(stage)?])
8901                    } else {
8902                        StageTransition::none()
8903                    }
8904                };
8905                Ok(result)
8906            }
8907            StageKind::MergeLatest {
8908                input_count,
8909                eager_complete,
8910                ..
8911            } => {
8912                let result = {
8913                    let StageState::MergeLatest {
8914                        completed_count,
8915                        pending,
8916                        completed,
8917                        ..
8918                    } = &mut self.stage_states[stage_index]
8919                    else {
8920                        return Err(StreamError::GraphValidation(
8921                            "merge-latest state is missing".into(),
8922                        ));
8923                    };
8924                    if *completed {
8925                        return Ok(StageTransition::none());
8926                    }
8927                    *completed_count += 1;
8928                    // Complete when all inputs are done, OR when eager_complete is set
8929                    // and there is no pending output (matches Akka ZipLatestWith semantics:
8930                    // complete as soon as any upstream completes if eager is true and the
8931                    // pending queue is drained).
8932                    let all_done = *completed_count >= *input_count;
8933                    let eager_done = *eager_complete && pending.is_empty();
8934                    if all_done || eager_done {
8935                        *completed = true;
8936                        StageTransition::emit(StageEmissions::None)
8937                            .with_completion(vec![single_outlet(stage)?])
8938                    } else {
8939                        StageTransition::none()
8940                    }
8941                };
8942                Ok(result)
8943            }
8944            StageKind::Partition { .. } => {
8945                let result = {
8946                    let StageState::Partition {
8947                        pending,
8948                        upstream_closed,
8949                        completed,
8950                        ..
8951                    } = &mut self.stage_states[stage_index]
8952                    else {
8953                        return Err(StreamError::GraphValidation(
8954                            "partition state is missing".into(),
8955                        ));
8956                    };
8957                    if *completed {
8958                        return Ok(StageTransition::none());
8959                    }
8960                    *upstream_closed = true;
8961                    if pending.is_none() {
8962                        *completed = true;
8963                        StageTransition::emit(StageEmissions::None)
8964                            .with_completion(stage.spec.outlets.iter().map(AnyOutlet::id).collect())
8965                    } else {
8966                        StageTransition::none()
8967                    }
8968                };
8969                Ok(result)
8970            }
8971        }
8972    }
8973
8974    fn process_pull(
8975        &mut self,
8976        stage_index: usize,
8977        outlet: PortId,
8978    ) -> StreamResult<StageTransition> {
8979        let stage = &self.graph.stages[stage_index];
8980        match &stage.spec.kind {
8981            StageKind::Opaque => {
8982                if let Some(logic) = self
8983                    .opaque_logics
8984                    .get_mut(stage_index)
8985                    .and_then(|l| l.as_mut())
8986                {
8987                    logic.drain_async_callbacks();
8988                    logic.set_demand_by_id(outlet)?;
8989                    let outlet_ref = stage
8990                        .spec
8991                        .outlets
8992                        .iter()
8993                        .find(|o| o.id() == outlet)
8994                        .cloned();
8995                    if let Some(outlet_ref) = outlet_ref {
8996                        let mut handler = logic.take_out_handler(outlet);
8997                        let result = if let Some(ref mut handler) = handler {
8998                            handler.on_pull(logic, outlet_ref)
8999                        } else {
9000                            Ok(())
9001                        };
9002                        if let Some(handler) = handler
9003                            && handler.keep_handler()
9004                            && logic.get_out_handler_mut(outlet).is_none()
9005                        {
9006                            logic.restore_out_handler(outlet, handler);
9007                        }
9008                        if result.is_err() {
9009                            logic.cancel_all_timers();
9010                        }
9011                        result?;
9012                    }
9013                    self.collect_opaque_emissions(stage, stage_index)
9014                } else {
9015                    Ok(StageTransition::none())
9016                }
9017            }
9018            StageKind::Unzip { .. } => {
9019                let StageState::Unzip {
9020                    demand, cancelled, ..
9021                } = &mut self.stage_states[stage_index]
9022                else {
9023                    return Ok(StageTransition::none());
9024                };
9025                let Some(idx) = stage.spec.outlets.iter().position(|o| o.id() == outlet) else {
9026                    return Ok(StageTransition::none());
9027                };
9028                if idx < 2 && !cancelled[idx] {
9029                    demand[idx] = true;
9030                }
9031                Ok(StageTransition::none())
9032            }
9033            StageKind::Partition { .. } => {
9034                let result = {
9035                    let StageState::Partition {
9036                        pending,
9037                        upstream_closed,
9038                        demand,
9039                        cancelled,
9040                        completed,
9041                        ..
9042                    } = &mut self.stage_states[stage_index]
9043                    else {
9044                        return Ok(StageTransition::none());
9045                    };
9046                    if *completed {
9047                        return Ok(StageTransition::none());
9048                    }
9049                    let Some(idx) = stage.spec.outlets.iter().position(|o| o.id() == outlet) else {
9050                        return Ok(StageTransition::none());
9051                    };
9052                    if cancelled[idx] {
9053                        return Ok(StageTransition::none());
9054                    }
9055
9056                    if let Some((p_idx, p_val)) = pending.take() {
9057                        if p_idx == idx {
9058                            let out = stage.spec.outlets[idx].id();
9059                            if *upstream_closed {
9060                                *completed = true;
9061                                StageTransition::emit(StageEmissions::One(out, p_val))
9062                                    .with_completion(
9063                                        stage.spec.outlets.iter().map(AnyOutlet::id).collect(),
9064                                    )
9065                            } else {
9066                                StageTransition::emit(StageEmissions::One(out, p_val))
9067                            }
9068                        } else {
9069                            *pending = Some((p_idx, p_val));
9070                            demand[idx] = true;
9071                            StageTransition::none()
9072                        }
9073                    } else {
9074                        demand[idx] = true;
9075                        StageTransition::none()
9076                    }
9077                };
9078                Ok(result)
9079            }
9080            _ => Ok(StageTransition::none()),
9081        }
9082    }
9083
9084    fn process_downstream_finish(
9085        &mut self,
9086        stage_index: usize,
9087        outlet: PortId,
9088    ) -> StreamResult<StageTransition> {
9089        let stage = &self.graph.stages[stage_index];
9090        match &stage.spec.kind {
9091            StageKind::Broadcast => {
9092                let StageState::Broadcast {
9093                    cancelled_outlets,
9094                    live_outlets,
9095                    ..
9096                } = &mut self.stage_states[stage_index]
9097                else {
9098                    return Err(StreamError::GraphValidation(
9099                        "broadcast state is missing".into(),
9100                    ));
9101                };
9102                let index = stage
9103                    .spec
9104                    .outlets
9105                    .iter()
9106                    .position(|candidate| candidate.id() == outlet)
9107                    .ok_or_else(|| {
9108                        StreamError::GraphValidation(format!(
9109                            "broadcast outlet {} is not part of the stage",
9110                            outlet.as_usize()
9111                        ))
9112                    })?;
9113                if cancelled_outlets[index] {
9114                    return Ok(StageTransition::none());
9115                }
9116                cancelled_outlets[index] = true;
9117                *live_outlets -= 1;
9118                if *live_outlets == 0 {
9119                    Ok(StageTransition::none()
9120                        .with_cancellations(stage.spec.inlets.iter().map(AnyInlet::id).collect()))
9121                } else {
9122                    Ok(StageTransition::none())
9123                }
9124            }
9125            StageKind::Balance => {
9126                let StageState::Balance {
9127                    cancelled_outlets,
9128                    live_outlets,
9129                    ..
9130                } = &mut self.stage_states[stage_index]
9131                else {
9132                    return Err(StreamError::GraphValidation(
9133                        "balance state is missing".into(),
9134                    ));
9135                };
9136                let index = stage
9137                    .spec
9138                    .outlets
9139                    .iter()
9140                    .position(|candidate| candidate.id() == outlet)
9141                    .ok_or_else(|| {
9142                        StreamError::GraphValidation(format!(
9143                            "balance outlet {} is not part of the stage",
9144                            outlet.as_usize()
9145                        ))
9146                    })?;
9147                if cancelled_outlets[index] {
9148                    return Ok(StageTransition::none());
9149                }
9150                cancelled_outlets[index] = true;
9151                *live_outlets -= 1;
9152                if *live_outlets == 0 {
9153                    Ok(StageTransition::none()
9154                        .with_cancellations(stage.spec.inlets.iter().map(AnyInlet::id).collect()))
9155                } else {
9156                    Ok(StageTransition::none())
9157                }
9158            }
9159            StageKind::Unzip { .. } => {
9160                let StageState::Unzip { cancelled, .. } = &mut self.stage_states[stage_index]
9161                else {
9162                    return Err(StreamError::GraphValidation(
9163                        "unzip state is missing".into(),
9164                    ));
9165                };
9166                let idx = stage
9167                    .spec
9168                    .outlets
9169                    .iter()
9170                    .position(|o| o.id() == outlet)
9171                    .unwrap_or(0);
9172                if idx < 2 && !cancelled[idx] {
9173                    cancelled[idx] = true;
9174                    let all_cancelled = cancelled.iter().all(|c| *c);
9175                    if all_cancelled {
9176                        Ok(StageTransition::none().with_cancellations(
9177                            stage.spec.inlets.iter().map(AnyInlet::id).collect(),
9178                        ))
9179                    } else {
9180                        Ok(StageTransition::none())
9181                    }
9182                } else {
9183                    Ok(StageTransition::none())
9184                }
9185            }
9186            StageKind::MergeSorted(_)
9187            | StageKind::MergeSequence { .. }
9188            | StageKind::MergeLatest { .. } => Ok(StageTransition::none()
9189                .with_cancellations(stage.spec.inlets.iter().map(AnyInlet::id).collect())),
9190            StageKind::Partition { eager_cancel, .. } => {
9191                let result = {
9192                    let StageState::Partition {
9193                        pending,
9194                        cancelled,
9195                        completed,
9196                        ..
9197                    } = &mut self.stage_states[stage_index]
9198                    else {
9199                        return Err(StreamError::GraphValidation(
9200                            "partition state is missing".into(),
9201                        ));
9202                    };
9203                    if *completed {
9204                        return Ok(StageTransition::none());
9205                    }
9206                    let Some(idx) = stage.spec.outlets.iter().position(|o| o.id() == outlet) else {
9207                        return Ok(StageTransition::none());
9208                    };
9209                    if cancelled[idx] {
9210                        return Ok(StageTransition::none());
9211                    }
9212                    cancelled[idx] = true;
9213                    // If the pending element was routed to this outlet, discard it
9214                    if let Some((p_idx, _)) = pending
9215                        && *p_idx == idx
9216                    {
9217                        *pending = None;
9218                    }
9219                    let all_cancelled = cancelled.iter().all(|c| *c);
9220                    if all_cancelled || *eager_cancel {
9221                        *completed = true;
9222                        StageTransition::none().with_cancellations(
9223                            stage.spec.inlets.iter().map(AnyInlet::id).collect(),
9224                        )
9225                    } else {
9226                        StageTransition::none()
9227                    }
9228                };
9229                Ok(result)
9230            }
9231            StageKind::Opaque => {
9232                let no_cancelled_outlets = self.cancelled_outlets.is_empty();
9233                if let Some(logic) = self
9234                    .opaque_logics
9235                    .get_mut(stage_index)
9236                    .and_then(|l| l.as_mut())
9237                {
9238                    logic.drain_async_callbacks();
9239                    logic.downstream_finish_by_id(outlet, "downstream_finish")?;
9240                    let outlet_ref = stage
9241                        .spec
9242                        .outlets
9243                        .iter()
9244                        .find(|o| o.id() == outlet)
9245                        .cloned();
9246                    if let Some(outlet_ref) = outlet_ref {
9247                        let mut handler = logic.take_out_handler(outlet);
9248                        let result = if let Some(ref mut handler) = handler {
9249                            handler.on_downstream_finish(logic, outlet_ref)
9250                        } else {
9251                            Ok(())
9252                        };
9253                        if let Some(handler) = handler
9254                            && handler.keep_handler()
9255                            && logic.get_out_handler_mut(outlet).is_none()
9256                        {
9257                            logic.restore_out_handler(outlet, handler);
9258                        }
9259                        if result.is_err() {
9260                            logic.cancel_all_timers();
9261                        }
9262                        result?;
9263                    }
9264                    let all_outlets_closed = stage.spec.outlets.iter().all(|candidate| {
9265                        logic.is_closed_by_id(candidate.id())
9266                            || (!no_cancelled_outlets
9267                                && self.cancelled_outlets.contains(&candidate.id()))
9268                    });
9269                    let mut transition = self.collect_opaque_emissions(stage, stage_index)?;
9270                    if all_outlets_closed {
9271                        transition.cancelled_inlets =
9272                            stage.spec.inlets.iter().map(AnyInlet::id).collect();
9273                    }
9274                    Ok(transition)
9275                } else {
9276                    Ok(StageTransition::none()
9277                        .with_cancellations(stage.spec.inlets.iter().map(AnyInlet::id).collect()))
9278                }
9279            }
9280            _ => Ok(StageTransition::none()
9281                .with_cancellations(stage.spec.inlets.iter().map(AnyInlet::id).collect())),
9282        }
9283    }
9284
9285    fn collect_opaque_emissions(
9286        &mut self,
9287        stage: &StageRecord,
9288        stage_index: usize,
9289    ) -> StreamResult<StageTransition> {
9290        if let Some(logic) = self
9291            .opaque_logics
9292            .get_mut(stage_index)
9293            .and_then(|l| l.as_mut())
9294        {
9295            let emissions_slots = std::mem::take(&mut logic.pending_emissions);
9296            let completions = std::mem::take(&mut logic.pending_completions);
9297            let has_stage_failed = logic.stage_error().is_some();
9298
9299            let emissions = if emissions_slots.is_empty() {
9300                StageEmissions::None
9301            } else if emissions_slots.len() == 1 {
9302                let (port, val) = emissions_slots.into_iter().next().unwrap();
9303                StageEmissions::One(port, val)
9304            } else {
9305                StageEmissions::Many(emissions_slots)
9306            };
9307
9308            if has_stage_failed {
9309                let _ = has_stage_failed;
9310            }
9311
9312            Ok(StageTransition {
9313                emissions,
9314                completed_outlets: completions,
9315                cancelled_inlets: Vec::new(),
9316            })
9317        } else {
9318            Ok(StageTransition::emit(StageEmissions::None)
9319                .with_completion(vec![single_outlet(stage)?]))
9320        }
9321    }
9322
9323    fn bump_event(&mut self) -> StreamResult<()> {
9324        bump_fused_event(&mut self.events, self.config)
9325    }
9326
9327    fn prime_connected_demands(&mut self) {
9328        for (stage_index, stage) in self.graph.stages.iter().enumerate() {
9329            match &stage.spec.kind {
9330                StageKind::Opaque => {
9331                    let Some(logic) = self
9332                        .opaque_logics
9333                        .get_mut(stage_index)
9334                        .and_then(|logic| logic.as_mut())
9335                    else {
9336                        continue;
9337                    };
9338                    for outlet in &stage.spec.outlets {
9339                        if self.edge_by_outlet.contains_key(&outlet.id()) {
9340                            let _ = logic.set_demand_by_id(outlet.id());
9341                        }
9342                    }
9343                }
9344                StageKind::Unzip { .. } => {
9345                    let StageState::Unzip { demand, .. } = &mut self.stage_states[stage_index]
9346                    else {
9347                        continue;
9348                    };
9349                    for (idx, outlet) in stage.spec.outlets.iter().enumerate() {
9350                        if self.edge_by_outlet.contains_key(&outlet.id()) {
9351                            demand[idx] = true;
9352                        }
9353                    }
9354                }
9355                StageKind::Partition { .. } => {
9356                    let StageState::Partition {
9357                        demand,
9358                        output_count,
9359                        ..
9360                    } = &mut self.stage_states[stage_index]
9361                    else {
9362                        continue;
9363                    };
9364                    for (idx, demand_slot) in demand.iter_mut().enumerate().take(*output_count) {
9365                        if idx < stage.spec.outlets.len()
9366                            && self
9367                                .edge_by_outlet
9368                                .contains_key(&stage.spec.outlets[idx].id())
9369                        {
9370                            *demand_slot = true;
9371                        }
9372                    }
9373                }
9374                _ => {}
9375            }
9376        }
9377    }
9378}
9379
9380impl<Left, Right> GraphBlueprint<ZipShape<Left, Right>>
9381where
9382    Left: Clone + Send + 'static,
9383    Right: Clone + Send + 'static,
9384{
9385    pub fn run_zip(&self, left: Vec<Left>, right: Vec<Right>) -> StreamResult<Vec<(Left, Right)>> {
9386        Ok(self
9387            .run_zip_report(left, right, FusedExecutionConfig::default())?
9388            .output)
9389    }
9390
9391    pub fn run_zip_report(
9392        &self,
9393        left: Vec<Left>,
9394        right: Vec<Right>,
9395        config: FusedExecutionConfig,
9396    ) -> StreamResult<FusedExecutionReport<(Left, Right)>> {
9397        let mut left = left.into_iter();
9398        let mut right = right.into_iter();
9399        let left_inlet = self.shape.in0().id();
9400        let right_inlet = self.shape.in1().id();
9401        let outlet = self.shape.outlet().id();
9402        let mut executor = FusedExecutor::new(self, config);
9403        let mut output = Vec::with_capacity(left.len().min(right.len()));
9404        let mut left_completed = false;
9405        let mut right_completed = false;
9406
9407        {
9408            let mut output_sink = VecOutputSink {
9409                output: &mut output,
9410            };
9411            if left.len() == 0 {
9412                executor.complete(left_inlet, outlet, &mut output_sink)?;
9413                left_completed = true;
9414            }
9415            if right.len() == 0 {
9416                executor.complete(right_inlet, outlet, &mut output_sink)?;
9417                right_completed = true;
9418            }
9419
9420            while left.len() > 0 || right.len() > 0 {
9421                if let Some(item) = left.next() {
9422                    executor.deliver(left_inlet, datum(item), outlet, &mut output_sink)?;
9423                    if left.len() == 0 && !left_completed {
9424                        executor.complete(left_inlet, outlet, &mut output_sink)?;
9425                        left_completed = true;
9426                    }
9427                }
9428                if let Some(item) = right.next() {
9429                    executor.deliver(right_inlet, datum(item), outlet, &mut output_sink)?;
9430                    if right.len() == 0 && !right_completed {
9431                        executor.complete(right_inlet, outlet, &mut output_sink)?;
9432                        right_completed = true;
9433                    }
9434                }
9435            }
9436        }
9437
9438        Ok(FusedExecutionReport {
9439            output,
9440            events: executor.events,
9441            async_boundary_crossings: executor.async_boundary_crossings,
9442        })
9443    }
9444}
9445
9446fn unzip_fan_in_fast_path<S: Shape>(
9447    stage: &StageRecord,
9448    graph: &GraphBlueprint<S>,
9449    edge_by_outlet: &HashMap<PortId, PortId>,
9450    stage_by_inlet: &HashMap<PortId, usize>,
9451) -> Option<UnzipFanInFastPath> {
9452    let outlets = &stage.spec.outlets;
9453    if outlets.len() != 2 {
9454        return None;
9455    }
9456    let inlet0 = edge_by_outlet.get(&outlets[0].id()).copied()?;
9457    let inlet1 = edge_by_outlet.get(&outlets[1].id()).copied()?;
9458    let stage0 = *stage_by_inlet.get(&inlet0)?;
9459    let stage1 = *stage_by_inlet.get(&inlet1)?;
9460    if stage0 != stage1 {
9461        return None;
9462    }
9463    let target = graph.stages.get(stage0)?;
9464    if !matches!(
9465        target.spec.kind,
9466        StageKind::MergeSorted(_) | StageKind::MergeSequence { .. } | StageKind::MergeLatest { .. }
9467    ) {
9468        return None;
9469    }
9470    // Resolve the exact inlet indices so the fast path routes each Unzip output
9471    // to the correct slot in the fan-in stage, regardless of wiring order.
9472    let idx0 = target.spec.inlets.iter().position(|i| i.id() == inlet0)?;
9473    let idx1 = target.spec.inlets.iter().position(|i| i.id() == inlet1)?;
9474    Some(UnzipFanInFastPath {
9475        fan_in_stage_index: stage0,
9476        target_inlet_indices: [idx0, idx1],
9477    })
9478}
9479
9480fn unzip_zip_fast_path<S: Shape>(
9481    stage: &StageRecord,
9482    graph: &GraphBlueprint<S>,
9483    edge_by_outlet: &HashMap<PortId, PortId>,
9484    stage_by_inlet: &HashMap<PortId, usize>,
9485) -> Option<UnzipZipFastPath> {
9486    let outlets = &stage.spec.outlets;
9487    if outlets.len() != 2 {
9488        return None;
9489    }
9490    let inlet0 = edge_by_outlet.get(&outlets[0].id()).copied()?;
9491    let inlet1 = edge_by_outlet.get(&outlets[1].id()).copied()?;
9492    let stage0 = *stage_by_inlet.get(&inlet0)?;
9493    let stage1 = *stage_by_inlet.get(&inlet1)?;
9494    if stage0 != stage1 {
9495        return None;
9496    }
9497    let target = graph.stages.get(stage0)?;
9498    if !matches!(target.spec.kind, StageKind::Zip(_)) {
9499        return None;
9500    }
9501    Some(UnzipZipFastPath {
9502        zip_stage_index: stage0,
9503    })
9504}
9505
9506/// Increments the event counter and returns an error if the configured limit
9507/// is exceeded.
9508///
9509/// `pub(crate)` so the typed-port executor (Phase 1+) can reuse the same
9510/// event-budget enforcement without duplicating the check.
9511pub(crate) fn bump_fused_event(
9512    events: &mut usize,
9513    config: FusedExecutionConfig,
9514) -> StreamResult<()> {
9515    *events += 1;
9516    if *events > config.event_limit {
9517        return Err(StreamError::EventLimitExceeded {
9518            limit: config.event_limit,
9519        });
9520    }
9521    Ok(())
9522}
9523
9524fn broadcast_emissions(outlets: &[AnyOutlet], value: DatumValue) -> StreamResult<StageEmissions> {
9525    match outlets {
9526        [] => Err(StreamError::GraphValidation(
9527            "broadcast has no outlets".into(),
9528        )),
9529        [outlet] => Ok(StageEmissions::One(outlet.id(), value)),
9530        [first, second] => Ok(StageEmissions::Two(
9531            (first.id(), value.clone_box()),
9532            (second.id(), value),
9533        )),
9534        outlets => {
9535            let mut emitted = Vec::with_capacity(outlets.len());
9536            for outlet in &outlets[..outlets.len() - 1] {
9537                emitted.push((outlet.id(), value.clone_box()));
9538            }
9539            emitted.push((outlets[outlets.len() - 1].id(), value));
9540            Ok(StageEmissions::Many(emitted))
9541        }
9542    }
9543}
9544
9545fn single_outlet(stage: &StageRecord) -> StreamResult<PortId> {
9546    stage
9547        .spec
9548        .outlets
9549        .first()
9550        .map(AnyOutlet::id)
9551        .ok_or_else(|| {
9552            StreamError::GraphValidation(format!("stage {} has no outlet", stage.spec.name()))
9553        })
9554}
9555
9556fn merge_emissions(first: StageEmissions, second: StageEmissions) -> StageEmissions {
9557    match (first, second) {
9558        (StageEmissions::None, other) | (other, StageEmissions::None) => other,
9559        (StageEmissions::One(p1, v1), StageEmissions::One(p2, v2)) => {
9560            StageEmissions::Many(vec![(p1, v1), (p2, v2)])
9561        }
9562        (StageEmissions::One(p, v), StageEmissions::Many(mut vec))
9563        | (StageEmissions::Many(mut vec), StageEmissions::One(p, v)) => {
9564            vec.push((p, v));
9565            StageEmissions::Many(vec)
9566        }
9567        (StageEmissions::Many(mut v1), StageEmissions::Many(v2)) => {
9568            v1.extend(v2);
9569            StageEmissions::Many(v1)
9570        }
9571        (a, b) => {
9572            let mut all = Vec::new();
9573            push_emissions(&mut all, a);
9574            push_emissions(&mut all, b);
9575            StageEmissions::Many(all)
9576        }
9577    }
9578}
9579
9580fn push_emissions(out: &mut Vec<(PortId, DatumValue)>, emissions: StageEmissions) {
9581    match emissions {
9582        StageEmissions::None => {}
9583        StageEmissions::One(p, v) => out.push((p, v)),
9584        StageEmissions::Two((p1, v1), (p2, v2)) => {
9585            out.push((p1, v1));
9586            out.push((p2, v2));
9587        }
9588        StageEmissions::Many(vec) => out.extend(vec),
9589    }
9590}