1use proc_macro::TokenStream;
2use quote::{ToTokens, format_ident, quote};
3use std::collections::{BTreeMap, BTreeSet, HashMap};
4use std::fs::read_to_string;
5use std::path::Path;
6use std::process::Command;
7use syn::Fields::{Named, Unnamed};
8use syn::meta::parser;
9use syn::parse::Parser;
10use syn::punctuated::Punctuated;
11use syn::{
12 Block, Expr, Field, Fields, ItemFn, ItemImpl, ItemStruct, Lit, LitStr, Stmt, Token, Type,
13 TypeTuple, parse_macro_input, parse_quote, parse_str,
14};
15
16use crate::utils::{config_id_to_bridge_const, config_id_to_enum, config_id_to_struct_member};
17use cu29_runtime::config::CuConfig;
18use cu29_runtime::config::{
19 BridgeChannelConfigRepresentation, ConfigGraphs, CuGraph, Flavor, HandleContent, Node, NodeId,
20 RT_POOL, ResourceBundleConfig, read_configuration,
21};
22use cu29_runtime::curuntime::{
23 CuExecutionLoop, CuExecutionStep, CuExecutionUnit, CuTaskType, compute_runtime_plan,
24 find_task_type_for_id,
25};
26use cu29_traits::{CuError, CuResult};
27use proc_macro2::{Ident, Span};
28
29mod bundle_resources;
30mod resources;
31mod utils;
32
33const DEFAULT_CLNB: usize = 2; #[inline]
36fn int2sliceindex(i: u32) -> syn::Index {
37 syn::Index::from(i as usize)
38}
39
40#[inline(always)]
41fn return_error(msg: String) -> TokenStream {
42 syn::Error::new(Span::call_site(), msg)
43 .to_compile_error()
44 .into()
45}
46
47fn rtsan_guard_tokens() -> proc_macro2::TokenStream {
48 if cfg!(feature = "rtsan") {
49 quote! {
50 let _rt_guard = ::cu29::rtsan::ScopedSanitizeRealtime::default();
51 }
52 } else {
53 quote! {}
54 }
55}
56
57fn alloc_scope_open_tokens() -> proc_macro2::TokenStream {
61 if cfg!(feature = "memory_monitoring") {
62 quote! {
63 let __cu_alloc_scope = cu29::monitoring::ScopedAllocCounter::new();
64 }
65 } else {
66 quote! {}
67 }
68}
69
70fn alloc_scope_close_tokens(
77 monitor_expr: proc_macro2::TokenStream,
78 component_index: proc_macro2::TokenStream,
79 step: proc_macro2::TokenStream,
80) -> proc_macro2::TokenStream {
81 if cfg!(feature = "memory_monitoring") {
82 quote! {
83 #monitor_expr.observe_alloc(
84 cu29::monitoring::ComponentId::new(#component_index),
85 #step,
86 __cu_alloc_scope.allocated(),
87 __cu_alloc_scope.deallocated(),
88 );
89 }
90 } else {
91 quote! {}
92 }
93}
94
95fn git_output_trimmed(repo_root: &Path, args: &[&str]) -> Option<String> {
96 let output = Command::new("git")
97 .arg("-C")
98 .arg(repo_root)
99 .args(args)
100 .output()
101 .ok()?;
102 if !output.status.success() {
103 return None;
104 }
105 let stdout = String::from_utf8(output.stdout).ok()?;
106 Some(stdout.trim().to_string())
107}
108
109fn detect_git_info(repo_root: &Path) -> (Option<String>, Option<bool>) {
110 let in_repo = git_output_trimmed(repo_root, &["rev-parse", "--is-inside-work-tree"])
111 .is_some_and(|value| value == "true");
112 if !in_repo {
113 return (None, None);
114 }
115
116 let commit = git_output_trimmed(repo_root, &["rev-parse", "HEAD"]).filter(|s| !s.is_empty());
117 let dirty = git_output_trimmed(repo_root, &["status", "--porcelain"]).map(|s| !s.is_empty());
119 (commit, dirty)
120}
121
122#[derive(Debug, Clone)]
123struct CopperRuntimeArgs {
124 config_path: String,
125 subsystem_id: Option<String>,
126 sim_mode: bool,
127 ignore_resources: bool,
128}
129
130impl CopperRuntimeArgs {
131 fn parse_tokens(args: proc_macro2::TokenStream) -> Result<Self, syn::Error> {
132 let mut config_file: Option<LitStr> = None;
133 let mut subsystem_id: Option<LitStr> = None;
134 let mut sim_mode = false;
135 let mut ignore_resources = false;
136
137 let parser = parser(|meta| {
138 if meta.path.is_ident("config") {
139 config_file = Some(meta.value()?.parse()?);
140 Ok(())
141 } else if meta.path.is_ident("subsystem") {
142 subsystem_id = Some(meta.value()?.parse()?);
143 Ok(())
144 } else if meta.path.is_ident("sim_mode") {
145 if meta.input.peek(syn::Token![=]) {
146 meta.input.parse::<syn::Token![=]>()?;
147 let value: syn::LitBool = meta.input.parse()?;
148 sim_mode = value.value();
149 } else {
150 sim_mode = true;
151 }
152 Ok(())
153 } else if meta.path.is_ident("ignore_resources") {
154 if meta.input.peek(syn::Token![=]) {
155 meta.input.parse::<syn::Token![=]>()?;
156 let value: syn::LitBool = meta.input.parse()?;
157 ignore_resources = value.value();
158 } else {
159 ignore_resources = true;
160 }
161 Ok(())
162 } else {
163 Err(meta.error("unsupported property"))
164 }
165 });
166
167 parser.parse2(args)?;
168
169 let config_path = config_file
170 .ok_or_else(|| {
171 syn::Error::new(
172 Span::call_site(),
173 "Expected config file attribute like #[copper_runtime(config = \"path\")]",
174 )
175 })?
176 .value();
177
178 Ok(Self {
179 config_path,
180 subsystem_id: subsystem_id.map(|value| value.value()),
181 sim_mode,
182 ignore_resources,
183 })
184 }
185}
186
187#[derive(Debug)]
188struct ResolvedRuntimeConfig {
189 local_config: CuConfig,
190 bundled_local_config_content: String,
191 subsystem_id: Option<String>,
192 subsystem_code: u16,
193}
194
195#[proc_macro]
196pub fn resources(input: TokenStream) -> TokenStream {
197 resources::resources(input)
198}
199
200#[proc_macro]
201pub fn bundle_resources(input: TokenStream) -> TokenStream {
202 bundle_resources::bundle_resources(input)
203}
204
205#[derive(Debug, Clone)]
206struct ParsedSafetyCheck {
207 check_id: String,
208 requirement_id: String,
209 kind: &'static str,
210}
211
212#[proc_macro_attribute]
213pub fn safety_case(args: TokenStream, input: TokenStream) -> TokenStream {
214 let case_id = parse_macro_input!(args as LitStr).value();
215 if let Err(err) = validate_case_id(&case_id) {
216 return err.to_compile_error().into();
217 }
218
219 let function = parse_macro_input!(input as ItemFn);
220 let checks = match collect_safety_checks(&case_id, &function.block) {
221 Ok(checks) => checks,
222 Err(err) => return err.to_compile_error().into(),
223 };
224
225 if checks.is_empty() {
226 return syn::Error::new_spanned(
227 &function.sig.ident,
228 format!("safety case '{case_id}' must contain at least one safety_check! or safety_check_eq!"),
229 )
230 .to_compile_error()
231 .into();
232 }
233
234 let function_ident = &function.sig.ident;
235 let checks_tokens = checks.iter().map(|check| {
236 let check_id = &check.check_id;
237 let requirement_id = &check.requirement_id;
238 let kind = check.kind;
239 quote! {
240 ::cu29::safety::SafetyCheckRef {
241 check_id: #check_id,
242 requirement_id: #requirement_id,
243 kind: #kind,
244 }
245 }
246 });
247
248 quote! {
249 #function
250
251 #[cfg(feature = "safety-ids")]
252 ::cu29::safety::inventory::submit! {
253 ::cu29::safety::SafetyCaseRef {
254 package: env!("CARGO_PKG_NAME"),
255 case_id: #case_id,
256 function: stringify!(#function_ident),
257 module_path: module_path!(),
258 file: file!(),
259 checks: &[#(#checks_tokens),*],
260 }
261 }
262 }
263 .into()
264}
265
266fn collect_safety_checks(
267 case_id: &str,
268 block: &Block,
269) -> Result<Vec<ParsedSafetyCheck>, syn::Error> {
270 let mut checks = Vec::new();
271 collect_safety_checks_from_block(block, &mut checks)?;
272
273 let mut ids = BTreeSet::new();
274 for check in &checks {
275 validate_check_id(case_id, &check.check_id)?;
276 validate_requirement_id(&check.requirement_id)?;
277 if !ids.insert(check.check_id.clone()) {
278 return Err(syn::Error::new(
279 Span::call_site(),
280 format!("duplicate safety check ID '{}'", check.check_id),
281 ));
282 }
283 }
284
285 Ok(checks)
286}
287
288fn collect_safety_checks_from_block(
289 block: &Block,
290 checks: &mut Vec<ParsedSafetyCheck>,
291) -> Result<(), syn::Error> {
292 for stmt in &block.stmts {
293 collect_safety_checks_from_stmt(stmt, checks)?;
294 }
295 Ok(())
296}
297
298fn collect_safety_checks_from_stmt(
299 stmt: &Stmt,
300 checks: &mut Vec<ParsedSafetyCheck>,
301) -> Result<(), syn::Error> {
302 match stmt {
303 Stmt::Local(local) => {
304 if let Some(init) = &local.init {
305 collect_safety_checks_from_expr(&init.expr, checks)?;
306 if let Some((_else, expr)) = &init.diverge {
307 collect_safety_checks_from_expr(expr, checks)?;
308 }
309 }
310 }
311 Stmt::Item(_) => {}
312 Stmt::Expr(expr, _) => collect_safety_checks_from_expr(expr, checks)?,
313 Stmt::Macro(stmt_macro) => {
314 if let Some(check) = parse_safety_check_macro(&stmt_macro.mac)? {
315 checks.push(check);
316 }
317 }
318 }
319 Ok(())
320}
321
322fn collect_safety_checks_from_expr(
323 expr: &Expr,
324 checks: &mut Vec<ParsedSafetyCheck>,
325) -> Result<(), syn::Error> {
326 match expr {
327 Expr::Array(expr) => {
328 for elem in &expr.elems {
329 collect_safety_checks_from_expr(elem, checks)?;
330 }
331 }
332 Expr::Assign(expr) => {
333 collect_safety_checks_from_expr(&expr.left, checks)?;
334 collect_safety_checks_from_expr(&expr.right, checks)?;
335 }
336 Expr::Async(expr) => collect_safety_checks_from_block(&expr.block, checks)?,
337 Expr::Await(expr) => collect_safety_checks_from_expr(&expr.base, checks)?,
338 Expr::Binary(expr) => {
339 collect_safety_checks_from_expr(&expr.left, checks)?;
340 collect_safety_checks_from_expr(&expr.right, checks)?;
341 }
342 Expr::Block(expr) => collect_safety_checks_from_block(&expr.block, checks)?,
343 Expr::Break(expr) => {
344 if let Some(value) = &expr.expr {
345 collect_safety_checks_from_expr(value, checks)?;
346 }
347 }
348 Expr::Call(expr) => {
349 collect_safety_checks_from_expr(&expr.func, checks)?;
350 for arg in &expr.args {
351 collect_safety_checks_from_expr(arg, checks)?;
352 }
353 }
354 Expr::Cast(expr) => collect_safety_checks_from_expr(&expr.expr, checks)?,
355 Expr::Closure(expr) => collect_safety_checks_from_expr(&expr.body, checks)?,
356 Expr::Field(expr) => collect_safety_checks_from_expr(&expr.base, checks)?,
357 Expr::ForLoop(expr) => {
358 collect_safety_checks_from_expr(&expr.expr, checks)?;
359 collect_safety_checks_from_block(&expr.body, checks)?;
360 }
361 Expr::Group(expr) => collect_safety_checks_from_expr(&expr.expr, checks)?,
362 Expr::If(expr) => {
363 collect_safety_checks_from_expr(&expr.cond, checks)?;
364 collect_safety_checks_from_block(&expr.then_branch, checks)?;
365 if let Some((_else, else_expr)) = &expr.else_branch {
366 collect_safety_checks_from_expr(else_expr, checks)?;
367 }
368 }
369 Expr::Index(expr) => {
370 collect_safety_checks_from_expr(&expr.expr, checks)?;
371 collect_safety_checks_from_expr(&expr.index, checks)?;
372 }
373 Expr::Let(expr) => collect_safety_checks_from_expr(&expr.expr, checks)?,
374 Expr::Loop(expr) => collect_safety_checks_from_block(&expr.body, checks)?,
375 Expr::Macro(expr_macro) => {
376 if let Some(check) = parse_safety_check_macro(&expr_macro.mac)? {
377 checks.push(check);
378 }
379 }
380 Expr::Match(expr) => {
381 collect_safety_checks_from_expr(&expr.expr, checks)?;
382 for arm in &expr.arms {
383 if let Some((_, guard)) = &arm.guard {
384 collect_safety_checks_from_expr(guard, checks)?;
385 }
386 collect_safety_checks_from_expr(&arm.body, checks)?;
387 }
388 }
389 Expr::MethodCall(expr) => {
390 collect_safety_checks_from_expr(&expr.receiver, checks)?;
391 for arg in &expr.args {
392 collect_safety_checks_from_expr(arg, checks)?;
393 }
394 }
395 Expr::Paren(expr) => collect_safety_checks_from_expr(&expr.expr, checks)?,
396 Expr::Reference(expr) => collect_safety_checks_from_expr(&expr.expr, checks)?,
397 Expr::Repeat(expr) => collect_safety_checks_from_expr(&expr.expr, checks)?,
398 Expr::Return(expr) => {
399 if let Some(value) = &expr.expr {
400 collect_safety_checks_from_expr(value, checks)?;
401 }
402 }
403 Expr::Struct(expr) => {
404 for field in &expr.fields {
405 collect_safety_checks_from_expr(&field.expr, checks)?;
406 }
407 if let Some(rest) = &expr.rest {
408 collect_safety_checks_from_expr(rest, checks)?;
409 }
410 }
411 Expr::Try(expr) => collect_safety_checks_from_expr(&expr.expr, checks)?,
412 Expr::TryBlock(expr) => collect_safety_checks_from_block(&expr.block, checks)?,
413 Expr::Tuple(expr) => {
414 for elem in &expr.elems {
415 collect_safety_checks_from_expr(elem, checks)?;
416 }
417 }
418 Expr::Unary(expr) => collect_safety_checks_from_expr(&expr.expr, checks)?,
419 Expr::Unsafe(expr) => collect_safety_checks_from_block(&expr.block, checks)?,
420 Expr::While(expr) => {
421 collect_safety_checks_from_expr(&expr.cond, checks)?;
422 collect_safety_checks_from_block(&expr.body, checks)?;
423 }
424 Expr::Yield(expr) => {
425 if let Some(value) = &expr.expr {
426 collect_safety_checks_from_expr(value, checks)?;
427 }
428 }
429 _ => {}
430 }
431 Ok(())
432}
433
434fn parse_safety_check_macro(mac: &syn::Macro) -> Result<Option<ParsedSafetyCheck>, syn::Error> {
435 let Some(segment) = mac.path.segments.last() else {
436 return Ok(None);
437 };
438
439 let kind = match segment.ident.to_string().as_str() {
440 "safety_check" => "assert",
441 "safety_check_eq" => "assert_eq",
442 _ => return Ok(None),
443 };
444
445 let args = Punctuated::<Expr, Token![,]>::parse_terminated.parse2(mac.tokens.clone())?;
446 let min_args = if kind == "assert_eq" { 4 } else { 3 };
447 if args.len() < min_args {
448 return Err(syn::Error::new_spanned(
449 mac,
450 format!(
451 "{}! expects at least {} arguments: check ID, requirement ID, and assertion inputs",
452 segment.ident, min_args
453 ),
454 ));
455 }
456
457 let check_id = string_literal_from_expr(&args[0], "safety check ID")?;
458 let requirement_id = string_literal_from_expr(&args[1], "requirement ID")?;
459
460 Ok(Some(ParsedSafetyCheck {
461 check_id,
462 requirement_id,
463 kind,
464 }))
465}
466
467fn string_literal_from_expr(expr: &Expr, label: &str) -> Result<String, syn::Error> {
468 let Expr::Lit(expr_lit) = expr else {
469 return Err(syn::Error::new_spanned(
470 expr,
471 format!("{label} must be a string literal"),
472 ));
473 };
474 let Lit::Str(value) = &expr_lit.lit else {
475 return Err(syn::Error::new_spanned(
476 expr,
477 format!("{label} must be a string literal"),
478 ));
479 };
480 Ok(value.value())
481}
482
483fn validate_case_id(case_id: &str) -> Result<(), syn::Error> {
484 validate_id(case_id, "TEST", false)
485}
486
487fn validate_check_id(case_id: &str, check_id: &str) -> Result<(), syn::Error> {
488 validate_id(check_id, "TEST", true)?;
489 if !check_id.starts_with(case_id) {
490 return Err(syn::Error::new(
491 Span::call_site(),
492 format!("safety check ID '{check_id}' must start with case ID '{case_id}'"),
493 ));
494 }
495 Ok(())
496}
497
498fn validate_requirement_id(requirement_id: &str) -> Result<(), syn::Error> {
499 validate_id(requirement_id, "REQ", false)
500}
501
502fn validate_id(value: &str, kind: &str, allow_check_suffix: bool) -> Result<(), syn::Error> {
503 let mut parts = value.split('-');
504 let Some(prefix) = parts.next() else {
505 return invalid_id(value, kind, allow_check_suffix);
506 };
507 let Some(actual_kind) = parts.next() else {
508 return invalid_id(value, kind, allow_check_suffix);
509 };
510 let Some(number) = parts.next() else {
511 return invalid_id(value, kind, allow_check_suffix);
512 };
513
514 if !prefix.chars().all(|ch| ch.is_ascii_uppercase())
515 || actual_kind != kind
516 || number.len() != 3
517 || !number.chars().all(|ch| ch.is_ascii_digit())
518 {
519 return invalid_id(value, kind, allow_check_suffix);
520 }
521
522 match parts.next() {
523 None => Ok(()),
524 Some(check_suffix) if allow_check_suffix && check_suffix.starts_with('C') => {
525 let digits = &check_suffix[1..];
526 if digits.is_empty() || !digits.chars().all(|ch| ch.is_ascii_digit()) {
527 return invalid_id(value, kind, allow_check_suffix);
528 }
529 if parts.next().is_some() {
530 return invalid_id(value, kind, allow_check_suffix);
531 }
532 Ok(())
533 }
534 _ => invalid_id(value, kind, allow_check_suffix),
535 }
536}
537
538fn invalid_id(value: &str, kind: &str, allow_check_suffix: bool) -> Result<(), syn::Error> {
539 let suffix = if allow_check_suffix {
540 " or PREFIX-TEST-001-C1"
541 } else {
542 ""
543 };
544 Err(syn::Error::new(
545 Span::call_site(),
546 format!("invalid ID '{value}', expected PREFIX-{kind}-001{suffix}"),
547 ))
548}
549
550#[proc_macro]
554pub fn gen_cumsgs(config_path_lit: TokenStream) -> TokenStream {
555 #[cfg(feature = "std")]
556 let std = true;
557
558 #[cfg(not(feature = "std"))]
559 let std = false;
560 let config = parse_macro_input!(config_path_lit as LitStr).value();
561 if !std::path::Path::new(&config_full_path(&config)).exists() {
562 return return_error(format!(
563 "The configuration file `{config}` does not exist. Please provide a valid path."
564 ));
565 }
566 #[cfg(feature = "macro_debug")]
567 eprintln!("[gen culist support with {config:?}]");
568 let cuconfig = match read_config(&config) {
569 Ok(cuconfig) => cuconfig,
570 Err(e) => return return_error(e.to_string()),
571 };
572
573 let extra_imports = if !std {
574 quote! {
575 use core::fmt::Debug;
576 use core::fmt::Formatter;
577 use core::fmt::Result as FmtResult;
578 use alloc::vec;
579 use alloc::vec::Vec;
580 }
581 } else {
582 quote! {
583 use std::fmt::Debug;
584 use std::fmt::Formatter;
585 use std::fmt::Result as FmtResult;
586 }
587 };
588
589 let common_imports = quote! {
590 use cu29::bincode::Encode;
591 use cu29::bincode::enc::Encoder;
592 use cu29::bincode::error::EncodeError;
593 use cu29::bincode::Decode;
594 use cu29::bincode::de::Decoder;
595 use cu29::bincode::error::DecodeError;
596 use cu29::copperlist::CopperList;
597 use cu29::prelude::ErasedCuStampedData;
598 use cu29::prelude::ErasedCuStampedDataSet;
599 use cu29::prelude::MatchingTasks;
600 use cu29::prelude::CuMsg;
601 use cu29::prelude::CuMsgMetadata;
602 use cu29::prelude::CuListZeroedInit;
603 use cu29::prelude::CuCompactString;
604 #extra_imports
605 };
606
607 let with_uses = match &cuconfig.graphs {
608 ConfigGraphs::Simple(graph) => {
609 let support = match build_gen_cumsgs_support(&cuconfig, graph, None) {
610 Ok(support) => support,
611 Err(e) => return return_error(e.to_string()),
612 };
613
614 quote! {
615 mod cumsgs {
616 #common_imports
617 #support
618 }
619 use cumsgs::CuStampedDataSet;
620 type CuMsgs=CuStampedDataSet;
621 }
622 }
623 ConfigGraphs::Missions(graphs) => {
624 let mut missions: Vec<_> = graphs.iter().collect();
625 missions.sort_by(|a, b| a.0.cmp(b.0));
626
627 let mut mission_modules = Vec::<proc_macro2::TokenStream>::new();
628 for (mission, graph) in missions {
629 let mission_mod = match parse_str::<Ident>(mission.as_str()) {
630 Ok(id) => id,
631 Err(_) => {
632 return return_error(format!(
633 "Mission '{mission}' is not a valid Rust identifier for gen_cumsgs output."
634 ));
635 }
636 };
637
638 let support = match build_gen_cumsgs_support(&cuconfig, graph, Some(mission)) {
639 Ok(support) => support,
640 Err(e) => return return_error(e.to_string()),
641 };
642
643 mission_modules.push(quote! {
644 pub mod #mission_mod {
645 #common_imports
646 #support
647 }
648 });
649 }
650
651 let default_exports = if graphs.contains_key("default") {
652 quote! {
653 use cumsgs::default::CuStampedDataSet;
654 type CuMsgs=CuStampedDataSet;
655 }
656 } else {
657 quote! {}
658 };
659
660 quote! {
661 mod cumsgs {
662 #(#mission_modules)*
663 }
664 #default_exports
665 }
666 }
667 };
668 with_uses.into()
669}
670
671fn build_gen_cumsgs_support(
672 cuconfig: &CuConfig,
673 graph: &CuGraph,
674 mission_label: Option<&str>,
675) -> CuResult<proc_macro2::TokenStream> {
676 let task_specs = CuTaskSpecSet::from_graph(graph)?;
677 let channel_usage = collect_bridge_channel_usage(graph);
678 let mut bridge_specs = build_bridge_specs(cuconfig, graph, &channel_usage);
679 let (culist_plan, exec_entities, plan_to_original) =
680 build_execution_plan(graph, &task_specs, &mut bridge_specs).map_err(|e| {
681 if let Some(mission) = mission_label {
682 CuError::from(format!(
683 "Could not compute copperlist plan for mission '{mission}': {e}"
684 ))
685 } else {
686 CuError::from(format!("Could not compute copperlist plan: {e}"))
687 }
688 })?;
689 let task_names = collect_task_names(graph);
690 let (culist_order, node_output_positions) = collect_culist_metadata(
691 &culist_plan,
692 &exec_entities,
693 &mut bridge_specs,
694 &plan_to_original,
695 );
696
697 #[cfg(feature = "macro_debug")]
698 if let Some(mission) = mission_label {
699 eprintln!(
700 "[The CuStampedDataSet matching tasks ids for mission '{mission}' are {:?}]",
701 culist_order
702 );
703 } else {
704 eprintln!(
705 "[The CuStampedDataSet matching tasks ids are {:?}]",
706 culist_order
707 );
708 }
709
710 Ok(gen_culist_support(
711 cuconfig,
712 mission_label,
713 &culist_plan,
714 &culist_order,
715 &node_output_positions,
716 &task_names,
717 &bridge_specs,
718 ))
719}
720
721fn gen_culist_support(
723 cuconfig: &CuConfig,
724 mission_label: Option<&str>,
725 runtime_plan: &CuExecutionLoop,
726 culist_indices_in_plan_order: &[usize],
727 node_output_positions: &HashMap<NodeId, usize>,
728 task_names: &[(NodeId, String, String)],
729 bridge_specs: &[BridgeSpec],
730) -> proc_macro2::TokenStream {
731 #[cfg(feature = "macro_debug")]
732 eprintln!("[Extract msgs types]");
733 let output_packs = extract_output_packs(runtime_plan);
734 let slot_types: Vec<Type> = output_packs.iter().map(|pack| pack.slot_type()).collect();
735
736 let culist_size = output_packs.len();
737
738 #[cfg(feature = "macro_debug")]
739 eprintln!("[build the copperlist struct]");
740 let msgs_types_tuple: TypeTuple = build_culist_tuple(&slot_types);
741 let cumsg_count: usize = output_packs.iter().map(|pack| pack.msg_types.len()).sum();
742 let flat_codec_bindings = build_flat_slot_codec_bindings(
743 cuconfig,
744 mission_label,
745 &output_packs,
746 node_output_positions,
747 task_names,
748 )
749 .unwrap_or_else(|err| panic!("Could not resolve log codec bindings: {err}"));
750 let default_config_ron_ident = format_ident!("__CU_LOGCODEC_DEFAULT_CONFIG_RON");
751 let default_config_ron = cuconfig
752 .serialize_ron()
753 .unwrap_or_else(|_| "<failed to serialize config>".to_string());
754 let default_config_ron_lit = LitStr::new(&default_config_ron, Span::call_site());
755 let (codec_helper_fns, encode_helper_names, decode_helper_names) = build_culist_codec_helpers(
756 &flat_codec_bindings,
757 &default_config_ron_ident,
758 mission_label,
759 );
760 let default_config_ron_const = if flat_codec_bindings.iter().any(Option::is_some) {
761 quote! {
762 const #default_config_ron_ident: &str = #default_config_ron_lit;
763 }
764 } else {
765 quote! {}
766 };
767
768 #[cfg(feature = "macro_debug")]
769 eprintln!("[build the copperlist tuple bincode support]");
770 let slot_handle_modes = build_slot_handle_modes(
771 cuconfig,
772 mission_label,
773 &output_packs,
774 node_output_positions,
775 task_names,
776 );
777 let msgs_types_tuple_encode =
778 build_culist_tuple_encode(&output_packs, &encode_helper_names, &slot_handle_modes);
779 let msgs_types_tuple_decode = build_culist_tuple_decode(
780 &output_packs,
781 &slot_types,
782 cumsg_count,
783 &decode_helper_names,
784 );
785
786 #[cfg(feature = "macro_debug")]
787 eprintln!("[build the copperlist tuple debug support]");
788 let msgs_types_tuple_debug = build_culist_tuple_debug(&slot_types);
789
790 #[cfg(feature = "macro_debug")]
791 eprintln!("[build the copperlist tuple serialize support]");
792 let msgs_types_tuple_serialize = build_culist_tuple_serialize(&slot_types);
793
794 #[cfg(feature = "macro_debug")]
795 eprintln!("[build the default tuple support]");
796 let msgs_types_tuple_default = build_culist_tuple_default(&slot_types, cumsg_count);
797
798 #[cfg(feature = "macro_debug")]
799 eprintln!("[build erasedcumsgs]");
800
801 let erasedmsg_trait_impl = build_culist_erasedcumsgs(&output_packs);
802
803 let metadata_accessors: Vec<proc_macro2::TokenStream> = culist_indices_in_plan_order
804 .iter()
805 .map(|idx| {
806 let slot_index = syn::Index::from(*idx);
807 let pack = output_packs
808 .get(*idx)
809 .unwrap_or_else(|| panic!("Missing output pack for index {idx}"));
810 if pack.is_multi() {
811 quote! { &culist.msgs.0.#slot_index.0.metadata }
812 } else {
813 quote! { &culist.msgs.0.#slot_index.metadata }
814 }
815 })
816 .collect();
817 let mut zeroed_init_tokens: Vec<proc_macro2::TokenStream> = Vec::new();
818 for idx in culist_indices_in_plan_order {
819 let slot_index = syn::Index::from(*idx);
820 let pack = output_packs
821 .get(*idx)
822 .unwrap_or_else(|| panic!("Missing output pack for index {idx}"));
823 if pack.is_multi() {
824 for port_idx in 0..pack.msg_types.len() {
825 let port_index = syn::Index::from(port_idx);
826 zeroed_init_tokens.push(quote! {
827 self.0.#slot_index.#port_index.metadata.status_txt = CuCompactString::default();
828 self.0.#slot_index.#port_index.metadata.process_time.start =
829 cu29::clock::OptionCuTime::none();
830 self.0.#slot_index.#port_index.metadata.process_time.end =
831 cu29::clock::OptionCuTime::none();
832 self.0.#slot_index.#port_index.metadata.origin = None;
833 });
834 }
835 } else {
836 zeroed_init_tokens.push(quote! {
837 self.0.#slot_index.metadata.status_txt = CuCompactString::default();
838 self.0.#slot_index.metadata.process_time.start = cu29::clock::OptionCuTime::none();
839 self.0.#slot_index.metadata.process_time.end = cu29::clock::OptionCuTime::none();
840 self.0.#slot_index.metadata.origin = None;
841 });
842 }
843 }
844 let collect_metadata_function = quote! {
845 pub fn collect_metadata<'a>(culist: &'a CuList) -> [&'a CuMsgMetadata; #culist_size] {
846 [#( #metadata_accessors, )*]
847 }
848 };
849
850 let payload_bytes_accumulators: Vec<proc_macro2::TokenStream> = culist_indices_in_plan_order
851 .iter()
852 .scan(0usize, |flat_idx, idx| {
853 let slot_index = syn::Index::from(*idx);
854 let pack = output_packs
855 .get(*idx)
856 .unwrap_or_else(|| panic!("Missing output pack for index {idx}"));
857 if pack.is_multi() {
858 let iter = (0..pack.msg_types.len()).map(|port_idx| {
859 let port_index = syn::Index::from(port_idx);
860 let cache_index = syn::Index::from(*flat_idx);
861 *flat_idx += 1;
862 quote! {
863 if let Some(payload) = culist.msgs.0.#slot_index.#port_index.payload() {
864 let cached = culist.msgs.1.get(#cache_index);
865 let io = if cached.present {
866 cu29::monitoring::PayloadIoStats {
867 resident_bytes: cached.resident_bytes as usize,
868 encoded_bytes: cached.encoded_bytes as usize,
869 handle_bytes: cached.handle_bytes as usize,
870 }
871 } else {
872 cu29::monitoring::payload_io_stats(payload)?
873 };
874 raw += io.resident_bytes;
875 handles += io.handle_bytes;
876 }
877 }
878 });
879 Some(quote! { #(#iter)* })
880 } else {
881 let cache_index = syn::Index::from(*flat_idx);
882 *flat_idx += 1;
883 Some(quote! {
884 if let Some(payload) = culist.msgs.0.#slot_index.payload() {
885 let cached = culist.msgs.1.get(#cache_index);
886 let io = if cached.present {
887 cu29::monitoring::PayloadIoStats {
888 resident_bytes: cached.resident_bytes as usize,
889 encoded_bytes: cached.encoded_bytes as usize,
890 handle_bytes: cached.handle_bytes as usize,
891 }
892 } else {
893 cu29::monitoring::payload_io_stats(payload)?
894 };
895 raw += io.resident_bytes;
896 handles += io.handle_bytes;
897 }
898 })
899 }
900 })
901 .collect();
902
903 let payload_raw_bytes_accumulators: Vec<proc_macro2::TokenStream> = output_packs
904 .iter()
905 .enumerate()
906 .scan(0usize, |flat_idx, (slot_idx, pack)| {
907 let slot_index = syn::Index::from(slot_idx);
908 if pack.is_multi() {
909 let iter = (0..pack.msg_types.len()).map(|port_idx| {
910 let port_index = syn::Index::from(port_idx);
911 let cache_index = syn::Index::from(*flat_idx);
912 *flat_idx += 1;
913 quote! {
914 if let Some(payload) = self.0.#slot_index.#port_index.payload() {
915 let cached = self.1.get(#cache_index);
916 bytes.push(if cached.present {
917 Some(cached.resident_bytes)
918 } else {
919 cu29::monitoring::payload_io_stats(payload)
920 .ok()
921 .map(|io| io.resident_bytes as u64)
922 });
923 } else {
924 bytes.push(None);
925 }
926 }
927 });
928 Some(quote! { #(#iter)* })
929 } else {
930 let cache_index = syn::Index::from(*flat_idx);
931 *flat_idx += 1;
932 Some(quote! {
933 if let Some(payload) = self.0.#slot_index.payload() {
934 let cached = self.1.get(#cache_index);
935 bytes.push(if cached.present {
936 Some(cached.resident_bytes)
937 } else {
938 cu29::monitoring::payload_io_stats(payload)
939 .ok()
940 .map(|io| io.resident_bytes as u64)
941 });
942 } else {
943 bytes.push(None);
944 }
945 })
946 }
947 })
948 .collect();
949
950 let compute_payload_bytes_fn = quote! {
951 pub fn compute_payload_bytes(culist: &CuList) -> cu29::prelude::CuResult<(u64, u64)> {
952 let mut raw: usize = 0;
953 let mut handles: usize = 0;
954 #(#payload_bytes_accumulators)*
955 Ok((raw as u64, handles as u64))
956 }
957 };
958
959 let payload_raw_bytes_impl = quote! {
960 impl ::cu29::CuPayloadRawBytes for CuStampedDataSet {
961 fn payload_raw_bytes(&self) -> Vec<Option<u64>> {
962 let mut bytes: Vec<Option<u64>> = Vec::with_capacity(#cumsg_count);
963 #(#payload_raw_bytes_accumulators)*
964 bytes
965 }
966 }
967 };
968
969 let mut slot_origin_ids: Vec<Option<String>> = vec![None; output_packs.len()];
970 let mut slot_task_names: Vec<Option<String>> = vec![None; output_packs.len()];
971
972 let mut methods = Vec::new();
973 for (node_id, task_id, member_name) in task_names {
974 let output_position = node_output_positions.get(node_id).unwrap_or_else(|| {
975 panic!("Task {task_id} (node id: {node_id}) not found in execution order")
976 });
977 let pack = output_packs
978 .get(*output_position)
979 .unwrap_or_else(|| panic!("Missing output pack for task {task_id}"));
980 let slot_index = syn::Index::from(*output_position);
981 slot_origin_ids[*output_position] = Some(task_id.clone());
982 slot_task_names[*output_position] = Some(member_name.clone());
983
984 if pack.msg_types.len() == 1 {
985 let fn_name = format_ident!("get_{}_output", member_name);
986 let payload_type = pack.msg_types.first().unwrap();
987 methods.push(quote! {
988 #[allow(dead_code)]
989 pub fn #fn_name(&self) -> &CuMsg<#payload_type> {
990 &self.0.#slot_index
991 }
992 });
993 } else {
994 let outputs_fn = format_ident!("get_{}_outputs", member_name);
995 let slot_type = pack.slot_type();
996 for (port_idx, payload_type) in pack.msg_types.iter().enumerate() {
997 let fn_name = format_ident!("get_{}_output_{}", member_name, port_idx);
998 let port_index = syn::Index::from(port_idx);
999 methods.push(quote! {
1000 #[allow(dead_code)]
1001 pub fn #fn_name(&self) -> &CuMsg<#payload_type> {
1002 &self.0.#slot_index.#port_index
1003 }
1004 });
1005 }
1006 methods.push(quote! {
1007 #[allow(dead_code)]
1008 pub fn #outputs_fn(&self) -> &#slot_type {
1009 &self.0.#slot_index
1010 }
1011 });
1012 }
1013 }
1014
1015 for spec in bridge_specs {
1016 for channel in &spec.rx_channels {
1017 if let Some(culist_index) = channel.culist_index {
1018 let origin_id = format!("bridge::{}::rx::{}", spec.id, channel.id);
1019 let Some(existing_slot) = slot_origin_ids.get_mut(culist_index) else {
1020 panic!(
1021 "Bridge origin '{origin_id}' points to out-of-range copperlist slot {culist_index}"
1022 );
1023 };
1024 if let Some(existing) = existing_slot.as_ref() {
1025 panic!(
1026 "Duplicate slot origin assignment for slot {culist_index}: '{existing}' and '{origin_id}'"
1027 );
1028 }
1029 *existing_slot = Some(origin_id.clone());
1030 let Some(slot_name) = slot_task_names.get_mut(culist_index) else {
1031 panic!(
1032 "Bridge origin '{origin_id}' points to out-of-range name slot {culist_index}"
1033 );
1034 };
1035 *slot_name = Some(origin_id);
1036 }
1037 }
1038 for channel in &spec.tx_channels {
1039 if let Some(culist_index) = channel.culist_index {
1040 let origin_id = format!("bridge::{}::tx::{}", spec.id, channel.id);
1041 let Some(existing_slot) = slot_origin_ids.get_mut(culist_index) else {
1042 panic!(
1043 "Bridge origin '{origin_id}' points to out-of-range copperlist slot {culist_index}"
1044 );
1045 };
1046 if let Some(existing) = existing_slot.as_ref() {
1047 panic!(
1048 "Duplicate slot origin assignment for slot {culist_index}: '{existing}' and '{origin_id}'"
1049 );
1050 }
1051 *existing_slot = Some(origin_id.clone());
1052 let Some(slot_name) = slot_task_names.get_mut(culist_index) else {
1053 panic!(
1054 "Bridge origin '{origin_id}' points to out-of-range name slot {culist_index}"
1055 );
1056 };
1057 *slot_name = Some(origin_id);
1058 }
1059 }
1060 }
1061
1062 let task_name_literals = flatten_slot_origin_ids(&output_packs, &slot_origin_ids);
1063 let task_output_specs = flatten_task_output_specs(&output_packs, &slot_origin_ids);
1064 let task_output_spec_literals: Vec<proc_macro2::TokenStream> = task_output_specs
1065 .iter()
1066 .map(|(task_id, msg_type, payload_type)| {
1067 let task_id = LitStr::new(task_id, Span::call_site());
1068 let msg_type = LitStr::new(msg_type, Span::call_site());
1069 quote! {
1070 cu29::TaskOutputSpec {
1071 task_id: #task_id,
1072 msg_type: #msg_type,
1073 payload_type_path_fn: <#payload_type as cu29::prelude::TypePath>::type_path,
1074 }
1075 }
1076 })
1077 .collect();
1078
1079 for spec in bridge_specs {
1081 for channel in &spec.rx_channels {
1082 if let Some(culist_index) = channel.culist_index {
1083 let slot_index = syn::Index::from(culist_index);
1084 let bridge_name = config_id_to_struct_member(spec.id.as_str());
1085 let channel_name = config_id_to_struct_member(channel.id.as_str());
1086 let fn_name = format_ident!("get_{}_rx_{}", bridge_name, channel_name);
1087 let msg_type = &channel.msg_type;
1088
1089 methods.push(quote! {
1090 #[allow(dead_code)]
1091 pub fn #fn_name(&self) -> &CuMsg<#msg_type> {
1092 &self.0.#slot_index
1093 }
1094 });
1095 }
1096 }
1097 }
1098
1099 quote! {
1101 #collect_metadata_function
1102 #compute_payload_bytes_fn
1103 #default_config_ron_const
1104 #(#codec_helper_fns)*
1105
1106 pub struct CuStampedDataSet(pub #msgs_types_tuple, cu29::monitoring::CuMsgIoCache<#cumsg_count>);
1107
1108 pub type CuList = CopperList<CuStampedDataSet>;
1109
1110 impl CuStampedDataSet {
1111 #(#methods)*
1112
1113 #[allow(dead_code)]
1114 fn get_tuple(&self) -> &#msgs_types_tuple {
1115 &self.0
1116 }
1117
1118 #[allow(dead_code)]
1119 fn get_tuple_mut(&mut self) -> &mut #msgs_types_tuple {
1120 &mut self.0
1121 }
1122 }
1123
1124 #payload_raw_bytes_impl
1125 impl MatchingTasks for CuStampedDataSet {
1126 #[allow(dead_code)]
1127 fn get_all_task_ids() -> &'static [&'static str] {
1128 &[#(#task_name_literals),*]
1129 }
1130
1131 #[allow(dead_code)]
1132 fn get_output_specs() -> &'static [cu29::TaskOutputSpec] {
1133 &[#(#task_output_spec_literals),*]
1134 }
1135 }
1136
1137 #msgs_types_tuple_encode
1143 #msgs_types_tuple_decode
1144
1145 #msgs_types_tuple_debug
1147
1148 #msgs_types_tuple_serialize
1150
1151 #msgs_types_tuple_default
1153
1154 #erasedmsg_trait_impl
1156
1157 impl CuListZeroedInit for CuStampedDataSet {
1158 fn init_zeroed(&mut self) {
1159 self.1.clear();
1160 #(#zeroed_init_tokens)*
1161 }
1162 }
1163 }
1164}
1165
1166fn gen_sim_support(
1167 runtime_plan: &CuExecutionLoop,
1168 exec_entities: &[ExecutionEntity],
1169 bridge_specs: &[BridgeSpec],
1170) -> proc_macro2::TokenStream {
1171 #[cfg(feature = "macro_debug")]
1172 eprintln!("[Sim: Build SimEnum]");
1173 let plan_enum: Vec<proc_macro2::TokenStream> = runtime_plan
1174 .steps
1175 .iter()
1176 .map(|unit| match unit {
1177 CuExecutionUnit::Step(step) => match &exec_entities[step.node_id as usize].kind {
1178 ExecutionEntityKind::Task { .. } => {
1179 let enum_entry_name = config_id_to_enum(step.node.get_id().as_str());
1180 let enum_ident = Ident::new(&enum_entry_name, Span::call_site());
1181 let inputs: Vec<Type> = step
1182 .input_msg_indices_types
1183 .iter()
1184 .map(|input| {
1185 parse_str::<Type>(format!("CuMsg<{}>", input.msg_type).as_str()).unwrap()
1186 })
1187 .collect();
1188 let output: Option<Type> = step.output_msg_pack.as_ref().map(|pack| {
1189 let msg_types: Vec<Type> = pack
1190 .msg_types
1191 .iter()
1192 .map(|msg_type| {
1193 parse_str::<Type>(msg_type.as_str()).unwrap_or_else(|_| {
1194 panic!("Could not transform {msg_type} into a message Rust type.")
1195 })
1196 })
1197 .collect();
1198 build_output_slot_type(&msg_types)
1199 });
1200 let no_output = parse_str::<Type>("CuMsg<()>").unwrap();
1201 let output = output.as_ref().unwrap_or(&no_output);
1202
1203 let inputs_type = if inputs.is_empty() {
1204 quote! { () }
1205 } else if inputs.len() == 1 {
1206 let input = inputs.first().unwrap();
1207 quote! { &'a #input }
1208 } else {
1209 quote! { &'a (#(&'a #inputs),*) }
1210 };
1211
1212 quote! {
1213 #enum_ident(CuTaskCallbackState<#inputs_type, &'a mut #output>)
1214 }
1215 }
1216 ExecutionEntityKind::BridgeRx { bridge_index, channel_index } => {
1217 let bridge_spec = &bridge_specs[*bridge_index];
1218 let channel = &bridge_spec.rx_channels[*channel_index];
1219 let enum_entry_name = config_id_to_enum(&format!("{}_rx_{}", bridge_spec.id, channel.id));
1220 let enum_ident = Ident::new(&enum_entry_name, Span::call_site());
1221 let channel_type: Type = parse_str::<Type>(channel.msg_type_name.as_str()).unwrap();
1222 let bridge_type = runtime_bridge_type_for_spec(bridge_spec, true);
1223 let _const_ident = &channel.const_ident;
1224 quote! {
1225 #enum_ident {
1226 channel: &'static cu29::cubridge::BridgeChannel<< <#bridge_type as cu29::cubridge::CuBridge>::Rx as cu29::cubridge::BridgeChannelSet >::Id, #channel_type>,
1227 msg: &'a mut CuMsg<#channel_type>,
1228 }
1229 }
1230 }
1231 ExecutionEntityKind::BridgeTx { bridge_index, channel_index } => {
1232 let bridge_spec = &bridge_specs[*bridge_index];
1233 let channel = &bridge_spec.tx_channels[*channel_index];
1234 let enum_entry_name = config_id_to_enum(&format!("{}_tx_{}", bridge_spec.id, channel.id));
1235 let enum_ident = Ident::new(&enum_entry_name, Span::call_site());
1236 let channel_type: Type = parse_str::<Type>(channel.msg_type_name.as_str()).unwrap();
1237 let output_pack = step
1238 .output_msg_pack
1239 .as_ref()
1240 .expect("Bridge Tx channel missing output pack for sim support");
1241 let output_types: Vec<Type> = output_pack
1242 .msg_types
1243 .iter()
1244 .map(|msg_type| {
1245 parse_str::<Type>(msg_type.as_str()).unwrap_or_else(|_| {
1246 panic!("Could not transform {msg_type} into a message Rust type.")
1247 })
1248 })
1249 .collect();
1250 let output_type = build_output_slot_type(&output_types);
1251 let bridge_type = runtime_bridge_type_for_spec(bridge_spec, true);
1252 let _const_ident = &channel.const_ident;
1253 quote! {
1254 #enum_ident {
1255 channel: &'static cu29::cubridge::BridgeChannel<< <#bridge_type as cu29::cubridge::CuBridge>::Tx as cu29::cubridge::BridgeChannelSet >::Id, #channel_type>,
1256 msg: &'a CuMsg<#channel_type>,
1257 output: &'a mut #output_type,
1258 }
1259 }
1260 }
1261 },
1262 CuExecutionUnit::Loop(_) => {
1263 todo!("Needs to be implemented")
1264 }
1265 })
1266 .collect();
1267
1268 let mut variants = plan_enum;
1270
1271 for bridge_spec in bridge_specs {
1273 let enum_entry_name = config_id_to_enum(&format!("{}_bridge", bridge_spec.id));
1274 let enum_ident = Ident::new(&enum_entry_name, Span::call_site());
1275 variants.push(quote! {
1276 #enum_ident(cu29::simulation::CuBridgeLifecycleState)
1277 });
1278 }
1279
1280 variants.push(quote! { __Phantom(core::marker::PhantomData<&'a ()>) });
1281 quote! {
1282 #[allow(dead_code, unused_lifetimes)]
1284 pub enum SimStep<'a> {
1285 #(#variants),*
1286 }
1287 }
1288}
1289
1290fn gen_recorded_replay_support(
1291 runtime_plan: &CuExecutionLoop,
1292 exec_entities: &[ExecutionEntity],
1293 bridge_specs: &[BridgeSpec],
1294) -> proc_macro2::TokenStream {
1295 let replay_arms: Vec<proc_macro2::TokenStream> = runtime_plan
1296 .steps
1297 .iter()
1298 .filter_map(|unit| match unit {
1299 CuExecutionUnit::Step(step) => match &exec_entities[step.node_id as usize].kind {
1300 ExecutionEntityKind::Task { .. } => {
1301 let enum_entry_name = config_id_to_enum(step.node.get_id().as_str());
1302 let enum_ident = Ident::new(&enum_entry_name, Span::call_site());
1303 let output_pack = step
1304 .output_msg_pack
1305 .as_ref()
1306 .expect("Task step missing output pack for recorded replay");
1307 let culist_index = int2sliceindex(output_pack.culist_index);
1308 Some(quote! {
1309 SimStep::#enum_ident(CuTaskCallbackState::Process(_, output)) => {
1310 *output = recorded.msgs.0.#culist_index.clone();
1311 SimOverride::ExecutedBySim
1312 }
1313 })
1314 }
1315 ExecutionEntityKind::BridgeRx {
1316 bridge_index,
1317 channel_index,
1318 } => {
1319 let bridge_spec = &bridge_specs[*bridge_index];
1320 let channel = &bridge_spec.rx_channels[*channel_index];
1321 let enum_entry_name =
1322 config_id_to_enum(&format!("{}_rx_{}", bridge_spec.id, channel.id));
1323 let enum_ident = Ident::new(&enum_entry_name, Span::call_site());
1324 let output_pack = step
1325 .output_msg_pack
1326 .as_ref()
1327 .expect("Bridge Rx channel missing output pack for recorded replay");
1328 let port_index = output_pack
1329 .msg_types
1330 .iter()
1331 .position(|msg| msg == &channel.msg_type_name)
1332 .unwrap_or_else(|| {
1333 panic!(
1334 "Bridge Rx channel '{}' missing output port for '{}'",
1335 channel.id, channel.msg_type_name
1336 )
1337 });
1338 let culist_index = int2sliceindex(output_pack.culist_index);
1339 let recorded_slot = if output_pack.msg_types.len() == 1 {
1340 quote! { recorded.msgs.0.#culist_index.clone() }
1341 } else {
1342 let port_index = syn::Index::from(port_index);
1343 quote! { recorded.msgs.0.#culist_index.#port_index.clone() }
1344 };
1345 Some(quote! {
1346 SimStep::#enum_ident { msg, .. } => {
1347 *msg = #recorded_slot;
1348 SimOverride::ExecutedBySim
1349 }
1350 })
1351 }
1352 ExecutionEntityKind::BridgeTx {
1353 bridge_index,
1354 channel_index,
1355 } => {
1356 let bridge_spec = &bridge_specs[*bridge_index];
1357 let channel = &bridge_spec.tx_channels[*channel_index];
1358 let enum_entry_name =
1359 config_id_to_enum(&format!("{}_tx_{}", bridge_spec.id, channel.id));
1360 let enum_ident = Ident::new(&enum_entry_name, Span::call_site());
1361 let output_pack = step
1362 .output_msg_pack
1363 .as_ref()
1364 .expect("Bridge Tx channel missing output pack for recorded replay");
1365 let culist_index = int2sliceindex(output_pack.culist_index);
1366 Some(quote! {
1367 SimStep::#enum_ident { output, .. } => {
1368 *output = recorded.msgs.0.#culist_index.clone();
1369 SimOverride::ExecutedBySim
1370 }
1371 })
1372 }
1373 },
1374 CuExecutionUnit::Loop(_) => None,
1375 })
1376 .collect();
1377 let debug_replay_arms: Vec<proc_macro2::TokenStream> =
1378 runtime_plan
1379 .steps
1380 .iter()
1381 .filter_map(|unit| match unit {
1382 CuExecutionUnit::Step(step) => match &exec_entities[step.node_id as usize].kind {
1383 ExecutionEntityKind::Task { .. } => {
1384 if step.task_type == CuTaskType::Regular {
1385 return None;
1386 }
1387 let enum_entry_name = config_id_to_enum(step.node.get_id().as_str());
1388 let enum_ident = Ident::new(&enum_entry_name, Span::call_site());
1389 let output_pack = step
1390 .output_msg_pack
1391 .as_ref()
1392 .expect("Task step missing output pack for recorded debug replay");
1393 let culist_index = int2sliceindex(output_pack.culist_index);
1394 Some(quote! {
1395 SimStep::#enum_ident(CuTaskCallbackState::Process(_, output)) => {
1396 *output = recorded.msgs.0.#culist_index.clone();
1397 SimOverride::ExecutedBySim
1398 }
1399 })
1400 }
1401 ExecutionEntityKind::BridgeRx {
1402 bridge_index,
1403 channel_index,
1404 } => {
1405 let bridge_spec = &bridge_specs[*bridge_index];
1406 let channel = &bridge_spec.rx_channels[*channel_index];
1407 let enum_entry_name =
1408 config_id_to_enum(&format!("{}_rx_{}", bridge_spec.id, channel.id));
1409 let enum_ident = Ident::new(&enum_entry_name, Span::call_site());
1410 let output_pack = step.output_msg_pack.as_ref().expect(
1411 "Bridge Rx channel missing output pack for recorded debug replay",
1412 );
1413 let port_index = output_pack
1414 .msg_types
1415 .iter()
1416 .position(|msg| msg == &channel.msg_type_name)
1417 .unwrap_or_else(|| {
1418 panic!(
1419 "Bridge Rx channel '{}' missing output port for '{}'",
1420 channel.id, channel.msg_type_name
1421 )
1422 });
1423 let culist_index = int2sliceindex(output_pack.culist_index);
1424 let recorded_slot = if output_pack.msg_types.len() == 1 {
1425 quote! { recorded.msgs.0.#culist_index.clone() }
1426 } else {
1427 let port_index = syn::Index::from(port_index);
1428 quote! { recorded.msgs.0.#culist_index.#port_index.clone() }
1429 };
1430 Some(quote! {
1431 SimStep::#enum_ident { msg, .. } => {
1432 *msg = #recorded_slot;
1433 SimOverride::ExecutedBySim
1434 }
1435 })
1436 }
1437 ExecutionEntityKind::BridgeTx {
1438 bridge_index,
1439 channel_index,
1440 } => {
1441 let bridge_spec = &bridge_specs[*bridge_index];
1442 let channel = &bridge_spec.tx_channels[*channel_index];
1443 let enum_entry_name =
1444 config_id_to_enum(&format!("{}_tx_{}", bridge_spec.id, channel.id));
1445 let enum_ident = Ident::new(&enum_entry_name, Span::call_site());
1446 let output_pack = step.output_msg_pack.as_ref().expect(
1447 "Bridge Tx channel missing output pack for recorded debug replay",
1448 );
1449 let culist_index = int2sliceindex(output_pack.culist_index);
1450 Some(quote! {
1451 SimStep::#enum_ident { output, .. } => {
1452 *output = recorded.msgs.0.#culist_index.clone();
1453 SimOverride::ExecutedBySim
1454 }
1455 })
1456 }
1457 },
1458 CuExecutionUnit::Loop(_) => None,
1459 })
1460 .collect();
1461
1462 quote! {
1463 #[allow(dead_code)]
1468 pub fn recorded_replay_step<'a>(
1469 step: SimStep<'a>,
1470 recorded: &CopperList<CuStampedDataSet>,
1471 ) -> SimOverride {
1472 match step {
1473 #(#replay_arms),*,
1474 _ => SimOverride::ExecuteByRuntime,
1475 }
1476 }
1477
1478 #[allow(dead_code)]
1484 pub fn recorded_debug_replay_step<'a>(
1485 step: SimStep<'a>,
1486 recorded: &CopperList<CuStampedDataSet>,
1487 ) -> SimOverride {
1488 match step {
1489 #(#debug_replay_arms),*,
1490 _ => SimOverride::ExecuteByRuntime,
1491 }
1492 }
1493 }
1494}
1495
1496#[proc_macro_attribute]
1504pub fn copper_runtime(args: TokenStream, input: TokenStream) -> TokenStream {
1505 #[cfg(feature = "macro_debug")]
1506 eprintln!("[entry]");
1507 let mut application_struct = parse_macro_input!(input as ItemStruct);
1508
1509 let application_name = &application_struct.ident;
1510 let builder_name = format_ident!("{}Builder", application_name);
1511 let runtime_args = match CopperRuntimeArgs::parse_tokens(args.into()) {
1512 Ok(runtime_args) => runtime_args,
1513 Err(err) => return err.to_compile_error().into(),
1514 };
1515 let config_file = runtime_args.config_path.clone();
1516 let sim_mode = runtime_args.sim_mode;
1517 let ignore_resources = runtime_args.ignore_resources;
1518
1519 #[cfg(feature = "std")]
1520 let std = true;
1521
1522 #[cfg(not(feature = "std"))]
1523 let std = false;
1524 let signal_handler = cfg!(feature = "signal-handler");
1525 let parallel_rt_enabled = cfg!(feature = "parallel-rt");
1526 let rt_guard = rtsan_guard_tokens();
1527
1528 if ignore_resources && !sim_mode {
1529 return return_error(
1530 "`ignore_resources` is only supported when `sim_mode` is enabled".to_string(),
1531 );
1532 }
1533
1534 let resolved_runtime_config = match resolve_runtime_config(&runtime_args) {
1544 Ok(resolved_runtime_config) => resolved_runtime_config,
1545 Err(e) => return return_error(e.to_string()),
1546 };
1547 let subsystem_code = resolved_runtime_config.subsystem_code;
1548 let subsystem_id = resolved_runtime_config.subsystem_id.clone();
1549 let copper_config_content = resolved_runtime_config.bundled_local_config_content.clone();
1550 let copper_config = resolved_runtime_config.local_config;
1551 let copperlist_count = copper_config
1552 .logging
1553 .as_ref()
1554 .and_then(|logging| logging.copperlist_count)
1555 .unwrap_or(DEFAULT_CLNB);
1556 let copperlist_count_tokens = proc_macro2::Literal::usize_unsuffixed(copperlist_count);
1557 let caller_root = utils::caller_crate_root();
1558 let (git_commit, git_dirty) = detect_git_info(&caller_root);
1559 let git_commit_tokens = if let Some(commit) = git_commit {
1560 quote! { Some(#commit.to_string()) }
1561 } else {
1562 quote! { None }
1563 };
1564 let git_dirty_tokens = if let Some(dirty) = git_dirty {
1565 quote! { Some(#dirty) }
1566 } else {
1567 quote! { None }
1568 };
1569 let subsystem_code_literal = proc_macro2::Literal::u16_unsuffixed(subsystem_code);
1570 let subsystem_id_tokens = if let Some(subsystem_id) = subsystem_id.as_deref() {
1571 quote! { Some(#subsystem_id) }
1572 } else {
1573 quote! { None }
1574 };
1575
1576 #[cfg(feature = "macro_debug")]
1577 eprintln!("[build monitor type]");
1578 let monitor_configs = copper_config.get_monitor_configs();
1579 let (monitor_type, monitor_instanciator_body) = if monitor_configs.is_empty() {
1580 (
1581 quote! { NoMonitor },
1582 quote! {
1583 let monitor_metadata = metadata.with_subsystem_id(#subsystem_id_tokens);
1584 let monitor = NoMonitor::new(monitor_metadata, runtime)
1585 .expect("Failed to create NoMonitor.");
1586 monitor
1587 },
1588 )
1589 } else if monitor_configs.len() == 1 {
1590 let only_monitor_type = parse_str::<Type>(monitor_configs[0].get_type())
1591 .expect("Could not transform the monitor type name into a Rust type.");
1592 (
1593 quote! { #only_monitor_type },
1594 quote! {
1595 let monitor_metadata = metadata.with_monitor_config(
1596 config
1597 .get_monitor_configs()
1598 .first()
1599 .and_then(|entry| entry.get_config().cloned())
1600 )
1601 .with_subsystem_id(#subsystem_id_tokens);
1602 let monitor = #only_monitor_type::new(monitor_metadata, runtime)
1603 .expect("Failed to create the given monitor.");
1604 monitor
1605 },
1606 )
1607 } else {
1608 let monitor_types: Vec<Type> = monitor_configs
1609 .iter()
1610 .map(|monitor_config| {
1611 parse_str::<Type>(monitor_config.get_type())
1612 .expect("Could not transform the monitor type name into a Rust type.")
1613 })
1614 .collect();
1615 let monitor_bindings: Vec<Ident> = (0..monitor_types.len())
1616 .map(|idx| format_ident!("__cu_monitor_{idx}"))
1617 .collect();
1618 let monitor_indices: Vec<syn::Index> =
1619 (0..monitor_types.len()).map(syn::Index::from).collect();
1620
1621 let monitor_builders: Vec<proc_macro2::TokenStream> = monitor_types
1622 .iter()
1623 .zip(monitor_bindings.iter())
1624 .zip(monitor_indices.iter())
1625 .map(|((monitor_ty, monitor_binding), monitor_idx)| {
1626 quote! {
1627 let __cu_monitor_cfg_entry = config
1628 .get_monitor_configs()
1629 .get(#monitor_idx)
1630 .and_then(|entry| entry.get_config().cloned());
1631 let __cu_monitor_metadata = metadata
1632 .clone()
1633 .with_monitor_config(__cu_monitor_cfg_entry)
1634 .with_subsystem_id(#subsystem_id_tokens);
1635 let #monitor_binding = #monitor_ty::new(__cu_monitor_metadata, runtime.clone())
1636 .expect("Failed to create one of the configured monitors.");
1637 }
1638 })
1639 .collect();
1640 let tuple_type: TypeTuple = parse_quote! { (#(#monitor_types),*,) };
1641 (
1642 quote! { #tuple_type },
1643 quote! {
1644 #(#monitor_builders)*
1645 let monitor: #tuple_type = (#(#monitor_bindings),*,);
1646 monitor
1647 },
1648 )
1649 };
1650
1651 #[cfg(feature = "macro_debug")]
1653 eprintln!("[build runtime field]");
1654 let runtime_field: Field = if sim_mode {
1656 parse_quote! {
1657 copper_runtime: cu29::curuntime::CuRuntime<CuSimTasks, CuBridges, CuStampedDataSet, #monitor_type, #copperlist_count_tokens>
1658 }
1659 } else {
1660 parse_quote! {
1661 copper_runtime: cu29::curuntime::CuRuntime<CuTasks, CuBridges, CuStampedDataSet, #monitor_type, #copperlist_count_tokens>
1662 }
1663 };
1664 let lifecycle_stream_field: Field = parse_quote! {
1665 runtime_lifecycle_stream: Option<Box<dyn WriteStream<RuntimeLifecycleRecord>>>
1666 };
1667 let logger_runtime_field: Field = parse_quote! {
1668 logger_runtime: cu29::prelude::LoggerRuntime
1669 };
1670
1671 #[cfg(feature = "macro_debug")]
1672 eprintln!("[match struct anonymity]");
1673 match &mut application_struct.fields {
1674 Named(fields_named) => {
1675 fields_named.named.push(runtime_field);
1676 fields_named.named.push(lifecycle_stream_field);
1677 fields_named.named.push(logger_runtime_field);
1678 }
1679 Unnamed(fields_unnamed) => {
1680 fields_unnamed.unnamed.push(runtime_field);
1681 fields_unnamed.unnamed.push(lifecycle_stream_field);
1682 fields_unnamed.unnamed.push(logger_runtime_field);
1683 }
1684 Fields::Unit => {
1685 panic!(
1686 "This struct is a unit struct, it should have named or unnamed fields. use struct Something {{}} and not struct Something;"
1687 )
1688 }
1689 };
1690
1691 let all_missions = sorted_mission_graphs(&copper_config);
1692 let task_input_layouts = match collect_task_input_layouts(&all_missions) {
1693 Ok(layouts) => layouts,
1694 Err(e) => return return_error(e.to_string()),
1695 };
1696 let mut all_missions_tokens = Vec::<proc_macro2::TokenStream>::new();
1697 for (mission, graph) in &all_missions {
1698 let git_commit_tokens = git_commit_tokens.clone();
1699 let git_dirty_tokens = git_dirty_tokens.clone();
1700 let mission_mod = parse_str::<Ident>(mission.as_str())
1701 .expect("Could not make an identifier of the mission name");
1702
1703 #[cfg(feature = "macro_debug")]
1704 eprintln!("[extract tasks ids & types]");
1705 let task_specs = match CuTaskSpecSet::from_graph(graph) {
1706 Ok(specs) => specs,
1707 Err(e) => return return_error(e.to_string()),
1708 };
1709
1710 let culist_channel_usage = collect_bridge_channel_usage(graph);
1711 let mut culist_bridge_specs =
1712 build_bridge_specs(&copper_config, graph, &culist_channel_usage);
1713 let (culist_plan, culist_exec_entities, culist_plan_to_original) =
1714 match build_execution_plan(graph, &task_specs, &mut culist_bridge_specs) {
1715 Ok(plan) => plan,
1716 Err(e) => return return_error(format!("Could not compute copperlist plan: {e}")),
1717 };
1718 let task_names = collect_task_names(graph);
1719 let (culist_call_order, node_output_positions) = collect_culist_metadata(
1720 &culist_plan,
1721 &culist_exec_entities,
1722 &mut culist_bridge_specs,
1723 &culist_plan_to_original,
1724 );
1725
1726 #[cfg(feature = "macro_debug")]
1727 {
1728 eprintln!("[runtime plan for mission {mission}]");
1729 eprintln!("{culist_plan:?}");
1730 }
1731
1732 let culist_support: proc_macro2::TokenStream = gen_culist_support(
1733 &copper_config,
1734 Some(mission.as_str()),
1735 &culist_plan,
1736 &culist_call_order,
1737 &node_output_positions,
1738 &task_names,
1739 &culist_bridge_specs,
1740 );
1741
1742 let (
1743 resources_module,
1744 resources_instanciator_fn,
1745 task_resource_mappings,
1746 bridge_resource_mappings,
1747 ) = if ignore_resources {
1748 let bundle_specs: Vec<BundleSpec> = Vec::new();
1749 let resource_specs: Vec<ResourceKeySpec> = Vec::new();
1750 let (resources_module, resources_instanciator_fn) =
1751 match build_resources_module(&bundle_specs) {
1752 Ok(tokens) => tokens,
1753 Err(e) => return return_error(e.to_string()),
1754 };
1755 let task_resource_mappings =
1756 match build_task_resource_mappings(&resource_specs, &task_specs, sim_mode) {
1757 Ok(tokens) => tokens,
1758 Err(e) => return return_error(e.to_string()),
1759 };
1760 let bridge_resource_mappings =
1761 build_bridge_resource_mappings(&resource_specs, &culist_bridge_specs, sim_mode);
1762 (
1763 resources_module,
1764 resources_instanciator_fn,
1765 task_resource_mappings,
1766 bridge_resource_mappings,
1767 )
1768 } else {
1769 let bundle_specs = match build_bundle_specs(&copper_config, mission.as_str()) {
1770 Ok(specs) => specs,
1771 Err(e) => return return_error(e.to_string()),
1772 };
1773
1774 let resource_specs = match collect_resource_specs(
1775 graph,
1776 &task_specs,
1777 &culist_bridge_specs,
1778 &bundle_specs,
1779 ) {
1780 Ok(specs) => specs,
1781 Err(e) => return return_error(e.to_string()),
1782 };
1783
1784 let (resources_module, resources_instanciator_fn) =
1785 match build_resources_module(&bundle_specs) {
1786 Ok(tokens) => tokens,
1787 Err(e) => return return_error(e.to_string()),
1788 };
1789 let task_resource_mappings =
1790 match build_task_resource_mappings(&resource_specs, &task_specs, sim_mode) {
1791 Ok(tokens) => tokens,
1792 Err(e) => return return_error(e.to_string()),
1793 };
1794 let bridge_resource_mappings =
1795 build_bridge_resource_mappings(&resource_specs, &culist_bridge_specs, sim_mode);
1796 (
1797 resources_module,
1798 resources_instanciator_fn,
1799 task_resource_mappings,
1800 bridge_resource_mappings,
1801 )
1802 };
1803
1804 let task_ids = task_specs.ids.clone();
1805 let autogenerated_output_warnings: Vec<proc_macro2::TokenStream> = task_specs
1806 .ids
1807 .iter()
1808 .zip(task_specs.cutypes.iter())
1809 .zip(task_specs.autogenerated_output_flags.iter())
1810 .filter_map(|((task_id, task_kind), autogenerated)| {
1811 if !*autogenerated {
1812 return None;
1813 }
1814 let warn_ident = format_ident!(
1815 "__CU_AUTOGEN_FLOATING_OUTPUT_WARNING__{}",
1816 config_id_to_enum(task_id)
1817 );
1818 let kind_str = match task_kind {
1819 CuTaskType::Source => "source",
1820 CuTaskType::Regular => "task",
1821 CuTaskType::Sink => return None,
1822 };
1823 let note = format!(
1824 "Task '{task_id}' is declared as kind '{kind_str}' but has no declared outputs. Copper synthesized a hidden floating output slot from the task trait. Add a real consumer or `dst: \"__nc__\"` if you want this to stay explicit."
1825 );
1826 Some(quote! {
1827 #[allow(dead_code)]
1828 #[deprecated(note = #note)]
1829 const #warn_ident: () = ();
1830 const _: () = {
1831 let _ = #warn_ident;
1832 };
1833 })
1834 })
1835 .collect();
1836 let ids = build_monitored_ids(&task_ids, &mut culist_bridge_specs);
1837 let parallel_rt_stage_entries = match build_parallel_rt_stage_entries(
1838 &culist_plan,
1839 &culist_exec_entities,
1840 &task_specs,
1841 &culist_bridge_specs,
1842 ) {
1843 Ok(entries) => entries,
1844 Err(e) => return return_error(e.to_string()),
1845 };
1846 let parallel_rt_metadata_defs = if std && parallel_rt_enabled {
1847 Some(quote! {
1848 pub const PARALLEL_RT_STAGES: &'static [cu29::parallel_rt::ParallelRtStageMetadata] =
1849 &[#( #parallel_rt_stage_entries ),*];
1850 pub const PARALLEL_RT_METADATA: cu29::parallel_rt::ParallelRtMetadata =
1851 cu29::parallel_rt::ParallelRtMetadata::new(PARALLEL_RT_STAGES);
1852 })
1853 } else {
1854 None
1855 };
1856 let monitored_component_entries: Vec<proc_macro2::TokenStream> = ids
1857 .iter()
1858 .enumerate()
1859 .map(|(idx, id)| {
1860 let id_lit = LitStr::new(id, Span::call_site());
1861 if idx < task_specs.task_types.len() {
1862 let task_ty = &task_specs.task_types[idx];
1863 let component_type = match task_specs.cutypes[idx] {
1864 CuTaskType::Source => quote! { cu29::monitoring::ComponentType::Source },
1865 CuTaskType::Regular => quote! { cu29::monitoring::ComponentType::Task },
1866 CuTaskType::Sink => quote! { cu29::monitoring::ComponentType::Sink },
1867 };
1868 quote! {
1869 cu29::monitoring::MonitorComponentMetadata::new(
1870 #id_lit,
1871 #component_type,
1872 Some(stringify!(#task_ty)),
1873 )
1874 }
1875 } else {
1876 quote! {
1877 cu29::monitoring::MonitorComponentMetadata::new(
1878 #id_lit,
1879 cu29::monitoring::ComponentType::Bridge,
1880 None,
1881 )
1882 }
1883 }
1884 })
1885 .collect();
1886 let culist_component_mapping = match build_monitor_culist_component_mapping(
1887 &culist_plan,
1888 &culist_exec_entities,
1889 &culist_bridge_specs,
1890 ) {
1891 Ok(mapping) => mapping,
1892 Err(e) => return return_error(e),
1893 };
1894
1895 let runtime_task_types: Vec<Type> = (0..task_specs.ids.len())
1896 .map(|index| runtime_task_type_for_index(&task_specs, graph, index, sim_mode))
1897 .collect();
1898
1899 let task_reflect_read_arms: Vec<proc_macro2::TokenStream> = task_specs
1900 .ids
1901 .iter()
1902 .enumerate()
1903 .map(|(index, task_id)| {
1904 let task_index = syn::Index::from(index);
1905 let task_id_lit = LitStr::new(task_id, Span::call_site());
1906 quote! {
1907 #task_id_lit => Some(&self.copper_runtime.tasks.#task_index as &dyn cu29::reflect::Reflect),
1908 }
1909 })
1910 .collect();
1911
1912 let task_reflect_write_arms: Vec<proc_macro2::TokenStream> = task_specs
1913 .ids
1914 .iter()
1915 .enumerate()
1916 .map(|(index, task_id)| {
1917 let task_index = syn::Index::from(index);
1918 let task_id_lit = LitStr::new(task_id, Span::call_site());
1919 quote! {
1920 #task_id_lit => Some(&mut self.copper_runtime.tasks.#task_index as &mut dyn cu29::reflect::Reflect),
1921 }
1922 })
1923 .collect();
1924
1925 let task_debug_state_type_path_arms: Vec<proc_macro2::TokenStream> = task_specs
1926 .ids
1927 .iter()
1928 .zip(runtime_task_types.iter())
1929 .zip(task_specs.cutypes.iter())
1930 .map(|((task_id, task_type), task_kind)| {
1931 let task_id_lit = LitStr::new(task_id, Span::call_site());
1932 let task_trait = task_trait_for_kind(*task_kind);
1933 quote! {
1934 #task_id_lit => Some(<#task_type as #task_trait>::debug_state_type_path()),
1935 }
1936 })
1937 .collect();
1938
1939 let task_debug_state_read_arms: Vec<proc_macro2::TokenStream> = task_specs
1940 .ids
1941 .iter()
1942 .zip(runtime_task_types.iter())
1943 .zip(task_specs.cutypes.iter())
1944 .enumerate()
1945 .map(|(index, ((task_id, task_type), task_kind))| {
1946 let task_index = syn::Index::from(index);
1947 let task_id_lit = LitStr::new(task_id, Span::call_site());
1948 let task_trait = task_trait_for_kind(*task_kind);
1949 quote! {
1950 #task_id_lit => Some(
1951 <#task_type as #task_trait>::with_debug_state(
1952 &self.copper_runtime.tasks.#task_index,
1953 f,
1954 )
1955 ),
1956 }
1957 })
1958 .collect();
1959
1960 let task_debug_state_registration_calls: Vec<proc_macro2::TokenStream> = task_specs
1961 .ids
1962 .iter()
1963 .enumerate()
1964 .map(|(index, _)| &runtime_task_types[index])
1965 .zip(task_specs.cutypes.iter())
1966 .map(|(task_type, task_kind)| {
1967 let task_trait = task_trait_for_kind(*task_kind);
1968 quote! {
1969 <#task_type as #task_trait>::register_debug_state_types(registry);
1970 }
1971 })
1972 .collect();
1973
1974 let mut reflect_registry_types: BTreeMap<String, Type> = BTreeMap::new();
1975 let mut add_reflect_type = |ty: Type| {
1976 let key = quote! { #ty }.to_string();
1977 reflect_registry_types.entry(key).or_insert(ty);
1978 };
1979
1980 let mut sim_bridge_channel_decls = Vec::<proc_macro2::TokenStream>::new();
1981 let bridge_runtime_types: Vec<Type> = culist_bridge_specs
1982 .iter()
1983 .map(|spec| {
1984 if sim_mode && !spec.run_in_sim {
1985 let (tx_set_ident, tx_id_ident, rx_set_ident, rx_id_ident) =
1986 sim_bridge_channel_set_idents(spec.tuple_index);
1987
1988 if !spec.tx_channels.is_empty() {
1989 let tx_entries = spec.tx_channels.iter().map(|channel| {
1990 let entry_ident = Ident::new(
1991 &channel.const_ident.to_string().to_lowercase(),
1992 Span::call_site(),
1993 );
1994 let msg_type = &channel.msg_type;
1995 quote! { #entry_ident => #msg_type, }
1996 });
1997 sim_bridge_channel_decls.push(quote! {
1998 cu29::tx_channels! {
1999 pub struct #tx_set_ident : #tx_id_ident {
2000 #(#tx_entries)*
2001 }
2002 }
2003 });
2004 }
2005
2006 if !spec.rx_channels.is_empty() {
2007 let rx_entries = spec.rx_channels.iter().map(|channel| {
2008 let entry_ident = Ident::new(
2009 &channel.const_ident.to_string().to_lowercase(),
2010 Span::call_site(),
2011 );
2012 let msg_type = &channel.msg_type;
2013 quote! { #entry_ident => #msg_type, }
2014 });
2015 sim_bridge_channel_decls.push(quote! {
2016 cu29::rx_channels! {
2017 pub struct #rx_set_ident : #rx_id_ident {
2018 #(#rx_entries)*
2019 }
2020 }
2021 });
2022 }
2023 }
2024 runtime_bridge_type_for_spec(spec, sim_mode)
2025 })
2026 .collect();
2027 let sim_bridge_channel_defs = quote! { #(#sim_bridge_channel_decls)* };
2028
2029 for (bridge_index, bridge_spec) in culist_bridge_specs.iter().enumerate() {
2030 add_reflect_type(bridge_runtime_types[bridge_index].clone());
2031 for channel in bridge_spec
2032 .rx_channels
2033 .iter()
2034 .chain(bridge_spec.tx_channels.iter())
2035 {
2036 add_reflect_type(channel.msg_type.clone());
2037 }
2038 }
2039
2040 for output_pack in extract_output_packs(&culist_plan) {
2041 for msg_type in output_pack.msg_types {
2042 add_reflect_type(msg_type);
2043 }
2044 }
2045
2046 let reflect_type_registration_calls: Vec<proc_macro2::TokenStream> = reflect_registry_types
2047 .values()
2048 .map(|ty| {
2049 quote! {
2050 registry.register::<#ty>();
2051 }
2052 })
2053 .collect();
2054
2055 let bridges_type_tokens: proc_macro2::TokenStream = if bridge_runtime_types.is_empty() {
2056 quote! { () }
2057 } else {
2058 let bridge_types_for_tuple = bridge_runtime_types.clone();
2059 let tuple: TypeTuple = parse_quote! { (#(#bridge_types_for_tuple),*,) };
2060 quote! { #tuple }
2061 };
2062
2063 let bridge_binding_idents: Vec<Ident> = culist_bridge_specs
2064 .iter()
2065 .enumerate()
2066 .map(|(idx, _)| format_ident!("bridge_{idx}"))
2067 .collect();
2068
2069 let bridge_init_statements: Vec<proc_macro2::TokenStream> = culist_bridge_specs
2070 .iter()
2071 .enumerate()
2072 .map(|(idx, spec)| {
2073 let binding_ident = &bridge_binding_idents[idx];
2074 let bridge_mapping_ref = bridge_resource_mappings.refs[idx].clone();
2075 let bridge_type = &bridge_runtime_types[idx];
2076 let bridge_name = spec.id.clone();
2077 let config_index = syn::Index::from(spec.config_index);
2078 let binding_error = LitStr::new(
2079 &format!("Failed to bind resources for bridge '{}'", bridge_name),
2080 Span::call_site(),
2081 );
2082 let tx_configs: Vec<proc_macro2::TokenStream> = spec
2083 .tx_channels
2084 .iter()
2085 .map(|channel| {
2086 let const_ident = &channel.const_ident;
2087 let channel_name = channel.id.clone();
2088 let channel_config_index = syn::Index::from(channel.config_index);
2089 quote! {
2090 {
2091 let (channel_route, channel_config) = match &bridge_cfg.channels[#channel_config_index] {
2092 cu29::config::BridgeChannelConfigRepresentation::Tx { route, config, .. } => {
2093 (route.clone(), config.clone())
2094 }
2095 _ => panic!(
2096 "Bridge '{}' channel '{}' expected to be Tx",
2097 #bridge_name,
2098 #channel_name
2099 ),
2100 };
2101 cu29::cubridge::BridgeChannelConfig::from_static(
2102 &<#bridge_type as cu29::cubridge::CuBridge>::Tx::#const_ident,
2103 channel_route,
2104 channel_config,
2105 )
2106 }
2107 }
2108 })
2109 .collect();
2110 let rx_configs: Vec<proc_macro2::TokenStream> = spec
2111 .rx_channels
2112 .iter()
2113 .map(|channel| {
2114 let const_ident = &channel.const_ident;
2115 let channel_name = channel.id.clone();
2116 let channel_config_index = syn::Index::from(channel.config_index);
2117 quote! {
2118 {
2119 let (channel_route, channel_config) = match &bridge_cfg.channels[#channel_config_index] {
2120 cu29::config::BridgeChannelConfigRepresentation::Rx { route, config, .. } => {
2121 (route.clone(), config.clone())
2122 }
2123 _ => panic!(
2124 "Bridge '{}' channel '{}' expected to be Rx",
2125 #bridge_name,
2126 #channel_name
2127 ),
2128 };
2129 cu29::cubridge::BridgeChannelConfig::from_static(
2130 &<#bridge_type as cu29::cubridge::CuBridge>::Rx::#const_ident,
2131 channel_route,
2132 channel_config,
2133 )
2134 }
2135 }
2136 })
2137 .collect();
2138 quote! {
2139 let #binding_ident = {
2140 let bridge_cfg = config
2141 .bridges
2142 .get(#config_index)
2143 .unwrap_or_else(|| panic!("Bridge '{}' missing from configuration", #bridge_name));
2144 let bridge_mapping = #bridge_mapping_ref;
2145 let bridge_resources = <<#bridge_type as cu29::cubridge::CuBridge>::Resources<'_> as ResourceBindings>::from_bindings(
2146 resources,
2147 bridge_mapping,
2148 )
2149 .map_err(|e| cu29::CuError::new_with_cause(#binding_error, e))?;
2150 let tx_channels: &[cu29::cubridge::BridgeChannelConfig<
2151 <<#bridge_type as cu29::cubridge::CuBridge>::Tx as cu29::cubridge::BridgeChannelSet>::Id,
2152 >] = &[#(#tx_configs),*];
2153 let rx_channels: &[cu29::cubridge::BridgeChannelConfig<
2154 <<#bridge_type as cu29::cubridge::CuBridge>::Rx as cu29::cubridge::BridgeChannelSet>::Id,
2155 >] = &[#(#rx_configs),*];
2156 <#bridge_type as cu29::cubridge::CuBridge>::new(
2157 bridge_cfg.config.as_ref(),
2158 tx_channels,
2159 rx_channels,
2160 bridge_resources,
2161 )?
2162 };
2163 }
2164 })
2165 .collect();
2166
2167 let bridges_instanciator = if culist_bridge_specs.is_empty() {
2168 quote! {
2169 pub fn bridges_instanciator(_config: &CuConfig, resources: &mut ResourceManager) -> CuResult<CuBridges> {
2170 let _ = resources;
2171 Ok(())
2172 }
2173 }
2174 } else {
2175 let bridge_bindings = bridge_binding_idents.clone();
2176 quote! {
2177 pub fn bridges_instanciator(config: &CuConfig, resources: &mut ResourceManager) -> CuResult<CuBridges> {
2178 #(#bridge_init_statements)*
2179 Ok((#(#bridge_bindings),*,))
2180 }
2181 }
2182 };
2183
2184 let all_sim_tasks_types = runtime_task_types.clone();
2185
2186 #[cfg(feature = "macro_debug")]
2187 eprintln!("[build task tuples]");
2188
2189 let task_types = &task_specs.task_types;
2190 let task_types_tuple: TypeTuple = if task_types.is_empty() {
2193 parse_quote! { () }
2194 } else {
2195 parse_quote! { (#(#task_types),*,) }
2196 };
2197
2198 let task_types_tuple_sim: TypeTuple = if all_sim_tasks_types.is_empty() {
2199 parse_quote! { () }
2200 } else {
2201 parse_quote! { (#(#all_sim_tasks_types),*,) }
2202 };
2203
2204 #[cfg(feature = "macro_debug")]
2205 eprintln!("[gen instances]");
2206
2207 let thread_pool_indices: HashMap<&str, usize> = copper_config
2212 .runtime
2213 .as_ref()
2214 .map(|runtime| {
2215 runtime
2216 .thread_pools
2217 .iter()
2218 .enumerate()
2219 .map(|(index, pool)| (pool.id.as_str(), index))
2220 .collect()
2221 })
2222 .unwrap_or_default();
2223 for (task_index, pool_name) in task_specs.background_pools.iter().enumerate() {
2224 if !task_specs.background_flags[task_index] {
2225 continue;
2226 }
2227 if pool_name == RT_POOL {
2233 return return_error(format!(
2234 "Background task '{}' may not use the reserved '{RT_POOL}' thread pool; it is dedicated to the parallel-rt execution engine.",
2235 task_specs.ids[task_index]
2236 ));
2237 }
2238 if !thread_pool_indices.contains_key(pool_name.as_str()) {
2239 return return_error(format!(
2240 "Background task '{}' references undefined thread pool '{}'. Define it under runtime.thread_pools.",
2241 task_specs.ids[task_index], pool_name
2242 ));
2243 }
2244 }
2245 let task_pool_indices: Vec<usize> = task_specs
2246 .background_pools
2247 .iter()
2248 .map(|pool_name| {
2249 thread_pool_indices
2250 .get(pool_name.as_str())
2251 .copied()
2252 .unwrap_or(0)
2253 })
2254 .collect();
2255
2256 let task_sim_instances_init_code = all_sim_tasks_types
2257 .iter()
2258 .enumerate()
2259 .map(|(index, ty)| {
2260 let additional_error_info = format!(
2261 "Failed to get create instance for {}, instance index {}.",
2262 task_specs.type_names[index], index
2263 );
2264 let mapping_ref = task_resource_mappings.refs[index].clone();
2265 let background = task_specs.background_flags[index]
2266 && !(sim_mode
2267 && task_specs.cutypes[index] == CuTaskType::Source
2268 && !task_specs.run_in_sim_flags[index]);
2269 let inner_task_type = &task_specs.sim_task_types[index];
2270 match task_specs.cutypes[index] {
2271 CuTaskType::Source => {
2272 if background {
2273 let pool_index = task_pool_indices[index];
2274 let pool_name = task_specs.background_pools[index].clone();
2275 quote! {
2276 {
2277 let inner_resources = <<#inner_task_type as CuSrcTask>::Resources<'_> as ResourceBindings>::from_bindings(
2278 resources,
2279 #mapping_ref,
2280 ).map_err(|e| e.add_cause(#additional_error_info))?;
2281 let threadpool = thread_pools
2282 .get(#pool_index)
2283 .and_then(|slot| slot.clone())
2284 .ok_or_else(|| CuError::from(format!(
2285 "Background task at index {} requested thread pool '{}' but it was not provided",
2286 #index, #pool_name,
2287 )))?;
2288 let resources = cu29::cuasynctask::CuAsyncSrcTaskResources {
2289 inner: inner_resources,
2290 threadpool,
2291 };
2292 <#ty as CuSrcTask>::new(all_instances_configs[#index], resources)
2293 .map_err(|e| e.add_cause(#additional_error_info))?
2294 }
2295 }
2296 } else {
2297 quote! {
2298 {
2299 let resources = <<#ty as CuSrcTask>::Resources<'_> as ResourceBindings>::from_bindings(
2300 resources,
2301 #mapping_ref,
2302 ).map_err(|e| e.add_cause(#additional_error_info))?;
2303 <#ty as CuSrcTask>::new(all_instances_configs[#index], resources)
2304 .map_err(|e| e.add_cause(#additional_error_info))?
2305 }
2306 }
2307 }
2308 }
2309 CuTaskType::Regular => {
2310 if background {
2311 let pool_index = task_pool_indices[index];
2312 let pool_name = task_specs.background_pools[index].clone();
2313 quote! {
2314 {
2315 let inner_resources = <<#inner_task_type as CuTask>::Resources<'_> as ResourceBindings>::from_bindings(
2316 resources,
2317 #mapping_ref,
2318 ).map_err(|e| e.add_cause(#additional_error_info))?;
2319 let threadpool = thread_pools
2320 .get(#pool_index)
2321 .and_then(|slot| slot.clone())
2322 .ok_or_else(|| CuError::from(format!(
2323 "Background task at index {} requested thread pool '{}' but it was not provided",
2324 #index, #pool_name,
2325 )))?;
2326 let resources = cu29::cuasynctask::CuAsyncTaskResources {
2327 inner: inner_resources,
2328 threadpool,
2329 };
2330 <#ty as CuTask>::new(all_instances_configs[#index], resources)
2331 .map_err(|e| e.add_cause(#additional_error_info))?
2332 }
2333 }
2334 } else {
2335 quote! {
2336 {
2337 let resources = <<#ty as CuTask>::Resources<'_> as ResourceBindings>::from_bindings(
2338 resources,
2339 #mapping_ref,
2340 ).map_err(|e| e.add_cause(#additional_error_info))?;
2341 <#ty as CuTask>::new(all_instances_configs[#index], resources)
2342 .map_err(|e| e.add_cause(#additional_error_info))?
2343 }
2344 }
2345 }
2346 }
2347 CuTaskType::Sink => quote! {
2348 {
2349 let resources = <<#ty as CuSinkTask>::Resources<'_> as ResourceBindings>::from_bindings(
2350 resources,
2351 #mapping_ref,
2352 ).map_err(|e| e.add_cause(#additional_error_info))?;
2353 <#ty as CuSinkTask>::new(all_instances_configs[#index], resources)
2354 .map_err(|e| e.add_cause(#additional_error_info))?
2355 }
2356 },
2357 }
2358 })
2359 .collect::<Vec<_>>();
2360
2361 let task_instances_init_code = task_specs
2362 .instantiation_types
2363 .iter()
2364 .zip(&task_specs.background_flags)
2365 .enumerate()
2366 .map(|(index, (task_type, background))| {
2367 let additional_error_info = format!(
2368 "Failed to get create instance for {}, instance index {}.",
2369 task_specs.type_names[index], index
2370 );
2371 let mapping_ref = task_resource_mappings.refs[index].clone();
2372 let inner_task_type = &task_specs.sim_task_types[index];
2373 match task_specs.cutypes[index] {
2374 CuTaskType::Source => {
2375 if *background {
2376 let pool_index = task_pool_indices[index];
2377 let pool_name = task_specs.background_pools[index].clone();
2378 quote! {
2379 {
2380 let inner_resources = <<#inner_task_type as CuSrcTask>::Resources<'_> as ResourceBindings>::from_bindings(
2381 resources,
2382 #mapping_ref,
2383 ).map_err(|e| e.add_cause(#additional_error_info))?;
2384 let threadpool = thread_pools
2385 .get(#pool_index)
2386 .and_then(|slot| slot.clone())
2387 .ok_or_else(|| CuError::from(format!(
2388 "Background task at index {} requested thread pool '{}' but it was not provided",
2389 #index, #pool_name,
2390 )))?;
2391 let resources = cu29::cuasynctask::CuAsyncSrcTaskResources {
2392 inner: inner_resources,
2393 threadpool,
2394 };
2395 <#task_type as CuSrcTask>::new(all_instances_configs[#index], resources)
2396 .map_err(|e| e.add_cause(#additional_error_info))?
2397 }
2398 }
2399 } else {
2400 quote! {
2401 {
2402 let resources = <<#task_type as CuSrcTask>::Resources<'_> as ResourceBindings>::from_bindings(
2403 resources,
2404 #mapping_ref,
2405 ).map_err(|e| e.add_cause(#additional_error_info))?;
2406 <#task_type as CuSrcTask>::new(all_instances_configs[#index], resources)
2407 .map_err(|e| e.add_cause(#additional_error_info))?
2408 }
2409 }
2410 }
2411 }
2412 CuTaskType::Regular => {
2413 if *background {
2414 let pool_index = task_pool_indices[index];
2415 let pool_name = task_specs.background_pools[index].clone();
2416 quote! {
2417 {
2418 let inner_resources = <<#inner_task_type as CuTask>::Resources<'_> as ResourceBindings>::from_bindings(
2419 resources,
2420 #mapping_ref,
2421 ).map_err(|e| e.add_cause(#additional_error_info))?;
2422 let threadpool = thread_pools
2423 .get(#pool_index)
2424 .and_then(|slot| slot.clone())
2425 .ok_or_else(|| CuError::from(format!(
2426 "Background task at index {} requested thread pool '{}' but it was not provided",
2427 #index, #pool_name,
2428 )))?;
2429 let resources = cu29::cuasynctask::CuAsyncTaskResources {
2430 inner: inner_resources,
2431 threadpool,
2432 };
2433 <#task_type as CuTask>::new(all_instances_configs[#index], resources)
2434 .map_err(|e| e.add_cause(#additional_error_info))?
2435 }
2436 }
2437 } else {
2438 quote! {
2439 {
2440 let resources = <<#task_type as CuTask>::Resources<'_> as ResourceBindings>::from_bindings(
2441 resources,
2442 #mapping_ref,
2443 ).map_err(|e| e.add_cause(#additional_error_info))?;
2444 <#task_type as CuTask>::new(all_instances_configs[#index], resources)
2445 .map_err(|e| e.add_cause(#additional_error_info))?
2446 }
2447 }
2448 }
2449 }
2450 CuTaskType::Sink => quote! {
2451 {
2452 let resources = <<#task_type as CuSinkTask>::Resources<'_> as ResourceBindings>::from_bindings(
2453 resources,
2454 #mapping_ref,
2455 ).map_err(|e| e.add_cause(#additional_error_info))?;
2456 <#task_type as CuSinkTask>::new(all_instances_configs[#index], resources)
2457 .map_err(|e| e.add_cause(#additional_error_info))?
2458 }
2459 },
2460 }
2461 })
2462 .collect::<Vec<_>>();
2463
2464 let mut keyframe_task_restore_order = Vec::new();
2465 for unit in &culist_plan.steps {
2466 let CuExecutionUnit::Step(step) = unit else {
2467 panic!("Execution loops are not supported in runtime generation");
2468 };
2469 let ExecutionEntityKind::Task { task_index } =
2470 &culist_exec_entities[step.node_id as usize].kind
2471 else {
2472 continue;
2473 };
2474 if !keyframe_task_restore_order.contains(task_index) {
2475 keyframe_task_restore_order.push(*task_index);
2476 }
2477 }
2478 if keyframe_task_restore_order.len() != task_specs.task_types.len() {
2479 return return_error(format!(
2480 "Keyframe restore order covers {} task steps but mission declares {} tasks",
2481 keyframe_task_restore_order.len(),
2482 task_specs.task_types.len()
2483 ));
2484 }
2485 let task_restore_code: Vec<proc_macro2::TokenStream> = keyframe_task_restore_order
2486 .iter()
2487 .map(|index| {
2488 let task_tuple_index = syn::Index::from(*index);
2489 quote! {
2490 tasks.#task_tuple_index.thaw(&mut decoder).map_err(|e| CuError::from("Failed to thaw").add_cause(&e.to_string()))?
2491 }
2492 })
2493 .collect();
2494
2495 let (
2498 task_start_calls,
2499 task_stop_calls,
2500 task_preprocess_calls,
2501 task_postprocess_calls,
2502 ): (Vec<_>, Vec<_>, Vec<_>, Vec<_>) = itertools::multiunzip(
2503 (0..task_specs.task_types.len())
2504 .map(|index| {
2505 let task_index = int2sliceindex(index as u32);
2506 let task_enum_name = config_id_to_enum(&task_specs.ids[index]);
2507 let enum_name = Ident::new(&task_enum_name, Span::call_site());
2508 (
2509 { let monitoring_action = quote! {
2511 let decision = self.copper_runtime.monitor.process_error(cu29::monitoring::ComponentId::new(#index), CuComponentState::Start, &error);
2512 match decision {
2513 Decision::Abort => {
2514 debug!(ctx, "Start: ABORT decision from monitoring. Component '{}' errored out \
2515 during start. Aborting all the other starts.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#index)));
2516 return Ok(());
2517
2518 }
2519 Decision::Ignore => {
2520 debug!(ctx, "Start: IGNORE decision from monitoring. Component '{}' errored out \
2521 during start. The runtime will continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#index)));
2522 }
2523 Decision::Shutdown => {
2524 debug!(ctx, "Start: SHUTDOWN decision from monitoring. Component '{}' errored out \
2525 during start. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#index)));
2526 return Err(CuError::new_with_cause("Component errored out during start.", error));
2527 }
2528 }
2529 };
2530
2531 let call_sim_callback = if sim_mode {
2532 quote! {
2533 let ovr = sim_callback(SimStep::#enum_name(CuTaskCallbackState::Start));
2535
2536 let doit = if let SimOverride::Errored(reason) = ovr {
2537 let error: CuError = reason.into();
2538 #monitoring_action
2539 false
2540 }
2541 else {
2542 ovr == SimOverride::ExecuteByRuntime
2543 };
2544 }
2545 } else {
2546 quote! {
2547 let doit = true; }
2549 };
2550
2551
2552 let alloc_open = alloc_scope_open_tokens();
2553 let alloc_close = alloc_scope_close_tokens(
2554 quote! { self.copper_runtime.monitor },
2555 quote! { #index },
2556 quote! { CuComponentState::Start },
2557 );
2558 quote! {
2559 #call_sim_callback
2560 if doit {
2561 self.copper_runtime.record_execution_marker(
2562 cu29::monitoring::ExecutionMarker {
2563 component_id: cu29::monitoring::ComponentId::new(#index),
2564 step: CuComponentState::Start,
2565 culistid: None,
2566 }
2567 );
2568 let task = &mut self.copper_runtime.tasks.#task_index;
2569 ctx.set_current_task(#index);
2570 #alloc_open
2571 let __cu_step_result = task.start(&ctx);
2572 #alloc_close
2573 if let Err(error) = __cu_step_result {
2574 #monitoring_action
2575 }
2576 }
2577 }
2578 },
2579 { let monitoring_action = quote! {
2581 let decision = self.copper_runtime.monitor.process_error(cu29::monitoring::ComponentId::new(#index), CuComponentState::Stop, &error);
2582 match decision {
2583 Decision::Abort => {
2584 debug!(ctx, "Stop: ABORT decision from monitoring. Component '{}' errored out \
2585 during stop. Aborting all the other starts.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#index)));
2586 return Ok(());
2587
2588 }
2589 Decision::Ignore => {
2590 debug!(ctx, "Stop: IGNORE decision from monitoring. Component '{}' errored out \
2591 during stop. The runtime will continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#index)));
2592 }
2593 Decision::Shutdown => {
2594 debug!(ctx, "Stop: SHUTDOWN decision from monitoring. Component '{}' errored out \
2595 during stop. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#index)));
2596 return Err(CuError::new_with_cause("Component errored out during stop.", error));
2597 }
2598 }
2599 };
2600 let call_sim_callback = if sim_mode {
2601 quote! {
2602 let ovr = sim_callback(SimStep::#enum_name(CuTaskCallbackState::Stop));
2604
2605 let doit = if let SimOverride::Errored(reason) = ovr {
2606 let error: CuError = reason.into();
2607 #monitoring_action
2608 false
2609 }
2610 else {
2611 ovr == SimOverride::ExecuteByRuntime
2612 };
2613 }
2614 } else {
2615 quote! {
2616 let doit = true; }
2618 };
2619 let alloc_open = alloc_scope_open_tokens();
2620 let alloc_close = alloc_scope_close_tokens(
2621 quote! { self.copper_runtime.monitor },
2622 quote! { #index },
2623 quote! { CuComponentState::Stop },
2624 );
2625 quote! {
2626 #call_sim_callback
2627 if doit {
2628 self.copper_runtime.record_execution_marker(
2629 cu29::monitoring::ExecutionMarker {
2630 component_id: cu29::monitoring::ComponentId::new(#index),
2631 step: CuComponentState::Stop,
2632 culistid: None,
2633 }
2634 );
2635 let task = &mut self.copper_runtime.tasks.#task_index;
2636 ctx.set_current_task(#index);
2637 #alloc_open
2638 let __cu_step_result = task.stop(&ctx);
2639 #alloc_close
2640 if let Err(error) = __cu_step_result {
2641 #monitoring_action
2642 }
2643 }
2644 }
2645 },
2646 { let monitoring_action = quote! {
2648 let decision = monitor.process_error(cu29::monitoring::ComponentId::new(#index), CuComponentState::Preprocess, &error);
2649 match decision {
2650 Decision::Abort => {
2651 debug!(ctx, "Preprocess: ABORT decision from monitoring. Component '{}' errored out \
2652 during preprocess. Aborting all the other starts.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#index)));
2653 return Ok(());
2654
2655 }
2656 Decision::Ignore => {
2657 debug!(ctx, "Preprocess: IGNORE decision from monitoring. Component '{}' errored out \
2658 during preprocess. The runtime will continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#index)));
2659 }
2660 Decision::Shutdown => {
2661 debug!(ctx, "Preprocess: SHUTDOWN decision from monitoring. Component '{}' errored out \
2662 during preprocess. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#index)));
2663 return Err(CuError::new_with_cause("Component errored out during preprocess.", error));
2664 }
2665 }
2666 };
2667 let call_sim_callback = if sim_mode {
2668 quote! {
2669 let ovr = sim_callback(SimStep::#enum_name(CuTaskCallbackState::Preprocess));
2671
2672 let doit = if let SimOverride::Errored(reason) = ovr {
2673 let error: CuError = reason.into();
2674 #monitoring_action
2675 false
2676 } else {
2677 ovr == SimOverride::ExecuteByRuntime
2678 };
2679 }
2680 } else {
2681 quote! {
2682 let doit = true; }
2684 };
2685 let alloc_open = alloc_scope_open_tokens();
2686 let alloc_close = alloc_scope_close_tokens(
2687 quote! { monitor },
2688 quote! { #index },
2689 quote! { CuComponentState::Preprocess },
2690 );
2691 quote! {
2692 #call_sim_callback
2693 if doit {
2694 execution_probe.record(cu29::monitoring::ExecutionMarker {
2695 component_id: cu29::monitoring::ComponentId::new(#index),
2696 step: CuComponentState::Preprocess,
2697 culistid: None,
2698 });
2699 ctx.set_current_task(#index);
2700 #alloc_open
2701 let maybe_error = {
2702 #rt_guard
2703 tasks.#task_index.preprocess(&ctx)
2704 };
2705 #alloc_close
2706 if let Err(error) = maybe_error {
2707 #monitoring_action
2708 }
2709 }
2710 }
2711 },
2712 { let monitoring_action = quote! {
2714 let decision = monitor.process_error(cu29::monitoring::ComponentId::new(#index), CuComponentState::Postprocess, &error);
2715 match decision {
2716 Decision::Abort => {
2717 debug!(ctx, "Postprocess: ABORT decision from monitoring. Component '{}' errored out \
2718 during postprocess. Aborting all the other starts.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#index)));
2719 return Ok(());
2720
2721 }
2722 Decision::Ignore => {
2723 debug!(ctx, "Postprocess: IGNORE decision from monitoring. Component '{}' errored out \
2724 during postprocess. The runtime will continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#index)));
2725 }
2726 Decision::Shutdown => {
2727 debug!(ctx, "Postprocess: SHUTDOWN decision from monitoring. Component '{}' errored out \
2728 during postprocess. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#index)));
2729 return Err(CuError::new_with_cause("Component errored out during postprocess.", error));
2730 }
2731 }
2732 };
2733 let call_sim_callback = if sim_mode {
2734 quote! {
2735 let ovr = sim_callback(SimStep::#enum_name(CuTaskCallbackState::Postprocess));
2737
2738 let doit = if let SimOverride::Errored(reason) = ovr {
2739 let error: CuError = reason.into();
2740 #monitoring_action
2741 false
2742 } else {
2743 ovr == SimOverride::ExecuteByRuntime
2744 };
2745 }
2746 } else {
2747 quote! {
2748 let doit = true; }
2750 };
2751 let alloc_open = alloc_scope_open_tokens();
2752 let alloc_close = alloc_scope_close_tokens(
2753 quote! { monitor },
2754 quote! { #index },
2755 quote! { CuComponentState::Postprocess },
2756 );
2757 quote! {
2758 #call_sim_callback
2759 if doit {
2760 execution_probe.record(cu29::monitoring::ExecutionMarker {
2761 component_id: cu29::monitoring::ComponentId::new(#index),
2762 step: CuComponentState::Postprocess,
2763 culistid: None,
2764 });
2765 ctx.set_current_task(#index);
2766 #alloc_open
2767 let maybe_error = {
2768 #rt_guard
2769 tasks.#task_index.postprocess(&ctx)
2770 };
2771 #alloc_close
2772 if let Err(error) = maybe_error {
2773 #monitoring_action
2774 }
2775 }
2776 }
2777 }
2778 )
2779 })
2780 );
2781
2782 let bridge_start_calls: Vec<proc_macro2::TokenStream> = culist_bridge_specs
2783 .iter()
2784 .map(|spec| {
2785 let bridge_index = int2sliceindex(spec.tuple_index as u32);
2786 let monitor_index = syn::Index::from(
2787 spec.monitor_index
2788 .expect("Bridge missing monitor index for start"),
2789 );
2790 let enum_ident = Ident::new(
2791 &config_id_to_enum(&format!("{}_bridge", spec.id)),
2792 Span::call_site(),
2793 );
2794 let call_sim = if sim_mode {
2795 quote! {
2796 let doit = {
2797 let state = SimStep::#enum_ident(cu29::simulation::CuBridgeLifecycleState::Start);
2798 let ovr = sim_callback(state);
2799 if let SimOverride::Errored(reason) = ovr {
2800 let error: CuError = reason.into();
2801 let decision = self.copper_runtime.monitor.process_error(cu29::monitoring::ComponentId::new(#monitor_index), CuComponentState::Start, &error);
2802 match decision {
2803 Decision::Abort => { debug!(ctx, "Start: ABORT decision from monitoring. Component '{}' errored out during start. Aborting all the other starts.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index))); return Ok(()); }
2804 Decision::Ignore => { debug!(ctx, "Start: IGNORE decision from monitoring. Component '{}' errored out during start. The runtime will continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index))); false }
2805 Decision::Shutdown => { debug!(ctx, "Start: SHUTDOWN decision from monitoring. Component '{}' errored out during start. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index))); return Err(CuError::new_with_cause("Component errored out during start.", error)); }
2806 }
2807 } else {
2808 ovr == SimOverride::ExecuteByRuntime
2809 }
2810 };
2811 }
2812 } else {
2813 quote! { let doit = true; }
2814 };
2815 let alloc_open = alloc_scope_open_tokens();
2816 let alloc_close = alloc_scope_close_tokens(
2817 quote! { self.copper_runtime.monitor },
2818 quote! { #monitor_index },
2819 quote! { CuComponentState::Start },
2820 );
2821 quote! {
2822 {
2823 #call_sim
2824 if !doit { return Ok(()); }
2825 self.copper_runtime.record_execution_marker(
2826 cu29::monitoring::ExecutionMarker {
2827 component_id: cu29::monitoring::ComponentId::new(#monitor_index),
2828 step: CuComponentState::Start,
2829 culistid: None,
2830 }
2831 );
2832 ctx.set_current_component(#monitor_index);
2833 ctx.clear_current_task();
2834 let bridge = &mut self.copper_runtime.bridges.#bridge_index;
2835 #alloc_open
2836 let __cu_step_result = bridge.start(&ctx);
2837 #alloc_close
2838 if let Err(error) = __cu_step_result {
2839 let decision = self.copper_runtime.monitor.process_error(cu29::monitoring::ComponentId::new(#monitor_index), CuComponentState::Start, &error);
2840 match decision {
2841 Decision::Abort => {
2842 debug!(ctx, "Start: ABORT decision from monitoring. Component '{}' errored out during start. Aborting all the other starts.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
2843 return Ok(());
2844 }
2845 Decision::Ignore => {
2846 debug!(ctx, "Start: IGNORE decision from monitoring. Component '{}' errored out during start. The runtime will continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
2847 }
2848 Decision::Shutdown => {
2849 debug!(ctx, "Start: SHUTDOWN decision from monitoring. Component '{}' errored out during start. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
2850 return Err(CuError::new_with_cause("Component errored out during start.", error));
2851 }
2852 }
2853 }
2854 }
2855 }
2856 })
2857 .collect();
2858
2859 let bridge_stop_calls: Vec<proc_macro2::TokenStream> = culist_bridge_specs
2860 .iter()
2861 .map(|spec| {
2862 let bridge_index = int2sliceindex(spec.tuple_index as u32);
2863 let monitor_index = syn::Index::from(
2864 spec.monitor_index
2865 .expect("Bridge missing monitor index for stop"),
2866 );
2867 let enum_ident = Ident::new(
2868 &config_id_to_enum(&format!("{}_bridge", spec.id)),
2869 Span::call_site(),
2870 );
2871 let call_sim = if sim_mode {
2872 quote! {
2873 let doit = {
2874 let state = SimStep::#enum_ident(cu29::simulation::CuBridgeLifecycleState::Stop);
2875 let ovr = sim_callback(state);
2876 if let SimOverride::Errored(reason) = ovr {
2877 let error: CuError = reason.into();
2878 let decision = self.copper_runtime.monitor.process_error(cu29::monitoring::ComponentId::new(#monitor_index), CuComponentState::Stop, &error);
2879 match decision {
2880 Decision::Abort => { debug!(ctx, "Stop: ABORT decision from monitoring. Component '{}' errored out during stop. Aborting all the other stops.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index))); return Ok(()); }
2881 Decision::Ignore => { debug!(ctx, "Stop: IGNORE decision from monitoring. Component '{}' errored out during stop. The runtime will continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index))); false }
2882 Decision::Shutdown => { debug!(ctx, "Stop: SHUTDOWN decision from monitoring. Component '{}' errored out during stop. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index))); return Err(CuError::new_with_cause("Component errored out during stop.", error)); }
2883 }
2884 } else {
2885 ovr == SimOverride::ExecuteByRuntime
2886 }
2887 };
2888 }
2889 } else {
2890 quote! { let doit = true; }
2891 };
2892 let alloc_open = alloc_scope_open_tokens();
2893 let alloc_close = alloc_scope_close_tokens(
2894 quote! { self.copper_runtime.monitor },
2895 quote! { #monitor_index },
2896 quote! { CuComponentState::Stop },
2897 );
2898 quote! {
2899 {
2900 #call_sim
2901 if !doit { return Ok(()); }
2902 self.copper_runtime.record_execution_marker(
2903 cu29::monitoring::ExecutionMarker {
2904 component_id: cu29::monitoring::ComponentId::new(#monitor_index),
2905 step: CuComponentState::Stop,
2906 culistid: None,
2907 }
2908 );
2909 ctx.set_current_component(#monitor_index);
2910 ctx.clear_current_task();
2911 let bridge = &mut self.copper_runtime.bridges.#bridge_index;
2912 #alloc_open
2913 let __cu_step_result = bridge.stop(&ctx);
2914 #alloc_close
2915 if let Err(error) = __cu_step_result {
2916 let decision = self.copper_runtime.monitor.process_error(cu29::monitoring::ComponentId::new(#monitor_index), CuComponentState::Stop, &error);
2917 match decision {
2918 Decision::Abort => {
2919 debug!(ctx, "Stop: ABORT decision from monitoring. Component '{}' errored out during stop. Aborting all the other stops.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
2920 return Ok(());
2921 }
2922 Decision::Ignore => {
2923 debug!(ctx, "Stop: IGNORE decision from monitoring. Component '{}' errored out during stop. The runtime will continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
2924 }
2925 Decision::Shutdown => {
2926 debug!(ctx, "Stop: SHUTDOWN decision from monitoring. Component '{}' errored out during stop. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
2927 return Err(CuError::new_with_cause("Component errored out during stop.", error));
2928 }
2929 }
2930 }
2931 }
2932 }
2933 })
2934 .collect();
2935
2936 let bridge_preprocess_calls: Vec<proc_macro2::TokenStream> = culist_bridge_specs
2937 .iter()
2938 .map(|spec| {
2939 let bridge_index = int2sliceindex(spec.tuple_index as u32);
2940 let monitor_index = syn::Index::from(
2941 spec.monitor_index
2942 .expect("Bridge missing monitor index for preprocess"),
2943 );
2944 let enum_ident = Ident::new(
2945 &config_id_to_enum(&format!("{}_bridge", spec.id)),
2946 Span::call_site(),
2947 );
2948 let call_sim = if sim_mode {
2949 quote! {
2950 let doit = {
2951 let state = SimStep::#enum_ident(cu29::simulation::CuBridgeLifecycleState::Preprocess);
2952 let ovr = sim_callback(state);
2953 if let SimOverride::Errored(reason) = ovr {
2954 let error: CuError = reason.into();
2955 let decision = monitor.process_error(cu29::monitoring::ComponentId::new(#monitor_index), CuComponentState::Preprocess, &error);
2956 match decision {
2957 Decision::Abort => { debug!(ctx, "Preprocess: ABORT decision from monitoring. Component '{}' errored out during preprocess. Aborting all the other starts.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index))); return Ok(()); }
2958 Decision::Ignore => { debug!(ctx, "Preprocess: IGNORE decision from monitoring. Component '{}' errored out during preprocess. The runtime will continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index))); false }
2959 Decision::Shutdown => { debug!(ctx, "Preprocess: SHUTDOWN decision from monitoring. Component '{}' errored out during preprocess. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index))); return Err(CuError::new_with_cause("Component errored out during preprocess.", error)); }
2960 }
2961 } else {
2962 ovr == SimOverride::ExecuteByRuntime
2963 }
2964 };
2965 }
2966 } else {
2967 quote! { let doit = true; }
2968 };
2969 let alloc_open = alloc_scope_open_tokens();
2970 let alloc_close = alloc_scope_close_tokens(
2971 quote! { monitor },
2972 quote! { #monitor_index },
2973 quote! { CuComponentState::Preprocess },
2974 );
2975 quote! {
2976 {
2977 #call_sim
2978 if doit {
2979 ctx.set_current_component(#monitor_index);
2980 ctx.clear_current_task();
2981 let bridge = &mut __cu_bridges.#bridge_index;
2982 execution_probe.record(cu29::monitoring::ExecutionMarker {
2983 component_id: cu29::monitoring::ComponentId::new(#monitor_index),
2984 step: CuComponentState::Preprocess,
2985 culistid: None,
2986 });
2987 #alloc_open
2988 let maybe_error = {
2989 #rt_guard
2990 bridge.preprocess(&ctx)
2991 };
2992 #alloc_close
2993 if let Err(error) = maybe_error {
2994 let decision = monitor.process_error(cu29::monitoring::ComponentId::new(#monitor_index), CuComponentState::Preprocess, &error);
2995 match decision {
2996 Decision::Abort => {
2997 debug!(ctx, "Preprocess: ABORT decision from monitoring. Component '{}' errored out during preprocess. Aborting all the other starts.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
2998 return Ok(());
2999 }
3000 Decision::Ignore => {
3001 debug!(ctx, "Preprocess: IGNORE decision from monitoring. Component '{}' errored out during preprocess. The runtime will continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
3002 }
3003 Decision::Shutdown => {
3004 debug!(ctx, "Preprocess: SHUTDOWN decision from monitoring. Component '{}' errored out during preprocess. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
3005 return Err(CuError::new_with_cause("Component errored out during preprocess.", error));
3006 }
3007 }
3008 }
3009 }
3010 }
3011 }
3012 })
3013 .collect();
3014
3015 let bridge_postprocess_calls: Vec<proc_macro2::TokenStream> = culist_bridge_specs
3016 .iter()
3017 .map(|spec| {
3018 let bridge_index = int2sliceindex(spec.tuple_index as u32);
3019 let monitor_index = syn::Index::from(
3020 spec.monitor_index
3021 .expect("Bridge missing monitor index for postprocess"),
3022 );
3023 let enum_ident = Ident::new(
3024 &config_id_to_enum(&format!("{}_bridge", spec.id)),
3025 Span::call_site(),
3026 );
3027 let call_sim = if sim_mode {
3028 quote! {
3029 let doit = {
3030 let state = SimStep::#enum_ident(cu29::simulation::CuBridgeLifecycleState::Postprocess);
3031 let ovr = sim_callback(state);
3032 if let SimOverride::Errored(reason) = ovr {
3033 let error: CuError = reason.into();
3034 let decision = monitor.process_error(cu29::monitoring::ComponentId::new(#monitor_index), CuComponentState::Postprocess, &error);
3035 match decision {
3036 Decision::Abort => { debug!(ctx, "Postprocess: ABORT decision from monitoring. Component '{}' errored out during postprocess. Aborting all the other starts.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index))); return Ok(()); }
3037 Decision::Ignore => { debug!(ctx, "Postprocess: IGNORE decision from monitoring. Component '{}' errored out during postprocess. The runtime will continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index))); false }
3038 Decision::Shutdown => { debug!(ctx, "Postprocess: SHUTDOWN decision from monitoring. Component '{}' errored out during postprocess. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index))); return Err(CuError::new_with_cause("Component errored out during postprocess.", error)); }
3039 }
3040 } else {
3041 ovr == SimOverride::ExecuteByRuntime
3042 }
3043 };
3044 }
3045 } else {
3046 quote! { let doit = true; }
3047 };
3048 let alloc_open = alloc_scope_open_tokens();
3049 let alloc_close = alloc_scope_close_tokens(
3050 quote! { monitor },
3051 quote! { #monitor_index },
3052 quote! { CuComponentState::Postprocess },
3053 );
3054 quote! {
3055 {
3056 #call_sim
3057 if doit {
3058 ctx.set_current_component(#monitor_index);
3059 ctx.clear_current_task();
3060 let bridge = &mut __cu_bridges.#bridge_index;
3061 kf_manager.freeze_any(clid, bridge)?;
3062 execution_probe.record(cu29::monitoring::ExecutionMarker {
3063 component_id: cu29::monitoring::ComponentId::new(#monitor_index),
3064 step: CuComponentState::Postprocess,
3065 culistid: Some(clid),
3066 });
3067 #alloc_open
3068 let maybe_error = {
3069 #rt_guard
3070 bridge.postprocess(&ctx)
3071 };
3072 #alloc_close
3073 if let Err(error) = maybe_error {
3074 let decision = monitor.process_error(cu29::monitoring::ComponentId::new(#monitor_index), CuComponentState::Postprocess, &error);
3075 match decision {
3076 Decision::Abort => {
3077 debug!(ctx, "Postprocess: ABORT decision from monitoring. Component '{}' errored out during postprocess. Aborting all the other starts.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
3078 return Ok(());
3079 }
3080 Decision::Ignore => {
3081 debug!(ctx, "Postprocess: IGNORE decision from monitoring. Component '{}' errored out during postprocess. The runtime will continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
3082 }
3083 Decision::Shutdown => {
3084 debug!(ctx, "Postprocess: SHUTDOWN decision from monitoring. Component '{}' errored out during postprocess. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
3085 return Err(CuError::new_with_cause("Component errored out during postprocess.", error));
3086 }
3087 }
3088 }
3089 }
3090 }
3091 }
3092 })
3093 .collect();
3094
3095 let mut start_calls = bridge_start_calls;
3096 start_calls.extend(task_start_calls);
3097 let mut stop_calls = task_stop_calls;
3098 stop_calls.extend(bridge_stop_calls);
3099 let mut preprocess_calls = bridge_preprocess_calls;
3100 preprocess_calls.extend(task_preprocess_calls);
3101 let mut postprocess_calls = task_postprocess_calls;
3102 postprocess_calls.extend(bridge_postprocess_calls);
3103 let parallel_rt_run_supported = std && parallel_rt_enabled && !sim_mode;
3104
3105 let bridge_restore_code: Vec<proc_macro2::TokenStream> = culist_bridge_specs
3107 .iter()
3108 .enumerate()
3109 .map(|(index, _)| {
3110 let bridge_tuple_index = syn::Index::from(index);
3111 quote! {
3112 __cu_bridges.#bridge_tuple_index
3113 .thaw(&mut decoder)
3114 .map_err(|e| CuError::from("Failed to thaw bridge").add_cause(&e.to_string()))?
3115 }
3116 })
3117 .collect();
3118
3119 let output_pack_sizes = collect_output_pack_sizes(&culist_plan);
3120 let runtime_plan_code_and_logging: Vec<(
3121 proc_macro2::TokenStream,
3122 proc_macro2::TokenStream,
3123 )> = culist_plan
3124 .steps
3125 .iter()
3126 .map(|unit| match unit {
3127 CuExecutionUnit::Step(step) => {
3128 #[cfg(feature = "macro_debug")]
3129 eprintln!(
3130 "{} -> {} as {:?}. task_id: {} Input={:?}, Output={:?}",
3131 step.node.get_id(),
3132 step.node.get_type(),
3133 step.task_type,
3134 step.node_id,
3135 step.input_msg_indices_types,
3136 step.output_msg_pack
3137 );
3138
3139 match &culist_exec_entities[step.node_id as usize].kind {
3140 ExecutionEntityKind::Task { task_index } => generate_task_execution_tokens(
3141 step,
3142 *task_index,
3143 &task_specs,
3144 &runtime_task_types[*task_index],
3145 StepGenerationContext::new(
3146 &output_pack_sizes,
3147 &task_input_layouts,
3148 mission.as_str(),
3149 sim_mode,
3150 &mission_mod,
3151 ParallelLifecyclePlacement::default(),
3152 false,
3153 ),
3154 TaskExecutionTokens::new(quote! {}, {
3155 let node_index = int2sliceindex(*task_index as u32);
3156 quote! { tasks.#node_index }
3157 }),
3158 ),
3159 ExecutionEntityKind::BridgeRx {
3160 bridge_index,
3161 channel_index,
3162 } => {
3163 let spec = &culist_bridge_specs[*bridge_index];
3164 generate_bridge_rx_execution_tokens(
3165 step,
3166 spec,
3167 *channel_index,
3168 StepGenerationContext::new(
3169 &output_pack_sizes,
3170 &task_input_layouts,
3171 mission.as_str(),
3172 sim_mode,
3173 &mission_mod,
3174 ParallelLifecyclePlacement::default(),
3175 false,
3176 ),
3177 {
3178 let bridge_tuple_index =
3179 int2sliceindex(spec.tuple_index as u32);
3180 quote! { let bridge = &mut __cu_bridges.#bridge_tuple_index; }
3181 },
3182 )
3183 }
3184 ExecutionEntityKind::BridgeTx {
3185 bridge_index,
3186 channel_index,
3187 } => {
3188 let spec = &culist_bridge_specs[*bridge_index];
3189 generate_bridge_tx_execution_tokens(
3190 step,
3191 spec,
3192 *channel_index,
3193 StepGenerationContext::new(
3194 &output_pack_sizes,
3195 &task_input_layouts,
3196 mission.as_str(),
3197 sim_mode,
3198 &mission_mod,
3199 ParallelLifecyclePlacement::default(),
3200 false,
3201 ),
3202 {
3203 let bridge_tuple_index =
3204 int2sliceindex(spec.tuple_index as u32);
3205 quote! { let bridge = &mut __cu_bridges.#bridge_tuple_index; }
3206 },
3207 )
3208 }
3209 }
3210 }
3211 CuExecutionUnit::Loop(_) => {
3212 panic!("Execution loops are not supported in runtime generation");
3213 }
3214 })
3215 .collect();
3216 let parallel_lifecycle_placements = if parallel_rt_run_supported {
3217 Some(build_parallel_lifecycle_placements(
3218 &culist_plan,
3219 &culist_exec_entities,
3220 ))
3221 } else {
3222 None
3223 };
3224 let runtime_plan_parallel_code_and_logging: Option<
3225 Vec<(proc_macro2::TokenStream, proc_macro2::TokenStream)>,
3226 > = if parallel_rt_run_supported {
3227 Some(
3228 culist_plan
3229 .steps
3230 .iter()
3231 .enumerate()
3232 .map(|(step_index, unit)| match unit {
3233 CuExecutionUnit::Step(step) => match &culist_exec_entities
3234 [step.node_id as usize]
3235 .kind
3236 {
3237 ExecutionEntityKind::Task { task_index } => {
3238 let task_index_ts = int2sliceindex(*task_index as u32);
3239 generate_task_execution_tokens(
3240 step,
3241 *task_index,
3242 &task_specs,
3243 &task_specs.task_types[*task_index],
3244 StepGenerationContext::new(
3245 &output_pack_sizes,
3246 &task_input_layouts,
3247 mission.as_str(),
3248 false,
3249 &mission_mod,
3250 parallel_lifecycle_placements
3251 .as_ref()
3252 .expect("parallel lifecycle placements missing")[step_index],
3253 true,
3254 ),
3255 TaskExecutionTokens::new(quote! {
3256 let _task_lock = step_rt.task_locks.#task_index_ts.lock().expect("parallel task lock poisoned");
3257 let task = unsafe { step_rt.task_ptrs.#task_index_ts.as_mut() };
3258 }, quote! { (*task) }),
3259 )
3260 }
3261 ExecutionEntityKind::BridgeRx {
3262 bridge_index,
3263 channel_index,
3264 } => {
3265 let spec = &culist_bridge_specs[*bridge_index];
3266 let bridge_index_ts = int2sliceindex(spec.tuple_index as u32);
3267 generate_bridge_rx_execution_tokens(
3268 step,
3269 spec,
3270 *channel_index,
3271 StepGenerationContext::new(
3272 &output_pack_sizes,
3273 &task_input_layouts,
3274 mission.as_str(),
3275 false,
3276 &mission_mod,
3277 parallel_lifecycle_placements
3278 .as_ref()
3279 .expect("parallel lifecycle placements missing")
3280 [step_index],
3281 true,
3282 ),
3283 quote! {
3284 let _bridge_lock = step_rt.bridge_locks.#bridge_index_ts.lock().expect("parallel bridge lock poisoned");
3285 let bridge = unsafe { step_rt.bridge_ptrs.#bridge_index_ts.as_mut() };
3286 },
3287 )
3288 }
3289 ExecutionEntityKind::BridgeTx {
3290 bridge_index,
3291 channel_index,
3292 } => {
3293 let spec = &culist_bridge_specs[*bridge_index];
3294 let bridge_index_ts = int2sliceindex(spec.tuple_index as u32);
3295 generate_bridge_tx_execution_tokens(
3296 step,
3297 spec,
3298 *channel_index,
3299 StepGenerationContext::new(
3300 &output_pack_sizes,
3301 &task_input_layouts,
3302 mission.as_str(),
3303 false,
3304 &mission_mod,
3305 parallel_lifecycle_placements
3306 .as_ref()
3307 .expect("parallel lifecycle placements missing")[step_index],
3308 true,
3309 ),
3310 quote! {
3311 let _bridge_lock = step_rt.bridge_locks.#bridge_index_ts.lock().expect("parallel bridge lock poisoned");
3312 let bridge = unsafe { step_rt.bridge_ptrs.#bridge_index_ts.as_mut() };
3313 },
3314 )
3315 }
3316 },
3317 CuExecutionUnit::Loop(_) => {
3318 panic!("Execution loops are not supported in runtime generation");
3319 }
3320 })
3321 .collect(),
3322 )
3323 } else {
3324 None
3325 };
3326
3327 let sim_support = if sim_mode {
3328 Some(gen_sim_support(
3329 &culist_plan,
3330 &culist_exec_entities,
3331 &culist_bridge_specs,
3332 ))
3333 } else {
3334 None
3335 };
3336
3337 let recorded_replay_support = if sim_mode {
3338 Some(gen_recorded_replay_support(
3339 &culist_plan,
3340 &culist_exec_entities,
3341 &culist_bridge_specs,
3342 ))
3343 } else {
3344 None
3345 };
3346
3347 let (run_one_iteration, start_all_tasks, stop_all_tasks, run) = if sim_mode {
3348 (
3349 quote! {
3350 fn run_one_iteration(&mut self, sim_callback: &mut impl FnMut(SimStep) -> SimOverride) -> CuResult<()>
3351 },
3352 quote! {
3353 fn start_all_tasks(&mut self, sim_callback: &mut impl FnMut(SimStep) -> SimOverride) -> CuResult<()>
3354 },
3355 quote! {
3356 fn stop_all_tasks(&mut self, sim_callback: &mut impl FnMut(SimStep) -> SimOverride) -> CuResult<()>
3357 },
3358 quote! {
3359 fn run(&mut self, sim_callback: &mut impl FnMut(SimStep) -> SimOverride) -> CuResult<()>
3360 },
3361 )
3362 } else {
3363 (
3364 quote! {
3365 fn run_one_iteration(&mut self) -> CuResult<()>
3366 },
3367 quote! {
3368 fn start_all_tasks(&mut self) -> CuResult<()>
3369 },
3370 quote! {
3371 fn stop_all_tasks(&mut self) -> CuResult<()>
3372 },
3373 quote! {
3374 fn run(&mut self) -> CuResult<()>
3375 },
3376 )
3377 };
3378
3379 let sim_callback_arg = if sim_mode {
3380 Some(quote!(sim_callback))
3381 } else {
3382 None
3383 };
3384
3385 let app_trait = if sim_mode {
3386 quote!(CuSimApplication)
3387 } else {
3388 quote!(CuApplication)
3389 };
3390
3391 let sim_callback_on_new_calls = task_specs.ids.iter().enumerate().map(|(i, id)| {
3392 let enum_name = config_id_to_enum(id);
3393 let enum_ident = Ident::new(&enum_name, Span::call_site());
3394 quote! {
3395 sim_callback(SimStep::#enum_ident(CuTaskCallbackState::New(all_instances_configs[#i].cloned())));
3397 }
3398 });
3399
3400 let sim_callback_on_new_bridges = culist_bridge_specs.iter().map(|spec| {
3401 let enum_ident = Ident::new(
3402 &config_id_to_enum(&format!("{}_bridge", spec.id)),
3403 Span::call_site(),
3404 );
3405 let cfg_index = syn::Index::from(spec.config_index);
3406 quote! {
3407 sim_callback(SimStep::#enum_ident(
3408 cu29::simulation::CuBridgeLifecycleState::New(config.bridges[#cfg_index].config.clone())
3409 ));
3410 }
3411 });
3412
3413 let sim_callback_on_new = if sim_mode {
3414 Some(quote! {
3415 let graph = config.get_graph(Some(#mission)).expect("Could not find the mission #mission");
3416 let all_instances_configs: Vec<Option<&ComponentConfig>> = graph
3417 .get_all_nodes()
3418 .iter()
3419 .map(|(_, node)| node.get_instance_config())
3420 .collect();
3421 #(#sim_callback_on_new_calls)*
3422 #(#sim_callback_on_new_bridges)*
3423 })
3424 } else {
3425 None
3426 };
3427
3428 let (runtime_plan_code, preprocess_logging_calls): (Vec<_>, Vec<_>) =
3429 itertools::multiunzip(runtime_plan_code_and_logging);
3430 let process_step_tasks_type = if sim_mode {
3431 quote!(CuSimTasks)
3432 } else {
3433 quote!(CuTasks)
3434 };
3435 let (
3436 parallel_process_step_idents,
3437 parallel_process_step_fn_defs,
3438 parallel_stage_worker_spawns,
3439 ): (
3440 Vec<Ident>,
3441 Vec<proc_macro2::TokenStream>,
3442 Vec<proc_macro2::TokenStream>,
3443 ) = if let Some(runtime_plan_parallel_code_and_logging) =
3444 &runtime_plan_parallel_code_and_logging
3445 {
3446 let (runtime_plan_parallel_step_code, _): (Vec<_>, Vec<_>) =
3447 itertools::multiunzip(runtime_plan_parallel_code_and_logging.clone());
3448 let parallel_process_step_idents: Vec<Ident> = (0..runtime_plan_parallel_step_code
3449 .len())
3450 .map(|index| format_ident!("__cu_parallel_process_step_{index}"))
3451 .collect();
3452 let parallel_process_step_fn_defs: Vec<proc_macro2::TokenStream> =
3453 parallel_process_step_idents
3454 .iter()
3455 .zip(runtime_plan_parallel_step_code.iter())
3456 .map(|(step_ident, step_code)| {
3457 quote! {
3458 #[inline(always)]
3459 fn #step_ident(
3460 step_rt: &mut ParallelProcessStepRuntime<'_>,
3461 ) -> cu29::curuntime::ProcessStepResult {
3462 let clock = step_rt.clock;
3463 let execution_probe = step_rt.execution_probe;
3464 let monitor = step_rt.monitor;
3465 let kf_manager = ParallelKeyFrameAccessor::new(
3466 step_rt.kf_manager_ptr,
3467 step_rt.kf_lock,
3468 );
3469 let culist = &mut *step_rt.culist;
3470 let clid = step_rt.clid;
3471 let ctx = &mut step_rt.ctx;
3472 let msgs = &mut culist.msgs.0;
3473 #step_code
3474 }
3475 }
3476 })
3477 .collect();
3478 let parallel_stage_worker_spawns: Vec<proc_macro2::TokenStream> =
3479 parallel_process_step_idents
3480 .iter()
3481 .enumerate()
3482 .map(|(stage_index, step_ident)| {
3483 let stage_index_lit = syn::Index::from(stage_index);
3484 let receiver_ident =
3485 format_ident!("__cu_parallel_stage_rx_{stage_index}");
3486 quote! {
3487 {
3488 let mut #receiver_ident = stage_receivers
3489 .next()
3490 .expect("parallel stage receiver missing");
3491 let mut next_stage_tx = stage_senders.next();
3492 let done_tx = done_tx.clone();
3493 let shutdown = std::sync::Arc::clone(&shutdown);
3494 let clock = clock.clone();
3495 let instance_id = instance_id;
3496 let subsystem_code = subsystem_code;
3497 let execution_probe_ptr = execution_probe_ptr;
3498 let monitor_ptr = monitor_ptr;
3499 let task_ptrs = task_ptrs;
3500 let task_locks = std::sync::Arc::clone(&task_locks);
3501 let bridge_ptrs = bridge_ptrs;
3502 let bridge_locks = std::sync::Arc::clone(&bridge_locks);
3503 let kf_manager_ptr = kf_manager_ptr;
3504 let kf_lock = std::sync::Arc::clone(&kf_lock);
3505 let rt_pool = std::sync::Arc::clone(&rt_pool);
3506 scope.spawn(move || {
3507 if let Some(rt_pool) = rt_pool.as_ref()
3511 && cu29::thread_pool::apply_current_thread_scheduling(
3512 rt_pool,
3513 #stage_index_lit,
3514 )
3515 .is_err()
3516 {
3517 shutdown.store(true, Ordering::Release);
3518 return;
3519 }
3520 loop {
3521 let job = match #receiver_ident.recv() {
3522 Ok(job) => job,
3523 Err(_) => break,
3524 };
3525 let clid = job.clid;
3526 let culist = job.culist;
3527
3528 let terminal_result = if shutdown.load(Ordering::Acquire) {
3529 #mission_mod::ParallelWorkerResult {
3530 clid,
3531 culist: Some(culist),
3532 outcome: Err(CuError::from(
3533 "Parallel runtime shutting down after an earlier stage failure",
3534 )),
3535 raw_payload_bytes: 0,
3536 handle_bytes: 0,
3537 }
3538 } else {
3539 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3540 let execution_probe = unsafe { execution_probe_ptr.as_ref() };
3541 let monitor = unsafe { monitor_ptr.as_ref() };
3542 let mut culist = culist;
3543 let mut step_rt = #mission_mod::ParallelProcessStepRuntime {
3544 clock: &clock,
3545 execution_probe,
3546 monitor,
3547 task_ptrs: &task_ptrs,
3548 task_locks: task_locks.as_ref(),
3549 bridge_ptrs: &bridge_ptrs,
3550 bridge_locks: bridge_locks.as_ref(),
3551 kf_manager_ptr,
3552 kf_lock: kf_lock.as_ref(),
3553 culist: culist.as_mut(),
3554 clid,
3555 ctx: cu29::context::CuContext::from_runtime_metadata(
3556 clock.clone(),
3557 clid,
3558 instance_id,
3559 subsystem_code,
3560 #mission_mod::TASK_IDS,
3561 ),
3562 };
3563 let outcome = #step_ident(&mut step_rt);
3564 drop(step_rt);
3565 (culist, outcome)
3566 })) {
3567 Ok((culist, Ok(cu29::curuntime::ProcessStepOutcome::Continue))) => {
3568 if shutdown.load(Ordering::Acquire) {
3569 #mission_mod::ParallelWorkerResult {
3570 clid,
3571 culist: Some(culist),
3572 outcome: Err(CuError::from(
3573 "Parallel runtime shutting down after an earlier stage failure",
3574 )),
3575 raw_payload_bytes: 0,
3576 handle_bytes: 0,
3577 }
3578 } else if let Some(next_stage_tx) = next_stage_tx.as_mut() {
3579 let forwarded_job = #mission_mod::ParallelWorkerJob { clid, culist };
3580 match next_stage_tx.send(forwarded_job) {
3581 Ok(()) => continue,
3582 Err(send_error) => {
3583 let failed_job = send_error.0;
3584 shutdown.store(true, Ordering::Release);
3585 #mission_mod::ParallelWorkerResult {
3586 clid,
3587 culist: Some(failed_job.culist),
3588 outcome: Err(CuError::from(format!(
3589 "Parallel stage {} could not hand CopperList #{} to the next stage",
3590 #stage_index_lit,
3591 clid
3592 ))),
3593 raw_payload_bytes: 0,
3594 handle_bytes: 0,
3595 }
3596 }
3597 }
3598 } else {
3599 #mission_mod::ParallelWorkerResult {
3600 clid,
3601 culist: Some(culist),
3602 outcome: Ok(cu29::curuntime::ProcessStepOutcome::Continue),
3603 raw_payload_bytes: 0,
3604 handle_bytes: 0,
3605 }
3606 }
3607 }
3608 Ok((culist, Ok(cu29::curuntime::ProcessStepOutcome::AbortCopperList))) => {
3609 #mission_mod::ParallelWorkerResult {
3610 clid,
3611 culist: Some(culist),
3612 outcome: Ok(cu29::curuntime::ProcessStepOutcome::AbortCopperList),
3613 raw_payload_bytes: 0,
3614 handle_bytes: 0,
3615 }
3616 }
3617 Ok((culist, Err(error))) => {
3618 shutdown.store(true, Ordering::Release);
3619 #mission_mod::ParallelWorkerResult {
3620 clid,
3621 culist: Some(culist),
3622 outcome: Err(error),
3623 raw_payload_bytes: 0,
3624 handle_bytes: 0,
3625 }
3626 }
3627 Err(payload) => {
3628 shutdown.store(true, Ordering::Release);
3629 let panic_message =
3630 cu29::monitoring::panic_payload_to_string(payload.as_ref());
3631 #mission_mod::ParallelWorkerResult {
3632 clid,
3633 culist: None,
3634 outcome: Err(CuError::from(format!(
3635 "Panic while processing CopperList #{} in stage {}: {}",
3636 clid,
3637 #stage_index_lit,
3638 panic_message
3639 ))),
3640 raw_payload_bytes: 0,
3641 handle_bytes: 0,
3642 }
3643 }
3644 }
3645 };
3646
3647 if done_tx.send(terminal_result).is_err() {
3648 break;
3649 }
3650 }
3651 });
3652 }
3653 }
3654 })
3655 .collect();
3656 (
3657 parallel_process_step_idents,
3658 parallel_process_step_fn_defs,
3659 parallel_stage_worker_spawns,
3660 )
3661 } else {
3662 (Vec::new(), Vec::new(), Vec::new())
3663 };
3664 let parallel_process_stage_count_tokens =
3665 proc_macro2::Literal::usize_unsuffixed(parallel_process_step_idents.len());
3666 let parallel_task_ptrs_type = if runtime_task_types.is_empty() {
3667 quote! { () }
3668 } else {
3669 let elems = runtime_task_types
3670 .iter()
3671 .map(|ty| quote! { ParallelSharedPtr<#ty> });
3672 quote! { (#(#elems),*,) }
3673 };
3674 let parallel_task_locks_type = if runtime_task_types.is_empty() {
3675 quote! { () }
3676 } else {
3677 let elems = (0..runtime_task_types.len()).map(|_| quote! { std::sync::Mutex<()> });
3678 quote! { (#(#elems),*,) }
3679 };
3680 let parallel_task_ptr_values = if runtime_task_types.is_empty() {
3681 quote! { () }
3682 } else {
3683 let elems = (0..runtime_task_types.len()).map(|index| {
3684 let index = syn::Index::from(index);
3685 quote! { ParallelSharedPtr::new(&mut runtime.tasks.#index as *mut _) }
3686 });
3687 quote! { (#(#elems),*,) }
3688 };
3689 let parallel_task_lock_values = if runtime_task_types.is_empty() {
3690 quote! { () }
3691 } else {
3692 let elems = (0..runtime_task_types.len()).map(|_| quote! { std::sync::Mutex::new(()) });
3693 quote! { (#(#elems),*,) }
3694 };
3695 let parallel_bridge_ptrs_type = if bridge_runtime_types.is_empty() {
3696 quote! { () }
3697 } else {
3698 let elems = bridge_runtime_types
3699 .iter()
3700 .map(|ty| quote! { ParallelSharedPtr<#ty> });
3701 quote! { (#(#elems),*,) }
3702 };
3703 let parallel_bridge_locks_type = if bridge_runtime_types.is_empty() {
3704 quote! { () }
3705 } else {
3706 let elems = (0..bridge_runtime_types.len()).map(|_| quote! { std::sync::Mutex<()> });
3707 quote! { (#(#elems),*,) }
3708 };
3709 let parallel_bridge_ptr_values = if bridge_runtime_types.is_empty() {
3710 quote! { () }
3711 } else {
3712 let elems = (0..bridge_runtime_types.len()).map(|index| {
3713 let index = syn::Index::from(index);
3714 quote! { ParallelSharedPtr::new(&mut runtime.bridges.#index as *mut _) }
3715 });
3716 quote! { (#(#elems),*,) }
3717 };
3718 let parallel_bridge_lock_values = if bridge_runtime_types.is_empty() {
3719 quote! { () }
3720 } else {
3721 let elems =
3722 (0..bridge_runtime_types.len()).map(|_| quote! { std::sync::Mutex::new(()) });
3723 quote! { (#(#elems),*,) }
3724 };
3725 let parallel_rt_support_tokens = if parallel_rt_run_supported {
3726 quote! {
3727 type ParallelTaskPtrs = #parallel_task_ptrs_type;
3728 type ParallelTaskLocks = #parallel_task_locks_type;
3729 type ParallelBridgePtrs = #parallel_bridge_ptrs_type;
3730 type ParallelBridgeLocks = #parallel_bridge_locks_type;
3731
3732 struct ParallelSharedPtr<T>(*mut T);
3733
3734 impl<T> Clone for ParallelSharedPtr<T> {
3735 #[inline(always)]
3736 fn clone(&self) -> Self {
3737 *self
3738 }
3739 }
3740
3741 impl<T> Copy for ParallelSharedPtr<T> {}
3742
3743 impl<T> ParallelSharedPtr<T> {
3744 #[inline(always)]
3745 const fn new(ptr: *mut T) -> Self {
3746 Self(ptr)
3747 }
3748
3749 #[inline(always)]
3750 const fn from_ref(ptr: *const T) -> Self {
3751 Self(ptr as *mut T)
3752 }
3753
3754 #[inline(always)]
3755 unsafe fn as_mut<'a>(self) -> &'a mut T {
3756 unsafe { &mut *self.0 }
3757 }
3758
3759 #[inline(always)]
3760 unsafe fn as_ref<'a>(self) -> &'a T {
3761 unsafe { &*self.0 }
3762 }
3763 }
3764
3765 unsafe impl<T: Send> Send for ParallelSharedPtr<T> {}
3766 unsafe impl<T: Send> Sync for ParallelSharedPtr<T> {}
3767
3768 struct ParallelKeyFrameAccessor<'a> {
3769 ptr: ParallelSharedPtr<cu29::curuntime::KeyFramesManager>,
3770 lock: &'a std::sync::Mutex<()>,
3771 }
3772
3773 impl<'a> ParallelKeyFrameAccessor<'a> {
3774 #[inline(always)]
3775 fn new(
3776 ptr: ParallelSharedPtr<cu29::curuntime::KeyFramesManager>,
3777 lock: &'a std::sync::Mutex<()>,
3778 ) -> Self {
3779 Self { ptr, lock }
3780 }
3781
3782 #[inline(always)]
3783 fn freeze_task(
3784 &self,
3785 culistid: u64,
3786 task: &impl cu29::cutask::Freezable,
3787 ) -> CuResult<usize> {
3788 let _guard = self.lock.lock().expect("parallel keyframe lock poisoned");
3789 let manager = unsafe { self.ptr.as_mut() };
3790 manager.freeze_task(culistid, task)
3791 }
3792
3793 #[inline(always)]
3794 fn freeze_any(
3795 &self,
3796 culistid: u64,
3797 item: &impl cu29::cutask::Freezable,
3798 ) -> CuResult<usize> {
3799 let _guard = self.lock.lock().expect("parallel keyframe lock poisoned");
3800 let manager = unsafe { self.ptr.as_mut() };
3801 manager.freeze_any(culistid, item)
3802 }
3803 }
3804
3805 struct ParallelProcessStepRuntime<'a> {
3806 clock: &'a RobotClock,
3807 execution_probe: &'a cu29::monitoring::RuntimeExecutionProbe,
3808 monitor: &'a #monitor_type,
3809 task_ptrs: &'a ParallelTaskPtrs,
3810 task_locks: &'a ParallelTaskLocks,
3811 bridge_ptrs: &'a ParallelBridgePtrs,
3812 bridge_locks: &'a ParallelBridgeLocks,
3813 kf_manager_ptr: ParallelSharedPtr<cu29::curuntime::KeyFramesManager>,
3814 kf_lock: &'a std::sync::Mutex<()>,
3815 culist: &'a mut CuList,
3816 clid: u64,
3817 ctx: cu29::context::CuContext,
3818 }
3819
3820 struct ParallelWorkerJob {
3821 clid: u64,
3822 culist: Box<CuList>,
3823 }
3824
3825 struct ParallelWorkerResult {
3826 clid: u64,
3827 culist: Option<Box<CuList>>,
3828 outcome: cu29::curuntime::ProcessStepResult,
3829 raw_payload_bytes: u64,
3830 handle_bytes: u64,
3831 }
3832
3833 #[inline(always)]
3834 fn assert_parallel_rt_send_bounds()
3835 where
3836 CuList: Send,
3837 #process_step_tasks_type: Send,
3838 CuBridges: Send,
3839 #monitor_type: Sync,
3840 {
3841 }
3842
3843 #(#parallel_process_step_fn_defs)*
3844 }
3845 } else {
3846 quote! {}
3847 };
3848
3849 let config_load_stmt =
3850 build_config_load_stmt(std, application_name, subsystem_id.as_deref());
3851
3852 let copperlist_count_check = quote! {
3853 let configured_copperlist_count = config
3854 .logging
3855 .as_ref()
3856 .and_then(|logging| logging.copperlist_count)
3857 .unwrap_or(#copperlist_count_tokens);
3858 if configured_copperlist_count != #copperlist_count_tokens {
3859 return Err(CuError::from(format!(
3860 "Configured logging.copperlist_count ({configured_copperlist_count}) does not match the runtime compiled into this binary ({})",
3861 #copperlist_count_tokens
3862 )));
3863 }
3864 };
3865
3866 let prepare_config_sig = if std {
3867 quote! {
3868 fn prepare_config(
3869 instance_id: u32,
3870 config_override: Option<CuConfig>,
3871 ) -> CuResult<(CuConfig, RuntimeLifecycleConfigSource)>
3872 }
3873 } else {
3874 quote! {
3875 fn prepare_config() -> CuResult<(CuConfig, RuntimeLifecycleConfigSource)>
3876 }
3877 };
3878
3879 let prepare_config_call = if std {
3880 quote! { Self::prepare_config(instance_id, config_override)? }
3881 } else {
3882 quote! { Self::prepare_config()? }
3883 };
3884
3885 let prepare_resources_sig = if std {
3886 quote! {
3887 pub fn prepare_resources_for_instance(
3888 instance_id: u32,
3889 config_override: Option<CuConfig>,
3890 ) -> CuResult<AppResources>
3891 }
3892 } else {
3893 quote! {
3894 pub fn prepare_resources() -> CuResult<AppResources>
3895 }
3896 };
3897
3898 let prepare_resources_compat_fn = if std {
3899 Some(quote! {
3900 pub fn prepare_resources(
3901 config_override: Option<CuConfig>,
3902 ) -> CuResult<AppResources> {
3903 Self::prepare_resources_for_instance(0, config_override)
3904 }
3905 })
3906 } else {
3907 None
3908 };
3909
3910 let init_resources_compat_fn = if std {
3911 Some(quote! {
3912 pub fn init_resources_for_instance(
3913 instance_id: u32,
3914 config_override: Option<CuConfig>,
3915 ) -> CuResult<AppResources> {
3916 Self::prepare_resources_for_instance(instance_id, config_override)
3917 }
3918
3919 pub fn init_resources(
3920 config_override: Option<CuConfig>,
3921 ) -> CuResult<AppResources> {
3922 Self::prepare_resources(config_override)
3923 }
3924 })
3925 } else {
3926 Some(quote! {
3927 pub fn init_resources() -> CuResult<AppResources> {
3928 Self::prepare_resources()
3929 }
3930 })
3931 };
3932
3933 let build_with_resources_sig = if sim_mode {
3934 quote! {
3935 fn build_with_resources<S: SectionStorage + 'static, L: UnifiedLogWrite<S> + 'static>(
3936 clock: RobotClock,
3937 unified_logger: Arc<Mutex<L>>,
3938 app_resources: AppResources,
3939 instance_id: u32,
3940 sim_callback: &mut impl FnMut(SimStep) -> SimOverride,
3941 ) -> CuResult<Self>
3942 }
3943 } else {
3944 quote! {
3945 fn build_with_resources<S: SectionStorage + 'static, L: UnifiedLogWrite<S> + 'static>(
3946 clock: RobotClock,
3947 unified_logger: Arc<Mutex<L>>,
3948 app_resources: AppResources,
3949 instance_id: u32,
3950 ) -> CuResult<Self>
3951 }
3952 };
3953 let parallel_rt_metadata_arg = if std && parallel_rt_enabled {
3954 Some(quote! {
3955 &#mission_mod::PARALLEL_RT_METADATA,
3956 })
3957 } else {
3958 None
3959 };
3960
3961 let kill_handler = if std && signal_handler {
3962 Some(quote! {
3963 ctrlc::set_handler(move || {
3964 STOP_FLAG.store(true, Ordering::SeqCst);
3965 }).expect("Error setting Ctrl-C handler");
3966 })
3967 } else {
3968 None
3969 };
3970
3971 let run_loop = if std {
3972 quote! {{
3973 let mut rate_limiter = self
3974 .copper_runtime
3975 .runtime_config
3976 .rate_target_hz
3977 .map(|rate| cu29::curuntime::LoopRateLimiter::from_rate_target_hz(
3978 rate,
3979 self.copper_runtime.clock_ref(),
3980 ))
3981 .transpose()?;
3982 loop {
3983 let result = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(
3984 || <Self as #app_trait<S, L>>::run_one_iteration(self, #sim_callback_arg)
3985 )) {
3986 Ok(result) => result,
3987 Err(payload) => {
3988 let panic_message = cu29::monitoring::panic_payload_to_string(payload.as_ref());
3989 self.copper_runtime.monitor.process_panic(&panic_message);
3990 let _ = self.log_runtime_lifecycle_event(RuntimeLifecycleEvent::Panic {
3991 message: panic_message.clone(),
3992 file: None,
3993 line: None,
3994 column: None,
3995 });
3996 Err(CuError::from(format!(
3997 "Panic while running one iteration: {}",
3998 panic_message
3999 )))
4000 }
4001 };
4002
4003 if let Some(rate_limiter) = rate_limiter.as_mut() {
4004 rate_limiter.limit(self.copper_runtime.clock_ref());
4005 }
4006
4007 if STOP_FLAG.load(Ordering::SeqCst) || result.is_err() {
4008 break result;
4009 }
4010 }
4011 }}
4012 } else {
4013 quote! {{
4014 let mut rate_limiter = self
4015 .copper_runtime
4016 .runtime_config
4017 .rate_target_hz
4018 .map(|rate| cu29::curuntime::LoopRateLimiter::from_rate_target_hz(
4019 rate,
4020 self.copper_runtime.clock_ref(),
4021 ))
4022 .transpose()?;
4023 loop {
4024 let result = <Self as #app_trait<S, L>>::run_one_iteration(self, #sim_callback_arg);
4025 if let Some(rate_limiter) = rate_limiter.as_mut() {
4026 rate_limiter.limit(self.copper_runtime.clock_ref());
4027 }
4028
4029 if STOP_FLAG.load(Ordering::SeqCst) || result.is_err() {
4030 break result;
4031 }
4032 }
4033 }}
4034 };
4035
4036 #[cfg(feature = "macro_debug")]
4037 eprintln!("[build the run methods]");
4038 let run_body: proc_macro2::TokenStream = if parallel_rt_run_supported {
4039 quote! {
4040 static STOP_FLAG: AtomicBool = AtomicBool::new(false);
4041
4042 #kill_handler
4043
4044 <Self as #app_trait<S, L>>::start_all_tasks(self)?;
4045 let result = std::thread::scope(|scope| -> CuResult<()> {
4046 #mission_mod::assert_parallel_rt_send_bounds();
4047
4048 let runtime = &mut self.copper_runtime;
4049 let clock_handle = runtime.clock();
4050 let clock = &clock_handle;
4051 let instance_id = runtime.instance_id();
4052 let subsystem_code = runtime.subsystem_code();
4053 let execution_probe = runtime.execution_probe.as_ref();
4054 let monitor = &runtime.monitor;
4055 let cl_manager = &mut runtime.copperlists_manager;
4056 let parallel_rt = &runtime.parallel_rt;
4057 let execution_probe_ptr =
4058 #mission_mod::ParallelSharedPtr::from_ref(execution_probe as *const _);
4059 let monitor_ptr =
4060 #mission_mod::ParallelSharedPtr::from_ref(monitor as *const _);
4061 let task_ptrs: #mission_mod::ParallelTaskPtrs = #parallel_task_ptr_values;
4062 let task_locks = std::sync::Arc::new(#parallel_task_lock_values);
4063 let bridge_ptrs: #mission_mod::ParallelBridgePtrs = #parallel_bridge_ptr_values;
4064 let bridge_locks = std::sync::Arc::new(#parallel_bridge_lock_values);
4065 let kf_manager_ptr =
4066 #mission_mod::ParallelSharedPtr::new(&mut runtime.keyframes_manager as *mut _);
4067 let kf_lock = std::sync::Arc::new(std::sync::Mutex::new(()));
4068 let mut free_copperlists =
4069 cu29::curuntime::allocate_boxed_copperlists::<CuStampedDataSet, #copperlist_count_tokens>();
4070 let start_clid = cl_manager.next_cl_id();
4071 parallel_rt.reset_cursors(start_clid);
4072
4073 let stage_count = #parallel_process_stage_count_tokens;
4074 debug_assert_eq!(parallel_rt.metadata().process_stage_count(), stage_count);
4075 if stage_count == 0 {
4076 return Err(CuError::from(
4077 "Parallel runtime requires at least one generated process stage",
4078 ));
4079 }
4080
4081 let queue_capacity = parallel_rt.in_flight_limit().max(1);
4082 let mut stage_senders = Vec::with_capacity(stage_count);
4083 let mut stage_receivers = Vec::with_capacity(stage_count);
4084 for _stage_index in 0..stage_count {
4085 let (stage_tx, stage_rx) =
4086 cu29::parallel_queue::stage_queue::<#mission_mod::ParallelWorkerJob>(
4087 queue_capacity,
4088 );
4089 stage_senders.push(stage_tx);
4090 stage_receivers.push(stage_rx);
4091 }
4092 let (done_tx, done_rx) =
4093 std::sync::mpsc::channel::<#mission_mod::ParallelWorkerResult>();
4094 let shutdown = std::sync::Arc::new(AtomicBool::new(false));
4095 let mut stage_senders = stage_senders.into_iter();
4096 let mut entry_stage_tx = stage_senders
4097 .next()
4098 .expect("parallel stage pipeline has no entry queue");
4099 let mut stage_receivers = stage_receivers.into_iter();
4100 let rt_pool = std::sync::Arc::new(
4103 runtime
4104 .runtime_config
4105 .thread_pools
4106 .iter()
4107 .find(|pool| pool.id == cu29::config::RT_POOL)
4108 .cloned(),
4109 );
4110 #(#parallel_stage_worker_spawns)*
4111 drop(done_tx);
4112
4113 let mut dispatch_limiter = runtime
4114 .runtime_config
4115 .rate_target_hz
4116 .map(|rate| cu29::curuntime::LoopRateLimiter::from_rate_target_hz(rate, clock))
4117 .transpose()?;
4118 let mut in_flight = 0usize;
4119 let mut stop_launching = false;
4120 let mut next_launch_clid = start_clid;
4121 let mut next_commit_clid = start_clid;
4122 let mut pending_results =
4123 std::collections::BTreeMap::<u64, #mission_mod::ParallelWorkerResult>::new();
4124 let mut active_keyframe_clid: Option<u64> = None;
4125 let mut fatal_error: Option<CuError> = None;
4126
4127 loop {
4128 while let Some(recycled_culist) = cl_manager.try_reclaim_boxed()? {
4129 free_copperlists.push(recycled_culist);
4130 }
4131
4132 if !stop_launching && fatal_error.is_none() {
4133 let next_clid = next_launch_clid;
4134 let rate_ready = dispatch_limiter
4135 .as_ref()
4136 .map(|limiter| limiter.is_ready(clock))
4137 .unwrap_or(true);
4138 let keyframe_ready = {
4139 let _keyframe_lock = kf_lock.lock().expect("parallel keyframe lock poisoned");
4140 let kf_manager = unsafe { kf_manager_ptr.as_mut() };
4141 active_keyframe_clid.is_none() || !kf_manager.captures_keyframe(next_clid)
4142 };
4143
4144 if in_flight < parallel_rt.in_flight_limit()
4145 && rate_ready
4146 && keyframe_ready
4147 && !free_copperlists.is_empty()
4148 {
4149 let should_launch = true;
4152
4153 if should_launch {
4154 let mut culist = free_copperlists
4155 .pop()
4156 .expect("parallel CopperList pool unexpectedly empty");
4157 let clid = next_clid;
4158 culist.reset_for_runtime_use(clid);
4159 {
4160 let _keyframe_lock =
4161 kf_lock.lock().expect("parallel keyframe lock poisoned");
4162 let kf_manager = unsafe { kf_manager_ptr.as_mut() };
4163 kf_manager.reset(clid, clock);
4164 if kf_manager.captures_keyframe(clid) {
4165 active_keyframe_clid = Some(clid);
4166 }
4167 }
4168 culist.change_state(cu29::copperlist::CopperListState::Processing);
4169 entry_stage_tx
4170 .send(#mission_mod::ParallelWorkerJob {
4171 clid,
4172 culist,
4173 })
4174 .map_err(|e| {
4175 shutdown.store(true, Ordering::Release);
4176 CuError::from("Failed to enqueue CopperList for parallel stage processing")
4177 .add_cause(e.to_string().as_str())
4178 })?;
4179 next_launch_clid += 1;
4180 in_flight += 1;
4181 if let Some(limiter) = dispatch_limiter.as_mut() {
4182 limiter.mark_tick(clock);
4183 }
4184 }
4185
4186 if STOP_FLAG.load(Ordering::SeqCst) {
4187 stop_launching = true;
4188 }
4189 continue;
4190 }
4191 }
4192
4193 if in_flight == 0 {
4194 if stop_launching || fatal_error.is_some() {
4195 break;
4196 }
4197
4198 if free_copperlists.is_empty() {
4199 free_copperlists.push(cl_manager.wait_reclaim_boxed()?);
4200 continue;
4201 }
4202
4203 if let Some(limiter) = dispatch_limiter.as_ref()
4204 && !limiter.is_ready(clock)
4205 {
4206 limiter.wait_until_ready(clock);
4207 continue;
4208 }
4209 }
4210
4211 let recv_result = if !stop_launching && fatal_error.is_none() {
4212 if let Some(limiter) = dispatch_limiter.as_ref() {
4213 if let Some(remaining) = limiter.remaining(clock)
4214 && in_flight > 0
4215 {
4216 done_rx.recv_timeout(std::time::Duration::from(remaining))
4217 } else {
4218 done_rx
4219 .recv()
4220 .map_err(|_| std::sync::mpsc::RecvTimeoutError::Disconnected)
4221 }
4222 } else {
4223 done_rx
4224 .recv()
4225 .map_err(|_| std::sync::mpsc::RecvTimeoutError::Disconnected)
4226 }
4227 } else {
4228 done_rx
4229 .recv()
4230 .map_err(|_| std::sync::mpsc::RecvTimeoutError::Disconnected)
4231 };
4232
4233 let worker_result = match recv_result {
4234 Ok(worker_result) => worker_result,
4235 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
4236 if STOP_FLAG.load(Ordering::SeqCst) {
4237 stop_launching = true;
4238 }
4239 continue;
4240 }
4241 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
4242 shutdown.store(true, Ordering::Release);
4243 return Err(CuError::from(
4244 "Parallel stage worker disconnected unexpectedly",
4245 ));
4246 }
4247 };
4248 in_flight = in_flight.saturating_sub(1);
4249 pending_results.insert(worker_result.clid, worker_result);
4250
4251 while let Some(worker_result) = pending_results.remove(&next_commit_clid) {
4252 if fatal_error.is_none()
4253 && parallel_rt.current_commit_clid() != worker_result.clid
4254 {
4255 shutdown.store(true, Ordering::Release);
4256 fatal_error = Some(CuError::from(format!(
4257 "Parallel commit checkpoint out of sync: expected {}, got {}",
4258 parallel_rt.current_commit_clid(),
4259 worker_result.clid
4260 )));
4261 stop_launching = true;
4262 }
4263
4264 let mut worker_result = worker_result;
4265 if fatal_error.is_none() {
4266 match worker_result.outcome {
4267 Ok(cu29::curuntime::ProcessStepOutcome::AbortCopperList) => {
4268 let mut culist = worker_result
4269 .culist
4270 .take()
4271 .expect("parallel abort result missing CopperList ownership");
4272 let mut commit_ctx = cu29::context::CuContext::from_runtime_metadata(
4273 clock.clone(),
4274 worker_result.clid,
4275 instance_id,
4276 subsystem_code,
4277 #mission_mod::TASK_IDS,
4278 );
4279 commit_ctx.clear_current_component();
4280 commit_ctx.clear_current_task();
4281 let monitor_result = monitor.process_copperlist(
4282 &commit_ctx,
4283 #mission_mod::MONITOR_LAYOUT.view(&#mission_mod::collect_metadata(&culist)),
4284 );
4285 match cl_manager.end_of_processing_boxed(culist)? {
4286 cu29::curuntime::OwnedCopperListSubmission::Recycled(culist) => {
4287 free_copperlists.push(culist);
4288 }
4289 cu29::curuntime::OwnedCopperListSubmission::Pending => {}
4290 }
4291 monitor_result?;
4292 }
4293 Ok(cu29::curuntime::ProcessStepOutcome::Continue) => {
4294 let mut culist = worker_result
4295 .culist
4296 .take()
4297 .expect("parallel worker result missing CopperList ownership");
4298 let mut commit_ctx = cu29::context::CuContext::from_runtime_metadata(
4299 clock.clone(),
4300 worker_result.clid,
4301 instance_id,
4302 subsystem_code,
4303 #mission_mod::TASK_IDS,
4304 );
4305 commit_ctx.clear_current_component();
4306 commit_ctx.clear_current_task();
4307 let monitor_result = monitor.process_copperlist(
4308 &commit_ctx,
4309 #mission_mod::MONITOR_LAYOUT.view(&#mission_mod::collect_metadata(&culist)),
4310 );
4311
4312 #(#preprocess_logging_calls)*
4313
4314 match cl_manager.end_of_processing_boxed(culist)? {
4315 cu29::curuntime::OwnedCopperListSubmission::Recycled(culist) => {
4316 free_copperlists.push(culist);
4317 }
4318 cu29::curuntime::OwnedCopperListSubmission::Pending => {}
4319 }
4320 let keyframe_bytes = {
4321 let _keyframe_lock =
4322 kf_lock.lock().expect("parallel keyframe lock poisoned");
4323 let kf_manager = unsafe { kf_manager_ptr.as_mut() };
4324 kf_manager.end_of_processing(worker_result.clid)?;
4325 kf_manager.last_encoded_bytes
4326 };
4327 monitor_result?;
4328 let stats = cu29::monitoring::CopperListIoStats {
4329 raw_culist_bytes: core::mem::size_of::<CuList>() as u64
4330 + cl_manager.last_handle_bytes,
4331 handle_bytes: cl_manager.last_handle_bytes,
4332 encoded_culist_bytes: cl_manager.last_encoded_bytes,
4333 keyframe_bytes,
4334 structured_log_bytes_total: ::cu29::prelude::structured_log_bytes_total(),
4335 culistid: worker_result.clid,
4336 };
4337 monitor.observe_copperlist_io(stats);
4338
4339 }
4342 Err(error) => {
4343 shutdown.store(true, Ordering::Release);
4344 stop_launching = true;
4345 fatal_error = Some(error);
4346 if let Some(mut culist) = worker_result.culist.take() {
4347 culist.change_state(cu29::copperlist::CopperListState::Free);
4348 free_copperlists.push(culist);
4349 }
4350 }
4351 }
4352 } else if let Some(mut culist) = worker_result.culist.take() {
4353 culist.change_state(cu29::copperlist::CopperListState::Free);
4354 free_copperlists.push(culist);
4355 }
4356
4357 if active_keyframe_clid == Some(worker_result.clid) {
4358 active_keyframe_clid = None;
4359 }
4360 parallel_rt.release_commit(worker_result.clid + 1);
4361 next_commit_clid += 1;
4362 }
4363
4364 if STOP_FLAG.load(Ordering::SeqCst) {
4365 stop_launching = true;
4366 }
4367 }
4368
4369 drop(entry_stage_tx);
4370 free_copperlists.extend(cl_manager.finish_pending_boxed()?);
4371 if let Some(error) = fatal_error {
4372 Err(error)
4373 } else {
4374 Ok(())
4375 }
4376 });
4377
4378 if result.is_err() {
4379 error!("A task errored out: {}", &result);
4380 }
4381 <Self as #app_trait<S, L>>::stop_all_tasks(self, #sim_callback_arg)?;
4382 let _ = self.log_shutdown_completed();
4383 result
4384 }
4385 } else {
4386 quote! {
4387 static STOP_FLAG: AtomicBool = AtomicBool::new(false);
4388
4389 #kill_handler
4390
4391 <Self as #app_trait<S, L>>::start_all_tasks(self, #sim_callback_arg)?;
4392 let result = #run_loop;
4393
4394 if result.is_err() {
4395 error!("A task errored out: {}", &result);
4396 }
4397 <Self as #app_trait<S, L>>::stop_all_tasks(self, #sim_callback_arg)?;
4398 let _ = self.log_shutdown_completed();
4399 result
4400 }
4401 };
4402 let run_methods: proc_macro2::TokenStream = quote! {
4403
4404 #run_one_iteration {
4405
4406 let runtime = &mut self.copper_runtime;
4408 let clock_handle = runtime.clock();
4409 let clock = &clock_handle;
4410 let instance_id = runtime.instance_id();
4411 let subsystem_code = runtime.subsystem_code();
4412 let execution_probe = &runtime.execution_probe;
4413 let monitor = &mut runtime.monitor;
4414 let tasks = &mut runtime.tasks;
4415 let __cu_bridges = &mut runtime.bridges;
4416 let cl_manager = &mut runtime.copperlists_manager;
4417 let kf_manager = &mut runtime.keyframes_manager;
4418 let iteration_clid = cl_manager.next_cl_id();
4419 let mut ctx = cu29::context::CuContext::from_runtime_metadata(
4420 clock.clone(),
4421 iteration_clid,
4422 instance_id,
4423 subsystem_code,
4424 #mission_mod::TASK_IDS,
4425 );
4426 let mut __cu_abort_copperlist = false;
4427
4428 #(#preprocess_calls)*
4430
4431 let culist = cl_manager.create()?;
4432 let clid = culist.id;
4433 debug_assert_eq!(clid, iteration_clid);
4434 kf_manager.reset(clid, clock); culist.change_state(cu29::copperlist::CopperListState::Processing);
4436 let mut ctx = cu29::context::CuContext::from_runtime_metadata(
4437 clock.clone(),
4438 iteration_clid,
4439 instance_id,
4440 subsystem_code,
4441 #mission_mod::TASK_IDS,
4442 );
4443 {
4444 let msgs = &mut culist.msgs.0;
4445 '__cu_process_steps: {
4446 #(#runtime_plan_code)*
4447 }
4448 } if __cu_abort_copperlist {
4450 ctx.clear_current_component();
4451 ctx.clear_current_task();
4452 let monitor_result = monitor.process_copperlist(&ctx, #mission_mod::MONITOR_LAYOUT.view(&#mission_mod::collect_metadata(&culist)));
4453 cl_manager.end_of_processing(clid)?;
4454 monitor_result?;
4455 return Ok(());
4456 }
4457 ctx.clear_current_component();
4458 ctx.clear_current_task();
4459 let monitor_result = monitor.process_copperlist(&ctx, #mission_mod::MONITOR_LAYOUT.view(&#mission_mod::collect_metadata(&culist)));
4460
4461 #(#preprocess_logging_calls)*
4463
4464 cl_manager.end_of_processing(clid)?;
4465 kf_manager.end_of_processing(clid)?;
4466 monitor_result?;
4467 let stats = cu29::monitoring::CopperListIoStats {
4468 raw_culist_bytes: core::mem::size_of::<CuList>() as u64 + cl_manager.last_handle_bytes,
4469 handle_bytes: cl_manager.last_handle_bytes,
4470 encoded_culist_bytes: cl_manager.last_encoded_bytes,
4471 keyframe_bytes: kf_manager.last_encoded_bytes,
4472 structured_log_bytes_total: ::cu29::prelude::structured_log_bytes_total(),
4473 culistid: clid,
4474 };
4475 monitor.observe_copperlist_io(stats);
4476
4477 #(#postprocess_calls)*
4479 Ok(())
4480 }
4481
4482 fn restore_keyframe(&mut self, keyframe: &KeyFrame) -> CuResult<()> {
4483 let runtime = &mut self.copper_runtime;
4484 let clock_handle = runtime.clock();
4485 let clock = &clock_handle;
4486 let tasks = &mut runtime.tasks;
4487 let __cu_bridges = &mut runtime.bridges;
4488 let config = cu29::bincode::config::standard();
4489 let reader = cu29::bincode::de::read::SliceReader::new(&keyframe.serialized_tasks);
4490 let mut decoder = DecoderImpl::new(reader, config, ());
4491 #(#task_restore_code);*;
4492 #(#bridge_restore_code);*;
4493 Ok(())
4494 }
4495
4496 #start_all_tasks {
4497 let _ = self.log_runtime_lifecycle_event(RuntimeLifecycleEvent::MissionStarted {
4498 mission: #mission.to_string(),
4499 });
4500 let lifecycle_clid = self.copper_runtime.copperlists_manager.last_cl_id();
4501 let mut ctx = cu29::context::CuContext::from_runtime_metadata(
4502 self.copper_runtime.clock(),
4503 lifecycle_clid,
4504 self.copper_runtime.instance_id(),
4505 self.copper_runtime.subsystem_code(),
4506 #mission_mod::TASK_IDS,
4507 );
4508 #(#start_calls)*
4509 ctx.clear_current_component();
4510 ctx.clear_current_task();
4511 self.copper_runtime.monitor.start(&ctx)?;
4512 Ok(())
4513 }
4514
4515 #stop_all_tasks {
4516 let lifecycle_clid = self.copper_runtime.copperlists_manager.last_cl_id();
4517 let mut ctx = cu29::context::CuContext::from_runtime_metadata(
4518 self.copper_runtime.clock(),
4519 lifecycle_clid,
4520 self.copper_runtime.instance_id(),
4521 self.copper_runtime.subsystem_code(),
4522 #mission_mod::TASK_IDS,
4523 );
4524 #(#stop_calls)*
4525 ctx.clear_current_component();
4526 ctx.clear_current_task();
4527 self.copper_runtime.monitor.stop(&ctx)?;
4528 self.copper_runtime.copperlists_manager.finish_pending()?;
4529 let _ = self.log_runtime_lifecycle_event(RuntimeLifecycleEvent::MissionStopped {
4532 mission: #mission.to_string(),
4533 reason: "stop_all_tasks".to_string(),
4534 });
4535 Ok(())
4536 }
4537
4538 #run {
4539 #run_body
4540 }
4541 };
4542
4543 let tasks_type = if sim_mode {
4544 quote!(CuSimTasks)
4545 } else {
4546 quote!(CuTasks)
4547 };
4548
4549 let tasks_instanciator_fn = if sim_mode {
4550 quote!(tasks_instanciator_sim)
4551 } else {
4552 quote!(tasks_instanciator)
4553 };
4554
4555 let app_impl_decl = if sim_mode {
4556 quote!(impl<S: SectionStorage + 'static, L: UnifiedLogWrite<S> + 'static> CuSimApplication<S, L> for #application_name)
4557 } else {
4558 quote!(impl<S: SectionStorage + 'static, L: UnifiedLogWrite<S> + 'static> CuApplication<S, L> for #application_name)
4559 };
4560
4561 let simstep_type_decl = if sim_mode {
4562 quote!(
4563 type Step<'z> = SimStep<'z>;
4564 )
4565 } else {
4566 quote!()
4567 };
4568
4569 let mission_id_method = if sim_mode {
4570 quote! {
4571 fn mission_id() -> Option<&'static str> {
4572 Some(#mission)
4573 }
4574 }
4575 } else {
4576 quote!()
4577 };
4578
4579 let app_resources_thread_pools_field = if std {
4580 quote! { pub thread_pools: Vec<Option<Arc<ThreadPool>>>, }
4581 } else {
4582 quote!()
4583 };
4584
4585 let app_resources_struct = quote! {
4586 pub struct AppResources {
4587 pub config: CuConfig,
4588 pub config_source: RuntimeLifecycleConfigSource,
4589 pub resources: ResourceManager,
4590 #app_resources_thread_pools_field
4591 }
4592 };
4593
4594 let prepare_config_fn = quote! {
4595 #prepare_config_sig {
4596 let config_filename = #config_file;
4597
4598 #[cfg(target_os = "none")]
4599 ::cu29::prelude::info!("CuApp init: config file {}", config_filename);
4600 #[cfg(target_os = "none")]
4601 ::cu29::prelude::info!("CuApp init: loading config");
4602 #config_load_stmt
4603 #copperlist_count_check
4604 #[cfg(target_os = "none")]
4605 ::cu29::prelude::info!("CuApp init: config loaded");
4606 if let Some(runtime) = &config.runtime {
4607 #[cfg(target_os = "none")]
4608 ::cu29::prelude::info!(
4609 "CuApp init: rate_target_hz={}",
4610 runtime.rate_target_hz.unwrap_or(0)
4611 );
4612 } else {
4613 #[cfg(target_os = "none")]
4614 ::cu29::prelude::info!("CuApp init: rate_target_hz=none");
4615 }
4616
4617 Ok((config, config_source))
4618 }
4619 };
4620
4621 let prepare_resources_thread_pools_stmt = if std {
4622 quote! {
4623 let thread_pools = #mission_mod::thread_pools_instanciator(&config)?;
4624 }
4625 } else {
4626 quote!()
4627 };
4628 let prepare_resources_thread_pools_init = if std {
4629 quote! { thread_pools, }
4630 } else {
4631 quote!()
4632 };
4633
4634 let prepare_resources_fn = quote! {
4635 #prepare_resources_sig {
4636 let (config, config_source) = #prepare_config_call;
4637
4638 #[cfg(target_os = "none")]
4639 ::cu29::prelude::info!("CuApp init: building resources");
4640 let resources = #mission_mod::resources_instanciator(&config)?;
4641 #prepare_resources_thread_pools_stmt
4642 #[cfg(target_os = "none")]
4643 ::cu29::prelude::info!("CuApp init: resources ready");
4644
4645 Ok(AppResources {
4646 config,
4647 config_source,
4648 resources,
4649 #prepare_resources_thread_pools_init
4650 })
4651 }
4652 };
4653
4654 let new_with_resources_compat_fn = if sim_mode {
4655 quote! {
4656 pub fn new_with_resources<S: SectionStorage + 'static, L: UnifiedLogWrite<S> + 'static>(
4657 clock: RobotClock,
4658 unified_logger: Arc<Mutex<L>>,
4659 app_resources: AppResources,
4660 instance_id: u32,
4661 sim_callback: &mut impl FnMut(SimStep) -> SimOverride,
4662 ) -> CuResult<Self> {
4663 Self::build_with_resources(
4664 clock,
4665 unified_logger,
4666 app_resources,
4667 instance_id,
4668 sim_callback,
4669 )
4670 }
4671 }
4672 } else {
4673 quote! {
4674 pub fn new_with_resources<S: SectionStorage + 'static, L: UnifiedLogWrite<S> + 'static>(
4675 clock: RobotClock,
4676 unified_logger: Arc<Mutex<L>>,
4677 app_resources: AppResources,
4678 instance_id: u32,
4679 ) -> CuResult<Self> {
4680 Self::build_with_resources(clock, unified_logger, app_resources, instance_id)
4681 }
4682 }
4683 };
4684
4685 let build_with_resources_thread_pools_destructure = if std {
4686 quote! { thread_pools, }
4687 } else {
4688 quote!()
4689 };
4690 let build_with_resources_thread_pools_call = if std {
4691 quote! { .with_thread_pools(thread_pools) }
4692 } else {
4693 quote!()
4694 };
4695
4696 let build_with_resources_fn = quote! {
4697 #build_with_resources_sig {
4698 let AppResources {
4699 config,
4700 config_source,
4701 resources,
4702 #build_with_resources_thread_pools_destructure
4703 } = app_resources;
4704
4705 let structured_stream = ::cu29::prelude::stream_write::<
4706 ::cu29::prelude::CuLogEntry,
4707 S,
4708 >(
4709 unified_logger.clone(),
4710 ::cu29::prelude::UnifiedLogType::StructuredLogLine,
4711 4096 * 10,
4712 )?;
4713 let logger_runtime = ::cu29::prelude::LoggerRuntime::init(
4714 clock.clone(),
4715 structured_stream,
4716 None::<::cu29::prelude::NullLog>,
4717 );
4718
4719 let mut default_section_size = size_of::<super::#mission_mod::CuList>() * 64;
4722 if let Some(section_size_mib) = config.logging.as_ref().and_then(|l| l.section_size_mib) {
4724 default_section_size = section_size_mib as usize * 1024usize * 1024usize;
4726 }
4727 #[cfg(target_os = "none")]
4728 ::cu29::prelude::info!(
4729 "CuApp new: copperlist section size={}",
4730 default_section_size
4731 );
4732 #[cfg(target_os = "none")]
4733 ::cu29::prelude::info!("CuApp new: creating copperlist stream");
4734 let copperlist_stream = stream_write::<#mission_mod::CuList, S>(
4735 unified_logger.clone(),
4736 UnifiedLogType::CopperList,
4737 default_section_size,
4738 )?;
4742 #[cfg(target_os = "none")]
4743 ::cu29::prelude::info!("CuApp new: copperlist stream ready");
4744
4745 #[cfg(target_os = "none")]
4746 ::cu29::prelude::info!("CuApp new: creating keyframes stream");
4747 let keyframes_stream = stream_write::<KeyFrame, S>(
4748 unified_logger.clone(),
4749 UnifiedLogType::FrozenTasks,
4750 1024 * 1024 * 10, )?;
4752 #[cfg(target_os = "none")]
4753 ::cu29::prelude::info!("CuApp new: keyframes stream ready");
4754
4755 #[cfg(target_os = "none")]
4756 ::cu29::prelude::info!("CuApp new: creating runtime lifecycle stream");
4757 let mut runtime_lifecycle_stream = stream_write::<RuntimeLifecycleRecord, S>(
4758 unified_logger.clone(),
4759 UnifiedLogType::RuntimeLifecycle,
4760 1024 * 64, )?;
4762 let effective_config_ron = config
4763 .serialize_ron()
4764 .unwrap_or_else(|_| "<failed to serialize config>".to_string());
4765 ::cu29::logcodec::set_effective_config_ron::<super::#mission_mod::CuStampedDataSet>(&effective_config_ron);
4766 let stack_info = RuntimeLifecycleStackInfo {
4767 app_name: env!("CARGO_PKG_NAME").to_string(),
4768 app_version: env!("CARGO_PKG_VERSION").to_string(),
4769 git_commit: #git_commit_tokens,
4770 git_dirty: #git_dirty_tokens,
4771 subsystem_id: #application_name::subsystem().id().map(str::to_string),
4772 subsystem_code: #application_name::subsystem().code(),
4773 instance_id,
4774 };
4775 runtime_lifecycle_stream.log(&RuntimeLifecycleRecord {
4776 timestamp: clock.now(),
4777 event: RuntimeLifecycleEvent::Instantiated {
4778 config_source,
4779 effective_config_ron,
4780 stack: stack_info,
4781 },
4782 })?;
4783 #[cfg(target_os = "none")]
4784 ::cu29::prelude::info!("CuApp new: runtime lifecycle stream ready");
4785
4786 #[cfg(target_os = "none")]
4787 ::cu29::prelude::info!("CuApp new: building runtime");
4788 let copper_runtime = CuRuntimeBuilder::<#mission_mod::#tasks_type, #mission_mod::CuBridges, #mission_mod::CuStampedDataSet, #monitor_type, #copperlist_count_tokens, _, _, _, _, _>::new(
4789 clock,
4790 &config,
4791 #mission,
4792 CuRuntimeParts::new(
4793 #mission_mod::#tasks_instanciator_fn,
4794 #mission_mod::MONITORED_COMPONENTS,
4795 #mission_mod::CULIST_COMPONENT_MAPPING,
4796 #parallel_rt_metadata_arg
4797 #mission_mod::monitor_instanciator,
4798 #mission_mod::bridges_instanciator,
4799 ),
4800 copperlist_stream,
4801 keyframes_stream,
4802 )
4803 .with_subsystem(#application_name::subsystem())
4804 .with_instance_id(instance_id)
4805 .with_resources(resources)
4806 #build_with_resources_thread_pools_call
4807 .build()?;
4808 #[cfg(target_os = "none")]
4809 ::cu29::prelude::info!("CuApp new: runtime built");
4810
4811 let application = Ok(#application_name {
4812 copper_runtime,
4813 runtime_lifecycle_stream: Some(Box::new(runtime_lifecycle_stream)),
4814 logger_runtime,
4815 });
4816
4817 #sim_callback_on_new
4818
4819 application
4820 }
4821 };
4822
4823 let app_inherent_impl = quote! {
4824 impl #application_name {
4825 const SUBSYSTEM: cu29::prelude::app::Subsystem =
4826 cu29::prelude::app::Subsystem::new(#subsystem_id_tokens, #subsystem_code_literal);
4827
4828 #[inline]
4829 pub fn subsystem() -> cu29::prelude::app::Subsystem {
4830 Self::SUBSYSTEM
4831 }
4832
4833 pub fn original_config() -> String {
4834 #copper_config_content.to_string()
4835 }
4836
4837 pub fn register_reflect_types(registry: &mut cu29::reflect::TypeRegistry) {
4838 #(#task_debug_state_registration_calls)*
4839 #(#reflect_type_registration_calls)*
4840 }
4841
4842 #[inline]
4844 pub fn clock(&self) -> cu29::clock::RobotClock {
4845 self.copper_runtime.clock()
4846 }
4847
4848 pub fn log_runtime_lifecycle_event(
4850 &mut self,
4851 event: RuntimeLifecycleEvent,
4852 ) -> CuResult<()> {
4853 let timestamp = self.copper_runtime.clock_ref().now();
4854 let Some(stream) = self.runtime_lifecycle_stream.as_mut() else {
4855 return Err(CuError::from("Runtime lifecycle stream is not initialized"));
4856 };
4857 stream.log(&RuntimeLifecycleRecord { timestamp, event })
4858 }
4859
4860 pub fn log_shutdown_completed(&mut self) -> CuResult<()> {
4864 self.log_runtime_lifecycle_event(RuntimeLifecycleEvent::ShutdownCompleted)
4865 }
4866
4867 #prepare_config_fn
4868 #prepare_resources_compat_fn
4869 #prepare_resources_fn
4870 #init_resources_compat_fn
4871 #new_with_resources_compat_fn
4872 #build_with_resources_fn
4873
4874 #[inline]
4876 pub fn copper_runtime_mut(&mut self) -> &mut CuRuntime<#mission_mod::#tasks_type, #mission_mod::CuBridges, #mission_mod::CuStampedDataSet, #monitor_type, #copperlist_count_tokens> {
4877 &mut self.copper_runtime
4878 }
4879 }
4880 };
4881
4882 let app_metadata_impl = quote! {
4883 impl cu29::prelude::app::CuSubsystemMetadata for #application_name {
4884 fn subsystem() -> cu29::prelude::app::Subsystem {
4885 #application_name::subsystem()
4886 }
4887 }
4888 };
4889
4890 let app_reflect_impl = quote! {
4891 impl cu29::reflect::ReflectTaskIntrospection for #application_name {
4892 fn reflect_task(&self, task_id: &str) -> Option<&dyn cu29::reflect::Reflect> {
4893 match task_id {
4894 #(#task_reflect_read_arms)*
4895 _ => None,
4896 }
4897 }
4898
4899 fn reflect_task_mut(
4900 &mut self,
4901 task_id: &str,
4902 ) -> Option<&mut dyn cu29::reflect::Reflect> {
4903 match task_id {
4904 #(#task_reflect_write_arms)*
4905 _ => None,
4906 }
4907 }
4908
4909 fn register_reflect_types(registry: &mut cu29::reflect::TypeRegistry) {
4910 #application_name::register_reflect_types(registry);
4911 }
4912
4913 fn debug_state_type_path(task_id: &str) -> Option<&'static str> {
4914 match task_id {
4915 #(#task_debug_state_type_path_arms)*
4916 _ => None,
4917 }
4918 }
4919
4920 fn with_debug_state<R>(
4921 &self,
4922 task_id: &str,
4923 f: impl FnOnce(&dyn cu29::reflect::Reflect) -> R,
4924 ) -> Option<R> {
4925 match task_id {
4926 #(#task_debug_state_read_arms)*
4927 _ => None,
4928 }
4929 }
4930 }
4931 };
4932
4933 let app_runtime_copperlist_impl = quote! {
4934 impl cu29::app::CurrentRuntimeCopperList<#mission_mod::CuStampedDataSet>
4935 for #application_name
4936 {
4937 fn current_runtime_copperlist_bytes(&self) -> Option<&[u8]> {
4938 self.copper_runtime.copperlists_manager.last_completed_encoded()
4939 }
4940
4941 fn set_current_runtime_copperlist_bytes(
4942 &mut self,
4943 snapshot: Option<Vec<u8>>,
4944 ) {
4945 self.copper_runtime
4946 .copperlists_manager
4947 .set_last_completed_encoded(snapshot);
4948 }
4949 }
4950 };
4951
4952 #[cfg(feature = "std")]
4953 #[cfg(feature = "macro_debug")]
4954 eprintln!("[build result]");
4955 let application_impl = quote! {
4956 #app_impl_decl {
4957 #simstep_type_decl
4958
4959 fn get_original_config() -> String {
4960 Self::original_config()
4961 }
4962
4963 #mission_id_method
4964
4965 #run_methods
4966 }
4967 };
4968
4969 let recorded_replay_app_impl = if sim_mode {
4970 Some(quote! {
4971 impl<S: SectionStorage + 'static, L: UnifiedLogWrite<S> + 'static>
4972 CuRecordedReplayApplication<S, L> for #application_name
4973 {
4974 type RecordedDataSet = #mission_mod::CuStampedDataSet;
4975
4976 fn replay_recorded_copperlist(
4977 &mut self,
4978 clock_mock: &RobotClockMock,
4979 copperlist: &CopperList<Self::RecordedDataSet>,
4980 keyframe: Option<&KeyFrame>,
4981 ) -> CuResult<()> {
4982 if let Some(keyframe) = keyframe {
4983 if keyframe.culistid != copperlist.id {
4984 return Err(CuError::from(format!(
4985 "Recorded keyframe culistid {} does not match copperlist {}",
4986 keyframe.culistid, copperlist.id
4987 )));
4988 }
4989
4990 if !self.copper_runtime_mut().captures_keyframe(copperlist.id) {
4991 return Err(CuError::from(format!(
4992 "CopperList {} is not configured to capture a keyframe in this runtime",
4993 copperlist.id
4994 )));
4995 }
4996
4997 self.copper_runtime_mut()
4998 .set_forced_keyframe_timestamp(keyframe.timestamp);
4999 self.copper_runtime_mut().lock_keyframe(keyframe);
5000 clock_mock.set_value(keyframe.timestamp.as_nanos());
5001 } else {
5002 let timestamp =
5003 cu29::simulation::recorded_copperlist_timestamp(copperlist)
5004 .ok_or_else(|| {
5005 CuError::from(format!(
5006 "Recorded copperlist {} has no process_time.start timestamps",
5007 copperlist.id
5008 ))
5009 })?;
5010 clock_mock.set_value(timestamp.as_nanos());
5011 }
5012
5013 let mut sim_callback = |step: SimStep<'_>| -> SimOverride {
5014 #mission_mod::recorded_replay_step(step, copperlist)
5015 };
5016 <Self as CuSimApplication<S, L>>::run_one_iteration(self, &mut sim_callback)
5017 }
5018 }
5019 })
5020 } else {
5021 None
5022 };
5023
5024 let distributed_replay_app_impl = if sim_mode {
5025 Some(quote! {
5026 impl<S: SectionStorage + 'static, L: UnifiedLogWrite<S> + 'static>
5027 cu29::prelude::app::CuDistributedReplayApplication<S, L> for #application_name
5028 {
5029 fn build_distributed_replay(
5030 clock: cu29::clock::RobotClock,
5031 unified_logger: std::sync::Arc<std::sync::Mutex<L>>,
5032 instance_id: u32,
5033 config_override: Option<cu29::config::CuConfig>,
5034 ) -> CuResult<Self> {
5035 let mut noop =
5036 |_step: SimStep<'_>| cu29::simulation::SimOverride::ExecuteByRuntime;
5037 let builder = Self::builder()
5038 .with_logger::<S, L>(unified_logger)
5039 .with_clock(clock)
5040 .with_instance_id(instance_id);
5041 let builder = if let Some(config_override) = config_override {
5042 builder.with_config(config_override)
5043 } else {
5044 builder
5045 };
5046 builder.with_sim_callback(&mut noop).build()
5047 }
5048 }
5049 })
5050 } else {
5051 None
5052 };
5053
5054 let (builder_build_thread_pools_stmt, builder_build_thread_pools_init) = if std {
5055 (
5056 quote! {
5057 let thread_pools = #mission_mod::thread_pools_instanciator(&config)?;
5058 },
5059 quote! { thread_pools, },
5060 )
5061 } else {
5062 (quote!(), quote!())
5063 };
5064
5065 let builder_prepare_config_call = if std {
5066 quote! { #application_name::prepare_config(self.instance_id, self.config_override)? }
5067 } else {
5068 quote! {{
5069 let _ = self.config_override;
5070 #application_name::prepare_config()?
5071 }}
5072 };
5073
5074 let builder_with_config_method = if std {
5075 Some(quote! {
5076 #[allow(dead_code)]
5077 pub fn with_config(mut self, config_override: CuConfig) -> Self {
5078 self.config_override = Some(config_override);
5079 self
5080 }
5081 })
5082 } else {
5083 None
5084 };
5085
5086 let builder_default_clock = if std {
5087 quote! { Some(RobotClock::default()) }
5088 } else {
5089 quote! { None }
5090 };
5091
5092 let (
5093 builder_struct,
5094 builder_impl,
5095 builder_ctor,
5096 builder_log_path_generics,
5097 builder_sim_callback_method,
5098 builder_build_sim_callback_arg,
5099 ) = if sim_mode {
5100 (
5101 quote! {
5102 #[allow(dead_code)]
5103 pub struct #builder_name<'a, F, S, L, R>
5104 where
5105 S: SectionStorage + 'static,
5106 L: UnifiedLogWrite<S> + 'static,
5107 R: FnOnce(&CuConfig) -> CuResult<ResourceManager>,
5108 F: FnMut(SimStep) -> SimOverride,
5109 {
5110 clock: Option<RobotClock>,
5111 unified_logger: Arc<Mutex<L>>,
5112 instance_id: u32,
5113 config_override: Option<CuConfig>,
5114 resources_factory: R,
5115 sim_callback: Option<&'a mut F>,
5116 _storage: core::marker::PhantomData<S>,
5117 }
5118 },
5119 quote! {
5120 impl<'a, F, S, L, R> #builder_name<'a, F, S, L, R>
5121 where
5122 S: SectionStorage + 'static,
5123 L: UnifiedLogWrite<S> + 'static,
5124 R: FnOnce(&CuConfig) -> CuResult<ResourceManager>,
5125 F: FnMut(SimStep) -> SimOverride,
5126 },
5127 quote! {
5128 #[allow(dead_code)]
5129 pub fn builder<'a, F>() -> #builder_name<'a, F, cu29::prelude::NoopSectionStorage, cu29::prelude::NoopLogger, fn(&CuConfig) -> CuResult<ResourceManager>>
5130 where
5131 F: FnMut(SimStep) -> SimOverride,
5132 {
5133 #builder_name {
5134 clock: #builder_default_clock,
5135 unified_logger: Arc::new(Mutex::new(cu29::prelude::NoopLogger::new())),
5136 instance_id: 0,
5137 config_override: None,
5138 resources_factory: #mission_mod::resources_instanciator as fn(&CuConfig) -> CuResult<ResourceManager>,
5139 sim_callback: None,
5140 _storage: core::marker::PhantomData,
5141 }
5142 }
5143 },
5144 quote! {'a, F, MmapSectionStorage, UnifiedLoggerWrite, R},
5145 Some(quote! {
5146 #[allow(dead_code)]
5147 pub fn with_sim_callback(mut self, sim_callback: &'a mut F) -> Self {
5148 self.sim_callback = Some(sim_callback);
5149 self
5150 }
5151 }),
5152 Some(quote! {
5153 self.sim_callback
5154 .ok_or(CuError::from("Sim callback missing from builder"))?,
5155 }),
5156 )
5157 } else {
5158 (
5159 quote! {
5160 #[allow(dead_code)]
5161 pub struct #builder_name<S, L, R>
5162 where
5163 S: SectionStorage + 'static,
5164 L: UnifiedLogWrite<S> + 'static,
5165 R: FnOnce(&CuConfig) -> CuResult<ResourceManager>,
5166 {
5167 clock: Option<RobotClock>,
5168 unified_logger: Arc<Mutex<L>>,
5169 instance_id: u32,
5170 config_override: Option<CuConfig>,
5171 resources_factory: R,
5172 _storage: core::marker::PhantomData<S>,
5173 }
5174 },
5175 quote! {
5176 impl<S, L, R> #builder_name<S, L, R>
5177 where
5178 S: SectionStorage + 'static,
5179 L: UnifiedLogWrite<S> + 'static,
5180 R: FnOnce(&CuConfig) -> CuResult<ResourceManager>,
5181 },
5182 quote! {
5183 #[allow(dead_code)]
5184 pub fn builder() -> #builder_name<cu29::prelude::NoopSectionStorage, cu29::prelude::NoopLogger, fn(&CuConfig) -> CuResult<ResourceManager>> {
5185 #builder_name {
5186 clock: #builder_default_clock,
5187 unified_logger: Arc::new(Mutex::new(cu29::prelude::NoopLogger::new())),
5188 instance_id: 0,
5189 config_override: None,
5190 resources_factory: #mission_mod::resources_instanciator as fn(&CuConfig) -> CuResult<ResourceManager>,
5191 _storage: core::marker::PhantomData,
5192 }
5193 }
5194 },
5195 quote! {MmapSectionStorage, UnifiedLoggerWrite, R},
5196 None,
5197 None,
5198 )
5199 };
5200
5201 let builder_with_logger_generics = if sim_mode {
5202 quote! {'a, F, S2, L2, R}
5203 } else {
5204 quote! {S2, L2, R}
5205 };
5206
5207 let builder_with_resources_generics = if sim_mode {
5208 quote! {'a, F, S, L, R2}
5209 } else {
5210 quote! {S, L, R2}
5211 };
5212
5213 let builder_sim_callback_field_copy = if sim_mode {
5214 Some(quote! {
5215 sim_callback: self.sim_callback,
5216 })
5217 } else {
5218 None
5219 };
5220
5221 let builder_with_log_path_method = if std {
5222 Some(quote! {
5223 #[allow(dead_code)]
5224 pub fn with_log_path(
5225 self,
5226 path: impl AsRef<std::path::Path>,
5227 slab_size: Option<usize>,
5228 ) -> CuResult<#builder_name<#builder_log_path_generics>> {
5229 let preallocated_size = slab_size.unwrap_or(1024 * 1024 * 10);
5230 let logger = cu29::prelude::UnifiedLoggerBuilder::new()
5231 .write(true)
5232 .create(true)
5233 .file_base_name(path.as_ref())
5234 .preallocated_size(preallocated_size)
5235 .build()
5236 .map_err(|e| CuError::new_with_cause("Failed to create unified logger", e))?;
5237 let logger = match logger {
5238 cu29::prelude::UnifiedLogger::Write(logger) => logger,
5239 cu29::prelude::UnifiedLogger::Read(_) => {
5240 return Err(CuError::from(
5241 "UnifiedLoggerBuilder did not create a write-capable logger",
5242 ));
5243 }
5244 };
5245 Ok(self.with_logger::<MmapSectionStorage, UnifiedLoggerWrite>(Arc::new(Mutex::new(
5246 logger,
5247 ))))
5248 }
5249 })
5250 } else {
5251 None
5252 };
5253
5254 let builder_with_unified_logger_method = if std {
5255 Some(quote! {
5256 #[allow(dead_code)]
5257 pub fn with_unified_logger(
5258 self,
5259 unified_logger: Arc<Mutex<UnifiedLoggerWrite>>,
5260 ) -> #builder_name<#builder_log_path_generics> {
5261 self.with_logger::<MmapSectionStorage, UnifiedLoggerWrite>(unified_logger)
5262 }
5263 })
5264 } else {
5265 None
5266 };
5267
5268 let std_application_impl = if sim_mode {
5270 Some(quote! {
5272 impl #application_name {
5273 pub fn start_all_tasks(&mut self, sim_callback: &mut impl FnMut(SimStep) -> SimOverride) -> CuResult<()> {
5274 <Self as #app_trait<MmapSectionStorage, UnifiedLoggerWrite>>::start_all_tasks(self, sim_callback)
5275 }
5276 pub fn run_one_iteration(&mut self, sim_callback: &mut impl FnMut(SimStep) -> SimOverride) -> CuResult<()> {
5277 <Self as #app_trait<MmapSectionStorage, UnifiedLoggerWrite>>::run_one_iteration(self, sim_callback)
5278 }
5279 pub fn run(&mut self, sim_callback: &mut impl FnMut(SimStep) -> SimOverride) -> CuResult<()> {
5280 <Self as #app_trait<MmapSectionStorage, UnifiedLoggerWrite>>::run(self, sim_callback)
5281 }
5282 pub fn stop_all_tasks(&mut self, sim_callback: &mut impl FnMut(SimStep) -> SimOverride) -> CuResult<()> {
5283 <Self as #app_trait<MmapSectionStorage, UnifiedLoggerWrite>>::stop_all_tasks(self, sim_callback)
5284 }
5285 pub fn replay_recorded_copperlist(
5286 &mut self,
5287 clock_mock: &RobotClockMock,
5288 copperlist: &CopperList<CuStampedDataSet>,
5289 keyframe: Option<&KeyFrame>,
5290 ) -> CuResult<()> {
5291 <Self as CuRecordedReplayApplication<MmapSectionStorage, UnifiedLoggerWrite>>::replay_recorded_copperlist(
5292 self,
5293 clock_mock,
5294 copperlist,
5295 keyframe,
5296 )
5297 }
5298 }
5299 })
5300 } else if std {
5301 Some(quote! {
5303 impl #application_name {
5304 pub fn start_all_tasks(&mut self) -> CuResult<()> {
5305 <Self as #app_trait<MmapSectionStorage, UnifiedLoggerWrite>>::start_all_tasks(self)
5306 }
5307 pub fn run_one_iteration(&mut self) -> CuResult<()> {
5308 <Self as #app_trait<MmapSectionStorage, UnifiedLoggerWrite>>::run_one_iteration(self)
5309 }
5310 pub fn run(&mut self) -> CuResult<()> {
5311 <Self as #app_trait<MmapSectionStorage, UnifiedLoggerWrite>>::run(self)
5312 }
5313 pub fn stop_all_tasks(&mut self) -> CuResult<()> {
5314 <Self as #app_trait<MmapSectionStorage, UnifiedLoggerWrite>>::stop_all_tasks(self)
5315 }
5316 }
5317 })
5318 } else {
5319 None };
5321
5322 let application_builder = Some(quote! {
5323 #builder_struct
5324
5325 #builder_impl
5326 {
5327 #[allow(dead_code)]
5328 pub fn with_clock(mut self, clock: RobotClock) -> Self {
5329 self.clock = Some(clock);
5330 self
5331 }
5332
5333 #[allow(dead_code)]
5334 pub fn with_logger<S2, L2>(
5335 self,
5336 unified_logger: Arc<Mutex<L2>>,
5337 ) -> #builder_name<#builder_with_logger_generics>
5338 where
5339 S2: SectionStorage + 'static,
5340 L2: UnifiedLogWrite<S2> + 'static,
5341 {
5342 #builder_name {
5343 clock: self.clock,
5344 unified_logger,
5345 instance_id: self.instance_id,
5346 config_override: self.config_override,
5347 resources_factory: self.resources_factory,
5348 #builder_sim_callback_field_copy
5349 _storage: core::marker::PhantomData,
5350 }
5351 }
5352
5353 #builder_with_unified_logger_method
5354
5355 #[allow(dead_code)]
5356 pub fn with_instance_id(mut self, instance_id: u32) -> Self {
5357 self.instance_id = instance_id;
5358 self
5359 }
5360
5361 pub fn with_resources<R2>(self, resources_factory: R2) -> #builder_name<#builder_with_resources_generics>
5362 where
5363 R2: FnOnce(&CuConfig) -> CuResult<ResourceManager>,
5364 {
5365 #builder_name {
5366 clock: self.clock,
5367 unified_logger: self.unified_logger,
5368 instance_id: self.instance_id,
5369 config_override: self.config_override,
5370 resources_factory,
5371 #builder_sim_callback_field_copy
5372 _storage: core::marker::PhantomData,
5373 }
5374 }
5375
5376 #builder_with_config_method
5377 #builder_with_log_path_method
5378 #builder_sim_callback_method
5379
5380 #[allow(dead_code)]
5381 pub fn build(self) -> CuResult<#application_name> {
5382 let clock = self
5383 .clock
5384 .ok_or(CuError::from("Clock missing from builder"))?;
5385 let (config, config_source) = #builder_prepare_config_call;
5386 let resources = (self.resources_factory)(&config)?;
5387 #builder_build_thread_pools_stmt
5388 let app_resources = AppResources {
5389 config,
5390 config_source,
5391 resources,
5392 #builder_build_thread_pools_init
5393 };
5394 #application_name::build_with_resources(
5395 clock,
5396 self.unified_logger,
5397 app_resources,
5398 self.instance_id,
5399 #builder_build_sim_callback_arg
5400 )
5401 }
5402 }
5403 });
5404
5405 let app_builder_inherent_impl = quote! {
5406 impl #application_name {
5407 #builder_ctor
5408 }
5409 };
5410
5411 let sim_imports = if sim_mode {
5412 Some(quote! {
5413 use cu29::simulation::SimOverride;
5414 use cu29::simulation::CuTaskCallbackState;
5415 use cu29::simulation::CuSimSrcTask;
5416 use cu29::simulation::CuSimSinkTask;
5417 use cu29::simulation::CuSimBridge;
5418 use cu29::prelude::app::CuSimApplication;
5419 use cu29::prelude::app::CuRecordedReplayApplication;
5420 use cu29::cubridge::BridgeChannelSet;
5421 })
5422 } else {
5423 None
5424 };
5425
5426 let sim_tasks = if sim_mode {
5427 Some(quote! {
5428 pub type CuSimTasks = #task_types_tuple_sim;
5431 })
5432 } else {
5433 None
5434 };
5435
5436 let sim_inst_body = if task_sim_instances_init_code.is_empty() {
5437 quote! {
5438 let _ = (resources, thread_pools);
5439 Ok(())
5440 }
5441 } else {
5442 quote! { Ok(( #(#task_sim_instances_init_code),*, )) }
5443 };
5444
5445 let sim_tasks_instanciator = if sim_mode {
5446 Some(quote! {
5447 pub fn tasks_instanciator_sim<'c>(
5448 all_instances_configs: Vec<Option<&'c ComponentConfig>>,
5449 resources: &mut ResourceManager,
5450 thread_pools: &[Option<Arc<ThreadPool>>],
5451 ) -> CuResult<CuSimTasks> {
5452 #sim_inst_body
5453 }})
5454 } else {
5455 None
5456 };
5457
5458 let tasks_inst_body_std = if task_instances_init_code.is_empty() {
5459 quote! {
5460 let _ = (resources, thread_pools);
5461 Ok(())
5462 }
5463 } else {
5464 quote! { Ok(( #(#task_instances_init_code),*, )) }
5465 };
5466
5467 let tasks_inst_body_nostd = if task_instances_init_code.is_empty() {
5468 quote! {
5469 let _ = resources;
5470 Ok(())
5471 }
5472 } else {
5473 quote! { Ok(( #(#task_instances_init_code),*, )) }
5474 };
5475
5476 let tasks_instanciator = if std {
5477 quote! {
5478 pub fn tasks_instanciator<'c>(
5479 all_instances_configs: Vec<Option<&'c ComponentConfig>>,
5480 resources: &mut ResourceManager,
5481 thread_pools: &[Option<Arc<ThreadPool>>],
5482 ) -> CuResult<CuTasks> {
5483 #tasks_inst_body_std
5484 }
5485 }
5486 } else {
5487 quote! {
5489 pub fn tasks_instanciator<'c>(
5490 all_instances_configs: Vec<Option<&'c ComponentConfig>>,
5491 resources: &mut ResourceManager,
5492 ) -> CuResult<CuTasks> {
5493 #tasks_inst_body_nostd
5494 }
5495 }
5496 };
5497
5498 let thread_pools_instanciator = if std {
5503 quote! {
5504 pub fn thread_pools_instanciator(
5505 config: &CuConfig,
5506 ) -> CuResult<Vec<Option<Arc<ThreadPool>>>> {
5507 let Some(runtime) = config.runtime.as_ref() else {
5508 return Ok(Vec::new());
5509 };
5510 let mut pools: Vec<Option<Arc<ThreadPool>>> =
5511 Vec::with_capacity(runtime.thread_pools.len());
5512 for pool_spec in &runtime.thread_pools {
5513 if pool_spec.id == cu29::config::RT_POOL {
5514 pools.push(None);
5515 continue;
5516 }
5517 let pool = cu29::thread_pool::build_pool(pool_spec)?;
5518 pools.push(Some(Arc::new(pool)));
5519 }
5520 Ok(pools)
5521 }
5522 }
5523 } else {
5524 quote! {}
5525 };
5526
5527 let imports = if std {
5528 quote! {
5529 use cu29::rayon::ThreadPool;
5530 use cu29::cuasynctask::CuAsyncSrcTask;
5531 use cu29::cuasynctask::CuAsyncTask;
5532 use cu29::resource::{ResourceBindings, ResourceManager};
5533 use cu29::prelude::SectionStorage;
5534 use cu29::prelude::UnifiedLoggerWrite;
5535 use cu29::prelude::memmap::MmapSectionStorage;
5536 use cu29::__private::sync::{Arc, Mutex};
5537 use std::fmt::{Debug, Formatter};
5538 use std::fmt::Result as FmtResult;
5539 use std::mem::size_of;
5540 use std::boxed::Box;
5541 use std::sync::atomic::{AtomicBool, Ordering};
5542 }
5543 } else {
5544 quote! {
5545 use alloc::boxed::Box;
5546 use alloc::string::String;
5547 use alloc::string::ToString;
5548 use cu29::__private::sync::{Arc, Mutex};
5549 use core::sync::atomic::{AtomicBool, Ordering};
5550 use core::fmt::{Debug, Formatter};
5551 use core::fmt::Result as FmtResult;
5552 use core::mem::size_of;
5553 use cu29::prelude::SectionStorage;
5554 use cu29::resource::{ResourceBindings, ResourceManager};
5555 }
5556 };
5557
5558 let task_mapping_defs = task_resource_mappings.defs.clone();
5559 let bridge_mapping_defs = bridge_resource_mappings.defs.clone();
5560
5561 let mission_mod_tokens = quote! {
5563 mod #mission_mod {
5564 use super::*; use cu29::bincode::Encode;
5567 use cu29::bincode::enc::Encoder;
5568 use cu29::bincode::error::EncodeError;
5569 use cu29::bincode::Decode;
5570 use cu29::bincode::de::Decoder;
5571 use cu29::bincode::de::DecoderImpl;
5572 use cu29::bincode::error::DecodeError;
5573 use cu29::clock::RobotClock;
5574 use cu29::clock::RobotClockMock;
5575 use cu29::config::CuConfig;
5576 use cu29::config::ComponentConfig;
5577 use cu29::curuntime::CuRuntime;
5578 use cu29::curuntime::CuRuntimeBuilder;
5579 use cu29::curuntime::CuRuntimeParts;
5580 use cu29::curuntime::KeyFrame;
5581 use cu29::curuntime::RuntimeLifecycleConfigSource;
5582 use cu29::curuntime::RuntimeLifecycleEvent;
5583 use cu29::curuntime::RuntimeLifecycleRecord;
5584 use cu29::curuntime::RuntimeLifecycleStackInfo;
5585 use cu29::CuResult;
5586 use cu29::CuError;
5587 use cu29::cutask::CuSrcTask;
5588 use cu29::cutask::CuSinkTask;
5589 use cu29::cutask::CuTask;
5590 use cu29::cutask::CuMsg;
5591 use cu29::cutask::CuMsgMetadata;
5592 use cu29::copperlist::CopperList;
5593 use cu29::monitoring::CuMonitor; use cu29::monitoring::CuComponentState;
5595 use cu29::monitoring::Decision;
5596 use cu29::prelude::app::CuApplication;
5597 use cu29::prelude::debug;
5598 use cu29::prelude::stream_write;
5599 use cu29::prelude::UnifiedLogType;
5600 use cu29::prelude::UnifiedLogWrite;
5601 use cu29::prelude::WriteStream;
5602
5603 #imports
5604
5605 #sim_imports
5606
5607 #[allow(unused_imports)]
5609 use cu29::monitoring::NoMonitor;
5610
5611 pub type CuTasks = #task_types_tuple;
5615 pub type CuBridges = #bridges_type_tokens;
5616 #sim_bridge_channel_defs
5617 #resources_module
5618 #resources_instanciator_fn
5619 #task_mapping_defs
5620 #bridge_mapping_defs
5621 #(#autogenerated_output_warnings)*
5622
5623 #sim_tasks
5624 #sim_support
5625 #recorded_replay_support
5626 #sim_tasks_instanciator
5627
5628 pub const TASK_IDS: &'static [&'static str] = &[#( #task_ids ),*];
5629 pub const MONITORED_COMPONENTS: &'static [cu29::monitoring::MonitorComponentMetadata] =
5630 &[#( #monitored_component_entries ),*];
5631 pub const CULIST_COMPONENT_MAPPING: &'static [cu29::monitoring::ComponentId] =
5632 &[#( cu29::monitoring::ComponentId::new(#culist_component_mapping) ),*];
5633 pub const MONITOR_LAYOUT: cu29::monitoring::CopperListLayout =
5634 cu29::monitoring::CopperListLayout::new(
5635 MONITORED_COMPONENTS,
5636 CULIST_COMPONENT_MAPPING,
5637 );
5638 #parallel_rt_metadata_defs
5639
5640 #[inline]
5641 pub fn monitor_component_label(
5642 component_id: cu29::monitoring::ComponentId,
5643 ) -> &'static str {
5644 MONITORED_COMPONENTS[component_id.index()].id()
5645 }
5646
5647 #culist_support
5648 #parallel_rt_support_tokens
5649
5650 #tasks_instanciator
5651 #thread_pools_instanciator
5652 #bridges_instanciator
5653
5654 pub fn monitor_instanciator(
5655 config: &CuConfig,
5656 metadata: ::cu29::monitoring::CuMonitoringMetadata,
5657 runtime: ::cu29::monitoring::CuMonitoringRuntime,
5658 ) -> #monitor_type {
5659 #monitor_instanciator_body
5660 }
5661
5662 #app_resources_struct
5664 pub #application_struct
5665
5666 #app_inherent_impl
5667 #app_builder_inherent_impl
5668 #app_metadata_impl
5669 #app_reflect_impl
5670 #app_runtime_copperlist_impl
5671 #application_impl
5672 #recorded_replay_app_impl
5673 #distributed_replay_app_impl
5674
5675 #std_application_impl
5676
5677 #application_builder
5678 }
5679
5680 };
5681 all_missions_tokens.push(mission_mod_tokens);
5682 }
5683
5684 let default_application_tokens = if all_missions
5685 .iter()
5686 .any(|(mission_name, _)| mission_name == "default")
5687 {
5688 let default_builder = quote! {
5689 #[allow(unused_imports)]
5690 use default::#builder_name;
5691 };
5692 quote! {
5693 #default_builder
5694
5695 #[allow(unused_imports)]
5696 use default::AppResources;
5697
5698 #[allow(unused_imports)]
5699 use default::resources as app_resources;
5700
5701 #[allow(unused_imports)]
5702 use default::#application_name;
5703 }
5704 } else {
5705 quote!() };
5707
5708 let result: proc_macro2::TokenStream = quote! {
5709 #(#all_missions_tokens)*
5710 #default_application_tokens
5711 };
5712
5713 result.into()
5714}
5715
5716fn resolve_runtime_config(args: &CopperRuntimeArgs) -> CuResult<ResolvedRuntimeConfig> {
5717 let caller_root = utils::caller_crate_root();
5718 resolve_runtime_config_with_root(args, &caller_root)
5719}
5720
5721fn resolve_runtime_config_with_root(
5722 args: &CopperRuntimeArgs,
5723 caller_root: &Path,
5724) -> CuResult<ResolvedRuntimeConfig> {
5725 let filename = config_full_path_from_root(caller_root, &args.config_path);
5726 if !Path::new(&filename).exists() {
5727 return Err(CuError::from(format!(
5728 "The configuration file `{}` does not exist. Please provide a valid path.",
5729 args.config_path
5730 )));
5731 }
5732
5733 if let Some(subsystem_id) = args.subsystem_id.as_deref() {
5734 let multi_config = cu29_runtime::config::read_multi_configuration(filename.as_str())
5735 .map_err(|e| {
5736 CuError::from(format!(
5737 "When `subsystem = \"{subsystem_id}\"` is provided, `config = \"{}\"` must point to a valid multi-Copper configuration: {e}",
5738 args.config_path
5739 ))
5740 })?;
5741 let subsystem = multi_config.subsystem(subsystem_id).ok_or_else(|| {
5742 CuError::from(format!(
5743 "Subsystem '{subsystem_id}' was not found in multi-Copper configuration '{}'.",
5744 args.config_path
5745 ))
5746 })?;
5747 let bundled_local_config_content = read_to_string(&subsystem.config_path).map_err(|e| {
5748 CuError::from(format!(
5749 "Failed to read bundled local configuration for subsystem '{subsystem_id}' from '{}'.",
5750 subsystem.config_path
5751 ))
5752 .add_cause(e.to_string().as_str())
5753 })?;
5754
5755 Ok(ResolvedRuntimeConfig {
5756 local_config: subsystem.config.clone(),
5757 bundled_local_config_content,
5758 subsystem_id: Some(subsystem_id.to_string()),
5759 subsystem_code: subsystem.subsystem_code,
5760 })
5761 } else {
5762 Ok(ResolvedRuntimeConfig {
5763 local_config: read_configuration(filename.as_str())?,
5764 bundled_local_config_content: read_to_string(&filename).map_err(|e| {
5765 CuError::from(format!(
5766 "Could not read the configuration file '{}'.",
5767 args.config_path
5768 ))
5769 .add_cause(e.to_string().as_str())
5770 })?,
5771 subsystem_id: None,
5772 subsystem_code: 0,
5773 })
5774 }
5775}
5776
5777fn build_config_load_stmt(
5778 std_enabled: bool,
5779 application_name: &Ident,
5780 subsystem_id: Option<&str>,
5781) -> proc_macro2::TokenStream {
5782 if std_enabled {
5783 if let Some(subsystem_id) = subsystem_id {
5784 quote! {
5785 let (config, config_source) = if let Some(overridden_config) = config_override {
5786 debug!("CuConfig: Overridden programmatically.");
5787 (overridden_config, RuntimeLifecycleConfigSource::ProgrammaticOverride)
5788 } else if ::std::path::Path::new(config_filename).exists() {
5789 let subsystem_id = #application_name::subsystem()
5790 .id()
5791 .expect("generated multi-Copper runtime is missing a subsystem id");
5792 debug!(
5793 "CuConfig: Reading multi-Copper configuration from file: {} (subsystem={})",
5794 config_filename,
5795 subsystem_id
5796 );
5797 let multi_config = cu29::config::read_multi_configuration(config_filename)?;
5798 (
5799 multi_config.resolve_subsystem_config_for_instance(subsystem_id, instance_id)?,
5800 RuntimeLifecycleConfigSource::ExternalFile,
5801 )
5802 } else {
5803 let original_config = Self::original_config();
5804 debug!(
5805 "CuConfig: Using the bundled subsystem configuration compiled into the binary (subsystem={}).",
5806 #subsystem_id
5807 );
5808 if instance_id != 0 {
5809 debug!(
5810 "CuConfig: runtime file '{}' is missing, so instance-specific overrides for instance_id={} cannot be resolved; using bundled subsystem defaults.",
5811 config_filename,
5812 instance_id
5813 );
5814 }
5815 (
5816 cu29::config::read_configuration_str(original_config, None)?,
5817 RuntimeLifecycleConfigSource::BundledDefault,
5818 )
5819 };
5820 }
5821 } else {
5822 quote! {
5823 let _ = instance_id;
5824 let (config, config_source) = if let Some(overridden_config) = config_override {
5825 debug!("CuConfig: Overridden programmatically.");
5826 (overridden_config, RuntimeLifecycleConfigSource::ProgrammaticOverride)
5827 } else if ::std::path::Path::new(config_filename).exists() {
5828 debug!("CuConfig: Reading configuration from file: {}", config_filename);
5829 (
5830 cu29::config::read_configuration(config_filename)?,
5831 RuntimeLifecycleConfigSource::ExternalFile,
5832 )
5833 } else {
5834 let original_config = Self::original_config();
5835 debug!("CuConfig: Using the bundled configuration compiled into the binary.");
5836 (
5837 cu29::config::read_configuration_str(original_config, None)?,
5838 RuntimeLifecycleConfigSource::BundledDefault,
5839 )
5840 };
5841 }
5842 }
5843 } else {
5844 quote! {
5845 let original_config = Self::original_config();
5847 debug!("CuConfig: Using the bundled configuration compiled into the binary.");
5848 let config = cu29::config::read_configuration_str(original_config, None)?;
5849 let config_source = RuntimeLifecycleConfigSource::BundledDefault;
5850 }
5851 }
5852}
5853
5854fn config_full_path(config_file: &str) -> String {
5855 config_full_path_from_root(&utils::caller_crate_root(), config_file)
5856}
5857
5858fn config_full_path_from_root(caller_root: &Path, config_file: &str) -> String {
5859 let mut config_full_path = caller_root.to_path_buf();
5860 config_full_path.push(config_file);
5861 let filename = config_full_path
5862 .as_os_str()
5863 .to_str()
5864 .expect("Could not interpret the config file name");
5865 filename.to_string()
5866}
5867
5868fn read_config(config_file: &str) -> CuResult<CuConfig> {
5869 let filename = config_full_path(config_file);
5870 read_configuration(filename.as_str())
5871}
5872
5873fn inferred_single_output_payload_type(task_type: &Type, task_kind: CuTaskType) -> Type {
5874 match task_kind {
5875 CuTaskType::Source => parse_quote! {
5876 <<#task_type as cu29::cutask::CuSrcTask>::Output<'static> as cu29::cutask::CuSingleOutputMsg>::Payload
5877 },
5878 CuTaskType::Regular => parse_quote! {
5879 <<#task_type as cu29::cutask::CuTask>::Output<'static> as cu29::cutask::CuSingleOutputMsg>::Payload
5880 },
5881 CuTaskType::Sink => panic!("Sinks do not have output payload types"),
5882 }
5883}
5884
5885fn task_trait_for_kind(task_kind: CuTaskType) -> proc_macro2::TokenStream {
5886 match task_kind {
5887 CuTaskType::Source => quote! { cu29::cutask::CuSrcTask },
5888 CuTaskType::Regular => quote! { cu29::cutask::CuTask },
5889 CuTaskType::Sink => quote! { cu29::cutask::CuSinkTask },
5890 }
5891}
5892
5893fn task_output_payload_type(
5894 graph: &CuGraph,
5895 node: &Node,
5896 task_kind: CuTaskType,
5897 task_type: &Type,
5898) -> Option<Type> {
5899 if task_kind == CuTaskType::Sink {
5900 return None;
5901 }
5902
5903 let id = node.get_id();
5904 if let Some(type_str) = graph.get_node_output_msg_type(id.as_str()) {
5905 return Some(
5906 parse_str::<Type>(type_str.as_str()).expect("Could not parse output message type."),
5907 );
5908 }
5909
5910 node.get_declared_task_kind()
5911 .map(|_| inferred_single_output_payload_type(task_type, task_kind))
5912}
5913
5914fn synthesized_single_output_msg_name(task_type: &Type, task_kind: CuTaskType) -> String {
5915 inferred_single_output_payload_type(task_type, task_kind)
5916 .to_token_stream()
5917 .to_string()
5918}
5919
5920struct CuTaskSpecSet {
5921 pub ids: Vec<String>,
5922 pub cutypes: Vec<CuTaskType>,
5923 pub background_flags: Vec<bool>,
5924 pub background_pools: Vec<String>,
5927 pub logging_enabled: Vec<bool>,
5928 pub type_names: Vec<String>,
5929 pub task_types: Vec<Type>,
5930 pub instantiation_types: Vec<Type>,
5931 pub sim_task_types: Vec<Type>,
5932 pub run_in_sim_flags: Vec<bool>,
5933 #[allow(dead_code)]
5934 pub output_types: Vec<Option<Type>>,
5935 pub autogenerated_output_flags: Vec<bool>,
5936 pub node_id_to_task_index: Vec<Option<usize>>,
5937}
5938
5939impl CuTaskSpecSet {
5940 pub fn from_graph(graph: &CuGraph) -> CuResult<Self> {
5941 let all_id_nodes: Vec<(NodeId, &Node)> = graph
5942 .get_all_nodes()
5943 .into_iter()
5944 .filter(|(_, node)| node.get_flavor() == Flavor::Task)
5945 .collect();
5946
5947 let ids = all_id_nodes
5948 .iter()
5949 .map(|(_, node)| node.get_id().to_string())
5950 .collect();
5951
5952 let cutypes: Vec<CuTaskType> = all_id_nodes
5953 .iter()
5954 .map(|(id, _)| find_task_type_for_id(graph, *id))
5955 .collect::<CuResult<Vec<_>>>()?;
5956
5957 let background_flags: Vec<bool> = all_id_nodes
5958 .iter()
5959 .map(|(_, node)| node.is_background())
5960 .collect();
5961
5962 let background_pools: Vec<String> = all_id_nodes
5963 .iter()
5964 .map(|(_, node)| node.background_pool().to_string())
5965 .collect();
5966
5967 let logging_enabled: Vec<bool> = all_id_nodes
5968 .iter()
5969 .map(|(_, node)| node.is_logging_enabled())
5970 .collect();
5971
5972 let type_names: Vec<String> = all_id_nodes
5973 .iter()
5974 .map(|(_, node)| node.get_type().to_string())
5975 .collect();
5976
5977 let parsed_task_types: Vec<Type> = type_names
5978 .iter()
5979 .map(|name| {
5980 parse_str::<Type>(name).unwrap_or_else(|error| {
5981 panic!("Could not transform {name} into a Task Rust type: {error}");
5982 })
5983 })
5984 .collect();
5985
5986 let output_types: Vec<Option<Type>> = all_id_nodes
5987 .iter()
5988 .zip(cutypes.iter())
5989 .zip(parsed_task_types.iter())
5990 .map(|(((_, node), &task_kind), task_type)| {
5991 task_output_payload_type(graph, node, task_kind, task_type)
5992 })
5993 .collect();
5994
5995 let autogenerated_output_flags: Vec<bool> = all_id_nodes
5996 .iter()
5997 .zip(cutypes.iter())
5998 .map(|((node_id, node), &task_kind)| {
5999 task_kind != CuTaskType::Sink
6000 && node.get_declared_task_kind().is_some()
6001 && graph
6002 .get_node_output_msg_types_by_id(*node_id)
6003 .expect("missing output type lookup")
6004 .is_empty()
6005 })
6006 .collect();
6007
6008 let task_types = parsed_task_types
6009 .iter()
6010 .zip(type_names.iter())
6011 .zip(cutypes.iter())
6012 .zip(background_flags.iter())
6013 .zip(output_types.iter())
6014 .map(|((((name_type, name), cutype), &background), output_type)| {
6015 if background {
6016 if let Some(output_type) = output_type {
6017 match cutype {
6018 CuTaskType::Source => {
6019 parse_quote!(CuAsyncSrcTask<#name_type, #output_type>)
6020 }
6021 CuTaskType::Regular => {
6022 parse_quote!(CuAsyncTask<#name_type, #output_type>)
6023 }
6024 CuTaskType::Sink => {
6025 panic!("CuSinkTask {name} cannot be a background task, it should be a regular task.");
6026 }
6027 }
6028 } else {
6029 panic!(
6030 "{}: If a task is background, it has to have an output",
6031 name_type.to_token_stream()
6032 );
6033 }
6034 } else {
6035 name_type.clone()
6036 }
6037 })
6038 .collect();
6039
6040 let instantiation_types = parsed_task_types
6041 .iter()
6042 .zip(type_names.iter())
6043 .zip(cutypes.iter())
6044 .zip(background_flags.iter())
6045 .zip(output_types.iter())
6046 .map(|((((name_type, name), cutype), &background), output_type)| {
6047 if background {
6048 if let Some(output_type) = output_type {
6049 match cutype {
6050 CuTaskType::Source => {
6051 parse_quote!(CuAsyncSrcTask::<#name_type, #output_type>)
6052 }
6053 CuTaskType::Regular => {
6054 parse_quote!(CuAsyncTask::<#name_type, #output_type>)
6055 }
6056 CuTaskType::Sink => {
6057 panic!("CuSinkTask {name} cannot be a background task, it should be a regular task.");
6058 }
6059 }
6060 } else {
6061 panic!(
6062 "{}: If a task is background, it has to have an output",
6063 name_type.to_token_stream()
6064 );
6065 }
6066 } else {
6067 name_type.clone()
6068 }
6069 })
6070 .collect();
6071
6072 let sim_task_types = parsed_task_types;
6073
6074 let run_in_sim_flags = all_id_nodes
6075 .iter()
6076 .map(|(_, node)| node.is_run_in_sim())
6077 .collect();
6078
6079 let mut node_id_to_task_index = vec![None; graph.node_count()];
6080 for (index, (node_id, _)) in all_id_nodes.iter().enumerate() {
6081 node_id_to_task_index[*node_id as usize] = Some(index);
6082 }
6083
6084 Ok(Self {
6085 ids,
6086 cutypes,
6087 background_flags,
6088 background_pools,
6089 logging_enabled,
6090 type_names,
6091 task_types,
6092 instantiation_types,
6093 sim_task_types,
6094 run_in_sim_flags,
6095 output_types,
6096 autogenerated_output_flags,
6097 node_id_to_task_index,
6098 })
6099 }
6100}
6101
6102#[derive(Clone)]
6103struct OutputPack {
6104 msg_types: Vec<Type>,
6105 msg_type_names: Vec<String>,
6106}
6107
6108impl OutputPack {
6109 fn slot_type(&self) -> Type {
6110 build_output_slot_type(&self.msg_types)
6111 }
6112
6113 fn is_multi(&self) -> bool {
6114 self.msg_types.len() > 1
6115 }
6116}
6117
6118fn build_output_slot_type(msg_types: &[Type]) -> Type {
6119 if msg_types.is_empty() {
6120 parse_quote! { () }
6121 } else if msg_types.len() == 1 {
6122 let msg_type = msg_types.first().unwrap();
6123 parse_quote! { CuMsg<#msg_type> }
6124 } else {
6125 parse_quote! { ( #( CuMsg<#msg_types> ),* ) }
6126 }
6127}
6128
6129fn flatten_slot_origin_ids(
6130 output_packs: &[OutputPack],
6131 slot_origin_ids: &[Option<String>],
6132) -> Vec<String> {
6133 let mut ids = Vec::new();
6134 for (slot, pack) in output_packs.iter().enumerate() {
6135 if pack.msg_types.is_empty() {
6136 continue;
6137 }
6138 let origin = slot_origin_ids
6139 .get(slot)
6140 .and_then(|origin| origin.as_ref())
6141 .unwrap_or_else(|| panic!("Missing slot origin id for copperlist output slot {slot}"));
6142 for _ in 0..pack.msg_types.len() {
6143 ids.push(origin.clone());
6144 }
6145 }
6146 ids
6147}
6148
6149fn flatten_task_output_specs(
6150 output_packs: &[OutputPack],
6151 slot_origin_ids: &[Option<String>],
6152) -> Vec<(String, String, Type)> {
6153 let mut specs = Vec::new();
6154 for (slot, pack) in output_packs.iter().enumerate() {
6155 if pack.msg_types.is_empty() {
6156 continue;
6157 }
6158 let origin = slot_origin_ids
6159 .get(slot)
6160 .and_then(|origin| origin.as_ref())
6161 .unwrap_or_else(|| panic!("Missing slot origin id for copperlist output slot {slot}"));
6162 for (msg_type, payload_type) in pack.msg_type_names.iter().zip(pack.msg_types.iter()) {
6163 specs.push((origin.clone(), msg_type.clone(), payload_type.clone()));
6164 }
6165 }
6166 specs
6167}
6168
6169fn build_slot_handle_modes(
6174 cuconfig: &CuConfig,
6175 mission_label: Option<&str>,
6176 output_packs: &[OutputPack],
6177 node_output_positions: &HashMap<NodeId, usize>,
6178 task_names: &[(NodeId, String, String)],
6179) -> Vec<HandleContent> {
6180 let mut slot_modes: Vec<HandleContent> = vec![HandleContent::default(); output_packs.len()];
6181 for (node_id, task_id, _member) in task_names {
6182 let Some(pos) = node_output_positions.get(node_id) else {
6183 continue;
6184 };
6185 if let Some(node) = cuconfig.find_task_node(mission_label, task_id) {
6186 slot_modes[*pos] = node.handle_content_policy();
6187 }
6188 }
6189 slot_modes
6190}
6191
6192fn extract_output_packs(runtime_plan: &CuExecutionLoop) -> Vec<OutputPack> {
6193 let mut packs: Vec<(u32, OutputPack)> = runtime_plan
6194 .steps
6195 .iter()
6196 .filter_map(|unit| match unit {
6197 CuExecutionUnit::Step(step) => {
6198 let output_pack = step.output_msg_pack.as_ref()?;
6199 let msg_types: Vec<Type> = output_pack
6200 .msg_types
6201 .iter()
6202 .map(|output_msg_type| {
6203 parse_str::<Type>(output_msg_type.as_str()).unwrap_or_else(|_| {
6204 panic!(
6205 "Could not transform {output_msg_type} into a message Rust type."
6206 )
6207 })
6208 })
6209 .collect();
6210 Some((
6211 output_pack.culist_index,
6212 OutputPack {
6213 msg_types,
6214 msg_type_names: output_pack.msg_types.clone(),
6215 },
6216 ))
6217 }
6218 CuExecutionUnit::Loop(_) => todo!("Needs to be implemented"),
6219 })
6220 .collect();
6221
6222 packs.sort_by_key(|(index, _)| *index);
6223 packs.into_iter().map(|(_, pack)| pack).collect()
6224}
6225
6226#[derive(Clone)]
6227struct SlotCodecBinding {
6228 payload_type: Type,
6229 task_id: String,
6230 msg_type: String,
6231 codec_type: syn::Path,
6232 codec_type_path: String,
6233}
6234
6235fn build_flat_slot_codec_bindings(
6236 cuconfig: &CuConfig,
6237 mission_label: Option<&str>,
6238 output_packs: &[OutputPack],
6239 node_output_positions: &HashMap<NodeId, usize>,
6240 task_names: &[(NodeId, String, String)],
6241) -> CuResult<Vec<Option<SlotCodecBinding>>> {
6242 let mut slot_task_ids: Vec<Option<String>> = vec![None; output_packs.len()];
6243 for (node_id, task_id, _) in task_names {
6244 let Some(output_position) = node_output_positions.get(node_id) else {
6245 continue;
6246 };
6247 slot_task_ids[*output_position] = Some(task_id.clone());
6248 }
6249
6250 let mut bindings =
6251 Vec::with_capacity(output_packs.iter().map(|pack| pack.msg_types.len()).sum());
6252 for (slot_idx, pack) in output_packs.iter().enumerate() {
6253 let task_id = slot_task_ids.get(slot_idx).and_then(|id| id.as_ref());
6254 for (port_idx, payload_type) in pack.msg_types.iter().enumerate() {
6255 let Some(task_id) = task_id else {
6256 bindings.push(None);
6257 continue;
6258 };
6259 let Some(msg_type) = pack.msg_type_names.get(port_idx) else {
6260 return Err(CuError::from(format!(
6261 "Missing message type name for task '{task_id}' slot {slot_idx} port {port_idx}."
6262 )));
6263 };
6264
6265 let spec = cuconfig
6266 .find_task_node(mission_label, task_id)
6267 .and_then(|node| node.get_logging())
6268 .and_then(|logging| logging.codec_for_msg_type(msg_type))
6269 .map(|codec_id| {
6270 cuconfig.find_logging_codec_spec(codec_id).ok_or_else(|| {
6271 CuError::from(format!(
6272 "Task '{task_id}' binds output '{msg_type}' to unknown logging codec '{codec_id}'."
6273 ))
6274 })
6275 })
6276 .transpose()?;
6277
6278 if let Some(spec) = spec {
6279 let codec_type = parse_str::<syn::Path>(&spec.type_).map_err(|_| {
6280 CuError::from(format!(
6281 "Logging codec '{}' for task '{task_id}' output '{msg_type}' is not a valid Rust type path.",
6282 spec.type_
6283 ))
6284 })?;
6285 bindings.push(Some(SlotCodecBinding {
6286 payload_type: payload_type.clone(),
6287 task_id: task_id.clone(),
6288 msg_type: msg_type.clone(),
6289 codec_type,
6290 codec_type_path: spec.type_.clone(),
6291 }));
6292 } else {
6293 bindings.push(None);
6294 }
6295 }
6296 }
6297
6298 Ok(bindings)
6299}
6300
6301fn build_culist_codec_helpers(
6302 flat_codec_bindings: &[Option<SlotCodecBinding>],
6303 default_config_ron_ident: &Ident,
6304 mission_label: Option<&str>,
6305) -> (
6306 Vec<proc_macro2::TokenStream>,
6307 Vec<Option<Ident>>,
6308 Vec<Option<Ident>>,
6309) {
6310 let mission_tokens = if let Some(mission) = mission_label {
6311 let lit = LitStr::new(mission, Span::call_site());
6312 quote! { Some(#lit) }
6313 } else {
6314 quote! { None }
6315 };
6316
6317 let mut helpers = Vec::new();
6318 let mut encode_helper_names = Vec::with_capacity(flat_codec_bindings.len());
6319 let mut decode_helper_names = Vec::with_capacity(flat_codec_bindings.len());
6320
6321 for (flat_idx, binding) in flat_codec_bindings.iter().enumerate() {
6322 let Some(binding) = binding else {
6323 encode_helper_names.push(None);
6324 decode_helper_names.push(None);
6325 continue;
6326 };
6327
6328 let encode_fn = format_ident!("__cu_logcodec_encode_slot_{flat_idx}");
6329 let decode_fn = format_ident!("__cu_logcodec_decode_slot_{flat_idx}");
6330 let payload_type = &binding.payload_type;
6331 let codec_type = &binding.codec_type;
6332 let task_id = LitStr::new(&binding.task_id, Span::call_site());
6333 let msg_type = LitStr::new(&binding.msg_type, Span::call_site());
6334 let codec_type_path = LitStr::new(&binding.codec_type_path, Span::call_site());
6335
6336 helpers.push(quote! {
6337 fn #encode_fn<E: Encoder>(msg: &CuMsg<#payload_type>, encoder: &mut E) -> Result<(), EncodeError> {
6338 static STATE: ::cu29::logcodec::CodecState<#codec_type> = ::cu29::logcodec::CodecState::new();
6339 let config_entry = ::cu29::logcodec::effective_config_entry::<CuStampedDataSet>(#default_config_ron_ident);
6340 ::cu29::logcodec::with_codec_for_encode(
6341 &STATE,
6342 config_entry,
6343 |effective_config_ron| {
6344 ::cu29::logcodec::instantiate_codec::<#codec_type, #payload_type>(
6345 effective_config_ron,
6346 #mission_tokens,
6347 #task_id,
6348 #msg_type,
6349 #codec_type_path,
6350 )
6351 },
6352 |codec| ::cu29::logcodec::encode_msg_with_codec(msg, codec, encoder),
6353 )
6354 }
6355
6356 fn #decode_fn<D: Decoder<Context = ()>>(decoder: &mut D) -> Result<CuMsg<#payload_type>, DecodeError> {
6357 static STATE: ::cu29::logcodec::CodecState<#codec_type> = ::cu29::logcodec::CodecState::new();
6358 let config_entry = ::cu29::logcodec::effective_config_entry::<CuStampedDataSet>(#default_config_ron_ident);
6359 ::cu29::logcodec::with_codec_for_decode(
6360 &STATE,
6361 config_entry,
6362 |effective_config_ron| {
6363 ::cu29::logcodec::instantiate_codec::<#codec_type, #payload_type>(
6364 effective_config_ron,
6365 #mission_tokens,
6366 #task_id,
6367 #msg_type,
6368 #codec_type_path,
6369 )
6370 },
6371 |codec| ::cu29::logcodec::decode_msg_with_codec(decoder, codec),
6372 )
6373 }
6374 });
6375 encode_helper_names.push(Some(encode_fn));
6376 decode_helper_names.push(Some(decode_fn));
6377 }
6378
6379 (helpers, encode_helper_names, decode_helper_names)
6380}
6381
6382fn collect_output_pack_sizes(runtime_plan: &CuExecutionLoop) -> Vec<usize> {
6383 let mut sizes: Vec<(u32, usize)> = runtime_plan
6384 .steps
6385 .iter()
6386 .filter_map(|unit| match unit {
6387 CuExecutionUnit::Step(step) => step
6388 .output_msg_pack
6389 .as_ref()
6390 .map(|output_pack| (output_pack.culist_index, output_pack.msg_types.len())),
6391 CuExecutionUnit::Loop(_) => todo!("Needs to be implemented"),
6392 })
6393 .collect();
6394
6395 sizes.sort_by_key(|(index, _)| *index);
6396 sizes.into_iter().map(|(_, size)| size).collect()
6397}
6398
6399fn sorted_mission_graphs(copper_config: &CuConfig) -> Vec<(String, CuGraph)> {
6400 let mut all_missions: Vec<_> = copper_config
6401 .graphs
6402 .get_all_missions_graphs()
6403 .into_iter()
6404 .collect();
6405 all_missions.sort_by(|(left, _), (right, _)| left.cmp(right));
6406 all_missions
6407}
6408
6409#[derive(Debug, Clone, PartialEq, Eq)]
6410struct CanonicalTaskInputSlot {
6411 msg_type: String,
6412 connection_orders: BTreeSet<usize>,
6413}
6414
6415#[derive(Debug, Clone, PartialEq, Eq)]
6416struct MissionTaskInput {
6417 msg_type: String,
6418 connection_order: usize,
6419}
6420
6421#[derive(Debug, Clone)]
6422struct TaskInputLayout {
6423 slots: Vec<CanonicalTaskInputSlot>,
6424 mission_slot_mappings: HashMap<String, Vec<Option<usize>>>,
6425}
6426
6427#[derive(Clone, Copy)]
6428enum AlignmentStep {
6429 Match {
6430 canonical_slot_index: usize,
6431 mission_input_index: usize,
6432 },
6433 ExistingGap {
6434 canonical_slot_index: usize,
6435 },
6436 Insert {
6437 mission_input_index: usize,
6438 },
6439}
6440
6441#[derive(Clone, Copy)]
6442enum AlignmentTransition {
6443 Match,
6444 ExistingGap,
6445 Insert,
6446}
6447
6448#[derive(Clone, Copy)]
6449struct AlignmentBackpointer {
6450 prev_i: usize,
6451 prev_j: usize,
6452 transition: AlignmentTransition,
6453}
6454
6455#[derive(Clone, Copy)]
6456struct AlignmentCell {
6457 score: i32,
6458 paths: u8,
6459 backpointer: Option<AlignmentBackpointer>,
6460}
6461
6462impl AlignmentCell {
6463 fn unreachable() -> Self {
6464 Self {
6465 score: i32::MIN,
6466 paths: 0,
6467 backpointer: None,
6468 }
6469 }
6470}
6471
6472fn collect_mission_task_inputs(
6473 graph: &CuGraph,
6474 node_id: NodeId,
6475 task_id: &str,
6476) -> CuResult<Vec<MissionTaskInput>> {
6477 let mut edge_ids = graph.get_dst_edges(node_id)?;
6478 edge_ids.sort_by_key(|edge_id| {
6479 graph
6480 .edge(*edge_id)
6481 .map(|edge| edge.order)
6482 .unwrap_or(usize::MAX)
6483 });
6484
6485 edge_ids
6486 .into_iter()
6487 .map(|edge_id| {
6488 let edge = graph.edge(edge_id).ok_or_else(|| {
6489 CuError::from(format!(
6490 "Missing edge {edge_id} while collecting inputs for task '{task_id}'"
6491 ))
6492 })?;
6493 Ok(MissionTaskInput {
6494 msg_type: edge.msg.clone(),
6495 connection_order: edge.order,
6496 })
6497 })
6498 .collect()
6499}
6500
6501fn format_canonical_input_slots(slots: &[CanonicalTaskInputSlot]) -> String {
6502 let parts: Vec<String> = slots
6503 .iter()
6504 .map(|slot| {
6505 let orders = slot
6506 .connection_orders
6507 .iter()
6508 .map(|order| order.to_string())
6509 .collect::<Vec<_>>()
6510 .join("|");
6511 format!("{}@{}", slot.msg_type, orders)
6512 })
6513 .collect();
6514 format!("[{}]", parts.join(", "))
6515}
6516
6517fn format_mission_task_inputs(inputs: &[MissionTaskInput]) -> String {
6518 let parts: Vec<String> = inputs
6519 .iter()
6520 .map(|input| format!("{}@{}", input.msg_type, input.connection_order))
6521 .collect();
6522 format!("[{}]", parts.join(", "))
6523}
6524
6525const INPUT_MATCH_SCORE: i32 = 100;
6526const ANCHORED_INPUT_MATCH_BONUS: i32 = 1;
6527
6528fn task_input_match_score(slot: &CanonicalTaskInputSlot, input: &MissionTaskInput) -> Option<i32> {
6529 if slot.msg_type != input.msg_type {
6530 return None;
6531 }
6532
6533 let anchored_bonus = if slot.connection_orders.contains(&input.connection_order) {
6534 ANCHORED_INPUT_MATCH_BONUS
6535 } else {
6536 0
6537 };
6538
6539 Some(INPUT_MATCH_SCORE + anchored_bonus)
6540}
6541
6542fn update_alignment_cell(
6543 cell: &mut AlignmentCell,
6544 candidate_score: i32,
6545 candidate_paths: u8,
6546 backpointer: Option<AlignmentBackpointer>,
6547) {
6548 if candidate_paths == 0 {
6549 return;
6550 }
6551
6552 if candidate_score > cell.score {
6553 cell.score = candidate_score;
6554 cell.paths = candidate_paths.min(2);
6555 cell.backpointer = if candidate_paths == 1 {
6556 backpointer
6557 } else {
6558 None
6559 };
6560 } else if candidate_score == cell.score {
6561 cell.paths = cell.paths.saturating_add(candidate_paths).min(2);
6562 cell.backpointer = None;
6563 }
6564}
6565
6566fn align_task_inputs(
6567 task_id: &str,
6568 mission_name: &str,
6569 canonical_slots: &[CanonicalTaskInputSlot],
6570 mission_inputs: &[MissionTaskInput],
6571) -> CuResult<Vec<AlignmentStep>> {
6572 let canonical_len = canonical_slots.len();
6573 let mission_len = mission_inputs.len();
6574 let mut table = vec![vec![AlignmentCell::unreachable(); mission_len + 1]; canonical_len + 1];
6575 table[0][0] = AlignmentCell {
6576 score: 0,
6577 paths: 1,
6578 backpointer: None,
6579 };
6580
6581 for i in 0..=canonical_len {
6582 for j in 0..=mission_len {
6583 let cell = table[i][j];
6584 if cell.paths == 0 {
6585 continue;
6586 }
6587
6588 if i < canonical_len {
6589 update_alignment_cell(
6590 &mut table[i + 1][j],
6591 cell.score,
6592 cell.paths,
6593 if cell.paths == 1 {
6594 Some(AlignmentBackpointer {
6595 prev_i: i,
6596 prev_j: j,
6597 transition: AlignmentTransition::ExistingGap,
6598 })
6599 } else {
6600 None
6601 },
6602 );
6603 }
6604
6605 if j < mission_len {
6606 update_alignment_cell(
6607 &mut table[i][j + 1],
6608 cell.score,
6609 cell.paths,
6610 if cell.paths == 1 {
6611 Some(AlignmentBackpointer {
6612 prev_i: i,
6613 prev_j: j,
6614 transition: AlignmentTransition::Insert,
6615 })
6616 } else {
6617 None
6618 },
6619 );
6620 }
6621
6622 if i < canonical_len
6623 && j < mission_len
6624 && let Some(match_score) =
6625 task_input_match_score(&canonical_slots[i], &mission_inputs[j])
6626 {
6627 update_alignment_cell(
6628 &mut table[i + 1][j + 1],
6629 cell.score + match_score,
6630 cell.paths,
6631 if cell.paths == 1 {
6632 Some(AlignmentBackpointer {
6633 prev_i: i,
6634 prev_j: j,
6635 transition: AlignmentTransition::Match,
6636 })
6637 } else {
6638 None
6639 },
6640 );
6641 }
6642 }
6643 }
6644
6645 let final_cell = table[canonical_len][mission_len];
6646 if final_cell.paths > 1 {
6647 return Err(CuError::from(format!(
6648 "Task '{task_id}' has ambiguous input alignment while merging mission '{mission_name}'. Existing canonical inputs {} and mission inputs {} admit multiple equally valid alignments.",
6649 format_canonical_input_slots(canonical_slots),
6650 format_mission_task_inputs(mission_inputs),
6651 )));
6652 }
6653
6654 let mut steps = Vec::new();
6655 let (mut i, mut j) = (canonical_len, mission_len);
6656 while i > 0 || j > 0 {
6657 let backpointer = table[i][j].backpointer.unwrap_or_else(|| {
6658 panic!(
6659 "Missing backpointer while aligning task '{task_id}' for mission '{mission_name}'"
6660 )
6661 });
6662
6663 match backpointer.transition {
6664 AlignmentTransition::Match => steps.push(AlignmentStep::Match {
6665 canonical_slot_index: i - 1,
6666 mission_input_index: j - 1,
6667 }),
6668 AlignmentTransition::ExistingGap => steps.push(AlignmentStep::ExistingGap {
6669 canonical_slot_index: i - 1,
6670 }),
6671 AlignmentTransition::Insert => steps.push(AlignmentStep::Insert {
6672 mission_input_index: j - 1,
6673 }),
6674 }
6675
6676 i = backpointer.prev_i;
6677 j = backpointer.prev_j;
6678 }
6679 steps.reverse();
6680 Ok(steps)
6681}
6682
6683fn merge_task_input_layout(
6684 task_id: &str,
6685 layout: &mut TaskInputLayout,
6686 mission_name: String,
6687 mission_inputs: Vec<MissionTaskInput>,
6688) -> CuResult<()> {
6689 let alignment = align_task_inputs(task_id, &mission_name, &layout.slots, &mission_inputs)?;
6690 let mut new_slots = Vec::with_capacity(alignment.len());
6691 let mut old_to_new = vec![None; layout.slots.len()];
6692 let mut mission_mapping = Vec::with_capacity(alignment.len());
6693
6694 for step in alignment {
6695 match step {
6696 AlignmentStep::Match {
6697 canonical_slot_index,
6698 mission_input_index,
6699 } => {
6700 let mut slot = layout.slots[canonical_slot_index].clone();
6701 slot.connection_orders
6702 .insert(mission_inputs[mission_input_index].connection_order);
6703 let new_index = new_slots.len();
6704 old_to_new[canonical_slot_index] = Some(new_index);
6705 new_slots.push(slot);
6706 mission_mapping.push(Some(mission_input_index));
6707 }
6708 AlignmentStep::ExistingGap {
6709 canonical_slot_index,
6710 } => {
6711 let new_index = new_slots.len();
6712 old_to_new[canonical_slot_index] = Some(new_index);
6713 new_slots.push(layout.slots[canonical_slot_index].clone());
6714 mission_mapping.push(None);
6715 }
6716 AlignmentStep::Insert {
6717 mission_input_index,
6718 } => {
6719 new_slots.push(CanonicalTaskInputSlot {
6720 msg_type: mission_inputs[mission_input_index].msg_type.clone(),
6721 connection_orders: BTreeSet::from([
6722 mission_inputs[mission_input_index].connection_order
6723 ]),
6724 });
6725 mission_mapping.push(Some(mission_input_index));
6726 }
6727 }
6728 }
6729
6730 let mut remapped_mission_slot_mappings =
6731 HashMap::with_capacity(layout.mission_slot_mappings.len() + 1);
6732 for (existing_mission, existing_mapping) in &layout.mission_slot_mappings {
6733 let mut remapped = vec![None; new_slots.len()];
6734 for (old_slot_index, maybe_local_input_index) in existing_mapping.iter().enumerate() {
6735 let new_slot_index = old_to_new[old_slot_index].unwrap_or_else(|| {
6736 panic!("Missing remap for task '{task_id}' canonical slot {old_slot_index}")
6737 });
6738 remapped[new_slot_index] = *maybe_local_input_index;
6739 }
6740 remapped_mission_slot_mappings.insert(existing_mission.clone(), remapped);
6741 }
6742 remapped_mission_slot_mappings.insert(mission_name, mission_mapping);
6743
6744 layout.slots = new_slots;
6745 layout.mission_slot_mappings = remapped_mission_slot_mappings;
6746 Ok(())
6747}
6748
6749fn collect_task_input_layouts(
6750 all_missions: &[(String, CuGraph)],
6751) -> CuResult<HashMap<String, TaskInputLayout>> {
6752 let mut task_mission_inputs: BTreeMap<String, Vec<(String, Vec<MissionTaskInput>)>> =
6753 BTreeMap::new();
6754
6755 for (mission_name, graph) in all_missions {
6756 for (node_id, node) in graph.get_all_nodes() {
6757 if node.get_flavor() != Flavor::Task {
6758 continue;
6759 }
6760
6761 if find_task_type_for_id(graph, node_id)? == CuTaskType::Source {
6762 continue;
6763 }
6764
6765 let task_id = node.get_id().to_string();
6766 let mission_inputs = collect_mission_task_inputs(graph, node_id, task_id.as_str())?;
6767 task_mission_inputs
6768 .entry(task_id)
6769 .or_default()
6770 .push((mission_name.clone(), mission_inputs));
6771 }
6772 }
6773
6774 let mut layouts = HashMap::new();
6775 for (task_id, mission_inputs) in task_mission_inputs {
6776 let mut mission_iter = mission_inputs.into_iter();
6777 let Some((first_mission, first_inputs)) = mission_iter.next() else {
6778 continue;
6779 };
6780
6781 let slots: Vec<CanonicalTaskInputSlot> = first_inputs
6782 .iter()
6783 .map(|input| CanonicalTaskInputSlot {
6784 msg_type: input.msg_type.clone(),
6785 connection_orders: BTreeSet::from([input.connection_order]),
6786 })
6787 .collect();
6788 let mut mission_slot_mappings = HashMap::new();
6789 mission_slot_mappings.insert(
6790 first_mission,
6791 (0..first_inputs.len()).map(Some).collect::<Vec<_>>(),
6792 );
6793
6794 let mut layout = TaskInputLayout {
6795 slots,
6796 mission_slot_mappings,
6797 };
6798 for (mission_name, mission_inputs) in mission_iter {
6799 merge_task_input_layout(&task_id, &mut layout, mission_name, mission_inputs)?;
6800 }
6801
6802 layouts.insert(task_id, layout);
6803 }
6804
6805 Ok(layouts)
6806}
6807
6808struct GeneratedTaskInput {
6809 setup: proc_macro2::TokenStream,
6810 expr: proc_macro2::TokenStream,
6811}
6812
6813fn present_task_input_expr(
6814 input: &cu29_runtime::curuntime::CuInputMsg,
6815 output_pack_sizes: &[usize],
6816) -> proc_macro2::TokenStream {
6817 let input_index = int2sliceindex(input.culist_index);
6818 let output_size = output_pack_sizes
6819 .get(input.culist_index as usize)
6820 .copied()
6821 .unwrap_or_else(|| {
6822 panic!(
6823 "Missing output pack size for culist index {}",
6824 input.culist_index
6825 )
6826 });
6827 if output_size > 1 {
6828 let port_index = syn::Index::from(input.src_port);
6829 quote! { &msgs.#input_index.#port_index }
6830 } else {
6831 quote! { &msgs.#input_index }
6832 }
6833}
6834
6835fn generate_task_input_binding(
6836 step: &CuExecutionStep,
6837 mission_name: &str,
6838 output_pack_sizes: &[usize],
6839 task_input_layouts: &HashMap<String, TaskInputLayout>,
6840) -> GeneratedTaskInput {
6841 let task_id = step.node.get_id().to_string();
6842 let layout = task_input_layouts
6843 .get(&task_id)
6844 .unwrap_or_else(|| panic!("Missing canonical input layout for task '{task_id}'"));
6845 let slot_mapping = layout
6846 .mission_slot_mappings
6847 .get(mission_name)
6848 .unwrap_or_else(|| {
6849 panic!("Missing input slot mapping for task '{task_id}' in mission '{mission_name}'")
6850 });
6851
6852 let mut setup = Vec::new();
6853 let mut refs = Vec::new();
6854
6855 for (slot_index, slot) in layout.slots.iter().enumerate() {
6856 if let Some(input_index) = slot_mapping.get(slot_index).copied().flatten() {
6857 let input = step.input_msg_indices_types.get(input_index).unwrap_or_else(|| {
6858 panic!(
6859 "Task '{task_id}' mission '{mission_name}' input slot {slot_index} mapped to missing input index {input_index}"
6860 )
6861 });
6862 refs.push(present_task_input_expr(input, output_pack_sizes));
6863 continue;
6864 }
6865
6866 let empty_input_ident = format_ident!("__cu_missing_input_{slot_index}");
6867 let input_ty: Type = parse_str(slot.msg_type.as_str()).unwrap_or_else(|err| {
6868 panic!(
6869 "Could not parse canonical input message type '{}' for task '{}': {err}",
6870 slot.msg_type, task_id
6871 )
6872 });
6873 setup.push(quote! {
6874 let #empty_input_ident = cu29::cutask::CuMsg::<#input_ty>::new(None);
6875 });
6876 refs.push(quote! { &#empty_input_ident });
6877 }
6878
6879 let expr = match refs.len() {
6880 0 => quote! { &() },
6881 1 => refs
6882 .into_iter()
6883 .next()
6884 .expect("single input expression missing"),
6885 _ => quote! { &( #(#refs),* ) },
6886 };
6887
6888 GeneratedTaskInput {
6889 setup: quote! { #(#setup)* },
6890 expr,
6891 }
6892}
6893
6894fn build_culist_tuple(slot_types: &[Type]) -> TypeTuple {
6896 if slot_types.is_empty() {
6897 parse_quote! { () }
6898 } else {
6899 parse_quote! { ( #( #slot_types ),*, ) }
6900 }
6901}
6902
6903fn build_culist_tuple_encode(
6905 output_packs: &[OutputPack],
6906 encode_helper_names: &[Option<Ident>],
6907 slot_handle_modes: &[HandleContent],
6908) -> ItemImpl {
6909 let mut flat_idx = 0usize;
6910 let mut encode_fields = Vec::new();
6911
6912 for (slot_idx, pack) in output_packs.iter().enumerate() {
6913 let slot_index = syn::Index::from(slot_idx);
6914 let mode = slot_handle_modes.get(slot_idx).copied();
6915
6916 if pack.is_multi() {
6917 for (port_idx, payload_ty) in pack.msg_types.iter().enumerate() {
6918 let port_index = syn::Index::from(port_idx);
6919 let cache_index = flat_idx;
6920 let encode_helper = encode_helper_names[flat_idx].clone();
6921 flat_idx += 1;
6922 let normal_encode = if let Some(helper) = encode_helper {
6923 quote! { #helper(&self.0.#slot_index.#port_index, encoder)?; }
6924 } else {
6925 quote! { self.0.#slot_index.#port_index.encode(encoder)?; }
6926 };
6927 let slot_access = quote! { self.0.#slot_index.#port_index };
6928 let slot_block =
6929 build_per_slot_encode_block(mode, payload_ty, &slot_access, &normal_encode);
6930 encode_fields.push(quote! {
6931 __cu_capture.select_slot(#cache_index);
6932 #slot_block
6933 });
6934 }
6935 } else {
6936 let cache_index = flat_idx;
6937 let encode_helper = encode_helper_names[flat_idx].clone();
6938 flat_idx += 1;
6939 let normal_encode = if let Some(helper) = encode_helper {
6940 quote! { #helper(&self.0.#slot_index, encoder)?; }
6941 } else {
6942 quote! { self.0.#slot_index.encode(encoder)?; }
6943 };
6944 let slot_access = quote! { self.0.#slot_index };
6945 let payload_ty = pack
6946 .msg_types
6947 .first()
6948 .expect("single-port pack must have a payload type");
6949 let slot_block =
6950 build_per_slot_encode_block(mode, payload_ty, &slot_access, &normal_encode);
6951 encode_fields.push(quote! {
6952 __cu_capture.select_slot(#cache_index);
6953 #slot_block
6954 });
6955 }
6956 }
6957
6958 parse_quote! {
6959 impl Encode for CuStampedDataSet {
6960 fn encode<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
6961 let __cu_capture = cu29::monitoring::start_copperlist_io_capture(&self.1);
6962 #(#encode_fields)*
6963 Ok(())
6964 }
6965 }
6966 }
6967}
6968
6969fn build_per_slot_encode_block(
6974 mode: Option<HandleContent>,
6975 payload_ty: &Type,
6976 slot_access: &proc_macro2::TokenStream,
6977 normal_encode: &proc_macro2::TokenStream,
6978) -> proc_macro2::TokenStream {
6979 let mode = match mode {
6980 Some(m) if m != HandleContent::default() => m,
6981 _ => return normal_encode.clone(),
6982 };
6983 let mode_u8 = mode as u8;
6984 quote! {
6985 {
6986 const _: fn() = || {
6989 fn assert_aware<__T: ::cu29::pool::HandleContentAware + ?::core::marker::Sized>() {}
6990 assert_aware::<#payload_ty>();
6991 };
6992 use ::cu29::pool::PayloadDefaultHandlePolicyApply as _;
6993 use ::cu29::pool::PayloadDefaultLoggingPolicy as _;
6994 let __cu_should_log = match #slot_access.payload() {
6997 Some(__cu_p) => {
6998 __cu_p.apply_handle_content_policy(
6999 ::cu29::config::HandleContent::from_u8(#mode_u8),
7000 );
7001 __cu_p.payload_should_log()
7002 }
7003 None => false,
7004 };
7005 if __cu_should_log {
7006 #normal_encode
7007 } else {
7008 ::cu29::cutask::encode_metadata_only(&#slot_access, encoder)?;
7009 }
7010 }
7011 }
7012}
7013
7014fn build_culist_tuple_decode(
7016 output_packs: &[OutputPack],
7017 slot_types: &[Type],
7018 cumsg_count: usize,
7019 decode_helper_names: &[Option<Ident>],
7020) -> ItemImpl {
7021 let mut flat_idx = 0usize;
7022 let mut decode_fields = Vec::with_capacity(slot_types.len());
7023 for (slot_idx, pack) in output_packs.iter().enumerate() {
7024 let slot_type = &slot_types[slot_idx];
7025 if pack.is_multi() {
7026 let mut slot_fields = Vec::with_capacity(pack.msg_types.len());
7027 for _ in 0..pack.msg_types.len() {
7028 let decode_helper = decode_helper_names[flat_idx].clone();
7029 flat_idx += 1;
7030 if let Some(decode_helper) = decode_helper {
7031 slot_fields.push(quote! { #decode_helper(decoder)? });
7032 } else {
7033 let msg_type = &pack.msg_types[slot_fields.len()];
7034 slot_fields.push(quote! { <CuMsg<#msg_type> as Decode<()>>::decode(decoder)? });
7035 }
7036 }
7037 decode_fields.push(quote! { ( #(#slot_fields),* ) });
7038 } else if let Some(decode_helper) = decode_helper_names[flat_idx].clone() {
7039 flat_idx += 1;
7040 decode_fields.push(quote! { #decode_helper(decoder)? });
7041 } else {
7042 flat_idx += 1;
7043 decode_fields.push(quote! { <#slot_type as Decode<()>>::decode(decoder)? });
7044 }
7045 }
7046
7047 parse_quote! {
7048 impl Decode<()> for CuStampedDataSet {
7049 fn decode<D: Decoder<Context=()>>(decoder: &mut D) -> Result<Self, DecodeError> {
7050 Ok(CuStampedDataSet(
7051 (
7052 #(#decode_fields),*,
7053 ),
7054 cu29::monitoring::CuMsgIoCache::<#cumsg_count>::default(),
7055 ))
7056 }
7057 }
7058 }
7059}
7060
7061fn build_culist_erasedcumsgs(output_packs: &[OutputPack]) -> ItemImpl {
7062 let mut casted_fields: Vec<proc_macro2::TokenStream> = Vec::new();
7063 for (idx, pack) in output_packs.iter().enumerate() {
7064 let slot_index = syn::Index::from(idx);
7065 if pack.is_multi() {
7066 for port_idx in 0..pack.msg_types.len() {
7067 let port_index = syn::Index::from(port_idx);
7068 casted_fields.push(quote! {
7069 &self.0.#slot_index.#port_index as &dyn ErasedCuStampedData
7070 });
7071 }
7072 } else {
7073 casted_fields.push(quote! { &self.0.#slot_index as &dyn ErasedCuStampedData });
7074 }
7075 }
7076 parse_quote! {
7077 impl ErasedCuStampedDataSet for CuStampedDataSet {
7078 fn cumsgs(&self) -> Vec<&dyn ErasedCuStampedData> {
7079 vec![
7080 #(#casted_fields),*
7081 ]
7082 }
7083 }
7084 }
7085}
7086
7087fn build_culist_tuple_debug(slot_types: &[Type]) -> ItemImpl {
7088 let indices: Vec<usize> = (0..slot_types.len()).collect();
7089
7090 let debug_fields: Vec<_> = indices
7091 .iter()
7092 .map(|i| {
7093 let idx = syn::Index::from(*i);
7094 quote! { .field(&self.0.#idx) }
7095 })
7096 .collect();
7097
7098 parse_quote! {
7099 impl Debug for CuStampedDataSet {
7100 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
7101 f.debug_tuple("CuStampedDataSet")
7102 #(#debug_fields)*
7103 .finish()
7104 }
7105 }
7106 }
7107}
7108
7109fn build_culist_tuple_serialize(slot_types: &[Type]) -> ItemImpl {
7111 let indices: Vec<usize> = (0..slot_types.len()).collect();
7112 let tuple_len = slot_types.len();
7113
7114 let serialize_fields: Vec<_> = indices
7116 .iter()
7117 .map(|i| {
7118 let idx = syn::Index::from(*i);
7119 quote! { &self.0.#idx }
7120 })
7121 .collect();
7122
7123 parse_quote! {
7124 impl cu29::serde::ser::Serialize for CuStampedDataSet {
7125 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
7126 where
7127 S: cu29::serde::Serializer,
7128 {
7129 use cu29::serde::ser::SerializeTuple;
7130 let mut tuple = serializer.serialize_tuple(#tuple_len)?;
7131 #(tuple.serialize_element(#serialize_fields)?;)*
7132 tuple.end()
7133 }
7134 }
7135 }
7136}
7137
7138fn build_culist_tuple_default(slot_types: &[Type], cumsg_count: usize) -> ItemImpl {
7140 let default_fields: Vec<_> = slot_types
7141 .iter()
7142 .map(|slot_type| quote! { <#slot_type as Default>::default() })
7143 .collect();
7144
7145 parse_quote! {
7146 impl Default for CuStampedDataSet {
7147 fn default() -> CuStampedDataSet
7148 {
7149 CuStampedDataSet(
7150 (
7151 #(#default_fields),*,
7152 ),
7153 cu29::monitoring::CuMsgIoCache::<#cumsg_count>::default(),
7154 )
7155 }
7156 }
7157 }
7158}
7159
7160fn collect_bridge_channel_usage(graph: &CuGraph) -> HashMap<BridgeChannelKey, String> {
7161 let mut usage = HashMap::new();
7162 for cnx in graph.edges() {
7163 if let Some(channel) = &cnx.src_channel {
7164 let key = BridgeChannelKey {
7165 bridge_id: cnx.src.clone(),
7166 channel_id: channel.clone(),
7167 direction: BridgeChannelDirection::Rx,
7168 };
7169 usage
7170 .entry(key)
7171 .and_modify(|msg| {
7172 if msg != &cnx.msg {
7173 panic!(
7174 "Bridge '{}' channel '{}' is used with incompatible message types: {} vs {}",
7175 cnx.src, channel, msg, cnx.msg
7176 );
7177 }
7178 })
7179 .or_insert(cnx.msg.clone());
7180 }
7181 if let Some(channel) = &cnx.dst_channel {
7182 let key = BridgeChannelKey {
7183 bridge_id: cnx.dst.clone(),
7184 channel_id: channel.clone(),
7185 direction: BridgeChannelDirection::Tx,
7186 };
7187 usage
7188 .entry(key)
7189 .and_modify(|msg| {
7190 if msg != &cnx.msg {
7191 panic!(
7192 "Bridge '{}' channel '{}' is used with incompatible message types: {} vs {}",
7193 cnx.dst, channel, msg, cnx.msg
7194 );
7195 }
7196 })
7197 .or_insert(cnx.msg.clone());
7198 }
7199 }
7200 usage
7201}
7202
7203fn build_bridge_specs(
7204 config: &CuConfig,
7205 graph: &CuGraph,
7206 channel_usage: &HashMap<BridgeChannelKey, String>,
7207) -> Vec<BridgeSpec> {
7208 let mut specs = Vec::new();
7209 for (bridge_index, bridge_cfg) in config.bridges.iter().enumerate() {
7210 if graph.get_node_id_by_name(bridge_cfg.id.as_str()).is_none() {
7211 continue;
7212 }
7213
7214 let type_path = parse_str::<Type>(bridge_cfg.type_.as_str()).unwrap_or_else(|err| {
7215 panic!(
7216 "Could not parse bridge type '{}' for '{}': {err}",
7217 bridge_cfg.type_, bridge_cfg.id
7218 )
7219 });
7220
7221 let mut rx_channels = Vec::new();
7222 let mut tx_channels = Vec::new();
7223
7224 for (channel_index, channel) in bridge_cfg.channels.iter().enumerate() {
7225 match channel {
7226 BridgeChannelConfigRepresentation::Rx { id, .. } => {
7227 let key = BridgeChannelKey {
7228 bridge_id: bridge_cfg.id.clone(),
7229 channel_id: id.clone(),
7230 direction: BridgeChannelDirection::Rx,
7231 };
7232 if let Some(msg_type) = channel_usage.get(&key) {
7233 let msg_type_name = msg_type.clone();
7234 let msg_type = parse_str::<Type>(msg_type).unwrap_or_else(|err| {
7235 panic!(
7236 "Could not parse message type '{msg_type}' for bridge '{}' channel '{}': {err}",
7237 bridge_cfg.id, id
7238 )
7239 });
7240 let const_ident =
7241 Ident::new(&config_id_to_bridge_const(id.as_str()), Span::call_site());
7242 rx_channels.push(BridgeChannelSpec {
7243 id: id.clone(),
7244 const_ident,
7245 msg_type,
7246 msg_type_name,
7247 config_index: channel_index,
7248 plan_node_id: None,
7249 culist_index: None,
7250 monitor_index: None,
7251 });
7252 }
7253 }
7254 BridgeChannelConfigRepresentation::Tx { id, .. } => {
7255 let key = BridgeChannelKey {
7256 bridge_id: bridge_cfg.id.clone(),
7257 channel_id: id.clone(),
7258 direction: BridgeChannelDirection::Tx,
7259 };
7260 if let Some(msg_type) = channel_usage.get(&key) {
7261 let msg_type_name = msg_type.clone();
7262 let msg_type = parse_str::<Type>(msg_type).unwrap_or_else(|err| {
7263 panic!(
7264 "Could not parse message type '{msg_type}' for bridge '{}' channel '{}': {err}",
7265 bridge_cfg.id, id
7266 )
7267 });
7268 let const_ident =
7269 Ident::new(&config_id_to_bridge_const(id.as_str()), Span::call_site());
7270 tx_channels.push(BridgeChannelSpec {
7271 id: id.clone(),
7272 const_ident,
7273 msg_type,
7274 msg_type_name,
7275 config_index: channel_index,
7276 plan_node_id: None,
7277 culist_index: None,
7278 monitor_index: None,
7279 });
7280 }
7281 }
7282 }
7283 }
7284
7285 if rx_channels.is_empty() && tx_channels.is_empty() {
7286 continue;
7287 }
7288
7289 specs.push(BridgeSpec {
7290 id: bridge_cfg.id.clone(),
7291 type_path,
7292 run_in_sim: bridge_cfg.is_run_in_sim(),
7293 config_index: bridge_index,
7294 tuple_index: 0,
7295 monitor_index: None,
7296 rx_channels,
7297 tx_channels,
7298 });
7299 }
7300
7301 for (tuple_index, spec) in specs.iter_mut().enumerate() {
7302 spec.tuple_index = tuple_index;
7303 }
7304
7305 specs
7306}
7307
7308fn collect_task_names(graph: &CuGraph) -> Vec<(NodeId, String, String)> {
7309 graph
7310 .get_all_nodes()
7311 .iter()
7312 .filter(|(_, node)| node.get_flavor() == Flavor::Task)
7313 .map(|(node_id, node)| {
7314 (
7315 *node_id,
7316 node.get_id().to_string(),
7317 config_id_to_struct_member(node.get_id().as_str()),
7318 )
7319 })
7320 .collect()
7321}
7322
7323#[derive(Clone, Copy)]
7324enum ResourceOwner {
7325 Task(usize),
7326 Bridge(usize),
7327}
7328
7329#[derive(Clone)]
7330struct ResourceKeySpec {
7331 bundle_index: usize,
7332 provider_path: syn::Path,
7333 resource_name: String,
7334 binding_name: String,
7335 owner: ResourceOwner,
7336}
7337
7338fn parse_resource_path(path: &str) -> CuResult<(String, String)> {
7339 let (bundle_id, name) = path.split_once('.').ok_or_else(|| {
7340 CuError::from(format!(
7341 "Resource '{path}' is missing a bundle prefix (expected bundle.resource)"
7342 ))
7343 })?;
7344
7345 if bundle_id.is_empty() || name.is_empty() {
7346 return Err(CuError::from(format!(
7347 "Resource '{path}' must use the 'bundle.resource' format"
7348 )));
7349 }
7350
7351 Ok((bundle_id.to_string(), name.to_string()))
7352}
7353
7354fn collect_resource_specs(
7355 graph: &CuGraph,
7356 task_specs: &CuTaskSpecSet,
7357 bridge_specs: &[BridgeSpec],
7358 bundle_specs: &[BundleSpec],
7359) -> CuResult<Vec<ResourceKeySpec>> {
7360 let mut bridge_lookup: BTreeMap<String, usize> = BTreeMap::new();
7361 for (idx, spec) in bridge_specs.iter().enumerate() {
7362 bridge_lookup.insert(spec.id.clone(), idx);
7363 }
7364
7365 let mut bundle_lookup: HashMap<String, (usize, syn::Path)> = HashMap::new();
7366 for (index, bundle) in bundle_specs.iter().enumerate() {
7367 bundle_lookup.insert(bundle.id.clone(), (index, bundle.provider_path.clone()));
7368 }
7369
7370 let mut specs = Vec::new();
7371
7372 for (node_id, node) in graph.get_all_nodes() {
7373 let resources = node.get_resources();
7374 if let Some(resources) = resources {
7375 let task_index = task_specs.node_id_to_task_index[node_id as usize];
7376 let owner = if let Some(task_index) = task_index {
7377 ResourceOwner::Task(task_index)
7378 } else if node.get_flavor() == Flavor::Bridge {
7379 let bridge_index = bridge_lookup.get(&node.get_id()).ok_or_else(|| {
7380 CuError::from(format!(
7381 "Resource mapping attached to unknown bridge node '{}'",
7382 node.get_id()
7383 ))
7384 })?;
7385 ResourceOwner::Bridge(*bridge_index)
7386 } else {
7387 return Err(CuError::from(format!(
7388 "Resource mapping attached to non-task node '{}'",
7389 node.get_id()
7390 )));
7391 };
7392
7393 for (binding_name, path) in resources {
7394 let (bundle_id, resource_name) = parse_resource_path(path)?;
7395 let (bundle_index, provider_path) =
7396 bundle_lookup.get(&bundle_id).ok_or_else(|| {
7397 CuError::from(format!(
7398 "Resource '{}' references unknown bundle '{}'",
7399 path, bundle_id
7400 ))
7401 })?;
7402 specs.push(ResourceKeySpec {
7403 bundle_index: *bundle_index,
7404 provider_path: provider_path.clone(),
7405 resource_name,
7406 binding_name: binding_name.clone(),
7407 owner,
7408 });
7409 }
7410 }
7411 }
7412
7413 Ok(specs)
7414}
7415
7416fn build_bundle_list<'a>(config: &'a CuConfig, mission: &str) -> Vec<&'a ResourceBundleConfig> {
7417 config
7418 .resources
7419 .iter()
7420 .filter(|bundle| {
7421 bundle
7422 .missions
7423 .as_ref()
7424 .is_none_or(|missions| missions.iter().any(|m| m == mission))
7425 })
7426 .collect()
7427}
7428
7429struct BundleSpec {
7430 id: String,
7431 provider_path: syn::Path,
7432}
7433
7434fn build_bundle_specs(config: &CuConfig, mission: &str) -> CuResult<Vec<BundleSpec>> {
7435 build_bundle_list(config, mission)
7436 .into_iter()
7437 .map(|bundle| {
7438 let provider_path: syn::Path =
7439 syn::parse_str(bundle.provider.as_str()).map_err(|err| {
7440 CuError::from(format!(
7441 "Failed to parse provider path '{}' for bundle '{}': {err}",
7442 bundle.provider, bundle.id
7443 ))
7444 })?;
7445 Ok(BundleSpec {
7446 id: bundle.id.clone(),
7447 provider_path,
7448 })
7449 })
7450 .collect()
7451}
7452
7453fn build_resources_module(
7454 bundle_specs: &[BundleSpec],
7455) -> CuResult<(proc_macro2::TokenStream, proc_macro2::TokenStream)> {
7456 let bundle_consts = bundle_specs.iter().enumerate().map(|(index, bundle)| {
7457 let const_ident = Ident::new(
7458 &config_id_to_bridge_const(bundle.id.as_str()),
7459 Span::call_site(),
7460 );
7461 quote! { pub const #const_ident: BundleIndex = BundleIndex::new(#index); }
7462 });
7463
7464 let resources_module = quote! {
7465 pub mod resources {
7466 #![allow(dead_code)]
7467 use cu29::resource::BundleIndex;
7468
7469 pub mod bundles {
7470 use super::BundleIndex;
7471 #(#bundle_consts)*
7472 }
7473 }
7474 };
7475
7476 let bundle_counts = bundle_specs.iter().map(|bundle| {
7477 let provider_path = &bundle.provider_path;
7478 quote! { <#provider_path as cu29::resource::ResourceBundleDecl>::Id::COUNT }
7479 });
7480
7481 let bundle_inits = bundle_specs
7482 .iter()
7483 .enumerate()
7484 .map(|(index, bundle)| {
7485 let bundle_id = LitStr::new(bundle.id.as_str(), Span::call_site());
7486 let provider_path = &bundle.provider_path;
7487 quote! {
7488 let bundle_cfg = config
7489 .resources
7490 .iter()
7491 .find(|b| b.id == #bundle_id)
7492 .unwrap_or_else(|| panic!("Resource bundle '{}' missing from configuration", #bundle_id));
7493 let bundle_ctx = cu29::resource::BundleContext::<#provider_path>::new(
7494 cu29::resource::BundleIndex::new(#index),
7495 #bundle_id,
7496 );
7497 <#provider_path as cu29::resource::ResourceBundle>::build(
7498 bundle_ctx,
7499 bundle_cfg.config.as_ref(),
7500 &mut manager,
7501 )?;
7502 }
7503 })
7504 .collect::<Vec<_>>();
7505
7506 let resources_instanciator = quote! {
7507 pub fn resources_instanciator(config: &CuConfig) -> CuResult<cu29::resource::ResourceManager> {
7508 let bundle_counts: &[usize] = &[ #(#bundle_counts),* ];
7509 let mut manager = cu29::resource::ResourceManager::new(bundle_counts);
7510 #(#bundle_inits)*
7511 Ok(manager)
7512 }
7513 };
7514
7515 Ok((resources_module, resources_instanciator))
7516}
7517
7518struct ResourceMappingTokens {
7519 defs: proc_macro2::TokenStream,
7520 refs: Vec<proc_macro2::TokenStream>,
7521}
7522
7523fn build_task_resource_mappings(
7524 resource_specs: &[ResourceKeySpec],
7525 task_specs: &CuTaskSpecSet,
7526 sim_mode: bool,
7527) -> CuResult<ResourceMappingTokens> {
7528 let mut per_task: Vec<Vec<&ResourceKeySpec>> = vec![Vec::new(); task_specs.ids.len()];
7529
7530 for spec in resource_specs {
7531 let ResourceOwner::Task(task_index) = spec.owner else {
7532 continue;
7533 };
7534 if sim_mode
7535 && !task_specs.run_in_sim_flags[task_index]
7536 && task_specs.cutypes[task_index] != CuTaskType::Regular
7537 {
7538 continue;
7539 }
7540 per_task
7541 .get_mut(task_index)
7542 .ok_or_else(|| {
7543 CuError::from(format!(
7544 "Resource '{}' mapped to invalid task index {}",
7545 spec.binding_name, task_index
7546 ))
7547 })?
7548 .push(spec);
7549 }
7550
7551 let mut mapping_defs = Vec::new();
7552 let mut mapping_refs = Vec::new();
7553
7554 for (idx, entries) in per_task.iter().enumerate() {
7555 if entries.is_empty() {
7556 mapping_refs.push(quote! { None });
7557 continue;
7558 }
7559
7560 let binding_task_type = if task_specs.background_flags[idx] {
7561 &task_specs.sim_task_types[idx]
7562 } else {
7563 &task_specs.task_types[idx]
7564 };
7565
7566 let binding_trait = match task_specs.cutypes[idx] {
7567 CuTaskType::Source => quote! { CuSrcTask },
7568 CuTaskType::Regular => quote! { CuTask },
7569 CuTaskType::Sink => quote! { CuSinkTask },
7570 };
7571
7572 let entries_ident = format_ident!("TASK{}_RES_ENTRIES", idx);
7573 let map_ident = format_ident!("TASK{}_RES_MAPPING", idx);
7574 let binding_type = quote! {
7575 <<#binding_task_type as #binding_trait>::Resources<'_> as ResourceBindings>::Binding
7576 };
7577 let entry_tokens = entries.iter().map(|spec| {
7578 let binding_ident = Ident::new(
7579 &config_id_to_enum(spec.binding_name.as_str()),
7580 Span::call_site(),
7581 );
7582 let resource_name = LitStr::new(spec.resource_name.as_str(), Span::call_site());
7583 let bundle_index = spec.bundle_index;
7584 let provider_path = &spec.provider_path;
7585 quote! {
7586 (#binding_type::#binding_ident, cu29::resource::ResourceKey::new(
7587 cu29::resource::BundleIndex::new(#bundle_index),
7588 cu29::resource::resource_index_by_name::<#provider_path>(#resource_name),
7589 ))
7590 }
7591 });
7592
7593 mapping_defs.push(quote! {
7594 const #entries_ident: &[(#binding_type, cu29::resource::ResourceKey)] = &[ #(#entry_tokens),* ];
7595 const #map_ident: cu29::resource::ResourceBindingMap<#binding_type> =
7596 cu29::resource::ResourceBindingMap::new(#entries_ident);
7597 });
7598 mapping_refs.push(quote! { Some(&#map_ident) });
7599 }
7600
7601 Ok(ResourceMappingTokens {
7602 defs: quote! { #(#mapping_defs)* },
7603 refs: mapping_refs,
7604 })
7605}
7606
7607fn build_bridge_resource_mappings(
7608 resource_specs: &[ResourceKeySpec],
7609 bridge_specs: &[BridgeSpec],
7610 sim_mode: bool,
7611) -> ResourceMappingTokens {
7612 let mut per_bridge: Vec<Vec<&ResourceKeySpec>> = vec![Vec::new(); bridge_specs.len()];
7613
7614 for spec in resource_specs {
7615 let ResourceOwner::Bridge(bridge_index) = spec.owner else {
7616 continue;
7617 };
7618 if sim_mode && !bridge_specs[bridge_index].run_in_sim {
7619 continue;
7620 }
7621 per_bridge[bridge_index].push(spec);
7622 }
7623
7624 let mut mapping_defs = Vec::new();
7625 let mut mapping_refs = Vec::new();
7626
7627 for (idx, entries) in per_bridge.iter().enumerate() {
7628 if entries.is_empty() {
7629 mapping_refs.push(quote! { None });
7630 continue;
7631 }
7632
7633 let bridge_type = &bridge_specs[idx].type_path;
7634 let binding_type = quote! {
7635 <<#bridge_type as cu29::cubridge::CuBridge>::Resources<'_> as ResourceBindings>::Binding
7636 };
7637 let entries_ident = format_ident!("BRIDGE{}_RES_ENTRIES", idx);
7638 let map_ident = format_ident!("BRIDGE{}_RES_MAPPING", idx);
7639 let entry_tokens = entries.iter().map(|spec| {
7640 let binding_ident = Ident::new(
7641 &config_id_to_enum(spec.binding_name.as_str()),
7642 Span::call_site(),
7643 );
7644 let resource_name = LitStr::new(spec.resource_name.as_str(), Span::call_site());
7645 let bundle_index = spec.bundle_index;
7646 let provider_path = &spec.provider_path;
7647 quote! {
7648 (#binding_type::#binding_ident, cu29::resource::ResourceKey::new(
7649 cu29::resource::BundleIndex::new(#bundle_index),
7650 cu29::resource::resource_index_by_name::<#provider_path>(#resource_name),
7651 ))
7652 }
7653 });
7654
7655 mapping_defs.push(quote! {
7656 const #entries_ident: &[(#binding_type, cu29::resource::ResourceKey)] = &[ #(#entry_tokens),* ];
7657 const #map_ident: cu29::resource::ResourceBindingMap<#binding_type> =
7658 cu29::resource::ResourceBindingMap::new(#entries_ident);
7659 });
7660 mapping_refs.push(quote! { Some(&#map_ident) });
7661 }
7662
7663 ResourceMappingTokens {
7664 defs: quote! { #(#mapping_defs)* },
7665 refs: mapping_refs,
7666 }
7667}
7668
7669fn build_execution_plan(
7670 graph: &CuGraph,
7671 task_specs: &CuTaskSpecSet,
7672 bridge_specs: &mut [BridgeSpec],
7673) -> CuResult<(
7674 CuExecutionLoop,
7675 Vec<ExecutionEntity>,
7676 HashMap<NodeId, NodeId>,
7677)> {
7678 let mut plan_graph = CuGraph::default();
7679 let mut exec_entities = Vec::new();
7680 let mut original_to_plan = HashMap::new();
7681 let mut plan_to_original = HashMap::new();
7682 let mut name_to_original = HashMap::new();
7683 let mut channel_nodes = HashMap::new();
7684
7685 for (node_id, node) in graph.get_all_nodes() {
7686 name_to_original.insert(node.get_id(), node_id);
7687 if node.get_flavor() != Flavor::Task {
7688 continue;
7689 }
7690 let plan_node_id = plan_graph.add_node(node.clone())?;
7691 let task_index = task_specs.node_id_to_task_index[node_id as usize]
7692 .expect("Task missing from specifications");
7693 plan_to_original.insert(plan_node_id, node_id);
7694 original_to_plan.insert(node_id, plan_node_id);
7695 if plan_node_id as usize != exec_entities.len() {
7696 panic!("Unexpected node ordering while mirroring tasks in plan graph");
7697 }
7698 exec_entities.push(ExecutionEntity {
7699 kind: ExecutionEntityKind::Task { task_index },
7700 });
7701 }
7702
7703 for (node_id, node) in graph.get_all_nodes() {
7704 if node.get_flavor() != Flavor::Task {
7705 continue;
7706 }
7707 let Some(task_index) = task_specs.node_id_to_task_index[node_id as usize] else {
7708 continue;
7709 };
7710 if !task_specs
7711 .autogenerated_output_flags
7712 .get(task_index)
7713 .copied()
7714 .unwrap_or(false)
7715 {
7716 continue;
7717 }
7718 let plan_node_id = *original_to_plan
7719 .get(&node_id)
7720 .unwrap_or_else(|| panic!("Task '{}' missing from mirrored plan graph", node.get_id()));
7721 let task_kind = task_specs.cutypes[task_index];
7722 let task_type: Type =
7723 parse_str(task_specs.type_names[task_index].as_str()).unwrap_or_else(|err| {
7724 panic!(
7725 "Could not parse task type '{}': {err}",
7726 task_specs.type_names[task_index]
7727 )
7728 });
7729 let msg_type = synthesized_single_output_msg_name(&task_type, task_kind);
7730 plan_graph
7731 .get_node_mut(plan_node_id)
7732 .unwrap_or_else(|| panic!("Plan node '{}' missing from mirrored graph", node.get_id()))
7733 .add_nc_output(msg_type.as_str(), usize::MAX);
7734 }
7735
7736 for (bridge_index, spec) in bridge_specs.iter_mut().enumerate() {
7737 for (channel_index, channel_spec) in spec.rx_channels.iter_mut().enumerate() {
7738 let mut node = Node::new(
7739 format!("{}::rx::{}", spec.id, channel_spec.id).as_str(),
7740 "__CuBridgeRxChannel",
7741 );
7742 node.set_flavor(Flavor::Bridge);
7743 let plan_node_id = plan_graph.add_node(node)?;
7744 if plan_node_id as usize != exec_entities.len() {
7745 panic!("Unexpected node ordering while inserting bridge rx channel");
7746 }
7747 channel_spec.plan_node_id = Some(plan_node_id);
7748 exec_entities.push(ExecutionEntity {
7749 kind: ExecutionEntityKind::BridgeRx {
7750 bridge_index,
7751 channel_index,
7752 },
7753 });
7754 channel_nodes.insert(
7755 BridgeChannelKey {
7756 bridge_id: spec.id.clone(),
7757 channel_id: channel_spec.id.clone(),
7758 direction: BridgeChannelDirection::Rx,
7759 },
7760 plan_node_id,
7761 );
7762 }
7763
7764 for (channel_index, channel_spec) in spec.tx_channels.iter_mut().enumerate() {
7765 let mut node = Node::new(
7766 format!("{}::tx::{}", spec.id, channel_spec.id).as_str(),
7767 "__CuBridgeTxChannel",
7768 );
7769 node.set_flavor(Flavor::Bridge);
7770 let plan_node_id = plan_graph.add_node(node)?;
7771 if plan_node_id as usize != exec_entities.len() {
7772 panic!("Unexpected node ordering while inserting bridge tx channel");
7773 }
7774 channel_spec.plan_node_id = Some(plan_node_id);
7775 exec_entities.push(ExecutionEntity {
7776 kind: ExecutionEntityKind::BridgeTx {
7777 bridge_index,
7778 channel_index,
7779 },
7780 });
7781 channel_nodes.insert(
7782 BridgeChannelKey {
7783 bridge_id: spec.id.clone(),
7784 channel_id: channel_spec.id.clone(),
7785 direction: BridgeChannelDirection::Tx,
7786 },
7787 plan_node_id,
7788 );
7789 }
7790 }
7791
7792 for cnx in graph.edges() {
7793 let src_plan = if let Some(channel) = &cnx.src_channel {
7794 let key = BridgeChannelKey {
7795 bridge_id: cnx.src.clone(),
7796 channel_id: channel.clone(),
7797 direction: BridgeChannelDirection::Rx,
7798 };
7799 *channel_nodes
7800 .get(&key)
7801 .unwrap_or_else(|| panic!("Bridge source {:?} missing from plan graph", key))
7802 } else {
7803 let node_id = name_to_original
7804 .get(&cnx.src)
7805 .copied()
7806 .unwrap_or_else(|| panic!("Unknown source node '{}'", cnx.src));
7807 *original_to_plan
7808 .get(&node_id)
7809 .unwrap_or_else(|| panic!("Source node '{}' missing from plan", cnx.src))
7810 };
7811
7812 let dst_plan = if let Some(channel) = &cnx.dst_channel {
7813 let key = BridgeChannelKey {
7814 bridge_id: cnx.dst.clone(),
7815 channel_id: channel.clone(),
7816 direction: BridgeChannelDirection::Tx,
7817 };
7818 *channel_nodes
7819 .get(&key)
7820 .unwrap_or_else(|| panic!("Bridge destination {:?} missing from plan graph", key))
7821 } else {
7822 let node_id = name_to_original
7823 .get(&cnx.dst)
7824 .copied()
7825 .unwrap_or_else(|| panic!("Unknown destination node '{}'", cnx.dst));
7826 *original_to_plan
7827 .get(&node_id)
7828 .unwrap_or_else(|| panic!("Destination node '{}' missing from plan", cnx.dst))
7829 };
7830
7831 plan_graph
7832 .connect_ext_with_order(
7833 src_plan,
7834 dst_plan,
7835 &cnx.msg,
7836 cnx.missions.clone(),
7837 None,
7838 None,
7839 cnx.order,
7840 )
7841 .map_err(|e| CuError::from(e.to_string()))?;
7842 }
7843
7844 let runtime_plan = compute_runtime_plan(&plan_graph)?;
7845 Ok((runtime_plan, exec_entities, plan_to_original))
7846}
7847
7848fn collect_culist_metadata(
7849 runtime_plan: &CuExecutionLoop,
7850 exec_entities: &[ExecutionEntity],
7851 bridge_specs: &mut [BridgeSpec],
7852 plan_to_original: &HashMap<NodeId, NodeId>,
7853) -> (Vec<usize>, HashMap<NodeId, usize>) {
7854 let mut culist_order = Vec::new();
7855 let mut node_output_positions = HashMap::new();
7856
7857 for unit in &runtime_plan.steps {
7858 if let CuExecutionUnit::Step(step) = unit
7859 && let Some(output_pack) = &step.output_msg_pack
7860 {
7861 let output_idx = output_pack.culist_index;
7862 culist_order.push(output_idx as usize);
7863 match &exec_entities[step.node_id as usize].kind {
7864 ExecutionEntityKind::Task { .. } => {
7865 if let Some(original_node_id) = plan_to_original.get(&step.node_id) {
7866 node_output_positions.insert(*original_node_id, output_idx as usize);
7867 }
7868 }
7869 ExecutionEntityKind::BridgeRx {
7870 bridge_index,
7871 channel_index,
7872 } => {
7873 bridge_specs[*bridge_index].rx_channels[*channel_index].culist_index =
7874 Some(output_idx as usize);
7875 }
7876 ExecutionEntityKind::BridgeTx {
7877 bridge_index,
7878 channel_index,
7879 } => {
7880 bridge_specs[*bridge_index].tx_channels[*channel_index].culist_index =
7881 Some(output_idx as usize);
7882 }
7883 }
7884 }
7885 }
7886
7887 (culist_order, node_output_positions)
7888}
7889
7890fn build_monitor_culist_component_mapping(
7891 runtime_plan: &CuExecutionLoop,
7892 exec_entities: &[ExecutionEntity],
7893 bridge_specs: &[BridgeSpec],
7894) -> Result<Vec<usize>, String> {
7895 let mut mapping = Vec::new();
7896 for unit in &runtime_plan.steps {
7897 if let CuExecutionUnit::Step(step) = unit
7898 && step.output_msg_pack.is_some()
7899 {
7900 let Some(entity) = exec_entities.get(step.node_id as usize) else {
7901 return Err(format!(
7902 "Missing execution entity for plan node {} while building monitor mapping",
7903 step.node_id
7904 ));
7905 };
7906 let component_index = match &entity.kind {
7907 ExecutionEntityKind::Task { task_index } => *task_index,
7908 ExecutionEntityKind::BridgeRx {
7909 bridge_index,
7910 channel_index,
7911 } => bridge_specs
7912 .get(*bridge_index)
7913 .and_then(|spec| spec.rx_channels.get(*channel_index))
7914 .and_then(|channel| channel.monitor_index)
7915 .ok_or_else(|| {
7916 format!(
7917 "Missing monitor index for bridge rx {}:{}",
7918 bridge_index, channel_index
7919 )
7920 })?,
7921 ExecutionEntityKind::BridgeTx {
7922 bridge_index,
7923 channel_index,
7924 } => bridge_specs
7925 .get(*bridge_index)
7926 .and_then(|spec| spec.tx_channels.get(*channel_index))
7927 .and_then(|channel| channel.monitor_index)
7928 .ok_or_else(|| {
7929 format!(
7930 "Missing monitor index for bridge tx {}:{}",
7931 bridge_index, channel_index
7932 )
7933 })?,
7934 };
7935 mapping.push(component_index);
7936 }
7937 }
7938 Ok(mapping)
7939}
7940
7941fn build_parallel_rt_stage_entries(
7942 runtime_plan: &CuExecutionLoop,
7943 exec_entities: &[ExecutionEntity],
7944 task_specs: &CuTaskSpecSet,
7945 bridge_specs: &[BridgeSpec],
7946) -> CuResult<Vec<proc_macro2::TokenStream>> {
7947 let mut entries = Vec::new();
7948
7949 for unit in &runtime_plan.steps {
7950 let CuExecutionUnit::Step(step) = unit else {
7951 todo!("parallel runtime metadata for nested loops is not implemented yet")
7952 };
7953
7954 let entity = exec_entities.get(step.node_id as usize).ok_or_else(|| {
7955 CuError::from(format!(
7956 "Missing execution entity for runtime plan node {} while building parallel runtime metadata",
7957 step.node_id
7958 ))
7959 })?;
7960
7961 let (label, kind_tokens, component_index) = match &entity.kind {
7962 ExecutionEntityKind::Task { task_index } => (
7963 task_specs
7964 .ids
7965 .get(*task_index)
7966 .cloned()
7967 .ok_or_else(|| {
7968 CuError::from(format!(
7969 "Missing task id for task index {} while building parallel runtime metadata",
7970 task_index
7971 ))
7972 })?,
7973 quote! { cu29::parallel_rt::ParallelRtStageKind::Task },
7974 *task_index,
7975 ),
7976 ExecutionEntityKind::BridgeRx {
7977 bridge_index,
7978 channel_index,
7979 } => {
7980 let bridge = bridge_specs.get(*bridge_index).ok_or_else(|| {
7981 CuError::from(format!(
7982 "Missing bridge spec {} while building parallel runtime metadata",
7983 bridge_index
7984 ))
7985 })?;
7986 let channel = bridge.rx_channels.get(*channel_index).ok_or_else(|| {
7987 CuError::from(format!(
7988 "Missing bridge rx channel {}:{} while building parallel runtime metadata",
7989 bridge_index, channel_index
7990 ))
7991 })?;
7992 let component_index = channel.monitor_index.ok_or_else(|| {
7993 CuError::from(format!(
7994 "Missing monitor index for bridge rx {}:{} while building parallel runtime metadata",
7995 bridge_index, channel_index
7996 ))
7997 })?;
7998 (
7999 format!("bridge::{}::rx::{}", bridge.id, channel.id),
8000 quote! { cu29::parallel_rt::ParallelRtStageKind::BridgeRx },
8001 component_index,
8002 )
8003 }
8004 ExecutionEntityKind::BridgeTx {
8005 bridge_index,
8006 channel_index,
8007 } => {
8008 let bridge = bridge_specs.get(*bridge_index).ok_or_else(|| {
8009 CuError::from(format!(
8010 "Missing bridge spec {} while building parallel runtime metadata",
8011 bridge_index
8012 ))
8013 })?;
8014 let channel = bridge.tx_channels.get(*channel_index).ok_or_else(|| {
8015 CuError::from(format!(
8016 "Missing bridge tx channel {}:{} while building parallel runtime metadata",
8017 bridge_index, channel_index
8018 ))
8019 })?;
8020 let component_index = channel.monitor_index.ok_or_else(|| {
8021 CuError::from(format!(
8022 "Missing monitor index for bridge tx {}:{} while building parallel runtime metadata",
8023 bridge_index, channel_index
8024 ))
8025 })?;
8026 (
8027 format!("bridge::{}::tx::{}", bridge.id, channel.id),
8028 quote! { cu29::parallel_rt::ParallelRtStageKind::BridgeTx },
8029 component_index,
8030 )
8031 }
8032 };
8033
8034 let node_id = step.node_id;
8035 entries.push(quote! {
8036 cu29::parallel_rt::ParallelRtStageMetadata::new(
8037 #label,
8038 #kind_tokens,
8039 #node_id,
8040 cu29::monitoring::ComponentId::new(#component_index),
8041 )
8042 });
8043 }
8044
8045 Ok(entries)
8046}
8047
8048#[allow(dead_code)]
8049fn build_monitored_ids(task_ids: &[String], bridge_specs: &mut [BridgeSpec]) -> Vec<String> {
8050 let mut names = task_ids.to_vec();
8051 for spec in bridge_specs.iter_mut() {
8052 spec.monitor_index = Some(names.len());
8053 names.push(format!("bridge::{}", spec.id));
8054 for channel in spec.rx_channels.iter_mut() {
8055 channel.monitor_index = Some(names.len());
8056 names.push(format!("bridge::{}::rx::{}", spec.id, channel.id));
8057 }
8058 for channel in spec.tx_channels.iter_mut() {
8059 channel.monitor_index = Some(names.len());
8060 names.push(format!("bridge::{}::tx::{}", spec.id, channel.id));
8061 }
8062 }
8063 names
8064}
8065
8066fn wrap_process_step_tokens(
8067 wrap_process_step: bool,
8068 body: proc_macro2::TokenStream,
8069) -> proc_macro2::TokenStream {
8070 if wrap_process_step {
8071 quote! {{
8072 let __cu_process_step_result: cu29::curuntime::ProcessStepResult = (|| {
8073 #body
8074 Ok(cu29::curuntime::ProcessStepOutcome::Continue)
8075 })();
8076 __cu_process_step_result
8077 }}
8078 } else {
8079 body
8080 }
8081}
8082
8083fn abort_process_step_tokens(wrap_process_step: bool) -> proc_macro2::TokenStream {
8084 if wrap_process_step {
8085 quote! {
8086 return Ok(cu29::curuntime::ProcessStepOutcome::AbortCopperList);
8087 }
8088 } else {
8089 quote! {
8090 __cu_abort_copperlist = true;
8091 break '__cu_process_steps;
8092 }
8093 }
8094}
8095
8096fn parallel_task_lifecycle_tokens(
8097 task_kind: CuTaskType,
8098 task_type: &Type,
8099 component_index: usize,
8100 mission_mod: &Ident,
8101 task_instance: &proc_macro2::TokenStream,
8102 placement: ParallelLifecyclePlacement,
8103) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
8104 let rt_guard = rtsan_guard_tokens();
8105 let abort_process_step = abort_process_step_tokens(true);
8106 let task_trait = match task_kind {
8107 CuTaskType::Source => quote! { cu29::cutask::CuSrcTask },
8108 CuTaskType::Sink => quote! { cu29::cutask::CuSinkTask },
8109 CuTaskType::Regular => quote! { cu29::cutask::CuTask },
8110 };
8111
8112 let preprocess_alloc_open = alloc_scope_open_tokens();
8113 let preprocess_alloc_close = alloc_scope_close_tokens(
8114 quote! { monitor },
8115 quote! { #component_index },
8116 quote! { CuComponentState::Preprocess },
8117 );
8118 let postprocess_alloc_open = alloc_scope_open_tokens();
8119 let postprocess_alloc_close = alloc_scope_close_tokens(
8120 quote! { monitor },
8121 quote! { #component_index },
8122 quote! { CuComponentState::Postprocess },
8123 );
8124 let preprocess = if placement.preprocess {
8125 quote! {
8126 execution_probe.record(cu29::monitoring::ExecutionMarker {
8127 component_id: cu29::monitoring::ComponentId::new(#component_index),
8128 step: CuComponentState::Preprocess,
8129 culistid: Some(clid),
8130 });
8131 ctx.set_current_task(#component_index);
8132 #preprocess_alloc_open
8133 let maybe_error = {
8134 #rt_guard
8135 <#task_type as #task_trait>::preprocess(&mut #task_instance, &ctx)
8136 };
8137 #preprocess_alloc_close
8138 if let Err(error) = maybe_error {
8139 let decision = monitor.process_error(
8140 cu29::monitoring::ComponentId::new(#component_index),
8141 CuComponentState::Preprocess,
8142 &error,
8143 );
8144 match decision {
8145 Decision::Abort => {
8146 debug!(ctx,
8147 "Preprocess: ABORT decision from monitoring. Component '{}' errored out during preprocess. Aborting CopperList {}.",
8148 #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index)),
8149 clid
8150 );
8151 #abort_process_step
8152 }
8153 Decision::Ignore => {
8154 debug!(ctx,
8155 "Preprocess: IGNORE decision from monitoring. Component '{}' errored out during preprocess. The runtime will continue.",
8156 #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index))
8157 );
8158 }
8159 Decision::Shutdown => {
8160 debug!(ctx,
8161 "Preprocess: SHUTDOWN decision from monitoring. Component '{}' errored out during preprocess. The runtime cannot continue.",
8162 #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index))
8163 );
8164 return Err(CuError::new_with_cause(
8165 "Component errored out during preprocess.",
8166 error,
8167 ));
8168 }
8169 }
8170 }
8171 }
8172 } else {
8173 quote! {}
8174 };
8175
8176 let postprocess = if placement.postprocess {
8177 quote! {
8178 execution_probe.record(cu29::monitoring::ExecutionMarker {
8179 component_id: cu29::monitoring::ComponentId::new(#component_index),
8180 step: CuComponentState::Postprocess,
8181 culistid: Some(clid),
8182 });
8183 ctx.set_current_task(#component_index);
8184 #postprocess_alloc_open
8185 let maybe_error = {
8186 #rt_guard
8187 <#task_type as #task_trait>::postprocess(&mut #task_instance, &ctx)
8188 };
8189 #postprocess_alloc_close
8190 if let Err(error) = maybe_error {
8191 let decision = monitor.process_error(
8192 cu29::monitoring::ComponentId::new(#component_index),
8193 CuComponentState::Postprocess,
8194 &error,
8195 );
8196 match decision {
8197 Decision::Abort => {
8198 debug!(ctx,
8199 "Postprocess: ABORT decision from monitoring. Component '{}' errored out during postprocess. Continuing with the completed CopperList.",
8200 #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index))
8201 );
8202 }
8203 Decision::Ignore => {
8204 debug!(ctx,
8205 "Postprocess: IGNORE decision from monitoring. Component '{}' errored out during postprocess. The runtime will continue.",
8206 #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index))
8207 );
8208 }
8209 Decision::Shutdown => {
8210 debug!(ctx,
8211 "Postprocess: SHUTDOWN decision from monitoring. Component '{}' errored out during postprocess. The runtime cannot continue.",
8212 #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index))
8213 );
8214 return Err(CuError::new_with_cause(
8215 "Component errored out during postprocess.",
8216 error,
8217 ));
8218 }
8219 }
8220 }
8221 }
8222 } else {
8223 quote! {}
8224 };
8225
8226 (preprocess, postprocess)
8227}
8228
8229fn parallel_bridge_lifecycle_tokens(
8230 bridge_type: &Type,
8231 component_index: usize,
8232 mission_mod: &Ident,
8233 placement: ParallelLifecyclePlacement,
8234) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
8235 let rt_guard = rtsan_guard_tokens();
8236 let abort_process_step = abort_process_step_tokens(true);
8237
8238 let preprocess_alloc_open = alloc_scope_open_tokens();
8239 let preprocess_alloc_close = alloc_scope_close_tokens(
8240 quote! { monitor },
8241 quote! { #component_index },
8242 quote! { CuComponentState::Preprocess },
8243 );
8244 let postprocess_alloc_open = alloc_scope_open_tokens();
8245 let postprocess_alloc_close = alloc_scope_close_tokens(
8246 quote! { monitor },
8247 quote! { #component_index },
8248 quote! { CuComponentState::Postprocess },
8249 );
8250 let preprocess = if placement.preprocess {
8251 quote! {
8252 execution_probe.record(cu29::monitoring::ExecutionMarker {
8253 component_id: cu29::monitoring::ComponentId::new(#component_index),
8254 step: CuComponentState::Preprocess,
8255 culistid: Some(clid),
8256 });
8257 ctx.set_current_component(#component_index);
8258 ctx.clear_current_task();
8259 #preprocess_alloc_open
8260 let maybe_error = {
8261 #rt_guard
8262 <#bridge_type as cu29::cubridge::CuBridge>::preprocess(bridge, &ctx)
8263 };
8264 #preprocess_alloc_close
8265 if let Err(error) = maybe_error {
8266 let decision = monitor.process_error(
8267 cu29::monitoring::ComponentId::new(#component_index),
8268 CuComponentState::Preprocess,
8269 &error,
8270 );
8271 match decision {
8272 Decision::Abort => {
8273 debug!(ctx,
8274 "Preprocess: ABORT decision from monitoring. Component '{}' errored out during preprocess. Aborting CopperList {}.",
8275 #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index)),
8276 clid
8277 );
8278 #abort_process_step
8279 }
8280 Decision::Ignore => {
8281 debug!(ctx,
8282 "Preprocess: IGNORE decision from monitoring. Component '{}' errored out during preprocess. The runtime will continue.",
8283 #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index))
8284 );
8285 }
8286 Decision::Shutdown => {
8287 debug!(ctx,
8288 "Preprocess: SHUTDOWN decision from monitoring. Component '{}' errored out during preprocess. The runtime cannot continue.",
8289 #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index))
8290 );
8291 return Err(CuError::new_with_cause(
8292 "Component errored out during preprocess.",
8293 error,
8294 ));
8295 }
8296 }
8297 }
8298 }
8299 } else {
8300 quote! {}
8301 };
8302
8303 let postprocess = if placement.postprocess {
8304 quote! {
8305 kf_manager.freeze_any(clid, bridge)?;
8306 execution_probe.record(cu29::monitoring::ExecutionMarker {
8307 component_id: cu29::monitoring::ComponentId::new(#component_index),
8308 step: CuComponentState::Postprocess,
8309 culistid: Some(clid),
8310 });
8311 ctx.set_current_component(#component_index);
8312 ctx.clear_current_task();
8313 #postprocess_alloc_open
8314 let maybe_error = {
8315 #rt_guard
8316 <#bridge_type as cu29::cubridge::CuBridge>::postprocess(bridge, &ctx)
8317 };
8318 #postprocess_alloc_close
8319 if let Err(error) = maybe_error {
8320 let decision = monitor.process_error(
8321 cu29::monitoring::ComponentId::new(#component_index),
8322 CuComponentState::Postprocess,
8323 &error,
8324 );
8325 match decision {
8326 Decision::Abort => {
8327 debug!(ctx,
8328 "Postprocess: ABORT decision from monitoring. Component '{}' errored out during postprocess. Continuing with the completed CopperList.",
8329 #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index))
8330 );
8331 }
8332 Decision::Ignore => {
8333 debug!(ctx,
8334 "Postprocess: IGNORE decision from monitoring. Component '{}' errored out during postprocess. The runtime will continue.",
8335 #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index))
8336 );
8337 }
8338 Decision::Shutdown => {
8339 debug!(ctx,
8340 "Postprocess: SHUTDOWN decision from monitoring. Component '{}' errored out during postprocess. The runtime cannot continue.",
8341 #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index))
8342 );
8343 return Err(CuError::new_with_cause(
8344 "Component errored out during postprocess.",
8345 error,
8346 ));
8347 }
8348 }
8349 }
8350 }
8351 } else {
8352 quote! {}
8353 };
8354
8355 (preprocess, postprocess)
8356}
8357
8358#[derive(Clone, Copy)]
8359struct StepGenerationContext<'a> {
8360 output_pack_sizes: &'a [usize],
8361 task_input_layouts: &'a HashMap<String, TaskInputLayout>,
8362 mission_name: &'a str,
8363 sim_mode: bool,
8364 mission_mod: &'a Ident,
8365 lifecycle_placement: ParallelLifecyclePlacement,
8366 wrap_process_step: bool,
8367}
8368
8369impl<'a> StepGenerationContext<'a> {
8370 fn new(
8371 output_pack_sizes: &'a [usize],
8372 task_input_layouts: &'a HashMap<String, TaskInputLayout>,
8373 mission_name: &'a str,
8374 sim_mode: bool,
8375 mission_mod: &'a Ident,
8376 lifecycle_placement: ParallelLifecyclePlacement,
8377 wrap_process_step: bool,
8378 ) -> Self {
8379 Self {
8380 output_pack_sizes,
8381 task_input_layouts,
8382 mission_name,
8383 sim_mode,
8384 mission_mod,
8385 lifecycle_placement,
8386 wrap_process_step,
8387 }
8388 }
8389}
8390
8391struct TaskExecutionTokens {
8392 setup: proc_macro2::TokenStream,
8393 instance: proc_macro2::TokenStream,
8394}
8395
8396impl TaskExecutionTokens {
8397 fn new(setup: proc_macro2::TokenStream, instance: proc_macro2::TokenStream) -> Self {
8398 Self { setup, instance }
8399 }
8400}
8401
8402fn generate_task_execution_tokens(
8403 step: &CuExecutionStep,
8404 task_index: usize,
8405 task_specs: &CuTaskSpecSet,
8406 runtime_task_type: &Type,
8407 ctx: StepGenerationContext<'_>,
8408 task_tokens: TaskExecutionTokens,
8409) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
8410 let StepGenerationContext {
8411 output_pack_sizes,
8412 task_input_layouts,
8413 mission_name,
8414 sim_mode,
8415 mission_mod,
8416 lifecycle_placement,
8417 wrap_process_step,
8418 } = ctx;
8419 let TaskExecutionTokens {
8420 setup: task_setup,
8421 instance: task_instance,
8422 } = task_tokens;
8423 let abort_process_step = abort_process_step_tokens(wrap_process_step);
8424 let comment_str = format!(
8425 "DEBUG ->> {} ({:?}) Id:{} I:{:?} O:{:?}",
8426 step.node.get_id(),
8427 step.task_type,
8428 step.node_id,
8429 step.input_msg_indices_types,
8430 step.output_msg_pack
8431 );
8432 let comment_tokens = quote! {{
8433 let _ = stringify!(#comment_str);
8434 }};
8435 let tid = task_index;
8436 let task_enum_name = config_id_to_enum(&task_specs.ids[tid]);
8437 let enum_name = Ident::new(&task_enum_name, Span::call_site());
8438 let task_hint = config_id_to_struct_member(&task_specs.ids[tid]);
8439 let source_slot_match_trait_ident = format_ident!(
8440 "__CuOutputSlotMustMatchTaskOutput__Task_{}__Add_dst___nc___connections_for_unused_outputs",
8441 task_hint
8442 );
8443 let source_slot_match_fn_ident = format_ident!(
8444 "__cu_source_output_slot_or_add_dst___nc___for_unused_outputs__task_{}",
8445 task_hint
8446 );
8447 let regular_slot_match_trait_ident = format_ident!(
8448 "__CuOutputSlotMustMatchTaskOutput__Task_{}__Add_dst___nc___connections_for_unused_outputs",
8449 task_hint
8450 );
8451 let regular_slot_match_fn_ident = format_ident!(
8452 "__cu_task_output_slot_or_add_dst___nc___for_unused_outputs__task_{}",
8453 task_hint
8454 );
8455 let rt_guard = rtsan_guard_tokens();
8456 let run_in_sim_flag = task_specs.run_in_sim_flags[tid];
8457 let (parallel_task_preprocess, parallel_task_postprocess) = parallel_task_lifecycle_tokens(
8458 step.task_type,
8459 runtime_task_type,
8460 tid,
8461 mission_mod,
8462 &task_instance,
8463 lifecycle_placement,
8464 );
8465 let maybe_sim_tick = if sim_mode && !run_in_sim_flag {
8466 quote! {
8467 if !doit {
8468 #task_instance.sim_tick();
8469 }
8470 }
8471 } else {
8472 quote!()
8473 };
8474
8475 let output_pack = step
8476 .output_msg_pack
8477 .as_ref()
8478 .expect("Task should have an output message pack.");
8479 let output_culist_index = int2sliceindex(output_pack.culist_index);
8480 let output_ports: Vec<syn::Index> = (0..output_pack.msg_types.len())
8481 .map(syn::Index::from)
8482 .collect();
8483 let output_clear_payload = if output_ports.len() == 1 {
8484 quote! { cumsg_output.clear_payload(); }
8485 } else {
8486 quote! { #(cumsg_output.#output_ports.clear_payload();)* }
8487 };
8488 let output_start_time = if output_ports.len() == 1 {
8489 quote! {
8490 if cumsg_output.metadata.process_time.start.is_none() {
8491 cumsg_output.metadata.process_time.start = cu29::curuntime::perf_now(clock).into();
8492 }
8493 }
8494 } else {
8495 quote! {
8496 let start_time = cu29::curuntime::perf_now(clock).into();
8497 #( if cumsg_output.#output_ports.metadata.process_time.start.is_none() {
8498 cumsg_output.#output_ports.metadata.process_time.start = start_time;
8499 } )*
8500 }
8501 };
8502 let output_end_time = if output_ports.len() == 1 {
8503 quote! {
8504 if cumsg_output.metadata.process_time.end.is_none() {
8505 cumsg_output.metadata.process_time.end = cu29::curuntime::perf_now(clock).into();
8506 }
8507 }
8508 } else {
8509 quote! {
8510 let end_time = cu29::curuntime::perf_now(clock).into();
8511 #( if cumsg_output.#output_ports.metadata.process_time.end.is_none() {
8512 cumsg_output.#output_ports.metadata.process_time.end = end_time;
8513 } )*
8514 }
8515 };
8516
8517 match step.task_type {
8518 CuTaskType::Source => {
8519 let monitoring_action = quote! {
8520 debug!(ctx, "Component {}: Error during process: {}", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#tid)), &error);
8521 let decision = monitor.process_error(cu29::monitoring::ComponentId::new(#tid), CuComponentState::Process, &error);
8522 match decision {
8523 Decision::Abort => {
8524 debug!(ctx, "Process: ABORT decision from monitoring. Component '{}' errored out \
8525 during process. Skipping the processing of CL {}.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#tid)), clid);
8526 #abort_process_step
8527 }
8528 Decision::Ignore => {
8529 debug!(ctx, "Process: IGNORE decision from monitoring. Component '{}' errored out \
8530 during process. The runtime will continue with a forced empty message.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#tid)));
8531 let cumsg_output = &mut msgs.#output_culist_index;
8532 #output_clear_payload
8533 }
8534 Decision::Shutdown => {
8535 debug!(ctx, "Process: SHUTDOWN decision from monitoring. Component '{}' errored out \
8536 during process. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#tid)));
8537 return Err(CuError::new_with_cause("Component errored out during process.", error));
8538 }
8539 }
8540 };
8541
8542 let call_sim_callback = if sim_mode {
8543 quote! {
8544 let doit = {
8545 let cumsg_output = &mut msgs.#output_culist_index;
8546 let state = CuTaskCallbackState::Process((), cumsg_output);
8547 let ovr = sim_callback(SimStep::#enum_name(state));
8548
8549 if let SimOverride::Errored(reason) = ovr {
8550 let error: CuError = reason.into();
8551 #monitoring_action
8552 false
8553 } else {
8554 ovr == SimOverride::ExecuteByRuntime
8555 }
8556 };
8557 }
8558 } else {
8559 quote! { let doit = true; }
8560 };
8561
8562 let logging_tokens = if !task_specs.logging_enabled[tid] {
8563 quote! {
8564 let mut cumsg_output = &mut culist.msgs.0.#output_culist_index;
8565 #output_clear_payload
8566 }
8567 } else {
8568 quote!()
8569 };
8570 let alloc_open = alloc_scope_open_tokens();
8571 let alloc_close = alloc_scope_close_tokens(
8572 quote! { monitor },
8573 quote! { #tid },
8574 quote! { CuComponentState::Process },
8575 );
8576 let source_process_tokens = quote! {
8577 #[allow(non_camel_case_types)]
8578 trait #source_slot_match_trait_ident<Expected> {
8579 fn __cu_cast_output_slot(slot: &mut Self) -> &mut Expected;
8580 }
8581 impl<T> #source_slot_match_trait_ident<T> for T {
8582 fn __cu_cast_output_slot(slot: &mut Self) -> &mut T {
8583 slot
8584 }
8585 }
8586
8587 fn #source_slot_match_fn_ident<'a, Task, Slot>(
8588 _task: &Task,
8589 slot: &'a mut Slot,
8590 ) -> &'a mut Task::Output<'static>
8591 where
8592 Task: cu29::cutask::CuSrcTask,
8593 Slot: #source_slot_match_trait_ident<Task::Output<'static>>,
8594 {
8595 <Slot as #source_slot_match_trait_ident<Task::Output<'static>>>::__cu_cast_output_slot(slot)
8596 }
8597
8598 #output_start_time
8599 #alloc_open
8600 let result = {
8601 let cumsg_output = #source_slot_match_fn_ident::<
8602 _,
8603 _,
8604 >(&#task_instance, cumsg_output);
8605 #rt_guard
8606 ctx.set_current_task(#tid);
8607 #task_instance.process(&ctx, cumsg_output)
8608 };
8609 #output_end_time
8610 #alloc_close
8611 result
8612 };
8613
8614 (
8615 wrap_process_step_tokens(
8616 wrap_process_step,
8617 quote! {
8618 #task_setup
8619 #parallel_task_preprocess
8620 #comment_tokens
8621 kf_manager.freeze_task(clid, &#task_instance)?;
8622 #call_sim_callback
8623 let cumsg_output = &mut msgs.#output_culist_index;
8624 #maybe_sim_tick
8625 let maybe_error = if doit {
8626 execution_probe.record(cu29::monitoring::ExecutionMarker {
8627 component_id: cu29::monitoring::ComponentId::new(#tid),
8628 step: CuComponentState::Process,
8629 culistid: Some(clid),
8630 });
8631 #source_process_tokens
8632 } else {
8633 Ok(())
8634 };
8635 if let Err(error) = maybe_error {
8636 #monitoring_action
8637 }
8638 #parallel_task_postprocess
8639 },
8640 ),
8641 logging_tokens,
8642 )
8643 }
8644 CuTaskType::Sink => {
8645 let GeneratedTaskInput {
8646 setup: task_input_setup,
8647 expr: task_input_expr,
8648 } = generate_task_input_binding(
8649 step,
8650 mission_name,
8651 output_pack_sizes,
8652 task_input_layouts,
8653 );
8654
8655 let monitoring_action = quote! {
8656 debug!(ctx, "Component {}: Error during process: {}", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#tid)), &error);
8657 let decision = monitor.process_error(cu29::monitoring::ComponentId::new(#tid), CuComponentState::Process, &error);
8658 match decision {
8659 Decision::Abort => {
8660 debug!(ctx, "Process: ABORT decision from monitoring. Component '{}' errored out \
8661 during process. Skipping the processing of CL {}.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#tid)), clid);
8662 #abort_process_step
8663 }
8664 Decision::Ignore => {
8665 debug!(ctx, "Process: IGNORE decision from monitoring. Component '{}' errored out \
8666 during process. The runtime will continue with a forced empty message.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#tid)));
8667 let cumsg_output = &mut msgs.#output_culist_index;
8668 #output_clear_payload
8669 }
8670 Decision::Shutdown => {
8671 debug!(ctx, "Process: SHUTDOWN decision from monitoring. Component '{}' errored out \
8672 during process. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#tid)));
8673 return Err(CuError::new_with_cause("Component errored out during process.", error));
8674 }
8675 }
8676 };
8677
8678 let call_sim_callback = if sim_mode {
8679 quote! {
8680 let doit = {
8681 let cumsg_input = #task_input_expr;
8682 let cumsg_output = &mut msgs.#output_culist_index;
8683 let state = CuTaskCallbackState::Process(cumsg_input, cumsg_output);
8684 let ovr = sim_callback(SimStep::#enum_name(state));
8685
8686 if let SimOverride::Errored(reason) = ovr {
8687 let error: CuError = reason.into();
8688 #monitoring_action
8689 false
8690 } else {
8691 ovr == SimOverride::ExecuteByRuntime
8692 }
8693 };
8694 }
8695 } else {
8696 quote! { let doit = true; }
8697 };
8698
8699 let alloc_open = alloc_scope_open_tokens();
8700 let alloc_close = alloc_scope_close_tokens(
8701 quote! { monitor },
8702 quote! { #tid },
8703 quote! { CuComponentState::Process },
8704 );
8705 (
8706 wrap_process_step_tokens(
8707 wrap_process_step,
8708 quote! {
8709 #task_setup
8710 #parallel_task_preprocess
8711 #comment_tokens
8712 kf_manager.freeze_task(clid, &#task_instance)?;
8713 #task_input_setup
8714 #call_sim_callback
8715 let cumsg_input = #task_input_expr;
8716 let cumsg_output = &mut msgs.#output_culist_index;
8717 let maybe_error = if doit {
8718 execution_probe.record(cu29::monitoring::ExecutionMarker {
8719 component_id: cu29::monitoring::ComponentId::new(#tid),
8720 step: CuComponentState::Process,
8721 culistid: Some(clid),
8722 });
8723 #output_start_time
8724 #alloc_open
8725 let result = {
8726 #rt_guard
8727 ctx.set_current_task(#tid);
8728 #task_instance.process(&ctx, cumsg_input)
8729 };
8730 #output_end_time
8731 #alloc_close
8732 result
8733 } else {
8734 Ok(())
8735 };
8736 if let Err(error) = maybe_error {
8737 #monitoring_action
8738 }
8739 #parallel_task_postprocess
8740 },
8741 ),
8742 quote! {},
8743 )
8744 }
8745 CuTaskType::Regular => {
8746 let GeneratedTaskInput {
8747 setup: task_input_setup,
8748 expr: task_input_expr,
8749 } = generate_task_input_binding(
8750 step,
8751 mission_name,
8752 output_pack_sizes,
8753 task_input_layouts,
8754 );
8755
8756 let monitoring_action = quote! {
8757 debug!(ctx, "Component {}: Error during process: {}", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#tid)), &error);
8758 let decision = monitor.process_error(cu29::monitoring::ComponentId::new(#tid), CuComponentState::Process, &error);
8759 match decision {
8760 Decision::Abort => {
8761 debug!(ctx, "Process: ABORT decision from monitoring. Component '{}' errored out \
8762 during process. Skipping the processing of CL {}.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#tid)), clid);
8763 #abort_process_step
8764 }
8765 Decision::Ignore => {
8766 debug!(ctx, "Process: IGNORE decision from monitoring. Component '{}' errored out \
8767 during process. The runtime will continue with a forced empty message.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#tid)));
8768 let cumsg_output = &mut msgs.#output_culist_index;
8769 #output_clear_payload
8770 }
8771 Decision::Shutdown => {
8772 debug!(ctx, "Process: SHUTDOWN decision from monitoring. Component '{}' errored out \
8773 during process. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#tid)));
8774 return Err(CuError::new_with_cause("Component errored out during process.", error));
8775 }
8776 }
8777 };
8778
8779 let call_sim_callback = if sim_mode {
8780 quote! {
8781 let doit = {
8782 let cumsg_input = #task_input_expr;
8783 let cumsg_output = &mut msgs.#output_culist_index;
8784 let state = CuTaskCallbackState::Process(cumsg_input, cumsg_output);
8785 let ovr = sim_callback(SimStep::#enum_name(state));
8786
8787 if let SimOverride::Errored(reason) = ovr {
8788 let error: CuError = reason.into();
8789 #monitoring_action
8790 false
8791 }
8792 else {
8793 ovr == SimOverride::ExecuteByRuntime
8794 }
8795 };
8796 }
8797 } else {
8798 quote! { let doit = true; }
8799 };
8800
8801 let logging_tokens = if !task_specs.logging_enabled[tid] {
8802 quote! {
8803 let mut cumsg_output = &mut culist.msgs.0.#output_culist_index;
8804 #output_clear_payload
8805 }
8806 } else {
8807 quote!()
8808 };
8809 let alloc_open = alloc_scope_open_tokens();
8810 let alloc_close = alloc_scope_close_tokens(
8811 quote! { monitor },
8812 quote! { #tid },
8813 quote! { CuComponentState::Process },
8814 );
8815 let regular_process_tokens = quote! {
8816 #[allow(non_camel_case_types)]
8817 trait #regular_slot_match_trait_ident<Expected> {
8818 fn __cu_cast_output_slot(slot: &mut Self) -> &mut Expected;
8819 }
8820 impl<T> #regular_slot_match_trait_ident<T> for T {
8821 fn __cu_cast_output_slot(slot: &mut Self) -> &mut T {
8822 slot
8823 }
8824 }
8825
8826 fn #regular_slot_match_fn_ident<'a, Task, Slot>(
8827 _task: &Task,
8828 slot: &'a mut Slot,
8829 ) -> &'a mut Task::Output<'static>
8830 where
8831 Task: cu29::cutask::CuTask,
8832 Slot: #regular_slot_match_trait_ident<Task::Output<'static>>,
8833 {
8834 <Slot as #regular_slot_match_trait_ident<Task::Output<'static>>>::__cu_cast_output_slot(slot)
8835 }
8836
8837 #output_start_time
8838 #alloc_open
8839 let result = {
8840 let cumsg_output = #regular_slot_match_fn_ident::<
8841 _,
8842 _,
8843 >(&#task_instance, cumsg_output);
8844 #rt_guard
8845 ctx.set_current_task(#tid);
8846 #task_instance.process(&ctx, cumsg_input, cumsg_output)
8847 };
8848 #output_end_time
8849 #alloc_close
8850 result
8851 };
8852
8853 (
8854 wrap_process_step_tokens(
8855 wrap_process_step,
8856 quote! {
8857 #task_setup
8858 #parallel_task_preprocess
8859 #comment_tokens
8860 kf_manager.freeze_task(clid, &#task_instance)?;
8861 #task_input_setup
8862 #call_sim_callback
8863 let cumsg_input = #task_input_expr;
8864 let cumsg_output = &mut msgs.#output_culist_index;
8865 let maybe_error = if doit {
8866 execution_probe.record(cu29::monitoring::ExecutionMarker {
8867 component_id: cu29::monitoring::ComponentId::new(#tid),
8868 step: CuComponentState::Process,
8869 culistid: Some(clid),
8870 });
8871 #regular_process_tokens
8872 } else {
8873 Ok(())
8874 };
8875 if let Err(error) = maybe_error {
8876 #monitoring_action
8877 }
8878 #parallel_task_postprocess
8879 },
8880 ),
8881 logging_tokens,
8882 )
8883 }
8884 }
8885}
8886
8887fn generate_bridge_rx_execution_tokens(
8888 step: &CuExecutionStep,
8889 bridge_spec: &BridgeSpec,
8890 channel_index: usize,
8891 ctx: StepGenerationContext<'_>,
8892 bridge_setup: proc_macro2::TokenStream,
8893) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
8894 let StepGenerationContext {
8895 output_pack_sizes: _,
8896 task_input_layouts: _,
8897 mission_name: _,
8898 sim_mode,
8899 mission_mod,
8900 lifecycle_placement,
8901 wrap_process_step,
8902 } = ctx;
8903 let rt_guard = rtsan_guard_tokens();
8904 let abort_process_step = abort_process_step_tokens(wrap_process_step);
8905 let channel = &bridge_spec.rx_channels[channel_index];
8906 let output_pack = step
8907 .output_msg_pack
8908 .as_ref()
8909 .expect("Bridge Rx channel missing output pack");
8910 let port_index = output_pack
8911 .msg_types
8912 .iter()
8913 .position(|msg| msg == &channel.msg_type_name)
8914 .unwrap_or_else(|| {
8915 panic!(
8916 "Bridge Rx channel '{}' missing output port for '{}'",
8917 channel.id, channel.msg_type_name
8918 )
8919 });
8920 let culist_index_ts = int2sliceindex(output_pack.culist_index);
8921 let output_ref = if output_pack.msg_types.len() == 1 {
8922 quote! { &mut msgs.#culist_index_ts }
8923 } else {
8924 let port_index = syn::Index::from(port_index);
8925 quote! { &mut msgs.#culist_index_ts.#port_index }
8926 };
8927 let monitor_index = syn::Index::from(
8928 channel
8929 .monitor_index
8930 .expect("Bridge Rx channel missing monitor index"),
8931 );
8932 let bridge_type = runtime_bridge_type_for_spec(bridge_spec, sim_mode);
8933 let (parallel_bridge_preprocess, parallel_bridge_postprocess) =
8934 parallel_bridge_lifecycle_tokens(
8935 &bridge_type,
8936 bridge_spec
8937 .monitor_index
8938 .expect("Bridge missing monitor index for lifecycle"),
8939 mission_mod,
8940 lifecycle_placement,
8941 );
8942 let const_ident = &channel.const_ident;
8943 let enum_ident = Ident::new(
8944 &config_id_to_enum(&format!("{}_rx_{}", bridge_spec.id, channel.id)),
8945 Span::call_site(),
8946 );
8947
8948 let call_sim_callback = if sim_mode {
8949 quote! {
8950 let doit = {
8951 let state = SimStep::#enum_ident {
8952 channel: &<#bridge_type as cu29::cubridge::CuBridge>::Rx::#const_ident,
8953 msg: cumsg_output,
8954 };
8955 let ovr = sim_callback(state);
8956 if let SimOverride::Errored(reason) = ovr {
8957 let error: CuError = reason.into();
8958 let decision = monitor.process_error(cu29::monitoring::ComponentId::new(#monitor_index), CuComponentState::Process, &error);
8959 match decision {
8960 Decision::Abort => {
8961 debug!(ctx, "Process: ABORT decision from monitoring. Component '{}' errored out during process. Skipping the processing of CL {}.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)), clid);
8962 #abort_process_step
8963 }
8964 Decision::Ignore => {
8965 debug!(ctx, "Process: IGNORE decision from monitoring. Component '{}' errored out during process. The runtime will continue with a forced empty message.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
8966 cumsg_output.clear_payload();
8967 false
8968 }
8969 Decision::Shutdown => {
8970 debug!(ctx, "Process: SHUTDOWN decision from monitoring. Component '{}' errored out during process. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
8971 return Err(CuError::new_with_cause("Component errored out during process.", error));
8972 }
8973 }
8974 } else {
8975 ovr == SimOverride::ExecuteByRuntime
8976 }
8977 };
8978 }
8979 } else {
8980 quote! { let doit = true; }
8981 };
8982 let alloc_open = alloc_scope_open_tokens();
8983 let alloc_close = alloc_scope_close_tokens(
8984 quote! { monitor },
8985 quote! { #monitor_index },
8986 quote! { CuComponentState::Process },
8987 );
8988 (
8989 wrap_process_step_tokens(
8990 wrap_process_step,
8991 quote! {
8992 #bridge_setup
8993 #parallel_bridge_preprocess
8994 let cumsg_output = #output_ref;
8995 #call_sim_callback
8996 if doit {
8997 execution_probe.record(cu29::monitoring::ExecutionMarker {
8998 component_id: cu29::monitoring::ComponentId::new(#monitor_index),
8999 step: CuComponentState::Process,
9000 culistid: Some(clid),
9001 });
9002 cumsg_output.metadata.process_time.start = cu29::curuntime::perf_now(clock).into();
9003 #alloc_open
9004 let maybe_error = {
9005 #rt_guard
9006 ctx.set_current_component(#monitor_index);
9007 ctx.clear_current_task();
9008 bridge.receive(
9009 &ctx,
9010 &<#bridge_type as cu29::cubridge::CuBridge>::Rx::#const_ident,
9011 cumsg_output,
9012 )
9013 };
9014 cumsg_output.metadata.process_time.end = cu29::curuntime::perf_now(clock).into();
9015 #alloc_close
9016 if let Err(error) = maybe_error {
9017 let decision = monitor.process_error(cu29::monitoring::ComponentId::new(#monitor_index), CuComponentState::Process, &error);
9018 match decision {
9019 Decision::Abort => {
9020 debug!(ctx, "Process: ABORT decision from monitoring. Component '{}' errored out during process. Skipping the processing of CL {}.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)), clid);
9021 #abort_process_step
9022 }
9023 Decision::Ignore => {
9024 debug!(ctx, "Process: IGNORE decision from monitoring. Component '{}' errored out during process. The runtime will continue with a forced empty message.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
9025 cumsg_output.clear_payload();
9026 }
9027 Decision::Shutdown => {
9028 debug!(ctx, "Process: SHUTDOWN decision from monitoring. Component '{}' errored out during process. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
9029 return Err(CuError::new_with_cause("Component errored out during process.", error));
9030 }
9031 }
9032 }
9033 }
9034 #parallel_bridge_postprocess
9035 },
9036 ),
9037 quote! {},
9038 )
9039}
9040
9041fn generate_bridge_tx_execution_tokens(
9042 step: &CuExecutionStep,
9043 bridge_spec: &BridgeSpec,
9044 channel_index: usize,
9045 ctx: StepGenerationContext<'_>,
9046 bridge_setup: proc_macro2::TokenStream,
9047) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
9048 let StepGenerationContext {
9049 output_pack_sizes,
9050 task_input_layouts: _,
9051 mission_name: _,
9052 sim_mode,
9053 mission_mod,
9054 lifecycle_placement,
9055 wrap_process_step,
9056 } = ctx;
9057 let rt_guard = rtsan_guard_tokens();
9058 let abort_process_step = abort_process_step_tokens(wrap_process_step);
9059 let channel = &bridge_spec.tx_channels[channel_index];
9060 let monitor_index = syn::Index::from(
9061 channel
9062 .monitor_index
9063 .expect("Bridge Tx channel missing monitor index"),
9064 );
9065 let input = step
9066 .input_msg_indices_types
9067 .first()
9068 .expect("Bridge Tx channel should have exactly one input");
9069 let input_index = int2sliceindex(input.culist_index);
9070 let output_size = output_pack_sizes
9071 .get(input.culist_index as usize)
9072 .copied()
9073 .unwrap_or_else(|| {
9074 panic!(
9075 "Missing output pack size for culist index {}",
9076 input.culist_index
9077 )
9078 });
9079 let input_ref = if output_size > 1 {
9080 let port_index = syn::Index::from(input.src_port);
9081 quote! { &mut msgs.#input_index.#port_index }
9082 } else {
9083 quote! { &mut msgs.#input_index }
9084 };
9085 let output_pack = step
9086 .output_msg_pack
9087 .as_ref()
9088 .expect("Bridge Tx channel missing output pack");
9089 if output_pack.msg_types.len() != 1 {
9090 panic!(
9091 "Bridge Tx channel '{}' expected a single output message slot, got {}",
9092 channel.id,
9093 output_pack.msg_types.len()
9094 );
9095 }
9096 let output_index = int2sliceindex(output_pack.culist_index);
9097 let output_ref = quote! { &mut msgs.#output_index };
9098 let bridge_type = runtime_bridge_type_for_spec(bridge_spec, sim_mode);
9099 let (parallel_bridge_preprocess, parallel_bridge_postprocess) =
9100 parallel_bridge_lifecycle_tokens(
9101 &bridge_type,
9102 bridge_spec
9103 .monitor_index
9104 .expect("Bridge missing monitor index for lifecycle"),
9105 mission_mod,
9106 lifecycle_placement,
9107 );
9108 let const_ident = &channel.const_ident;
9109 let enum_ident = Ident::new(
9110 &config_id_to_enum(&format!("{}_tx_{}", bridge_spec.id, channel.id)),
9111 Span::call_site(),
9112 );
9113
9114 let call_sim_callback = if sim_mode {
9115 quote! {
9116 let doit = {
9117 let state = SimStep::#enum_ident {
9118 channel: &<#bridge_type as cu29::cubridge::CuBridge>::Tx::#const_ident,
9119 msg: &*cumsg_input,
9120 output: cumsg_output,
9121 };
9122 let ovr = sim_callback(state);
9123 if let SimOverride::Errored(reason) = ovr {
9124 let error: CuError = reason.into();
9125 let decision = monitor.process_error(cu29::monitoring::ComponentId::new(#monitor_index), CuComponentState::Process, &error);
9126 match decision {
9127 Decision::Abort => {
9128 debug!(ctx, "Process: ABORT decision from monitoring. Component '{}' errored out during process. Skipping the processing of CL {}.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)), clid);
9129 #abort_process_step
9130 }
9131 Decision::Ignore => {
9132 debug!(ctx, "Process: IGNORE decision from monitoring. Component '{}' errored out during process. The runtime will continue with a forced empty message.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
9133 false
9134 }
9135 Decision::Shutdown => {
9136 debug!(ctx, "Process: SHUTDOWN decision from monitoring. Component '{}' errored out during process. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
9137 return Err(CuError::new_with_cause("Component errored out during process.", error));
9138 }
9139 }
9140 } else {
9141 ovr == SimOverride::ExecuteByRuntime
9142 }
9143 };
9144 }
9145 } else {
9146 quote! { let doit = true; }
9147 };
9148 let alloc_open = alloc_scope_open_tokens();
9149 let alloc_close = alloc_scope_close_tokens(
9150 quote! { monitor },
9151 quote! { #monitor_index },
9152 quote! { CuComponentState::Process },
9153 );
9154 (
9155 wrap_process_step_tokens(
9156 wrap_process_step,
9157 quote! {
9158 #bridge_setup
9159 #parallel_bridge_preprocess
9160 let cumsg_input = #input_ref;
9161 let cumsg_output = #output_ref;
9162 let bridge_channel = &<#bridge_type as cu29::cubridge::CuBridge>::Tx::#const_ident;
9163 #call_sim_callback
9164 if doit {
9165 execution_probe.record(cu29::monitoring::ExecutionMarker {
9166 component_id: cu29::monitoring::ComponentId::new(#monitor_index),
9167 step: CuComponentState::Process,
9168 culistid: Some(clid),
9169 });
9170 cumsg_output.metadata.process_time.start = cu29::curuntime::perf_now(clock).into();
9171 #alloc_open
9172 let maybe_error = if bridge_channel.should_send(cumsg_input.payload().is_some()) {
9173 {
9174 #rt_guard
9175 ctx.set_current_component(#monitor_index);
9176 ctx.clear_current_task();
9177 bridge.send(
9178 &ctx,
9179 bridge_channel,
9180 &*cumsg_input,
9181 )
9182 }
9183 } else {
9184 Ok(())
9185 };
9186 #alloc_close
9187 if let Err(error) = maybe_error {
9188 let decision = monitor.process_error(cu29::monitoring::ComponentId::new(#monitor_index), CuComponentState::Process, &error);
9189 match decision {
9190 Decision::Abort => {
9191 debug!(ctx, "Process: ABORT decision from monitoring. Component '{}' errored out during process. Skipping the processing of CL {}.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)), clid);
9192 #abort_process_step
9193 }
9194 Decision::Ignore => {
9195 debug!(ctx, "Process: IGNORE decision from monitoring. Component '{}' errored out during process. The runtime will continue with a forced empty message.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
9196 }
9197 Decision::Shutdown => {
9198 debug!(ctx, "Process: SHUTDOWN decision from monitoring. Component '{}' errored out during process. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
9199 return Err(CuError::new_with_cause("Component errored out during process.", error));
9200 }
9201 }
9202 }
9203 cumsg_output.metadata.process_time.end = cu29::curuntime::perf_now(clock).into();
9204 }
9205 #parallel_bridge_postprocess
9206 },
9207 ),
9208 quote! {},
9209 )
9210}
9211
9212#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
9213enum BridgeChannelDirection {
9214 Rx,
9215 Tx,
9216}
9217
9218#[derive(Clone, Debug, PartialEq, Eq, Hash)]
9219struct BridgeChannelKey {
9220 bridge_id: String,
9221 channel_id: String,
9222 direction: BridgeChannelDirection,
9223}
9224
9225#[derive(Clone)]
9226struct BridgeChannelSpec {
9227 id: String,
9228 const_ident: Ident,
9229 #[allow(dead_code)]
9230 msg_type: Type,
9231 msg_type_name: String,
9232 config_index: usize,
9233 plan_node_id: Option<NodeId>,
9234 culist_index: Option<usize>,
9235 monitor_index: Option<usize>,
9236}
9237
9238#[derive(Clone)]
9239struct BridgeSpec {
9240 id: String,
9241 type_path: Type,
9242 run_in_sim: bool,
9243 config_index: usize,
9244 tuple_index: usize,
9245 monitor_index: Option<usize>,
9246 rx_channels: Vec<BridgeChannelSpec>,
9247 tx_channels: Vec<BridgeChannelSpec>,
9248}
9249
9250#[derive(Clone, Copy, Debug, Default)]
9251struct ParallelLifecyclePlacement {
9252 preprocess: bool,
9253 postprocess: bool,
9254}
9255
9256#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
9257enum ParallelLifecycleKey {
9258 Task(usize),
9259 Bridge(usize),
9260}
9261
9262fn build_parallel_lifecycle_placements(
9263 culist_plan: &CuExecutionLoop,
9264 culist_exec_entities: &[ExecutionEntity],
9265) -> Vec<ParallelLifecyclePlacement> {
9266 let step_keys: Vec<Option<ParallelLifecycleKey>> = culist_plan
9267 .steps
9268 .iter()
9269 .map(|unit| match unit {
9270 CuExecutionUnit::Step(step) => {
9271 match &culist_exec_entities[step.node_id as usize].kind {
9272 ExecutionEntityKind::Task { task_index } => {
9273 Some(ParallelLifecycleKey::Task(*task_index))
9274 }
9275 ExecutionEntityKind::BridgeRx { bridge_index, .. }
9276 | ExecutionEntityKind::BridgeTx { bridge_index, .. } => {
9277 Some(ParallelLifecycleKey::Bridge(*bridge_index))
9278 }
9279 }
9280 }
9281 CuExecutionUnit::Loop(_) => None,
9282 })
9283 .collect();
9284
9285 let mut placements = vec![ParallelLifecyclePlacement::default(); step_keys.len()];
9286 let mut seen_forward = std::collections::HashSet::new();
9287 for (index, key) in step_keys.iter().enumerate() {
9288 let Some(key) = key else {
9289 continue;
9290 };
9291 if seen_forward.insert(*key) {
9292 placements[index].preprocess = true;
9293 }
9294 }
9295
9296 let mut seen_reverse = std::collections::HashSet::new();
9297 for (index, key) in step_keys.iter().enumerate().rev() {
9298 let Some(key) = key else {
9299 continue;
9300 };
9301 if seen_reverse.insert(*key) {
9302 placements[index].postprocess = true;
9303 }
9304 }
9305
9306 placements
9307}
9308
9309fn sim_bridge_channel_set_idents(bridge_tuple_index: usize) -> (Ident, Ident, Ident, Ident) {
9310 (
9311 format_ident!("__CuSimBridge{}TxChannels", bridge_tuple_index),
9312 format_ident!("__CuSimBridge{}TxId", bridge_tuple_index),
9313 format_ident!("__CuSimBridge{}RxChannels", bridge_tuple_index),
9314 format_ident!("__CuSimBridge{}RxId", bridge_tuple_index),
9315 )
9316}
9317
9318fn runtime_bridge_type_for_spec(bridge_spec: &BridgeSpec, sim_mode: bool) -> Type {
9319 if sim_mode && !bridge_spec.run_in_sim {
9320 let (tx_set_ident, _tx_id_ident, rx_set_ident, _rx_id_ident) =
9321 sim_bridge_channel_set_idents(bridge_spec.tuple_index);
9322 let tx_type: Type = if bridge_spec.tx_channels.is_empty() {
9323 parse_quote!(cu29::simulation::CuNoBridgeChannels)
9324 } else {
9325 parse_quote!(#tx_set_ident)
9326 };
9327 let rx_type: Type = if bridge_spec.rx_channels.is_empty() {
9328 parse_quote!(cu29::simulation::CuNoBridgeChannels)
9329 } else {
9330 parse_quote!(#rx_set_ident)
9331 };
9332 parse_quote!(cu29::simulation::CuSimBridge<#tx_type, #rx_type>)
9333 } else {
9334 bridge_spec.type_path.clone()
9335 }
9336}
9337
9338fn runtime_task_type_for_index(
9339 task_specs: &CuTaskSpecSet,
9340 graph: &CuGraph,
9341 index: usize,
9342 sim_mode: bool,
9343) -> Type {
9344 let task_id = &task_specs.ids[index];
9345 let declared_task_type = &task_specs.sim_task_types[index];
9346 let background = task_specs.background_flags[index];
9347 let run_in_sim = task_specs.run_in_sim_flags[index];
9348 let output_type = &task_specs.output_types[index];
9349
9350 match task_specs.cutypes[index] {
9351 CuTaskType::Source => {
9352 if sim_mode && !run_in_sim {
9353 let msg_type = graph.get_node_output_msg_type(task_id.as_str()).unwrap_or_else(|| {
9354 panic!(
9355 "CuSrcTask {task_id} should have an outgoing connection with a valid output msg type"
9356 )
9357 });
9358 let sim_task_name = format!("CuSimSrcTask<{msg_type}>");
9359 parse_str(sim_task_name.as_str()).unwrap_or_else(|_| {
9360 panic!("Could not build the placeholder for simulation: {sim_task_name}")
9361 })
9362 } else if background {
9363 if let Some(out_ty) = output_type {
9364 parse_quote!(CuAsyncSrcTask<#declared_task_type, #out_ty>)
9365 } else {
9366 panic!("{task_id}: If a source is background, it has to have an output");
9367 }
9368 } else {
9369 declared_task_type.clone()
9370 }
9371 }
9372 CuTaskType::Regular => {
9373 if background {
9374 if let Some(out_ty) = output_type {
9375 parse_quote!(CuAsyncTask<#declared_task_type, #out_ty>)
9376 } else {
9377 panic!("{task_id}: If a task is background, it has to have an output");
9378 }
9379 } else {
9380 declared_task_type.clone()
9382 }
9383 }
9384 CuTaskType::Sink => {
9385 if background {
9386 panic!(
9387 "CuSinkTask {task_id} cannot be a background task, it should be a regular task."
9388 );
9389 }
9390
9391 if sim_mode && !run_in_sim {
9392 let msg_types = graph.get_node_input_msg_types(task_id.as_str()).unwrap_or_else(|| {
9393 panic!(
9394 "CuSinkTask {task_id} should have an incoming connection with a valid input msg type"
9395 )
9396 });
9397 let msg_type = if msg_types.len() == 1 {
9398 format!("({},)", msg_types[0])
9399 } else {
9400 format!("({})", msg_types.join(", "))
9401 };
9402 let sim_task_name = format!("CuSimSinkTask<{msg_type}>");
9403 parse_str(sim_task_name.as_str()).unwrap_or_else(|_| {
9404 panic!("Could not build the placeholder for simulation: {sim_task_name}")
9405 })
9406 } else {
9407 declared_task_type.clone()
9408 }
9409 }
9410 }
9411}
9412
9413#[derive(Clone)]
9414struct ExecutionEntity {
9415 kind: ExecutionEntityKind,
9416}
9417
9418#[derive(Clone)]
9419enum ExecutionEntityKind {
9420 Task {
9421 task_index: usize,
9422 },
9423 BridgeRx {
9424 bridge_index: usize,
9425 channel_index: usize,
9426 },
9427 BridgeTx {
9428 bridge_index: usize,
9429 channel_index: usize,
9430 },
9431}
9432
9433#[cfg(test)]
9434mod tests {
9435 use std::fs;
9436 use std::path::{Path, PathBuf};
9437
9438 fn unique_test_dir(name: &str) -> PathBuf {
9439 let nanos = std::time::SystemTime::now()
9440 .duration_since(std::time::UNIX_EPOCH)
9441 .expect("system clock before unix epoch")
9442 .as_nanos();
9443 std::env::temp_dir().join(format!("cu29_derive_{name}_{nanos}"))
9444 }
9445
9446 fn write_file(path: &Path, content: &str) {
9447 if let Some(parent) = path.parent() {
9448 fs::create_dir_all(parent).expect("create parent dirs");
9449 }
9450 fs::write(path, content).expect("write file");
9451 }
9452
9453 #[test]
9455 fn test_compile_fail() {
9456 use rustc_version::{Channel, version_meta};
9457 use std::{env, fs, path::Path};
9458
9459 let log_index_dir = env::temp_dir()
9460 .join("cu29_derive_trybuild_log_index")
9461 .join("a")
9462 .join("b")
9463 .join("c");
9464 fs::create_dir_all(&log_index_dir).unwrap();
9465 unsafe {
9466 env::set_var("LOG_INDEX_DIR", &log_index_dir);
9467 }
9468
9469 let dir = Path::new("tests/compile_fail");
9470 for entry in fs::read_dir(dir).unwrap() {
9471 let entry = entry.unwrap();
9472 if !entry.file_type().unwrap().is_dir() {
9473 continue;
9474 }
9475 for file in fs::read_dir(entry.path()).unwrap() {
9476 let file = file.unwrap();
9477 let p = file.path();
9478 if p.extension().and_then(|x| x.to_str()) != Some("rs") {
9479 continue;
9480 }
9481
9482 let base = p.with_extension("stderr"); let src = match version_meta().unwrap().channel {
9484 Channel::Beta => Path::new(&format!("{}.beta", base.display())).to_path_buf(),
9485 _ => Path::new(&format!("{}.stable", base.display())).to_path_buf(),
9486 };
9487
9488 if src.exists() {
9489 fs::copy(src, &base).unwrap();
9490 }
9491 }
9492 }
9493
9494 let umbrella = build_compile_pass_umbrella();
9497 let t = trybuild::TestCases::new();
9498 t.compile_fail("tests/compile_fail/*/*.rs");
9499 t.pass(&umbrella);
9500 }
9501
9502 fn build_compile_pass_umbrella() -> std::path::PathBuf {
9503 use std::fmt::Write as _;
9504 let pass_dir = std::path::Path::new("tests/compile_pass");
9505 let mut entries: Vec<std::path::PathBuf> = Vec::new();
9506 for sub in std::fs::read_dir(pass_dir).expect("read tests/compile_pass") {
9507 let sub = sub.expect("read tests/compile_pass entry");
9508 if !sub
9509 .file_type()
9510 .expect("stat tests/compile_pass entry")
9511 .is_dir()
9512 {
9513 continue;
9514 }
9515 for file in std::fs::read_dir(sub.path()).expect("read compile_pass subdir") {
9516 let p = file.expect("read compile_pass subdir entry").path();
9517 if p.extension().and_then(|x| x.to_str()) == Some("rs") {
9518 entries.push(p);
9519 }
9520 }
9521 }
9522 entries.sort();
9523
9524 let mut src = String::from("#![allow(dead_code, unused_imports, non_snake_case)]\n");
9525 for p in &entries {
9526 let abs = std::fs::canonicalize(p)
9527 .unwrap_or_else(|e| panic!("canonicalize {}: {e}", p.display()));
9528 let subdir = abs
9529 .parent()
9530 .and_then(|d| d.file_name())
9531 .map(|s| s.to_string_lossy().into_owned())
9532 .unwrap_or_default();
9533 let stem = abs
9534 .file_stem()
9535 .expect("compile_pass file has stem")
9536 .to_string_lossy()
9537 .into_owned();
9538 let mod_name = format!("{subdir}_{stem}").replace('-', "_");
9539 writeln!(
9540 src,
9541 "#[path = {:?}] mod compile_pass_{};",
9542 abs.to_string_lossy(),
9543 mod_name
9544 )
9545 .expect("write to String");
9546 }
9547 src.push_str("fn main() {}\n");
9548
9549 let target_dir = std::env::var_os("CARGO_TARGET_DIR")
9552 .map(std::path::PathBuf::from)
9553 .unwrap_or_else(|| std::path::PathBuf::from("../../target"));
9554 let umbrella_dir = target_dir.join("generated");
9555 std::fs::create_dir_all(&umbrella_dir)
9556 .unwrap_or_else(|e| panic!("create {}: {e}", umbrella_dir.display()));
9557 let umbrella = umbrella_dir.join("compile_pass_umbrella.rs");
9558 if std::fs::read_to_string(&umbrella).ok().as_deref() != Some(src.as_str()) {
9559 std::fs::write(&umbrella, &src)
9560 .unwrap_or_else(|e| panic!("write {}: {e}", umbrella.display()));
9561 }
9562 std::fs::canonicalize(&umbrella)
9563 .unwrap_or_else(|e| panic!("canonicalize {}: {e}", umbrella.display()))
9564 }
9565
9566 #[test]
9567 fn runtime_plan_keeps_nc_order_for_non_first_connected_output() {
9568 use super::*;
9569 use cu29::config::CuConfig;
9570 use cu29::curuntime::{CuExecutionUnit, compute_runtime_plan};
9571
9572 let config: CuConfig =
9573 read_config("tests/config/multi_output_source_non_first_connected_valid.ron")
9574 .expect("failed to read test config");
9575 let graph = config.get_graph(None).expect("missing graph");
9576 let src_id = graph.get_node_id_by_name("src").expect("missing src node");
9577
9578 let runtime = compute_runtime_plan(graph).expect("runtime plan failed");
9579 let src_step = runtime
9580 .steps
9581 .iter()
9582 .find_map(|step| match step {
9583 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
9584 _ => None,
9585 })
9586 .expect("missing source step");
9587
9588 assert_eq!(
9589 src_step.output_msg_pack.as_ref().unwrap().msg_types,
9590 vec!["i32", "bool"]
9591 );
9592 }
9593
9594 #[test]
9595 fn matching_task_ids_are_flattened_per_output_message() {
9596 use super::*;
9597 use cu29::config::CuConfig;
9598
9599 let config: CuConfig =
9600 read_config("tests/config/multi_output_source_non_first_connected_valid.ron")
9601 .expect("failed to read test config");
9602 let graph = config.get_graph(None).expect("missing graph");
9603 let task_specs = CuTaskSpecSet::from_graph(graph).expect("task specs");
9604 let channel_usage = collect_bridge_channel_usage(graph);
9605 let mut bridge_specs = build_bridge_specs(&config, graph, &channel_usage);
9606 let (runtime_plan, exec_entities, plan_to_original) =
9607 build_execution_plan(graph, &task_specs, &mut bridge_specs)
9608 .expect("runtime plan failed");
9609 let output_packs = extract_output_packs(&runtime_plan);
9610 let task_names = collect_task_names(graph);
9611 let (_, node_output_positions) = collect_culist_metadata(
9612 &runtime_plan,
9613 &exec_entities,
9614 &mut bridge_specs,
9615 &plan_to_original,
9616 );
9617
9618 let mut slot_origin_ids: Vec<Option<String>> = vec![None; output_packs.len()];
9620 for (node_id, task_id, _) in task_names {
9621 let output_position = node_output_positions
9622 .get(&node_id)
9623 .unwrap_or_else(|| panic!("Task {task_id} (node id: {node_id}) not found"));
9624 slot_origin_ids[*output_position] = Some(task_id);
9625 }
9626
9627 let flattened_ids = flatten_slot_origin_ids(&output_packs, &slot_origin_ids);
9628
9629 assert_eq!(
9632 flattened_ids,
9633 vec!["src".to_string(), "src".to_string(), "sink".to_string()]
9634 );
9635 }
9636
9637 #[test]
9638 fn bridge_resources_are_collected() {
9639 use super::*;
9640 use cu29::config::{CuGraph, Flavor, Node};
9641 use std::collections::HashMap;
9642 use syn::parse_str;
9643
9644 let mut graph = CuGraph::default();
9645 let mut node = Node::new_with_flavor("radio", "bridge::Dummy", Flavor::Bridge);
9646 let mut res = HashMap::new();
9647 res.insert("serial".to_string(), "fc.serial0".to_string());
9648 node.set_resources(Some(res));
9649 graph.add_node(node).expect("bridge node");
9650
9651 let task_specs = CuTaskSpecSet::from_graph(&graph).expect("task specs");
9652 let bridge_spec = BridgeSpec {
9653 id: "radio".to_string(),
9654 type_path: parse_str("bridge::Dummy").unwrap(),
9655 run_in_sim: true,
9656 config_index: 0,
9657 tuple_index: 0,
9658 monitor_index: None,
9659 rx_channels: Vec::new(),
9660 tx_channels: Vec::new(),
9661 };
9662
9663 let mut config = cu29::config::CuConfig::default();
9664 config.resources.push(ResourceBundleConfig {
9665 id: "fc".to_string(),
9666 provider: "board::Bundle".to_string(),
9667 config: None,
9668 missions: None,
9669 });
9670 let bundle_specs = build_bundle_specs(&config, "default").expect("bundle specs");
9671 let specs = collect_resource_specs(&graph, &task_specs, &[bridge_spec], &bundle_specs)
9672 .expect("collect specs");
9673 assert_eq!(specs.len(), 1);
9674 assert!(matches!(specs[0].owner, ResourceOwner::Bridge(0)));
9675 assert_eq!(specs[0].binding_name, "serial");
9676 assert_eq!(specs[0].bundle_index, 0);
9677 assert_eq!(specs[0].resource_name, "serial0");
9678 }
9679
9680 #[test]
9681 fn copper_runtime_args_parse_subsystem_mode() {
9682 use super::*;
9683 use quote::quote;
9684
9685 let args = CopperRuntimeArgs::parse_tokens(quote!(
9686 config = "multi_copper.ron",
9687 subsystem = "ping",
9688 sim_mode,
9689 ignore_resources
9690 ))
9691 .expect("parse runtime args");
9692
9693 assert_eq!(args.config_path, "multi_copper.ron");
9694 assert_eq!(args.subsystem_id.as_deref(), Some("ping"));
9695 assert!(args.sim_mode);
9696 assert!(args.ignore_resources);
9697 }
9698
9699 #[test]
9700 fn resolve_runtime_config_from_multi_config_selects_local_subsystem() {
9701 use super::*;
9702
9703 let root = unique_test_dir("multi_runtime_resolve");
9704 let alpha_config = root.join("alpha.ron");
9705 let beta_config = root.join("beta.ron");
9706 let network_config = root.join("multi.ron");
9707
9708 write_file(
9709 &alpha_config,
9710 r#"
9711(
9712 tasks: [
9713 (id: "src", type: "AlphaSource", run_in_sim: true),
9714 (id: "sink", type: "AlphaSink", run_in_sim: true),
9715 ],
9716 cnx: [
9717 (src: "src", dst: "sink", msg: "u32"),
9718 ],
9719)
9720"#,
9721 );
9722 write_file(
9723 &beta_config,
9724 r#"
9725(
9726 tasks: [
9727 (id: "src", type: "BetaSource", run_in_sim: true),
9728 (id: "sink", type: "BetaSink", run_in_sim: true),
9729 ],
9730 cnx: [
9731 (src: "src", dst: "sink", msg: "u64"),
9732 ],
9733)
9734"#,
9735 );
9736 write_file(
9737 &network_config,
9738 r#"
9739(
9740 subsystems: [
9741 (id: "beta", config: "beta.ron"),
9742 (id: "alpha", config: "alpha.ron"),
9743 ],
9744 interconnects: [],
9745)
9746"#,
9747 );
9748
9749 let args = CopperRuntimeArgs {
9750 config_path: "multi.ron".to_string(),
9751 subsystem_id: Some("beta".to_string()),
9752 sim_mode: false,
9753 ignore_resources: false,
9754 };
9755
9756 let resolved =
9757 resolve_runtime_config_with_root(&args, &root).expect("resolve multi runtime config");
9758
9759 assert_eq!(resolved.subsystem_id.as_deref(), Some("beta"));
9760 assert_eq!(resolved.subsystem_code, 1);
9761 let graph = resolved
9762 .local_config
9763 .get_graph(None)
9764 .expect("resolved local config graph");
9765 assert!(graph.get_node_id_by_name("src").is_some());
9766 assert!(resolved.bundled_local_config_content.contains("BetaSource"));
9767 }
9768
9769 #[test]
9770 fn resolve_runtime_config_rejects_missing_subsystem() {
9771 use super::*;
9772
9773 let root = unique_test_dir("multi_runtime_missing_subsystem");
9774 let alpha_config = root.join("alpha.ron");
9775 let network_config = root.join("multi.ron");
9776
9777 write_file(
9778 &alpha_config,
9779 r#"
9780(
9781 tasks: [
9782 (id: "src", type: "AlphaSource", run_in_sim: true),
9783 (id: "sink", type: "AlphaSink", run_in_sim: true),
9784 ],
9785 cnx: [
9786 (src: "src", dst: "sink", msg: "u32"),
9787 ],
9788)
9789"#,
9790 );
9791 write_file(
9792 &network_config,
9793 r#"
9794(
9795 subsystems: [
9796 (id: "alpha", config: "alpha.ron"),
9797 ],
9798 interconnects: [],
9799)
9800"#,
9801 );
9802
9803 let args = CopperRuntimeArgs {
9804 config_path: "multi.ron".to_string(),
9805 subsystem_id: Some("missing".to_string()),
9806 sim_mode: false,
9807 ignore_resources: false,
9808 };
9809
9810 let err = resolve_runtime_config_with_root(&args, &root).expect_err("missing subsystem");
9811 assert!(err.to_string().contains("Subsystem 'missing'"));
9812 }
9813
9814 #[test]
9815 fn synthesized_single_output_type_name_parses_for_source_and_regular_tasks() {
9816 use super::*;
9817
9818 let src_ty: Type = parse_quote!(SingleSource);
9819 let regular_ty: Type = parse_quote!(RegularTask);
9820
9821 let src_name = synthesized_single_output_msg_name(&src_ty, CuTaskType::Source);
9822 let regular_name = synthesized_single_output_msg_name(®ular_ty, CuTaskType::Regular);
9823
9824 parse_str::<Type>(src_name.as_str()).expect("source payload type should parse");
9825 parse_str::<Type>(regular_name.as_str()).expect("regular payload type should parse");
9826 }
9827}