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