1use crate::config::{
11 BridgeChannelConfigRepresentation, ComponentConfig, ConfigGraphs, CuConfig, CuDirection,
12 CuGraph, Flavor, Node, NodeId,
13};
14use crate::curuntime::{
15 CuExecutionLoop, CuExecutionStep, CuExecutionUnit, CuInputMsg, CuOutputPack, CuStepPhase,
16 CuTaskType, expand_anytime_steps, find_task_type_for_id,
17};
18use alloc::boxed::Box;
19use alloc::collections::{BTreeMap, BTreeSet, VecDeque};
20use alloc::format;
21use alloc::string::{String, ToString};
22use alloc::vec;
23use alloc::vec::Vec;
24use cu29_traits::{CuError, CuResult};
25use serde::{Deserialize, Serialize};
26
27#[doc(hidden)]
32pub const DEFAULT_COPPERLIST_COUNT: usize = 2;
33
34#[doc(hidden)]
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum PlanEntityKind {
38 Task {
39 original_node_id: NodeId,
40 task_index: usize,
41 },
42 BridgeRx {
43 bridge_config_index: usize,
44 channel_config_index: usize,
45 },
46 BridgeTx {
47 bridge_config_index: usize,
48 channel_config_index: usize,
49 },
50}
51
52#[doc(hidden)]
54#[derive(Clone, Debug, PartialEq, Eq)]
55pub struct PlanEntity {
56 pub key: String,
57 pub label: String,
58 pub kind: PlanEntityKind,
59}
60
61#[doc(hidden)]
63pub struct AssembledPlan {
64 pub execution: CuExecutionLoop,
65 pub entities: Vec<PlanEntity>,
67 pub plan_to_original: Vec<Option<NodeId>>,
69}
70
71#[derive(Clone, Debug, PartialEq, Eq)]
73pub struct StepOrder(pub Vec<NodeId>);
74
75pub trait CuPlanner {
89 fn new(config: Option<&ComponentConfig>) -> CuResult<Self>
91 where
92 Self: Sized;
93
94 fn plan(&self, graph: &CuGraph) -> CuResult<StepOrder>;
96}
97
98const LINEARITY_PLANNER: &str = "cu29::planner::Linearity";
100
101const PINNED_PLANNER: &str = "cu29::planner::Pinned";
103
104#[derive(Default)]
108pub struct Linearity;
109
110impl CuPlanner for Linearity {
111 fn new(_config: Option<&ComponentConfig>) -> CuResult<Self> {
112 Ok(Linearity)
113 }
114
115 fn plan(&self, graph: &CuGraph) -> CuResult<StepOrder> {
116 topo_bfs_order(graph)
117 }
118}
119
120pub struct Pinned {
125 order: Vec<String>,
126}
127
128impl CuPlanner for Pinned {
129 fn new(config: Option<&ComponentConfig>) -> CuResult<Self> {
130 const NEEDS_ORDER: &str = "The Pinned planner needs config: { \"order\": [..task ids..] }";
131 let order = config
132 .ok_or(CuError::from(NEEDS_ORDER))?
133 .get_value::<Vec<String>>("order")
134 .map_err(|e| CuError::from(format!("Pinned planner: {e}")))?
135 .ok_or(CuError::from(NEEDS_ORDER))?;
136 Ok(Pinned { order })
137 }
138
139 fn plan(&self, graph: &CuGraph) -> CuResult<StepOrder> {
140 pinned_order(graph, &resolve_pinned_ids(graph, &self.order)?)
141 }
142}
143
144fn instantiate_builtin_planner(
146 type_path: &str,
147 config: Option<&ComponentConfig>,
148) -> CuResult<Option<Box<dyn CuPlanner>>> {
149 Ok(Some(match type_path {
150 LINEARITY_PLANNER => Box::new(Linearity::new(config)?),
151 PINNED_PLANNER => Box::new(Pinned::new(config)?),
152 _ => return Ok(None),
153 }))
154}
155
156#[doc(hidden)]
158pub const BUILTIN_PLANNERS: [&str; 2] = [LINEARITY_PLANNER, PINNED_PLANNER];
159
160#[doc(hidden)]
162pub fn is_builtin_planner(type_path: &str) -> bool {
163 BUILTIN_PLANNERS.contains(&type_path)
164}
165
166fn resolve_pinned_ids(graph: &CuGraph, ids: &[String]) -> CuResult<Vec<NodeId>> {
169 let mut task_ids: BTreeMap<String, NodeId> = BTreeMap::new();
170 let mut bridge_labels: BTreeSet<String> = BTreeSet::new();
171 for (node_id, node) in graph.get_all_nodes() {
172 match node.get_flavor() {
173 Flavor::Task => {
174 task_ids.insert(node.get_id(), node_id);
175 }
176 Flavor::Bridge => {
177 bridge_labels.insert(node.get_id());
178 }
179 }
180 }
181 let valid_ids = || {
182 let mut names: Vec<String> = task_ids.keys().cloned().collect();
183 names.sort();
184 names.join(", ")
185 };
186
187 let mut resolved = Vec::with_capacity(ids.len());
188 let mut seen: BTreeSet<NodeId> = BTreeSet::new();
189 for id in ids {
190 if let Some(&node_id) = task_ids.get(id) {
191 if !seen.insert(node_id) {
192 return Err(CuError::from(format!(
193 "Pinned plan lists task '{id}' more than once."
194 )));
195 }
196 resolved.push(node_id);
197 } else if bridge_labels.contains(id) {
198 return Err(CuError::from(format!(
199 "Pinned plan lists bridge stage '{id}'; pin only task ids: [{}].",
200 valid_ids()
201 )));
202 } else {
203 return Err(CuError::from(format!(
204 "Pinned plan lists unknown task '{id}'; valid task ids: [{}].",
205 valid_ids()
206 )));
207 }
208 }
209
210 if resolved.len() != task_ids.len() {
211 let mut missing: Vec<String> = task_ids
212 .iter()
213 .filter(|(_, node_id)| !seen.contains(node_id))
214 .map(|(name, _)| name.clone())
215 .collect();
216 missing.sort();
217 return Err(CuError::from(format!(
218 "Pinned plan must list every task exactly once; missing: [{}].",
219 missing.join(", ")
220 )));
221 }
222
223 Ok(resolved)
224}
225
226fn topo_bfs_order(graph: &CuGraph) -> CuResult<StepOrder> {
235 #[cfg(all(feature = "std", feature = "macro_debug"))]
236 eprintln!("[step order: Linearity]");
237 let mut order: Vec<NodeId> = Vec::new();
238 let mut planned: BTreeSet<NodeId> = BTreeSet::new();
239
240 let mut queue: VecDeque<NodeId> = VecDeque::new();
241 for node_id in graph.node_ids() {
242 if find_task_type_for_id(graph, node_id)? == CuTaskType::Source {
243 queue.push_back(node_id);
244 }
245 }
246 #[cfg(all(feature = "std", feature = "macro_debug"))]
247 eprintln!("Initial source nodes: {queue:?}");
248
249 while let Some(start_node) = queue.pop_front() {
250 #[cfg(all(feature = "std", feature = "macro_debug"))]
251 eprintln!("→ Starting BFS from source {start_node}");
252 for node_id in graph.bfs_nodes(start_node) {
253 if planned.contains(&node_id) {
254 continue;
255 }
256 if topo_bfs_branch(graph, node_id, &mut order, &mut planned)? {
257 for neighbor in graph.get_neighbor_ids(node_id, CuDirection::Outgoing) {
258 queue.push_back(neighbor);
259 }
260 }
261 }
262 }
263
264 Ok(StepOrder(order))
265}
266
267fn topo_bfs_branch(
271 graph: &CuGraph,
272 starting_point: NodeId,
273 order: &mut Vec<NodeId>,
274 planned: &mut BTreeSet<NodeId>,
275) -> CuResult<bool> {
276 #[cfg(all(feature = "std", feature = "macro_debug"))]
277 eprintln!("-- starting branch from node {starting_point}");
278 let mut handled = false;
279 for id in graph.bfs_nodes(starting_point) {
280 #[cfg(all(feature = "std", feature = "macro_debug"))]
281 eprintln!(" Visiting node: {:?}", graph.get_node(id));
282 if find_task_type_for_id(graph, id)? != CuTaskType::Source {
283 let mut edge_ids = graph.get_dst_edges(id).unwrap_or_default();
284 edge_ids.sort();
285 let mut ready = true;
286 for edge_id in edge_ids {
287 let edge = graph
288 .edge(edge_id)
289 .unwrap_or_else(|| panic!("Missing edge {edge_id} for node {id}"));
290 let pid = graph
291 .get_node_id_by_name(edge.src.as_str())
292 .unwrap_or_else(|| {
293 panic!("Missing source node '{}' for edge {edge_id}", edge.src)
294 });
295 if !planned.contains(&pid) {
296 #[cfg(all(feature = "std", feature = "macro_debug"))]
297 eprintln!(" ✗ Input from {pid} not ready, returning");
298 ready = false;
299 break;
300 }
301 }
302 if !ready {
303 return Ok(handled);
304 }
305 }
306 if planned.contains(&id) {
312 unreachable!("plan re-visit path reached for node {id}");
313 }
314 #[cfg(all(feature = "std", feature = "macro_debug"))]
315 eprintln!(" → Node {id} added to the order");
316 order.push(id);
317 planned.insert(id);
318 handled = true;
319 }
320 #[cfg(all(feature = "std", feature = "macro_debug"))]
321 eprintln!("-- finished branch from node {starting_point} with handled={handled}");
322 Ok(handled)
323}
324
325fn pinned_order(graph: &CuGraph, pinned_tasks: &[NodeId]) -> CuResult<StepOrder> {
333 let mut task_position: BTreeMap<NodeId, usize> = BTreeMap::new();
334 for (position, &task) in pinned_tasks.iter().enumerate() {
335 task_position.insert(task, position);
336 }
337
338 let mut before: BTreeMap<usize, Vec<NodeId>> = BTreeMap::new();
339 let mut after: BTreeMap<usize, Vec<NodeId>> = BTreeMap::new();
340 let mut leading: Vec<NodeId> = Vec::new();
341 let mut trailing: Vec<NodeId> = Vec::new();
342
343 for (node_id, node) in graph.get_all_nodes() {
344 if node.get_flavor() != Flavor::Bridge {
345 continue;
346 }
347 match find_task_type_for_id(graph, node_id)? {
348 CuTaskType::Source => {
349 match graph
350 .get_neighbor_ids(node_id, CuDirection::Outgoing)
351 .into_iter()
352 .filter_map(|consumer| task_position.get(&consumer).copied())
353 .min()
354 {
355 Some(pos) => before.entry(pos).or_default().push(node_id),
356 None => leading.push(node_id),
357 }
358 }
359 CuTaskType::Sink => {
360 match graph
361 .get_neighbor_ids(node_id, CuDirection::Incoming)
362 .into_iter()
363 .filter_map(|producer| task_position.get(&producer).copied())
364 .max()
365 {
366 Some(pos) => after.entry(pos).or_default().push(node_id),
367 None => trailing.push(node_id),
368 }
369 }
370 CuTaskType::Regular => trailing.push(node_id),
371 }
372 }
373
374 for stages in before.values_mut() {
375 stages.sort_unstable();
376 }
377 for stages in after.values_mut() {
378 stages.sort_unstable();
379 }
380 leading.sort_unstable();
381 trailing.sort_unstable();
382
383 let mut order = Vec::new();
384 order.append(&mut leading);
385 for (position, &task) in pinned_tasks.iter().enumerate() {
386 if let Some(stages) = before.get(&position) {
387 order.extend(stages.iter().copied());
388 }
389 order.push(task);
390 if let Some(stages) = after.get(&position) {
391 order.extend(stages.iter().copied());
392 }
393 }
394 order.append(&mut trailing);
395
396 Ok(StepOrder(order))
397}
398
399pub(crate) fn check_order(graph: &CuGraph, order: &StepOrder) -> CuResult<()> {
405 let mut position: Vec<Option<usize>> = vec![None; graph.node_count()];
406
407 for (index, &node_id) in order.0.iter().enumerate() {
408 let slot = position.get_mut(node_id as usize).ok_or_else(|| {
409 CuError::from(format!("Plan order references unknown node id {node_id}."))
410 })?;
411 if slot.is_some() {
412 return Err(CuError::from(format!(
413 "Task '{}' appears more than once in the plan order.",
414 node_name(graph, node_id)
415 )));
416 }
417 *slot = Some(index);
418 }
419
420 let mut missing: Vec<String> = Vec::new();
421 for node_id in graph.node_ids() {
422 if position[node_id as usize].is_none() {
423 missing.push(node_name(graph, node_id));
424 }
425 }
426 if !missing.is_empty() {
427 missing.sort();
428 return Err(CuError::from(format!(
429 "Execution plan could not include all nodes. Missing: {}. Check for loopback or missing source connections.",
430 missing.join(", ")
431 )));
432 }
433
434 for edge in graph.edges() {
435 let (Some(src), Some(dst)) = (
436 graph.get_node_id_by_name(edge.src.as_str()),
437 graph.get_node_id_by_name(edge.dst.as_str()),
438 ) else {
439 continue;
440 };
441 if position[src as usize] >= position[dst as usize] {
442 return Err(CuError::from(format!(
443 "Task '{}' is scheduled before its input '{}'.",
444 node_name(graph, dst),
445 node_name(graph, src)
446 )));
447 }
448 }
449
450 Ok(())
451}
452
453pub(crate) fn plan_from_order(graph: &CuGraph, order: &StepOrder) -> CuResult<CuExecutionLoop> {
458 #[cfg(all(feature = "std", feature = "macro_debug"))]
459 eprintln!("[runtime plan]");
460 let mut plan: Vec<CuExecutionUnit> = Vec::new();
461 let mut next_culist_output_index = 0u32;
462
463 for &id in &order.0 {
464 let node_ref = graph
465 .get_node(id)
466 .ok_or_else(|| CuError::from(format!("Node id {id} not found")))?;
467 let task_type = find_task_type_for_id(graph, id)?;
468 let mut input_msg_indices_types = if task_type == CuTaskType::Source {
469 Vec::new()
470 } else {
471 collect_step_inputs(graph, id, &plan)?
472 };
473 #[cfg(all(feature = "std", feature = "macro_debug"))]
474 eprintln!(
475 " {task_type:?} node {id} → output index {next_culist_output_index}, inputs {input_msg_indices_types:?}"
476 );
477 let output_msg_pack: Option<CuOutputPack>;
478
479 match task_type {
480 CuTaskType::Source => {
481 let msg_types = graph.get_node_output_msg_types_by_id(id)?;
482 if msg_types.is_empty() {
483 return Err(CuError::from(format!(
484 "Source node '{}' has no declared outputs",
485 node_ref.get_id()
486 )));
487 }
488 output_msg_pack = Some(CuOutputPack {
489 culist_index: next_culist_output_index,
490 msg_types,
491 });
492 next_culist_output_index += 1;
493 }
494 CuTaskType::Sink => {
495 output_msg_pack = Some(CuOutputPack {
496 culist_index: next_culist_output_index,
497 msg_types: Vec::from(["()".to_string()]),
498 });
499 next_culist_output_index += 1;
500 }
501 CuTaskType::Regular => {
502 let msg_types = graph.get_node_output_msg_types_by_id(id)?;
503 if msg_types.is_empty() {
504 return Err(CuError::from(format!(
505 "Regular node '{}' has no declared outputs",
506 node_ref.get_id()
507 )));
508 }
509 output_msg_pack = Some(CuOutputPack {
510 culist_index: next_culist_output_index,
511 msg_types,
512 });
513 next_culist_output_index += 1;
514 }
515 }
516
517 sort_inputs_by_connection_order(&mut input_msg_indices_types);
518 plan.push(CuExecutionUnit::Step(Box::new(CuExecutionStep {
519 node_id: id,
520 node: node_ref.clone(),
521 task_type,
522 phase: CuStepPhase::default(),
523 input_msg_indices_types,
524 output_msg_pack,
525 })));
526 }
527
528 Ok(CuExecutionLoop {
529 steps: plan,
530 loop_count: None,
531 })
532}
533
534fn collect_step_inputs(
539 graph: &CuGraph,
540 id: NodeId,
541 plan: &[CuExecutionUnit],
542) -> CuResult<Vec<CuInputMsg>> {
543 let mut inputs = Vec::new();
544 let mut edge_ids = graph.get_dst_edges(id).unwrap_or_default();
545 edge_ids.sort();
546 for edge_id in edge_ids {
547 let edge = graph
548 .edge(edge_id)
549 .unwrap_or_else(|| panic!("Missing edge {edge_id} for node {id}"));
550 let pid = graph
551 .get_node_id_by_name(edge.src.as_str())
552 .unwrap_or_else(|| panic!("Missing source node '{}' for edge {edge_id}", edge.src));
553 let output_pack = find_output_pack_from_nodeid(pid, plan).ok_or_else(|| {
554 CuError::from(format!(
555 "Plan materialization: input from node {pid} is not available before node {id}"
556 ))
557 })?;
558 let msg_type = edge.msg.as_str();
559 let src_port = output_pack
560 .msg_types
561 .iter()
562 .position(|msg| msg == msg_type)
563 .unwrap_or_else(|| {
564 panic!("Missing output port for message type '{msg_type}' on node {pid}")
565 });
566 inputs.push(CuInputMsg {
567 culist_index: output_pack.culist_index,
568 msg_type: msg_type.to_string(),
569 src_port,
570 edge_id,
571 connection_order: edge.order,
572 });
573 }
574 Ok(inputs)
575}
576
577fn find_output_pack_from_nodeid(
578 node_id: NodeId,
579 steps: &[CuExecutionUnit],
580) -> Option<CuOutputPack> {
581 for step in steps {
582 match step {
583 CuExecutionUnit::Loop(loop_unit) => {
584 if let Some(output_pack) = find_output_pack_from_nodeid(node_id, &loop_unit.steps) {
585 return Some(output_pack);
586 }
587 }
588 CuExecutionUnit::Step(step) if step.node_id == node_id => {
589 return step.output_msg_pack.clone();
590 }
591 _ => {}
592 }
593 }
594 None
595}
596
597fn sort_inputs_by_connection_order(input_msg_indices_types: &mut [CuInputMsg]) {
602 input_msg_indices_types.sort_by_key(|input| input.connection_order);
603}
604
605fn node_name(graph: &CuGraph, node_id: NodeId) -> String {
606 graph
607 .get_node(node_id)
608 .map(|node| node.get_id())
609 .unwrap_or_else(|| format!("node_id_{node_id}"))
610}
611
612#[derive(Clone, Copy, Debug, PartialEq, Eq)]
613enum ChannelDirection {
614 Rx,
615 Tx,
616}
617
618fn channel_is_used(
619 graph: &CuGraph,
620 bridge_id: &str,
621 channel_id: &str,
622 direction: ChannelDirection,
623) -> bool {
624 graph.edges().any(|connection| match direction {
625 ChannelDirection::Rx => {
626 connection.src == bridge_id && connection.src_channel.as_deref() == Some(channel_id)
627 }
628 ChannelDirection::Tx => {
629 connection.dst == bridge_id && connection.dst_channel.as_deref() == Some(channel_id)
630 }
631 })
632}
633
634fn inferred_output_name(node: &Node, task_type: CuTaskType) -> String {
635 let rust_type = node.get_type();
636 if node.anytime().is_some() {
637 return format!(
638 "<<{rust_type} as cu29::cutask_anytime::CuAnytimeTask>::Output<'static> as cu29::cutask::CuSingleOutputMsg>::Payload"
639 );
640 }
641 let task_trait = match task_type {
642 CuTaskType::Source => "cu29::cutask::CuSrcTask",
643 CuTaskType::Regular => "cu29::cutask::CuTask",
644 CuTaskType::Sink => unreachable!("sinks do not have inferred outputs"),
645 };
646 format!(
647 "<<{rust_type} as {task_trait}>::Output<'static> as cu29::cutask::CuSingleOutputMsg>::Payload"
648 )
649}
650
651struct PlanGraph {
653 graph: CuGraph,
654 entities: Vec<PlanEntity>,
655 plan_to_original: Vec<Option<NodeId>>,
656}
657
658fn build_plan_graph(config: &CuConfig, graph: &CuGraph) -> CuResult<PlanGraph> {
662 let mut plan_graph = CuGraph::default();
663 let mut entities = Vec::new();
664 let mut plan_to_original = Vec::new();
665 let mut original_to_plan = Vec::new();
666 original_to_plan.resize(graph.node_count(), None);
667
668 let mut task_index = 0usize;
669 for (original_node_id, node) in graph.get_all_nodes() {
670 if node.get_flavor() != Flavor::Task {
671 continue;
672 }
673 let plan_node_id = plan_graph.add_node(node.clone())?;
674 debug_assert_eq!(plan_node_id as usize, entities.len());
675 original_to_plan[original_node_id as usize] = Some(plan_node_id);
676 plan_to_original.push(Some(original_node_id));
677 entities.push(PlanEntity {
678 key: format!("task:{}", node.get_id()),
679 label: node.get_id(),
680 kind: PlanEntityKind::Task {
681 original_node_id,
682 task_index,
683 },
684 });
685 task_index += 1;
686 }
687
688 for (original_node_id, node) in graph.get_all_nodes() {
692 if node.get_flavor() != Flavor::Task || node.get_declared_task_kind().is_none() {
693 continue;
694 }
695 let task_type = find_task_type_for_id(graph, original_node_id)?;
696 if task_type == CuTaskType::Sink
697 || !graph
698 .get_node_output_msg_types_by_id(original_node_id)?
699 .is_empty()
700 {
701 continue;
702 }
703 let plan_node_id = original_to_plan[original_node_id as usize]
704 .expect("task was mirrored into the plan graph");
705 let message_type = inferred_output_name(node, task_type);
706 plan_graph
707 .get_node_mut(plan_node_id)
708 .expect("mirrored task is present")
709 .add_nc_output(&message_type, usize::MAX);
710 }
711
712 let mut channel_nodes: Vec<(usize, usize, ChannelDirection, NodeId)> = Vec::new();
715 for (bridge_config_index, bridge) in config.bridges.iter().enumerate() {
716 if graph.get_node_id_by_name(&bridge.id).is_none() {
717 continue;
718 }
719 for direction in [ChannelDirection::Rx, ChannelDirection::Tx] {
720 for (channel_config_index, channel) in bridge.channels.iter().enumerate() {
721 let (channel_id, channel_direction) = match channel {
722 BridgeChannelConfigRepresentation::Rx { id, .. } => (id, ChannelDirection::Rx),
723 BridgeChannelConfigRepresentation::Tx { id, .. } => (id, ChannelDirection::Tx),
724 };
725 if channel_direction != direction
726 || !channel_is_used(graph, &bridge.id, channel_id, direction)
727 {
728 continue;
729 }
730
731 let direction_label = match direction {
732 ChannelDirection::Rx => "rx",
733 ChannelDirection::Tx => "tx",
734 };
735 let label = format!("{}::{direction_label}::{channel_id}", bridge.id);
736 let synthetic_type = match direction {
737 ChannelDirection::Rx => "__CuBridgeRxChannel",
738 ChannelDirection::Tx => "__CuBridgeTxChannel",
739 };
740 let mut node = Node::new(&label, synthetic_type);
741 node.set_flavor(Flavor::Bridge);
742 let plan_node_id = plan_graph.add_node(node)?;
743 debug_assert_eq!(plan_node_id as usize, entities.len());
744 plan_to_original.push(None);
745 entities.push(PlanEntity {
746 key: format!("bridge:{}:{direction_label}:{channel_id}", bridge.id),
747 label,
748 kind: match direction {
749 ChannelDirection::Rx => PlanEntityKind::BridgeRx {
750 bridge_config_index,
751 channel_config_index,
752 },
753 ChannelDirection::Tx => PlanEntityKind::BridgeTx {
754 bridge_config_index,
755 channel_config_index,
756 },
757 },
758 });
759 channel_nodes.push((
760 bridge_config_index,
761 channel_config_index,
762 direction,
763 plan_node_id,
764 ));
765 }
766 }
767 }
768
769 for connection in graph.edges() {
770 let src_plan = if let Some(channel_id) = connection.src_channel.as_deref() {
771 find_channel_plan_node(
772 config,
773 &channel_nodes,
774 &connection.src,
775 channel_id,
776 ChannelDirection::Rx,
777 )?
778 } else {
779 let original_id = graph.get_node_id_by_name(&connection.src).ok_or_else(|| {
780 CuError::from(format!("Unknown source node '{}'", connection.src))
781 })?;
782 original_to_plan[original_id as usize].ok_or_else(|| {
783 CuError::from(format!("Source node '{}' is not a task", connection.src))
784 })?
785 };
786 let dst_plan = if let Some(channel_id) = connection.dst_channel.as_deref() {
787 find_channel_plan_node(
788 config,
789 &channel_nodes,
790 &connection.dst,
791 channel_id,
792 ChannelDirection::Tx,
793 )?
794 } else {
795 let original_id = graph.get_node_id_by_name(&connection.dst).ok_or_else(|| {
796 CuError::from(format!("Unknown destination node '{}'", connection.dst))
797 })?;
798 original_to_plan[original_id as usize].ok_or_else(|| {
799 CuError::from(format!(
800 "Destination node '{}' is not a task",
801 connection.dst
802 ))
803 })?
804 };
805
806 plan_graph
807 .connect_ext_with_order(
808 src_plan,
809 dst_plan,
810 &connection.msg,
811 connection.missions.clone(),
812 None,
813 None,
814 connection.order,
815 )
816 .map_err(|error| CuError::from(error.to_string()))?;
817 }
818
819 Ok(PlanGraph {
820 graph: plan_graph,
821 entities,
822 plan_to_original,
823 })
824}
825
826fn assemble_from_order(plan_graph: PlanGraph, order: StepOrder) -> CuResult<AssembledPlan> {
829 check_order(&plan_graph.graph, &order)?;
830 let mut execution = plan_from_order(&plan_graph.graph, &order)?;
831 expand_anytime_steps(&mut execution)?;
832 Ok(AssembledPlan {
833 execution,
834 entities: plan_graph.entities,
835 plan_to_original: plan_graph.plan_to_original,
836 })
837}
838
839#[doc(hidden)]
846pub fn assemble_runtime_plan(config: &CuConfig, graph: &CuGraph) -> CuResult<AssembledPlan> {
847 let planner: Box<dyn CuPlanner> = match config.planner_config() {
848 None => Box::new(Linearity),
849 Some(selection) => instantiate_builtin_planner(selection.get_type(), selection.get_config())?
850 .ok_or_else(|| {
851 CuError::from(format!(
852 "Planner '{}' is not shipped with copper (shipped: {}) and the config carries no resolved order for this mission. Resolve it at build time: call cu29::planner::emit_plan::<{}>(\"<config>.ron\") from the application's build.rs.",
853 selection.get_type(),
854 BUILTIN_PLANNERS.join(", "),
855 selection.get_type(),
856 ))
857 })?,
858 };
859 assemble_runtime_plan_with_planner(config, graph, planner.as_ref())
860}
861
862#[doc(hidden)]
864pub fn assemble_runtime_plan_with_planner(
865 config: &CuConfig,
866 graph: &CuGraph,
867 planner: &dyn CuPlanner,
868) -> CuResult<AssembledPlan> {
869 let plan_graph = build_plan_graph(config, graph)?;
870 let order = planner.plan(&plan_graph.graph)?;
871 assemble_from_order(plan_graph, order)
872}
873
874#[doc(hidden)]
877pub fn assemble_runtime_plan_from_step_keys(
878 config: &CuConfig,
879 graph: &CuGraph,
880 step_keys: &[String],
881) -> CuResult<AssembledPlan> {
882 let plan_graph = build_plan_graph(config, graph)?;
883 let by_key: BTreeMap<&str, NodeId> = plan_graph
884 .entities
885 .iter()
886 .enumerate()
887 .map(|(id, entity)| (entity.key.as_str(), id as NodeId))
888 .collect();
889 let order = step_keys
890 .iter()
891 .map(|key| {
892 by_key.get(key.as_str()).copied().ok_or_else(|| {
893 CuError::from(format!(
894 "Resolved plan references unknown step '{key}'; the baked order no longer matches the config."
895 ))
896 })
897 })
898 .collect::<CuResult<Vec<NodeId>>>()
899 .map(StepOrder)?;
900 assemble_from_order(plan_graph, order)
901}
902
903fn find_channel_plan_node(
904 config: &CuConfig,
905 channel_nodes: &[(usize, usize, ChannelDirection, NodeId)],
906 bridge_id: &str,
907 channel_id: &str,
908 direction: ChannelDirection,
909) -> CuResult<NodeId> {
910 channel_nodes
911 .iter()
912 .find_map(
913 |(bridge_index, channel_index, candidate_direction, node_id)| {
914 let bridge = &config.bridges[*bridge_index];
915 let channel = &bridge.channels[*channel_index];
916 (bridge.id == bridge_id
917 && channel.id() == channel_id
918 && *candidate_direction == direction)
919 .then_some(*node_id)
920 },
921 )
922 .ok_or_else(|| {
923 CuError::from(format!(
924 "Bridge channel '{bridge_id}/{channel_id}' is missing from the execution plan"
925 ))
926 })
927}
928
929#[doc(hidden)]
931pub fn mission_graphs(config: &CuConfig) -> Vec<(String, &CuGraph)> {
932 match &config.graphs {
933 ConfigGraphs::Simple(graph) => vec![("default".to_string(), graph)],
934 ConfigGraphs::Missions(graphs) => {
935 let mut missions: Vec<_> = graphs
936 .iter()
937 .map(|(mission, graph)| (mission.clone(), graph))
938 .collect();
939 missions.sort_by(|left, right| left.0.cmp(&right.0));
940 missions
941 }
942 }
943}
944
945#[doc(hidden)]
947pub fn step_key(
948 mission: &str,
949 entity: &PlanEntity,
950 phase: CuStepPhase,
951 refine_ordinal: Option<u32>,
952) -> String {
953 let phase = match phase {
954 CuStepPhase::Whole => "whole".to_string(),
955 CuStepPhase::AnytimeBase => "base".to_string(),
956 CuStepPhase::AnytimeRefine => format!("refine:{}", refine_ordinal.unwrap_or(0)),
957 };
958 format!("mission:{mission}|{}|phase:{phase}", entity.key)
959}
960
961#[doc(hidden)]
963pub const PLAN_ARTIFACT_FILE: &str = "cu29_plan.ron";
964
965#[doc(hidden)]
969#[derive(Serialize, Deserialize)]
970pub struct PlanArtifact {
971 pub planner_type: String,
972 pub config_digest: String,
973 pub orders: BTreeMap<String, Vec<String>>,
974}
975
976#[doc(hidden)]
983pub fn config_digest(config: &CuConfig) -> CuResult<String> {
984 let ron = config.serialize_ron()?;
985 let value: ron::Value = CuConfig::get_options()
986 .from_str(&ron)
987 .map_err(|e| CuError::from(format!("Could not re-parse the config for digesting: {e}")))?;
988 let mut canonical = String::new();
989 write_canonical_ron(&value, &mut canonical);
990 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
991 for byte in canonical.into_bytes() {
992 hash ^= u64::from(byte);
993 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
994 }
995 Ok(format!("{hash:016x}"))
996}
997
998fn write_canonical_ron(value: &ron::Value, out: &mut String) {
999 use core::fmt::Write;
1000 match value {
1001 ron::Value::Map(map) => {
1002 let mut entries: Vec<(String, &ron::Value)> = map
1003 .iter()
1004 .map(|(key, entry)| {
1005 let mut rendered = String::new();
1006 write_canonical_ron(key, &mut rendered);
1007 (rendered, entry)
1008 })
1009 .collect();
1010 entries.sort_by(|left, right| left.0.cmp(&right.0));
1011 out.push('{');
1012 for (key, entry) in entries {
1013 out.push_str(&key);
1014 out.push(':');
1015 write_canonical_ron(entry, out);
1016 out.push(',');
1017 }
1018 out.push('}');
1019 }
1020 ron::Value::Seq(entries) => {
1021 out.push('[');
1022 for entry in entries {
1023 write_canonical_ron(entry, out);
1024 out.push(',');
1025 }
1026 out.push(']');
1027 }
1028 other => {
1029 let _ = write!(out, "{other:?}");
1030 }
1031 }
1032}
1033
1034#[cfg(feature = "std")]
1050pub fn emit_plan<P: CuPlanner>(config_path: &str) -> CuResult<()> {
1051 let out_dir = std::env::var("OUT_DIR")
1052 .map_err(|_| CuError::from("emit_plan must run from a build.rs (OUT_DIR is not set)"))?;
1053 let artifact = build_plan_artifact::<P>(config_path)?;
1054 let ron = ron::ser::to_string(&artifact)
1055 .map_err(|e| CuError::from(format!("Could not serialize the plan artifact: {e}")))?;
1056 let path = std::path::Path::new(&out_dir).join(PLAN_ARTIFACT_FILE);
1057 std::fs::write(&path, ron)
1058 .map_err(|e| CuError::new_with_cause("Could not write the plan artifact", e))?;
1059 println!("cargo::rerun-if-changed={config_path}");
1060 Ok(())
1061}
1062
1063#[cfg(feature = "std")]
1065fn build_plan_artifact<P: CuPlanner>(config_path: &str) -> CuResult<PlanArtifact> {
1066 let features_var = std::env::var("CARGO_CFG_FEATURE").unwrap_or_default();
1069 let features: Vec<&str> = features_var.split(',').filter(|f| !f.is_empty()).collect();
1070 let config = crate::config::read_configuration_with_features(config_path, &features)?;
1071 let planner = P::new(
1072 config
1073 .planner_config()
1074 .and_then(|selection| selection.get_config()),
1075 )?;
1076 let mut orders = BTreeMap::new();
1077 for (mission, graph) in mission_graphs(&config) {
1078 let keys = (|| -> CuResult<Vec<String>> {
1079 let plan_graph = build_plan_graph(&config, graph)?;
1080 let order = planner.plan(&plan_graph.graph)?;
1081 check_order(&plan_graph.graph, &order)?;
1082 Ok(order
1083 .0
1084 .iter()
1085 .map(|&id| plan_graph.entities[id as usize].key.clone())
1086 .collect())
1087 })()
1088 .map_err(|e| CuError::from(format!("mission '{mission}': {e}")))?;
1089 orders.insert(mission, keys);
1090 }
1091 Ok(PlanArtifact {
1092 planner_type: core::any::type_name::<P>().to_string(),
1093 config_digest: config_digest(&config)?,
1094 orders,
1095 })
1096}
1097
1098#[doc(hidden)]
1100#[cfg(feature = "std")]
1101pub fn read_plan_artifact(path: &std::path::Path) -> CuResult<PlanArtifact> {
1102 let text = std::fs::read_to_string(path)
1103 .map_err(|e| CuError::new_with_cause("Could not read the plan artifact", e))?;
1104 ron::from_str(&text)
1105 .map_err(|e| CuError::from(format!("Could not parse the plan artifact: {e}")))
1106}
1107
1108#[cfg(test)]
1109mod tests {
1110 use super::*;
1111 use crate::curuntime::CuExecutionUnit;
1112
1113 fn config(ron: &str) -> CuConfig {
1114 CuConfig::deserialize_ron(ron).expect("valid planner test config")
1115 }
1116
1117 fn step_labels(plan: &AssembledPlan) -> Vec<String> {
1118 plan.execution
1119 .steps
1120 .iter()
1121 .map(|unit| match unit {
1122 CuExecutionUnit::Step(step) => plan.entities[step.node_id as usize].label.clone(),
1123 CuExecutionUnit::Loop(_) => panic!("unexpected nested loop"),
1124 })
1125 .collect()
1126 }
1127
1128 #[test]
1129 fn plans_diamond_fan_in_with_stable_input_order() {
1130 let config = config(
1131 r#"(
1132 tasks: [
1133 (id: "left", type: "demo::Left"),
1134 (id: "right", type: "demo::Right"),
1135 (id: "join", type: "demo::Join"),
1136 (id: "sink", type: "demo::Sink"),
1137 ],
1138 cnx: [
1139 (src: "right", dst: "join", msg: "demo::RightMsg"),
1140 (src: "left", dst: "join", msg: "demo::LeftMsg"),
1141 (src: "join", dst: "sink", msg: "demo::Joined"),
1142 ],
1143 )"#,
1144 );
1145 let graph = config.get_graph(None).unwrap();
1146 let plan = assemble_runtime_plan(&config, graph).unwrap();
1147 assert_eq!(step_labels(&plan), ["left", "right", "join", "sink"]);
1148 let join = plan
1149 .execution
1150 .steps
1151 .iter()
1152 .find_map(|unit| match unit {
1153 CuExecutionUnit::Step(step) if step.node.get_id() == "join" => Some(step),
1154 _ => None,
1155 })
1156 .unwrap();
1157 assert_eq!(join.input_msg_indices_types.len(), 2);
1158 assert_eq!(join.input_msg_indices_types[0].msg_type, "demo::RightMsg");
1159 assert_eq!(join.input_msg_indices_types[1].msg_type, "demo::LeftMsg");
1160 }
1161
1162 #[test]
1163 fn inserts_bridge_rx_and_tx_channel_stages() {
1164 let config = config(
1165 r#"(
1166 tasks: [
1167 (id: "task", type: "demo::Task"),
1168 ],
1169 bridges: [
1170 (
1171 id: "radio",
1172 type: "demo::Radio",
1173 channels: [Rx(id: "incoming"), Tx(id: "outgoing")],
1174 ),
1175 ],
1176 cnx: [
1177 (src: "radio/incoming", dst: "task", msg: "demo::In"),
1178 (src: "task", dst: "radio/outgoing", msg: "demo::Out"),
1179 ],
1180 )"#,
1181 );
1182 let graph = config.get_graph(None).unwrap();
1183 let plan = assemble_runtime_plan(&config, graph).unwrap();
1184 assert_eq!(
1185 step_labels(&plan),
1186 ["radio::rx::incoming", "task", "radio::tx::outgoing"]
1187 );
1188 assert!(matches!(
1189 plan.entities[1].kind,
1190 PlanEntityKind::BridgeRx { .. }
1191 ));
1192 assert!(matches!(
1193 plan.entities[2].kind,
1194 PlanEntityKind::BridgeTx { .. }
1195 ));
1196 }
1197
1198 #[test]
1199 fn synthesizes_declared_unconnected_output() {
1200 let config = config(
1201 r#"(
1202 tasks: [(id: "generated", type: "demo::Generated", kind: source)],
1203 cnx: [],
1204 )"#,
1205 );
1206 let graph = config.get_graph(None).unwrap();
1207 let plan = assemble_runtime_plan(&config, graph).unwrap();
1208 let CuExecutionUnit::Step(step) = &plan.execution.steps[0] else {
1209 panic!("expected one generated step")
1210 };
1211 let output = step.output_msg_pack.as_ref().unwrap();
1212 assert_eq!(output.culist_index, 0);
1213 assert!(output.msg_types[0].contains("CuSingleOutputMsg"));
1214 assert!(output.msg_types[0].contains("CuSrcTask"));
1215 }
1216
1217 fn pinned_graph(planner: &str) -> CuConfig {
1222 config(&format!(
1223 r#"(
1224 tasks: [
1225 (id: "cam", type: "demo::Cam"),
1226 (id: "ekf", type: "demo::Ekf"),
1227 (id: "motor", type: "demo::Motor"),
1228 ],
1229 bridges: [(
1230 id: "radio",
1231 type: "demo::Radio",
1232 channels: [Rx(id: "incoming"), Tx(id: "outgoing")],
1233 )],
1234 cnx: [
1235 (src: "radio/incoming", dst: "cam", msg: "demo::In"),
1236 (src: "cam", dst: "ekf", msg: "demo::Frame"),
1237 (src: "ekf", dst: "motor", msg: "demo::State"),
1238 (src: "motor", dst: "radio/outgoing", msg: "demo::Cmd"),
1239 ],
1240 runtime: (planner: {planner}),
1241 )"#
1242 ))
1243 }
1244
1245 fn pinned(ids: &[&str]) -> String {
1246 let quoted: Vec<String> = ids.iter().map(|id| format!("{id:?}")).collect();
1247 format!(
1248 r#"(type: "cu29::planner::Pinned", config: {{ "order": [{}] }})"#,
1249 quoted.join(", ")
1250 )
1251 }
1252
1253 #[test]
1254 fn pinned_plan_weaves_bridge_stages_and_matches_task_order() {
1255 let config = pinned_graph(&pinned(&["cam", "ekf", "motor"]));
1256 let graph = config.get_graph(None).unwrap();
1257 let plan = assemble_runtime_plan(&config, graph).unwrap();
1258 assert_eq!(
1259 step_labels(&plan),
1260 [
1261 "radio::rx::incoming",
1262 "cam",
1263 "ekf",
1264 "motor",
1265 "radio::tx::outgoing"
1266 ]
1267 );
1268 }
1269
1270 #[test]
1271 fn pinned_plan_rejects_bad_id_lists() {
1272 let rejects = |planner: &str| {
1273 let config = pinned_graph(planner);
1274 let graph = config.get_graph(None).unwrap();
1275 assemble_runtime_plan(&config, graph)
1276 .err()
1277 .unwrap()
1278 .to_string()
1279 };
1280
1281 let err = rejects(&pinned(&["cam", "ekf"]));
1282 assert!(err.contains("missing"), "{err}");
1283
1284 let err = rejects(&pinned(&["radio::rx::incoming", "cam", "ekf", "motor"]));
1285 assert!(err.contains("bridge stage"), "{err}");
1286
1287 let err = rejects(&pinned(&["cam", "ekf", "motor", "ghost"]));
1288 assert!(err.contains("unknown task 'ghost'"), "{err}");
1289
1290 let err = rejects(&pinned(&["cam", "cam", "ekf"]));
1291 assert!(err.contains("more than once"), "{err}");
1292
1293 let err = rejects(r#"(type: "cu29::planner::Pinned")"#);
1294 assert!(err.contains("needs config"), "{err}");
1295
1296 let err = rejects(r#"(type: "acme::Planner")"#);
1298 assert!(err.contains("emit_plan"), "{err}");
1299 }
1300
1301 struct ReverseAlpha;
1306
1307 impl CuPlanner for ReverseAlpha {
1308 fn new(_config: Option<&ComponentConfig>) -> CuResult<Self> {
1309 Ok(ReverseAlpha)
1310 }
1311
1312 fn plan(&self, graph: &CuGraph) -> CuResult<StepOrder> {
1313 let mut order = Vec::new();
1314 let mut planned: BTreeSet<NodeId> = BTreeSet::new();
1315 while order.len() < graph.node_count() {
1316 let next = graph
1317 .get_all_nodes()
1318 .into_iter()
1319 .filter(|(id, _)| !planned.contains(id))
1320 .filter(|(id, _)| {
1321 graph
1322 .get_neighbor_ids(*id, CuDirection::Incoming)
1323 .iter()
1324 .all(|input| planned.contains(input))
1325 })
1326 .max_by_key(|(_, node)| node.get_id())
1327 .map(|(id, _)| id)
1328 .expect("acyclic graph always has a ready node");
1329 planned.insert(next);
1330 order.push(next);
1331 }
1332 Ok(StepOrder(order))
1333 }
1334 }
1335
1336 #[test]
1337 fn custom_planner_orders_the_plan() {
1338 let config = config(
1339 r#"(
1340 tasks: [
1341 (id: "left", type: "demo::Left"),
1342 (id: "right", type: "demo::Right"),
1343 (id: "join", type: "demo::Join"),
1344 (id: "sink", type: "demo::Sink"),
1345 ],
1346 cnx: [
1347 (src: "left", dst: "join", msg: "demo::LeftMsg"),
1348 (src: "right", dst: "join", msg: "demo::RightMsg"),
1349 (src: "join", dst: "sink", msg: "demo::Joined"),
1350 ],
1351 )"#,
1352 );
1353 let graph = config.get_graph(None).unwrap();
1354 let plan = assemble_runtime_plan_with_planner(&config, graph, &ReverseAlpha).unwrap();
1355 assert_eq!(step_labels(&plan), ["right", "left", "join", "sink"]);
1356
1357 let keys: Vec<String> = plan
1359 .execution
1360 .steps
1361 .iter()
1362 .map(|unit| match unit {
1363 CuExecutionUnit::Step(step) => plan.entities[step.node_id as usize].key.clone(),
1364 CuExecutionUnit::Loop(_) => panic!("unexpected nested loop"),
1365 })
1366 .collect();
1367 let replayed = assemble_runtime_plan_from_step_keys(&config, graph, &keys).unwrap();
1368 assert_eq!(step_labels(&replayed), step_labels(&plan));
1369
1370 let err = assemble_runtime_plan_from_step_keys(&config, graph, &["task:ghost".to_string()])
1371 .err()
1372 .unwrap()
1373 .to_string();
1374 assert!(err.contains("unknown step 'task:ghost'"), "{err}");
1375 }
1376
1377 struct Backwards;
1379
1380 impl CuPlanner for Backwards {
1381 fn new(_config: Option<&ComponentConfig>) -> CuResult<Self> {
1382 Ok(Backwards)
1383 }
1384
1385 fn plan(&self, graph: &CuGraph) -> CuResult<StepOrder> {
1386 let StepOrder(mut order) = topo_bfs_order(graph)?;
1387 order.reverse();
1388 Ok(StepOrder(order))
1389 }
1390 }
1391
1392 #[test]
1393 fn illegal_planner_output_is_rejected() {
1394 let config = build_config(&["s", "k"], &[("s", "k", "m")]);
1395 let graph = config.get_graph(None).unwrap();
1396 let err = assemble_runtime_plan_with_planner(&config, graph, &Backwards)
1397 .err()
1398 .unwrap()
1399 .to_string();
1400 assert!(err.contains("scheduled before its input"), "{err}");
1401 }
1402
1403 #[test]
1404 fn canonical_ron_ignores_map_entry_order() {
1405 let render = |txt: &str| {
1409 let value: ron::Value = ron::from_str(txt).unwrap();
1410 let mut out = String::new();
1411 write_canonical_ron(&value, &mut out);
1412 out
1413 };
1414 assert_eq!(
1415 render(r#"{"a": 1, "b": [2, 3], "c": {"x": 4, "y": 5}}"#),
1416 render(r#"{"c": {"y": 5, "x": 4}, "b": [2, 3], "a": 1}"#),
1417 );
1418 assert_ne!(render(r#"{"b": [2, 3]}"#), render(r#"{"b": [3, 2]}"#));
1419 }
1420
1421 #[test]
1422 fn check_order_flags_precedence_and_missing() {
1423 let config = build_config(&["s", "k"], &[("s", "k", "m")]);
1424 let graph = config.get_graph(None).unwrap();
1425 let s = graph.get_node_id_by_name("s").unwrap();
1426 let k = graph.get_node_id_by_name("k").unwrap();
1427
1428 let err = check_order(graph, &StepOrder(vec![k, s])).unwrap_err();
1430 assert!(
1431 err.to_string().contains("scheduled before its input"),
1432 "{err}"
1433 );
1434
1435 let err = check_order(graph, &StepOrder(vec![s])).unwrap_err();
1437 assert!(err.to_string().contains("Missing"), "{err}");
1438 }
1439
1440 fn build_config(nodes: &[&str], edges: &[(&str, &str, &str)]) -> CuConfig {
1444 let mut config = CuConfig::default();
1445 let graph = config.get_graph_mut(None).unwrap();
1446 let mut ids: BTreeMap<String, NodeId> = BTreeMap::new();
1447 for &name in nodes {
1448 let id = graph.add_node(Node::new(name, "demo::T")).unwrap();
1449 ids.insert(name.to_string(), id);
1450 }
1451 for &(src, dst, msg) in edges {
1452 graph.connect(ids[src], ids[dst], msg).unwrap();
1453 }
1454 config
1455 }
1456
1457 fn corpus() -> Vec<(String, CuConfig)> {
1460 let mut cases = vec![
1463 (
1464 "chain".to_string(),
1465 build_config(
1466 &["s", "r1", "r2", "k"],
1467 &[("s", "r1", "m0"), ("r1", "r2", "m1"), ("r2", "k", "m2")],
1468 ),
1469 ),
1470 (
1471 "fanout_shared_msg".to_string(),
1472 build_config(
1473 &["s", "a", "b", "c"],
1474 &[("s", "a", "m"), ("s", "b", "m"), ("s", "c", "n")],
1475 ),
1476 ),
1477 (
1478 "fanin_multisource".to_string(),
1479 build_config(
1480 &["s1", "s2", "s3", "k"],
1481 &[("s2", "k", "m2"), ("s1", "k", "m1"), ("s3", "k", "m3")],
1482 ),
1483 ),
1484 (
1485 "diamond".to_string(),
1486 build_config(
1487 &["s", "a", "b", "j", "k"],
1488 &[
1489 ("s", "a", "m0"),
1490 ("s", "b", "m1"),
1491 ("a", "j", "ma"),
1492 ("b", "j", "mb"),
1493 ("j", "k", "mj"),
1494 ],
1495 ),
1496 ),
1497 (
1498 "bridge_like".to_string(),
1499 build_config(
1500 &["rx1", "rx2", "r", "tx1", "tx2"],
1501 &[
1502 ("rx1", "r", "m1"),
1503 ("rx2", "r", "m2"),
1504 ("r", "tx1", "o1"),
1505 ("r", "tx2", "o2"),
1506 ],
1507 ),
1508 ),
1509 (
1510 "multisource_layers".to_string(),
1511 build_config(
1512 &["s1", "s2", "r1", "r2", "k"],
1513 &[
1514 ("s1", "r1", "a"),
1515 ("s2", "r1", "b"),
1516 ("s1", "r2", "c"),
1517 ("s2", "r2", "d"),
1518 ("r1", "k", "e"),
1519 ("r2", "k", "f"),
1520 ],
1521 ),
1522 ),
1523 (
1524 "side_branch".to_string(),
1525 build_config(
1526 &["s", "r1", "r2", "r3", "k1", "k2"],
1527 &[
1528 ("s", "r1", "m0"),
1529 ("r1", "r2", "m1"),
1530 ("r1", "r3", "m2"),
1531 ("r2", "k1", "m3"),
1532 ("r3", "k2", "m4"),
1533 ],
1534 ),
1535 ),
1536 ];
1537 for seed in 0u64..6 {
1539 cases.push((format!("layered_{seed}"), layered_dag(seed)));
1540 }
1541 cases
1542 }
1543
1544 fn layered_dag(seed: u64) -> CuConfig {
1547 let layers = [2usize, 3, 3, 2];
1548 let mut state = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
1549 let mut next = || {
1550 state = state
1551 .wrapping_mul(6364136223846793005)
1552 .wrapping_add(1442695040888963407);
1553 state >> 33
1554 };
1555 let name = |layer: usize, idx: usize| format!("n{layer}_{idx}");
1556 let mut nodes = Vec::new();
1557 for (layer, count) in layers.iter().enumerate() {
1558 for idx in 0..*count {
1559 nodes.push(name(layer, idx));
1560 }
1561 }
1562 let node_refs: Vec<&str> = nodes.iter().map(|s| s.as_str()).collect();
1563 let mut edges: Vec<(String, String, String)> = Vec::new();
1564 for layer in 0..layers.len() - 1 {
1565 for from in 0..layers[layer] {
1566 let mut connected = false;
1568 for to in 0..layers[layer + 1] {
1569 if next() % 2 == 0 || (to == layers[layer + 1] - 1 && !connected) {
1570 edges.push((
1571 name(layer, from),
1572 name(layer + 1, to),
1573 format!("m{layer}_{from}_{to}"),
1574 ));
1575 connected = true;
1576 }
1577 }
1578 }
1579 for to in 0..layers[layer + 1] {
1581 if !edges.iter().any(|(_, d, _)| *d == name(layer + 1, to)) {
1582 edges.push((
1583 name(layer, 0),
1584 name(layer + 1, to),
1585 format!("f{layer}_{to}"),
1586 ));
1587 }
1588 }
1589 }
1590 let edge_refs: Vec<(&str, &str, &str)> = edges
1591 .iter()
1592 .map(|(s, d, m)| (s.as_str(), d.as_str(), m.as_str()))
1593 .collect();
1594 build_config(&node_refs, &edge_refs)
1595 }
1596
1597 fn assert_same_plan(name: &str, config: &CuConfig) {
1600 let graph = config.get_graph(None).unwrap();
1601 let legacy = compute_runtime_plan_legacy(graph).expect("legacy plan");
1602 let fresh = crate::curuntime::compute_runtime_plan(graph).expect("new plan");
1603 assert_eq!(
1604 legacy.steps.len(),
1605 fresh.steps.len(),
1606 "{name}: step count differs"
1607 );
1608 for (index, (a, b)) in legacy.steps.iter().zip(fresh.steps.iter()).enumerate() {
1609 let (CuExecutionUnit::Step(a), CuExecutionUnit::Step(b)) = (a, b) else {
1610 panic!("{name}: unexpected nested loop");
1611 };
1612 assert_eq!(a.node_id, b.node_id, "{name}: step {index} node id");
1613 assert_eq!(a.phase, b.phase, "{name}: step {index} phase");
1614 let (oa, ob) = (a.output_msg_pack.as_ref(), b.output_msg_pack.as_ref());
1615 assert_eq!(
1616 oa.map(|p| p.culist_index),
1617 ob.map(|p| p.culist_index),
1618 "{name}: step {index} culist index"
1619 );
1620 assert_eq!(
1621 oa.map(|p| &p.msg_types),
1622 ob.map(|p| &p.msg_types),
1623 "{name}: step {index} output msg types"
1624 );
1625 assert_eq!(
1626 a.input_msg_indices_types.len(),
1627 b.input_msg_indices_types.len(),
1628 "{name}: step {index} input arity"
1629 );
1630 for (ia, ib) in a
1631 .input_msg_indices_types
1632 .iter()
1633 .zip(b.input_msg_indices_types.iter())
1634 {
1635 assert_eq!(ia.culist_index, ib.culist_index, "{name}: input culist");
1636 assert_eq!(ia.msg_type, ib.msg_type, "{name}: input msg");
1637 assert_eq!(ia.src_port, ib.src_port, "{name}: input src_port");
1638 assert_eq!(ia.edge_id, ib.edge_id, "{name}: input edge_id");
1639 assert_eq!(
1640 ia.connection_order, ib.connection_order,
1641 "{name}: input connection_order"
1642 );
1643 }
1644 }
1645 }
1646
1647 #[test]
1648 fn topo_bfs_matches_legacy_walk_over_corpus() {
1649 for (name, config) in corpus() {
1650 assert_same_plan(&name, &config);
1651 }
1652 }
1653
1654 fn find_output_pack_from_nodeid_legacy(
1656 node_id: NodeId,
1657 steps: &[CuExecutionUnit],
1658 ) -> Option<CuOutputPack> {
1659 for step in steps {
1660 match step {
1661 CuExecutionUnit::Loop(loop_unit) => {
1662 if let Some(pack) =
1663 find_output_pack_from_nodeid_legacy(node_id, &loop_unit.steps)
1664 {
1665 return Some(pack);
1666 }
1667 }
1668 CuExecutionUnit::Step(step) if step.node_id == node_id => {
1669 return step.output_msg_pack.clone();
1670 }
1671 _ => {}
1672 }
1673 }
1674 None
1675 }
1676
1677 fn plan_tasks_tree_branch_legacy(
1678 graph: &CuGraph,
1679 mut next_culist_output_index: u32,
1680 starting_point: NodeId,
1681 plan: &mut Vec<CuExecutionUnit>,
1682 ) -> CuResult<(u32, bool)> {
1683 let mut handled = false;
1684 for id in graph.bfs_nodes(starting_point) {
1685 let node_ref = graph.get_node(id).unwrap();
1686 let mut input_msg_indices_types: Vec<CuInputMsg> = Vec::new();
1687 let output_msg_pack: Option<CuOutputPack>;
1688 let task_type = find_task_type_for_id(graph, id)?;
1689 match task_type {
1690 CuTaskType::Source => {
1691 let msg_types = graph.get_node_output_msg_types_by_id(id)?;
1692 if msg_types.is_empty() {
1693 return Err(CuError::from(format!(
1694 "Source node '{}' has no declared outputs",
1695 node_ref.get_id()
1696 )));
1697 }
1698 output_msg_pack = Some(CuOutputPack {
1699 culist_index: next_culist_output_index,
1700 msg_types,
1701 });
1702 next_culist_output_index += 1;
1703 }
1704 CuTaskType::Sink => {
1705 let mut edge_ids = graph.get_dst_edges(id).unwrap_or_default();
1706 edge_ids.sort();
1707 for edge_id in edge_ids {
1708 let edge = graph
1709 .edge(edge_id)
1710 .unwrap_or_else(|| panic!("Missing edge {edge_id} for node {id}"));
1711 let pid =
1712 graph
1713 .get_node_id_by_name(edge.src.as_str())
1714 .unwrap_or_else(|| {
1715 panic!("Missing source node '{}' for edge {edge_id}", edge.src)
1716 });
1717 let output_pack = find_output_pack_from_nodeid_legacy(pid, plan);
1718 if let Some(output_pack) = output_pack {
1719 let msg_type = edge.msg.as_str();
1720 let src_port = output_pack
1721 .msg_types
1722 .iter()
1723 .position(|msg| msg == msg_type)
1724 .unwrap_or_else(|| {
1725 panic!(
1726 "Missing output port for message type '{msg_type}' on node {pid}"
1727 )
1728 });
1729 input_msg_indices_types.push(CuInputMsg {
1730 culist_index: output_pack.culist_index,
1731 msg_type: msg_type.to_string(),
1732 src_port,
1733 edge_id,
1734 connection_order: edge.order,
1735 });
1736 } else {
1737 return Ok((next_culist_output_index, handled));
1738 }
1739 }
1740 output_msg_pack = Some(CuOutputPack {
1741 culist_index: next_culist_output_index,
1742 msg_types: Vec::from(["()".to_string()]),
1743 });
1744 next_culist_output_index += 1;
1745 }
1746 CuTaskType::Regular => {
1747 let mut edge_ids = graph.get_dst_edges(id).unwrap_or_default();
1748 edge_ids.sort();
1749 for edge_id in edge_ids {
1750 let edge = graph
1751 .edge(edge_id)
1752 .unwrap_or_else(|| panic!("Missing edge {edge_id} for node {id}"));
1753 let pid =
1754 graph
1755 .get_node_id_by_name(edge.src.as_str())
1756 .unwrap_or_else(|| {
1757 panic!("Missing source node '{}' for edge {edge_id}", edge.src)
1758 });
1759 let output_pack = find_output_pack_from_nodeid_legacy(pid, plan);
1760 if let Some(output_pack) = output_pack {
1761 let msg_type = edge.msg.as_str();
1762 let src_port = output_pack
1763 .msg_types
1764 .iter()
1765 .position(|msg| msg == msg_type)
1766 .unwrap_or_else(|| {
1767 panic!(
1768 "Missing output port for message type '{msg_type}' on node {pid}"
1769 )
1770 });
1771 input_msg_indices_types.push(CuInputMsg {
1772 culist_index: output_pack.culist_index,
1773 msg_type: msg_type.to_string(),
1774 src_port,
1775 edge_id,
1776 connection_order: edge.order,
1777 });
1778 } else {
1779 return Ok((next_culist_output_index, handled));
1780 }
1781 }
1782 let msg_types = graph.get_node_output_msg_types_by_id(id)?;
1783 if msg_types.is_empty() {
1784 return Err(CuError::from(format!(
1785 "Regular node '{}' has no declared outputs",
1786 node_ref.get_id()
1787 )));
1788 }
1789 output_msg_pack = Some(CuOutputPack {
1790 culist_index: next_culist_output_index,
1791 msg_types,
1792 });
1793 next_culist_output_index += 1;
1794 }
1795 }
1796
1797 sort_inputs_by_connection_order(&mut input_msg_indices_types);
1798 if let Some(pos) = plan
1799 .iter()
1800 .position(|step| matches!(step, CuExecutionUnit::Step(s) if s.node_id == id))
1801 {
1802 let mut step = plan.remove(pos);
1803 if let CuExecutionUnit::Step(ref mut s) = step {
1804 s.input_msg_indices_types = input_msg_indices_types;
1805 }
1806 plan.push(step);
1807 } else {
1808 let step = CuExecutionStep {
1809 node_id: id,
1810 node: node_ref.clone(),
1811 task_type,
1812 phase: CuStepPhase::default(),
1813 input_msg_indices_types,
1814 output_msg_pack,
1815 };
1816 plan.push(CuExecutionUnit::Step(Box::new(step)));
1817 }
1818 handled = true;
1819 }
1820 Ok((next_culist_output_index, handled))
1821 }
1822
1823 fn compute_runtime_plan_legacy(graph: &CuGraph) -> CuResult<CuExecutionLoop> {
1826 let mut plan = Vec::new();
1827 let mut next_culist_output_index = 0u32;
1828 let mut queue: VecDeque<NodeId> = VecDeque::new();
1829 for node_id in graph.node_ids() {
1830 if find_task_type_for_id(graph, node_id)? == CuTaskType::Source {
1831 queue.push_back(node_id);
1832 }
1833 }
1834 while let Some(start_node) = queue.pop_front() {
1835 for node_id in graph.bfs_nodes(start_node) {
1836 let already = plan
1837 .iter()
1838 .any(|unit| matches!(unit, CuExecutionUnit::Step(s) if s.node_id == node_id));
1839 if already {
1840 continue;
1841 }
1842 let (new_index, handled) = plan_tasks_tree_branch_legacy(
1843 graph,
1844 next_culist_output_index,
1845 node_id,
1846 &mut plan,
1847 )?;
1848 next_culist_output_index = new_index;
1849 if !handled {
1850 continue;
1851 }
1852 for neighbor in graph.get_neighbor_ids(node_id, CuDirection::Outgoing) {
1853 queue.push_back(neighbor);
1854 }
1855 }
1856 }
1857 let mut planned_nodes = BTreeSet::new();
1858 for unit in &plan {
1859 if let CuExecutionUnit::Step(step) = unit {
1860 planned_nodes.insert(step.node_id);
1861 }
1862 }
1863 let mut missing = Vec::new();
1864 for node_id in graph.node_ids() {
1865 if !planned_nodes.contains(&node_id) {
1866 if let Some(node) = graph.get_node(node_id) {
1867 missing.push(node.get_id().to_string());
1868 } else {
1869 missing.push(format!("node_id_{node_id}"));
1870 }
1871 }
1872 }
1873 if !missing.is_empty() {
1874 missing.sort();
1875 return Err(CuError::from(format!(
1876 "Execution plan could not include all nodes. Missing: {}. Check for loopback or missing source connections.",
1877 missing.join(", ")
1878 )));
1879 }
1880 Ok(CuExecutionLoop {
1881 steps: plan,
1882 loop_count: None,
1883 })
1884 }
1885}