Skip to main content

cu29_derive/
lib.rs

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