1use std::{
36 collections::HashSet,
37 sync::{Arc, OnceLock},
38 time::{Duration, Instant},
39};
40
41use futures_util::stream::{self, StreamExt};
42use indexmap::{IndexMap, IndexSet};
43
44use super::{
45 BspContext,
46 condition::{Condition, ConditionEval},
47 error::BspError,
48 events::BspInterceptor,
49 graph::{BspGraph, ERROR_PORT, EdgeId, InputPin, NodeId, OutputPin, PortIndex},
50 merge::Merger,
51 node::{Activation, BspNode, NodeOutcome},
52 recovery::{Recovery, RecoveryPolicy},
53 run::{EngineResult, PendingSuspension, RunContext, RunState, SuspendedExecution, ValueStore},
54 value::{BspSkip, BspValue},
55};
56
57pub const DEFAULT_MAX_STEPS: u64 = 1024;
61
62pub const DEFAULT_MAX_PENDING_TOKENS: u64 = 1024;
64
65pub type BranchTransform<S> = Arc<dyn Fn(&mut S, usize) + Send + Sync>;
70
71#[derive(Debug)]
73enum TaskResult {
74 Success {
76 outputs: IndexMap<PortIndex, BspValue>,
77 skipped: bool,
78 },
79 Suspended {
81 payload: serde_json::Value,
82 waiting_inputs: Vec<PortIndex>,
83 },
84 RoutedError(BspError),
86}
87
88enum NextStep {
90 Frontier(Vec<NodeId>),
92 Complete,
94 StepLimit,
96}
97
98#[derive(Debug, Clone)]
103pub struct ValidationReport {
104 pub error: Option<BspError>,
106 pub warnings: Vec<String>,
108}
109
110impl ValidationReport {
111 #[must_use]
113 pub const fn is_ok(&self) -> bool {
114 self.error.is_none()
115 }
116}
117
118pub struct BspEngine<S = ()> {
123 graph: BspGraph<S>,
124 ctx: BspContext,
125 interceptor: Option<Arc<dyn BspInterceptor>>,
126 max_concurrency: Option<usize>,
127 max_steps: u64,
128 max_pending_tokens: u64,
129 branch_transform: Option<BranchTransform<S>>,
130 validation_cache: OnceLock<ValidationReport>,
131}
132
133impl<S> BspEngine<S> {
134 #[must_use]
136 pub fn new(graph: BspGraph<S>) -> Self {
137 Self {
138 graph,
139 ctx: BspContext::new(),
140 interceptor: None,
141 max_concurrency: None,
142 max_steps: DEFAULT_MAX_STEPS,
143 max_pending_tokens: DEFAULT_MAX_PENDING_TOKENS,
144 branch_transform: None,
145 validation_cache: OnceLock::new(),
146 }
147 }
148
149 #[must_use]
151 pub fn with_context(mut self, ctx: BspContext) -> Self {
152 self.ctx = ctx;
153 self
154 }
155
156 #[must_use]
158 pub const fn context(&self) -> &BspContext {
159 &self.ctx
160 }
161
162 #[must_use]
164 pub const fn graph(&self) -> &BspGraph<S> {
165 &self.graph
166 }
167
168 #[must_use]
170 pub fn with_interceptor(mut self, interceptor: Arc<dyn BspInterceptor>) -> Self {
171 self.interceptor = Some(interceptor);
172 self
173 }
174
175 #[must_use]
182 pub const fn with_max_concurrency(mut self, limit: usize) -> Self {
183 self.max_concurrency = Some(limit);
184 self
185 }
186
187 #[must_use]
190 pub const fn with_max_steps(mut self, steps: u64) -> Self {
191 self.max_steps = steps;
192 self
193 }
194
195 #[must_use]
197 pub const fn with_max_pending_tokens(mut self, cap: u64) -> Self {
198 self.max_pending_tokens = cap;
199 self
200 }
201
202 #[must_use]
215 pub fn with_branch_transform(mut self, transform: BranchTransform<S>) -> Self {
216 self.branch_transform = Some(transform);
217 self
218 }
219
220 pub fn validate(&self) -> Result<(), BspError> {
223 let report = self.validation_report();
224 match &report.error {
225 Some(err) => Err(err.clone()),
226 None => Ok(()),
227 }
228 }
229
230 pub fn validate_with_report(&self) -> ValidationReport {
234 self.validation_report().clone()
235 }
236
237 fn validation_report(&self) -> &ValidationReport {
238 self.validation_cache
239 .get_or_init(|| compute_validation(&self.graph))
240 }
241
242 fn compute_frontier(&self, state: &RunState<S>, forced: IndexSet<NodeId>) -> IndexSet<NodeId> {
249 let mut frontier = forced;
250 for (&edge_id, &count) in &state.pending_tokens {
251 if count == 0 {
252 continue;
253 }
254 let target = self.graph.edges[edge_id].target;
255 if frontier.contains(&target) {
256 continue;
257 }
258 let node = &self.graph.nodes[&target];
259 if self.is_activatable(state, target, node.activation()) {
260 frontier.insert(target);
261 }
262 }
263 frontier
264 }
265
266 fn next_step(
269 &self,
270 state: &mut RunState<S>,
271 forced: &mut IndexSet<NodeId>,
272 ) -> Result<NextStep, BspError> {
273 let frontier = self.compute_frontier(state, std::mem::take(forced));
274
275 if frontier.is_empty() {
276 if state.pending_tokens.values().any(|&c| c > 0) {
277 tracing::error!("❌ [BspEngine] Deadlock detected during scheduling.");
278 return Err(BspError::Deadlock);
279 }
280 return Ok(NextStep::Complete);
281 }
282
283 if state.step >= self.max_steps {
284 tracing::error!(
285 "🛑 [BspEngine] Superstep budget of {} exhausted; returning live state.",
286 self.max_steps
287 );
288 return Ok(NextStep::StepLimit);
289 }
290
291 state.frontier = frontier.iter().copied().collect();
292 let mut frontier_nodes: Vec<NodeId> = frontier.into_iter().collect();
295 frontier_nodes.sort_unstable();
296
297 if let Some(interceptor) = &self.interceptor {
298 interceptor.on_superstep_start(state.step, &frontier_nodes);
299 }
300 tracing::debug!(
301 "🌀 [BspEngine] Superstep {}: Launching {} concurrent tasks...",
302 state.step,
303 frontier_nodes.len()
304 );
305 Ok(NextStep::Frontier(frontier_nodes))
306 }
307
308 fn is_activatable(&self, state: &RunState<S>, node_id: NodeId, activation: Activation) -> bool {
310 let Some(incoming) = self.graph.incoming.get(&node_id) else {
311 return false;
312 };
313 match activation {
314 Activation::Any => incoming
315 .iter()
316 .any(|e| state.pending_tokens.get(e).is_some_and(|&c| c > 0)),
317 Activation::All => {
318 !incoming.is_empty()
319 && incoming
320 .iter()
321 .all(|e| state.pending_tokens.get(e).is_some_and(|&c| c > 0))
322 }
323 }
324 }
325
326 fn consume_tokens(&self, state: &mut RunState<S>, node_id: NodeId, activation: Activation) {
330 let Some(incoming) = self.graph.incoming.get(&node_id) else {
331 return;
332 };
333 match activation {
334 Activation::Any => {
335 let selected = incoming
336 .iter()
337 .find(|e| state.pending_tokens.get(*e).is_some_and(|&c| c > 0))
338 .copied();
339 if let Some(e) = selected
340 && let Some(count) = state.pending_tokens.get_mut(&e)
341 {
342 *count = count.saturating_sub(1);
343 if *count == 0 {
344 state.pending_tokens.swap_remove(&e);
345 }
346 }
347 }
348 Activation::All => {
349 let mut emptied = Vec::new();
350 for &e in incoming {
351 if let Some(count) = state.pending_tokens.get_mut(&e) {
352 *count = count.saturating_sub(1);
353 if *count == 0 {
354 emptied.push(e);
355 }
356 }
357 }
358 for e in emptied {
359 state.pending_tokens.swap_remove(&e);
360 }
361 }
362 }
363 }
364
365 fn prepare_node(
368 &self,
369 state: &mut RunState<S>,
370 node_id: NodeId,
371 ) -> Result<(u64, IndexMap<PortIndex, BspValue>, bool), BspError> {
372 let node = &self.graph.nodes[&node_id];
373 let exec_base = state.execution_count(node_id);
374 if let Some(max) = node.max_executions()
375 && exec_base >= max
376 {
377 return Err(BspError::MaxExecutionsExceeded { node_id, max });
378 }
379 self.consume_tokens(state, node_id, node.activation());
380 let (inputs, should_skip) = self.gather_node_inputs(node_id, node.as_ref(), state);
381 Ok((exec_base, inputs, should_skip))
382 }
383
384 fn gather_node_inputs(
387 &self,
388 node_id: NodeId,
389 node: &dyn BspNode<S>,
390 state: &RunState<S>,
391 ) -> (IndexMap<PortIndex, BspValue>, bool) {
392 let mut inputs = IndexMap::new();
393 let mut should_skip = false;
394
395 for &port in &node.input_ports() {
396 let pin = InputPin { node_id, port };
397 let mut gathered_values = Vec::new();
398
399 if let Some(static_val) = self.graph.static_inputs.get(&pin) {
401 gathered_values.push(static_val.clone());
402 }
403
404 if let Some(injected_val) = state.values.injected.get(&pin) {
406 gathered_values.push(injected_val.clone());
407 }
408
409 if let Some(edge_ids) = self.graph.incoming_by_pin.get(&pin) {
411 for &edge_id in edge_ids {
412 let edge = &self.graph.edges[edge_id];
413 let source_pin = OutputPin {
414 node_id: edge.source,
415 port: edge.source_port.unwrap_or(0),
416 };
417 if let Some(val) = state.values.outputs.get(&source_pin) {
418 gathered_values.push(val.clone());
419 }
420 }
421 }
422
423 if gathered_values.iter().any(BspValue::is::<BspSkip>) {
425 should_skip = true;
426 }
427
428 if !gathered_values.is_empty() {
429 if gathered_values.len() == 1 {
430 inputs.insert(port, gathered_values[0].clone());
431 } else {
432 inputs.insert(port, BspValue::new(gathered_values));
433 }
434 }
435 }
436
437 (inputs, should_skip)
438 }
439
440 #[allow(clippy::too_many_arguments)]
446 fn emit_edges(
447 &self,
448 source: NodeId,
449 branch: &S,
450 values: &ValueStore,
451 pending_tokens: &mut IndexMap<EdgeId, u64>,
452 iterations: &mut IndexMap<EdgeId, u64>,
453 source_skipped: bool,
454 source_errored: bool,
455 ) -> Result<(), BspError> {
456 let Some(outgoing) = self.graph.outgoing.get(&source) else {
457 return Ok(());
458 };
459 for &edge_id in outgoing {
460 let edge = &self.graph.edges[edge_id];
461 let eval = ConditionEval {
462 state: branch,
463 values,
464 conditions: &self.graph.conditions,
465 source_skipped,
466 source_errored,
467 };
468 if !edge.condition.eval(&eval) {
469 continue;
470 }
471
472 if let Some(max) = edge.max_iterations {
473 let iteration = iterations.entry(edge_id).or_insert(0);
474 *iteration += 1;
475 let iteration = *iteration;
476 if let Some(interceptor) = &self.interceptor {
477 interceptor.on_loop_iteration(edge_id, iteration, Some(max));
478 }
479 if iteration > max {
480 return Err(BspError::LoopBoundExceeded {
481 edge: edge_id,
482 from: edge.source,
483 to: edge.target,
484 max,
485 });
486 }
487 }
488
489 let pending = pending_tokens.entry(edge_id).or_insert(0);
490 if *pending >= self.max_pending_tokens {
491 return Err(BspError::TokenLimitExceeded {
492 edge: edge_id,
493 from: edge.source,
494 to: edge.target,
495 cap: self.max_pending_tokens,
496 });
497 }
498 *pending += 1;
499
500 if let Some(interceptor) = &self.interceptor {
501 interceptor.on_edge_taken(edge.source, edge.target, &edge.condition.label());
502 }
503 }
504 Ok(())
505 }
506
507 fn resolve_merger(&self, frontier_nodes: &[NodeId]) -> Option<Arc<dyn Merger<S>>> {
511 for &node_id in frontier_nodes {
512 if let Some(outgoing) = self.graph.outgoing.get(&node_id) {
513 for &edge_id in outgoing {
514 let target = self.graph.edges[edge_id].target;
515 if let Some(node) = self.graph.nodes.get(&target)
516 && let Some(name) = node.merger()
517 && let Some(merger) = self.graph.mergers.get(name)
518 {
519 return Some(merger.clone());
520 }
521 }
522 }
523 }
524 None
525 }
526
527 async fn run_task(
533 &self,
534 node_id: NodeId,
535 branch: &mut S,
536 inputs: IndexMap<PortIndex, BspValue>,
537 should_skip: bool,
538 exec_base: u64,
539 ) -> (u64, Result<TaskResult, BspError>) {
540 let node = self
541 .graph
542 .nodes
543 .get(&node_id)
544 .expect("frontier nodes are graph members");
545 let name = node.name().to_string();
546
547 if let Some(interceptor) = &self.interceptor {
548 interceptor.on_node_start(node_id, &name);
549 }
550
551 if should_skip {
552 let mut outputs = IndexMap::new();
554 for &port in &node.output_ports() {
555 outputs.insert(port, BspValue::new(BspSkip));
556 }
557 if let Some(interceptor) = &self.interceptor {
558 interceptor.on_node_complete(node_id, &name, Duration::ZERO);
559 }
560 return (
561 1,
562 Ok(TaskResult::Success {
563 outputs,
564 skipped: true,
565 }),
566 );
567 }
568
569 let recovery = node.recovery();
570 let start = Instant::now();
571 let mut calls = 0_u32;
572
573 loop {
574 calls += 1;
575 let (result, staged, skip_flag) = {
576 let mut run = RunContext::new(
577 &mut *branch,
578 node_id,
579 &self.ctx,
580 exec_base + u64::from(calls) - 1,
581 inputs.clone(),
582 );
583 let result = node.execute(&mut run).await;
584 let (staged, skip_flag) = run.take_staged();
585 (result, staged, skip_flag)
586 };
587
588 match result {
589 Ok(NodeOutcome::Success(mut map)) => {
590 for (port, value) in staged {
592 map.entry(port).or_insert(value);
593 }
594 let (outputs, skipped) = if skip_flag {
595 let mut outputs = IndexMap::new();
596 for &port in &node.output_ports() {
597 outputs.insert(port, BspValue::new(BspSkip));
598 }
599 (outputs, true)
600 } else {
601 let skipped = map.values().any(BspValue::is::<BspSkip>);
602 (map, skipped)
603 };
604 let duration = start.elapsed();
605 if let Some(interceptor) = &self.interceptor {
606 interceptor.on_node_complete(node_id, &name, duration);
607 }
608 return (
609 u64::from(calls),
610 Ok(TaskResult::Success { outputs, skipped }),
611 );
612 }
613 Ok(NodeOutcome::Suspend {
614 payload,
615 waiting_inputs,
616 }) => {
617 return (
618 u64::from(calls),
619 Ok(TaskResult::Suspended {
620 payload,
621 waiting_inputs,
622 }),
623 );
624 }
625 Err(err) => match &recovery {
626 RecoveryPolicy::Retry { policy, classify } => match classify(&err) {
627 Recovery::Retry { after } if calls <= policy.max_retries => {
628 let wait = policy
629 .backoff(calls - 1)
630 .max(after.unwrap_or(Duration::ZERO));
631 if let Some(interceptor) = &self.interceptor {
632 interceptor.on_node_retry(node_id, calls, wait, &err);
633 }
634 super::recovery::sleep(wait).await;
635 }
636 _ => {
637 if let Some(interceptor) = &self.interceptor {
638 interceptor.on_node_error(node_id, &name, &err);
639 }
640 return (u64::from(calls), Err(err));
641 }
642 },
643 RecoveryPolicy::RouteToErrorEdge => {
644 if let Some(interceptor) = &self.interceptor {
645 interceptor.on_node_error(node_id, &name, &err);
646 }
647 return (u64::from(calls), Ok(TaskResult::RoutedError(err)));
648 }
649 RecoveryPolicy::FailFast => {
650 if let Some(interceptor) = &self.interceptor {
651 interceptor.on_node_error(node_id, &name, &err);
652 }
653 return (u64::from(calls), Err(err));
654 }
655 },
656 }
657 }
658 }
659
660 fn process_result(
668 &self,
669 state: &mut RunState<S>,
670 node_id: NodeId,
671 branch: Option<(S, Option<&dyn Merger<S>>)>,
672 result: Result<TaskResult, BspError>,
673 suspensions: &mut Vec<PendingSuspension>,
674 ) -> Result<(), BspError> {
675 let RunState {
676 user,
677 values,
678 pending_tokens,
679 iterations,
680 errors,
681 ..
682 } = state;
683
684 match result {
685 Ok(TaskResult::Success { outputs, skipped }) => {
686 for (port, value) in outputs {
687 values.outputs.insert(OutputPin { node_id, port }, value);
688 }
689 let branch_state: &S = branch.as_ref().map_or(&*user, |(b, _)| b);
690 self.emit_edges(
691 node_id,
692 branch_state,
693 values,
694 pending_tokens,
695 iterations,
696 skipped,
697 false,
698 )?;
699 }
700 Ok(TaskResult::Suspended {
701 payload,
702 waiting_inputs,
703 }) => {
704 suspensions.push(PendingSuspension {
705 node_id,
706 waiting_inputs,
707 payload,
708 });
709 }
710 Ok(TaskResult::RoutedError(err)) => {
711 errors.insert(node_id, err.clone());
712 values.outputs.insert(
713 OutputPin {
714 node_id,
715 port: ERROR_PORT,
716 },
717 BspValue::new(err),
718 );
719 let branch_state: &S = branch.as_ref().map_or(&*user, |(b, _)| b);
720 self.emit_edges(
721 node_id,
722 branch_state,
723 values,
724 pending_tokens,
725 iterations,
726 false,
727 true,
728 )?;
729 }
730 Err(err) => return Err(err),
731 }
732
733 if let Some((branch_state, merger)) = branch {
734 match merger {
735 Some(merger) => merger.merge(user, branch_state),
736 None => *user = branch_state,
737 }
738 }
739 Ok(())
740 }
741
742 fn epilogue(&self, state: &mut RunState<S>, suspensions: &[PendingSuspension]) -> bool {
745 if suspensions.is_empty() {
746 state.step += 1;
747 return false;
748 }
749 tracing::warn!(
750 "⏸️ [BspEngine] {} node(s) requested execution suspension.",
751 suspensions.len()
752 );
753 if let Some(interceptor) = &self.interceptor {
754 interceptor.on_suspended(suspensions);
755 }
756 state.frontier = suspensions.iter().map(|s| s.node_id).collect();
757 true
758 }
759
760 async fn execute_loop(
766 &self,
767 mut state: RunState<S>,
768 forced: IndexSet<NodeId>,
769 ) -> Result<EngineResult<S>, BspError>
770 where
771 S: Clone,
772 {
773 if let Some(err) = &self.validation_report().error {
774 return Err(err.clone());
775 }
776
777 let mut forced = forced;
778
779 loop {
780 let frontier_nodes = match self.next_step(&mut state, &mut forced)? {
781 NextStep::Frontier(nodes) => nodes,
782 NextStep::Complete => {
783 tracing::info!("✅ [BspEngine] Graph executed to completion.");
784 if let Some(interceptor) = &self.interceptor {
785 interceptor.on_complete(state.step);
786 }
787 return Ok(EngineResult::Complete(Box::new(state)));
788 }
789 NextStep::StepLimit => return Ok(EngineResult::StepLimitReached(Box::new(state))),
790 };
791
792 let mut suspensions = Vec::new();
793
794 if frontier_nodes.len() == 1 {
795 let node_id = frontier_nodes[0];
798 let (exec_base, inputs, should_skip) = self.prepare_node(&mut state, node_id)?;
799 let (calls, result) = self
800 .run_task(node_id, &mut state.user, inputs, should_skip, exec_base)
801 .await;
802 state.executions.insert(node_id, exec_base + calls);
803 self.process_result(&mut state, node_id, None, result, &mut suspensions)?;
804 } else {
805 let merger = self.resolve_merger(&frontier_nodes);
809 if merger.is_none() {
810 tracing::warn!(
811 "🔀 [BspEngine] Folding {} branch states without a named merger: last-writer-wins.",
812 frontier_nodes.len()
813 );
814 }
815
816 let mut prepared = Vec::with_capacity(frontier_nodes.len());
817 for (ordinal, &node_id) in frontier_nodes.iter().enumerate() {
818 let (exec_base, inputs, should_skip) =
819 self.prepare_node(&mut state, node_id)?;
820 prepared.push((ordinal, node_id, exec_base, inputs, should_skip));
821 }
822
823 let limit = match self.max_concurrency {
824 Some(0) | None => prepared.len(),
825 Some(n) => n,
826 };
827 let tasks = prepared.into_iter().map(
828 |(ordinal, node_id, exec_base, inputs, should_skip)| {
829 let mut branch = state.user.clone();
830 if let Some(transform) = &self.branch_transform {
831 transform(&mut branch, ordinal);
832 }
833 async move {
834 let (calls, result) = self
835 .run_task(node_id, &mut branch, inputs, should_skip, exec_base)
836 .await;
837 (ordinal, node_id, exec_base, calls, branch, result)
838 }
839 },
840 );
841 let mut results = stream::iter(tasks)
845 .buffer_unordered(limit)
846 .collect::<Vec<_>>()
847 .await;
848 results.sort_by_key(|(ordinal, ..)| *ordinal);
849
850 for (_, node_id, exec_base, calls, branch, result) in results {
851 state.executions.insert(node_id, exec_base + calls);
852 self.process_result(
853 &mut state,
854 node_id,
855 Some((branch, merger.as_deref())),
856 result,
857 &mut suspensions,
858 )?;
859 }
860
861 if let Some(merger) = &merger {
864 merger.finish(&mut state.user).await?;
865 }
866 }
867
868 if self.epilogue(&mut state, &suspensions) {
869 return Ok(EngineResult::Suspended(Box::new(SuspendedExecution {
870 suspensions,
871 state,
872 })));
873 }
874 }
875 }
876
877 async fn execute_loop_sequential(
882 &self,
883 mut state: RunState<S>,
884 forced: IndexSet<NodeId>,
885 ) -> Result<EngineResult<S>, BspError> {
886 if let Some(err) = &self.validation_report().error {
887 return Err(err.clone());
888 }
889
890 let mut forced = forced;
891
892 loop {
893 let frontier_nodes = match self.next_step(&mut state, &mut forced)? {
894 NextStep::Frontier(nodes) => nodes,
895 NextStep::Complete => {
896 tracing::info!("✅ [BspEngine] Graph executed to completion.");
897 if let Some(interceptor) = &self.interceptor {
898 interceptor.on_complete(state.step);
899 }
900 return Ok(EngineResult::Complete(Box::new(state)));
901 }
902 NextStep::StepLimit => return Ok(EngineResult::StepLimitReached(Box::new(state))),
903 };
904
905 let mut suspensions = Vec::new();
906 for node_id in frontier_nodes {
907 let (exec_base, inputs, should_skip) = self.prepare_node(&mut state, node_id)?;
908 let (calls, result) = self
909 .run_task(node_id, &mut state.user, inputs, should_skip, exec_base)
910 .await;
911 state.executions.insert(node_id, exec_base + calls);
912 self.process_result(&mut state, node_id, None, result, &mut suspensions)?;
913 }
914
915 if self.epilogue(&mut state, &suspensions) {
916 return Ok(EngineResult::Suspended(Box::new(SuspendedExecution {
917 suspensions,
918 state,
919 })));
920 }
921 }
922 }
923}
924
925impl<S: Clone> BspEngine<S> {
926 pub async fn run(&self, state: S) -> Result<EngineResult<S>, BspError> {
933 tracing::info!("🏁 [BspEngine] Initiating graph execution pass...");
934 let forced = self.graph.entries();
935 self.execute_loop(RunState::new(state), forced).await
936 }
937
938 pub async fn run_with_entries(
947 &self,
948 state: S,
949 entries: impl IntoIterator<Item = NodeId>,
950 ) -> Result<EngineResult<S>, BspError> {
951 tracing::info!("🏁 [BspEngine] Initiating graph execution pass with forced entries...");
952 self.execute_loop(RunState::new(state), entries.into_iter().collect())
953 .await
954 }
955
956 pub async fn resume(
959 &self,
960 mut suspended: SuspendedExecution<S>,
961 injections: IndexMap<InputPin, BspValue>,
962 ) -> Result<EngineResult<S>, BspError> {
963 tracing::info!(
964 "🔄 [BspEngine] Resuming execution with {} injected inputs...",
965 injections.len()
966 );
967 for (pin, val) in injections {
968 suspended.state.values.injected.insert(pin, val);
969 }
970 let forced = suspended.suspensions.iter().map(|s| s.node_id).collect();
971 self.execute_loop(suspended.state, forced).await
972 }
973}
974
975impl<S> BspEngine<S> {
976 pub async fn run_sequential(&self, state: S) -> Result<EngineResult<S>, BspError> {
981 tracing::info!("🏁 [BspEngine] Initiating sequential graph execution pass...");
982 let forced = self.graph.entries();
983 self.execute_loop_sequential(RunState::new(state), forced)
984 .await
985 }
986
987 pub async fn resume_sequential(
989 &self,
990 mut suspended: SuspendedExecution<S>,
991 injections: IndexMap<InputPin, BspValue>,
992 ) -> Result<EngineResult<S>, BspError> {
993 tracing::info!(
994 "🔄 [BspEngine] Resuming sequential execution with {} injected inputs...",
995 injections.len()
996 );
997 for (pin, val) in injections {
998 suspended.state.values.injected.insert(pin, val);
999 }
1000 let forced = suspended.suspensions.iter().map(|s| s.node_id).collect();
1001 self.execute_loop_sequential(suspended.state, forced).await
1002 }
1003}
1004
1005#[allow(clippy::too_many_lines)]
1011fn compute_validation<S>(graph: &BspGraph<S>) -> ValidationReport {
1012 let mut warnings = Vec::new();
1013
1014 macro_rules! fail {
1015 ($err:expr) => {
1016 return ValidationReport {
1017 error: Some($err),
1018 warnings,
1019 }
1020 };
1021 }
1022
1023 let mut unique_edges = HashSet::new();
1028 for edge in &graph.edges {
1029 let key = (
1030 edge.source,
1031 edge.target,
1032 edge.source_port,
1033 edge.target_port,
1034 edge.condition.label(),
1035 );
1036 if !unique_edges.insert(key) {
1037 let fmt_port = |p: Option<PortIndex>| p.map_or("-".to_string(), |p| p.to_string());
1038 fail!(BspError::ValidationError(format!(
1039 "Duplicate link detected from Node {} (Port {}) to Node {} (Port {})",
1040 edge.source,
1041 fmt_port(edge.source_port),
1042 edge.target,
1043 fmt_port(edge.target_port)
1044 )));
1045 }
1046 }
1047
1048 for edge in &graph.edges {
1050 let Some(source_node) = graph.nodes.get(&edge.source) else {
1051 fail!(BspError::NodeNotFound {
1052 node_id: edge.source
1053 });
1054 };
1055 let Some(target_node) = graph.nodes.get(&edge.target) else {
1056 fail!(BspError::NodeNotFound {
1057 node_id: edge.target
1058 });
1059 };
1060
1061 if let Some(port) = edge.source_port
1062 && !source_node.is_dynamic_output()
1063 && !source_node.output_ports().contains(&port)
1064 {
1065 fail!(BspError::ValidationError(format!(
1066 "Link setup references invalid output port {} on Node {} ('{}')",
1067 port,
1068 edge.source,
1069 source_node.name()
1070 )));
1071 }
1072
1073 if let Some(port) = edge.target_port
1074 && !target_node.input_ports().contains(&port)
1075 {
1076 fail!(BspError::ValidationError(format!(
1077 "Link setup references invalid input port {} on Node {} ('{}')",
1078 port,
1079 edge.target,
1080 target_node.name()
1081 )));
1082 }
1083 }
1084
1085 for pin in graph.static_inputs.keys() {
1087 let Some(node) = graph.nodes.get(&pin.node_id) else {
1088 fail!(BspError::NodeNotFound {
1089 node_id: pin.node_id
1090 });
1091 };
1092 if !node.input_ports().contains(&pin.port) {
1093 fail!(BspError::ValidationError(format!(
1094 "Static input targets invalid port {} on Node {} ('{}')",
1095 pin.port,
1096 pin.node_id,
1097 node.name()
1098 )));
1099 }
1100 }
1101
1102 let mut missing: Vec<String> = Vec::new();
1104 for edge in &graph.edges {
1105 let mut named = Vec::new();
1106 edge.condition.collect_named(&mut named);
1107 for name in named {
1108 if !graph.conditions.contains_key(&name) {
1109 missing.push(format!("condition '{name}'"));
1110 }
1111 }
1112 }
1113 for node in graph.nodes.values() {
1114 if let Some(merger) = node.merger()
1115 && !graph.mergers.contains_key(merger)
1116 {
1117 missing.push(format!("merger '{merger}'"));
1118 }
1119 }
1120 if !missing.is_empty() {
1121 missing.sort();
1122 missing.dedup();
1123 fail!(BspError::ValidationError(format!(
1124 "graph references unregistered names: {}",
1125 missing.join(", ")
1126 )));
1127 }
1128
1129 let live_edges: Vec<EdgeId> = graph
1131 .edges
1132 .iter()
1133 .enumerate()
1134 .filter(|(_, e)| !matches!(e.condition, Condition::Never))
1135 .map(|(id, _)| id)
1136 .collect();
1137
1138 let mut incoming_live: IndexMap<NodeId, Vec<EdgeId>> = IndexMap::new();
1139 let mut adjacency: IndexMap<NodeId, Vec<NodeId>> = IndexMap::new();
1140 for &edge_id in &live_edges {
1141 let edge = &graph.edges[edge_id];
1142 incoming_live.entry(edge.target).or_default().push(edge_id);
1143 adjacency.entry(edge.source).or_default().push(edge.target);
1144 }
1145
1146 let mut fireable: IndexSet<NodeId> = IndexSet::new();
1148 for &id in &graph.entry {
1149 fireable.insert(id);
1150 }
1151 for &id in graph.nodes.keys() {
1152 if incoming_live.get(&id).is_none_or(Vec::is_empty) {
1153 fireable.insert(id);
1154 }
1155 }
1156 loop {
1157 let mut changed = false;
1158 for (&id, node) in &graph.nodes {
1159 if fireable.contains(&id) {
1160 continue;
1161 }
1162 let incoming = &incoming_live[&id];
1163 let fires = match node.activation() {
1164 Activation::Any => incoming
1165 .iter()
1166 .any(|e| fireable.contains(&graph.edges[*e].source)),
1167 Activation::All => incoming
1168 .iter()
1169 .all(|e| fireable.contains(&graph.edges[*e].source)),
1170 };
1171 if fires {
1172 fireable.insert(id);
1173 changed = true;
1174 }
1175 }
1176 if !changed {
1177 break;
1178 }
1179 }
1180
1181 let sccs = strongly_connected_components(&adjacency, graph.nodes.keys().copied());
1183 let mut cyclic_nodes: HashSet<NodeId> = HashSet::new();
1184 for scc in &sccs {
1185 if scc.len() > 1 {
1186 cyclic_nodes.extend(scc.iter().copied());
1187 }
1188 }
1189 for &edge_id in &live_edges {
1190 let edge = &graph.edges[edge_id];
1191 if edge.source == edge.target {
1192 cyclic_nodes.insert(edge.source);
1193 }
1194 }
1195
1196 let unfireable: Vec<NodeId> = graph
1198 .nodes
1199 .keys()
1200 .copied()
1201 .filter(|id| !fireable.contains(id))
1202 .collect();
1203 if !unfireable.is_empty() {
1204 if let Some(&first) = unfireable.iter().find(|id| cyclic_nodes.contains(id)) {
1205 fail!(BspError::CycleDetected { node_id: first });
1206 }
1207 for id in &unfireable {
1209 warnings.push(format!(
1210 "Node {id} ('{}') is unreachable: no live path from any entry",
1211 graph.nodes[id].name()
1212 ));
1213 }
1214 }
1215
1216 for scc in &sccs {
1218 let is_cycle = scc.len() > 1
1219 || (scc.len() == 1 && {
1220 let id = scc[0];
1221 live_edges.iter().any(|&e| {
1222 let edge = &graph.edges[e];
1223 edge.source == id && edge.target == id
1224 })
1225 });
1226 if !is_cycle {
1227 continue;
1228 }
1229 let members: HashSet<NodeId> = scc.iter().copied().collect();
1230 let bounded = live_edges.iter().any(|&e| {
1231 let edge = &graph.edges[e];
1232 members.contains(&edge.source)
1233 && members.contains(&edge.target)
1234 && edge.max_iterations.is_some()
1235 });
1236 if !bounded {
1237 warnings.push(format!(
1238 "Cycle involving nodes {scc:?} has no max_iterations budget on any edge; \
1239 an unbounded loop only stops at the global max_steps limit"
1240 ));
1241 }
1242 for &id in scc {
1243 let incoming_arity = incoming_live.get(&id).map_or(0, Vec::len);
1246 if incoming_arity >= 2
1247 && fireable.contains(&id)
1248 && graph.nodes[&id].activation() == Activation::All
1249 {
1250 warnings.push(format!(
1251 "Node {id} ('{}') is an All-join on a cycle: tokens on one incoming edge \
1252 arrive once per iteration while another's may arrive once ever, starving \
1253 the join. Prefer Any or a merger-join inside loops",
1254 graph.nodes[&id].name()
1255 ));
1256 }
1257 }
1258 }
1259
1260 for (&id, node) in &graph.nodes {
1261 if let Some(outgoing) = graph.outgoing.get(&id) {
1262 let live: Vec<EdgeId> = outgoing
1263 .iter()
1264 .copied()
1265 .filter(|e| !matches!(graph.edges[*e].condition, Condition::Never))
1266 .collect();
1267 if !live.is_empty()
1269 && !live
1270 .iter()
1271 .any(|&e| matches!(graph.edges[e].condition, Condition::Always))
1272 {
1273 warnings.push(format!(
1274 "Node {id} ('{}') has no Always fallback on its outgoing edges; if no \
1275 condition matches the run dead-ends here",
1276 node.name()
1277 ));
1278 }
1279 }
1280
1281 if matches!(node.recovery(), RecoveryPolicy::RouteToErrorEdge) {
1284 let has_error_edge = graph.outgoing.get(&id).is_some_and(|outgoing| {
1285 outgoing
1286 .iter()
1287 .any(|&e| matches!(graph.edges[e].condition, Condition::OnError))
1288 });
1289 if !has_error_edge {
1290 warnings.push(format!(
1291 "Node {id} ('{}') uses RouteToErrorEdge recovery but has no outgoing \
1292 OnError edge; failures will abort the run",
1293 node.name()
1294 ));
1295 }
1296 }
1297
1298 if let Some(incoming) = graph.incoming.get(&id) {
1300 let distinct_sources: HashSet<NodeId> =
1301 incoming.iter().map(|&e| graph.edges[e].source).collect();
1302 if distinct_sources.len() >= 2 && node.merger().is_none() {
1303 warnings.push(format!(
1304 "Node {id} ('{}') joins multiple branches without a named merger; \
1305 concurrent branch states fold last-writer-wins (harmless only if nodes \
1306 do not mutate shared state)",
1307 node.name()
1308 ));
1309 }
1310 }
1311 }
1312
1313 for (pin, value) in &graph.static_inputs {
1316 if crate::def::value_to_json(value).is_none()
1317 && let Some(node) = graph.nodes.get(&pin.node_id)
1318 {
1319 warnings.push(format!(
1320 "Static input on node {} ('{}') port {} is not GraphDef-serializable; \
1321 `to_def` will skip it",
1322 pin.node_id,
1323 node.name(),
1324 pin.port
1325 ));
1326 }
1327 }
1328
1329 ValidationReport {
1330 error: None,
1331 warnings,
1332 }
1333}
1334
1335fn strongly_connected_components(
1337 adjacency: &IndexMap<NodeId, Vec<NodeId>>,
1338 nodes: impl Iterator<Item = NodeId>,
1339) -> Vec<Vec<NodeId>> {
1340 let all: Vec<NodeId> = nodes.collect();
1341
1342 let mut visited: HashSet<NodeId> = HashSet::new();
1344 let mut finish_order: Vec<NodeId> = Vec::new();
1345 for &start in &all {
1346 if visited.contains(&start) {
1347 continue;
1348 }
1349 let mut stack: Vec<(NodeId, usize)> = vec![(start, 0)];
1350 visited.insert(start);
1351 while let Some((node, child_idx)) = stack.pop() {
1352 let neighbors: &[NodeId] = adjacency.get(&node).map_or(&[], Vec::as_slice);
1353 if child_idx < neighbors.len() {
1354 stack.push((node, child_idx + 1));
1355 let next = neighbors[child_idx];
1356 if visited.insert(next) {
1357 stack.push((next, 0));
1358 }
1359 } else {
1360 finish_order.push(node);
1361 }
1362 }
1363 }
1364
1365 let mut reverse: IndexMap<NodeId, Vec<NodeId>> = IndexMap::new();
1367 for (from, targets) in adjacency {
1368 for &to in targets {
1369 reverse.entry(to).or_default().push(*from);
1370 }
1371 }
1372
1373 let mut visited: HashSet<NodeId> = HashSet::new();
1374 let mut sccs = Vec::new();
1375 for &start in finish_order.iter().rev() {
1376 if visited.contains(&start) {
1377 continue;
1378 }
1379 let mut scc = Vec::new();
1380 let mut stack = vec![start];
1381 visited.insert(start);
1382 while let Some(node) = stack.pop() {
1383 scc.push(node);
1384 if let Some(predecessors) = reverse.get(&node) {
1385 for &pred in predecessors {
1386 if visited.insert(pred) {
1387 stack.push(pred);
1388 }
1389 }
1390 }
1391 }
1392 sccs.push(scc);
1393 }
1394 sccs
1395}
1396
1397#[cfg(test)]
1398mod tests {
1399 use super::*;
1400
1401 #[test]
1402 fn test_sccs_find_cycles() {
1403 let mut adjacency: IndexMap<NodeId, Vec<NodeId>> = IndexMap::new();
1404 adjacency.insert(1, vec![2]);
1405 adjacency.insert(2, vec![1, 3]);
1406 adjacency.insert(3, vec![4]);
1407 adjacency.insert(4, vec![3]);
1408 let sccs = strongly_connected_components(&adjacency, adjacency.keys().copied());
1409 let mut sizes: Vec<usize> = sccs.iter().map(Vec::len).collect();
1410 sizes.sort_unstable();
1411 assert_eq!(sizes, vec![2, 2]);
1412 }
1413
1414 #[test]
1415 fn test_sccs_of_dag_are_singletons() {
1416 let mut adjacency: IndexMap<NodeId, Vec<NodeId>> = IndexMap::new();
1417 adjacency.insert(1, vec![2, 3]);
1418 adjacency.insert(2, vec![4]);
1419 adjacency.insert(3, vec![4]);
1420 let sccs = strongly_connected_components(&adjacency, adjacency.keys().copied());
1421 assert!(sccs.iter().all(|scc| scc.len() == 1));
1422 assert_eq!(sccs.len(), 4);
1423 }
1424}