Skip to main content

datum/graph/
executor.rs

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