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