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