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 ports = graph.get_node_output_ports_by_id(id)?;
482 if ports.is_empty() {
483 return Err(CuError::from(format!(
484 "Source node '{}' has no declared outputs",
485 node_ref.get_id()
486 )));
487 }
488 let (msg_types, src_channels) = ports.into_iter().unzip();
489 output_msg_pack = Some(CuOutputPack {
490 culist_index: next_culist_output_index,
491 msg_types,
492 src_channels,
493 });
494 next_culist_output_index += 1;
495 }
496 CuTaskType::Sink => {
497 output_msg_pack = Some(CuOutputPack {
498 culist_index: next_culist_output_index,
499 msg_types: Vec::from(["()".to_string()]),
500 src_channels: vec![None],
501 });
502 next_culist_output_index += 1;
503 }
504 CuTaskType::Regular => {
505 let ports = graph.get_node_output_ports_by_id(id)?;
506 if ports.is_empty() {
507 return Err(CuError::from(format!(
508 "Regular node '{}' has no declared outputs",
509 node_ref.get_id()
510 )));
511 }
512 let (msg_types, src_channels) = ports.into_iter().unzip();
513 output_msg_pack = Some(CuOutputPack {
514 culist_index: next_culist_output_index,
515 msg_types,
516 src_channels,
517 });
518 next_culist_output_index += 1;
519 }
520 }
521
522 sort_inputs_by_connection_order(&mut input_msg_indices_types);
523 plan.push(CuExecutionUnit::Step(Box::new(CuExecutionStep {
524 node_id: id,
525 node: node_ref.clone(),
526 task_type,
527 phase: CuStepPhase::default(),
528 input_msg_indices_types,
529 output_msg_pack,
530 })));
531 }
532
533 Ok(CuExecutionLoop {
534 steps: plan,
535 loop_count: None,
536 })
537}
538
539fn collect_step_inputs(
544 graph: &CuGraph,
545 id: NodeId,
546 plan: &[CuExecutionUnit],
547) -> CuResult<Vec<CuInputMsg>> {
548 let mut inputs = Vec::new();
549 let mut edge_ids = graph.get_dst_edges(id).unwrap_or_default();
550 edge_ids.sort();
551 for edge_id in edge_ids {
552 let edge = graph
553 .edge(edge_id)
554 .unwrap_or_else(|| panic!("Missing edge {edge_id} for node {id}"));
555 let pid = graph
556 .get_node_id_by_name(edge.src.as_str())
557 .unwrap_or_else(|| panic!("Missing source node '{}' for edge {edge_id}", edge.src));
558 let output_pack = find_output_pack_from_nodeid(pid, plan).ok_or_else(|| {
559 CuError::from(format!(
560 "Plan materialization: input from node {pid} is not available before node {id}"
561 ))
562 })?;
563 let msg_type = edge.msg.as_str();
564 let src_channel = edge.src_channel.as_deref();
565 let src_port = output_pack
566 .msg_types
567 .iter()
568 .zip(output_pack.src_channels.iter())
569 .position(|(msg, ch)| msg == msg_type && ch.as_deref() == src_channel)
570 .unwrap_or_else(|| {
571 panic!("Missing output port for message type '{msg_type}' on node {pid}")
572 });
573 inputs.push(CuInputMsg {
574 culist_index: output_pack.culist_index,
575 msg_type: msg_type.to_string(),
576 src_port,
577 edge_id,
578 connection_order: edge.order,
579 });
580 }
581 Ok(inputs)
582}
583
584fn find_output_pack_from_nodeid(
585 node_id: NodeId,
586 steps: &[CuExecutionUnit],
587) -> Option<CuOutputPack> {
588 for step in steps {
589 match step {
590 CuExecutionUnit::Loop(loop_unit) => {
591 if let Some(output_pack) = find_output_pack_from_nodeid(node_id, &loop_unit.steps) {
592 return Some(output_pack);
593 }
594 }
595 CuExecutionUnit::Step(step) if step.node_id == node_id => {
596 return step.output_msg_pack.clone();
597 }
598 _ => {}
599 }
600 }
601 None
602}
603
604fn sort_inputs_by_connection_order(input_msg_indices_types: &mut [CuInputMsg]) {
609 input_msg_indices_types.sort_by_key(|input| input.connection_order);
610}
611
612fn node_name(graph: &CuGraph, node_id: NodeId) -> String {
613 graph
614 .get_node(node_id)
615 .map(|node| node.get_id())
616 .unwrap_or_else(|| format!("node_id_{node_id}"))
617}
618
619#[derive(Clone, Copy, Debug, PartialEq, Eq)]
620enum ChannelDirection {
621 Rx,
622 Tx,
623}
624
625fn channel_is_used(
626 graph: &CuGraph,
627 bridge_id: &str,
628 channel_id: &str,
629 direction: ChannelDirection,
630) -> bool {
631 graph.edges().any(|connection| match direction {
632 ChannelDirection::Rx => {
633 connection.src == bridge_id && connection.src_channel.as_deref() == Some(channel_id)
634 }
635 ChannelDirection::Tx => {
636 connection.dst == bridge_id && connection.dst_channel.as_deref() == Some(channel_id)
637 }
638 })
639}
640
641fn inferred_output_name(node: &Node, task_type: CuTaskType) -> String {
642 let rust_type = node.get_type();
643 if node.anytime().is_some() {
644 return format!(
645 "<<{rust_type} as cu29::cutask_anytime::CuAnytimeTask>::Output<'static> as cu29::cutask::CuSingleOutputMsg>::Payload"
646 );
647 }
648 let task_trait = match task_type {
649 CuTaskType::Source => "cu29::cutask::CuSrcTask",
650 CuTaskType::Regular => "cu29::cutask::CuTask",
651 CuTaskType::Sink => unreachable!("sinks do not have inferred outputs"),
652 };
653 format!(
654 "<<{rust_type} as {task_trait}>::Output<'static> as cu29::cutask::CuSingleOutputMsg>::Payload"
655 )
656}
657
658struct PlanGraph {
660 graph: CuGraph,
661 entities: Vec<PlanEntity>,
662 plan_to_original: Vec<Option<NodeId>>,
663}
664
665fn build_plan_graph(config: &CuConfig, graph: &CuGraph) -> CuResult<PlanGraph> {
669 let mut plan_graph = CuGraph::default();
670 let mut entities = Vec::new();
671 let mut plan_to_original = Vec::new();
672 let mut original_to_plan = Vec::new();
673 original_to_plan.resize(graph.node_count(), None);
674
675 let mut task_index = 0usize;
676 for (original_node_id, node) in graph.get_all_nodes() {
677 if node.get_flavor() != Flavor::Task {
678 continue;
679 }
680 let plan_node_id = plan_graph.add_node(node.clone())?;
681 debug_assert_eq!(plan_node_id as usize, entities.len());
682 original_to_plan[original_node_id as usize] = Some(plan_node_id);
683 plan_to_original.push(Some(original_node_id));
684 entities.push(PlanEntity {
685 key: format!("task:{}", node.get_id()),
686 label: node.get_id(),
687 kind: PlanEntityKind::Task {
688 original_node_id,
689 task_index,
690 },
691 });
692 task_index += 1;
693 }
694
695 for (original_node_id, node) in graph.get_all_nodes() {
699 if node.get_flavor() != Flavor::Task || node.get_declared_task_kind().is_none() {
700 continue;
701 }
702 let task_type = find_task_type_for_id(graph, original_node_id)?;
703 if task_type == CuTaskType::Sink
704 || !graph
705 .get_node_output_msg_types_by_id(original_node_id)?
706 .is_empty()
707 {
708 continue;
709 }
710 let plan_node_id = original_to_plan[original_node_id as usize]
711 .expect("task was mirrored into the plan graph");
712 let message_type = inferred_output_name(node, task_type);
713 plan_graph
714 .get_node_mut(plan_node_id)
715 .expect("mirrored task is present")
716 .add_nc_output(&message_type, usize::MAX);
717 }
718
719 let mut channel_nodes: Vec<(usize, usize, ChannelDirection, NodeId)> = Vec::new();
722 for (bridge_config_index, bridge) in config.bridges.iter().enumerate() {
723 if graph.get_node_id_by_name(&bridge.id).is_none() {
724 continue;
725 }
726 for direction in [ChannelDirection::Rx, ChannelDirection::Tx] {
727 for (channel_config_index, channel) in bridge.channels.iter().enumerate() {
728 let (channel_id, channel_direction) = match channel {
729 BridgeChannelConfigRepresentation::Rx { id, .. } => (id, ChannelDirection::Rx),
730 BridgeChannelConfigRepresentation::Tx { id, .. } => (id, ChannelDirection::Tx),
731 };
732 if channel_direction != direction
733 || !channel_is_used(graph, &bridge.id, channel_id, direction)
734 {
735 continue;
736 }
737
738 let direction_label = match direction {
739 ChannelDirection::Rx => "rx",
740 ChannelDirection::Tx => "tx",
741 };
742 let label = format!("{}::{direction_label}::{channel_id}", bridge.id);
743 let synthetic_type = match direction {
744 ChannelDirection::Rx => "__CuBridgeRxChannel",
745 ChannelDirection::Tx => "__CuBridgeTxChannel",
746 };
747 let mut node = Node::new(&label, synthetic_type);
748 node.set_flavor(Flavor::Bridge);
749 let plan_node_id = plan_graph.add_node(node)?;
750 debug_assert_eq!(plan_node_id as usize, entities.len());
751 plan_to_original.push(None);
752 entities.push(PlanEntity {
753 key: format!("bridge:{}:{direction_label}:{channel_id}", bridge.id),
754 label,
755 kind: match direction {
756 ChannelDirection::Rx => PlanEntityKind::BridgeRx {
757 bridge_config_index,
758 channel_config_index,
759 },
760 ChannelDirection::Tx => PlanEntityKind::BridgeTx {
761 bridge_config_index,
762 channel_config_index,
763 },
764 },
765 });
766 channel_nodes.push((
767 bridge_config_index,
768 channel_config_index,
769 direction,
770 plan_node_id,
771 ));
772 }
773 }
774 }
775
776 for connection in graph.edges() {
777 let src_plan = if let Some(channel_id) = connection.src_channel.as_deref() {
778 find_channel_plan_node(
779 config,
780 &channel_nodes,
781 &connection.src,
782 channel_id,
783 ChannelDirection::Rx,
784 )?
785 } else {
786 let original_id = graph.get_node_id_by_name(&connection.src).ok_or_else(|| {
787 CuError::from(format!("Unknown source node '{}'", connection.src))
788 })?;
789 original_to_plan[original_id as usize].ok_or_else(|| {
790 CuError::from(format!("Source node '{}' is not a task", connection.src))
791 })?
792 };
793 let dst_plan = if let Some(channel_id) = connection.dst_channel.as_deref() {
794 find_channel_plan_node(
795 config,
796 &channel_nodes,
797 &connection.dst,
798 channel_id,
799 ChannelDirection::Tx,
800 )?
801 } else {
802 let original_id = graph.get_node_id_by_name(&connection.dst).ok_or_else(|| {
803 CuError::from(format!("Unknown destination node '{}'", connection.dst))
804 })?;
805 original_to_plan[original_id as usize].ok_or_else(|| {
806 CuError::from(format!(
807 "Destination node '{}' is not a task",
808 connection.dst
809 ))
810 })?
811 };
812
813 plan_graph
814 .connect_ext_with_order(
815 src_plan,
816 dst_plan,
817 &connection.msg,
818 connection.missions.clone(),
819 None,
820 None,
821 connection.order,
822 )
823 .map_err(|error| CuError::from(error.to_string()))?;
824 }
825
826 Ok(PlanGraph {
827 graph: plan_graph,
828 entities,
829 plan_to_original,
830 })
831}
832
833fn assemble_from_order(plan_graph: PlanGraph, order: StepOrder) -> CuResult<AssembledPlan> {
836 check_order(&plan_graph.graph, &order)?;
837 let mut execution = plan_from_order(&plan_graph.graph, &order)?;
838 expand_anytime_steps(&mut execution)?;
839 Ok(AssembledPlan {
840 execution,
841 entities: plan_graph.entities,
842 plan_to_original: plan_graph.plan_to_original,
843 })
844}
845
846#[doc(hidden)]
853pub fn assemble_runtime_plan(config: &CuConfig, graph: &CuGraph) -> CuResult<AssembledPlan> {
854 let planner: Box<dyn CuPlanner> = match config.planner_config() {
855 None => Box::new(Linearity),
856 Some(selection) => instantiate_builtin_planner(selection.get_type(), selection.get_config())?
857 .ok_or_else(|| {
858 CuError::from(format!(
859 "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.",
860 selection.get_type(),
861 BUILTIN_PLANNERS.join(", "),
862 selection.get_type(),
863 ))
864 })?,
865 };
866 assemble_runtime_plan_with_planner(config, graph, planner.as_ref())
867}
868
869#[doc(hidden)]
871pub fn assemble_runtime_plan_with_planner(
872 config: &CuConfig,
873 graph: &CuGraph,
874 planner: &dyn CuPlanner,
875) -> CuResult<AssembledPlan> {
876 let plan_graph = build_plan_graph(config, graph)?;
877 let order = planner.plan(&plan_graph.graph)?;
878 assemble_from_order(plan_graph, order)
879}
880
881#[doc(hidden)]
884pub fn assemble_runtime_plan_from_step_keys(
885 config: &CuConfig,
886 graph: &CuGraph,
887 step_keys: &[String],
888) -> CuResult<AssembledPlan> {
889 let plan_graph = build_plan_graph(config, graph)?;
890 let by_key: BTreeMap<&str, NodeId> = plan_graph
891 .entities
892 .iter()
893 .enumerate()
894 .map(|(id, entity)| (entity.key.as_str(), id as NodeId))
895 .collect();
896 let order = step_keys
897 .iter()
898 .map(|key| {
899 by_key.get(key.as_str()).copied().ok_or_else(|| {
900 CuError::from(format!(
901 "Resolved plan references unknown step '{key}'; the baked order no longer matches the config."
902 ))
903 })
904 })
905 .collect::<CuResult<Vec<NodeId>>>()
906 .map(StepOrder)?;
907 assemble_from_order(plan_graph, order)
908}
909
910fn find_channel_plan_node(
911 config: &CuConfig,
912 channel_nodes: &[(usize, usize, ChannelDirection, NodeId)],
913 bridge_id: &str,
914 channel_id: &str,
915 direction: ChannelDirection,
916) -> CuResult<NodeId> {
917 channel_nodes
918 .iter()
919 .find_map(
920 |(bridge_index, channel_index, candidate_direction, node_id)| {
921 let bridge = &config.bridges[*bridge_index];
922 let channel = &bridge.channels[*channel_index];
923 (bridge.id == bridge_id
924 && channel.id() == channel_id
925 && *candidate_direction == direction)
926 .then_some(*node_id)
927 },
928 )
929 .ok_or_else(|| {
930 CuError::from(format!(
931 "Bridge channel '{bridge_id}/{channel_id}' is missing from the execution plan"
932 ))
933 })
934}
935
936#[doc(hidden)]
938pub fn mission_graphs(config: &CuConfig) -> Vec<(String, &CuGraph)> {
939 match &config.graphs {
940 ConfigGraphs::Simple(graph) => vec![("default".to_string(), graph)],
941 ConfigGraphs::Missions(graphs) => {
942 let mut missions: Vec<_> = graphs
943 .iter()
944 .map(|(mission, graph)| (mission.clone(), graph))
945 .collect();
946 missions.sort_by(|left, right| left.0.cmp(&right.0));
947 missions
948 }
949 }
950}
951
952#[doc(hidden)]
954pub fn step_key(
955 mission: &str,
956 entity: &PlanEntity,
957 phase: CuStepPhase,
958 refine_ordinal: Option<u32>,
959) -> String {
960 let phase = match phase {
961 CuStepPhase::Whole => "whole".to_string(),
962 CuStepPhase::AnytimeBase => "base".to_string(),
963 CuStepPhase::AnytimeRefine => format!("refine:{}", refine_ordinal.unwrap_or(0)),
964 };
965 format!("mission:{mission}|{}|phase:{phase}", entity.key)
966}
967
968#[doc(hidden)]
970pub const PLAN_ARTIFACT_FILE: &str = "cu29_plan.ron";
971
972#[doc(hidden)]
976#[derive(Serialize, Deserialize)]
977pub struct PlanArtifact {
978 pub planner_type: String,
979 pub config_digest: String,
980 pub orders: BTreeMap<String, Vec<String>>,
981}
982
983#[doc(hidden)]
990pub fn config_digest(config: &CuConfig) -> CuResult<String> {
991 let ron = config.serialize_ron()?;
992 let value: ron::Value = CuConfig::get_options()
993 .from_str(&ron)
994 .map_err(|e| CuError::from(format!("Could not re-parse the config for digesting: {e}")))?;
995 let mut canonical = String::new();
996 write_canonical_ron(&value, &mut canonical);
997 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
998 for byte in canonical.into_bytes() {
999 hash ^= u64::from(byte);
1000 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
1001 }
1002 Ok(format!("{hash:016x}"))
1003}
1004
1005fn write_canonical_ron(value: &ron::Value, out: &mut String) {
1006 use core::fmt::Write;
1007 match value {
1008 ron::Value::Map(map) => {
1009 let mut entries: Vec<(String, &ron::Value)> = map
1010 .iter()
1011 .map(|(key, entry)| {
1012 let mut rendered = String::new();
1013 write_canonical_ron(key, &mut rendered);
1014 (rendered, entry)
1015 })
1016 .collect();
1017 entries.sort_by(|left, right| left.0.cmp(&right.0));
1018 out.push('{');
1019 for (key, entry) in entries {
1020 out.push_str(&key);
1021 out.push(':');
1022 write_canonical_ron(entry, out);
1023 out.push(',');
1024 }
1025 out.push('}');
1026 }
1027 ron::Value::Seq(entries) => {
1028 out.push('[');
1029 for entry in entries {
1030 write_canonical_ron(entry, out);
1031 out.push(',');
1032 }
1033 out.push(']');
1034 }
1035 other => {
1036 let _ = write!(out, "{other:?}");
1037 }
1038 }
1039}
1040
1041#[cfg(feature = "std")]
1057pub fn emit_plan<P: CuPlanner>(config_path: &str) -> CuResult<()> {
1058 let out_dir = std::env::var("OUT_DIR")
1059 .map_err(|_| CuError::from("emit_plan must run from a build.rs (OUT_DIR is not set)"))?;
1060 let artifact = build_plan_artifact::<P>(config_path)?;
1061 let ron = ron::ser::to_string(&artifact)
1062 .map_err(|e| CuError::from(format!("Could not serialize the plan artifact: {e}")))?;
1063 let path = std::path::Path::new(&out_dir).join(PLAN_ARTIFACT_FILE);
1064 std::fs::write(&path, ron)
1065 .map_err(|e| CuError::new_with_cause("Could not write the plan artifact", e))?;
1066 println!("cargo::rerun-if-changed={config_path}");
1067 Ok(())
1068}
1069
1070#[cfg(feature = "std")]
1072fn build_plan_artifact<P: CuPlanner>(config_path: &str) -> CuResult<PlanArtifact> {
1073 let features_var = std::env::var("CARGO_CFG_FEATURE").unwrap_or_default();
1076 let features: Vec<&str> = features_var.split(',').filter(|f| !f.is_empty()).collect();
1077 let config = crate::config::read_configuration_with_features(config_path, &features)?;
1078 let planner = P::new(
1079 config
1080 .planner_config()
1081 .and_then(|selection| selection.get_config()),
1082 )?;
1083 let mut orders = BTreeMap::new();
1084 for (mission, graph) in mission_graphs(&config) {
1085 let keys = (|| -> CuResult<Vec<String>> {
1086 let plan_graph = build_plan_graph(&config, graph)?;
1087 let order = planner.plan(&plan_graph.graph)?;
1088 check_order(&plan_graph.graph, &order)?;
1089 Ok(order
1090 .0
1091 .iter()
1092 .map(|&id| plan_graph.entities[id as usize].key.clone())
1093 .collect())
1094 })()
1095 .map_err(|e| CuError::from(format!("mission '{mission}': {e}")))?;
1096 orders.insert(mission, keys);
1097 }
1098 Ok(PlanArtifact {
1099 planner_type: core::any::type_name::<P>().to_string(),
1100 config_digest: config_digest(&config)?,
1101 orders,
1102 })
1103}
1104
1105#[doc(hidden)]
1107#[cfg(feature = "std")]
1108pub fn read_plan_artifact(path: &std::path::Path) -> CuResult<PlanArtifact> {
1109 let text = std::fs::read_to_string(path)
1110 .map_err(|e| CuError::new_with_cause("Could not read the plan artifact", e))?;
1111 ron::from_str(&text)
1112 .map_err(|e| CuError::from(format!("Could not parse the plan artifact: {e}")))
1113}
1114
1115#[cfg(test)]
1116mod tests {
1117 use super::*;
1118 use crate::curuntime::CuExecutionUnit;
1119
1120 fn config(ron: &str) -> CuConfig {
1121 CuConfig::deserialize_ron(ron).expect("valid planner test config")
1122 }
1123
1124 fn step_labels(plan: &AssembledPlan) -> Vec<String> {
1125 plan.execution
1126 .steps
1127 .iter()
1128 .map(|unit| match unit {
1129 CuExecutionUnit::Step(step) => plan.entities[step.node_id as usize].label.clone(),
1130 CuExecutionUnit::Loop(_) => panic!("unexpected nested loop"),
1131 })
1132 .collect()
1133 }
1134
1135 #[test]
1136 fn plans_diamond_fan_in_with_stable_input_order() {
1137 let config = config(
1138 r#"(
1139 tasks: [
1140 (id: "left", type: "demo::Left"),
1141 (id: "right", type: "demo::Right"),
1142 (id: "join", type: "demo::Join"),
1143 (id: "sink", type: "demo::Sink"),
1144 ],
1145 cnx: [
1146 (src: "right", dst: "join", msg: "demo::RightMsg"),
1147 (src: "left", dst: "join", msg: "demo::LeftMsg"),
1148 (src: "join", dst: "sink", msg: "demo::Joined"),
1149 ],
1150 )"#,
1151 );
1152 let graph = config.get_graph(None).unwrap();
1153 let plan = assemble_runtime_plan(&config, graph).unwrap();
1154 assert_eq!(step_labels(&plan), ["left", "right", "join", "sink"]);
1155 let join = plan
1156 .execution
1157 .steps
1158 .iter()
1159 .find_map(|unit| match unit {
1160 CuExecutionUnit::Step(step) if step.node.get_id() == "join" => Some(step),
1161 _ => None,
1162 })
1163 .unwrap();
1164 assert_eq!(join.input_msg_indices_types.len(), 2);
1165 assert_eq!(join.input_msg_indices_types[0].msg_type, "demo::RightMsg");
1166 assert_eq!(join.input_msg_indices_types[1].msg_type, "demo::LeftMsg");
1167 }
1168
1169 #[test]
1170 fn inserts_bridge_rx_and_tx_channel_stages() {
1171 let config = config(
1172 r#"(
1173 tasks: [
1174 (id: "task", type: "demo::Task"),
1175 ],
1176 bridges: [
1177 (
1178 id: "radio",
1179 type: "demo::Radio",
1180 channels: [Rx(id: "incoming"), Tx(id: "outgoing")],
1181 ),
1182 ],
1183 cnx: [
1184 (src: "radio/incoming", dst: "task", msg: "demo::In"),
1185 (src: "task", dst: "radio/outgoing", msg: "demo::Out"),
1186 ],
1187 )"#,
1188 );
1189 let graph = config.get_graph(None).unwrap();
1190 let plan = assemble_runtime_plan(&config, graph).unwrap();
1191 assert_eq!(
1192 step_labels(&plan),
1193 ["radio::rx::incoming", "task", "radio::tx::outgoing"]
1194 );
1195 assert!(matches!(
1196 plan.entities[1].kind,
1197 PlanEntityKind::BridgeRx { .. }
1198 ));
1199 assert!(matches!(
1200 plan.entities[2].kind,
1201 PlanEntityKind::BridgeTx { .. }
1202 ));
1203 }
1204
1205 #[test]
1206 fn synthesizes_declared_unconnected_output() {
1207 let config = config(
1208 r#"(
1209 tasks: [(id: "generated", type: "demo::Generated", kind: source)],
1210 cnx: [],
1211 )"#,
1212 );
1213 let graph = config.get_graph(None).unwrap();
1214 let plan = assemble_runtime_plan(&config, graph).unwrap();
1215 let CuExecutionUnit::Step(step) = &plan.execution.steps[0] else {
1216 panic!("expected one generated step")
1217 };
1218 let output = step.output_msg_pack.as_ref().unwrap();
1219 assert_eq!(output.culist_index, 0);
1220 assert!(output.msg_types[0].contains("CuSingleOutputMsg"));
1221 assert!(output.msg_types[0].contains("CuSrcTask"));
1222 }
1223
1224 fn pinned_graph(planner: &str) -> CuConfig {
1229 config(&format!(
1230 r#"(
1231 tasks: [
1232 (id: "cam", type: "demo::Cam"),
1233 (id: "ekf", type: "demo::Ekf"),
1234 (id: "motor", type: "demo::Motor"),
1235 ],
1236 bridges: [(
1237 id: "radio",
1238 type: "demo::Radio",
1239 channels: [Rx(id: "incoming"), Tx(id: "outgoing")],
1240 )],
1241 cnx: [
1242 (src: "radio/incoming", dst: "cam", msg: "demo::In"),
1243 (src: "cam", dst: "ekf", msg: "demo::Frame"),
1244 (src: "ekf", dst: "motor", msg: "demo::State"),
1245 (src: "motor", dst: "radio/outgoing", msg: "demo::Cmd"),
1246 ],
1247 runtime: (planner: {planner}),
1248 )"#
1249 ))
1250 }
1251
1252 fn pinned(ids: &[&str]) -> String {
1253 let quoted: Vec<String> = ids.iter().map(|id| format!("{id:?}")).collect();
1254 format!(
1255 r#"(type: "cu29::planner::Pinned", config: {{ "order": [{}] }})"#,
1256 quoted.join(", ")
1257 )
1258 }
1259
1260 #[test]
1261 fn pinned_plan_weaves_bridge_stages_and_matches_task_order() {
1262 let config = pinned_graph(&pinned(&["cam", "ekf", "motor"]));
1263 let graph = config.get_graph(None).unwrap();
1264 let plan = assemble_runtime_plan(&config, graph).unwrap();
1265 assert_eq!(
1266 step_labels(&plan),
1267 [
1268 "radio::rx::incoming",
1269 "cam",
1270 "ekf",
1271 "motor",
1272 "radio::tx::outgoing"
1273 ]
1274 );
1275 }
1276
1277 #[test]
1278 fn pinned_plan_rejects_bad_id_lists() {
1279 let rejects = |planner: &str| {
1280 let config = pinned_graph(planner);
1281 let graph = config.get_graph(None).unwrap();
1282 assemble_runtime_plan(&config, graph)
1283 .err()
1284 .unwrap()
1285 .to_string()
1286 };
1287
1288 let err = rejects(&pinned(&["cam", "ekf"]));
1289 assert!(err.contains("missing"), "{err}");
1290
1291 let err = rejects(&pinned(&["radio::rx::incoming", "cam", "ekf", "motor"]));
1292 assert!(err.contains("bridge stage"), "{err}");
1293
1294 let err = rejects(&pinned(&["cam", "ekf", "motor", "ghost"]));
1295 assert!(err.contains("unknown task 'ghost'"), "{err}");
1296
1297 let err = rejects(&pinned(&["cam", "cam", "ekf"]));
1298 assert!(err.contains("more than once"), "{err}");
1299
1300 let err = rejects(r#"(type: "cu29::planner::Pinned")"#);
1301 assert!(err.contains("needs config"), "{err}");
1302
1303 let err = rejects(r#"(type: "acme::Planner")"#);
1305 assert!(err.contains("emit_plan"), "{err}");
1306 }
1307
1308 struct ReverseAlpha;
1313
1314 impl CuPlanner for ReverseAlpha {
1315 fn new(_config: Option<&ComponentConfig>) -> CuResult<Self> {
1316 Ok(ReverseAlpha)
1317 }
1318
1319 fn plan(&self, graph: &CuGraph) -> CuResult<StepOrder> {
1320 let mut order = Vec::new();
1321 let mut planned: BTreeSet<NodeId> = BTreeSet::new();
1322 while order.len() < graph.node_count() {
1323 let next = graph
1324 .get_all_nodes()
1325 .into_iter()
1326 .filter(|(id, _)| !planned.contains(id))
1327 .filter(|(id, _)| {
1328 graph
1329 .get_neighbor_ids(*id, CuDirection::Incoming)
1330 .iter()
1331 .all(|input| planned.contains(input))
1332 })
1333 .max_by_key(|(_, node)| node.get_id())
1334 .map(|(id, _)| id)
1335 .expect("acyclic graph always has a ready node");
1336 planned.insert(next);
1337 order.push(next);
1338 }
1339 Ok(StepOrder(order))
1340 }
1341 }
1342
1343 #[test]
1344 fn custom_planner_orders_the_plan() {
1345 let config = config(
1346 r#"(
1347 tasks: [
1348 (id: "left", type: "demo::Left"),
1349 (id: "right", type: "demo::Right"),
1350 (id: "join", type: "demo::Join"),
1351 (id: "sink", type: "demo::Sink"),
1352 ],
1353 cnx: [
1354 (src: "left", dst: "join", msg: "demo::LeftMsg"),
1355 (src: "right", dst: "join", msg: "demo::RightMsg"),
1356 (src: "join", dst: "sink", msg: "demo::Joined"),
1357 ],
1358 )"#,
1359 );
1360 let graph = config.get_graph(None).unwrap();
1361 let plan = assemble_runtime_plan_with_planner(&config, graph, &ReverseAlpha).unwrap();
1362 assert_eq!(step_labels(&plan), ["right", "left", "join", "sink"]);
1363
1364 let keys: Vec<String> = plan
1366 .execution
1367 .steps
1368 .iter()
1369 .map(|unit| match unit {
1370 CuExecutionUnit::Step(step) => plan.entities[step.node_id as usize].key.clone(),
1371 CuExecutionUnit::Loop(_) => panic!("unexpected nested loop"),
1372 })
1373 .collect();
1374 let replayed = assemble_runtime_plan_from_step_keys(&config, graph, &keys).unwrap();
1375 assert_eq!(step_labels(&replayed), step_labels(&plan));
1376
1377 let err = assemble_runtime_plan_from_step_keys(&config, graph, &["task:ghost".to_string()])
1378 .err()
1379 .unwrap()
1380 .to_string();
1381 assert!(err.contains("unknown step 'task:ghost'"), "{err}");
1382 }
1383
1384 struct Backwards;
1386
1387 impl CuPlanner for Backwards {
1388 fn new(_config: Option<&ComponentConfig>) -> CuResult<Self> {
1389 Ok(Backwards)
1390 }
1391
1392 fn plan(&self, graph: &CuGraph) -> CuResult<StepOrder> {
1393 let StepOrder(mut order) = topo_bfs_order(graph)?;
1394 order.reverse();
1395 Ok(StepOrder(order))
1396 }
1397 }
1398
1399 #[test]
1400 fn illegal_planner_output_is_rejected() {
1401 let config = build_config(&["s", "k"], &[("s", "k", "m")]);
1402 let graph = config.get_graph(None).unwrap();
1403 let err = assemble_runtime_plan_with_planner(&config, graph, &Backwards)
1404 .err()
1405 .unwrap()
1406 .to_string();
1407 assert!(err.contains("scheduled before its input"), "{err}");
1408 }
1409
1410 #[test]
1411 fn canonical_ron_ignores_map_entry_order() {
1412 let render = |txt: &str| {
1416 let value: ron::Value = ron::from_str(txt).unwrap();
1417 let mut out = String::new();
1418 write_canonical_ron(&value, &mut out);
1419 out
1420 };
1421 assert_eq!(
1422 render(r#"{"a": 1, "b": [2, 3], "c": {"x": 4, "y": 5}}"#),
1423 render(r#"{"c": {"y": 5, "x": 4}, "b": [2, 3], "a": 1}"#),
1424 );
1425 assert_ne!(render(r#"{"b": [2, 3]}"#), render(r#"{"b": [3, 2]}"#));
1426 }
1427
1428 #[test]
1429 fn check_order_flags_precedence_and_missing() {
1430 let config = build_config(&["s", "k"], &[("s", "k", "m")]);
1431 let graph = config.get_graph(None).unwrap();
1432 let s = graph.get_node_id_by_name("s").unwrap();
1433 let k = graph.get_node_id_by_name("k").unwrap();
1434
1435 let err = check_order(graph, &StepOrder(vec![k, s])).unwrap_err();
1437 assert!(
1438 err.to_string().contains("scheduled before its input"),
1439 "{err}"
1440 );
1441
1442 let err = check_order(graph, &StepOrder(vec![s])).unwrap_err();
1444 assert!(err.to_string().contains("Missing"), "{err}");
1445 }
1446
1447 fn build_config(nodes: &[&str], edges: &[(&str, &str, &str)]) -> CuConfig {
1451 let mut config = CuConfig::default();
1452 let graph = config.get_graph_mut(None).unwrap();
1453 let mut ids: BTreeMap<String, NodeId> = BTreeMap::new();
1454 for &name in nodes {
1455 let id = graph.add_node(Node::new(name, "demo::T")).unwrap();
1456 ids.insert(name.to_string(), id);
1457 }
1458 for &(src, dst, msg) in edges {
1459 graph.connect(ids[src], ids[dst], msg).unwrap();
1460 }
1461 config
1462 }
1463
1464 fn corpus() -> Vec<(String, CuConfig)> {
1467 let mut cases = vec![
1470 (
1471 "chain".to_string(),
1472 build_config(
1473 &["s", "r1", "r2", "k"],
1474 &[("s", "r1", "m0"), ("r1", "r2", "m1"), ("r2", "k", "m2")],
1475 ),
1476 ),
1477 (
1478 "fanout_shared_msg".to_string(),
1479 build_config(
1480 &["s", "a", "b", "c"],
1481 &[("s", "a", "m"), ("s", "b", "m"), ("s", "c", "n")],
1482 ),
1483 ),
1484 (
1485 "fanin_multisource".to_string(),
1486 build_config(
1487 &["s1", "s2", "s3", "k"],
1488 &[("s2", "k", "m2"), ("s1", "k", "m1"), ("s3", "k", "m3")],
1489 ),
1490 ),
1491 (
1492 "diamond".to_string(),
1493 build_config(
1494 &["s", "a", "b", "j", "k"],
1495 &[
1496 ("s", "a", "m0"),
1497 ("s", "b", "m1"),
1498 ("a", "j", "ma"),
1499 ("b", "j", "mb"),
1500 ("j", "k", "mj"),
1501 ],
1502 ),
1503 ),
1504 (
1505 "bridge_like".to_string(),
1506 build_config(
1507 &["rx1", "rx2", "r", "tx1", "tx2"],
1508 &[
1509 ("rx1", "r", "m1"),
1510 ("rx2", "r", "m2"),
1511 ("r", "tx1", "o1"),
1512 ("r", "tx2", "o2"),
1513 ],
1514 ),
1515 ),
1516 (
1517 "multisource_layers".to_string(),
1518 build_config(
1519 &["s1", "s2", "r1", "r2", "k"],
1520 &[
1521 ("s1", "r1", "a"),
1522 ("s2", "r1", "b"),
1523 ("s1", "r2", "c"),
1524 ("s2", "r2", "d"),
1525 ("r1", "k", "e"),
1526 ("r2", "k", "f"),
1527 ],
1528 ),
1529 ),
1530 (
1531 "side_branch".to_string(),
1532 build_config(
1533 &["s", "r1", "r2", "r3", "k1", "k2"],
1534 &[
1535 ("s", "r1", "m0"),
1536 ("r1", "r2", "m1"),
1537 ("r1", "r3", "m2"),
1538 ("r2", "k1", "m3"),
1539 ("r3", "k2", "m4"),
1540 ],
1541 ),
1542 ),
1543 ];
1544 for seed in 0u64..6 {
1546 cases.push((format!("layered_{seed}"), layered_dag(seed)));
1547 }
1548 cases
1549 }
1550
1551 fn layered_dag(seed: u64) -> CuConfig {
1554 let layers = [2usize, 3, 3, 2];
1555 let mut state = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
1556 let mut next = || {
1557 state = state
1558 .wrapping_mul(6364136223846793005)
1559 .wrapping_add(1442695040888963407);
1560 state >> 33
1561 };
1562 let name = |layer: usize, idx: usize| format!("n{layer}_{idx}");
1563 let mut nodes = Vec::new();
1564 for (layer, count) in layers.iter().enumerate() {
1565 for idx in 0..*count {
1566 nodes.push(name(layer, idx));
1567 }
1568 }
1569 let node_refs: Vec<&str> = nodes.iter().map(|s| s.as_str()).collect();
1570 let mut edges: Vec<(String, String, String)> = Vec::new();
1571 for layer in 0..layers.len() - 1 {
1572 for from in 0..layers[layer] {
1573 let mut connected = false;
1575 for to in 0..layers[layer + 1] {
1576 if next() % 2 == 0 || (to == layers[layer + 1] - 1 && !connected) {
1577 edges.push((
1578 name(layer, from),
1579 name(layer + 1, to),
1580 format!("m{layer}_{from}_{to}"),
1581 ));
1582 connected = true;
1583 }
1584 }
1585 }
1586 for to in 0..layers[layer + 1] {
1588 if !edges.iter().any(|(_, d, _)| *d == name(layer + 1, to)) {
1589 edges.push((
1590 name(layer, 0),
1591 name(layer + 1, to),
1592 format!("f{layer}_{to}"),
1593 ));
1594 }
1595 }
1596 }
1597 let edge_refs: Vec<(&str, &str, &str)> = edges
1598 .iter()
1599 .map(|(s, d, m)| (s.as_str(), d.as_str(), m.as_str()))
1600 .collect();
1601 build_config(&node_refs, &edge_refs)
1602 }
1603
1604 fn assert_same_plan(name: &str, config: &CuConfig) {
1607 let graph = config.get_graph(None).unwrap();
1608 let legacy = compute_runtime_plan_legacy(graph).expect("legacy plan");
1609 let fresh = crate::curuntime::compute_runtime_plan(graph).expect("new plan");
1610 assert_eq!(
1611 legacy.steps.len(),
1612 fresh.steps.len(),
1613 "{name}: step count differs"
1614 );
1615 for (index, (a, b)) in legacy.steps.iter().zip(fresh.steps.iter()).enumerate() {
1616 let (CuExecutionUnit::Step(a), CuExecutionUnit::Step(b)) = (a, b) else {
1617 panic!("{name}: unexpected nested loop");
1618 };
1619 assert_eq!(a.node_id, b.node_id, "{name}: step {index} node id");
1620 assert_eq!(a.phase, b.phase, "{name}: step {index} phase");
1621 let (oa, ob) = (a.output_msg_pack.as_ref(), b.output_msg_pack.as_ref());
1622 assert_eq!(
1623 oa.map(|p| p.culist_index),
1624 ob.map(|p| p.culist_index),
1625 "{name}: step {index} culist index"
1626 );
1627 assert_eq!(
1628 oa.map(|p| &p.msg_types),
1629 ob.map(|p| &p.msg_types),
1630 "{name}: step {index} output msg types"
1631 );
1632 assert_eq!(
1633 a.input_msg_indices_types.len(),
1634 b.input_msg_indices_types.len(),
1635 "{name}: step {index} input arity"
1636 );
1637 for (ia, ib) in a
1638 .input_msg_indices_types
1639 .iter()
1640 .zip(b.input_msg_indices_types.iter())
1641 {
1642 assert_eq!(ia.culist_index, ib.culist_index, "{name}: input culist");
1643 assert_eq!(ia.msg_type, ib.msg_type, "{name}: input msg");
1644 assert_eq!(ia.src_port, ib.src_port, "{name}: input src_port");
1645 assert_eq!(ia.edge_id, ib.edge_id, "{name}: input edge_id");
1646 assert_eq!(
1647 ia.connection_order, ib.connection_order,
1648 "{name}: input connection_order"
1649 );
1650 }
1651 }
1652 }
1653
1654 #[test]
1655 fn topo_bfs_matches_legacy_walk_over_corpus() {
1656 for (name, config) in corpus() {
1657 assert_same_plan(&name, &config);
1658 }
1659 }
1660
1661 fn find_output_pack_from_nodeid_legacy(
1663 node_id: NodeId,
1664 steps: &[CuExecutionUnit],
1665 ) -> Option<CuOutputPack> {
1666 for step in steps {
1667 match step {
1668 CuExecutionUnit::Loop(loop_unit) => {
1669 if let Some(pack) =
1670 find_output_pack_from_nodeid_legacy(node_id, &loop_unit.steps)
1671 {
1672 return Some(pack);
1673 }
1674 }
1675 CuExecutionUnit::Step(step) if step.node_id == node_id => {
1676 return step.output_msg_pack.clone();
1677 }
1678 _ => {}
1679 }
1680 }
1681 None
1682 }
1683
1684 fn plan_tasks_tree_branch_legacy(
1685 graph: &CuGraph,
1686 mut next_culist_output_index: u32,
1687 starting_point: NodeId,
1688 plan: &mut Vec<CuExecutionUnit>,
1689 ) -> CuResult<(u32, bool)> {
1690 let mut handled = false;
1691 for id in graph.bfs_nodes(starting_point) {
1692 let node_ref = graph.get_node(id).unwrap();
1693 let mut input_msg_indices_types: Vec<CuInputMsg> = Vec::new();
1694 let output_msg_pack: Option<CuOutputPack>;
1695 let task_type = find_task_type_for_id(graph, id)?;
1696 match task_type {
1697 CuTaskType::Source => {
1698 let msg_types = graph.get_node_output_msg_types_by_id(id)?;
1699 if msg_types.is_empty() {
1700 return Err(CuError::from(format!(
1701 "Source node '{}' has no declared outputs",
1702 node_ref.get_id()
1703 )));
1704 }
1705 output_msg_pack = Some(CuOutputPack {
1706 culist_index: next_culist_output_index,
1707 src_channels: vec![None; msg_types.len()],
1708 msg_types,
1709 });
1710 next_culist_output_index += 1;
1711 }
1712 CuTaskType::Sink => {
1713 let mut edge_ids = graph.get_dst_edges(id).unwrap_or_default();
1714 edge_ids.sort();
1715 for edge_id in edge_ids {
1716 let edge = graph
1717 .edge(edge_id)
1718 .unwrap_or_else(|| panic!("Missing edge {edge_id} for node {id}"));
1719 let pid =
1720 graph
1721 .get_node_id_by_name(edge.src.as_str())
1722 .unwrap_or_else(|| {
1723 panic!("Missing source node '{}' for edge {edge_id}", edge.src)
1724 });
1725 let output_pack = find_output_pack_from_nodeid_legacy(pid, plan);
1726 if let Some(output_pack) = output_pack {
1727 let msg_type = edge.msg.as_str();
1728 let src_port = output_pack
1729 .msg_types
1730 .iter()
1731 .position(|msg| msg == msg_type)
1732 .unwrap_or_else(|| {
1733 panic!(
1734 "Missing output port for message type '{msg_type}' on node {pid}"
1735 )
1736 });
1737 input_msg_indices_types.push(CuInputMsg {
1738 culist_index: output_pack.culist_index,
1739 msg_type: msg_type.to_string(),
1740 src_port,
1741 edge_id,
1742 connection_order: edge.order,
1743 });
1744 } else {
1745 return Ok((next_culist_output_index, handled));
1746 }
1747 }
1748 output_msg_pack = Some(CuOutputPack {
1749 culist_index: next_culist_output_index,
1750 msg_types: Vec::from(["()".to_string()]),
1751 src_channels: vec![None],
1752 });
1753 next_culist_output_index += 1;
1754 }
1755 CuTaskType::Regular => {
1756 let mut edge_ids = graph.get_dst_edges(id).unwrap_or_default();
1757 edge_ids.sort();
1758 for edge_id in edge_ids {
1759 let edge = graph
1760 .edge(edge_id)
1761 .unwrap_or_else(|| panic!("Missing edge {edge_id} for node {id}"));
1762 let pid =
1763 graph
1764 .get_node_id_by_name(edge.src.as_str())
1765 .unwrap_or_else(|| {
1766 panic!("Missing source node '{}' for edge {edge_id}", edge.src)
1767 });
1768 let output_pack = find_output_pack_from_nodeid_legacy(pid, plan);
1769 if let Some(output_pack) = output_pack {
1770 let msg_type = edge.msg.as_str();
1771 let src_port = output_pack
1772 .msg_types
1773 .iter()
1774 .position(|msg| msg == msg_type)
1775 .unwrap_or_else(|| {
1776 panic!(
1777 "Missing output port for message type '{msg_type}' on node {pid}"
1778 )
1779 });
1780 input_msg_indices_types.push(CuInputMsg {
1781 culist_index: output_pack.culist_index,
1782 msg_type: msg_type.to_string(),
1783 src_port,
1784 edge_id,
1785 connection_order: edge.order,
1786 });
1787 } else {
1788 return Ok((next_culist_output_index, handled));
1789 }
1790 }
1791 let msg_types = graph.get_node_output_msg_types_by_id(id)?;
1792 if msg_types.is_empty() {
1793 return Err(CuError::from(format!(
1794 "Regular node '{}' has no declared outputs",
1795 node_ref.get_id()
1796 )));
1797 }
1798 output_msg_pack = Some(CuOutputPack {
1799 culist_index: next_culist_output_index,
1800 src_channels: vec![None; msg_types.len()],
1801 msg_types,
1802 });
1803 next_culist_output_index += 1;
1804 }
1805 }
1806
1807 sort_inputs_by_connection_order(&mut input_msg_indices_types);
1808 if let Some(pos) = plan
1809 .iter()
1810 .position(|step| matches!(step, CuExecutionUnit::Step(s) if s.node_id == id))
1811 {
1812 let mut step = plan.remove(pos);
1813 if let CuExecutionUnit::Step(ref mut s) = step {
1814 s.input_msg_indices_types = input_msg_indices_types;
1815 }
1816 plan.push(step);
1817 } else {
1818 let step = CuExecutionStep {
1819 node_id: id,
1820 node: node_ref.clone(),
1821 task_type,
1822 phase: CuStepPhase::default(),
1823 input_msg_indices_types,
1824 output_msg_pack,
1825 };
1826 plan.push(CuExecutionUnit::Step(Box::new(step)));
1827 }
1828 handled = true;
1829 }
1830 Ok((next_culist_output_index, handled))
1831 }
1832
1833 fn compute_runtime_plan_legacy(graph: &CuGraph) -> CuResult<CuExecutionLoop> {
1836 let mut plan = Vec::new();
1837 let mut next_culist_output_index = 0u32;
1838 let mut queue: VecDeque<NodeId> = VecDeque::new();
1839 for node_id in graph.node_ids() {
1840 if find_task_type_for_id(graph, node_id)? == CuTaskType::Source {
1841 queue.push_back(node_id);
1842 }
1843 }
1844 while let Some(start_node) = queue.pop_front() {
1845 for node_id in graph.bfs_nodes(start_node) {
1846 let already = plan
1847 .iter()
1848 .any(|unit| matches!(unit, CuExecutionUnit::Step(s) if s.node_id == node_id));
1849 if already {
1850 continue;
1851 }
1852 let (new_index, handled) = plan_tasks_tree_branch_legacy(
1853 graph,
1854 next_culist_output_index,
1855 node_id,
1856 &mut plan,
1857 )?;
1858 next_culist_output_index = new_index;
1859 if !handled {
1860 continue;
1861 }
1862 for neighbor in graph.get_neighbor_ids(node_id, CuDirection::Outgoing) {
1863 queue.push_back(neighbor);
1864 }
1865 }
1866 }
1867 let mut planned_nodes = BTreeSet::new();
1868 for unit in &plan {
1869 if let CuExecutionUnit::Step(step) = unit {
1870 planned_nodes.insert(step.node_id);
1871 }
1872 }
1873 let mut missing = Vec::new();
1874 for node_id in graph.node_ids() {
1875 if !planned_nodes.contains(&node_id) {
1876 if let Some(node) = graph.get_node(node_id) {
1877 missing.push(node.get_id().to_string());
1878 } else {
1879 missing.push(format!("node_id_{node_id}"));
1880 }
1881 }
1882 }
1883 if !missing.is_empty() {
1884 missing.sort();
1885 return Err(CuError::from(format!(
1886 "Execution plan could not include all nodes. Missing: {}. Check for loopback or missing source connections.",
1887 missing.join(", ")
1888 )));
1889 }
1890 Ok(CuExecutionLoop {
1891 steps: plan,
1892 loop_count: None,
1893 })
1894 }
1895}