Skip to main content

datum/graph/
executor.rs

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