1use crate::config::{
5 BridgeChannelConfigRepresentation, ComponentConfig, ConfigGraphs, CuConfig, CuDirection,
6 CuGraph, Flavor, Node, NodeId, PlannerKind, TaskKind,
7};
8use crate::curuntime::{
9 CuExecutionLoop, CuExecutionStep, CuExecutionUnit, CuInputMsg, CuOutputPack, CuStepPhase,
10 CuTaskType, expand_anytime_steps, find_task_type_for_id,
11};
12use alloc::boxed::Box;
13use alloc::collections::{BTreeMap, BTreeSet, VecDeque};
14use alloc::format;
15use alloc::string::{String, ToString};
16use alloc::vec;
17use alloc::vec::Vec;
18use cu29_traits::{CuError, CuResult};
19
20mod distributed;
21pub use distributed::DistributedSchedule;
22mod explicit_schedule;
23pub use explicit_schedule::CuPlan;
24pub use explicit_schedule::ExplicitSchedule;
25mod pipeline;
26pub use pipeline::Pipeline;
27#[cfg(feature = "std")]
28mod pgs;
29#[cfg(feature = "std")]
30mod propose;
31mod schedule;
32#[cfg(feature = "std")]
33mod score;
34pub use schedule::CuMissionPlan;
35pub use schedule::CuPlanBackground;
36pub use schedule::CuPlanBackgroundResult;
37pub use schedule::CuPlanDependency;
38pub use schedule::CuPlanPlacement;
39pub use schedule::CuPlanStep;
40pub use schedule::CuPlanThread;
41pub use schedule::CuPlanWorker;
42
43#[cfg(feature = "std")]
44#[doc(hidden)]
45pub use pgs::{
46 CuChain, CuChainProfile, CuContract, CuCostStats, CuFiringPattern, CuOperationProfile,
47 CuProfile, CuSourceProfile, CuSourceRate, graph_signature,
48};
49#[cfg(feature = "std")]
50#[doc(hidden)]
51pub use propose::CuCandidate;
52#[cfg(feature = "std")]
53#[doc(hidden)]
54pub use propose::CuChainPrediction;
55#[cfg(feature = "std")]
56#[doc(hidden)]
57pub use propose::CuPrediction;
58#[cfg(feature = "std")]
59#[doc(hidden)]
60pub use propose::CuWorkerPrediction;
61#[cfg(feature = "std")]
62#[doc(hidden)]
63pub use propose::ProposeRequest;
64#[cfg(feature = "std")]
65#[doc(hidden)]
66pub use propose::propose;
67#[cfg(feature = "std")]
68#[doc(hidden)]
69pub use score::CuChainScore;
70#[cfg(feature = "std")]
71#[doc(hidden)]
72pub use score::CuScoreRow;
73#[cfg(feature = "std")]
74#[doc(hidden)]
75pub use score::CuScoreTable;
76
77#[doc(hidden)]
80pub use crate::config::DEFAULT_COPPERLIST_COUNT;
81
82#[doc(hidden)]
84#[derive(Clone, Copy, Debug, PartialEq, Eq)]
85pub enum PlanEntityKind {
86 Task {
87 original_node_id: NodeId,
88 task_index: usize,
89 },
90 BridgeRx {
91 bridge_config_index: usize,
92 channel_config_index: usize,
93 },
94 BridgeTx {
95 bridge_config_index: usize,
96 channel_config_index: usize,
97 },
98}
99
100#[doc(hidden)]
102#[derive(Clone, Debug, PartialEq, Eq)]
103pub struct PlanEntity {
104 pub key: String,
105 pub label: String,
106 pub kind: PlanEntityKind,
107}
108
109#[doc(hidden)]
111pub struct AssembledPlan {
112 pub execution: CuExecutionLoop,
113 pub entities: Vec<PlanEntity>,
115 pub plan_to_original: Vec<Option<NodeId>>,
117 pub background: Vec<CuPlanBackground>,
119 pub lanes: Option<LanePlan>,
121}
122
123#[doc(hidden)]
125#[derive(Clone, Debug, PartialEq, Eq)]
126pub struct LanePlan {
127 pub copperlists_per_cycle: u32,
128 pub max_in_flight: u32,
129 pub occurrences: Vec<LaneOccurrence>,
130 pub workers: Vec<LaneWorker>,
131 pub dispatcher: Option<CuPlanThread>,
132 pub dependencies: Vec<CuPlanDependency>,
133}
134
135#[doc(hidden)]
136#[derive(Clone, Copy, Debug, PartialEq, Eq)]
137pub struct LaneOccurrence {
138 pub step: usize,
139 pub copperlist: u32,
140}
141
142#[doc(hidden)]
143#[derive(Clone, Debug, PartialEq, Eq)]
144pub struct LaneWorker {
145 pub id: String,
146 pub placement: CuPlanPlacement,
147 pub occurrences: Vec<usize>,
148}
149
150#[derive(Clone, Debug, PartialEq, Eq)]
152pub(crate) struct StepOrder(pub Vec<NodeId>);
153
154pub struct PlanningInput<'a> {
156 pub config: &'a CuConfig,
157 pub baseline: CuPlan,
158}
159
160impl<'a> PlanningInput<'a> {
161 pub fn new(config: &'a CuConfig) -> CuResult<Self> {
162 Ok(Self {
163 config,
164 baseline: CuPlan::from_config_cyclic(config, 1)?,
165 })
166 }
167}
168
169pub trait CuPlanner {
171 fn plan(&self, input: &PlanningInput<'_>) -> CuResult<CuPlan>;
172}
173
174trait NodeOrderPlanner {
175 fn node_order(&self, graph: &CuGraph) -> CuResult<StepOrder>;
176}
177
178#[derive(Default)]
180pub struct Serial;
181
182impl NodeOrderPlanner for Serial {
183 fn node_order(&self, graph: &CuGraph) -> CuResult<StepOrder> {
184 topo_bfs_order(graph)
185 }
186}
187
188pub struct TaskOrder {
190 order: Vec<String>,
191}
192
193impl TaskOrder {
194 fn new(config: Option<&ComponentConfig>) -> CuResult<Self> {
195 const NEEDS_ORDER: &str = "TaskOrder needs config: { \"order\": [..task ids..] }";
196 let order = config
197 .ok_or(CuError::from(NEEDS_ORDER))?
198 .get_value::<Vec<String>>("order")?
199 .ok_or(CuError::from(NEEDS_ORDER))?;
200 Ok(Self { order })
201 }
202}
203
204impl NodeOrderPlanner for TaskOrder {
205 fn node_order(&self, graph: &CuGraph) -> CuResult<StepOrder> {
206 task_order(graph, &resolve_task_order_ids(graph, &self.order)?)
207 }
208}
209
210impl CuPlanner for Serial {
211 fn plan(&self, input: &PlanningInput<'_>) -> CuResult<CuPlan> {
212 Ok(input.baseline.clone())
213 }
214}
215
216impl CuPlanner for TaskOrder {
217 fn plan(&self, input: &PlanningInput<'_>) -> CuResult<CuPlan> {
218 let mut plan = input.baseline.clone();
219 for (mission, graph) in mission_graphs(input.config) {
220 let assembled = assemble_runtime_plan_with_planner(input.config, graph, self)?;
221 plan.missions.insert(
222 mission.clone(),
223 schedule::PlanShape::new(&assembled, input.config, graph, &mission, &[])?
224 .serial_plan()?,
225 );
226 }
227 Ok(plan)
228 }
229}
230
231fn resolve_schedule(config: &CuConfig) -> CuResult<CuPlan> {
232 let selection = config.planner_config();
233 let params = selection.and_then(|selection| selection.get_config());
234 let planner: Box<dyn CuPlanner> = match selection
235 .map(|selection| selection.kind)
236 .unwrap_or_default()
237 {
238 PlannerKind::Serial => Box::new(Serial),
239 PlannerKind::TaskOrder => Box::new(TaskOrder::new(params)?),
240 PlannerKind::Pipeline => Box::new(Pipeline::new(params)?),
241 PlannerKind::ExplicitSchedule => {
242 let plan = params
243 .ok_or_else(|| CuError::from("ExplicitSchedule needs config.plan"))?
244 .get_value::<CuPlan>("plan")
245 .map_err(|error| CuError::from(format!("ExplicitSchedule: {error}")))?
246 .ok_or_else(|| CuError::from("ExplicitSchedule needs config.plan"))?;
247 Box::new(ExplicitSchedule::new(plan))
248 }
249 };
250 let input = PlanningInput::new(config)?;
251 let plan = planner.plan(&input)?;
252 plan.validate(config)?;
253 Ok(plan)
254}
255
256fn resolve_task_order_ids(graph: &CuGraph, ids: &[String]) -> CuResult<Vec<NodeId>> {
259 let mut task_ids: BTreeMap<String, NodeId> = BTreeMap::new();
260 let mut bridge_labels: BTreeSet<String> = BTreeSet::new();
261 for (node_id, node) in graph.get_all_nodes() {
262 match node.get_flavor() {
263 Flavor::Task => {
264 task_ids.insert(node.get_id(), node_id);
265 }
266 Flavor::Bridge => {
267 bridge_labels.insert(node.get_id());
268 }
269 }
270 }
271 let valid_ids = || {
272 let mut names: Vec<String> = task_ids.keys().cloned().collect();
273 names.sort();
274 names.join(", ")
275 };
276
277 let mut resolved = Vec::with_capacity(ids.len());
278 let mut seen: BTreeSet<NodeId> = BTreeSet::new();
279 for id in ids {
280 if let Some(&node_id) = task_ids.get(id) {
281 if !seen.insert(node_id) {
282 return Err(CuError::from(format!(
283 "TaskOrder lists task '{id}' more than once."
284 )));
285 }
286 resolved.push(node_id);
287 } else if bridge_labels.contains(id) {
288 return Err(CuError::from(format!(
289 "TaskOrder lists bridge stage '{id}'; list only task ids: [{}].",
290 valid_ids()
291 )));
292 } else {
293 return Err(CuError::from(format!(
294 "TaskOrder lists unknown task '{id}'; valid task ids: [{}].",
295 valid_ids()
296 )));
297 }
298 }
299
300 if resolved.len() != task_ids.len() {
301 let mut missing: Vec<String> = task_ids
302 .iter()
303 .filter(|(_, node_id)| !seen.contains(node_id))
304 .map(|(name, _)| name.clone())
305 .collect();
306 missing.sort();
307 return Err(CuError::from(format!(
308 "TaskOrder must list every task exactly once; missing: [{}].",
309 missing.join(", ")
310 )));
311 }
312
313 Ok(resolved)
314}
315
316pub(crate) fn topo_bfs_order(graph: &CuGraph) -> CuResult<StepOrder> {
324 #[cfg(all(feature = "std", feature = "macro_debug"))]
325 eprintln!("[step order: graph order]");
326 let mut order: Vec<NodeId> = Vec::new();
327 let mut planned: BTreeSet<NodeId> = BTreeSet::new();
328
329 let mut queue: VecDeque<NodeId> = VecDeque::new();
330 for node_id in graph.node_ids() {
331 if find_task_type_for_id(graph, node_id)? == CuTaskType::Source {
332 queue.push_back(node_id);
333 }
334 }
335 #[cfg(all(feature = "std", feature = "macro_debug"))]
336 eprintln!("Initial source nodes: {queue:?}");
337
338 while let Some(start_node) = queue.pop_front() {
339 #[cfg(all(feature = "std", feature = "macro_debug"))]
340 eprintln!("→ Starting BFS from source {start_node}");
341 for node_id in graph.bfs_nodes(start_node) {
342 if planned.contains(&node_id) {
343 continue;
344 }
345 if topo_bfs_branch(graph, node_id, &mut order, &mut planned)? {
346 for neighbor in graph.get_neighbor_ids(node_id, CuDirection::Outgoing) {
347 queue.push_back(neighbor);
348 }
349 }
350 }
351 }
352
353 Ok(StepOrder(order))
354}
355
356fn topo_bfs_branch(
360 graph: &CuGraph,
361 starting_point: NodeId,
362 order: &mut Vec<NodeId>,
363 planned: &mut BTreeSet<NodeId>,
364) -> CuResult<bool> {
365 #[cfg(all(feature = "std", feature = "macro_debug"))]
366 eprintln!("-- starting branch from node {starting_point}");
367 let mut handled = false;
368 for id in graph.bfs_nodes(starting_point) {
369 #[cfg(all(feature = "std", feature = "macro_debug"))]
370 eprintln!(" Visiting node: {:?}", graph.get_node(id));
371 if find_task_type_for_id(graph, id)? != CuTaskType::Source {
372 let mut edge_ids = graph.get_dst_edges(id).unwrap_or_default();
373 edge_ids.sort();
374 let mut ready = true;
375 for edge_id in edge_ids {
376 let edge = graph
377 .edge(edge_id)
378 .unwrap_or_else(|| panic!("Missing edge {edge_id} for node {id}"));
379 let pid = graph
380 .get_node_id_by_name(edge.src.as_str())
381 .unwrap_or_else(|| {
382 panic!("Missing source node '{}' for edge {edge_id}", edge.src)
383 });
384 if !planned.contains(&pid) {
385 #[cfg(all(feature = "std", feature = "macro_debug"))]
386 eprintln!(" ✗ Input from {pid} not ready, returning");
387 ready = false;
388 break;
389 }
390 }
391 if !ready {
392 return Ok(handled);
393 }
394 }
395 if planned.contains(&id) {
401 unreachable!("plan re-visit path reached for node {id}");
402 }
403 #[cfg(all(feature = "std", feature = "macro_debug"))]
404 eprintln!(" → Node {id} added to the order");
405 order.push(id);
406 planned.insert(id);
407 handled = true;
408 }
409 #[cfg(all(feature = "std", feature = "macro_debug"))]
410 eprintln!("-- finished branch from node {starting_point} with handled={handled}");
411 Ok(handled)
412}
413
414fn task_order(graph: &CuGraph, ordered_tasks: &[NodeId]) -> CuResult<StepOrder> {
422 let mut task_position: BTreeMap<NodeId, usize> = BTreeMap::new();
423 for (position, &task) in ordered_tasks.iter().enumerate() {
424 task_position.insert(task, position);
425 }
426
427 let mut before: BTreeMap<usize, Vec<NodeId>> = BTreeMap::new();
428 let mut after: BTreeMap<usize, Vec<NodeId>> = BTreeMap::new();
429 let mut leading: Vec<NodeId> = Vec::new();
430 let mut trailing: Vec<NodeId> = Vec::new();
431
432 for (node_id, node) in graph.get_all_nodes() {
433 if node.get_flavor() != Flavor::Bridge {
434 continue;
435 }
436 match find_task_type_for_id(graph, node_id)? {
437 CuTaskType::Source => {
438 match graph
439 .get_neighbor_ids(node_id, CuDirection::Outgoing)
440 .into_iter()
441 .filter_map(|consumer| task_position.get(&consumer).copied())
442 .min()
443 {
444 Some(pos) => before.entry(pos).or_default().push(node_id),
445 None => leading.push(node_id),
446 }
447 }
448 CuTaskType::Sink => {
449 match graph
450 .get_neighbor_ids(node_id, CuDirection::Incoming)
451 .into_iter()
452 .filter_map(|producer| task_position.get(&producer).copied())
453 .max()
454 {
455 Some(pos) => after.entry(pos).or_default().push(node_id),
456 None => trailing.push(node_id),
457 }
458 }
459 CuTaskType::Regular => trailing.push(node_id),
460 }
461 }
462
463 for stages in before.values_mut() {
464 stages.sort_unstable();
465 }
466 for stages in after.values_mut() {
467 stages.sort_unstable();
468 }
469 leading.sort_unstable();
470 trailing.sort_unstable();
471
472 let mut order = Vec::new();
473 order.append(&mut leading);
474 for (position, &task) in ordered_tasks.iter().enumerate() {
475 if let Some(stages) = before.get(&position) {
476 order.extend(stages.iter().copied());
477 }
478 order.push(task);
479 if let Some(stages) = after.get(&position) {
480 order.extend(stages.iter().copied());
481 }
482 }
483 order.append(&mut trailing);
484
485 Ok(StepOrder(order))
486}
487
488pub(crate) fn check_order(graph: &CuGraph, order: &StepOrder) -> CuResult<()> {
494 let mut position: Vec<Option<usize>> = vec![None; graph.node_count()];
495
496 for (index, &node_id) in order.0.iter().enumerate() {
497 let slot = position.get_mut(node_id as usize).ok_or_else(|| {
498 CuError::from(format!("Plan order references unknown node id {node_id}."))
499 })?;
500 if slot.is_some() {
501 return Err(CuError::from(format!(
502 "Task '{}' appears more than once in the plan order.",
503 node_name(graph, node_id)
504 )));
505 }
506 *slot = Some(index);
507 }
508
509 let mut missing: Vec<String> = Vec::new();
510 for node_id in graph.node_ids() {
511 if position[node_id as usize].is_none() {
512 missing.push(node_name(graph, node_id));
513 }
514 }
515 if !missing.is_empty() {
516 missing.sort();
517 return Err(CuError::from(format!(
518 "Execution plan could not include all nodes. Missing: {}. Check for loopback or missing source connections.",
519 missing.join(", ")
520 )));
521 }
522
523 for edge in graph.edges() {
524 let (Some(src), Some(dst)) = (
525 graph.get_node_id_by_name(edge.src.as_str()),
526 graph.get_node_id_by_name(edge.dst.as_str()),
527 ) else {
528 continue;
529 };
530 if position[src as usize] >= position[dst as usize] {
531 return Err(CuError::from(format!(
532 "Task '{}' is scheduled before its input '{}'.",
533 node_name(graph, dst),
534 node_name(graph, src)
535 )));
536 }
537 }
538
539 Ok(())
540}
541
542pub(crate) fn plan_from_order(graph: &CuGraph, order: &StepOrder) -> CuResult<CuExecutionLoop> {
547 #[cfg(all(feature = "std", feature = "macro_debug"))]
548 eprintln!("[runtime plan]");
549 let mut plan: Vec<CuExecutionUnit> = Vec::new();
550 let mut next_culist_output_index = 0u32;
551
552 for &id in &order.0 {
553 let node_ref = graph
554 .get_node(id)
555 .ok_or_else(|| CuError::from(format!("Node id {id} not found")))?;
556 let task_type = find_task_type_for_id(graph, id)?;
557 let mut input_msg_indices_types = if task_type == CuTaskType::Source {
558 Vec::new()
559 } else {
560 collect_step_inputs(graph, id, &plan)?
561 };
562 #[cfg(all(feature = "std", feature = "macro_debug"))]
563 eprintln!(
564 " {task_type:?} node {id} → output index {next_culist_output_index}, inputs {input_msg_indices_types:?}"
565 );
566 let output_msg_pack: Option<CuOutputPack>;
567
568 match task_type {
569 CuTaskType::Source => {
570 let ports = graph.get_node_output_ports_by_id(id)?;
571 if ports.is_empty() {
572 return Err(CuError::from(format!(
573 "Source node '{}' has no declared outputs",
574 node_ref.get_id()
575 )));
576 }
577 let (msg_types, src_channels) = ports.into_iter().unzip();
578 output_msg_pack = Some(CuOutputPack {
579 culist_index: next_culist_output_index,
580 msg_types,
581 src_channels,
582 });
583 next_culist_output_index += 1;
584 }
585 CuTaskType::Sink => {
586 output_msg_pack = Some(CuOutputPack {
587 culist_index: next_culist_output_index,
588 msg_types: Vec::from(["()".to_string()]),
589 src_channels: vec![None],
590 });
591 next_culist_output_index += 1;
592 }
593 CuTaskType::Regular => {
594 let ports = graph.get_node_output_ports_by_id(id)?;
595 if ports.is_empty() {
596 return Err(CuError::from(format!(
597 "Regular node '{}' has no declared outputs",
598 node_ref.get_id()
599 )));
600 }
601 let (msg_types, src_channels) = ports.into_iter().unzip();
602 output_msg_pack = Some(CuOutputPack {
603 culist_index: next_culist_output_index,
604 msg_types,
605 src_channels,
606 });
607 next_culist_output_index += 1;
608 }
609 }
610
611 sort_inputs_by_connection_order(&mut input_msg_indices_types);
612 plan.push(CuExecutionUnit::Step(Box::new(CuExecutionStep {
613 node_id: id,
614 node: node_ref.clone(),
615 task_type,
616 phase: CuStepPhase::default(),
617 input_msg_indices_types,
618 output_msg_pack,
619 })));
620 }
621
622 Ok(CuExecutionLoop {
623 steps: plan,
624 loop_count: None,
625 })
626}
627
628fn collect_step_inputs(
633 graph: &CuGraph,
634 id: NodeId,
635 plan: &[CuExecutionUnit],
636) -> CuResult<Vec<CuInputMsg>> {
637 let mut inputs = Vec::new();
638 let mut edge_ids = graph.get_dst_edges(id).unwrap_or_default();
639 edge_ids.sort();
640 for edge_id in edge_ids {
641 let edge = graph
642 .edge(edge_id)
643 .unwrap_or_else(|| panic!("Missing edge {edge_id} for node {id}"));
644 let pid = graph
645 .get_node_id_by_name(edge.src.as_str())
646 .unwrap_or_else(|| panic!("Missing source node '{}' for edge {edge_id}", edge.src));
647 let output_pack = find_output_pack_from_nodeid(pid, plan).ok_or_else(|| {
648 CuError::from(format!(
649 "Plan materialization: input from node {pid} is not available before node {id}"
650 ))
651 })?;
652 let msg_type = edge.msg.as_str();
653 let src_channel = edge.src_channel.as_deref();
654 let src_port = output_pack
655 .msg_types
656 .iter()
657 .zip(output_pack.src_channels.iter())
658 .position(|(msg, ch)| msg == msg_type && ch.as_deref() == src_channel)
659 .unwrap_or_else(|| {
660 panic!("Missing output port for message type '{msg_type}' on node {pid}")
661 });
662 inputs.push(CuInputMsg {
663 culist_index: output_pack.culist_index,
664 msg_type: msg_type.to_string(),
665 src_port,
666 edge_id,
667 connection_order: edge.order,
668 });
669 }
670 Ok(inputs)
671}
672
673fn find_output_pack_from_nodeid(
674 node_id: NodeId,
675 steps: &[CuExecutionUnit],
676) -> Option<CuOutputPack> {
677 for step in steps {
678 match step {
679 CuExecutionUnit::Loop(loop_unit) => {
680 if let Some(output_pack) = find_output_pack_from_nodeid(node_id, &loop_unit.steps) {
681 return Some(output_pack);
682 }
683 }
684 CuExecutionUnit::Step(step) if step.node_id == node_id => {
685 return step.output_msg_pack.clone();
686 }
687 _ => {}
688 }
689 }
690 None
691}
692
693fn sort_inputs_by_connection_order(input_msg_indices_types: &mut [CuInputMsg]) {
698 input_msg_indices_types.sort_by_key(|input| input.connection_order);
699}
700
701fn node_name(graph: &CuGraph, node_id: NodeId) -> String {
702 graph
703 .get_node(node_id)
704 .map(|node| node.get_id())
705 .unwrap_or_else(|| format!("node_id_{node_id}"))
706}
707
708#[derive(Clone, Copy, Debug, PartialEq, Eq)]
709enum ChannelDirection {
710 Rx,
711 Tx,
712}
713
714fn channel_is_used(
715 graph: &CuGraph,
716 bridge_id: &str,
717 channel_id: &str,
718 direction: ChannelDirection,
719) -> bool {
720 graph.edges().any(|connection| match direction {
721 ChannelDirection::Rx => {
722 connection.src == bridge_id && connection.src_channel.as_deref() == Some(channel_id)
723 }
724 ChannelDirection::Tx => {
725 connection.dst == bridge_id && connection.dst_channel.as_deref() == Some(channel_id)
726 }
727 })
728}
729
730fn inferred_output_name(node: &Node, task_type: CuTaskType) -> String {
731 let rust_type = node.get_type();
732 if node.get_declared_task_kind() == Some(TaskKind::Stateless) {
733 return format!(
734 "<<{rust_type} as cu29::cutask::CuStatelessTask>::Output<'static> as cu29::cutask::CuSingleOutputMsg>::Payload"
735 );
736 }
737 if node.anytime().is_some() {
738 return format!(
739 "<<{rust_type} as cu29::cutask_anytime::CuAnytimeTask>::Output<'static> as cu29::cutask::CuSingleOutputMsg>::Payload"
740 );
741 }
742 let task_trait = match task_type {
743 CuTaskType::Source => "cu29::cutask::CuSrcTask",
744 CuTaskType::Regular => "cu29::cutask::CuTask",
745 CuTaskType::Sink => unreachable!("sinks do not have inferred outputs"),
746 };
747 format!(
748 "<<{rust_type} as {task_trait}>::Output<'static> as cu29::cutask::CuSingleOutputMsg>::Payload"
749 )
750}
751
752struct PlanGraph {
754 graph: CuGraph,
755 entities: Vec<PlanEntity>,
756 plan_to_original: Vec<Option<NodeId>>,
757}
758
759fn build_plan_graph(config: &CuConfig, graph: &CuGraph) -> CuResult<PlanGraph> {
763 let mut plan_graph = CuGraph::default();
764 let mut entities = Vec::new();
765 let mut plan_to_original = Vec::new();
766 let mut original_to_plan = Vec::new();
767 original_to_plan.resize(graph.node_count(), None);
768
769 let mut task_index = 0usize;
770 for (original_node_id, node) in graph.get_all_nodes() {
771 if node.get_flavor() != Flavor::Task {
772 continue;
773 }
774 let plan_node_id = plan_graph.add_node(node.clone())?;
775 debug_assert_eq!(plan_node_id as usize, entities.len());
776 original_to_plan[original_node_id as usize] = Some(plan_node_id);
777 plan_to_original.push(Some(original_node_id));
778 entities.push(PlanEntity {
779 key: format!("task:{}", node.get_id()),
780 label: node.get_id(),
781 kind: PlanEntityKind::Task {
782 original_node_id,
783 task_index,
784 },
785 });
786 task_index += 1;
787 }
788
789 for (original_node_id, node) in graph.get_all_nodes() {
793 if node.get_flavor() != Flavor::Task || node.get_declared_task_kind().is_none() {
794 continue;
795 }
796 let task_type = find_task_type_for_id(graph, original_node_id)?;
797 if task_type == CuTaskType::Sink
798 || !graph
799 .get_node_output_msg_types_by_id(original_node_id)?
800 .is_empty()
801 {
802 continue;
803 }
804 let plan_node_id = original_to_plan[original_node_id as usize]
805 .expect("task was mirrored into the plan graph");
806 let message_type = inferred_output_name(node, task_type);
807 plan_graph
808 .get_node_mut(plan_node_id)
809 .expect("mirrored task is present")
810 .add_nc_output(&message_type, usize::MAX);
811 }
812
813 let mut channel_nodes: Vec<(usize, usize, ChannelDirection, NodeId)> = Vec::new();
816 for (bridge_config_index, bridge) in config.bridges.iter().enumerate() {
817 if graph.get_node_id_by_name(&bridge.id).is_none() {
818 continue;
819 }
820 for direction in [ChannelDirection::Rx, ChannelDirection::Tx] {
821 for (channel_config_index, channel) in bridge.channels.iter().enumerate() {
822 let (channel_id, channel_direction) = match channel {
823 BridgeChannelConfigRepresentation::Rx { id, .. } => (id, ChannelDirection::Rx),
824 BridgeChannelConfigRepresentation::Tx { id, .. } => (id, ChannelDirection::Tx),
825 };
826 if channel_direction != direction
827 || !channel_is_used(graph, &bridge.id, channel_id, direction)
828 {
829 continue;
830 }
831
832 let direction_label = match direction {
833 ChannelDirection::Rx => "rx",
834 ChannelDirection::Tx => "tx",
835 };
836 let label = format!("{}::{direction_label}::{channel_id}", bridge.id);
837 let synthetic_type = match direction {
838 ChannelDirection::Rx => "__CuBridgeRxChannel",
839 ChannelDirection::Tx => "__CuBridgeTxChannel",
840 };
841 let mut node = Node::new(&label, synthetic_type);
842 node.set_flavor(Flavor::Bridge);
843 let plan_node_id = plan_graph.add_node(node)?;
844 debug_assert_eq!(plan_node_id as usize, entities.len());
845 plan_to_original.push(None);
846 entities.push(PlanEntity {
847 key: format!("bridge:{}:{direction_label}:{channel_id}", bridge.id),
848 label,
849 kind: match direction {
850 ChannelDirection::Rx => PlanEntityKind::BridgeRx {
851 bridge_config_index,
852 channel_config_index,
853 },
854 ChannelDirection::Tx => PlanEntityKind::BridgeTx {
855 bridge_config_index,
856 channel_config_index,
857 },
858 },
859 });
860 channel_nodes.push((
861 bridge_config_index,
862 channel_config_index,
863 direction,
864 plan_node_id,
865 ));
866 }
867 }
868 }
869
870 for connection in graph.edges() {
871 let src_plan = if let Some(channel_id) = connection.src_channel.as_deref() {
872 find_channel_plan_node(
873 config,
874 &channel_nodes,
875 &connection.src,
876 channel_id,
877 ChannelDirection::Rx,
878 )?
879 } else {
880 let original_id = graph.get_node_id_by_name(&connection.src).ok_or_else(|| {
881 CuError::from(format!("Unknown source node '{}'", connection.src))
882 })?;
883 original_to_plan[original_id as usize].ok_or_else(|| {
884 CuError::from(format!("Source node '{}' is not a task", connection.src))
885 })?
886 };
887 let dst_plan = if let Some(channel_id) = connection.dst_channel.as_deref() {
888 find_channel_plan_node(
889 config,
890 &channel_nodes,
891 &connection.dst,
892 channel_id,
893 ChannelDirection::Tx,
894 )?
895 } else {
896 let original_id = graph.get_node_id_by_name(&connection.dst).ok_or_else(|| {
897 CuError::from(format!("Unknown destination node '{}'", connection.dst))
898 })?;
899 original_to_plan[original_id as usize].ok_or_else(|| {
900 CuError::from(format!(
901 "Destination node '{}' is not a task",
902 connection.dst
903 ))
904 })?
905 };
906
907 plan_graph
908 .connect_ext_with_order(
909 src_plan,
910 dst_plan,
911 &connection.msg,
912 connection.missions.clone(),
913 None,
914 None,
915 connection.order,
916 )
917 .map_err(|error| CuError::from(error.to_string()))?;
918 }
919
920 Ok(PlanGraph {
921 graph: plan_graph,
922 entities,
923 plan_to_original,
924 })
925}
926
927fn assemble_from_order(plan_graph: PlanGraph, order: StepOrder) -> CuResult<AssembledPlan> {
930 check_order(&plan_graph.graph, &order)?;
931 let mut execution = plan_from_order(&plan_graph.graph, &order)?;
932 expand_anytime_steps(&mut execution)?;
933 Ok(AssembledPlan {
934 execution,
935 entities: plan_graph.entities,
936 plan_to_original: plan_graph.plan_to_original,
937 background: Vec::new(),
938 lanes: None,
939 })
940}
941
942#[doc(hidden)]
944pub fn assemble_runtime_plan(config: &CuConfig, graph: &CuGraph) -> CuResult<AssembledPlan> {
945 match config.planner_kind() {
946 PlannerKind::Serial => assemble_runtime_plan_with_planner(config, graph, &Serial),
947 PlannerKind::TaskOrder => assemble_runtime_plan_with_planner(
948 config,
949 graph,
950 &TaskOrder::new(
951 config
952 .planner_config()
953 .and_then(|selection| selection.get_config()),
954 )?,
955 ),
956 PlannerKind::Pipeline | PlannerKind::ExplicitSchedule => Err(CuError::from(
957 "Use assemble_runtime_plan_for_mission for complete schedules",
958 )),
959 }
960}
961
962#[doc(hidden)]
964pub fn assemble_runtime_plan_for_mission(
965 config: &CuConfig,
966 graph: &CuGraph,
967 mission: &str,
968) -> CuResult<AssembledPlan> {
969 ExplicitSchedule::new(resolve_schedule(config)?).assemble(config, graph, mission)
970}
971
972fn assemble_runtime_plan_with_planner(
973 config: &CuConfig,
974 graph: &CuGraph,
975 planner: &dyn NodeOrderPlanner,
976) -> CuResult<AssembledPlan> {
977 let plan_graph = build_plan_graph(config, graph)?;
978 let order = planner.node_order(&plan_graph.graph)?;
979 assemble_from_order(plan_graph, order)
980}
981
982fn find_channel_plan_node(
983 config: &CuConfig,
984 channel_nodes: &[(usize, usize, ChannelDirection, NodeId)],
985 bridge_id: &str,
986 channel_id: &str,
987 direction: ChannelDirection,
988) -> CuResult<NodeId> {
989 channel_nodes
990 .iter()
991 .find_map(
992 |(bridge_index, channel_index, candidate_direction, node_id)| {
993 let bridge = &config.bridges[*bridge_index];
994 let channel = &bridge.channels[*channel_index];
995 (bridge.id == bridge_id
996 && channel.id() == channel_id
997 && *candidate_direction == direction)
998 .then_some(*node_id)
999 },
1000 )
1001 .ok_or_else(|| {
1002 CuError::from(format!(
1003 "Bridge channel '{bridge_id}/{channel_id}' is missing from the execution plan"
1004 ))
1005 })
1006}
1007
1008#[doc(hidden)]
1010pub fn mission_graphs(config: &CuConfig) -> Vec<(String, &CuGraph)> {
1011 match &config.graphs {
1012 ConfigGraphs::Simple(graph) => vec![("default".to_string(), graph)],
1013 ConfigGraphs::Missions(graphs) => {
1014 let mut missions: Vec<_> = graphs
1015 .iter()
1016 .map(|(mission, graph)| (mission.clone(), graph))
1017 .collect();
1018 missions.sort_by(|left, right| left.0.cmp(&right.0));
1019 missions
1020 }
1021 }
1022}
1023
1024#[doc(hidden)]
1026pub fn step_key(
1027 mission: &str,
1028 entity: &PlanEntity,
1029 phase: CuStepPhase,
1030 refine_ordinal: Option<u32>,
1031) -> String {
1032 let phase = match phase {
1033 CuStepPhase::Whole => "whole".to_string(),
1034 CuStepPhase::AnytimeBase => "base".to_string(),
1035 CuStepPhase::AnytimeRefine => format!("refine:{}", refine_ordinal.unwrap_or(0)),
1036 };
1037 format!("mission:{mission}|{}|phase:{phase}", entity.key)
1038}
1039
1040#[cfg(test)]
1041mod tests {
1042 use super::*;
1043 use crate::curuntime::CuExecutionUnit;
1044
1045 fn config(ron: &str) -> CuConfig {
1046 CuConfig::deserialize_ron(ron).expect("valid planner test config")
1047 }
1048
1049 fn step_labels(plan: &AssembledPlan) -> Vec<String> {
1050 plan.execution
1051 .steps
1052 .iter()
1053 .map(|unit| match unit {
1054 CuExecutionUnit::Step(step) => plan.entities[step.node_id as usize].label.clone(),
1055 CuExecutionUnit::Loop(_) => panic!("unexpected nested loop"),
1056 })
1057 .collect()
1058 }
1059
1060 #[test]
1061 fn plans_diamond_fan_in_with_stable_input_order() {
1062 let config = config(
1063 r#"(
1064 tasks: [
1065 (id: "left", type: "demo::Left"),
1066 (id: "right", type: "demo::Right"),
1067 (id: "join", type: "demo::Join"),
1068 (id: "sink", type: "demo::Sink"),
1069 ],
1070 cnx: [
1071 (src: "right", dst: "join", msg: "demo::RightMsg"),
1072 (src: "left", dst: "join", msg: "demo::LeftMsg"),
1073 (src: "join", dst: "sink", msg: "demo::Joined"),
1074 ],
1075 )"#,
1076 );
1077 let graph = config.get_graph(None).unwrap();
1078 let plan = assemble_runtime_plan(&config, graph).unwrap();
1079 assert_eq!(step_labels(&plan), ["left", "right", "join", "sink"]);
1080 let join = plan
1081 .execution
1082 .steps
1083 .iter()
1084 .find_map(|unit| match unit {
1085 CuExecutionUnit::Step(step) if step.node.get_id() == "join" => Some(step),
1086 _ => None,
1087 })
1088 .unwrap();
1089 assert_eq!(join.input_msg_indices_types.len(), 2);
1090 assert_eq!(join.input_msg_indices_types[0].msg_type, "demo::RightMsg");
1091 assert_eq!(join.input_msg_indices_types[1].msg_type, "demo::LeftMsg");
1092 }
1093
1094 #[test]
1095 fn inserts_bridge_rx_and_tx_channel_stages() {
1096 let config = config(
1097 r#"(
1098 tasks: [
1099 (id: "task", type: "demo::Task"),
1100 ],
1101 bridges: [
1102 (
1103 id: "radio",
1104 type: "demo::Radio",
1105 channels: [Rx(id: "incoming"), Tx(id: "outgoing")],
1106 ),
1107 ],
1108 cnx: [
1109 (src: "radio/incoming", dst: "task", msg: "demo::In"),
1110 (src: "task", dst: "radio/outgoing", msg: "demo::Out"),
1111 ],
1112 )"#,
1113 );
1114 let graph = config.get_graph(None).unwrap();
1115 let plan = assemble_runtime_plan(&config, graph).unwrap();
1116 assert_eq!(
1117 step_labels(&plan),
1118 ["radio::rx::incoming", "task", "radio::tx::outgoing"]
1119 );
1120 assert!(matches!(
1121 plan.entities[1].kind,
1122 PlanEntityKind::BridgeRx { .. }
1123 ));
1124 assert!(matches!(
1125 plan.entities[2].kind,
1126 PlanEntityKind::BridgeTx { .. }
1127 ));
1128 }
1129
1130 #[test]
1131 fn synthesizes_declared_unconnected_output() {
1132 let config = config(
1133 r#"(
1134 tasks: [(id: "generated", type: "demo::Generated", kind: source)],
1135 cnx: [],
1136 )"#,
1137 );
1138 let graph = config.get_graph(None).unwrap();
1139 let plan = assemble_runtime_plan(&config, graph).unwrap();
1140 let CuExecutionUnit::Step(step) = &plan.execution.steps[0] else {
1141 panic!("expected one generated step")
1142 };
1143 let output = step.output_msg_pack.as_ref().unwrap();
1144 assert_eq!(output.culist_index, 0);
1145 assert!(output.msg_types[0].contains("CuSingleOutputMsg"));
1146 assert!(output.msg_types[0].contains("CuSrcTask"));
1147 }
1148
1149 #[test]
1150 fn synthesizes_declared_stateless_output() {
1151 let config = config(
1152 r#"(
1153 tasks: [
1154 (id: "src", type: "demo::Source"),
1155 (id: "generated", type: "demo::Generated", kind: stateless_task),
1156 ],
1157 cnx: [(src: "src", dst: "generated", msg: "u32")],
1158 )"#,
1159 );
1160 let graph = config.get_graph(None).unwrap();
1161 let plan = assemble_runtime_plan(&config, graph).unwrap();
1162 let CuExecutionUnit::Step(step) = &plan.execution.steps[1] else {
1163 panic!("expected generated stateless step")
1164 };
1165 let output = step.output_msg_pack.as_ref().unwrap();
1166 assert!(output.msg_types[0].contains("CuSingleOutputMsg"));
1167 assert!(output.msg_types[0].contains("CuStatelessTask"));
1168 }
1169
1170 fn task_order_graph(planner: &str) -> CuConfig {
1175 config(&format!(
1176 r#"(
1177 tasks: [
1178 (id: "cam", type: "demo::Cam"),
1179 (id: "ekf", type: "demo::Ekf"),
1180 (id: "motor", type: "demo::Motor"),
1181 ],
1182 bridges: [(
1183 id: "radio",
1184 type: "demo::Radio",
1185 channels: [Rx(id: "incoming"), Tx(id: "outgoing")],
1186 )],
1187 cnx: [
1188 (src: "radio/incoming", dst: "cam", msg: "demo::In"),
1189 (src: "cam", dst: "ekf", msg: "demo::Frame"),
1190 (src: "ekf", dst: "motor", msg: "demo::State"),
1191 (src: "motor", dst: "radio/outgoing", msg: "demo::Cmd"),
1192 ],
1193 runtime: (planner: {planner}),
1194 )"#
1195 ))
1196 }
1197
1198 fn task_order(ids: &[&str]) -> String {
1199 let quoted: Vec<String> = ids.iter().map(|id| format!("{id:?}")).collect();
1200 format!(
1201 r#"(kind: TaskOrder, config: {{ "order": [{}] }})"#,
1202 quoted.join(", ")
1203 )
1204 }
1205
1206 #[test]
1207 fn task_order_plan_weaves_bridge_stages_and_matches_task_order() {
1208 let config = task_order_graph(&task_order(&["cam", "ekf", "motor"]));
1209 let graph = config.get_graph(None).unwrap();
1210 let plan = assemble_runtime_plan(&config, graph).unwrap();
1211 assert_eq!(
1212 step_labels(&plan),
1213 [
1214 "radio::rx::incoming",
1215 "cam",
1216 "ekf",
1217 "motor",
1218 "radio::tx::outgoing"
1219 ]
1220 );
1221 }
1222
1223 #[test]
1224 fn task_order_plan_rejects_bad_id_lists() {
1225 let rejects = |planner: &str| {
1226 let config = task_order_graph(planner);
1227 let graph = config.get_graph(None).unwrap();
1228 assemble_runtime_plan(&config, graph)
1229 .err()
1230 .unwrap()
1231 .to_string()
1232 };
1233
1234 let err = rejects(&task_order(&["cam", "ekf"]));
1235 assert!(err.contains("missing"), "{err}");
1236
1237 let err = rejects(&task_order(&["radio::rx::incoming", "cam", "ekf", "motor"]));
1238 assert!(err.contains("bridge stage"), "{err}");
1239
1240 let err = rejects(&task_order(&["cam", "ekf", "motor", "ghost"]));
1241 assert!(err.contains("unknown task 'ghost'"), "{err}");
1242
1243 let err = rejects(&task_order(&["cam", "cam", "ekf"]));
1244 assert!(err.contains("more than once"), "{err}");
1245
1246 let err = rejects(r#"(kind: TaskOrder)"#);
1247 assert!(err.contains("needs config"), "{err}");
1248 }
1249
1250 #[test]
1251 fn check_order_flags_precedence_and_missing() {
1252 let config = build_config(&["s", "k"], &[("s", "k", "m")]);
1253 let graph = config.get_graph(None).unwrap();
1254 let s = graph.get_node_id_by_name("s").unwrap();
1255 let k = graph.get_node_id_by_name("k").unwrap();
1256
1257 let err = check_order(graph, &StepOrder(vec![k, s])).unwrap_err();
1259 assert!(
1260 err.to_string().contains("scheduled before its input"),
1261 "{err}"
1262 );
1263
1264 let err = check_order(graph, &StepOrder(vec![s])).unwrap_err();
1266 assert!(err.to_string().contains("Missing"), "{err}");
1267 }
1268
1269 fn build_config(nodes: &[&str], edges: &[(&str, &str, &str)]) -> CuConfig {
1273 let mut config = CuConfig::default();
1274 let graph = config.get_graph_mut(None).unwrap();
1275 let mut ids: BTreeMap<String, NodeId> = BTreeMap::new();
1276 for &name in nodes {
1277 let id = graph.add_node(Node::new(name, "demo::T")).unwrap();
1278 ids.insert(name.to_string(), id);
1279 }
1280 for &(src, dst, msg) in edges {
1281 graph.connect(ids[src], ids[dst], msg).unwrap();
1282 }
1283 config
1284 }
1285
1286 fn corpus() -> Vec<(String, CuConfig)> {
1289 let mut cases = vec![
1292 (
1293 "chain".to_string(),
1294 build_config(
1295 &["s", "r1", "r2", "k"],
1296 &[("s", "r1", "m0"), ("r1", "r2", "m1"), ("r2", "k", "m2")],
1297 ),
1298 ),
1299 (
1300 "fanout_shared_msg".to_string(),
1301 build_config(
1302 &["s", "a", "b", "c"],
1303 &[("s", "a", "m"), ("s", "b", "m"), ("s", "c", "n")],
1304 ),
1305 ),
1306 (
1307 "fanin_multisource".to_string(),
1308 build_config(
1309 &["s1", "s2", "s3", "k"],
1310 &[("s2", "k", "m2"), ("s1", "k", "m1"), ("s3", "k", "m3")],
1311 ),
1312 ),
1313 (
1314 "diamond".to_string(),
1315 build_config(
1316 &["s", "a", "b", "j", "k"],
1317 &[
1318 ("s", "a", "m0"),
1319 ("s", "b", "m1"),
1320 ("a", "j", "ma"),
1321 ("b", "j", "mb"),
1322 ("j", "k", "mj"),
1323 ],
1324 ),
1325 ),
1326 (
1327 "bridge_like".to_string(),
1328 build_config(
1329 &["rx1", "rx2", "r", "tx1", "tx2"],
1330 &[
1331 ("rx1", "r", "m1"),
1332 ("rx2", "r", "m2"),
1333 ("r", "tx1", "o1"),
1334 ("r", "tx2", "o2"),
1335 ],
1336 ),
1337 ),
1338 (
1339 "multisource_layers".to_string(),
1340 build_config(
1341 &["s1", "s2", "r1", "r2", "k"],
1342 &[
1343 ("s1", "r1", "a"),
1344 ("s2", "r1", "b"),
1345 ("s1", "r2", "c"),
1346 ("s2", "r2", "d"),
1347 ("r1", "k", "e"),
1348 ("r2", "k", "f"),
1349 ],
1350 ),
1351 ),
1352 (
1353 "side_branch".to_string(),
1354 build_config(
1355 &["s", "r1", "r2", "r3", "k1", "k2"],
1356 &[
1357 ("s", "r1", "m0"),
1358 ("r1", "r2", "m1"),
1359 ("r1", "r3", "m2"),
1360 ("r2", "k1", "m3"),
1361 ("r3", "k2", "m4"),
1362 ],
1363 ),
1364 ),
1365 ];
1366 for seed in 0u64..6 {
1368 cases.push((format!("layered_{seed}"), layered_dag(seed)));
1369 }
1370 cases
1371 }
1372
1373 fn layered_dag(seed: u64) -> CuConfig {
1376 let layers = [2usize, 3, 3, 2];
1377 let mut state = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
1378 let mut next = || {
1379 state = state
1380 .wrapping_mul(6364136223846793005)
1381 .wrapping_add(1442695040888963407);
1382 state >> 33
1383 };
1384 let name = |layer: usize, idx: usize| format!("n{layer}_{idx}");
1385 let mut nodes = Vec::new();
1386 for (layer, count) in layers.iter().enumerate() {
1387 for idx in 0..*count {
1388 nodes.push(name(layer, idx));
1389 }
1390 }
1391 let node_refs: Vec<&str> = nodes.iter().map(|s| s.as_str()).collect();
1392 let mut edges: Vec<(String, String, String)> = Vec::new();
1393 for layer in 0..layers.len() - 1 {
1394 for from in 0..layers[layer] {
1395 let mut connected = false;
1397 for to in 0..layers[layer + 1] {
1398 if next() % 2 == 0 || (to == layers[layer + 1] - 1 && !connected) {
1399 edges.push((
1400 name(layer, from),
1401 name(layer + 1, to),
1402 format!("m{layer}_{from}_{to}"),
1403 ));
1404 connected = true;
1405 }
1406 }
1407 }
1408 for to in 0..layers[layer + 1] {
1410 if !edges.iter().any(|(_, d, _)| *d == name(layer + 1, to)) {
1411 edges.push((
1412 name(layer, 0),
1413 name(layer + 1, to),
1414 format!("f{layer}_{to}"),
1415 ));
1416 }
1417 }
1418 }
1419 let edge_refs: Vec<(&str, &str, &str)> = edges
1420 .iter()
1421 .map(|(s, d, m)| (s.as_str(), d.as_str(), m.as_str()))
1422 .collect();
1423 build_config(&node_refs, &edge_refs)
1424 }
1425
1426 fn assert_same_plan(name: &str, config: &CuConfig) {
1429 let graph = config.get_graph(None).unwrap();
1430 let legacy = compute_runtime_plan_legacy(graph).expect("legacy plan");
1431 let fresh = crate::curuntime::compute_runtime_plan(graph).expect("new plan");
1432 assert_eq!(
1433 legacy.steps.len(),
1434 fresh.steps.len(),
1435 "{name}: step count differs"
1436 );
1437 for (index, (a, b)) in legacy.steps.iter().zip(fresh.steps.iter()).enumerate() {
1438 let (CuExecutionUnit::Step(a), CuExecutionUnit::Step(b)) = (a, b) else {
1439 panic!("{name}: unexpected nested loop");
1440 };
1441 assert_eq!(a.node_id, b.node_id, "{name}: step {index} node id");
1442 assert_eq!(a.phase, b.phase, "{name}: step {index} phase");
1443 let (oa, ob) = (a.output_msg_pack.as_ref(), b.output_msg_pack.as_ref());
1444 assert_eq!(
1445 oa.map(|p| p.culist_index),
1446 ob.map(|p| p.culist_index),
1447 "{name}: step {index} culist index"
1448 );
1449 assert_eq!(
1450 oa.map(|p| &p.msg_types),
1451 ob.map(|p| &p.msg_types),
1452 "{name}: step {index} output msg types"
1453 );
1454 assert_eq!(
1455 a.input_msg_indices_types.len(),
1456 b.input_msg_indices_types.len(),
1457 "{name}: step {index} input arity"
1458 );
1459 for (ia, ib) in a
1460 .input_msg_indices_types
1461 .iter()
1462 .zip(b.input_msg_indices_types.iter())
1463 {
1464 assert_eq!(ia.culist_index, ib.culist_index, "{name}: input culist");
1465 assert_eq!(ia.msg_type, ib.msg_type, "{name}: input msg");
1466 assert_eq!(ia.src_port, ib.src_port, "{name}: input src_port");
1467 assert_eq!(ia.edge_id, ib.edge_id, "{name}: input edge_id");
1468 assert_eq!(
1469 ia.connection_order, ib.connection_order,
1470 "{name}: input connection_order"
1471 );
1472 }
1473 }
1474 }
1475
1476 #[test]
1477 fn topo_bfs_matches_legacy_walk_over_corpus() {
1478 for (name, config) in corpus() {
1479 assert_same_plan(&name, &config);
1480 }
1481 }
1482
1483 fn find_output_pack_from_nodeid_legacy(
1485 node_id: NodeId,
1486 steps: &[CuExecutionUnit],
1487 ) -> Option<CuOutputPack> {
1488 for step in steps {
1489 match step {
1490 CuExecutionUnit::Loop(loop_unit) => {
1491 if let Some(pack) =
1492 find_output_pack_from_nodeid_legacy(node_id, &loop_unit.steps)
1493 {
1494 return Some(pack);
1495 }
1496 }
1497 CuExecutionUnit::Step(step) if step.node_id == node_id => {
1498 return step.output_msg_pack.clone();
1499 }
1500 _ => {}
1501 }
1502 }
1503 None
1504 }
1505
1506 fn plan_tasks_tree_branch_legacy(
1507 graph: &CuGraph,
1508 mut next_culist_output_index: u32,
1509 starting_point: NodeId,
1510 plan: &mut Vec<CuExecutionUnit>,
1511 ) -> CuResult<(u32, bool)> {
1512 let mut handled = false;
1513 for id in graph.bfs_nodes(starting_point) {
1514 let node_ref = graph.get_node(id).unwrap();
1515 let mut input_msg_indices_types: Vec<CuInputMsg> = Vec::new();
1516 let output_msg_pack: Option<CuOutputPack>;
1517 let task_type = find_task_type_for_id(graph, id)?;
1518 match task_type {
1519 CuTaskType::Source => {
1520 let msg_types = graph.get_node_output_msg_types_by_id(id)?;
1521 if msg_types.is_empty() {
1522 return Err(CuError::from(format!(
1523 "Source node '{}' has no declared outputs",
1524 node_ref.get_id()
1525 )));
1526 }
1527 output_msg_pack = Some(CuOutputPack {
1528 culist_index: next_culist_output_index,
1529 src_channels: vec![None; msg_types.len()],
1530 msg_types,
1531 });
1532 next_culist_output_index += 1;
1533 }
1534 CuTaskType::Sink => {
1535 let mut edge_ids = graph.get_dst_edges(id).unwrap_or_default();
1536 edge_ids.sort();
1537 for edge_id in edge_ids {
1538 let edge = graph
1539 .edge(edge_id)
1540 .unwrap_or_else(|| panic!("Missing edge {edge_id} for node {id}"));
1541 let pid =
1542 graph
1543 .get_node_id_by_name(edge.src.as_str())
1544 .unwrap_or_else(|| {
1545 panic!("Missing source node '{}' for edge {edge_id}", edge.src)
1546 });
1547 let output_pack = find_output_pack_from_nodeid_legacy(pid, plan);
1548 if let Some(output_pack) = output_pack {
1549 let msg_type = edge.msg.as_str();
1550 let src_port = output_pack
1551 .msg_types
1552 .iter()
1553 .position(|msg| msg == msg_type)
1554 .unwrap_or_else(|| {
1555 panic!(
1556 "Missing output port for message type '{msg_type}' on node {pid}"
1557 )
1558 });
1559 input_msg_indices_types.push(CuInputMsg {
1560 culist_index: output_pack.culist_index,
1561 msg_type: msg_type.to_string(),
1562 src_port,
1563 edge_id,
1564 connection_order: edge.order,
1565 });
1566 } else {
1567 return Ok((next_culist_output_index, handled));
1568 }
1569 }
1570 output_msg_pack = Some(CuOutputPack {
1571 culist_index: next_culist_output_index,
1572 msg_types: Vec::from(["()".to_string()]),
1573 src_channels: vec![None],
1574 });
1575 next_culist_output_index += 1;
1576 }
1577 CuTaskType::Regular => {
1578 let mut edge_ids = graph.get_dst_edges(id).unwrap_or_default();
1579 edge_ids.sort();
1580 for edge_id in edge_ids {
1581 let edge = graph
1582 .edge(edge_id)
1583 .unwrap_or_else(|| panic!("Missing edge {edge_id} for node {id}"));
1584 let pid =
1585 graph
1586 .get_node_id_by_name(edge.src.as_str())
1587 .unwrap_or_else(|| {
1588 panic!("Missing source node '{}' for edge {edge_id}", edge.src)
1589 });
1590 let output_pack = find_output_pack_from_nodeid_legacy(pid, plan);
1591 if let Some(output_pack) = output_pack {
1592 let msg_type = edge.msg.as_str();
1593 let src_port = output_pack
1594 .msg_types
1595 .iter()
1596 .position(|msg| msg == msg_type)
1597 .unwrap_or_else(|| {
1598 panic!(
1599 "Missing output port for message type '{msg_type}' on node {pid}"
1600 )
1601 });
1602 input_msg_indices_types.push(CuInputMsg {
1603 culist_index: output_pack.culist_index,
1604 msg_type: msg_type.to_string(),
1605 src_port,
1606 edge_id,
1607 connection_order: edge.order,
1608 });
1609 } else {
1610 return Ok((next_culist_output_index, handled));
1611 }
1612 }
1613 let msg_types = graph.get_node_output_msg_types_by_id(id)?;
1614 if msg_types.is_empty() {
1615 return Err(CuError::from(format!(
1616 "Regular node '{}' has no declared outputs",
1617 node_ref.get_id()
1618 )));
1619 }
1620 output_msg_pack = Some(CuOutputPack {
1621 culist_index: next_culist_output_index,
1622 src_channels: vec![None; msg_types.len()],
1623 msg_types,
1624 });
1625 next_culist_output_index += 1;
1626 }
1627 }
1628
1629 sort_inputs_by_connection_order(&mut input_msg_indices_types);
1630 if let Some(pos) = plan
1631 .iter()
1632 .position(|step| matches!(step, CuExecutionUnit::Step(s) if s.node_id == id))
1633 {
1634 let mut step = plan.remove(pos);
1635 if let CuExecutionUnit::Step(ref mut s) = step {
1636 s.input_msg_indices_types = input_msg_indices_types;
1637 }
1638 plan.push(step);
1639 } else {
1640 let step = CuExecutionStep {
1641 node_id: id,
1642 node: node_ref.clone(),
1643 task_type,
1644 phase: CuStepPhase::default(),
1645 input_msg_indices_types,
1646 output_msg_pack,
1647 };
1648 plan.push(CuExecutionUnit::Step(Box::new(step)));
1649 }
1650 handled = true;
1651 }
1652 Ok((next_culist_output_index, handled))
1653 }
1654
1655 fn compute_runtime_plan_legacy(graph: &CuGraph) -> CuResult<CuExecutionLoop> {
1658 let mut plan = Vec::new();
1659 let mut next_culist_output_index = 0u32;
1660 let mut queue: VecDeque<NodeId> = VecDeque::new();
1661 for node_id in graph.node_ids() {
1662 if find_task_type_for_id(graph, node_id)? == CuTaskType::Source {
1663 queue.push_back(node_id);
1664 }
1665 }
1666 while let Some(start_node) = queue.pop_front() {
1667 for node_id in graph.bfs_nodes(start_node) {
1668 let already = plan
1669 .iter()
1670 .any(|unit| matches!(unit, CuExecutionUnit::Step(s) if s.node_id == node_id));
1671 if already {
1672 continue;
1673 }
1674 let (new_index, handled) = plan_tasks_tree_branch_legacy(
1675 graph,
1676 next_culist_output_index,
1677 node_id,
1678 &mut plan,
1679 )?;
1680 next_culist_output_index = new_index;
1681 if !handled {
1682 continue;
1683 }
1684 for neighbor in graph.get_neighbor_ids(node_id, CuDirection::Outgoing) {
1685 queue.push_back(neighbor);
1686 }
1687 }
1688 }
1689 let mut planned_nodes = BTreeSet::new();
1690 for unit in &plan {
1691 if let CuExecutionUnit::Step(step) = unit {
1692 planned_nodes.insert(step.node_id);
1693 }
1694 }
1695 let mut missing = Vec::new();
1696 for node_id in graph.node_ids() {
1697 if !planned_nodes.contains(&node_id) {
1698 if let Some(node) = graph.get_node(node_id) {
1699 missing.push(node.get_id().to_string());
1700 } else {
1701 missing.push(format!("node_id_{node_id}"));
1702 }
1703 }
1704 }
1705 if !missing.is_empty() {
1706 missing.sort();
1707 return Err(CuError::from(format!(
1708 "Execution plan could not include all nodes. Missing: {}. Check for loopback or missing source connections.",
1709 missing.join(", ")
1710 )));
1711 }
1712 Ok(CuExecutionLoop {
1713 steps: plan,
1714 loop_count: None,
1715 })
1716 }
1717}