Skip to main content

datum/graph/
executor.rs

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