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