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