Skip to main content

cu29_derive/
lib.rs

1mod live_twin;
2use proc_macro::TokenStream;
3use quote::{ToTokens, format_ident, quote};
4use std::collections::{BTreeMap, BTreeSet, HashMap};
5use std::path::Path;
6use std::process::Command;
7use syn::Fields::{Named, Unnamed};
8use syn::Path as SynPath;
9use syn::ext::IdentExt;
10use syn::meta::parser;
11use syn::parse::Parser;
12use syn::punctuated::Punctuated;
13use syn::{
14    Block, Expr, Field, Fields, ItemFn, ItemImpl, ItemStruct, Lit, LitStr, Stmt, Token, Type,
15    TypeTuple, parse_macro_input, parse_quote, parse_str,
16};
17
18use crate::utils::{config_id_to_bridge_const, config_id_to_enum, config_id_to_struct_member};
19use cu29_build::COPPER_CFG_FEATURES_ENV;
20use cu29_runtime::config::CuConfig;
21use cu29_runtime::config::DEFAULT_MISSION_ID;
22use cu29_runtime::config::{
23    AnytimeConfig, BridgeChannelConfigRepresentation, ConfigGraphs, ConstantConfig, ConstantNumber,
24    ConstantStorage, CuGraph, Flavor, HandleContent, Node, NodeId, RT_POOL, ResourceBundleConfig,
25    read_configuration_with_features, read_configuration_with_resolved_ron_and_features,
26};
27use cu29_runtime::curuntime::{
28    CuExecutionLoop, CuExecutionStep, CuExecutionUnit, CuStepPhase, CuTaskType,
29    find_task_type_for_id,
30};
31use cu29_runtime::planner::{
32    BUILTIN_PLANNERS, DEFAULT_COPPERLIST_COUNT, PLAN_ARTIFACT_FILE, PlanEntityKind,
33    assemble_runtime_plan, assemble_runtime_plan_from_step_keys, config_digest, is_builtin_planner,
34    read_plan_artifact,
35};
36use cu29_traits::{CuError, CuResult};
37use proc_macro2::{Ident, Span};
38
39mod bundle_resources;
40mod resources;
41mod utils;
42
43#[inline]
44fn int2sliceindex(i: u32) -> syn::Index {
45    syn::Index::from(i as usize)
46}
47
48#[inline(always)]
49fn return_error(msg: String) -> TokenStream {
50    syn::Error::new(Span::call_site(), msg)
51        .to_compile_error()
52        .into()
53}
54
55fn rtsan_guard_tokens() -> proc_macro2::TokenStream {
56    if cfg!(feature = "rtsan") {
57        quote! {
58            let _rt_guard = ::cu29::rtsan::ScopedSanitizeRealtime::default();
59        }
60    } else {
61        quote! {}
62    }
63}
64
65fn primitive_constant_number_tokens(
66    storage: ConstantStorage,
67    number: ConstantNumber,
68) -> CuResult<proc_macro2::TokenStream> {
69    if storage == ConstantStorage::F32 {
70        let bits = proc_macro2::Literal::u32_suffixed((number.as_f64() as f32).to_bits());
71        return Ok(quote! { ::core::primitive::f32::from_bits(#bits) });
72    }
73    if storage == ConstantStorage::F64 {
74        let bits = proc_macro2::Literal::u64_suffixed(number.as_f64().to_bits());
75        return Ok(quote! { ::core::primitive::f64::from_bits(#bits) });
76    }
77    let source = match (storage, number) {
78        (storage, ConstantNumber::Signed(value)) => {
79            format!("{value}{}", storage.rust_type())
80        }
81        (storage, ConstantNumber::Unsigned(value)) => {
82            format!("{value}{}", storage.rust_type())
83        }
84        (storage, ConstantNumber::Float(value)) => {
85            return Err(CuError::from(format!(
86                "Floating-point value {value:?} cannot be emitted as {}",
87                storage.rust_type()
88            )));
89        }
90    };
91    let expression = parse_str::<Expr>(&source).map_err(|error| {
92        CuError::from(format!(
93            "Could not generate constant expression '{source}': {error}"
94        ))
95    })?;
96    Ok(quote! { #expression })
97}
98
99fn build_constant_def(constant: &ConstantConfig) -> CuResult<proc_macro2::TokenStream> {
100    let id = parse_str::<Ident>(constant.id()).map_err(|error| {
101        CuError::from(format!(
102            "Constant id '{}' is not a valid Rust identifier: {error}",
103            constant.id()
104        ))
105    })?;
106
107    if let Some((rust_type, expression)) = constant.expression_definition() {
108        let constant_type = parse_str::<Type>(rust_type).map_err(|error| {
109            CuError::from(format!(
110                "Constant '{}' type '{}' is not a valid Rust type: {error}",
111                constant.id(),
112                rust_type
113            ))
114        })?;
115        let expression = parse_str::<Expr>(expression).map_err(|error| {
116            CuError::from(format!(
117                "Constant '{}' expression is not a valid Rust expression: {error}",
118                constant.id()
119            ))
120        })?;
121        return Ok(quote! {
122            pub const #id: #constant_type = #expression;
123        });
124    }
125
126    if let Some(quantity) = constant.quantity() {
127        let definition = cu29_units::constant::definition(quantity).ok_or_else(|| {
128            CuError::from(format!(
129                "Constant '{}' quantity '{}' is missing from the unit catalogue",
130                constant.id(),
131                quantity.name()
132            ))
133        })?;
134        let (constant_type, values): (Type, Vec<proc_macro2::TokenStream>) =
135            match constant.storage() {
136                ConstantStorage::F32 => {
137                    let ty = parse_str::<Type>(definition.rust_type_f32).map_err(|error| {
138                        CuError::from(format!(
139                            "Invalid f32 type metadata for quantity '{}': {error}",
140                            quantity.name()
141                        ))
142                    })?;
143                    let (_, values) = constant.normalized_f32().map_err(CuError::from)?;
144                    let values = values
145                        .into_iter()
146                        .map(|value| {
147                            let bits = proc_macro2::Literal::u32_suffixed(value.to_bits());
148                            quote! {
149                                #ty {
150                                    value: ::core::primitive::f32::from_bits(#bits),
151                                }
152                            }
153                        })
154                        .collect();
155                    (ty, values)
156                }
157                ConstantStorage::F64 => {
158                    let ty = parse_str::<Type>(definition.rust_type_f64).map_err(|error| {
159                        CuError::from(format!(
160                            "Invalid f64 type metadata for quantity '{}': {error}",
161                            quantity.name()
162                        ))
163                    })?;
164                    let (_, values) = constant.normalized_f64().map_err(CuError::from)?;
165                    let values = values
166                        .into_iter()
167                        .map(|value| {
168                            let bits = proc_macro2::Literal::u64_suffixed(value.to_bits());
169                            quote! {
170                                #ty {
171                                    value: ::core::primitive::f64::from_bits(#bits),
172                                }
173                            }
174                        })
175                        .collect();
176                    (ty, values)
177                }
178                storage => {
179                    return Err(CuError::from(format!(
180                        "Constant '{}' quantity '{}' cannot use storage {}",
181                        constant.id(),
182                        quantity.name(),
183                        storage.rust_type()
184                    )));
185                }
186            };
187        let (is_array, _) = constant.numbers().map_err(CuError::from)?;
188        if is_array {
189            let length = values.len();
190            Ok(quote! {
191                pub const #id: [#constant_type; #length] = [#(#values),*];
192            })
193        } else {
194            let value = values.into_iter().next().ok_or_else(|| {
195                CuError::from(format!("Constant '{}' has no scalar value", constant.id()))
196            })?;
197            Ok(quote! {
198                pub const #id: #constant_type = #value;
199            })
200        }
201    } else {
202        let constant_type = parse_str::<Type>(constant.storage().rust_type()).map_err(|error| {
203            CuError::from(format!(
204                "Invalid primitive storage type '{}': {error}",
205                constant.storage().rust_type()
206            ))
207        })?;
208        let (is_array, numbers) = constant.numbers().map_err(CuError::from)?;
209        let values = numbers
210            .into_iter()
211            .map(|number| primitive_constant_number_tokens(constant.storage(), number))
212            .collect::<CuResult<Vec<_>>>()?;
213        if is_array {
214            let length = values.len();
215            Ok(quote! {
216                pub const #id: [#constant_type; #length] = [#(#values),*];
217            })
218        } else {
219            let value = values.into_iter().next().ok_or_else(|| {
220                CuError::from(format!("Constant '{}' has no scalar value", constant.id()))
221            })?;
222            Ok(quote! {
223                pub const #id: #constant_type = #value;
224            })
225        }
226    }
227}
228
229#[derive(Default)]
230struct ConstantModuleTree {
231    ident: Option<Ident>,
232    constants: Vec<proc_macro2::TokenStream>,
233    constant_names: BTreeSet<String>,
234    children: BTreeMap<String, ConstantModuleTree>,
235}
236
237impl ConstantModuleTree {
238    fn insert(
239        &mut self,
240        module_path: &[Ident],
241        constant_id: &Ident,
242        definition: proc_macro2::TokenStream,
243        qualified_id: &str,
244    ) -> CuResult<()> {
245        let mut module = self;
246        for segment in module_path {
247            let canonical = segment.unraw().to_string();
248            if module.constant_names.contains(&canonical) {
249                return Err(CuError::from(format!(
250                    "Constant module '{}' conflicts with constant '{}'",
251                    module_path
252                        .iter()
253                        .map(ToString::to_string)
254                        .collect::<Vec<_>>()
255                        .join("::"),
256                    canonical
257                )));
258            }
259            module = module
260                .children
261                .entry(canonical)
262                .or_insert_with(|| ConstantModuleTree {
263                    ident: Some(segment.clone()),
264                    ..Self::default()
265                });
266        }
267
268        let canonical_id = constant_id.unraw().to_string();
269        if module.children.contains_key(&canonical_id) {
270            return Err(CuError::from(format!(
271                "Constant '{qualified_id}' conflicts with a generated module of the same name"
272            )));
273        }
274        if !module.constant_names.insert(canonical_id) {
275            return Err(CuError::from(format!(
276                "Duplicate constant '{qualified_id}'. Constant ids must be unique within a module."
277            )));
278        }
279        module.constants.push(definition);
280        Ok(())
281    }
282
283    fn contents(&self) -> proc_macro2::TokenStream {
284        let constants = &self.constants;
285        let children = self.children.values().map(Self::module_tokens);
286        quote! {
287            #(#constants)*
288            #(#children)*
289        }
290    }
291
292    fn module_tokens(&self) -> proc_macro2::TokenStream {
293        let ident = self
294            .ident
295            .as_ref()
296            .expect("only non-root constant module nodes are rendered");
297        let contents = self.contents();
298        quote! {
299            pub mod #ident {
300                #contents
301            }
302        }
303    }
304
305    fn child_contents(&self, ident: &Ident) -> proc_macro2::TokenStream {
306        self.children
307            .get(&ident.unraw().to_string())
308            .map(Self::contents)
309            .unwrap_or_default()
310    }
311
312    fn root_modules_except(&self, excluded: &BTreeSet<String>) -> proc_macro2::TokenStream {
313        let modules = self
314            .children
315            .iter()
316            .filter(|(name, _)| !excluded.contains(*name))
317            .map(|(_, module)| module.module_tokens());
318        quote! { #(#modules)* }
319    }
320}
321
322fn parse_constant_module_path(constant: &ConstantConfig) -> CuResult<Vec<Ident>> {
323    let source = constant.module_path();
324    let path = parse_str::<SynPath>(source).map_err(|error| {
325        CuError::from(format!(
326            "Constant '{}' module path '{}' is not a valid Rust module path: {error}",
327            constant.id(),
328            source
329        ))
330    })?;
331    if path.leading_colon.is_some() {
332        return Err(CuError::from(format!(
333            "Constant '{}' module path '{}' must be relative",
334            constant.id(),
335            source
336        )));
337    }
338
339    let mut segments = Vec::with_capacity(path.segments.len());
340    for segment in path.segments {
341        if !segment.arguments.is_none() {
342            return Err(CuError::from(format!(
343                "Constant '{}' module path '{}' cannot contain generic arguments",
344                constant.id(),
345                source
346            )));
347        }
348        let canonical = segment.ident.unraw().to_string();
349        if matches!(canonical.as_str(), "crate" | "self" | "super") {
350            return Err(CuError::from(format!(
351                "Constant '{}' module path '{}' must not contain '{canonical}'",
352                constant.id(),
353                source
354            )));
355        }
356        segments.push(segment.ident);
357    }
358    if segments.is_empty() {
359        return Err(CuError::from(format!(
360            "Constant '{}' module path cannot be empty",
361            constant.id()
362        )));
363    }
364    Ok(segments)
365}
366
367fn build_constant_modules(constants: &[ConstantConfig]) -> CuResult<ConstantModuleTree> {
368    let mut modules = ConstantModuleTree::default();
369    for constant in constants {
370        let module_path = parse_constant_module_path(constant)?;
371        let constant_id = parse_str::<Ident>(constant.id()).map_err(|error| {
372            CuError::from(format!(
373                "Constant id '{}' is not a valid Rust identifier: {error}",
374                constant.id()
375            ))
376        })?;
377        let definition = build_constant_def(constant)?;
378        modules.insert(
379            &module_path,
380            &constant_id,
381            definition,
382            &constant.qualified_id(),
383        )?;
384    }
385    Ok(modules)
386}
387
388/// Opens a heap-allocation accounting scope (binds `__cu_alloc_scope`) iff the
389/// `memory_monitoring` feature is enabled. Otherwise expands to nothing, so a
390/// default build emits zero extra code per task step.
391fn alloc_scope_open_tokens() -> proc_macro2::TokenStream {
392    if cfg!(feature = "memory_monitoring") {
393        quote! {
394            let __cu_alloc_scope = cu29::monitoring::ScopedAllocCounter::new();
395        }
396    } else {
397        quote! {}
398    }
399}
400
401/// Forwards the scope's accumulated delta to `monitor.observe_alloc(...)` and
402/// drops the scope. `monitor_expr` is whatever expression reaches the monitor
403/// in the current code block (`monitor` for preprocess/process/postprocess,
404/// `self.copper_runtime.monitor` for start/stop). `component_index` and `step`
405/// are the tokens to be passed through to `ComponentId::new(...)` and the
406/// `CuComponentState` variant.
407fn alloc_scope_close_tokens(
408    monitor_expr: proc_macro2::TokenStream,
409    component_index: proc_macro2::TokenStream,
410    step: proc_macro2::TokenStream,
411) -> proc_macro2::TokenStream {
412    if cfg!(feature = "memory_monitoring") {
413        quote! {
414            #monitor_expr.observe_alloc(
415                cu29::monitoring::ComponentId::new(#component_index),
416                #step,
417                __cu_alloc_scope.allocated(),
418                __cu_alloc_scope.deallocated(),
419            );
420        }
421    } else {
422        quote! {}
423    }
424}
425
426fn git_output_trimmed(repo_root: &Path, args: &[&str]) -> Option<String> {
427    let output = Command::new("git")
428        .arg("-C")
429        .arg(repo_root)
430        .args(args)
431        .output()
432        .ok()?;
433    if !output.status.success() {
434        return None;
435    }
436    let stdout = String::from_utf8(output.stdout).ok()?;
437    Some(stdout.trim().to_string())
438}
439
440fn detect_git_info(repo_root: &Path) -> (Option<String>, Option<bool>) {
441    let in_repo = git_output_trimmed(repo_root, &["rev-parse", "--is-inside-work-tree"])
442        .is_some_and(|value| value == "true");
443    if !in_repo {
444        return (None, None);
445    }
446
447    let commit = git_output_trimmed(repo_root, &["rev-parse", "HEAD"]).filter(|s| !s.is_empty());
448    // Porcelain output is empty when tree is clean.
449    let dirty = git_output_trimmed(repo_root, &["status", "--porcelain"]).map(|s| !s.is_empty());
450    (commit, dirty)
451}
452
453#[derive(Debug, Clone)]
454struct CopperRuntimeArgs {
455    config_path: String,
456    subsystem_id: Option<String>,
457    sim_mode: bool,
458    ignore_resources: bool,
459}
460
461impl CopperRuntimeArgs {
462    fn parse_tokens(args: proc_macro2::TokenStream) -> Result<Self, syn::Error> {
463        let mut config_file: Option<LitStr> = None;
464        let mut subsystem_id: Option<LitStr> = None;
465        let mut sim_mode = false;
466        let mut ignore_resources = false;
467
468        let parser = parser(|meta| {
469            if meta.path.is_ident("config") {
470                config_file = Some(meta.value()?.parse()?);
471                Ok(())
472            } else if meta.path.is_ident("subsystem") {
473                subsystem_id = Some(meta.value()?.parse()?);
474                Ok(())
475            } else if meta.path.is_ident("sim_mode") {
476                if meta.input.peek(syn::Token![=]) {
477                    meta.input.parse::<syn::Token![=]>()?;
478                    let value: syn::LitBool = meta.input.parse()?;
479                    sim_mode = value.value();
480                } else {
481                    sim_mode = true;
482                }
483                Ok(())
484            } else if meta.path.is_ident("ignore_resources") {
485                if meta.input.peek(syn::Token![=]) {
486                    meta.input.parse::<syn::Token![=]>()?;
487                    let value: syn::LitBool = meta.input.parse()?;
488                    ignore_resources = value.value();
489                } else {
490                    ignore_resources = true;
491                }
492                Ok(())
493            } else {
494                Err(meta.error("unsupported property"))
495            }
496        });
497
498        parser.parse2(args)?;
499
500        let config_path = config_file
501            .ok_or_else(|| {
502                syn::Error::new(
503                    Span::call_site(),
504                    "Expected config file attribute like #[copper_runtime(config = \"path\")]",
505                )
506            })?
507            .value();
508
509        Ok(Self {
510            config_path,
511            subsystem_id: subsystem_id.map(|value| value.value()),
512            sim_mode,
513            ignore_resources,
514        })
515    }
516}
517
518#[derive(Debug)]
519struct ResolvedRuntimeConfig {
520    local_config: CuConfig,
521    bundled_local_config_content: String,
522    active_features: Vec<String>,
523    subsystem_id: Option<String>,
524    subsystem_code: u16,
525}
526
527#[proc_macro]
528pub fn resources(input: TokenStream) -> TokenStream {
529    resources::resources(input)
530}
531
532#[proc_macro]
533pub fn bundle_resources(input: TokenStream) -> TokenStream {
534    bundle_resources::bundle_resources(input)
535}
536
537#[derive(Debug, Clone)]
538struct ParsedSafetyCheck {
539    check_id: String,
540    requirement_id: String,
541    kind: &'static str,
542}
543
544#[proc_macro_attribute]
545pub fn safety_case(args: TokenStream, input: TokenStream) -> TokenStream {
546    let case_id = parse_macro_input!(args as LitStr).value();
547    if let Err(err) = validate_case_id(&case_id) {
548        return err.to_compile_error().into();
549    }
550
551    let function = parse_macro_input!(input as ItemFn);
552    let checks = match collect_safety_checks(&case_id, &function.block) {
553        Ok(checks) => checks,
554        Err(err) => return err.to_compile_error().into(),
555    };
556
557    if checks.is_empty() {
558        return syn::Error::new_spanned(
559            &function.sig.ident,
560            format!("safety case '{case_id}' must contain at least one safety_check! or safety_check_eq!"),
561        )
562        .to_compile_error()
563        .into();
564    }
565
566    let function_ident = &function.sig.ident;
567    let checks_tokens = checks.iter().map(|check| {
568        let check_id = &check.check_id;
569        let requirement_id = &check.requirement_id;
570        let kind = check.kind;
571        quote! {
572            ::cu29::safety::SafetyCheckRef {
573                check_id: #check_id,
574                requirement_id: #requirement_id,
575                kind: #kind,
576            }
577        }
578    });
579
580    quote! {
581        #function
582
583        #[cfg(feature = "safety-ids")]
584        ::cu29::safety::inventory::submit! {
585            ::cu29::safety::SafetyCaseRef {
586                package: env!("CARGO_PKG_NAME"),
587                case_id: #case_id,
588                function: stringify!(#function_ident),
589                module_path: module_path!(),
590                file: file!(),
591                checks: &[#(#checks_tokens),*],
592            }
593        }
594    }
595    .into()
596}
597
598fn collect_safety_checks(
599    case_id: &str,
600    block: &Block,
601) -> Result<Vec<ParsedSafetyCheck>, syn::Error> {
602    let mut checks = Vec::new();
603    collect_safety_checks_from_block(block, &mut checks)?;
604
605    let mut ids = BTreeSet::new();
606    for check in &checks {
607        validate_check_id(case_id, &check.check_id)?;
608        validate_requirement_id(&check.requirement_id)?;
609        if !ids.insert(check.check_id.clone()) {
610            return Err(syn::Error::new(
611                Span::call_site(),
612                format!("duplicate safety check ID '{}'", check.check_id),
613            ));
614        }
615    }
616
617    Ok(checks)
618}
619
620fn collect_safety_checks_from_block(
621    block: &Block,
622    checks: &mut Vec<ParsedSafetyCheck>,
623) -> Result<(), syn::Error> {
624    for stmt in &block.stmts {
625        collect_safety_checks_from_stmt(stmt, checks)?;
626    }
627    Ok(())
628}
629
630fn collect_safety_checks_from_stmt(
631    stmt: &Stmt,
632    checks: &mut Vec<ParsedSafetyCheck>,
633) -> Result<(), syn::Error> {
634    match stmt {
635        Stmt::Local(local) => {
636            if let Some(init) = &local.init {
637                collect_safety_checks_from_expr(&init.expr, checks)?;
638                if let Some((_else, expr)) = &init.diverge {
639                    collect_safety_checks_from_expr(expr, checks)?;
640                }
641            }
642        }
643        Stmt::Item(_) => {}
644        Stmt::Expr(expr, _) => collect_safety_checks_from_expr(expr, checks)?,
645        Stmt::Macro(stmt_macro) => {
646            if let Some(check) = parse_safety_check_macro(&stmt_macro.mac)? {
647                checks.push(check);
648            }
649        }
650    }
651    Ok(())
652}
653
654fn collect_safety_checks_from_expr(
655    expr: &Expr,
656    checks: &mut Vec<ParsedSafetyCheck>,
657) -> Result<(), syn::Error> {
658    match expr {
659        Expr::Array(expr) => {
660            for elem in &expr.elems {
661                collect_safety_checks_from_expr(elem, checks)?;
662            }
663        }
664        Expr::Assign(expr) => {
665            collect_safety_checks_from_expr(&expr.left, checks)?;
666            collect_safety_checks_from_expr(&expr.right, checks)?;
667        }
668        Expr::Async(expr) => collect_safety_checks_from_block(&expr.block, checks)?,
669        Expr::Await(expr) => collect_safety_checks_from_expr(&expr.base, checks)?,
670        Expr::Binary(expr) => {
671            collect_safety_checks_from_expr(&expr.left, checks)?;
672            collect_safety_checks_from_expr(&expr.right, checks)?;
673        }
674        Expr::Block(expr) => collect_safety_checks_from_block(&expr.block, checks)?,
675        Expr::Break(expr) => {
676            if let Some(value) = &expr.expr {
677                collect_safety_checks_from_expr(value, checks)?;
678            }
679        }
680        Expr::Call(expr) => {
681            collect_safety_checks_from_expr(&expr.func, checks)?;
682            for arg in &expr.args {
683                collect_safety_checks_from_expr(arg, checks)?;
684            }
685        }
686        Expr::Cast(expr) => collect_safety_checks_from_expr(&expr.expr, checks)?,
687        Expr::Closure(expr) => collect_safety_checks_from_expr(&expr.body, checks)?,
688        Expr::Field(expr) => collect_safety_checks_from_expr(&expr.base, checks)?,
689        Expr::ForLoop(expr) => {
690            collect_safety_checks_from_expr(&expr.expr, checks)?;
691            collect_safety_checks_from_block(&expr.body, checks)?;
692        }
693        Expr::Group(expr) => collect_safety_checks_from_expr(&expr.expr, checks)?,
694        Expr::If(expr) => {
695            collect_safety_checks_from_expr(&expr.cond, checks)?;
696            collect_safety_checks_from_block(&expr.then_branch, checks)?;
697            if let Some((_else, else_expr)) = &expr.else_branch {
698                collect_safety_checks_from_expr(else_expr, checks)?;
699            }
700        }
701        Expr::Index(expr) => {
702            collect_safety_checks_from_expr(&expr.expr, checks)?;
703            collect_safety_checks_from_expr(&expr.index, checks)?;
704        }
705        Expr::Let(expr) => collect_safety_checks_from_expr(&expr.expr, checks)?,
706        Expr::Loop(expr) => collect_safety_checks_from_block(&expr.body, checks)?,
707        Expr::Macro(expr_macro) => {
708            if let Some(check) = parse_safety_check_macro(&expr_macro.mac)? {
709                checks.push(check);
710            }
711        }
712        Expr::Match(expr) => {
713            collect_safety_checks_from_expr(&expr.expr, checks)?;
714            for arm in &expr.arms {
715                if let syn::Pat::Guard(pat_guard) = &arm.pat {
716                    collect_safety_checks_from_expr(&pat_guard.guard, checks)?;
717                }
718                collect_safety_checks_from_expr(&arm.body, checks)?;
719            }
720        }
721        Expr::MethodCall(expr) => {
722            collect_safety_checks_from_expr(&expr.receiver, checks)?;
723            for arg in &expr.args {
724                collect_safety_checks_from_expr(arg, checks)?;
725            }
726        }
727        Expr::Paren(expr) => collect_safety_checks_from_expr(&expr.expr, checks)?,
728        Expr::Reference(expr) => collect_safety_checks_from_expr(&expr.expr, checks)?,
729        Expr::Repeat(expr) => collect_safety_checks_from_expr(&expr.expr, checks)?,
730        Expr::Return(expr) => {
731            if let Some(value) = &expr.expr {
732                collect_safety_checks_from_expr(value, checks)?;
733            }
734        }
735        Expr::Struct(expr) => {
736            for field in &expr.fields {
737                collect_safety_checks_from_expr(&field.expr, checks)?;
738            }
739            if let Some(rest) = &expr.rest {
740                collect_safety_checks_from_expr(rest, checks)?;
741            }
742        }
743        Expr::Try(expr) => collect_safety_checks_from_expr(&expr.expr, checks)?,
744        Expr::TryBlock(expr) => collect_safety_checks_from_block(&expr.block, checks)?,
745        Expr::Tuple(expr) => {
746            for elem in &expr.elems {
747                collect_safety_checks_from_expr(elem, checks)?;
748            }
749        }
750        Expr::Unary(expr) => collect_safety_checks_from_expr(&expr.expr, checks)?,
751        Expr::Unsafe(expr) => collect_safety_checks_from_block(&expr.block, checks)?,
752        Expr::While(expr) => {
753            collect_safety_checks_from_expr(&expr.cond, checks)?;
754            collect_safety_checks_from_block(&expr.body, checks)?;
755        }
756        Expr::Yield(expr) => {
757            if let Some(value) = &expr.expr {
758                collect_safety_checks_from_expr(value, checks)?;
759            }
760        }
761        _ => {}
762    }
763    Ok(())
764}
765
766fn parse_safety_check_macro(mac: &syn::Macro) -> Result<Option<ParsedSafetyCheck>, syn::Error> {
767    let Some(segment) = mac.path.segments.last() else {
768        return Ok(None);
769    };
770
771    let kind = match segment.ident.to_string().as_str() {
772        "safety_check" => "assert",
773        "safety_check_eq" => "assert_eq",
774        _ => return Ok(None),
775    };
776
777    let args = Punctuated::<Expr, Token![,]>::parse_terminated.parse2(mac.tokens.clone())?;
778    let min_args = if kind == "assert_eq" { 4 } else { 3 };
779    if args.len() < min_args {
780        return Err(syn::Error::new_spanned(
781            mac,
782            format!(
783                "{}! expects at least {} arguments: check ID, requirement ID, and assertion inputs",
784                segment.ident, min_args
785            ),
786        ));
787    }
788
789    let check_id = string_literal_from_expr(&args[0], "safety check ID")?;
790    let requirement_id = string_literal_from_expr(&args[1], "requirement ID")?;
791
792    Ok(Some(ParsedSafetyCheck {
793        check_id,
794        requirement_id,
795        kind,
796    }))
797}
798
799fn string_literal_from_expr(expr: &Expr, label: &str) -> Result<String, syn::Error> {
800    let Expr::Lit(expr_lit) = expr else {
801        return Err(syn::Error::new_spanned(
802            expr,
803            format!("{label} must be a string literal"),
804        ));
805    };
806    let Lit::Str(value) = &expr_lit.lit else {
807        return Err(syn::Error::new_spanned(
808            expr,
809            format!("{label} must be a string literal"),
810        ));
811    };
812    Ok(value.value())
813}
814
815fn validate_case_id(case_id: &str) -> Result<(), syn::Error> {
816    validate_id(case_id, "TEST", false)
817}
818
819fn validate_check_id(case_id: &str, check_id: &str) -> Result<(), syn::Error> {
820    validate_id(check_id, "TEST", true)?;
821    if !check_id.starts_with(case_id) {
822        return Err(syn::Error::new(
823            Span::call_site(),
824            format!("safety check ID '{check_id}' must start with case ID '{case_id}'"),
825        ));
826    }
827    Ok(())
828}
829
830fn validate_requirement_id(requirement_id: &str) -> Result<(), syn::Error> {
831    validate_id(requirement_id, "REQ", false)
832}
833
834fn validate_id(value: &str, kind: &str, allow_check_suffix: bool) -> Result<(), syn::Error> {
835    let mut parts = value.split('-');
836    let Some(prefix) = parts.next() else {
837        return invalid_id(value, kind, allow_check_suffix);
838    };
839    let Some(actual_kind) = parts.next() else {
840        return invalid_id(value, kind, allow_check_suffix);
841    };
842    let Some(number) = parts.next() else {
843        return invalid_id(value, kind, allow_check_suffix);
844    };
845
846    if !prefix.chars().all(|ch| ch.is_ascii_uppercase())
847        || actual_kind != kind
848        || number.len() != 3
849        || !number.chars().all(|ch| ch.is_ascii_digit())
850    {
851        return invalid_id(value, kind, allow_check_suffix);
852    }
853
854    match parts.next() {
855        None => Ok(()),
856        Some(check_suffix) if allow_check_suffix && check_suffix.starts_with('C') => {
857            let digits = &check_suffix[1..];
858            if digits.is_empty() || !digits.chars().all(|ch| ch.is_ascii_digit()) {
859                return invalid_id(value, kind, allow_check_suffix);
860            }
861            if parts.next().is_some() {
862                return invalid_id(value, kind, allow_check_suffix);
863            }
864            Ok(())
865        }
866        _ => invalid_id(value, kind, allow_check_suffix),
867    }
868}
869
870fn invalid_id(value: &str, kind: &str, allow_check_suffix: bool) -> Result<(), syn::Error> {
871    let suffix = if allow_check_suffix {
872        " or PREFIX-TEST-001-C1"
873    } else {
874        ""
875    };
876    Err(syn::Error::new(
877        Span::call_site(),
878        format!("invalid ID '{value}', expected PREFIX-{kind}-001{suffix}"),
879    ))
880}
881
882/// Generates the CopperList content type from a config.
883/// gen_cumsgs!("path/to/config.toml")
884/// It will create a new type called CuStampedDataSet you can pass to the log reader for decoding:
885#[proc_macro]
886pub fn gen_cumsgs(config_path_lit: TokenStream) -> TokenStream {
887    #[cfg(feature = "std")]
888    let std = true;
889
890    #[cfg(not(feature = "std"))]
891    let std = false;
892    let config = parse_macro_input!(config_path_lit as LitStr).value();
893    if !std::path::Path::new(&config_full_path(&config)).exists() {
894        return return_error(format!(
895            "The configuration file `{config}` does not exist. Please provide a valid path."
896        ));
897    }
898    #[cfg(feature = "macro_debug")]
899    eprintln!("[gen culist support with {config:?}]");
900    let mut cuconfig = match read_config(&config) {
901        Ok(cuconfig) => cuconfig,
902        Err(e) => return return_error(e.to_string()),
903    };
904    if let Err(e) = apply_external_plan(&mut cuconfig) {
905        return return_error(e.to_string());
906    }
907    let cuconfig = cuconfig;
908
909    let extra_imports = if !std {
910        quote! {
911            use core::fmt::Debug;
912            use core::fmt::Formatter;
913            use core::fmt::Result as FmtResult;
914            use alloc::vec;
915            use alloc::vec::Vec;
916        }
917    } else {
918        quote! {
919            use std::fmt::Debug;
920            use std::fmt::Formatter;
921            use std::fmt::Result as FmtResult;
922        }
923    };
924
925    let common_imports = quote! {
926        use cu29::bincode::Encode;
927        use cu29::bincode::enc::Encoder;
928        use cu29::bincode::error::EncodeError;
929        use cu29::bincode::Decode;
930        use cu29::bincode::de::Decoder;
931        use cu29::bincode::error::DecodeError;
932        use cu29::copperlist::CopperList;
933        use cu29::prelude::ErasedCuStampedData;
934        use cu29::prelude::ErasedCuStampedDataSet;
935        use cu29::prelude::MatchingTasks;
936        use cu29::prelude::CuMsg;
937        use cu29::prelude::CuMsgMetadata;
938        use cu29::prelude::CuListZeroedInit;
939        use cu29::prelude::CuCompactString;
940        #extra_imports
941    };
942
943    let with_uses = match &cuconfig.graphs {
944        ConfigGraphs::Simple(graph) => {
945            let support = match build_gen_cumsgs_support(&cuconfig, graph, None) {
946                Ok(support) => support,
947                Err(e) => return return_error(e.to_string()),
948            };
949
950            quote! {
951                mod cumsgs {
952                    #common_imports
953                    #support
954                }
955                use cumsgs::CuStampedDataSet;
956                type CuMsgs=CuStampedDataSet;
957            }
958        }
959        ConfigGraphs::Missions(graphs) => {
960            let mut missions: Vec<_> = graphs.iter().collect();
961            missions.sort_by(|a, b| a.0.cmp(b.0));
962
963            let mut mission_modules = Vec::<proc_macro2::TokenStream>::new();
964            for (mission, graph) in missions {
965                let mission_mod = match parse_str::<Ident>(mission.as_str()) {
966                    Ok(id) => id,
967                    Err(_) => {
968                        return return_error(format!(
969                            "Mission '{mission}' is not a valid Rust identifier for gen_cumsgs output."
970                        ));
971                    }
972                };
973
974                let support = match build_gen_cumsgs_support(&cuconfig, graph, Some(mission)) {
975                    Ok(support) => support,
976                    Err(e) => return return_error(e.to_string()),
977                };
978
979                mission_modules.push(quote! {
980                    pub mod #mission_mod {
981                        #common_imports
982                        #support
983                    }
984                });
985            }
986
987            let default_exports = if graphs.contains_key("default") {
988                quote! {
989                    use cumsgs::default::CuStampedDataSet;
990                    type CuMsgs=CuStampedDataSet;
991                }
992            } else {
993                quote! {}
994            };
995
996            quote! {
997                mod cumsgs {
998                    #(#mission_modules)*
999                }
1000                #default_exports
1001            }
1002        }
1003    };
1004    with_uses.into()
1005}
1006
1007fn build_gen_cumsgs_support(
1008    cuconfig: &CuConfig,
1009    graph: &CuGraph,
1010    mission_label: Option<&str>,
1011) -> CuResult<proc_macro2::TokenStream> {
1012    let channel_usage = collect_bridge_channel_usage(graph);
1013    let mut bridge_specs = build_bridge_specs(cuconfig, graph, &channel_usage);
1014    let (culist_plan, exec_entities, plan_to_original) = build_execution_plan(
1015        cuconfig,
1016        graph,
1017        mission_label.unwrap_or(DEFAULT_MISSION_ID),
1018        &mut bridge_specs,
1019    )
1020    .map_err(|e| {
1021        if let Some(mission) = mission_label {
1022            CuError::from(format!(
1023                "Could not compute copperlist plan for mission '{mission}': {e}"
1024            ))
1025        } else {
1026            CuError::from(format!("Could not compute copperlist plan: {e}"))
1027        }
1028    })?;
1029    let task_names = collect_task_names(graph);
1030    let (culist_order, node_output_positions) = collect_culist_metadata(
1031        &culist_plan,
1032        &exec_entities,
1033        &mut bridge_specs,
1034        &plan_to_original,
1035    );
1036
1037    #[cfg(feature = "macro_debug")]
1038    if let Some(mission) = mission_label {
1039        eprintln!(
1040            "[The CuStampedDataSet matching tasks ids for mission '{mission}' are {:?}]",
1041            culist_order
1042        );
1043    } else {
1044        eprintln!(
1045            "[The CuStampedDataSet matching tasks ids are {:?}]",
1046            culist_order
1047        );
1048    }
1049
1050    Ok(gen_culist_support(
1051        cuconfig,
1052        mission_label,
1053        &culist_plan,
1054        &culist_order,
1055        &node_output_positions,
1056        &task_names,
1057        &bridge_specs,
1058    ))
1059}
1060
1061/// Build the inner support of the copper list.
1062fn gen_culist_support(
1063    cuconfig: &CuConfig,
1064    mission_label: Option<&str>,
1065    runtime_plan: &CuExecutionLoop,
1066    culist_indices_in_plan_order: &[usize],
1067    node_output_positions: &HashMap<NodeId, usize>,
1068    task_names: &[(NodeId, String, String)],
1069    bridge_specs: &[BridgeSpec],
1070) -> proc_macro2::TokenStream {
1071    #[cfg(feature = "macro_debug")]
1072    eprintln!("[Extract msgs types]");
1073    let output_packs = extract_output_packs(runtime_plan);
1074    let slot_types: Vec<Type> = output_packs.iter().map(|pack| pack.slot_type()).collect();
1075
1076    let culist_size = output_packs.len();
1077
1078    #[cfg(feature = "macro_debug")]
1079    eprintln!("[build the copperlist struct]");
1080    let msgs_types_tuple: TypeTuple = build_culist_tuple(&slot_types);
1081    let cumsg_count: usize = output_packs.iter().map(|pack| pack.msg_types.len()).sum();
1082    let flat_codec_bindings = build_flat_slot_codec_bindings(
1083        cuconfig,
1084        mission_label,
1085        &output_packs,
1086        node_output_positions,
1087        task_names,
1088    )
1089    .unwrap_or_else(|err| panic!("Could not resolve log codec bindings: {err}"));
1090    let default_config_ron_ident = format_ident!("__CU_LOGCODEC_DEFAULT_CONFIG_RON");
1091    let default_config_ron = cuconfig
1092        .serialize_ron()
1093        .unwrap_or_else(|_| "<failed to serialize config>".to_string());
1094    let default_config_ron_lit = LitStr::new(&default_config_ron, Span::call_site());
1095    let (codec_helper_fns, encode_helper_names, decode_helper_names) = build_culist_codec_helpers(
1096        &flat_codec_bindings,
1097        &default_config_ron_ident,
1098        mission_label,
1099    );
1100    let default_config_ron_const = if flat_codec_bindings.iter().any(Option::is_some) {
1101        quote! {
1102            const #default_config_ron_ident: &str = #default_config_ron_lit;
1103        }
1104    } else {
1105        quote! {}
1106    };
1107
1108    #[cfg(feature = "macro_debug")]
1109    eprintln!("[build the copperlist tuple bincode support]");
1110    let slot_handle_modes = build_slot_handle_modes(
1111        cuconfig,
1112        mission_label,
1113        &output_packs,
1114        node_output_positions,
1115        task_names,
1116    );
1117    let compressed = !cfg!(feature = "flat-copperlist-encoding");
1118    let (msgs_types_tuple_encode, msgs_types_tuple_decode) = if compressed {
1119        (
1120            build_compressed_culist_tuple_encode(
1121                &output_packs,
1122                &encode_helper_names,
1123                &slot_handle_modes,
1124                cumsg_count,
1125                None,
1126            ),
1127            build_compressed_culist_tuple_decode(
1128                &output_packs,
1129                &slot_types,
1130                cumsg_count,
1131                &decode_helper_names,
1132            ),
1133        )
1134    } else {
1135        (
1136            build_culist_tuple_encode(&output_packs, &encode_helper_names, &slot_handle_modes),
1137            build_culist_tuple_decode(
1138                &output_packs,
1139                &slot_types,
1140                cumsg_count,
1141                &decode_helper_names,
1142            ),
1143        )
1144    };
1145
1146    #[cfg(feature = "macro_debug")]
1147    eprintln!("[build the copperlist tuple debug support]");
1148    let msgs_types_tuple_debug = build_culist_tuple_debug(&slot_types);
1149
1150    #[cfg(feature = "macro_debug")]
1151    eprintln!("[build the copperlist tuple serialize support]");
1152    let msgs_types_tuple_serialize = build_culist_tuple_serialize(&slot_types);
1153
1154    #[cfg(feature = "macro_debug")]
1155    eprintln!("[build the default tuple support]");
1156    let msgs_types_tuple_default = build_culist_tuple_default(&slot_types, cumsg_count);
1157
1158    #[cfg(feature = "macro_debug")]
1159    eprintln!("[build erasedcumsgs]");
1160
1161    let erasedmsg_trait_impl = build_culist_erasedcumsgs(&output_packs);
1162
1163    let metadata_accessors: Vec<proc_macro2::TokenStream> = culist_indices_in_plan_order
1164        .iter()
1165        .map(|idx| {
1166            let slot_index = syn::Index::from(*idx);
1167            let pack = output_packs
1168                .get(*idx)
1169                .unwrap_or_else(|| panic!("Missing output pack for index {idx}"));
1170            if pack.is_multi() {
1171                quote! { &culist.msgs.0.#slot_index.0.metadata }
1172            } else {
1173                quote! { &culist.msgs.0.#slot_index.metadata }
1174            }
1175        })
1176        .collect();
1177    let mut zeroed_init_tokens: Vec<proc_macro2::TokenStream> = Vec::new();
1178    let mut in_place_init_tokens: Vec<proc_macro2::TokenStream> = Vec::new();
1179    for idx in culist_indices_in_plan_order {
1180        let slot_index = syn::Index::from(*idx);
1181        let pack = output_packs
1182            .get(*idx)
1183            .unwrap_or_else(|| panic!("Missing output pack for index {idx}"));
1184        if pack.is_multi() {
1185            for port_idx in 0..pack.msg_types.len() {
1186                let port_index = syn::Index::from(port_idx);
1187                in_place_init_tokens.push(quote! {
1188                    CuMsg::init_in_place(core::ptr::addr_of_mut!((*dst).0.#slot_index.#port_index));
1189                });
1190                zeroed_init_tokens.push(quote! {
1191                    self.0.#slot_index.#port_index.metadata.status_txt = CuCompactString::default();
1192                    self.0.#slot_index.#port_index.metadata.process_time.start =
1193                        cu29::clock::OptionCuTime::none();
1194                    self.0.#slot_index.#port_index.metadata.process_time.end =
1195                        cu29::clock::OptionCuTime::none();
1196                    self.0.#slot_index.#port_index.metadata.origin = None;
1197                });
1198            }
1199        } else {
1200            in_place_init_tokens.push(quote! {
1201                CuMsg::init_in_place(core::ptr::addr_of_mut!((*dst).0.#slot_index));
1202            });
1203            zeroed_init_tokens.push(quote! {
1204                self.0.#slot_index.metadata.status_txt = CuCompactString::default();
1205                self.0.#slot_index.metadata.process_time.start = cu29::clock::OptionCuTime::none();
1206                self.0.#slot_index.metadata.process_time.end = cu29::clock::OptionCuTime::none();
1207                self.0.#slot_index.metadata.origin = None;
1208            });
1209        }
1210    }
1211    let collect_metadata_function = quote! {
1212        pub fn collect_metadata<'a>(culist: &'a CuList) -> [&'a CuMsgMetadata; #culist_size] {
1213            [#( #metadata_accessors, )*]
1214        }
1215    };
1216
1217    let payload_bytes_accumulators: Vec<proc_macro2::TokenStream> = culist_indices_in_plan_order
1218        .iter()
1219        .scan(0usize, |flat_idx, idx| {
1220            let slot_index = syn::Index::from(*idx);
1221            let pack = output_packs
1222                .get(*idx)
1223                .unwrap_or_else(|| panic!("Missing output pack for index {idx}"));
1224            if pack.is_multi() {
1225                let iter = (0..pack.msg_types.len()).map(|port_idx| {
1226                    let port_index = syn::Index::from(port_idx);
1227                    let cache_index = syn::Index::from(*flat_idx);
1228                    *flat_idx += 1;
1229                    quote! {
1230                        if let Some(payload) = culist.msgs.0.#slot_index.#port_index.payload() {
1231                            let cached = culist.msgs.1.get(#cache_index);
1232                            let io = if cached.present {
1233                                cu29::monitoring::PayloadIoStats {
1234                                    resident_bytes: cached.resident_bytes as usize,
1235                                    encoded_bytes: cached.encoded_bytes as usize,
1236                                    handle_bytes: cached.handle_bytes as usize,
1237                                }
1238                            } else {
1239                                cu29::monitoring::payload_io_stats(payload)?
1240                            };
1241                            raw += io.resident_bytes;
1242                            handles += io.handle_bytes;
1243                        }
1244                    }
1245                });
1246                Some(quote! { #(#iter)* })
1247            } else {
1248                let cache_index = syn::Index::from(*flat_idx);
1249                *flat_idx += 1;
1250                Some(quote! {
1251                    if let Some(payload) = culist.msgs.0.#slot_index.payload() {
1252                        let cached = culist.msgs.1.get(#cache_index);
1253                        let io = if cached.present {
1254                            cu29::monitoring::PayloadIoStats {
1255                                resident_bytes: cached.resident_bytes as usize,
1256                                encoded_bytes: cached.encoded_bytes as usize,
1257                                handle_bytes: cached.handle_bytes as usize,
1258                            }
1259                        } else {
1260                            cu29::monitoring::payload_io_stats(payload)?
1261                        };
1262                        raw += io.resident_bytes;
1263                        handles += io.handle_bytes;
1264                    }
1265                })
1266            }
1267        })
1268        .collect();
1269
1270    let payload_raw_bytes_accumulators: Vec<proc_macro2::TokenStream> = output_packs
1271        .iter()
1272        .enumerate()
1273        .scan(0usize, |flat_idx, (slot_idx, pack)| {
1274            let slot_index = syn::Index::from(slot_idx);
1275            if pack.is_multi() {
1276                let iter = (0..pack.msg_types.len()).map(|port_idx| {
1277                    let port_index = syn::Index::from(port_idx);
1278                    let cache_index = syn::Index::from(*flat_idx);
1279                    *flat_idx += 1;
1280                    quote! {
1281                        if let Some(payload) = self.0.#slot_index.#port_index.payload() {
1282                            let cached = self.1.get(#cache_index);
1283                            bytes.push(if cached.present {
1284                                Some(cached.resident_bytes)
1285                            } else {
1286                                cu29::monitoring::payload_io_stats(payload)
1287                                    .ok()
1288                                    .map(|io| io.resident_bytes as u64)
1289                            });
1290                        } else {
1291                            bytes.push(None);
1292                        }
1293                    }
1294                });
1295                Some(quote! { #(#iter)* })
1296            } else {
1297                let cache_index = syn::Index::from(*flat_idx);
1298                *flat_idx += 1;
1299                Some(quote! {
1300                    if let Some(payload) = self.0.#slot_index.payload() {
1301                        let cached = self.1.get(#cache_index);
1302                        bytes.push(if cached.present {
1303                            Some(cached.resident_bytes)
1304                        } else {
1305                            cu29::monitoring::payload_io_stats(payload)
1306                                .ok()
1307                                .map(|io| io.resident_bytes as u64)
1308                        });
1309                    } else {
1310                        bytes.push(None);
1311                    }
1312                })
1313            }
1314        })
1315        .collect();
1316
1317    let compute_payload_bytes_fn = quote! {
1318        pub fn compute_payload_bytes(culist: &CuList) -> cu29::prelude::CuResult<(u64, u64)> {
1319            let mut raw: usize = 0;
1320            let mut handles: usize = 0;
1321            #(#payload_bytes_accumulators)*
1322            Ok((raw as u64, handles as u64))
1323        }
1324    };
1325
1326    let payload_raw_bytes_impl = quote! {
1327        impl ::cu29::CuPayloadRawBytes for CuStampedDataSet {
1328            fn payload_raw_bytes(&self) -> Vec<Option<u64>> {
1329                let mut bytes: Vec<Option<u64>> = Vec::with_capacity(#cumsg_count);
1330                #(#payload_raw_bytes_accumulators)*
1331                bytes
1332            }
1333        }
1334    };
1335
1336    let mut slot_origin_ids: Vec<Option<String>> = vec![None; output_packs.len()];
1337    let mut slot_task_names: Vec<Option<String>> = vec![None; output_packs.len()];
1338
1339    let mut methods = Vec::new();
1340    for (node_id, task_id, member_name) in task_names {
1341        let output_position = node_output_positions.get(node_id).unwrap_or_else(|| {
1342            panic!("Task {task_id} (node id: {node_id}) not found in execution order")
1343        });
1344        let pack = output_packs
1345            .get(*output_position)
1346            .unwrap_or_else(|| panic!("Missing output pack for task {task_id}"));
1347        let slot_index = syn::Index::from(*output_position);
1348        slot_origin_ids[*output_position] = Some(task_id.clone());
1349        slot_task_names[*output_position] = Some(member_name.clone());
1350
1351        if pack.msg_types.len() == 1 {
1352            let fn_name = format_ident!("get_{}_output", member_name);
1353            let payload_type = pack.msg_types.first().unwrap();
1354            methods.push(quote! {
1355                #[allow(dead_code)]
1356                pub fn #fn_name(&self) -> &CuMsg<#payload_type> {
1357                    &self.0.#slot_index
1358                }
1359            });
1360        } else {
1361            let outputs_fn = format_ident!("get_{}_outputs", member_name);
1362            let slot_type = pack.slot_type();
1363            for (port_idx, payload_type) in pack.msg_types.iter().enumerate() {
1364                let fn_name = format_ident!("get_{}_output_{}", member_name, port_idx);
1365                let port_index = syn::Index::from(port_idx);
1366                methods.push(quote! {
1367                    #[allow(dead_code)]
1368                    pub fn #fn_name(&self) -> &CuMsg<#payload_type> {
1369                        &self.0.#slot_index.#port_index
1370                    }
1371                });
1372            }
1373            methods.push(quote! {
1374                #[allow(dead_code)]
1375                pub fn #outputs_fn(&self) -> &#slot_type {
1376                    &self.0.#slot_index
1377                }
1378            });
1379        }
1380    }
1381
1382    for spec in bridge_specs {
1383        for channel in &spec.rx_channels {
1384            if let Some(culist_index) = channel.culist_index {
1385                let origin_id = format!("bridge::{}::rx::{}", spec.id, channel.id);
1386                let Some(existing_slot) = slot_origin_ids.get_mut(culist_index) else {
1387                    panic!(
1388                        "Bridge origin '{origin_id}' points to out-of-range copperlist slot {culist_index}"
1389                    );
1390                };
1391                if let Some(existing) = existing_slot.as_ref() {
1392                    panic!(
1393                        "Duplicate slot origin assignment for slot {culist_index}: '{existing}' and '{origin_id}'"
1394                    );
1395                }
1396                *existing_slot = Some(origin_id.clone());
1397                let Some(slot_name) = slot_task_names.get_mut(culist_index) else {
1398                    panic!(
1399                        "Bridge origin '{origin_id}' points to out-of-range name slot {culist_index}"
1400                    );
1401                };
1402                *slot_name = Some(origin_id);
1403            }
1404        }
1405        for channel in &spec.tx_channels {
1406            if let Some(culist_index) = channel.culist_index {
1407                let origin_id = format!("bridge::{}::tx::{}", spec.id, channel.id);
1408                let Some(existing_slot) = slot_origin_ids.get_mut(culist_index) else {
1409                    panic!(
1410                        "Bridge origin '{origin_id}' points to out-of-range copperlist slot {culist_index}"
1411                    );
1412                };
1413                if let Some(existing) = existing_slot.as_ref() {
1414                    panic!(
1415                        "Duplicate slot origin assignment for slot {culist_index}: '{existing}' and '{origin_id}'"
1416                    );
1417                }
1418                *existing_slot = Some(origin_id.clone());
1419                let Some(slot_name) = slot_task_names.get_mut(culist_index) else {
1420                    panic!(
1421                        "Bridge origin '{origin_id}' points to out-of-range name slot {culist_index}"
1422                    );
1423                };
1424                *slot_name = Some(origin_id);
1425            }
1426        }
1427    }
1428
1429    let task_name_literals = flatten_slot_origin_ids(&output_packs, &slot_origin_ids);
1430    let task_output_specs = flatten_task_output_specs(&output_packs, &slot_origin_ids);
1431    let task_output_spec_literals: Vec<proc_macro2::TokenStream> = task_output_specs
1432        .iter()
1433        .map(|(task_id, msg_type, payload_type)| {
1434            let task_id = LitStr::new(task_id, Span::call_site());
1435            let msg_type = LitStr::new(msg_type, Span::call_site());
1436            quote! {
1437                cu29::TaskOutputSpec::new::<#payload_type>(#task_id, #msg_type)
1438            }
1439        })
1440        .collect();
1441
1442    // Generate bridge channel getter methods
1443    for spec in bridge_specs {
1444        for channel in &spec.rx_channels {
1445            if let Some(culist_index) = channel.culist_index {
1446                let slot_index = syn::Index::from(culist_index);
1447                let bridge_name = config_id_to_struct_member(spec.id.as_str());
1448                let channel_name = config_id_to_struct_member(channel.id.as_str());
1449                let fn_name = format_ident!("get_{}_rx_{}", bridge_name, channel_name);
1450                let msg_type = &channel.msg_type;
1451
1452                methods.push(quote! {
1453                    #[allow(dead_code)]
1454                    pub fn #fn_name(&self) -> &CuMsg<#msg_type> {
1455                        &self.0.#slot_index
1456                    }
1457                });
1458            }
1459        }
1460    }
1461
1462    let capture_support = if cfg!(feature = "logstream") {
1463        live_twin::dataset_support(
1464            runtime_plan,
1465            &output_packs,
1466            &encode_helper_names,
1467            &slot_handle_modes,
1468            cumsg_count,
1469            &flat_codec_bindings,
1470        )
1471    } else {
1472        quote! {}
1473    };
1474
1475    // This generates a way to get the metadata of every single message of a culist at low cost
1476    quote! {
1477        #capture_support
1478        #collect_metadata_function
1479        #compute_payload_bytes_fn
1480        #default_config_ron_const
1481        #(#codec_helper_fns)*
1482
1483        pub struct CuStampedDataSet(pub #msgs_types_tuple, cu29::monitoring::CuMsgIoCache<#cumsg_count>);
1484
1485        pub type CuList = CopperList<CuStampedDataSet>;
1486
1487        const TASK_OUTPUT_SPECS: &[cu29::TaskOutputSpec] = &[
1488            #(#task_output_spec_literals),*
1489        ];
1490
1491        impl CuStampedDataSet {
1492            #(#methods)*
1493
1494            #[allow(dead_code)]
1495            fn get_tuple(&self) -> &#msgs_types_tuple {
1496                &self.0
1497            }
1498
1499            #[allow(dead_code)]
1500            fn get_tuple_mut(&mut self) -> &mut #msgs_types_tuple {
1501                &mut self.0
1502            }
1503        }
1504
1505        #payload_raw_bytes_impl
1506        impl MatchingTasks for CuStampedDataSet {
1507            #[allow(dead_code)]
1508            fn get_all_task_ids() -> &'static [&'static str] {
1509                &[#(#task_name_literals),*]
1510            }
1511
1512            #[allow(dead_code)]
1513            fn get_output_specs() -> &'static [cu29::TaskOutputSpec] {
1514                TASK_OUTPUT_SPECS
1515            }
1516        }
1517
1518        // Adds the bincode support for the copper list tuple
1519        #msgs_types_tuple_encode
1520        #msgs_types_tuple_decode
1521
1522        // Adds the debug support
1523        #msgs_types_tuple_debug
1524
1525        // Adds the serialization support
1526        #msgs_types_tuple_serialize
1527
1528        // Adds the default support
1529        #msgs_types_tuple_default
1530
1531        // Adds the type erased CuStampedDataSet support (to help generic serialized conversions)
1532        #erasedmsg_trait_impl
1533
1534        impl CuListZeroedInit for CuStampedDataSet {
1535            fn init_in_place(storage: &mut core::mem::MaybeUninit<Self>) -> &mut Self {
1536                let dst = storage.as_mut_ptr();
1537                // SAFETY: The pool supplies uninitialized dataset storage. Every
1538                // message and the I/O cache are written before it is borrowed.
1539                unsafe {
1540                    #(#in_place_init_tokens)*
1541                    core::ptr::addr_of_mut!((*dst).1)
1542                        .write(cu29::monitoring::CuMsgIoCache::default());
1543                    storage.assume_init_mut()
1544                }
1545            }
1546
1547            fn init_zeroed(&mut self) {
1548                self.1.clear();
1549                #(#zeroed_init_tokens)*
1550            }
1551        }
1552    }
1553}
1554
1555fn gen_sim_support(
1556    runtime_plan: &CuExecutionLoop,
1557    exec_entities: &[ExecutionEntity],
1558    bridge_specs: &[BridgeSpec],
1559) -> proc_macro2::TokenStream {
1560    #[cfg(feature = "macro_debug")]
1561    eprintln!("[Sim: Build SimEnum]");
1562    let plan_enum: Vec<proc_macro2::TokenStream> = runtime_plan
1563        .steps
1564        .iter()
1565        .filter(|unit| {
1566            // The sim callback fires once per node, in the base step; refine
1567            // steps get no SimStep variant of their own.
1568            !matches!(unit, CuExecutionUnit::Step(step) if step.phase == CuStepPhase::AnytimeRefine)
1569        })
1570        .map(|unit| match unit {
1571            CuExecutionUnit::Step(step) => match &exec_entities[step.node_id as usize].kind {
1572                ExecutionEntityKind::Task { .. } => {
1573                    let enum_entry_name = config_id_to_enum(step.node.get_id().as_str());
1574                    let enum_ident = Ident::new(&enum_entry_name, Span::call_site());
1575                    let inputs: Vec<Type> = step
1576                        .input_msg_indices_types
1577                        .iter()
1578                        .map(|input| {
1579                            parse_str::<Type>(format!("CuMsg<{}>", input.msg_type).as_str()).unwrap()
1580                        })
1581                        .collect();
1582                    let output: Option<Type> = step.output_msg_pack.as_ref().map(|pack| {
1583                        let msg_types: Vec<Type> = pack
1584                            .msg_types
1585                            .iter()
1586                            .map(|msg_type| {
1587                                parse_str::<Type>(msg_type.as_str()).unwrap_or_else(|_| {
1588                                    panic!("Could not transform {msg_type} into a message Rust type.")
1589                                })
1590                            })
1591                            .collect();
1592                        build_output_slot_type(&msg_types)
1593                    });
1594                    let no_output = parse_str::<Type>("CuMsg<()>").unwrap();
1595                    let output = output.as_ref().unwrap_or(&no_output);
1596
1597                    let inputs_type = if inputs.is_empty() {
1598                        quote! { () }
1599                    } else if inputs.len() == 1 {
1600                        let input = inputs.first().unwrap();
1601                        quote! { &'a #input }
1602                    } else {
1603                        quote! { &'a (#(&'a #inputs),*) }
1604                    };
1605
1606                    quote! {
1607                        #enum_ident(CuTaskCallbackState<#inputs_type, &'a mut #output>)
1608                    }
1609                }
1610                ExecutionEntityKind::BridgeRx { bridge_index, channel_index } => {
1611                    let bridge_spec = &bridge_specs[*bridge_index];
1612                    let channel = &bridge_spec.rx_channels[*channel_index];
1613                    let enum_entry_name = config_id_to_enum(&format!("{}_rx_{}", bridge_spec.id, channel.id));
1614                    let enum_ident = Ident::new(&enum_entry_name, Span::call_site());
1615                    let channel_type: Type = parse_str::<Type>(channel.msg_type_name.as_str()).unwrap();
1616                    let bridge_type = runtime_bridge_type_for_spec(bridge_spec, true);
1617                    let _const_ident = &channel.const_ident;
1618                    quote! {
1619                        #enum_ident {
1620                            channel: &'static cu29::cubridge::BridgeChannel<< <#bridge_type as cu29::cubridge::CuBridge>::Rx as cu29::cubridge::BridgeChannelSet >::Id, #channel_type>,
1621                            msg: &'a mut CuMsg<#channel_type>,
1622                        }
1623                    }
1624                }
1625                ExecutionEntityKind::BridgeTx { bridge_index, channel_index } => {
1626                    let bridge_spec = &bridge_specs[*bridge_index];
1627                    let channel = &bridge_spec.tx_channels[*channel_index];
1628                    let enum_entry_name = config_id_to_enum(&format!("{}_tx_{}", bridge_spec.id, channel.id));
1629                    let enum_ident = Ident::new(&enum_entry_name, Span::call_site());
1630                    let channel_type: Type = parse_str::<Type>(channel.msg_type_name.as_str()).unwrap();
1631                    let output_pack = step
1632                        .output_msg_pack
1633                        .as_ref()
1634                        .expect("Bridge Tx channel missing output pack for sim support");
1635                    let output_types: Vec<Type> = output_pack
1636                        .msg_types
1637                        .iter()
1638                        .map(|msg_type| {
1639                            parse_str::<Type>(msg_type.as_str()).unwrap_or_else(|_| {
1640                                panic!("Could not transform {msg_type} into a message Rust type.")
1641                            })
1642                        })
1643                        .collect();
1644                    let output_type = build_output_slot_type(&output_types);
1645                    let bridge_type = runtime_bridge_type_for_spec(bridge_spec, true);
1646                    let _const_ident = &channel.const_ident;
1647                    quote! {
1648                        #enum_ident {
1649                            channel: &'static cu29::cubridge::BridgeChannel<< <#bridge_type as cu29::cubridge::CuBridge>::Tx as cu29::cubridge::BridgeChannelSet >::Id, #channel_type>,
1650                            msg: &'a CuMsg<#channel_type>,
1651                            output: &'a mut #output_type,
1652                        }
1653                    }
1654                }
1655            },
1656            CuExecutionUnit::Loop(_) => {
1657                todo!("Needs to be implemented")
1658            }
1659        })
1660        .collect();
1661
1662    // bridge lifecycle variants (one per bridge)
1663    let mut variants = plan_enum;
1664
1665    // add bridge lifecycle variants
1666    for bridge_spec in bridge_specs {
1667        let enum_entry_name = config_id_to_enum(&format!("{}_bridge", bridge_spec.id));
1668        let enum_ident = Ident::new(&enum_entry_name, Span::call_site());
1669        variants.push(quote! {
1670            #enum_ident(cu29::simulation::CuBridgeLifecycleState)
1671        });
1672    }
1673
1674    if cfg!(feature = "logstream") {
1675        variants.push(quote! { CopperListCompleted(&'a mut CopperList<CuStampedDataSet>) });
1676    }
1677    variants.push(quote! { __Phantom(core::marker::PhantomData<&'a ()>) });
1678    quote! {
1679        // not used if sim is not generated but this is ok.
1680        #[allow(dead_code, unused_lifetimes)]
1681        pub enum SimStep<'a> {
1682            #(#variants),*
1683        }
1684    }
1685}
1686
1687fn gen_recorded_replay_support(
1688    runtime_plan: &CuExecutionLoop,
1689    exec_entities: &[ExecutionEntity],
1690    bridge_specs: &[BridgeSpec],
1691) -> proc_macro2::TokenStream {
1692    let replay_arms: Vec<proc_macro2::TokenStream> = runtime_plan
1693        .steps
1694        .iter()
1695        .filter(|unit| {
1696            // One replay arm per node: refine steps have no SimStep variant.
1697            !matches!(unit, CuExecutionUnit::Step(step) if step.phase == CuStepPhase::AnytimeRefine)
1698        })
1699        .filter_map(|unit| match unit {
1700            CuExecutionUnit::Step(step) => match &exec_entities[step.node_id as usize].kind {
1701                ExecutionEntityKind::Task { .. } => {
1702                    let enum_entry_name = config_id_to_enum(step.node.get_id().as_str());
1703                    let enum_ident = Ident::new(&enum_entry_name, Span::call_site());
1704                    let output_pack = step
1705                        .output_msg_pack
1706                        .as_ref()
1707                        .expect("Task step missing output pack for recorded replay");
1708                    let culist_index = int2sliceindex(output_pack.culist_index);
1709                    Some(quote! {
1710                        SimStep::#enum_ident(CuTaskCallbackState::Process(_, output)) => {
1711                            *output = recorded.msgs.0.#culist_index.clone();
1712                            SimOverride::ExecutedBySim
1713                        }
1714                    })
1715                }
1716                ExecutionEntityKind::BridgeRx {
1717                    bridge_index,
1718                    channel_index,
1719                } => {
1720                    let bridge_spec = &bridge_specs[*bridge_index];
1721                    let channel = &bridge_spec.rx_channels[*channel_index];
1722                    let enum_entry_name =
1723                        config_id_to_enum(&format!("{}_rx_{}", bridge_spec.id, channel.id));
1724                    let enum_ident = Ident::new(&enum_entry_name, Span::call_site());
1725                    let output_pack = step
1726                        .output_msg_pack
1727                        .as_ref()
1728                        .expect("Bridge Rx channel missing output pack for recorded replay");
1729                    let port_index = output_pack
1730                        .msg_types
1731                        .iter()
1732                        .position(|msg| msg == &channel.msg_type_name)
1733                        .unwrap_or_else(|| {
1734                            panic!(
1735                                "Bridge Rx channel '{}' missing output port for '{}'",
1736                                channel.id, channel.msg_type_name
1737                            )
1738                        });
1739                    let culist_index = int2sliceindex(output_pack.culist_index);
1740                    let recorded_slot = if output_pack.msg_types.len() == 1 {
1741                        quote! { recorded.msgs.0.#culist_index.clone() }
1742                    } else {
1743                        let port_index = syn::Index::from(port_index);
1744                        quote! { recorded.msgs.0.#culist_index.#port_index.clone() }
1745                    };
1746                    Some(quote! {
1747                        SimStep::#enum_ident { msg, .. } => {
1748                            *msg = #recorded_slot;
1749                            SimOverride::ExecutedBySim
1750                        }
1751                    })
1752                }
1753                ExecutionEntityKind::BridgeTx {
1754                    bridge_index,
1755                    channel_index,
1756                } => {
1757                    let bridge_spec = &bridge_specs[*bridge_index];
1758                    let channel = &bridge_spec.tx_channels[*channel_index];
1759                    let enum_entry_name =
1760                        config_id_to_enum(&format!("{}_tx_{}", bridge_spec.id, channel.id));
1761                    let enum_ident = Ident::new(&enum_entry_name, Span::call_site());
1762                    let output_pack = step
1763                        .output_msg_pack
1764                        .as_ref()
1765                        .expect("Bridge Tx channel missing output pack for recorded replay");
1766                    let culist_index = int2sliceindex(output_pack.culist_index);
1767                    Some(quote! {
1768                        SimStep::#enum_ident { output, .. } => {
1769                            *output = recorded.msgs.0.#culist_index.clone();
1770                            SimOverride::ExecutedBySim
1771                        }
1772                    })
1773                }
1774            },
1775            CuExecutionUnit::Loop(_) => None,
1776        })
1777        .collect();
1778    let debug_replay_arms: Vec<proc_macro2::TokenStream> =
1779        runtime_plan
1780            .steps
1781            .iter()
1782            .filter_map(|unit| match unit {
1783                CuExecutionUnit::Step(step) => match &exec_entities[step.node_id as usize].kind {
1784                    ExecutionEntityKind::Task { .. } => {
1785                        if step.task_type == CuTaskType::Regular {
1786                            return None;
1787                        }
1788                        let enum_entry_name = config_id_to_enum(step.node.get_id().as_str());
1789                        let enum_ident = Ident::new(&enum_entry_name, Span::call_site());
1790                        let output_pack = step
1791                            .output_msg_pack
1792                            .as_ref()
1793                            .expect("Task step missing output pack for recorded debug replay");
1794                        let culist_index = int2sliceindex(output_pack.culist_index);
1795                        Some(quote! {
1796                            SimStep::#enum_ident(CuTaskCallbackState::Process(_, output)) => {
1797                                *output = recorded.msgs.0.#culist_index.clone();
1798                                SimOverride::ExecutedBySim
1799                            }
1800                        })
1801                    }
1802                    ExecutionEntityKind::BridgeRx {
1803                        bridge_index,
1804                        channel_index,
1805                    } => {
1806                        let bridge_spec = &bridge_specs[*bridge_index];
1807                        let channel = &bridge_spec.rx_channels[*channel_index];
1808                        let enum_entry_name =
1809                            config_id_to_enum(&format!("{}_rx_{}", bridge_spec.id, channel.id));
1810                        let enum_ident = Ident::new(&enum_entry_name, Span::call_site());
1811                        let output_pack = step.output_msg_pack.as_ref().expect(
1812                            "Bridge Rx channel missing output pack for recorded debug replay",
1813                        );
1814                        let port_index = output_pack
1815                            .msg_types
1816                            .iter()
1817                            .position(|msg| msg == &channel.msg_type_name)
1818                            .unwrap_or_else(|| {
1819                                panic!(
1820                                    "Bridge Rx channel '{}' missing output port for '{}'",
1821                                    channel.id, channel.msg_type_name
1822                                )
1823                            });
1824                        let culist_index = int2sliceindex(output_pack.culist_index);
1825                        let recorded_slot = if output_pack.msg_types.len() == 1 {
1826                            quote! { recorded.msgs.0.#culist_index.clone() }
1827                        } else {
1828                            let port_index = syn::Index::from(port_index);
1829                            quote! { recorded.msgs.0.#culist_index.#port_index.clone() }
1830                        };
1831                        Some(quote! {
1832                            SimStep::#enum_ident { msg, .. } => {
1833                                *msg = #recorded_slot;
1834                                SimOverride::ExecutedBySim
1835                            }
1836                        })
1837                    }
1838                    ExecutionEntityKind::BridgeTx {
1839                        bridge_index,
1840                        channel_index,
1841                    } => {
1842                        let bridge_spec = &bridge_specs[*bridge_index];
1843                        let channel = &bridge_spec.tx_channels[*channel_index];
1844                        let enum_entry_name =
1845                            config_id_to_enum(&format!("{}_tx_{}", bridge_spec.id, channel.id));
1846                        let enum_ident = Ident::new(&enum_entry_name, Span::call_site());
1847                        let output_pack = step.output_msg_pack.as_ref().expect(
1848                            "Bridge Tx channel missing output pack for recorded debug replay",
1849                        );
1850                        let culist_index = int2sliceindex(output_pack.culist_index);
1851                        Some(quote! {
1852                            SimStep::#enum_ident { output, .. } => {
1853                                *output = recorded.msgs.0.#culist_index.clone();
1854                                SimOverride::ExecutedBySim
1855                            }
1856                        })
1857                    }
1858                },
1859                CuExecutionUnit::Loop(_) => None,
1860            })
1861            .collect();
1862
1863    quote! {
1864        /// Exact-output replay callback: every recorded task and bridge output is
1865        /// copied from the CopperList and the runtime implementation is skipped.
1866        ///
1867        /// This is for deterministic log reproduction, not for debugger state replay.
1868        #[allow(dead_code)]
1869        pub fn recorded_replay_step<'a>(
1870            step: SimStep<'a>,
1871            recorded: &CopperList<CuStampedDataSet>,
1872        ) -> SimOverride {
1873            match step {
1874                #(#replay_arms),*,
1875                _ => SimOverride::ExecuteByRuntime,
1876            }
1877        }
1878
1879        /// Debugger state replay callback: recorded external inputs are injected,
1880        /// regular Copper tasks execute normally, and external sink/bridge effects
1881        /// are suppressed.
1882        ///
1883        /// This preserves task-state evolution after restoring an intra-CL keyframe.
1884        #[allow(dead_code)]
1885        pub fn recorded_debug_replay_step<'a>(
1886            step: SimStep<'a>,
1887            recorded: &CopperList<CuStampedDataSet>,
1888        ) -> SimOverride {
1889            match step {
1890                #(#debug_replay_arms),*,
1891                _ => SimOverride::ExecuteByRuntime,
1892            }
1893        }
1894    }
1895}
1896
1897/// Adds `#[copper_runtime(config = "path", subsystem = "id", sim_mode = false/true, ignore_resources = false/true)]`
1898/// to your application struct to generate the runtime.
1899/// if sim_mode is omitted, it is set to false.
1900/// if ignore_resources is omitted, it is set to false.
1901/// if `subsystem` is provided, `config` must point to a strict multi-Copper config and the
1902/// selected subsystem local config will be embedded into the generated runtime.
1903/// This will add a "runtime" field to your struct and implement the "new" and "run" methods.
1904#[proc_macro_attribute]
1905pub fn copper_runtime(args: TokenStream, input: TokenStream) -> TokenStream {
1906    #[cfg(feature = "macro_debug")]
1907    eprintln!("[entry]");
1908    let mut application_struct = parse_macro_input!(input as ItemStruct);
1909
1910    let application_name = &application_struct.ident;
1911    let builder_name = format_ident!("{}Builder", application_name);
1912    let runtime_args = match CopperRuntimeArgs::parse_tokens(args.into()) {
1913        Ok(runtime_args) => runtime_args,
1914        Err(err) => return err.to_compile_error().into(),
1915    };
1916    let config_file = runtime_args.config_path.clone();
1917    let sim_mode = runtime_args.sim_mode;
1918    let ignore_resources = runtime_args.ignore_resources;
1919
1920    #[cfg(feature = "std")]
1921    let std = true;
1922
1923    #[cfg(not(feature = "std"))]
1924    let std = false;
1925    let signal_handler = cfg!(feature = "signal-handler");
1926    let parallel_rt_enabled = cfg!(feature = "parallel-rt");
1927    let logstream_enabled = cfg!(feature = "logstream");
1928    let rt_guard = rtsan_guard_tokens();
1929
1930    if ignore_resources && !sim_mode {
1931        return return_error(
1932            "`ignore_resources` is only supported when `sim_mode` is enabled".to_string(),
1933        );
1934    }
1935
1936    // Adds the generic parameter for the UnifiedLogger if this is a real application (not sim)
1937    // This allows to adapt either to the no-std (custom impl) and std (default file based one)
1938    // if !sim_mode {
1939    //     application_struct
1940    //         .generics
1941    //         .params
1942    //         .push(syn::parse_quote!(L: UnifiedLogWrite + 'static));
1943    // }
1944
1945    let resolved_runtime_config = match resolve_runtime_config(&runtime_args) {
1946        Ok(resolved_runtime_config) => resolved_runtime_config,
1947        Err(e) => return return_error(e.to_string()),
1948    };
1949    let subsystem_code = resolved_runtime_config.subsystem_code;
1950    let subsystem_id = resolved_runtime_config.subsystem_id.clone();
1951    let config_features = resolved_runtime_config.active_features.clone();
1952    let copper_config_content = resolved_runtime_config.bundled_local_config_content.clone();
1953    let mut copper_config = resolved_runtime_config.local_config;
1954    if let Err(e) = apply_external_plan(&mut copper_config) {
1955        return return_error(e.to_string());
1956    }
1957    let copper_config = copper_config;
1958    if copper_config.log_streaming.is_some() && !logstream_enabled {
1959        return return_error(
1960            "copperconfig.ron declares log_streaming but the cu29 'logstream' feature is disabled"
1961                .to_string(),
1962        );
1963    }
1964    if copper_config.log_streaming.is_some() && ignore_resources {
1965        return return_error(
1966            "log_streaming resource bindings cannot be used with ignore_resources".to_string(),
1967        );
1968    }
1969    // Re-apply the baked step orders at startup so the effective config the
1970    // app logs stays self-describing, whatever RON file it was started with.
1971    let planner_resolved_stamp = match copper_config.planner_config().and_then(|selection| {
1972        selection
1973            .resolved_orders()
1974            .map(|orders| (selection.get_type().to_string(), orders))
1975    }) {
1976        Some((planner_type, orders)) => {
1977            let entries = orders.iter().map(|(mission, keys)| {
1978                quote! { (#mission.to_string(), vec![#(#keys.to_string()),*]) }
1979            });
1980            quote! {
1981                let config = {
1982                    let mut config = config;
1983                    config.set_planner_resolved_orders(#planner_type, [#(#entries),*]);
1984                    config
1985                };
1986            }
1987        }
1988        None => quote! {},
1989    };
1990    let copperlist_count = copper_config
1991        .logging
1992        .as_ref()
1993        .and_then(|logging| logging.copperlist_count)
1994        .unwrap_or(DEFAULT_COPPERLIST_COUNT);
1995    let local_keyframe_logging_enabled = copper_config
1996        .logging
1997        .as_ref()
1998        .is_none_or(|logging| logging.enable_keyframe_logging && logging.enable_task_logging);
1999    // A feature-gated streaming destination may request keyframes even when the local archive
2000    // does not. Compile the capture path in that case; runtime output requirements keep it idle
2001    // when no logstream destination is configured.
2002    let keyframe_logging_enabled = local_keyframe_logging_enabled || logstream_enabled;
2003    let copperlist_count_tokens = proc_macro2::Literal::usize_unsuffixed(copperlist_count);
2004    let caller_root = utils::caller_crate_root();
2005    let (git_commit, git_dirty) = detect_git_info(&caller_root);
2006    let git_commit_tokens = if let Some(commit) = git_commit {
2007        quote! { Some(#commit.to_string()) }
2008    } else {
2009        quote! { None }
2010    };
2011    let git_dirty_tokens = if let Some(dirty) = git_dirty {
2012        quote! { Some(#dirty) }
2013    } else {
2014        quote! { None }
2015    };
2016    let subsystem_code_literal = proc_macro2::Literal::u16_unsuffixed(subsystem_code);
2017    let subsystem_id_tokens = if let Some(subsystem_id) = subsystem_id.as_deref() {
2018        quote! { Some(#subsystem_id) }
2019    } else {
2020        quote! { None }
2021    };
2022
2023    #[cfg(feature = "macro_debug")]
2024    eprintln!("[build monitor type]");
2025    let monitor_configs = copper_config.get_monitor_configs();
2026    let (monitor_type, monitor_instanciator_body) = if monitor_configs.is_empty() {
2027        (
2028            quote! { NoMonitor },
2029            quote! {
2030                let monitor_metadata = metadata.with_subsystem_id(#subsystem_id_tokens);
2031                let monitor = NoMonitor::new(monitor_metadata, runtime)
2032                    .expect("Failed to create NoMonitor.");
2033                monitor
2034            },
2035        )
2036    } else if monitor_configs.len() == 1 {
2037        let only_monitor_type = parse_str::<Type>(monitor_configs[0].get_type())
2038            .expect("Could not transform the monitor type name into a Rust type.");
2039        (
2040            quote! { #only_monitor_type },
2041            quote! {
2042                let monitor_metadata = metadata.with_monitor_config(
2043                    config
2044                        .get_monitor_configs()
2045                        .first()
2046                        .and_then(|entry| entry.get_config().cloned())
2047                )
2048                .with_subsystem_id(#subsystem_id_tokens);
2049                let monitor = #only_monitor_type::new(monitor_metadata, runtime)
2050                    .expect("Failed to create the given monitor.");
2051                monitor
2052            },
2053        )
2054    } else {
2055        let monitor_types: Vec<Type> = monitor_configs
2056            .iter()
2057            .map(|monitor_config| {
2058                parse_str::<Type>(monitor_config.get_type())
2059                    .expect("Could not transform the monitor type name into a Rust type.")
2060            })
2061            .collect();
2062        let monitor_bindings: Vec<Ident> = (0..monitor_types.len())
2063            .map(|idx| format_ident!("__cu_monitor_{idx}"))
2064            .collect();
2065        let monitor_indices: Vec<syn::Index> =
2066            (0..monitor_types.len()).map(syn::Index::from).collect();
2067
2068        let monitor_builders: Vec<proc_macro2::TokenStream> = monitor_types
2069            .iter()
2070            .zip(monitor_bindings.iter())
2071            .zip(monitor_indices.iter())
2072            .map(|((monitor_ty, monitor_binding), monitor_idx)| {
2073                quote! {
2074                    let __cu_monitor_cfg_entry = config
2075                        .get_monitor_configs()
2076                        .get(#monitor_idx)
2077                        .and_then(|entry| entry.get_config().cloned());
2078                    let __cu_monitor_metadata = metadata
2079                        .clone()
2080                        .with_monitor_config(__cu_monitor_cfg_entry)
2081                        .with_subsystem_id(#subsystem_id_tokens);
2082                    let #monitor_binding = #monitor_ty::new(__cu_monitor_metadata, runtime.clone())
2083                    .expect("Failed to create one of the configured monitors.");
2084                }
2085            })
2086            .collect();
2087        let tuple_type: TypeTuple = parse_quote! { (#(#monitor_types),*,) };
2088        (
2089            quote! { #tuple_type },
2090            quote! {
2091                #(#monitor_builders)*
2092                let monitor: #tuple_type = (#(#monitor_bindings),*,);
2093                monitor
2094            },
2095        )
2096    };
2097
2098    // This is common for all the mission as it will be inserted in the respective modules with their local CuTasks, CuStampedDataSet etc...
2099    #[cfg(feature = "macro_debug")]
2100    eprintln!("[build runtime field]");
2101    // add that to a new field
2102    let runtime_field: Field = if sim_mode {
2103        parse_quote! {
2104            copper_runtime: cu29::curuntime::CuRuntime<CuSimTasks, CuBridges, CuStampedDataSet, #monitor_type, #copperlist_count_tokens>
2105        }
2106    } else {
2107        parse_quote! {
2108            copper_runtime: cu29::curuntime::CuRuntime<CuTasks, CuBridges, CuStampedDataSet, #monitor_type, #copperlist_count_tokens>
2109        }
2110    };
2111    let lifecycle_sink_field: Field = parse_quote! {
2112        runtime_lifecycle_sink: Option<Box<::cu29::curuntime::RuntimeLifecycleSink>>
2113    };
2114    let logger_runtime_field: Field = parse_quote! {
2115        logger_runtime: cu29::prelude::LoggerRuntime
2116    };
2117
2118    #[cfg(feature = "macro_debug")]
2119    eprintln!("[match struct anonymity]");
2120    match &mut application_struct.fields {
2121        Named(fields_named) => {
2122            fields_named.named.push(runtime_field);
2123            fields_named.named.push(lifecycle_sink_field);
2124            fields_named.named.push(logger_runtime_field);
2125        }
2126        Unnamed(fields_unnamed) => {
2127            fields_unnamed.unnamed.push(runtime_field);
2128            fields_unnamed.unnamed.push(lifecycle_sink_field);
2129            fields_unnamed.unnamed.push(logger_runtime_field);
2130        }
2131        Fields::Unit => {
2132            panic!(
2133                "This struct is a unit struct, it should have named or unnamed fields. use struct Something {{}} and not struct Something;"
2134            )
2135        }
2136    };
2137
2138    let all_missions = sorted_mission_graphs(&copper_config);
2139    let constant_modules = match build_constant_modules(&copper_config.constants) {
2140        Ok(modules) => modules,
2141        Err(error) => return return_error(error.to_string()),
2142    };
2143    let constant_fingerprints = match copper_config
2144        .constants
2145        .iter()
2146        .map(|constant| {
2147            constant.semantic_fingerprint().map(|fingerprint| {
2148                (
2149                    constant.module_path().to_string(),
2150                    constant.id().to_string(),
2151                    fingerprint,
2152                )
2153            })
2154        })
2155        .collect::<Result<Vec<_>, _>>()
2156    {
2157        Ok(fingerprints) => fingerprints,
2158        Err(error) => return return_error(error),
2159    };
2160    let task_input_layouts = match collect_task_input_layouts(&all_missions) {
2161        Ok(layouts) => layouts,
2162        Err(e) => return return_error(e.to_string()),
2163    };
2164    let mut all_missions_tokens = Vec::<proc_macro2::TokenStream>::new();
2165    for (mission, graph) in &all_missions {
2166        let git_commit_tokens = git_commit_tokens.clone();
2167        let git_dirty_tokens = git_dirty_tokens.clone();
2168        let mission_mod = parse_str::<Ident>(mission.as_str())
2169            .expect("Could not make an identifier of the mission name");
2170        let mission_constant_contents = constant_modules.child_contents(&mission_mod);
2171
2172        #[cfg(feature = "macro_debug")]
2173        eprintln!("[extract tasks ids & types]");
2174        let task_specs = match CuTaskSpecSet::from_graph(graph) {
2175            Ok(specs) => specs,
2176            Err(e) => return return_error(e.to_string()),
2177        };
2178
2179        let culist_channel_usage = collect_bridge_channel_usage(graph);
2180        let mut culist_bridge_specs =
2181            build_bridge_specs(&copper_config, graph, &culist_channel_usage);
2182        let (culist_plan, culist_exec_entities, culist_plan_to_original) =
2183            match build_execution_plan(&copper_config, graph, mission, &mut culist_bridge_specs) {
2184                Ok(plan) => plan,
2185                Err(e) => {
2186                    return return_error(format!(
2187                        "Could not compute copperlist plan for mission '{mission}': {e}"
2188                    ));
2189                }
2190            };
2191
2192        // Single-input/single-output arity is validated at configuration time
2193        // (config.rs validate_anytime_graph), before the plan is built.
2194
2195        // Per-node refine totals and each refine step's 1-based ordinal: the
2196        // emitter bakes `k` and last-ness in as literals while walking the
2197        // plan — the runtime carries no counter.
2198        // One walk: the running counter's final value per node is that node's total.
2199        let (anytime_refine_ordinals, anytime_refine_totals): (
2200            Vec<Option<u32>>,
2201            HashMap<NodeId, u32>,
2202        ) = {
2203            let mut running: HashMap<NodeId, u32> = HashMap::new();
2204            let ordinals = culist_plan
2205                .steps
2206                .iter()
2207                .map(|unit| match unit {
2208                    CuExecutionUnit::Step(step) if step.phase == CuStepPhase::AnytimeRefine => {
2209                        let ordinal = running.entry(step.node_id).or_insert(0u32);
2210                        *ordinal += 1;
2211                        Some(*ordinal)
2212                    }
2213                    _ => None,
2214                })
2215                .collect();
2216            (ordinals, running)
2217        };
2218
2219        let task_names = collect_task_names(graph);
2220        let (culist_call_order, node_output_positions) = collect_culist_metadata(
2221            &culist_plan,
2222            &culist_exec_entities,
2223            &mut culist_bridge_specs,
2224            &culist_plan_to_original,
2225        );
2226
2227        #[cfg(feature = "macro_debug")]
2228        {
2229            eprintln!("[runtime plan for mission {mission}]");
2230            eprintln!("{culist_plan:?}");
2231        }
2232
2233        let culist_support: proc_macro2::TokenStream = gen_culist_support(
2234            &copper_config,
2235            Some(mission.as_str()),
2236            &culist_plan,
2237            &culist_call_order,
2238            &node_output_positions,
2239            &task_names,
2240            &culist_bridge_specs,
2241        );
2242
2243        let (
2244            resources_module,
2245            resources_instanciator_fn,
2246            task_resource_mappings,
2247            bridge_resource_mappings,
2248            logstream_resource_specs,
2249        ) = if ignore_resources {
2250            let bundle_specs: Vec<BundleSpec> = Vec::new();
2251            let resource_specs: Vec<ResourceKeySpec> = Vec::new();
2252            let (resources_module, resources_instanciator_fn) =
2253                match build_resources_module(&bundle_specs) {
2254                    Ok(tokens) => tokens,
2255                    Err(e) => return return_error(e.to_string()),
2256                };
2257            let task_resource_mappings =
2258                match build_task_resource_mappings(&resource_specs, &task_specs, sim_mode) {
2259                    Ok(tokens) => tokens,
2260                    Err(e) => return return_error(e.to_string()),
2261                };
2262            let bridge_resource_mappings =
2263                build_bridge_resource_mappings(&resource_specs, &culist_bridge_specs, sim_mode);
2264            (
2265                resources_module,
2266                resources_instanciator_fn,
2267                task_resource_mappings,
2268                bridge_resource_mappings,
2269                Vec::new(),
2270            )
2271        } else {
2272            let bundle_specs = match build_bundle_specs(&copper_config, mission.as_str()) {
2273                Ok(specs) => specs,
2274                Err(e) => return return_error(e.to_string()),
2275            };
2276
2277            let resource_specs = match collect_resource_specs(
2278                graph,
2279                &task_specs,
2280                &culist_bridge_specs,
2281                &bundle_specs,
2282            ) {
2283                Ok(specs) => specs,
2284                Err(e) => return return_error(e.to_string()),
2285            };
2286
2287            let (resources_module, resources_instanciator_fn) =
2288                match build_resources_module(&bundle_specs) {
2289                    Ok(tokens) => tokens,
2290                    Err(e) => return return_error(e.to_string()),
2291                };
2292            let task_resource_mappings =
2293                match build_task_resource_mappings(&resource_specs, &task_specs, sim_mode) {
2294                    Ok(tokens) => tokens,
2295                    Err(e) => return return_error(e.to_string()),
2296                };
2297            let bridge_resource_mappings =
2298                build_bridge_resource_mappings(&resource_specs, &culist_bridge_specs, sim_mode);
2299            let logstream_resource_specs = if sim_mode {
2300                Vec::new()
2301            } else {
2302                match build_logstream_resource_specs(
2303                    &copper_config,
2304                    mission.as_str(),
2305                    &resource_specs,
2306                ) {
2307                    Ok(specs) => specs,
2308                    Err(e) => return return_error(e.to_string()),
2309                }
2310            };
2311            (
2312                resources_module,
2313                resources_instanciator_fn,
2314                task_resource_mappings,
2315                bridge_resource_mappings,
2316                logstream_resource_specs,
2317            )
2318        };
2319
2320        let task_ids = task_specs.ids.clone();
2321        let autogenerated_output_warnings: Vec<proc_macro2::TokenStream> = task_specs
2322            .ids
2323            .iter()
2324            .zip(task_specs.cutypes.iter())
2325            .zip(task_specs.autogenerated_output_flags.iter())
2326            .filter_map(|((task_id, task_kind), autogenerated)| {
2327                if !*autogenerated {
2328                    return None;
2329                }
2330                let warn_ident = format_ident!(
2331                    "__CU_AUTOGEN_FLOATING_OUTPUT_WARNING__{}",
2332                    config_id_to_enum(task_id)
2333                );
2334                let kind_str = match task_kind {
2335                    CuTaskType::Source => "source",
2336                    CuTaskType::Regular => "task",
2337                    CuTaskType::Sink => return None,
2338                };
2339                let note = format!(
2340                    "Task '{task_id}' is declared as kind '{kind_str}' but has no declared outputs. Copper synthesized a hidden floating output slot from the task trait. Add a real consumer or `dst: \"__nc__\"` if you want this to stay explicit."
2341                );
2342                Some(quote! {
2343                    #[allow(dead_code)]
2344                    #[deprecated(note = #note)]
2345                    const #warn_ident: () = ();
2346                    const _: () = {
2347                        let _ = #warn_ident;
2348                    };
2349                })
2350            })
2351            .collect();
2352        let ids = build_monitored_ids(&task_ids, &mut culist_bridge_specs);
2353        let parallel_rt_stage_entries = match build_parallel_rt_stage_entries(
2354            &culist_plan,
2355            &culist_exec_entities,
2356            &task_specs,
2357            &culist_bridge_specs,
2358        ) {
2359            Ok(entries) => entries,
2360            Err(e) => return return_error(e.to_string()),
2361        };
2362        let parallel_rt_metadata_defs = if std && parallel_rt_enabled {
2363            Some(quote! {
2364                pub const PARALLEL_RT_STAGES: &'static [cu29::parallel_rt::ParallelRtStageMetadata] =
2365                    &[#( #parallel_rt_stage_entries ),*];
2366                pub const PARALLEL_RT_METADATA: cu29::parallel_rt::ParallelRtMetadata =
2367                    cu29::parallel_rt::ParallelRtMetadata::new(PARALLEL_RT_STAGES);
2368            })
2369        } else {
2370            None
2371        };
2372        let monitored_component_entries: Vec<proc_macro2::TokenStream> = ids
2373            .iter()
2374            .enumerate()
2375            .map(|(idx, id)| {
2376                let id_lit = LitStr::new(id, Span::call_site());
2377                if idx < task_specs.task_types.len() {
2378                    let task_ty = &task_specs.task_types[idx];
2379                    let component_type = match task_specs.cutypes[idx] {
2380                        CuTaskType::Source => quote! { cu29::monitoring::ComponentType::Source },
2381                        CuTaskType::Regular => quote! { cu29::monitoring::ComponentType::Task },
2382                        CuTaskType::Sink => quote! { cu29::monitoring::ComponentType::Sink },
2383                    };
2384                    quote! {
2385                        cu29::monitoring::MonitorComponentMetadata::new(
2386                            #id_lit,
2387                            #component_type,
2388                            Some(stringify!(#task_ty)),
2389                        )
2390                    }
2391                } else {
2392                    quote! {
2393                        cu29::monitoring::MonitorComponentMetadata::new(
2394                            #id_lit,
2395                            cu29::monitoring::ComponentType::Bridge,
2396                            None,
2397                        )
2398                    }
2399                }
2400            })
2401            .collect();
2402        let culist_component_mapping = match build_monitor_culist_component_mapping(
2403            &culist_plan,
2404            &culist_exec_entities,
2405            &culist_bridge_specs,
2406        ) {
2407            Ok(mapping) => mapping,
2408            Err(e) => return return_error(e),
2409        };
2410
2411        let runtime_task_types: Vec<Type> = (0..task_specs.ids.len())
2412            .map(|index| runtime_task_type_for_index(&task_specs, graph, index, sim_mode))
2413            .collect();
2414
2415        let task_reflect_read_arms: Vec<proc_macro2::TokenStream> = task_specs
2416            .ids
2417            .iter()
2418            .enumerate()
2419            .map(|(index, task_id)| {
2420                let task_index = syn::Index::from(index);
2421                let task_id_lit = LitStr::new(task_id, Span::call_site());
2422                quote! {
2423                    #task_id_lit => Some(&self.copper_runtime.tasks.#task_index as &dyn cu29::reflect::Reflect),
2424                }
2425            })
2426            .collect();
2427
2428        let task_reflect_write_arms: Vec<proc_macro2::TokenStream> = task_specs
2429            .ids
2430            .iter()
2431            .enumerate()
2432            .map(|(index, task_id)| {
2433                let task_index = syn::Index::from(index);
2434                let task_id_lit = LitStr::new(task_id, Span::call_site());
2435                quote! {
2436                    #task_id_lit => Some(&mut self.copper_runtime.tasks.#task_index as &mut dyn cu29::reflect::Reflect),
2437                }
2438            })
2439            .collect();
2440
2441        let task_debug_state_type_path_arms: Vec<proc_macro2::TokenStream> = task_specs
2442            .ids
2443            .iter()
2444            .zip(runtime_task_types.iter())
2445            .enumerate()
2446            .map(|(index, (task_id, task_type))| {
2447                let task_id_lit = LitStr::new(task_id, Span::call_site());
2448                let task_trait = task_trait_for_specs(&task_specs, index);
2449                quote! {
2450                    #task_id_lit => Some(<#task_type as #task_trait>::debug_state_type_path()),
2451                }
2452            })
2453            .collect();
2454
2455        let task_debug_state_read_arms: Vec<proc_macro2::TokenStream> = task_specs
2456            .ids
2457            .iter()
2458            .zip(runtime_task_types.iter())
2459            .enumerate()
2460            .map(|(index, (task_id, task_type))| {
2461                let task_index = syn::Index::from(index);
2462                let task_id_lit = LitStr::new(task_id, Span::call_site());
2463                let task_trait = task_trait_for_specs(&task_specs, index);
2464                quote! {
2465                    #task_id_lit => Some(
2466                        <#task_type as #task_trait>::with_debug_state(
2467                            &self.copper_runtime.tasks.#task_index,
2468                            f,
2469                        )
2470                    ),
2471                }
2472            })
2473            .collect();
2474
2475        let task_debug_state_registration_calls: Vec<proc_macro2::TokenStream> = task_specs
2476            .ids
2477            .iter()
2478            .enumerate()
2479            .map(|(index, _)| {
2480                let task_type = &runtime_task_types[index];
2481                let task_trait = task_trait_for_specs(&task_specs, index);
2482                quote! {
2483                    <#task_type as #task_trait>::register_debug_state_types(registry);
2484                }
2485            })
2486            .collect();
2487
2488        let mut reflect_registry_types: BTreeMap<String, Type> = BTreeMap::new();
2489        let mut add_reflect_type = |ty: Type| {
2490            let key = quote! { #ty }.to_string();
2491            reflect_registry_types.entry(key).or_insert(ty);
2492        };
2493
2494        let mut sim_bridge_channel_decls = Vec::<proc_macro2::TokenStream>::new();
2495        let bridge_runtime_types: Vec<Type> = culist_bridge_specs
2496            .iter()
2497            .map(|spec| {
2498                if sim_mode && !spec.run_in_sim {
2499                    let (tx_set_ident, tx_id_ident, rx_set_ident, rx_id_ident) =
2500                        sim_bridge_channel_set_idents(spec.tuple_index);
2501
2502                    if !spec.tx_channels.is_empty() {
2503                        let tx_entries = spec.tx_channels.iter().map(|channel| {
2504                            let entry_ident = Ident::new(
2505                                &channel.const_ident.to_string().to_lowercase(),
2506                                Span::call_site(),
2507                            );
2508                            let msg_type = &channel.msg_type;
2509                            quote! { #entry_ident => #msg_type, }
2510                        });
2511                        sim_bridge_channel_decls.push(quote! {
2512                            cu29::tx_channels! {
2513                                pub struct #tx_set_ident : #tx_id_ident {
2514                                    #(#tx_entries)*
2515                                }
2516                            }
2517                        });
2518                    }
2519
2520                    if !spec.rx_channels.is_empty() {
2521                        let rx_entries = spec.rx_channels.iter().map(|channel| {
2522                            let entry_ident = Ident::new(
2523                                &channel.const_ident.to_string().to_lowercase(),
2524                                Span::call_site(),
2525                            );
2526                            let msg_type = &channel.msg_type;
2527                            quote! { #entry_ident => #msg_type, }
2528                        });
2529                        sim_bridge_channel_decls.push(quote! {
2530                            cu29::rx_channels! {
2531                                pub struct #rx_set_ident : #rx_id_ident {
2532                                    #(#rx_entries)*
2533                                }
2534                            }
2535                        });
2536                    }
2537                }
2538                runtime_bridge_type_for_spec(spec, sim_mode)
2539            })
2540            .collect();
2541        let sim_bridge_channel_defs = quote! { #(#sim_bridge_channel_decls)* };
2542
2543        for (bridge_index, bridge_spec) in culist_bridge_specs.iter().enumerate() {
2544            add_reflect_type(bridge_runtime_types[bridge_index].clone());
2545            for channel in bridge_spec
2546                .rx_channels
2547                .iter()
2548                .chain(bridge_spec.tx_channels.iter())
2549            {
2550                add_reflect_type(channel.msg_type.clone());
2551            }
2552        }
2553
2554        for output_pack in extract_output_packs(&culist_plan) {
2555            for msg_type in output_pack.msg_types {
2556                add_reflect_type(msg_type);
2557            }
2558        }
2559
2560        let reflect_type_registration_calls: Vec<proc_macro2::TokenStream> = reflect_registry_types
2561            .values()
2562            .map(|ty| {
2563                quote! {
2564                    registry.register::<#ty>();
2565                }
2566            })
2567            .collect();
2568
2569        let bridges_type_tokens: proc_macro2::TokenStream = if bridge_runtime_types.is_empty() {
2570            quote! { () }
2571        } else {
2572            let bridge_types_for_tuple = bridge_runtime_types.clone();
2573            let tuple: TypeTuple = parse_quote! { (#(#bridge_types_for_tuple),*,) };
2574            quote! { #tuple }
2575        };
2576
2577        let bridge_binding_idents: Vec<Ident> = culist_bridge_specs
2578            .iter()
2579            .enumerate()
2580            .map(|(idx, _)| format_ident!("bridge_{idx}"))
2581            .collect();
2582
2583        let bridge_init_statements: Vec<proc_macro2::TokenStream> = culist_bridge_specs
2584            .iter()
2585            .enumerate()
2586            .map(|(idx, spec)| {
2587                let binding_ident = &bridge_binding_idents[idx];
2588                let bridge_mapping_ref = bridge_resource_mappings.refs[idx].clone();
2589                let bridge_type = &bridge_runtime_types[idx];
2590                let bridge_name = spec.id.clone();
2591                let config_index = syn::Index::from(spec.config_index);
2592                let binding_error = LitStr::new(
2593                    &format!("Failed to bind resources for bridge '{}'", bridge_name),
2594                    Span::call_site(),
2595                );
2596                let tx_configs: Vec<proc_macro2::TokenStream> = spec
2597                    .tx_channels
2598                    .iter()
2599                    .map(|channel| {
2600                        let const_ident = &channel.const_ident;
2601                        let channel_name = channel.id.clone();
2602                        let channel_config_index = syn::Index::from(channel.config_index);
2603                        quote! {
2604                            {
2605                        let (channel_route, channel_config) = match &bridge_cfg.channels[#channel_config_index] {
2606                            cu29::config::BridgeChannelConfigRepresentation::Tx { route, config, .. } => {
2607                                (route.clone(), config.clone())
2608                                    }
2609                                    _ => panic!(
2610                                        "Bridge '{}' channel '{}' expected to be Tx",
2611                                        #bridge_name,
2612                                        #channel_name
2613                                    ),
2614                                };
2615                                cu29::cubridge::BridgeChannelConfig::from_static(
2616                                    &<#bridge_type as cu29::cubridge::CuBridge>::Tx::#const_ident,
2617                                    channel_route,
2618                                    channel_config,
2619                                )
2620                            }
2621                        }
2622                    })
2623                    .collect();
2624                let rx_configs: Vec<proc_macro2::TokenStream> = spec
2625                    .rx_channels
2626                    .iter()
2627                    .map(|channel| {
2628                        let const_ident = &channel.const_ident;
2629                        let channel_name = channel.id.clone();
2630                        let channel_config_index = syn::Index::from(channel.config_index);
2631                        quote! {
2632                            {
2633                                let (channel_route, channel_config) = match &bridge_cfg.channels[#channel_config_index] {
2634                                    cu29::config::BridgeChannelConfigRepresentation::Rx { route, config, .. } => {
2635                                        (route.clone(), config.clone())
2636                                    }
2637                                    _ => panic!(
2638                                        "Bridge '{}' channel '{}' expected to be Rx",
2639                                        #bridge_name,
2640                                        #channel_name
2641                                    ),
2642                                };
2643                                cu29::cubridge::BridgeChannelConfig::from_static(
2644                                    &<#bridge_type as cu29::cubridge::CuBridge>::Rx::#const_ident,
2645                                    channel_route,
2646                                    channel_config,
2647                                )
2648                            }
2649                        }
2650                    })
2651                    .collect();
2652                quote! {
2653                    let #binding_ident = {
2654                        let bridge_cfg = config
2655                            .bridges
2656                            .get(#config_index)
2657                            .unwrap_or_else(|| panic!("Bridge '{}' missing from configuration", #bridge_name));
2658                        let bridge_mapping = #bridge_mapping_ref;
2659                        let bridge_resources = <<#bridge_type as cu29::cubridge::CuBridge>::Resources<'_> as ResourceBindings>::from_bindings(
2660                            resources,
2661                            bridge_mapping,
2662                        )
2663                        .map_err(|e| cu29::CuError::new_with_cause(#binding_error, e))?;
2664                        let tx_channels: &[cu29::cubridge::BridgeChannelConfig<
2665                            <<#bridge_type as cu29::cubridge::CuBridge>::Tx as cu29::cubridge::BridgeChannelSet>::Id,
2666                        >] = &[#(#tx_configs),*];
2667                        let rx_channels: &[cu29::cubridge::BridgeChannelConfig<
2668                            <<#bridge_type as cu29::cubridge::CuBridge>::Rx as cu29::cubridge::BridgeChannelSet>::Id,
2669                        >] = &[#(#rx_configs),*];
2670                        <#bridge_type as cu29::cubridge::CuBridge>::new(
2671                            bridge_cfg.config.as_ref(),
2672                            tx_channels,
2673                            rx_channels,
2674                            bridge_resources,
2675                        )?
2676                    };
2677                }
2678            })
2679            .collect();
2680
2681        let bridges_instanciator = if culist_bridge_specs.is_empty() {
2682            quote! {
2683                pub fn bridges_instanciator(_config: &CuConfig, resources: &mut ResourceManager) -> CuResult<CuBridges> {
2684                    let _ = resources;
2685                    Ok(())
2686                }
2687            }
2688        } else {
2689            let bridge_bindings = bridge_binding_idents.clone();
2690            quote! {
2691                pub fn bridges_instanciator(config: &CuConfig, resources: &mut ResourceManager) -> CuResult<CuBridges> {
2692                    #(#bridge_init_statements)*
2693                    Ok((#(#bridge_bindings),*,))
2694                }
2695            }
2696        };
2697
2698        let all_sim_tasks_types = runtime_task_types.clone();
2699
2700        #[cfg(feature = "macro_debug")]
2701        eprintln!("[build task tuples]");
2702
2703        let task_types = &task_specs.task_types;
2704        // Build the tuple of all those types
2705        // note the extraneous, at the end is to make the tuple work even if this is only one element
2706        let task_types_tuple: TypeTuple = if task_types.is_empty() {
2707            parse_quote! { () }
2708        } else {
2709            parse_quote! { (#(#task_types),*,) }
2710        };
2711
2712        let task_types_tuple_sim: TypeTuple = if all_sim_tasks_types.is_empty() {
2713            parse_quote! { () }
2714        } else {
2715            parse_quote! { (#(#all_sim_tasks_types),*,) }
2716        };
2717
2718        #[cfg(feature = "macro_debug")]
2719        eprintln!("[gen instances]");
2720
2721        // Resolve each background task's thread pool name to its index in
2722        // `runtime.thread_pools` (matching the slot order the runtime owns the
2723        // built pools in). Non-background tasks get a placeholder index that is
2724        // never used.
2725        let thread_pool_indices: HashMap<&str, usize> = copper_config
2726            .runtime
2727            .as_ref()
2728            .map(|runtime| {
2729                runtime
2730                    .thread_pools
2731                    .iter()
2732                    .enumerate()
2733                    .map(|(index, pool)| (pool.id.as_str(), index))
2734                    .collect()
2735            })
2736            .unwrap_or_default();
2737        for (task_index, pool_name) in task_specs.background_pools.iter().enumerate() {
2738            if !task_specs.background_flags[task_index] {
2739                continue;
2740            }
2741            // "rt" is the default pool applied to every task running under the
2742            // parallel-rt engine; a task only deviates from it by setting
2743            // `background: true` and picking a different pool here. The "rt"
2744            // pool itself is dedicated to the parallel-rt stage workers, so it
2745            // is rejected as a per-task override.
2746            if pool_name == RT_POOL {
2747                return return_error(format!(
2748                    "Background task '{}' may not use the reserved '{RT_POOL}' thread pool; it is dedicated to the parallel-rt execution engine.",
2749                    task_specs.ids[task_index]
2750                ));
2751            }
2752            if !thread_pool_indices.contains_key(pool_name.as_str()) {
2753                return return_error(format!(
2754                    "Background task '{}' references undefined thread pool '{}'. Define it under runtime.thread_pools.",
2755                    task_specs.ids[task_index], pool_name
2756                ));
2757            }
2758        }
2759        let task_pool_indices: Vec<usize> = task_specs
2760            .background_pools
2761            .iter()
2762            .map(|pool_name| {
2763                thread_pool_indices
2764                    .get(pool_name.as_str())
2765                    .copied()
2766                    .unwrap_or(0)
2767            })
2768            .collect();
2769
2770        let task_sim_instances_init_code = all_sim_tasks_types
2771            .iter()
2772            .enumerate()
2773            .map(|(index, ty)| {
2774                let additional_error_info = format!(
2775                    "Failed to get create instance for {}, instance index {}.",
2776                    task_specs.type_names[index], index
2777                );
2778                let mapping_ref = task_resource_mappings.refs[index].clone();
2779                let background = task_specs.background_flags[index]
2780                    && !(sim_mode
2781                        && task_specs.cutypes[index] == CuTaskType::Source
2782                        && !task_specs.run_in_sim_flags[index]);
2783                let inner_task_type = &task_specs.async_inner_task_types[index];
2784                match task_specs.cutypes[index] {
2785                    CuTaskType::Source => {
2786                        if background {
2787                            let pool_index = task_pool_indices[index];
2788                            let pool_name = task_specs.background_pools[index].clone();
2789                            quote! {
2790                                {
2791                                    let inner_resources = <<#inner_task_type as CuSrcTask>::Resources<'_> as ResourceBindings>::from_bindings(
2792                                        resources,
2793                                        #mapping_ref,
2794                                    ).map_err(|e| e.add_cause(#additional_error_info))?;
2795                                    let threadpool = thread_pools
2796                                        .get(#pool_index)
2797                                        .and_then(|slot| slot.clone())
2798                                        .ok_or_else(|| CuError::from(format!(
2799                                            "Background task at index {} requested thread pool '{}' but it was not provided",
2800                                            #index, #pool_name,
2801                                        )))?;
2802                                    let resources = cu29::cuasynctask::CuAsyncSrcTaskResources {
2803                                        inner: inner_resources,
2804                                        threadpool,
2805                                    };
2806                                    <#ty as CuSrcTask>::new(all_instances_configs[#index], resources)
2807                                        .map_err(|e| e.add_cause(#additional_error_info))?
2808                                }
2809                            }
2810                        } else {
2811                            quote! {
2812                                {
2813                                    let resources = <<#ty as CuSrcTask>::Resources<'_> as ResourceBindings>::from_bindings(
2814                                        resources,
2815                                        #mapping_ref,
2816                                    ).map_err(|e| e.add_cause(#additional_error_info))?;
2817                                    <#ty as CuSrcTask>::new(all_instances_configs[#index], resources)
2818                                        .map_err(|e| e.add_cause(#additional_error_info))?
2819                                }
2820                            }
2821                        }
2822                    }
2823                    CuTaskType::Regular => {
2824                        if background {
2825                            let pool_index = task_pool_indices[index];
2826                            let pool_name = task_specs.background_pools[index].clone();
2827                            quote! {
2828                                {
2829                                    let inner_resources = <<#inner_task_type as CuTask>::Resources<'_> as ResourceBindings>::from_bindings(
2830                                        resources,
2831                                        #mapping_ref,
2832                                    ).map_err(|e| e.add_cause(#additional_error_info))?;
2833                                    let threadpool = thread_pools
2834                                        .get(#pool_index)
2835                                        .and_then(|slot| slot.clone())
2836                                        .ok_or_else(|| CuError::from(format!(
2837                                            "Background task at index {} requested thread pool '{}' but it was not provided",
2838                                            #index, #pool_name,
2839                                        )))?;
2840                                    let resources = cu29::cuasynctask::CuAsyncTaskResources {
2841                                        inner: inner_resources,
2842                                        threadpool,
2843                                    };
2844                                    <#ty as CuTask>::new(all_instances_configs[#index], resources)
2845                                        .map_err(|e| e.add_cause(#additional_error_info))?
2846                                }
2847                            }
2848                        } else {
2849                            let regular_trait = task_trait_for_specs(&task_specs, index);
2850                            quote! {
2851                                {
2852                                    let resources = <<#ty as #regular_trait>::Resources<'_> as ResourceBindings>::from_bindings(
2853                                        resources,
2854                                        #mapping_ref,
2855                                    ).map_err(|e| e.add_cause(#additional_error_info))?;
2856                                    <#ty as #regular_trait>::new(all_instances_configs[#index], resources)
2857                                        .map_err(|e| e.add_cause(#additional_error_info))?
2858                                }
2859                            }
2860                        }
2861                    }
2862                    CuTaskType::Sink => quote! {
2863                        {
2864                            let resources = <<#ty as CuSinkTask>::Resources<'_> as ResourceBindings>::from_bindings(
2865                                resources,
2866                                #mapping_ref,
2867                            ).map_err(|e| e.add_cause(#additional_error_info))?;
2868                            <#ty as CuSinkTask>::new(all_instances_configs[#index], resources)
2869                                .map_err(|e| e.add_cause(#additional_error_info))?
2870                        }
2871                    },
2872                }
2873            })
2874            .collect::<Vec<_>>();
2875
2876        let task_instances_init_code = task_specs
2877            .instantiation_types
2878            .iter()
2879            .zip(&task_specs.background_flags)
2880            .enumerate()
2881            .map(|(index, (task_type, background))| {
2882                let additional_error_info = format!(
2883                    "Failed to get create instance for {}, instance index {}.",
2884                    task_specs.type_names[index], index
2885                );
2886                let mapping_ref = task_resource_mappings.refs[index].clone();
2887                let inner_task_type = &task_specs.async_inner_task_types[index];
2888                match task_specs.cutypes[index] {
2889                    CuTaskType::Source => {
2890                        if *background {
2891                            let pool_index = task_pool_indices[index];
2892                            let pool_name = task_specs.background_pools[index].clone();
2893                            quote! {
2894                                {
2895                                    let inner_resources = <<#inner_task_type as CuSrcTask>::Resources<'_> as ResourceBindings>::from_bindings(
2896                                        resources,
2897                                        #mapping_ref,
2898                                    ).map_err(|e| e.add_cause(#additional_error_info))?;
2899                                    let threadpool = thread_pools
2900                                        .get(#pool_index)
2901                                        .and_then(|slot| slot.clone())
2902                                        .ok_or_else(|| CuError::from(format!(
2903                                            "Background task at index {} requested thread pool '{}' but it was not provided",
2904                                            #index, #pool_name,
2905                                        )))?;
2906                                    let resources = cu29::cuasynctask::CuAsyncSrcTaskResources {
2907                                        inner: inner_resources,
2908                                        threadpool,
2909                                    };
2910                                    <#task_type as CuSrcTask>::new(all_instances_configs[#index], resources)
2911                                        .map_err(|e| e.add_cause(#additional_error_info))?
2912                                }
2913                            }
2914                        } else {
2915                            quote! {
2916                                {
2917                                    let resources = <<#task_type as CuSrcTask>::Resources<'_> as ResourceBindings>::from_bindings(
2918                                        resources,
2919                                        #mapping_ref,
2920                                    ).map_err(|e| e.add_cause(#additional_error_info))?;
2921                                    <#task_type as CuSrcTask>::new(all_instances_configs[#index], resources)
2922                                        .map_err(|e| e.add_cause(#additional_error_info))?
2923                                }
2924                            }
2925                        }
2926                    }
2927                    CuTaskType::Regular => {
2928                        if *background {
2929                            let pool_index = task_pool_indices[index];
2930                            let pool_name = task_specs.background_pools[index].clone();
2931                            quote! {
2932                                {
2933                                    let inner_resources = <<#inner_task_type as CuTask>::Resources<'_> as ResourceBindings>::from_bindings(
2934                                        resources,
2935                                        #mapping_ref,
2936                                    ).map_err(|e| e.add_cause(#additional_error_info))?;
2937                                    let threadpool = thread_pools
2938                                        .get(#pool_index)
2939                                        .and_then(|slot| slot.clone())
2940                                        .ok_or_else(|| CuError::from(format!(
2941                                            "Background task at index {} requested thread pool '{}' but it was not provided",
2942                                            #index, #pool_name,
2943                                        )))?;
2944                                    let resources = cu29::cuasynctask::CuAsyncTaskResources {
2945                                        inner: inner_resources,
2946                                        threadpool,
2947                                    };
2948                                    <#task_type as CuTask>::new(all_instances_configs[#index], resources)
2949                                        .map_err(|e| e.add_cause(#additional_error_info))?
2950                                }
2951                            }
2952                        } else {
2953                            let regular_trait = task_trait_for_specs(&task_specs, index);
2954                            quote! {
2955                                {
2956                                    let resources = <<#task_type as #regular_trait>::Resources<'_> as ResourceBindings>::from_bindings(
2957                                        resources,
2958                                        #mapping_ref,
2959                                    ).map_err(|e| e.add_cause(#additional_error_info))?;
2960                                    <#task_type as #regular_trait>::new(all_instances_configs[#index], resources)
2961                                        .map_err(|e| e.add_cause(#additional_error_info))?
2962                                }
2963                            }
2964                        }
2965                    }
2966                    CuTaskType::Sink => quote! {
2967                        {
2968                            let resources = <<#task_type as CuSinkTask>::Resources<'_> as ResourceBindings>::from_bindings(
2969                                resources,
2970                                #mapping_ref,
2971                            ).map_err(|e| e.add_cause(#additional_error_info))?;
2972                            <#task_type as CuSinkTask>::new(all_instances_configs[#index], resources)
2973                                .map_err(|e| e.add_cause(#additional_error_info))?
2974                        }
2975                    },
2976                }
2977            })
2978            .collect::<Vec<_>>();
2979
2980        let mut keyframe_task_restore_order = Vec::new();
2981        for unit in &culist_plan.steps {
2982            let CuExecutionUnit::Step(step) = unit else {
2983                panic!("Execution loops are not supported in runtime generation");
2984            };
2985            let ExecutionEntityKind::Task { task_index } =
2986                &culist_exec_entities[step.node_id as usize].kind
2987            else {
2988                continue;
2989            };
2990            if !keyframe_task_restore_order.contains(task_index) {
2991                keyframe_task_restore_order.push(*task_index);
2992            }
2993        }
2994        if keyframe_task_restore_order.len() != task_specs.task_types.len() {
2995            return return_error(format!(
2996                "Keyframe restore order covers {} task steps but mission declares {} tasks",
2997                keyframe_task_restore_order.len(),
2998                task_specs.task_types.len()
2999            ));
3000        }
3001        // Generate the code to create instances of the nodes
3002        // It maps the types to their index
3003        let (
3004            task_start_calls,
3005            task_stop_calls,
3006            task_preprocess_calls,
3007            task_postprocess_calls,
3008        ): (Vec<_>, Vec<_>, Vec<_>, Vec<_>) = itertools::multiunzip(
3009            (0..task_specs.task_types.len())
3010            .map(|index| {
3011                let task_index = int2sliceindex(index as u32);
3012                let task_enum_name = config_id_to_enum(&task_specs.ids[index]);
3013                let enum_name = Ident::new(&task_enum_name, Span::call_site());
3014                (
3015                    {  // Start calls
3016                        let monitoring_action = quote! {
3017                            let decision = self.copper_runtime.monitor.process_error(cu29::monitoring::ComponentId::new(#index), CuComponentState::Start, &error);
3018                            match decision {
3019                                Decision::Abort => {
3020                                    debug!(ctx, "Start: ABORT decision from monitoring. Component '{}' errored out \
3021                                during start. Aborting all the other starts.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#index)));
3022                                    return Ok(());
3023
3024                                }
3025                                Decision::Ignore => {
3026                                    debug!(ctx, "Start: IGNORE decision from monitoring. Component '{}' errored out \
3027                                during start. The runtime will continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#index)));
3028                                }
3029                                Decision::Shutdown => {
3030                                    debug!(ctx, "Start: SHUTDOWN decision from monitoring. Component '{}' errored out \
3031                                during start. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#index)));
3032                                    return Err(CuError::new_with_cause("Component errored out during start.", error));
3033                                }
3034                            }
3035                        };
3036
3037                        let call_sim_callback = if sim_mode {
3038                            quote! {
3039                                // Ask the sim if this task should be executed or overridden by the sim.
3040                                let ovr = sim_callback(SimStep::#enum_name(CuTaskCallbackState::Start));
3041
3042                                let doit = if let SimOverride::Errored(reason) = ovr  {
3043                                    let error: CuError = reason.into();
3044                                    #monitoring_action
3045                                    false
3046                               }
3047                               else {
3048                                    ovr == SimOverride::ExecuteByRuntime
3049                               };
3050                            }
3051                        } else {
3052                            quote! {
3053                                let doit = true;  // in normal mode always execute the steps in the runtime.
3054                            }
3055                        };
3056
3057
3058                        let alloc_open = alloc_scope_open_tokens();
3059                        let alloc_close = alloc_scope_close_tokens(
3060                            quote! { self.copper_runtime.monitor },
3061                            quote! { #index },
3062                            quote! { CuComponentState::Start },
3063                        );
3064                        quote! {
3065                            #call_sim_callback
3066                            if doit {
3067                                self.copper_runtime.record_execution_marker(
3068                                    cu29::monitoring::ExecutionMarker {
3069                                        component_id: cu29::monitoring::ComponentId::new(#index),
3070                                        step: CuComponentState::Start,
3071                                        culistid: None,
3072                                    }
3073                                );
3074                                let task = &mut self.copper_runtime.tasks.#task_index;
3075                                ctx.set_current_task(#index);
3076                                #alloc_open
3077                                let __cu_step_result = task.start(&ctx);
3078                                #alloc_close
3079                                if let Err(error) = __cu_step_result {
3080                                    #monitoring_action
3081                                }
3082                            }
3083                        }
3084                    },
3085                    {  // Stop calls
3086                        let monitoring_action = quote! {
3087                                    let decision = self.copper_runtime.monitor.process_error(cu29::monitoring::ComponentId::new(#index), CuComponentState::Stop, &error);
3088                                    match decision {
3089                                        Decision::Abort => {
3090                                            debug!(ctx, "Stop: ABORT decision from monitoring. Component '{}' errored out \
3091                                    during stop. Aborting all the other starts.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#index)));
3092                                            return Ok(());
3093
3094                                        }
3095                                        Decision::Ignore => {
3096                                            debug!(ctx, "Stop: IGNORE decision from monitoring. Component '{}' errored out \
3097                                    during stop. The runtime will continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#index)));
3098                                        }
3099                                        Decision::Shutdown => {
3100                                            debug!(ctx, "Stop: SHUTDOWN decision from monitoring. Component '{}' errored out \
3101                                    during stop. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#index)));
3102                                            return Err(CuError::new_with_cause("Component errored out during stop.", error));
3103                                        }
3104                                    }
3105                            };
3106                        let call_sim_callback = if sim_mode {
3107                            quote! {
3108                                // Ask the sim if this task should be executed or overridden by the sim.
3109                                let ovr = sim_callback(SimStep::#enum_name(CuTaskCallbackState::Stop));
3110
3111                                let doit = if let SimOverride::Errored(reason) = ovr  {
3112                                    let error: CuError = reason.into();
3113                                    #monitoring_action
3114                                    false
3115                               }
3116                               else {
3117                                    ovr == SimOverride::ExecuteByRuntime
3118                               };
3119                            }
3120                        } else {
3121                            quote! {
3122                                let doit = true;  // in normal mode always execute the steps in the runtime.
3123                            }
3124                        };
3125                        let alloc_open = alloc_scope_open_tokens();
3126                        let alloc_close = alloc_scope_close_tokens(
3127                            quote! { self.copper_runtime.monitor },
3128                            quote! { #index },
3129                            quote! { CuComponentState::Stop },
3130                        );
3131                        quote! {
3132                            #call_sim_callback
3133                            if doit {
3134                                self.copper_runtime.record_execution_marker(
3135                                    cu29::monitoring::ExecutionMarker {
3136                                        component_id: cu29::monitoring::ComponentId::new(#index),
3137                                        step: CuComponentState::Stop,
3138                                        culistid: None,
3139                                    }
3140                                );
3141                                let task = &mut self.copper_runtime.tasks.#task_index;
3142                                ctx.set_current_task(#index);
3143                                #alloc_open
3144                                let __cu_step_result = task.stop(&ctx);
3145                                #alloc_close
3146                                if let Err(error) = __cu_step_result {
3147                                    #monitoring_action
3148                                }
3149                            }
3150                        }
3151                    },
3152                    {  // Preprocess calls
3153                        let monitoring_action = quote! {
3154                            let decision = monitor.process_error(cu29::monitoring::ComponentId::new(#index), CuComponentState::Preprocess, &error);
3155                            match decision {
3156                                Decision::Abort => {
3157                                    debug!(ctx, "Preprocess: ABORT decision from monitoring. Component '{}' errored out \
3158                                during preprocess. Aborting all the other starts.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#index)));
3159                                    return Ok(());
3160
3161                                }
3162                                Decision::Ignore => {
3163                                    debug!(ctx, "Preprocess: IGNORE decision from monitoring. Component '{}' errored out \
3164                                during preprocess. The runtime will continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#index)));
3165                                }
3166                                Decision::Shutdown => {
3167                                    debug!(ctx, "Preprocess: SHUTDOWN decision from monitoring. Component '{}' errored out \
3168                                during preprocess. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#index)));
3169                                    return Err(CuError::new_with_cause("Component errored out during preprocess.", error));
3170                                }
3171                            }
3172                        };
3173                        let call_sim_callback = if sim_mode {
3174                            quote! {
3175                                // Ask the sim if this task should be executed or overridden by the sim.
3176                                let ovr = sim_callback(SimStep::#enum_name(CuTaskCallbackState::Preprocess));
3177
3178                                let doit = if let SimOverride::Errored(reason) = ovr  {
3179                                    let error: CuError = reason.into();
3180                                    #monitoring_action
3181                                    false
3182                                } else {
3183                                    ovr == SimOverride::ExecuteByRuntime
3184                                };
3185                            }
3186                        } else {
3187                            quote! {
3188                                let doit = true;  // in normal mode always execute the steps in the runtime.
3189                            }
3190                        };
3191                        let alloc_open = alloc_scope_open_tokens();
3192                        let alloc_close = alloc_scope_close_tokens(
3193                            quote! { monitor },
3194                            quote! { #index },
3195                            quote! { CuComponentState::Preprocess },
3196                        );
3197                        quote! {
3198                            #call_sim_callback
3199                            if doit {
3200                                execution_probe.record(cu29::monitoring::ExecutionMarker {
3201                                    component_id: cu29::monitoring::ComponentId::new(#index),
3202                                    step: CuComponentState::Preprocess,
3203                                    culistid: None,
3204                                });
3205                                ctx.set_current_task(#index);
3206                                #alloc_open
3207                                let maybe_error = {
3208                                    #rt_guard
3209                                    tasks.#task_index.preprocess(&ctx)
3210                                };
3211                                #alloc_close
3212                                if let Err(error) = maybe_error {
3213                                    #monitoring_action
3214                                }
3215                            }
3216                        }
3217                    },
3218                    {  // Postprocess calls
3219                        let monitoring_action = quote! {
3220                            let decision = monitor.process_error(cu29::monitoring::ComponentId::new(#index), CuComponentState::Postprocess, &error);
3221                            match decision {
3222                                Decision::Abort => {
3223                                    debug!(ctx, "Postprocess: ABORT decision from monitoring. Component '{}' errored out \
3224                                during postprocess. Aborting all the other starts.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#index)));
3225                                    return Ok(());
3226
3227                                }
3228                                Decision::Ignore => {
3229                                    debug!(ctx, "Postprocess: IGNORE decision from monitoring. Component '{}' errored out \
3230                                during postprocess. The runtime will continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#index)));
3231                                }
3232                                Decision::Shutdown => {
3233                                    debug!(ctx, "Postprocess: SHUTDOWN decision from monitoring. Component '{}' errored out \
3234                                during postprocess. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#index)));
3235                                    return Err(CuError::new_with_cause("Component errored out during postprocess.", error));
3236                                }
3237                            }
3238                        };
3239                        let call_sim_callback = if sim_mode {
3240                            quote! {
3241                                // Ask the sim if this task should be executed or overridden by the sim.
3242                                let ovr = sim_callback(SimStep::#enum_name(CuTaskCallbackState::Postprocess));
3243
3244                                let doit = if let SimOverride::Errored(reason) = ovr  {
3245                                    let error: CuError = reason.into();
3246                                    #monitoring_action
3247                                    false
3248                                } else {
3249                                    ovr == SimOverride::ExecuteByRuntime
3250                                };
3251                            }
3252                        } else {
3253                            quote! {
3254                                let doit = true;  // in normal mode always execute the steps in the runtime.
3255                            }
3256                        };
3257                        let alloc_open = alloc_scope_open_tokens();
3258                        let alloc_close = alloc_scope_close_tokens(
3259                            quote! { monitor },
3260                            quote! { #index },
3261                            quote! { CuComponentState::Postprocess },
3262                        );
3263                        quote! {
3264                            #call_sim_callback
3265                            if doit {
3266                                execution_probe.record(cu29::monitoring::ExecutionMarker {
3267                                    component_id: cu29::monitoring::ComponentId::new(#index),
3268                                    step: CuComponentState::Postprocess,
3269                                    culistid: None,
3270                                });
3271                                ctx.set_current_task(#index);
3272                                #alloc_open
3273                                let maybe_error = {
3274                                    #rt_guard
3275                                    tasks.#task_index.postprocess(&ctx)
3276                                };
3277                                #alloc_close
3278                                if let Err(error) = maybe_error {
3279                                    #monitoring_action
3280                                }
3281                            }
3282                        }
3283                    }
3284                )
3285            })
3286        );
3287
3288        let bridge_start_calls: Vec<proc_macro2::TokenStream> = culist_bridge_specs
3289            .iter()
3290            .map(|spec| {
3291                let bridge_index = int2sliceindex(spec.tuple_index as u32);
3292                let monitor_index = syn::Index::from(
3293                    spec.monitor_index
3294                        .expect("Bridge missing monitor index for start"),
3295                );
3296                let enum_ident = Ident::new(
3297                    &config_id_to_enum(&format!("{}_bridge", spec.id)),
3298                    Span::call_site(),
3299                );
3300                let call_sim = if sim_mode {
3301                    quote! {
3302                        let doit = {
3303                            let state = SimStep::#enum_ident(cu29::simulation::CuBridgeLifecycleState::Start);
3304                            let ovr = sim_callback(state);
3305                            if let SimOverride::Errored(reason) = ovr {
3306                                let error: CuError = reason.into();
3307                                let decision = self.copper_runtime.monitor.process_error(cu29::monitoring::ComponentId::new(#monitor_index), CuComponentState::Start, &error);
3308                                match decision {
3309                                    Decision::Abort => { debug!(ctx, "Start: ABORT decision from monitoring. Component '{}' errored out during start. Aborting all the other starts.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index))); return Ok(()); }
3310                                    Decision::Ignore => { debug!(ctx, "Start: IGNORE decision from monitoring. Component '{}' errored out during start. The runtime will continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index))); false }
3311                                    Decision::Shutdown => { debug!(ctx, "Start: SHUTDOWN decision from monitoring. Component '{}' errored out during start. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index))); return Err(CuError::new_with_cause("Component errored out during start.", error)); }
3312                                }
3313                            } else {
3314                                ovr == SimOverride::ExecuteByRuntime
3315                            }
3316                        };
3317                    }
3318                } else {
3319                    quote! { let doit = true; }
3320                };
3321                let alloc_open = alloc_scope_open_tokens();
3322                let alloc_close = alloc_scope_close_tokens(
3323                    quote! { self.copper_runtime.monitor },
3324                    quote! { #monitor_index },
3325                    quote! { CuComponentState::Start },
3326                );
3327                quote! {
3328                    {
3329                        #call_sim
3330                        if !doit { return Ok(()); }
3331                        self.copper_runtime.record_execution_marker(
3332                            cu29::monitoring::ExecutionMarker {
3333                                component_id: cu29::monitoring::ComponentId::new(#monitor_index),
3334                                step: CuComponentState::Start,
3335                                culistid: None,
3336                            }
3337                        );
3338                        ctx.set_current_component(#monitor_index);
3339                        ctx.clear_current_task();
3340                        let bridge = &mut self.copper_runtime.bridges.#bridge_index;
3341                        #alloc_open
3342                        let __cu_step_result = bridge.start(&ctx);
3343                        #alloc_close
3344                        if let Err(error) = __cu_step_result {
3345                            let decision = self.copper_runtime.monitor.process_error(cu29::monitoring::ComponentId::new(#monitor_index), CuComponentState::Start, &error);
3346                            match decision {
3347                                Decision::Abort => {
3348                                    debug!(ctx, "Start: ABORT decision from monitoring. Component '{}' errored out during start. Aborting all the other starts.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
3349                                    return Ok(());
3350                                }
3351                                Decision::Ignore => {
3352                                    debug!(ctx, "Start: IGNORE decision from monitoring. Component '{}' errored out during start. The runtime will continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
3353                                }
3354                                Decision::Shutdown => {
3355                                    debug!(ctx, "Start: SHUTDOWN decision from monitoring. Component '{}' errored out during start. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
3356                                    return Err(CuError::new_with_cause("Component errored out during start.", error));
3357                                }
3358                            }
3359                        }
3360                    }
3361                }
3362            })
3363            .collect();
3364
3365        let bridge_stop_calls: Vec<proc_macro2::TokenStream> = culist_bridge_specs
3366            .iter()
3367            .map(|spec| {
3368                let bridge_index = int2sliceindex(spec.tuple_index as u32);
3369                let monitor_index = syn::Index::from(
3370                    spec.monitor_index
3371                        .expect("Bridge missing monitor index for stop"),
3372                );
3373                let enum_ident = Ident::new(
3374                    &config_id_to_enum(&format!("{}_bridge", spec.id)),
3375                    Span::call_site(),
3376                );
3377                let call_sim = if sim_mode {
3378                    quote! {
3379                        let doit = {
3380                            let state = SimStep::#enum_ident(cu29::simulation::CuBridgeLifecycleState::Stop);
3381                            let ovr = sim_callback(state);
3382                            if let SimOverride::Errored(reason) = ovr {
3383                                let error: CuError = reason.into();
3384                                let decision = self.copper_runtime.monitor.process_error(cu29::monitoring::ComponentId::new(#monitor_index), CuComponentState::Stop, &error);
3385                                match decision {
3386                                    Decision::Abort => { debug!(ctx, "Stop: ABORT decision from monitoring. Component '{}' errored out during stop. Aborting all the other stops.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index))); return Ok(()); }
3387                                    Decision::Ignore => { debug!(ctx, "Stop: IGNORE decision from monitoring. Component '{}' errored out during stop. The runtime will continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index))); false }
3388                                    Decision::Shutdown => { debug!(ctx, "Stop: SHUTDOWN decision from monitoring. Component '{}' errored out during stop. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index))); return Err(CuError::new_with_cause("Component errored out during stop.", error)); }
3389                                }
3390                            } else {
3391                                ovr == SimOverride::ExecuteByRuntime
3392                            }
3393                        };
3394                    }
3395                } else {
3396                    quote! { let doit = true; }
3397                };
3398                let alloc_open = alloc_scope_open_tokens();
3399                let alloc_close = alloc_scope_close_tokens(
3400                    quote! { self.copper_runtime.monitor },
3401                    quote! { #monitor_index },
3402                    quote! { CuComponentState::Stop },
3403                );
3404                quote! {
3405                    {
3406                        #call_sim
3407                        if !doit { return Ok(()); }
3408                        self.copper_runtime.record_execution_marker(
3409                            cu29::monitoring::ExecutionMarker {
3410                                component_id: cu29::monitoring::ComponentId::new(#monitor_index),
3411                                step: CuComponentState::Stop,
3412                                culistid: None,
3413                            }
3414                        );
3415                        ctx.set_current_component(#monitor_index);
3416                        ctx.clear_current_task();
3417                        let bridge = &mut self.copper_runtime.bridges.#bridge_index;
3418                        #alloc_open
3419                        let __cu_step_result = bridge.stop(&ctx);
3420                        #alloc_close
3421                        if let Err(error) = __cu_step_result {
3422                            let decision = self.copper_runtime.monitor.process_error(cu29::monitoring::ComponentId::new(#monitor_index), CuComponentState::Stop, &error);
3423                            match decision {
3424                                Decision::Abort => {
3425                                    debug!(ctx, "Stop: ABORT decision from monitoring. Component '{}' errored out during stop. Aborting all the other stops.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
3426                                    return Ok(());
3427                                }
3428                                Decision::Ignore => {
3429                                    debug!(ctx, "Stop: IGNORE decision from monitoring. Component '{}' errored out during stop. The runtime will continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
3430                                }
3431                                Decision::Shutdown => {
3432                                    debug!(ctx, "Stop: SHUTDOWN decision from monitoring. Component '{}' errored out during stop. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
3433                                    return Err(CuError::new_with_cause("Component errored out during stop.", error));
3434                                }
3435                            }
3436                        }
3437                    }
3438                }
3439            })
3440            .collect();
3441
3442        let bridge_preprocess_calls: Vec<proc_macro2::TokenStream> = culist_bridge_specs
3443            .iter()
3444            .map(|spec| {
3445                let bridge_index = int2sliceindex(spec.tuple_index as u32);
3446                let monitor_index = syn::Index::from(
3447                    spec.monitor_index
3448                        .expect("Bridge missing monitor index for preprocess"),
3449                );
3450                let enum_ident = Ident::new(
3451                    &config_id_to_enum(&format!("{}_bridge", spec.id)),
3452                    Span::call_site(),
3453                );
3454                let call_sim = if sim_mode {
3455                    quote! {
3456                        let doit = {
3457                            let state = SimStep::#enum_ident(cu29::simulation::CuBridgeLifecycleState::Preprocess);
3458                            let ovr = sim_callback(state);
3459                            if let SimOverride::Errored(reason) = ovr {
3460                                let error: CuError = reason.into();
3461                                let decision = monitor.process_error(cu29::monitoring::ComponentId::new(#monitor_index), CuComponentState::Preprocess, &error);
3462                                match decision {
3463                                    Decision::Abort => { debug!(ctx, "Preprocess: ABORT decision from monitoring. Component '{}' errored out during preprocess. Aborting all the other starts.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index))); return Ok(()); }
3464                                    Decision::Ignore => { debug!(ctx, "Preprocess: IGNORE decision from monitoring. Component '{}' errored out during preprocess. The runtime will continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index))); false }
3465                                    Decision::Shutdown => { debug!(ctx, "Preprocess: SHUTDOWN decision from monitoring. Component '{}' errored out during preprocess. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index))); return Err(CuError::new_with_cause("Component errored out during preprocess.", error)); }
3466                                }
3467                            } else {
3468                                ovr == SimOverride::ExecuteByRuntime
3469                            }
3470                        };
3471                    }
3472                } else {
3473                    quote! { let doit = true; }
3474                };
3475                let alloc_open = alloc_scope_open_tokens();
3476                let alloc_close = alloc_scope_close_tokens(
3477                    quote! { monitor },
3478                    quote! { #monitor_index },
3479                    quote! { CuComponentState::Preprocess },
3480                );
3481                quote! {
3482                    {
3483                        #call_sim
3484                        if doit {
3485                            ctx.set_current_component(#monitor_index);
3486                            ctx.clear_current_task();
3487                            let bridge = &mut __cu_bridges.#bridge_index;
3488                            execution_probe.record(cu29::monitoring::ExecutionMarker {
3489                                component_id: cu29::monitoring::ComponentId::new(#monitor_index),
3490                                step: CuComponentState::Preprocess,
3491                                culistid: None,
3492                            });
3493                            #alloc_open
3494                            let maybe_error = {
3495                                #rt_guard
3496                                bridge.preprocess(&ctx)
3497                            };
3498                            #alloc_close
3499                            if let Err(error) = maybe_error {
3500                                let decision = monitor.process_error(cu29::monitoring::ComponentId::new(#monitor_index), CuComponentState::Preprocess, &error);
3501                                match decision {
3502                                    Decision::Abort => {
3503                                        debug!(ctx, "Preprocess: ABORT decision from monitoring. Component '{}' errored out during preprocess. Aborting all the other starts.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
3504                                        return Ok(());
3505                                    }
3506                                    Decision::Ignore => {
3507                                        debug!(ctx, "Preprocess: IGNORE decision from monitoring. Component '{}' errored out during preprocess. The runtime will continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
3508                                    }
3509                                    Decision::Shutdown => {
3510                                        debug!(ctx, "Preprocess: SHUTDOWN decision from monitoring. Component '{}' errored out during preprocess. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
3511                                        return Err(CuError::new_with_cause("Component errored out during preprocess.", error));
3512                                    }
3513                                }
3514                            }
3515                        }
3516                    }
3517                }
3518            })
3519            .collect();
3520
3521        let bridge_postprocess_calls: Vec<proc_macro2::TokenStream> = culist_bridge_specs
3522            .iter()
3523            .map(|spec| {
3524                let freeze_bridge = keyframe_freeze_bridge_tokens(keyframe_logging_enabled);
3525                let bridge_index = int2sliceindex(spec.tuple_index as u32);
3526                let monitor_index = syn::Index::from(
3527                    spec.monitor_index
3528                        .expect("Bridge missing monitor index for postprocess"),
3529                );
3530                let enum_ident = Ident::new(
3531                    &config_id_to_enum(&format!("{}_bridge", spec.id)),
3532                    Span::call_site(),
3533                );
3534                let call_sim = if sim_mode {
3535                    quote! {
3536                        let doit = {
3537                            let state = SimStep::#enum_ident(cu29::simulation::CuBridgeLifecycleState::Postprocess);
3538                            let ovr = sim_callback(state);
3539                            if let SimOverride::Errored(reason) = ovr {
3540                                let error: CuError = reason.into();
3541                                let decision = monitor.process_error(cu29::monitoring::ComponentId::new(#monitor_index), CuComponentState::Postprocess, &error);
3542                                match decision {
3543                                    Decision::Abort => { debug!(ctx, "Postprocess: ABORT decision from monitoring. Component '{}' errored out during postprocess. Aborting all the other starts.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index))); return Ok(()); }
3544                                    Decision::Ignore => { debug!(ctx, "Postprocess: IGNORE decision from monitoring. Component '{}' errored out during postprocess. The runtime will continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index))); false }
3545                                    Decision::Shutdown => { debug!(ctx, "Postprocess: SHUTDOWN decision from monitoring. Component '{}' errored out during postprocess. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index))); return Err(CuError::new_with_cause("Component errored out during postprocess.", error)); }
3546                                }
3547                            } else {
3548                                ovr == SimOverride::ExecuteByRuntime
3549                            }
3550                        };
3551                    }
3552                } else {
3553                    quote! { let doit = true; }
3554                };
3555                let alloc_open = alloc_scope_open_tokens();
3556                let alloc_close = alloc_scope_close_tokens(
3557                    quote! { monitor },
3558                    quote! { #monitor_index },
3559                    quote! { CuComponentState::Postprocess },
3560                );
3561                quote! {
3562                    {
3563                        #call_sim
3564                        if doit {
3565                            ctx.set_current_component(#monitor_index);
3566                            ctx.clear_current_task();
3567                            let bridge = &mut __cu_bridges.#bridge_index;
3568                            #freeze_bridge
3569                            execution_probe.record(cu29::monitoring::ExecutionMarker {
3570                                component_id: cu29::monitoring::ComponentId::new(#monitor_index),
3571                                step: CuComponentState::Postprocess,
3572                                culistid: Some(clid),
3573                            });
3574                            #alloc_open
3575                            let maybe_error = {
3576                                #rt_guard
3577                                bridge.postprocess(&ctx)
3578                            };
3579                            #alloc_close
3580                            if let Err(error) = maybe_error {
3581                                let decision = monitor.process_error(cu29::monitoring::ComponentId::new(#monitor_index), CuComponentState::Postprocess, &error);
3582                                match decision {
3583                                    Decision::Abort => {
3584                                        debug!(ctx, "Postprocess: ABORT decision from monitoring. Component '{}' errored out during postprocess. Aborting all the other starts.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
3585                                        return Ok(());
3586                                    }
3587                                    Decision::Ignore => {
3588                                        debug!(ctx, "Postprocess: IGNORE decision from monitoring. Component '{}' errored out during postprocess. The runtime will continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
3589                                    }
3590                                    Decision::Shutdown => {
3591                                        debug!(ctx, "Postprocess: SHUTDOWN decision from monitoring. Component '{}' errored out during postprocess. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
3592                                        return Err(CuError::new_with_cause("Component errored out during postprocess.", error));
3593                                    }
3594                                }
3595                            }
3596                        }
3597                    }
3598                }
3599            })
3600            .collect();
3601
3602        let mut start_calls = bridge_start_calls;
3603        start_calls.extend(task_start_calls);
3604        let mut stop_calls = task_stop_calls;
3605        stop_calls.extend(bridge_stop_calls);
3606        let mut preprocess_calls = bridge_preprocess_calls;
3607        preprocess_calls.extend(task_preprocess_calls);
3608        let mut postprocess_calls = task_postprocess_calls;
3609        postprocess_calls.extend(bridge_postprocess_calls);
3610        let parallel_rt_run_supported = std && parallel_rt_enabled && !sim_mode;
3611
3612        let output_pack_sizes = collect_output_pack_sizes(&culist_plan);
3613        let runtime_plan_code_and_logging: Vec<(
3614            proc_macro2::TokenStream,
3615            proc_macro2::TokenStream,
3616        )> = culist_plan
3617            .steps
3618            .iter()
3619            .enumerate()
3620            .map(|(step_index, unit)| match unit {
3621                CuExecutionUnit::Step(step) => {
3622                    #[cfg(feature = "macro_debug")]
3623                    eprintln!(
3624                        "{} -> {} as {:?}/{:?}. task_id: {} Input={:?}, Output={:?}",
3625                        step.node.get_id(),
3626                        step.node.get_type(),
3627                        step.task_type,
3628                        step.phase,
3629                        step.node_id,
3630                        step.input_msg_indices_types,
3631                        step.output_msg_pack
3632                    );
3633
3634                    match &culist_exec_entities[step.node_id as usize].kind {
3635                        ExecutionEntityKind::Task { task_index } => {
3636                            let step_ctx = StepGenerationContext::new(
3637                                &output_pack_sizes,
3638                                &task_input_layouts,
3639                                mission.as_str(),
3640                                sim_mode,
3641                                keyframe_logging_enabled,
3642                                &mission_mod,
3643                                ParallelLifecyclePlacement::default(),
3644                                false,
3645                            );
3646                            let task_instance = {
3647                                let node_index = int2sliceindex(*task_index as u32);
3648                                quote! { tasks.#node_index }
3649                            };
3650                            match step.phase {
3651                                CuStepPhase::Whole => generate_task_execution_tokens(
3652                                    step,
3653                                    *task_index,
3654                                    &task_specs,
3655                                    &runtime_task_types[*task_index],
3656                                    step_ctx,
3657                                    TaskExecutionTokens::new(quote! {}, task_instance),
3658                                ),
3659                                CuStepPhase::AnytimeBase => generate_anytime_base_block(
3660                                    step,
3661                                    *task_index,
3662                                    &task_specs,
3663                                    &step_ctx,
3664                                    &task_instance,
3665                                ),
3666                                CuStepPhase::AnytimeRefine => {
3667                                    let ordinal = anytime_refine_ordinals[step_index]
3668                                        .expect("refine step without a precomputed ordinal");
3669                                    let total = anytime_refine_totals[&step.node_id];
3670                                    (
3671                                        generate_anytime_refine_block(
3672                                            step,
3673                                            *task_index,
3674                                            &task_specs,
3675                                            &step_ctx,
3676                                            &task_instance,
3677                                            ordinal,
3678                                            total,
3679                                        ),
3680                                        quote! {},
3681                                    )
3682                                }
3683                            }
3684                        }
3685                        ExecutionEntityKind::BridgeRx {
3686                            bridge_index,
3687                            channel_index,
3688                        } => {
3689                            let spec = &culist_bridge_specs[*bridge_index];
3690                            generate_bridge_rx_execution_tokens(
3691                                step,
3692                                spec,
3693                                *channel_index,
3694                                StepGenerationContext::new(
3695                                    &output_pack_sizes,
3696                                    &task_input_layouts,
3697                                    mission.as_str(),
3698                                    sim_mode,
3699                                    keyframe_logging_enabled,
3700                                    &mission_mod,
3701                                    ParallelLifecyclePlacement::default(),
3702                                    false,
3703                                ),
3704                                {
3705                                    let bridge_tuple_index =
3706                                        int2sliceindex(spec.tuple_index as u32);
3707                                    quote! { let bridge = &mut __cu_bridges.#bridge_tuple_index; }
3708                                },
3709                            )
3710                        }
3711                        ExecutionEntityKind::BridgeTx {
3712                            bridge_index,
3713                            channel_index,
3714                        } => {
3715                            let spec = &culist_bridge_specs[*bridge_index];
3716                            generate_bridge_tx_execution_tokens(
3717                                step,
3718                                spec,
3719                                *channel_index,
3720                                StepGenerationContext::new(
3721                                    &output_pack_sizes,
3722                                    &task_input_layouts,
3723                                    mission.as_str(),
3724                                    sim_mode,
3725                                    keyframe_logging_enabled,
3726                                    &mission_mod,
3727                                    ParallelLifecyclePlacement::default(),
3728                                    false,
3729                                ),
3730                                {
3731                                    let bridge_tuple_index =
3732                                        int2sliceindex(spec.tuple_index as u32);
3733                                    quote! { let bridge = &mut __cu_bridges.#bridge_tuple_index; }
3734                                },
3735                            )
3736                        }
3737                    }
3738                }
3739                CuExecutionUnit::Loop(_) => {
3740                    panic!("Execution loops are not supported in runtime generation");
3741                }
3742            })
3743            .collect();
3744        let parallel_lifecycle_placements = if parallel_rt_run_supported {
3745            Some(build_parallel_lifecycle_placements(
3746                &culist_plan,
3747                &culist_exec_entities,
3748            ))
3749        } else {
3750            None
3751        };
3752        // A sim runtime does not execute parallel-rt, but it must decode the component
3753        // frames in the execution-wave order used by the matching recording runtime.
3754        let restore_parallel_placements = (std && parallel_rt_enabled)
3755            .then(|| build_parallel_lifecycle_placements(&culist_plan, &culist_exec_entities));
3756        let mut keyframe_restore_order = Vec::<ParallelLifecycleKey>::new();
3757        if let Some(placements) = restore_parallel_placements.as_ref() {
3758            for (step_index, unit) in culist_plan.steps.iter().enumerate() {
3759                let CuExecutionUnit::Step(step) = unit else {
3760                    panic!("Execution loops are not supported in runtime generation");
3761                };
3762                match &culist_exec_entities[step.node_id as usize].kind {
3763                    ExecutionEntityKind::Task { task_index } => {
3764                        if step.phase != CuStepPhase::AnytimeRefine
3765                            && !keyframe_restore_order
3766                                .contains(&ParallelLifecycleKey::Task(*task_index))
3767                        {
3768                            keyframe_restore_order.push(ParallelLifecycleKey::Task(*task_index));
3769                        }
3770                    }
3771                    ExecutionEntityKind::BridgeRx { bridge_index, .. }
3772                    | ExecutionEntityKind::BridgeTx { bridge_index, .. } => {
3773                        if placements[step_index].postprocess {
3774                            keyframe_restore_order
3775                                .push(ParallelLifecycleKey::Bridge(*bridge_index));
3776                        }
3777                    }
3778                }
3779            }
3780        } else {
3781            keyframe_restore_order.extend(
3782                keyframe_task_restore_order
3783                    .iter()
3784                    .copied()
3785                    .map(ParallelLifecycleKey::Task),
3786            );
3787            keyframe_restore_order
3788                .extend((0..culist_bridge_specs.len()).map(ParallelLifecycleKey::Bridge));
3789        }
3790        let keyframe_restore_code: Vec<proc_macro2::TokenStream> = keyframe_restore_order
3791            .iter()
3792            .map(|component| match component {
3793                ParallelLifecycleKey::Task(index) => {
3794                    let task_tuple_index = syn::Index::from(*index);
3795                    let skip_substituted = sim_mode
3796                        && task_specs.cutypes[*index] != CuTaskType::Regular
3797                        && !task_specs.run_in_sim_flags[*index];
3798                    if skip_substituted {
3799                        quote! {
3800                            let _ = frames.next_frame()?;
3801                        }
3802                    } else {
3803                        quote! {
3804                            let frame = frames.next_frame()?;
3805                            cu29::curuntime::thaw_keyframe_component(
3806                                &mut tasks.#task_tuple_index,
3807                                frame,
3808                            )?;
3809                        }
3810                    }
3811                }
3812                ParallelLifecycleKey::Bridge(index) => {
3813                    let bridge_tuple_index = syn::Index::from(*index);
3814                    if sim_mode && !culist_bridge_specs[*index].run_in_sim {
3815                        quote! {
3816                            let _ = frames.next_frame()?;
3817                        }
3818                    } else {
3819                        quote! {
3820                            let frame = frames.next_frame()?;
3821                            cu29::curuntime::thaw_keyframe_component(
3822                                &mut __cu_bridges.#bridge_tuple_index,
3823                                frame,
3824                            )?;
3825                        }
3826                    }
3827                }
3828            })
3829            .collect();
3830        let keyframe_preallocation_code: Vec<proc_macro2::TokenStream> = keyframe_restore_order
3831            .iter()
3832            .map(|component| match component {
3833                ParallelLifecycleKey::Task(index) => {
3834                    let task_tuple_index = syn::Index::from(*index);
3835                    quote! {
3836                        kf_manager.include_capture_capacity(&tasks.#task_tuple_index)?;
3837                    }
3838                }
3839                ParallelLifecycleKey::Bridge(index) => {
3840                    let bridge_tuple_index = syn::Index::from(*index);
3841                    quote! {
3842                        kf_manager.include_capture_capacity(&__cu_bridges.#bridge_tuple_index)?;
3843                    }
3844                }
3845            })
3846            .collect();
3847        let runtime_plan_parallel_code_and_logging: Option<
3848            Vec<(proc_macro2::TokenStream, proc_macro2::TokenStream)>,
3849        > = if parallel_rt_run_supported {
3850            Some(
3851                culist_plan
3852                    .steps
3853                    .iter()
3854                    .enumerate()
3855                    .filter_map(|(step_index, unit)| match unit {
3856                        CuExecutionUnit::Step(step) => match &culist_exec_entities
3857                            [step.node_id as usize]
3858                            .kind
3859                        {
3860                            ExecutionEntityKind::Task { task_index } => {
3861                                let task_index_ts = int2sliceindex(*task_index as u32);
3862                                // The simple first parallel-rt placement for an
3863                                // anytime node: ONE stage running base and every
3864                                // refine quantum contiguously, so the whole job
3865                                // stays on the same worker thread (the quanta
3866                                // touch the same task state and output slot —
3867                                // memory locality). Refine steps therefore emit
3868                                // no stage of their own.
3869                                if step.phase == CuStepPhase::AnytimeRefine {
3870                                    return None;
3871                                }
3872                                if step.phase == CuStepPhase::AnytimeBase {
3873                                    let step_ctx = StepGenerationContext::new(
3874                                        &output_pack_sizes,
3875                                        &task_input_layouts,
3876                                        mission.as_str(),
3877                                        false,
3878                                        keyframe_logging_enabled,
3879                                        &mission_mod,
3880                                        ParallelLifecyclePlacement::default(),
3881                                        true,
3882                                    );
3883                                    let task_instance = quote! { (*task) };
3884                                    let (base_block, logging) = generate_anytime_base_block(
3885                                        step,
3886                                        *task_index,
3887                                        &task_specs,
3888                                        &step_ctx,
3889                                        &task_instance,
3890                                    );
3891                                    let total = anytime_refine_totals[&step.node_id];
3892                                    let refine_blocks: Vec<proc_macro2::TokenStream> = (1..=total)
3893                                        .map(|ordinal| {
3894                                            generate_anytime_refine_block(
3895                                                step,
3896                                                *task_index,
3897                                                &task_specs,
3898                                                &step_ctx,
3899                                                &task_instance,
3900                                                ordinal,
3901                                                total,
3902                                            )
3903                                        })
3904                                        .collect();
3905                                    let (parallel_pre, parallel_post) = parallel_task_lifecycle_tokens(
3906                                        task_trait_for_specs(&task_specs, *task_index),
3907                                        &task_specs.task_types[*task_index],
3908                                        *task_index,
3909                                        &mission_mod,
3910                                        &task_instance,
3911                                        parallel_lifecycle_placements
3912                                            .as_ref()
3913                                            .expect("parallel lifecycle placements missing")[step_index],
3914                                    );
3915                                    let job_local = anytime_job_local_tokens(&task_specs, *task_index);
3916                                    let body = quote! {
3917                                        let _task_lock = step_rt.task_locks.#task_index_ts.lock().expect("parallel task lock poisoned");
3918                                        let task = unsafe { step_rt.task_ptrs.#task_index_ts.as_mut() };
3919                                        #parallel_pre
3920                                        #job_local
3921                                        #base_block
3922                                        #(#refine_blocks)*
3923                                        #parallel_post
3924                                    };
3925                                    return Some((wrap_process_step_tokens(true, body), logging));
3926                                }
3927                                Some(generate_task_execution_tokens(
3928                                    step,
3929                                    *task_index,
3930                                    &task_specs,
3931                                    &task_specs.task_types[*task_index],
3932                                    StepGenerationContext::new(
3933                                        &output_pack_sizes,
3934                                        &task_input_layouts,
3935                                        mission.as_str(),
3936                                        false,
3937                                        keyframe_logging_enabled,
3938                                        &mission_mod,
3939                                        parallel_lifecycle_placements
3940                                            .as_ref()
3941                                            .expect("parallel lifecycle placements missing")[step_index],
3942                                        true,
3943                                    ),
3944                                    TaskExecutionTokens::new(quote! {
3945                                        let _task_lock = step_rt.task_locks.#task_index_ts.lock().expect("parallel task lock poisoned");
3946                                        let task = unsafe { step_rt.task_ptrs.#task_index_ts.as_mut() };
3947                                    }, quote! { (*task) }),
3948                                ))
3949                            }
3950                            ExecutionEntityKind::BridgeRx {
3951                                bridge_index,
3952                                channel_index,
3953                            } => {
3954                                let spec = &culist_bridge_specs[*bridge_index];
3955                                let bridge_index_ts = int2sliceindex(spec.tuple_index as u32);
3956                                Some(generate_bridge_rx_execution_tokens(
3957                                    step,
3958                                    spec,
3959                                    *channel_index,
3960                                    StepGenerationContext::new(
3961                                        &output_pack_sizes,
3962                                        &task_input_layouts,
3963                                        mission.as_str(),
3964                                        false,
3965                                        keyframe_logging_enabled,
3966                                        &mission_mod,
3967                                        parallel_lifecycle_placements
3968                                            .as_ref()
3969                                            .expect("parallel lifecycle placements missing")
3970                                            [step_index],
3971                                        true,
3972                                    ),
3973                                    quote! {
3974                                        let _bridge_lock = step_rt.bridge_locks.#bridge_index_ts.lock().expect("parallel bridge lock poisoned");
3975                                        let bridge = unsafe { step_rt.bridge_ptrs.#bridge_index_ts.as_mut() };
3976                                    },
3977                                ))
3978                            }
3979                            ExecutionEntityKind::BridgeTx {
3980                                bridge_index,
3981                                channel_index,
3982                            } => {
3983                                let spec = &culist_bridge_specs[*bridge_index];
3984                                let bridge_index_ts = int2sliceindex(spec.tuple_index as u32);
3985                                Some(generate_bridge_tx_execution_tokens(
3986                                    step,
3987                                    spec,
3988                                    *channel_index,
3989                                    StepGenerationContext::new(
3990                                        &output_pack_sizes,
3991                                        &task_input_layouts,
3992                                        mission.as_str(),
3993                                        false,
3994                                        keyframe_logging_enabled,
3995                                        &mission_mod,
3996                                        parallel_lifecycle_placements
3997                                            .as_ref()
3998                                            .expect("parallel lifecycle placements missing")[step_index],
3999                                        true,
4000                                    ),
4001                                    quote! {
4002                                        let _bridge_lock = step_rt.bridge_locks.#bridge_index_ts.lock().expect("parallel bridge lock poisoned");
4003                                        let bridge = unsafe { step_rt.bridge_ptrs.#bridge_index_ts.as_mut() };
4004                                    },
4005                                ))
4006                            }
4007                        },
4008                        CuExecutionUnit::Loop(_) => {
4009                            panic!("Execution loops are not supported in runtime generation");
4010                        }
4011                    })
4012                    .collect(),
4013            )
4014        } else {
4015            None
4016        };
4017
4018        let sim_support = if sim_mode {
4019            Some(gen_sim_support(
4020                &culist_plan,
4021                &culist_exec_entities,
4022                &culist_bridge_specs,
4023            ))
4024        } else {
4025            None
4026        };
4027
4028        let recorded_replay_support = if sim_mode {
4029            Some(gen_recorded_replay_support(
4030                &culist_plan,
4031                &culist_exec_entities,
4032                &culist_bridge_specs,
4033            ))
4034        } else {
4035            None
4036        };
4037
4038        let (run_one_iteration, start_all_tasks, stop_all_tasks, run) = if sim_mode {
4039            (
4040                quote! {
4041                    #[allow(deprecated)] // implements the deprecated raw trait method
4042                    fn run_one_iteration(&mut self, sim_callback: &mut impl FnMut(SimStep) -> SimOverride) -> CuResult<()>
4043                },
4044                quote! {
4045                    #[allow(deprecated)] // implements the deprecated raw trait method
4046                    fn start_all_tasks(&mut self, sim_callback: &mut impl FnMut(SimStep) -> SimOverride) -> CuResult<()>
4047                },
4048                quote! {
4049                    #[allow(deprecated)] // implements the deprecated raw trait method
4050                    fn stop_all_tasks(&mut self, sim_callback: &mut impl FnMut(SimStep) -> SimOverride) -> CuResult<()>
4051                },
4052                quote! {
4053                    #[allow(deprecated)] // implements the deprecated raw trait method
4054                    fn run(&mut self, sim_callback: &mut impl FnMut(SimStep) -> SimOverride) -> CuResult<()>
4055                },
4056            )
4057        } else {
4058            (
4059                quote! {
4060                    #[allow(deprecated)] // implements the deprecated raw trait method
4061                    fn run_one_iteration(&mut self) -> CuResult<()>
4062                },
4063                quote! {
4064                    #[allow(deprecated)] // implements the deprecated raw trait method
4065                    fn start_all_tasks(&mut self) -> CuResult<()>
4066                },
4067                quote! {
4068                    #[allow(deprecated)] // implements the deprecated raw trait method
4069                    fn stop_all_tasks(&mut self) -> CuResult<()>
4070                },
4071                quote! {
4072                    #[allow(deprecated)] // implements the deprecated raw trait method
4073                    fn run(&mut self) -> CuResult<()>
4074                },
4075            )
4076        };
4077
4078        let sim_callback_arg = if sim_mode {
4079            Some(quote!(sim_callback))
4080        } else {
4081            None
4082        };
4083
4084        let app_trait = if sim_mode {
4085            quote!(CuSimApplication)
4086        } else {
4087            quote!(CuApplication)
4088        };
4089
4090        let sim_callback_on_new_calls = task_specs.ids.iter().enumerate().map(|(i, id)| {
4091            let enum_name = config_id_to_enum(id);
4092            let enum_ident = Ident::new(&enum_name, Span::call_site());
4093            quote! {
4094                // the answer is ignored, we have to instantiate the tasks anyway.
4095                sim_callback(SimStep::#enum_ident(CuTaskCallbackState::New(all_instances_configs[#i].cloned())));
4096            }
4097        });
4098
4099        let sim_callback_on_new_bridges = culist_bridge_specs.iter().map(|spec| {
4100            let enum_ident = Ident::new(
4101                &config_id_to_enum(&format!("{}_bridge", spec.id)),
4102                Span::call_site(),
4103            );
4104            let cfg_index = syn::Index::from(spec.config_index);
4105            quote! {
4106                sim_callback(SimStep::#enum_ident(
4107                    cu29::simulation::CuBridgeLifecycleState::New(config.bridges[#cfg_index].config.clone())
4108                ));
4109            }
4110        });
4111
4112        let sim_callback_on_new = if sim_mode {
4113            Some(quote! {
4114                let graph = config.get_graph(Some(#mission)).expect("Could not find the mission #mission");
4115                let all_instances_configs: Vec<Option<&ComponentConfig>> = graph
4116                    .get_all_nodes()
4117                    .iter()
4118                    .map(|(_, node)| node.get_instance_config())
4119                    .collect();
4120                #(#sim_callback_on_new_calls)*
4121                #(#sim_callback_on_new_bridges)*
4122            })
4123        } else {
4124            None
4125        };
4126
4127        let anytime_job_locals = build_anytime_job_locals(&task_specs);
4128        let anytime_policy_defs = build_anytime_policy_defs(&task_specs);
4129        let (runtime_plan_code, preprocess_logging_calls): (Vec<_>, Vec<_>) =
4130            itertools::multiunzip(runtime_plan_code_and_logging);
4131        let process_step_tasks_type = if sim_mode {
4132            quote!(CuSimTasks)
4133        } else {
4134            quote!(CuTasks)
4135        };
4136        let parallel_step_keyframe_local = keyframe_logging_enabled.then(|| {
4137            quote! {
4138                let kf_manager = ParallelKeyFrameAccessor::new(
4139                    step_rt.kf_manager_ptr,
4140                    step_rt.kf_lock,
4141                );
4142            }
4143        });
4144        let parallel_worker_keyframe_captures = keyframe_logging_enabled.then(|| {
4145            quote! {
4146                let kf_manager_ptr = kf_manager_ptr;
4147                let kf_lock = std::sync::Arc::clone(&kf_lock);
4148            }
4149        });
4150        let parallel_step_keyframe_fields = keyframe_logging_enabled.then(|| {
4151            quote! {
4152                kf_manager_ptr,
4153                kf_lock: kf_lock.as_ref(),
4154            }
4155        });
4156        let (
4157            parallel_process_step_idents,
4158            parallel_process_step_fn_defs,
4159            parallel_stage_worker_spawns,
4160        ): (
4161            Vec<Ident>,
4162            Vec<proc_macro2::TokenStream>,
4163            Vec<proc_macro2::TokenStream>,
4164        ) = if let Some(runtime_plan_parallel_code_and_logging) =
4165            &runtime_plan_parallel_code_and_logging
4166        {
4167            let (runtime_plan_parallel_step_code, _): (Vec<_>, Vec<_>) =
4168                itertools::multiunzip(runtime_plan_parallel_code_and_logging.clone());
4169            let parallel_process_step_idents: Vec<Ident> = (0..runtime_plan_parallel_step_code
4170                .len())
4171                .map(|index| format_ident!("__cu_parallel_process_step_{index}"))
4172                .collect();
4173            let parallel_process_step_fn_defs: Vec<proc_macro2::TokenStream> =
4174                parallel_process_step_idents
4175                    .iter()
4176                    .zip(runtime_plan_parallel_step_code.iter())
4177                    .map(|(step_ident, step_code)| {
4178                        quote! {
4179                            #[inline(always)]
4180                            fn #step_ident(
4181                                step_rt: &mut ParallelProcessStepRuntime<'_>,
4182                            ) -> cu29::curuntime::ProcessStepResult {
4183                                let clock = step_rt.clock;
4184                                let execution_probe = step_rt.execution_probe;
4185                                let monitor = step_rt.monitor;
4186                                #parallel_step_keyframe_local
4187                                let culist = &mut *step_rt.culist;
4188                                let clid = step_rt.clid;
4189                                let ctx = &mut step_rt.ctx;
4190                                let msgs = &mut culist.msgs.0;
4191                                #step_code
4192                            }
4193                        }
4194                    })
4195                    .collect();
4196            let parallel_stage_worker_spawns: Vec<proc_macro2::TokenStream> =
4197                parallel_process_step_idents
4198                    .iter()
4199                    .enumerate()
4200                    .map(|(stage_index, step_ident)| {
4201                        let stage_index_lit = syn::Index::from(stage_index);
4202                        let receiver_ident =
4203                            format_ident!("__cu_parallel_stage_rx_{stage_index}");
4204                        quote! {
4205                            {
4206                                let mut #receiver_ident = stage_receivers
4207                                    .next()
4208                                    .expect("parallel stage receiver missing");
4209                                let mut next_stage_tx = stage_senders.next();
4210                                let done_tx = done_tx.clone();
4211                                let shutdown = std::sync::Arc::clone(&shutdown);
4212                                let clock = clock.clone();
4213                                let instance_id = instance_id;
4214                                let subsystem_code = subsystem_code;
4215                                let execution_probe_ptr = execution_probe_ptr;
4216                                let monitor_ptr = monitor_ptr;
4217                                let task_ptrs = task_ptrs;
4218                                let task_locks = std::sync::Arc::clone(&task_locks);
4219                                let bridge_ptrs = bridge_ptrs;
4220                                let bridge_locks = std::sync::Arc::clone(&bridge_locks);
4221                                #parallel_worker_keyframe_captures
4222                                let rt_pool = std::sync::Arc::clone(&rt_pool);
4223                                scope.spawn(move || {
4224                                    // Apply the "rt" pool's CPU affinity / scheduling policy to
4225                                    // this stage worker (Spread by stage index). On a Strict pool
4226                                    // this fails the worker, which aborts the pipeline.
4227                                    if let Some(rt_pool) = rt_pool.as_ref()
4228                                        && cu29::thread_pool::apply_current_thread_scheduling(
4229                                            rt_pool,
4230                                            #stage_index_lit,
4231                                        )
4232                                        .is_err()
4233                                    {
4234                                        shutdown.store(true, Ordering::Release);
4235                                        return;
4236                                    }
4237                                    loop {
4238                                        let job = match #receiver_ident.recv() {
4239                                            Ok(job) => job,
4240                                            Err(_) => break,
4241                                        };
4242                                        let clid = job.clid;
4243                                        let culist = job.culist;
4244
4245                                        let terminal_result = if shutdown.load(Ordering::Acquire) {
4246                                            #mission_mod::ParallelWorkerResult {
4247                                                clid,
4248                                                culist: Some(culist),
4249                                                outcome: Err(CuError::from(
4250                                                    "Parallel runtime shutting down after an earlier stage failure",
4251                                                )),
4252                                                raw_payload_bytes: 0,
4253                                                handle_bytes: 0,
4254                                            }
4255                                        } else {
4256                                            match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4257                                                let execution_probe = unsafe { execution_probe_ptr.as_ref() };
4258                                                let monitor = unsafe { monitor_ptr.as_ref() };
4259                                                let mut culist = culist;
4260                                                let mut step_rt = #mission_mod::ParallelProcessStepRuntime {
4261                                                    clock: &clock,
4262                                                    execution_probe,
4263                                                    monitor,
4264                                                    task_ptrs: &task_ptrs,
4265                                                    task_locks: task_locks.as_ref(),
4266                                                    bridge_ptrs: &bridge_ptrs,
4267                                                    bridge_locks: bridge_locks.as_ref(),
4268                                                    #parallel_step_keyframe_fields
4269                                                    culist: culist.as_mut(),
4270                                                    clid,
4271                                                    ctx: cu29::context::CuContext::from_runtime_metadata(
4272                                                        clock.clone(),
4273                                                        clid,
4274                                                        instance_id,
4275                                                        subsystem_code,
4276                                                        #mission_mod::TASK_IDS,
4277                                                    ),
4278                                                };
4279                                                let outcome = #step_ident(&mut step_rt);
4280                                                drop(step_rt);
4281                                                (culist, outcome)
4282                                            })) {
4283                                                Ok((culist, Ok(cu29::curuntime::ProcessStepOutcome::Continue))) => {
4284                                                    if shutdown.load(Ordering::Acquire) {
4285                                                        #mission_mod::ParallelWorkerResult {
4286                                                            clid,
4287                                                            culist: Some(culist),
4288                                                            outcome: Err(CuError::from(
4289                                                                "Parallel runtime shutting down after an earlier stage failure",
4290                                                            )),
4291                                                            raw_payload_bytes: 0,
4292                                                            handle_bytes: 0,
4293                                                        }
4294                                                    } else if let Some(next_stage_tx) = next_stage_tx.as_mut() {
4295                                                        let forwarded_job = #mission_mod::ParallelWorkerJob { clid, culist };
4296                                                        match next_stage_tx.send(forwarded_job) {
4297                                                            Ok(()) => continue,
4298                                                            Err(send_error) => {
4299                                                                let failed_job = send_error.0;
4300                                                                shutdown.store(true, Ordering::Release);
4301                                                                #mission_mod::ParallelWorkerResult {
4302                                                                    clid,
4303                                                                    culist: Some(failed_job.culist),
4304                                                                    outcome: Err(CuError::from(format!(
4305                                                                        "Parallel stage {} could not hand CopperList #{} to the next stage",
4306                                                                        #stage_index_lit,
4307                                                                        clid
4308                                                                    ))),
4309                                                                    raw_payload_bytes: 0,
4310                                                                    handle_bytes: 0,
4311                                                                }
4312                                                            }
4313                                                        }
4314                                                    } else {
4315                                                        #mission_mod::ParallelWorkerResult {
4316                                                            clid,
4317                                                            culist: Some(culist),
4318                                                            outcome: Ok(cu29::curuntime::ProcessStepOutcome::Continue),
4319                                                            raw_payload_bytes: 0,
4320                                                            handle_bytes: 0,
4321                                                        }
4322                                                    }
4323                                                }
4324                                                Ok((culist, Ok(cu29::curuntime::ProcessStepOutcome::AbortCopperList))) => {
4325                                                    #mission_mod::ParallelWorkerResult {
4326                                                        clid,
4327                                                        culist: Some(culist),
4328                                                        outcome: Ok(cu29::curuntime::ProcessStepOutcome::AbortCopperList),
4329                                                        raw_payload_bytes: 0,
4330                                                        handle_bytes: 0,
4331                                                    }
4332                                                }
4333                                                Ok((culist, Err(error))) => {
4334                                                    shutdown.store(true, Ordering::Release);
4335                                                    #mission_mod::ParallelWorkerResult {
4336                                                        clid,
4337                                                        culist: Some(culist),
4338                                                        outcome: Err(error),
4339                                                        raw_payload_bytes: 0,
4340                                                        handle_bytes: 0,
4341                                                    }
4342                                                }
4343                                                Err(payload) => {
4344                                                    shutdown.store(true, Ordering::Release);
4345                                                    let panic_message =
4346                                                        cu29::monitoring::panic_payload_to_string(payload.as_ref());
4347                                                    #mission_mod::ParallelWorkerResult {
4348                                                        clid,
4349                                                        culist: None,
4350                                                        outcome: Err(CuError::from(format!(
4351                                                            "Panic while processing CopperList #{} in stage {}: {}",
4352                                                            clid,
4353                                                            #stage_index_lit,
4354                                                            panic_message
4355                                                        ))),
4356                                                        raw_payload_bytes: 0,
4357                                                        handle_bytes: 0,
4358                                                    }
4359                                                }
4360                                            }
4361                                        };
4362
4363                                        if done_tx.send(terminal_result).is_err() {
4364                                            break;
4365                                        }
4366                                    }
4367                                });
4368                            }
4369                        }
4370                    })
4371                    .collect();
4372            (
4373                parallel_process_step_idents,
4374                parallel_process_step_fn_defs,
4375                parallel_stage_worker_spawns,
4376            )
4377        } else {
4378            (Vec::new(), Vec::new(), Vec::new())
4379        };
4380        let parallel_process_stage_count_tokens =
4381            proc_macro2::Literal::usize_unsuffixed(parallel_process_step_idents.len());
4382        let parallel_task_ptrs_type = if runtime_task_types.is_empty() {
4383            quote! { () }
4384        } else {
4385            let elems = runtime_task_types
4386                .iter()
4387                .map(|ty| quote! { ParallelSharedPtr<#ty> });
4388            quote! { (#(#elems),*,) }
4389        };
4390        let parallel_task_locks_type = if runtime_task_types.is_empty() {
4391            quote! { () }
4392        } else {
4393            let elems = (0..runtime_task_types.len()).map(|_| quote! { std::sync::Mutex<()> });
4394            quote! { (#(#elems),*,) }
4395        };
4396        let parallel_task_ptr_values = if runtime_task_types.is_empty() {
4397            quote! { () }
4398        } else {
4399            let elems = (0..runtime_task_types.len()).map(|index| {
4400                let index = syn::Index::from(index);
4401                quote! { ParallelSharedPtr::new(&mut runtime.tasks.#index as *mut _) }
4402            });
4403            quote! { (#(#elems),*,) }
4404        };
4405        let parallel_task_lock_values = if runtime_task_types.is_empty() {
4406            quote! { () }
4407        } else {
4408            let elems = (0..runtime_task_types.len()).map(|_| quote! { std::sync::Mutex::new(()) });
4409            quote! { (#(#elems),*,) }
4410        };
4411        let parallel_bridge_ptrs_type = if bridge_runtime_types.is_empty() {
4412            quote! { () }
4413        } else {
4414            let elems = bridge_runtime_types
4415                .iter()
4416                .map(|ty| quote! { ParallelSharedPtr<#ty> });
4417            quote! { (#(#elems),*,) }
4418        };
4419        let parallel_bridge_locks_type = if bridge_runtime_types.is_empty() {
4420            quote! { () }
4421        } else {
4422            let elems = (0..bridge_runtime_types.len()).map(|_| quote! { std::sync::Mutex<()> });
4423            quote! { (#(#elems),*,) }
4424        };
4425        let parallel_bridge_ptr_values = if bridge_runtime_types.is_empty() {
4426            quote! { () }
4427        } else {
4428            let elems = (0..bridge_runtime_types.len()).map(|index| {
4429                let index = syn::Index::from(index);
4430                quote! { ParallelSharedPtr::new(&mut runtime.bridges.#index as *mut _) }
4431            });
4432            quote! { (#(#elems),*,) }
4433        };
4434        let parallel_bridge_lock_values = if bridge_runtime_types.is_empty() {
4435            quote! { () }
4436        } else {
4437            let elems =
4438                (0..bridge_runtime_types.len()).map(|_| quote! { std::sync::Mutex::new(()) });
4439            quote! { (#(#elems),*,) }
4440        };
4441        let parallel_keyframe_accessor = keyframe_logging_enabled.then(|| {
4442            quote! {
4443                struct ParallelKeyFrameAccessor<'a> {
4444                    ptr: ParallelSharedPtr<cu29::curuntime::KeyFramesManager>,
4445                    lock: &'a std::sync::Mutex<()>,
4446                }
4447
4448                impl<'a> ParallelKeyFrameAccessor<'a> {
4449                    #[inline(always)]
4450                    fn new(
4451                        ptr: ParallelSharedPtr<cu29::curuntime::KeyFramesManager>,
4452                        lock: &'a std::sync::Mutex<()>,
4453                    ) -> Self {
4454                        Self { ptr, lock }
4455                    }
4456
4457                    #[inline(always)]
4458                    fn freeze_task(
4459                        &self,
4460                        culistid: u64,
4461                        task: &impl cu29::cutask::Freezable,
4462                    ) -> CuResult<usize> {
4463                        let _guard = self.lock.lock().expect("parallel keyframe lock poisoned");
4464                        let manager = unsafe { self.ptr.as_mut() };
4465                        manager.freeze_task(culistid, task)
4466                    }
4467
4468                    #[inline(always)]
4469                    fn freeze_any(
4470                        &self,
4471                        culistid: u64,
4472                        item: &impl cu29::cutask::Freezable,
4473                    ) -> CuResult<usize> {
4474                        let _guard = self.lock.lock().expect("parallel keyframe lock poisoned");
4475                        let manager = unsafe { self.ptr.as_mut() };
4476                        manager.freeze_any(culistid, item)
4477                    }
4478                }
4479            }
4480        });
4481        let parallel_process_step_keyframe_fields = keyframe_logging_enabled.then(|| {
4482            quote! {
4483                kf_manager_ptr: ParallelSharedPtr<cu29::curuntime::KeyFramesManager>,
4484                kf_lock: &'a std::sync::Mutex<()>,
4485            }
4486        });
4487        let parallel_rt_support_tokens = if parallel_rt_run_supported {
4488            quote! {
4489                type ParallelTaskPtrs = #parallel_task_ptrs_type;
4490                type ParallelTaskLocks = #parallel_task_locks_type;
4491                type ParallelBridgePtrs = #parallel_bridge_ptrs_type;
4492                type ParallelBridgeLocks = #parallel_bridge_locks_type;
4493
4494                struct ParallelSharedPtr<T>(*mut T);
4495
4496                impl<T> Clone for ParallelSharedPtr<T> {
4497                    #[inline(always)]
4498                    fn clone(&self) -> Self {
4499                        *self
4500                    }
4501                }
4502
4503                impl<T> Copy for ParallelSharedPtr<T> {}
4504
4505                impl<T> ParallelSharedPtr<T> {
4506                    #[inline(always)]
4507                    const fn new(ptr: *mut T) -> Self {
4508                        Self(ptr)
4509                    }
4510
4511                    #[inline(always)]
4512                    const fn from_ref(ptr: *const T) -> Self {
4513                        Self(ptr as *mut T)
4514                    }
4515
4516                    #[inline(always)]
4517                    unsafe fn as_mut<'a>(self) -> &'a mut T {
4518                        unsafe { &mut *self.0 }
4519                    }
4520
4521                    #[inline(always)]
4522                    unsafe fn as_ref<'a>(self) -> &'a T {
4523                        unsafe { &*self.0 }
4524                    }
4525                }
4526
4527                unsafe impl<T: Send> Send for ParallelSharedPtr<T> {}
4528                unsafe impl<T: Send> Sync for ParallelSharedPtr<T> {}
4529
4530                #parallel_keyframe_accessor
4531
4532                struct ParallelProcessStepRuntime<'a> {
4533                    clock: &'a RobotClock,
4534                    execution_probe: &'a cu29::monitoring::RuntimeExecutionProbe,
4535                    monitor: &'a #monitor_type,
4536                    task_ptrs: &'a ParallelTaskPtrs,
4537                    task_locks: &'a ParallelTaskLocks,
4538                    bridge_ptrs: &'a ParallelBridgePtrs,
4539                    bridge_locks: &'a ParallelBridgeLocks,
4540                    #parallel_process_step_keyframe_fields
4541                    culist: &'a mut CuList,
4542                    clid: u64,
4543                    ctx: cu29::context::CuContext,
4544                }
4545
4546                struct ParallelWorkerJob {
4547                    clid: u64,
4548                    culist: Box<CuList>,
4549                }
4550
4551                struct ParallelWorkerResult {
4552                    clid: u64,
4553                    culist: Option<Box<CuList>>,
4554                    outcome: cu29::curuntime::ProcessStepResult,
4555                    raw_payload_bytes: u64,
4556                    handle_bytes: u64,
4557                }
4558
4559                #[inline(always)]
4560                fn assert_parallel_rt_send_bounds()
4561                where
4562                    CuList: Send,
4563                    #process_step_tasks_type: Send,
4564                    CuBridges: Send,
4565                    #monitor_type: Sync,
4566                {
4567                }
4568
4569                #(#parallel_process_step_fn_defs)*
4570            }
4571        } else {
4572            quote! {}
4573        };
4574
4575        let config_load_stmt = build_config_load_stmt(
4576            std,
4577            application_name,
4578            subsystem_id.as_deref(),
4579            &config_features,
4580        );
4581        let constant_override_warning = if std {
4582            let comparisons = constant_fingerprints
4583                .iter()
4584                .map(|(module, id, fingerprint)| {
4585                    quote! { (#module, #id) => fingerprint != #fingerprint, }
4586                });
4587            Some(quote! {
4588                for constant in &config.constants {
4589                    let changed = constant
4590                        .semantic_fingerprint()
4591                        .map_or(true, |fingerprint| match (constant.module_path(), constant.id()) {
4592                            #(#comparisons)*
4593                            _ => true,
4594                        });
4595                    if changed {
4596                        ::cu29::prelude::warning!(
4597                            "Runtime configuration tried to override compile-time constant '{}'; the value baked into this binary will be used.",
4598                            constant.qualified_id()
4599                        );
4600                    }
4601                }
4602            })
4603        } else {
4604            None
4605        };
4606
4607        let copperlist_count_check = quote! {
4608            let configured_copperlist_count = config
4609                .logging
4610                .as_ref()
4611                .and_then(|logging| logging.copperlist_count)
4612                .unwrap_or(#copperlist_count_tokens);
4613            if configured_copperlist_count != #copperlist_count_tokens {
4614                return Err(CuError::from(format!(
4615                    "Configured logging.copperlist_count ({configured_copperlist_count}) does not match the runtime compiled into this binary ({})",
4616                    #copperlist_count_tokens
4617                )));
4618            }
4619        };
4620        let keyframe_logging_check = quote! {
4621            let configured_keyframe_logging = config
4622                .logging
4623                .as_ref()
4624                .is_none_or(|logging| logging.enable_keyframe_logging && logging.enable_task_logging);
4625            // A ground-side simulation may disable local output entirely while
4626            // retaining the generated freeze/thaw plan for received recovery points.
4627            if configured_keyframe_logging != #local_keyframe_logging_enabled
4628                && !(#sim_mode && #logstream_enabled && !configured_keyframe_logging) {
4629                return Err(CuError::from(format!(
4630                    "Configured keyframe logging ({configured_keyframe_logging}) does not match the runtime compiled into this binary ({})",
4631                    #local_keyframe_logging_enabled
4632                )));
4633            }
4634        };
4635        let logstream_topology_check = if logstream_enabled {
4636            let expected_count = logstream_resource_specs.len();
4637            let destination_checks = logstream_resource_specs.iter().map(|spec| {
4638                let index = syn::Index::from(spec.destination_index);
4639                let destination = &copper_config
4640                    .log_streaming
4641                    .as_ref()
4642                    .expect("logstream specs require config")
4643                    .destinations[spec.destination_index];
4644                let expected_id = LitStr::new(&destination.id, Span::call_site());
4645                let expected_type = LitStr::new(&destination.transport.type_, Span::call_site());
4646                let expected_resource =
4647                    LitStr::new(&destination.transport.resource, Span::call_site());
4648                let expected_feedback = match &destination.feedback {
4649                    Some(feedback) => {
4650                        let ty = LitStr::new(&feedback.transport.type_, Span::call_site());
4651                        let resource = LitStr::new(&feedback.transport.resource, Span::call_site());
4652                        quote! { Some((#ty, #resource)) }
4653                    }
4654                    None => quote! { None },
4655                };
4656                quote! {
4657                    let destination = &configured_logstream_destinations[#index];
4658                    if destination.id != #expected_id
4659                        || destination.transport.type_ != #expected_type
4660                        || destination.transport.resource != #expected_resource
4661                        || destination.feedback.as_ref().map(|f| (f.transport.type_.as_str(), f.transport.resource.as_str())) != #expected_feedback
4662                    {
4663                        return Err(CuError::from(format!(
4664                            "Configured log_streaming destination topology at index {} does not match the runtime compiled into this binary",
4665                            #index,
4666                        )));
4667                    }
4668                }
4669            });
4670            quote! {
4671                let configured_logstream_destinations = config
4672                    .log_streaming
4673                    .as_ref()
4674                    .map_or(&[][..], |streaming| streaming.destinations.as_slice());
4675                if configured_logstream_destinations.len() != #expected_count {
4676                    return Err(CuError::from(format!(
4677                        "Configured log_streaming destination count ({}) does not match the runtime compiled into this binary ({})",
4678                        configured_logstream_destinations.len(),
4679                        #expected_count,
4680                    )));
4681                }
4682                #(#destination_checks)*
4683            }
4684        } else {
4685            quote! {}
4686        };
4687
4688        let prepare_config_sig = if std {
4689            quote! {
4690                fn prepare_config(
4691                    instance_id: u32,
4692                    config_override: Option<CuConfig>,
4693                ) -> CuResult<(CuConfig, RuntimeLifecycleConfigSource)>
4694            }
4695        } else {
4696            quote! {
4697                fn prepare_config() -> CuResult<(CuConfig, RuntimeLifecycleConfigSource)>
4698            }
4699        };
4700
4701        let prepare_config_call = if std {
4702            quote! { Self::prepare_config(instance_id, config_override)? }
4703        } else {
4704            quote! { Self::prepare_config()? }
4705        };
4706
4707        let prepare_resources_sig = if std {
4708            quote! {
4709                pub fn prepare_resources_for_instance(
4710                    instance_id: u32,
4711                    config_override: Option<CuConfig>,
4712                ) -> CuResult<AppResources>
4713            }
4714        } else {
4715            quote! {
4716                pub fn prepare_resources() -> CuResult<AppResources>
4717            }
4718        };
4719
4720        let prepare_resources_compat_fn = if std {
4721            Some(quote! {
4722                pub fn prepare_resources(
4723                    config_override: Option<CuConfig>,
4724                ) -> CuResult<AppResources> {
4725                    Self::prepare_resources_for_instance(0, config_override)
4726                }
4727            })
4728        } else {
4729            None
4730        };
4731
4732        let init_resources_compat_fn = if std {
4733            Some(quote! {
4734                pub fn init_resources_for_instance(
4735                    instance_id: u32,
4736                    config_override: Option<CuConfig>,
4737                ) -> CuResult<AppResources> {
4738                    Self::prepare_resources_for_instance(instance_id, config_override)
4739                }
4740
4741                pub fn init_resources(
4742                    config_override: Option<CuConfig>,
4743                ) -> CuResult<AppResources> {
4744                    Self::prepare_resources(config_override)
4745                }
4746            })
4747        } else {
4748            Some(quote! {
4749                pub fn init_resources() -> CuResult<AppResources> {
4750                    Self::prepare_resources()
4751                }
4752            })
4753        };
4754
4755        let build_with_resources_logstream_param = if logstream_enabled {
4756            quote! {
4757                logstream: Option<(
4758                    Box<dyn ::cu29::logstream::CuStreamTx>,
4759                    ::cu29::logstream::LogStreamSenderConfig,
4760                )>,
4761            }
4762        } else {
4763            quote! {}
4764        };
4765        let build_with_resources_sig = if sim_mode {
4766            quote! {
4767                fn build_with_resources<S: SectionStorage + 'static, L: UnifiedLogWrite<S> + 'static>(
4768                    clock: RobotClock,
4769                    unified_logger: Arc<Mutex<L>>,
4770                    app_resources: AppResources,
4771                    instance_id: u32,
4772                    #build_with_resources_logstream_param
4773                    sim_callback: &mut impl FnMut(SimStep) -> SimOverride,
4774                ) -> CuResult<Self>
4775            }
4776        } else {
4777            quote! {
4778                fn build_with_resources<S: SectionStorage + 'static, L: UnifiedLogWrite<S> + 'static>(
4779                    clock: RobotClock,
4780                    unified_logger: Arc<Mutex<L>>,
4781                    app_resources: AppResources,
4782                    instance_id: u32,
4783                    #build_with_resources_logstream_param
4784                ) -> CuResult<Self>
4785            }
4786        };
4787        let parallel_rt_metadata_arg = if std && parallel_rt_enabled {
4788            Some(quote! {
4789                &#mission_mod::PARALLEL_RT_METADATA,
4790            })
4791        } else {
4792            None
4793        };
4794
4795        let kill_handler = if std && signal_handler {
4796            Some(quote! {
4797                ctrlc::set_handler(move || {
4798                    STOP_FLAG.store(true, Ordering::SeqCst);
4799                }).expect("Error setting Ctrl-C handler");
4800            })
4801        } else {
4802            None
4803        };
4804
4805        let run_loop = if std {
4806            quote! {{
4807                let mut rate_limiter = self
4808                    .copper_runtime
4809                    .runtime_config
4810                    .rate_target_hz
4811                    .map(|rate| cu29::curuntime::LoopRateLimiter::from_rate_target_hz(
4812                        rate,
4813                        self.copper_runtime.clock_ref(),
4814                    ))
4815                    .transpose()?;
4816                loop  {
4817                    let result = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(
4818                        || <Self as #app_trait<S, L>>::run_one_iteration(self, #sim_callback_arg)
4819                    )) {
4820                        Ok(result) => result,
4821                        Err(payload) => {
4822                            let panic_message = cu29::monitoring::panic_payload_to_string(payload.as_ref());
4823                            self.copper_runtime.monitor.process_panic(&panic_message);
4824                            let _ = self.log_runtime_lifecycle_event(RuntimeLifecycleEvent::Panic {
4825                                message: panic_message.clone(),
4826                                file: None,
4827                                line: None,
4828                                column: None,
4829                            });
4830                            Err(CuError::from(format!(
4831                                "Panic while running one iteration: {}",
4832                                panic_message
4833                            )))
4834                        }
4835                    };
4836
4837                    if let Some(rate_limiter) = rate_limiter.as_mut() {
4838                        rate_limiter.limit(self.copper_runtime.clock_ref());
4839                    }
4840
4841                    if STOP_FLAG.load(Ordering::SeqCst) || result.is_err() {
4842                        break result;
4843                    }
4844                }
4845            }}
4846        } else {
4847            quote! {{
4848                let mut rate_limiter = self
4849                    .copper_runtime
4850                    .runtime_config
4851                    .rate_target_hz
4852                    .map(|rate| cu29::curuntime::LoopRateLimiter::from_rate_target_hz(
4853                        rate,
4854                        self.copper_runtime.clock_ref(),
4855                    ))
4856                    .transpose()?;
4857                loop  {
4858                    let result = <Self as #app_trait<S, L>>::run_one_iteration(self, #sim_callback_arg);
4859                    if let Some(rate_limiter) = rate_limiter.as_mut() {
4860                        rate_limiter.limit(self.copper_runtime.clock_ref());
4861                    }
4862
4863                    if STOP_FLAG.load(Ordering::SeqCst) || result.is_err() {
4864                        break result;
4865                    }
4866                }
4867            }}
4868        };
4869
4870        let parallel_keyframe_runtime = keyframe_logging_enabled.then(|| {
4871            quote! {
4872                let kf_manager_ptr =
4873                    #mission_mod::ParallelSharedPtr::new(&mut runtime.keyframes_manager as *mut _);
4874                let kf_lock = std::sync::Arc::new(std::sync::Mutex::new(()));
4875            }
4876        });
4877        let parallel_active_keyframe = keyframe_logging_enabled.then(|| {
4878            quote! { let mut active_keyframe_clid: Option<u64> = None; }
4879        });
4880        let parallel_keyframe_ready = if keyframe_logging_enabled {
4881            quote! {{
4882                let _keyframe_lock = kf_lock.lock().expect("parallel keyframe lock poisoned");
4883                let kf_manager = unsafe { kf_manager_ptr.as_mut() };
4884                active_keyframe_clid.is_none() || !kf_manager.captures_keyframe(next_clid)
4885            }}
4886        } else {
4887            quote! { true }
4888        };
4889        let parallel_keyframe_reset = keyframe_logging_enabled.then(|| {
4890            quote! {
4891                {
4892                    let _keyframe_lock =
4893                        kf_lock.lock().expect("parallel keyframe lock poisoned");
4894                    let kf_manager = unsafe { kf_manager_ptr.as_mut() };
4895                    kf_manager.try_reset(clid, clock)?;
4896                    if kf_manager.captures_keyframe(clid) {
4897                        active_keyframe_clid = Some(clid);
4898                    }
4899                }
4900            }
4901        });
4902        let parallel_keyframe_finish = if keyframe_logging_enabled {
4903            quote! {{
4904                let _keyframe_lock =
4905                    kf_lock.lock().expect("parallel keyframe lock poisoned");
4906                let kf_manager = unsafe { kf_manager_ptr.as_mut() };
4907                kf_manager.end_of_processing(worker_result.clid)?;
4908                (
4909                    kf_manager.last_encoded_bytes,
4910                    kf_manager.dropped_keyframes_total(),
4911                )
4912            }}
4913        } else {
4914            quote! { (0u64, 0u64) }
4915        };
4916        let parallel_keyframe_clear = keyframe_logging_enabled.then(|| {
4917            quote! {
4918                if active_keyframe_clid == Some(worker_result.clid) {
4919                    active_keyframe_clid = None;
4920                }
4921            }
4922        });
4923
4924        #[cfg(feature = "macro_debug")]
4925        eprintln!("[build the run methods]");
4926        let run_body: proc_macro2::TokenStream = if parallel_rt_run_supported {
4927            quote! {
4928                static STOP_FLAG: AtomicBool = AtomicBool::new(false);
4929
4930                #kill_handler
4931
4932                <Self as #app_trait<S, L>>::start_all_tasks(self)?;
4933                let result = std::thread::scope(|scope| -> CuResult<()> {
4934                    #mission_mod::assert_parallel_rt_send_bounds();
4935
4936                    let runtime = &mut self.copper_runtime;
4937                    let clock_handle = runtime.clock();
4938                    let clock = &clock_handle;
4939                    let instance_id = runtime.instance_id();
4940                    let subsystem_code = runtime.subsystem_code();
4941                    let execution_probe = runtime.execution_probe.as_ref();
4942                    let monitor = &runtime.monitor;
4943                    let cl_manager = &mut runtime.copperlists_manager;
4944                    let parallel_rt = &runtime.parallel_rt;
4945                    let execution_probe_ptr =
4946                        #mission_mod::ParallelSharedPtr::from_ref(execution_probe as *const _);
4947                    let monitor_ptr =
4948                        #mission_mod::ParallelSharedPtr::from_ref(monitor as *const _);
4949                    let task_ptrs: #mission_mod::ParallelTaskPtrs = #parallel_task_ptr_values;
4950                    let task_locks = std::sync::Arc::new(#parallel_task_lock_values);
4951                    let bridge_ptrs: #mission_mod::ParallelBridgePtrs = #parallel_bridge_ptr_values;
4952                    let bridge_locks = std::sync::Arc::new(#parallel_bridge_lock_values);
4953                    #parallel_keyframe_runtime
4954                    let mut free_copperlists =
4955                        cu29::curuntime::allocate_boxed_copperlists::<CuStampedDataSet, #copperlist_count_tokens>();
4956                    let start_clid = cl_manager.next_cl_id();
4957                    parallel_rt.reset_cursors(start_clid);
4958
4959                    let stage_count = #parallel_process_stage_count_tokens;
4960                    debug_assert_eq!(parallel_rt.metadata().process_stage_count(), stage_count);
4961                    if stage_count == 0 {
4962                        return Err(CuError::from(
4963                            "Parallel runtime requires at least one generated process stage",
4964                        ));
4965                    }
4966
4967                    let queue_capacity = parallel_rt.in_flight_limit().max(1);
4968                    let mut stage_senders = Vec::with_capacity(stage_count);
4969                    let mut stage_receivers = Vec::with_capacity(stage_count);
4970                    for _stage_index in 0..stage_count {
4971                        let (stage_tx, stage_rx) =
4972                            cu29::parallel_queue::stage_queue::<#mission_mod::ParallelWorkerJob>(
4973                                queue_capacity,
4974                            );
4975                        stage_senders.push(stage_tx);
4976                        stage_receivers.push(stage_rx);
4977                    }
4978                    let (done_tx, done_rx) =
4979                        std::sync::mpsc::channel::<#mission_mod::ParallelWorkerResult>();
4980                    let shutdown = std::sync::Arc::new(AtomicBool::new(false));
4981                    let mut stage_senders = stage_senders.into_iter();
4982                    let mut entry_stage_tx = stage_senders
4983                        .next()
4984                        .expect("parallel stage pipeline has no entry queue");
4985                    let mut stage_receivers = stage_receivers.into_iter();
4986                    // Optional "rt" thread pool spec: its CPU affinity / scheduling
4987                    // policy is applied to each stage worker at startup.
4988                    let rt_pool = std::sync::Arc::new(
4989                        runtime
4990                            .runtime_config
4991                            .thread_pools
4992                            .iter()
4993                            .find(|pool| pool.id == cu29::config::RT_POOL)
4994                            .cloned(),
4995                    );
4996                    #(#parallel_stage_worker_spawns)*
4997                    drop(done_tx);
4998
4999                    let mut dispatch_limiter = runtime
5000                        .runtime_config
5001                        .rate_target_hz
5002                        .map(|rate| cu29::curuntime::LoopRateLimiter::from_rate_target_hz(rate, clock))
5003                        .transpose()?;
5004                    let mut in_flight = 0usize;
5005                    let mut stop_launching = false;
5006                    let mut next_launch_clid = start_clid;
5007                    let mut next_commit_clid = start_clid;
5008                    let mut pending_results =
5009                        std::collections::BTreeMap::<u64, #mission_mod::ParallelWorkerResult>::new();
5010                    #parallel_active_keyframe
5011                    let mut fatal_error: Option<CuError> = None;
5012
5013                    loop {
5014                        while let Some(recycled_culist) = cl_manager.try_reclaim_boxed()? {
5015                            free_copperlists.push(recycled_culist);
5016                        }
5017
5018                        if !stop_launching && fatal_error.is_none() {
5019                            let next_clid = next_launch_clid;
5020                            let rate_ready = dispatch_limiter
5021                                .as_ref()
5022                                .map(|limiter| limiter.is_ready(clock))
5023                                .unwrap_or(true);
5024                            let keyframe_ready = #parallel_keyframe_ready;
5025
5026                            if in_flight < parallel_rt.in_flight_limit()
5027                                && rate_ready
5028                                && keyframe_ready
5029                                && !free_copperlists.is_empty()
5030                            {
5031                                // Parallel lifecycle is attached to component-local stage work,
5032                                // so dispatch itself can launch the next CopperList immediately.
5033                                let should_launch = true;
5034
5035                                if should_launch {
5036                                    let mut culist = free_copperlists
5037                                        .pop()
5038                                        .expect("parallel CopperList pool unexpectedly empty");
5039                                    let clid = next_clid;
5040                                    culist.reset_for_runtime_use(clid);
5041                                    #parallel_keyframe_reset
5042                                    culist.change_state(cu29::copperlist::CopperListState::Processing);
5043                                    entry_stage_tx
5044                                        .send(#mission_mod::ParallelWorkerJob {
5045                                            clid,
5046                                            culist,
5047                                        })
5048                                        .map_err(|e| {
5049                                            shutdown.store(true, Ordering::Release);
5050                                            CuError::from("Failed to enqueue CopperList for parallel stage processing")
5051                                                .add_cause(e.to_string().as_str())
5052                                        })?;
5053                                    next_launch_clid += 1;
5054                                    in_flight += 1;
5055                                    if let Some(limiter) = dispatch_limiter.as_mut() {
5056                                        limiter.mark_tick(clock);
5057                                    }
5058                                }
5059
5060                                if STOP_FLAG.load(Ordering::SeqCst) {
5061                                    stop_launching = true;
5062                                }
5063                                continue;
5064                            }
5065                        }
5066
5067                        if in_flight == 0 {
5068                            if stop_launching || fatal_error.is_some() {
5069                                break;
5070                            }
5071
5072                            if free_copperlists.is_empty() {
5073                                free_copperlists.push(cl_manager.wait_reclaim_boxed()?);
5074                                continue;
5075                            }
5076
5077                            if let Some(limiter) = dispatch_limiter.as_ref()
5078                                && !limiter.is_ready(clock)
5079                            {
5080                                limiter.wait_until_ready(clock);
5081                                continue;
5082                            }
5083                        }
5084
5085                        let recv_result = if !stop_launching && fatal_error.is_none() {
5086                            if let Some(limiter) = dispatch_limiter.as_ref() {
5087                                if let Some(remaining) = limiter.remaining(clock)
5088                                    && in_flight > 0
5089                                {
5090                                    done_rx.recv_timeout(std::time::Duration::from(remaining))
5091                                } else {
5092                                    done_rx
5093                                        .recv()
5094                                        .map_err(|_| std::sync::mpsc::RecvTimeoutError::Disconnected)
5095                                }
5096                            } else {
5097                                done_rx
5098                                    .recv()
5099                                    .map_err(|_| std::sync::mpsc::RecvTimeoutError::Disconnected)
5100                            }
5101                        } else {
5102                            done_rx
5103                                .recv()
5104                                .map_err(|_| std::sync::mpsc::RecvTimeoutError::Disconnected)
5105                        };
5106
5107                        let worker_result = match recv_result {
5108                            Ok(worker_result) => worker_result,
5109                            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
5110                                if STOP_FLAG.load(Ordering::SeqCst) {
5111                                    stop_launching = true;
5112                                }
5113                                continue;
5114                            }
5115                            Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
5116                                shutdown.store(true, Ordering::Release);
5117                                return Err(CuError::from(
5118                                    "Parallel stage worker disconnected unexpectedly",
5119                                ));
5120                            }
5121                        };
5122                        in_flight = in_flight.saturating_sub(1);
5123                        pending_results.insert(worker_result.clid, worker_result);
5124
5125                        while let Some(worker_result) = pending_results.remove(&next_commit_clid) {
5126                            if fatal_error.is_none()
5127                                && parallel_rt.current_commit_clid() != worker_result.clid
5128                            {
5129                                shutdown.store(true, Ordering::Release);
5130                                fatal_error = Some(CuError::from(format!(
5131                                    "Parallel commit checkpoint out of sync: expected {}, got {}",
5132                                    parallel_rt.current_commit_clid(),
5133                                    worker_result.clid
5134                                )));
5135                                stop_launching = true;
5136                            }
5137
5138                            let mut worker_result = worker_result;
5139                            if fatal_error.is_none() {
5140                                match worker_result.outcome {
5141                                    Ok(cu29::curuntime::ProcessStepOutcome::AbortCopperList) => {
5142                                        let mut culist = worker_result
5143                                            .culist
5144                                            .take()
5145                                            .expect("parallel abort result missing CopperList ownership");
5146                                        let mut commit_ctx = cu29::context::CuContext::from_runtime_metadata(
5147                                            clock.clone(),
5148                                            worker_result.clid,
5149                                            instance_id,
5150                                            subsystem_code,
5151                                            #mission_mod::TASK_IDS,
5152                                        );
5153                                        commit_ctx.clear_current_component();
5154                                        commit_ctx.clear_current_task();
5155                                        let monitor_result = monitor.process_copperlist(
5156                                            &commit_ctx,
5157                                            #mission_mod::MONITOR_LAYOUT.view(&#mission_mod::collect_metadata(&culist)),
5158                                        );
5159                                        match cl_manager.end_of_processing_boxed(culist)? {
5160                                            cu29::curuntime::OwnedCopperListSubmission::Recycled(culist) => {
5161                                                free_copperlists.push(culist);
5162                                            }
5163                                            cu29::curuntime::OwnedCopperListSubmission::Pending => {}
5164                                        }
5165                                        monitor_result?;
5166                                    }
5167                                    Ok(cu29::curuntime::ProcessStepOutcome::Continue) => {
5168                                        let mut culist = worker_result
5169                                            .culist
5170                                            .take()
5171                                            .expect("parallel worker result missing CopperList ownership");
5172                                        let mut commit_ctx = cu29::context::CuContext::from_runtime_metadata(
5173                                            clock.clone(),
5174                                            worker_result.clid,
5175                                            instance_id,
5176                                            subsystem_code,
5177                                            #mission_mod::TASK_IDS,
5178                                        );
5179                                        commit_ctx.clear_current_component();
5180                                        commit_ctx.clear_current_task();
5181                                        let monitor_result = monitor.process_copperlist(
5182                                            &commit_ctx,
5183                                            #mission_mod::MONITOR_LAYOUT.view(&#mission_mod::collect_metadata(&culist)),
5184                                        );
5185
5186                                        #(#preprocess_logging_calls)*
5187
5188                                        match cl_manager.end_of_processing_boxed(culist)? {
5189                                            cu29::curuntime::OwnedCopperListSubmission::Recycled(culist) => {
5190                                                free_copperlists.push(culist);
5191                                            }
5192                                            cu29::curuntime::OwnedCopperListSubmission::Pending => {}
5193                                        }
5194                                        let (keyframe_bytes, dropped_keyframes_total) = #parallel_keyframe_finish;
5195                                        monitor_result?;
5196                                        let stats = cu29::monitoring::CopperListIoStats {
5197                                            raw_culist_bytes: core::mem::size_of::<CuList>() as u64
5198                                                + cl_manager.last_handle_bytes,
5199                                            handle_bytes: cl_manager.last_handle_bytes,
5200                                            encoded_culist_bytes: cl_manager.last_encoded_bytes,
5201                                            keyframe_bytes,
5202                                            structured_log_bytes_total: ::cu29::prelude::structured_log_bytes_total(),
5203                                            culistid: worker_result.clid,
5204                                            dropped_copperlists_total: cl_manager.dropped_copperlists_total(),
5205                                            dropped_keyframes_total,
5206                                        };
5207                                        monitor.observe_copperlist_io(stats);
5208
5209                                        // Postprocess, when present, now runs inside the owning
5210                                        // component stage instead of on the ordered commit path.
5211                                    }
5212                                    Err(error) => {
5213                                        shutdown.store(true, Ordering::Release);
5214                                        stop_launching = true;
5215                                        fatal_error = Some(error);
5216                                        if let Some(mut culist) = worker_result.culist.take() {
5217                                            culist.change_state(cu29::copperlist::CopperListState::Free);
5218                                            free_copperlists.push(culist);
5219                                        }
5220                                    }
5221                                }
5222                            } else if let Some(mut culist) = worker_result.culist.take() {
5223                                culist.change_state(cu29::copperlist::CopperListState::Free);
5224                                free_copperlists.push(culist);
5225                            }
5226
5227                            #parallel_keyframe_clear
5228                            parallel_rt.release_commit(worker_result.clid + 1);
5229                            next_commit_clid += 1;
5230                        }
5231
5232                        if STOP_FLAG.load(Ordering::SeqCst) {
5233                            stop_launching = true;
5234                        }
5235                    }
5236
5237                    drop(entry_stage_tx);
5238                    free_copperlists.extend(cl_manager.finish_pending_boxed()?);
5239                    if let Some(error) = fatal_error {
5240                        Err(error)
5241                    } else {
5242                        Ok(())
5243                    }
5244                });
5245
5246                if result.is_err() {
5247                    error!("A task errored out: {}", &result);
5248                }
5249                <Self as #app_trait<S, L>>::stop_all_tasks(self, #sim_callback_arg)?;
5250                let _ = self.log_shutdown_completed();
5251                result
5252            }
5253        } else {
5254            quote! {
5255                static STOP_FLAG: AtomicBool = AtomicBool::new(false);
5256
5257                #kill_handler
5258
5259                <Self as #app_trait<S, L>>::start_all_tasks(self, #sim_callback_arg)?;
5260                let result = #run_loop;
5261
5262                if result.is_err() {
5263                    error!("A task errored out: {}", &result);
5264                }
5265                <Self as #app_trait<S, L>>::stop_all_tasks(self, #sim_callback_arg)?;
5266                let _ = self.log_shutdown_completed();
5267                result
5268            }
5269        };
5270        let keyframe_manager_binding = keyframe_logging_enabled
5271            .then(|| quote! { let kf_manager = &mut runtime.keyframes_manager; });
5272        let keyframe_reset =
5273            keyframe_logging_enabled.then(|| quote! { kf_manager.try_reset(clid, clock)?; });
5274        let keyframe_finish =
5275            keyframe_logging_enabled.then(|| quote! { kf_manager.end_of_processing(clid)?; });
5276        let keyframe_bytes = if keyframe_logging_enabled {
5277            quote! { kf_manager.last_encoded_bytes }
5278        } else {
5279            quote! { 0 }
5280        };
5281        let dropped_keyframes_total = if keyframe_logging_enabled {
5282            quote! { kf_manager.dropped_keyframes_total() }
5283        } else {
5284            quote! { 0 }
5285        };
5286        let keyframe_preallocation = keyframe_logging_enabled.then(|| {
5287            quote! {
5288                {
5289                    let runtime = &mut self.copper_runtime;
5290                    let tasks = &runtime.tasks;
5291                    let __cu_bridges = &runtime.bridges;
5292                    let kf_manager = &mut runtime.keyframes_manager;
5293                    kf_manager.begin_capture_preallocation();
5294                    #(#keyframe_preallocation_code)*
5295                    kf_manager.finish_capture_preallocation()?;
5296                }
5297            }
5298        });
5299        let keyframe_finish_pending = keyframe_logging_enabled.then(|| {
5300            quote! { self.copper_runtime.keyframes_manager.finish_pending()?; }
5301        });
5302
5303        let live_completed = (sim_mode && logstream_enabled).then(|| {
5304            quote! {
5305                let _ = sim_callback(SimStep::CopperListCompleted(culist));
5306            }
5307        });
5308        let live_replay_impl = if sim_mode && logstream_enabled {
5309            live_twin::runtime_support(
5310                application_name,
5311                &mission_mod,
5312                &culist_plan,
5313                &culist_exec_entities,
5314            )
5315        } else {
5316            quote! {}
5317        };
5318        let run_methods: proc_macro2::TokenStream = quote! {
5319
5320            #run_one_iteration {
5321                let result: CuResult<()> = (|| {
5322                // Pre-explode the runtime to avoid complexity with partial borrowing in the generated code.
5323                let runtime = &mut self.copper_runtime;
5324                let clock_handle = runtime.clock();
5325                let clock = &clock_handle;
5326                let instance_id = runtime.instance_id();
5327                let subsystem_code = runtime.subsystem_code();
5328                let execution_probe = &runtime.execution_probe;
5329                let monitor = &mut runtime.monitor;
5330                let tasks = &mut runtime.tasks;
5331                let __cu_bridges = &mut runtime.bridges;
5332                let cl_manager = &mut runtime.copperlists_manager;
5333                #keyframe_manager_binding
5334                let iteration_clid = cl_manager.next_cl_id();
5335                let mut ctx = cu29::context::CuContext::from_runtime_metadata(
5336                    clock.clone(),
5337                    iteration_clid,
5338                    instance_id,
5339                    subsystem_code,
5340                    #mission_mod::TASK_IDS,
5341                );
5342                let mut __cu_abort_copperlist = false;
5343
5344                // Preprocess calls can happen at any time, just packed them up front.
5345                #(#preprocess_calls)*
5346
5347                let culist = cl_manager.create()?;
5348                let clid = culist.id;
5349                debug_assert_eq!(clid, iteration_clid);
5350                #keyframe_reset
5351                culist.change_state(cu29::copperlist::CopperListState::Processing);
5352                let mut ctx = cu29::context::CuContext::from_runtime_metadata(
5353                    clock.clone(),
5354                    iteration_clid,
5355                    instance_id,
5356                    subsystem_code,
5357                    #mission_mod::TASK_IDS,
5358                );
5359                {
5360                    let msgs = &mut culist.msgs.0;
5361                    // One hoisted local per anytime node — the only value
5362                    // crossing that node's base/refine steps (is the job
5363                    // still live). Step bodies can be wrapped in closures, so
5364                    // it cannot live inside a step.
5365                    #(#anytime_job_locals)*
5366                    '__cu_process_steps: {
5367                    #(#runtime_plan_code)*
5368                    }
5369                } // drop(msgs);
5370                if __cu_abort_copperlist {
5371                    ctx.clear_current_component();
5372                    ctx.clear_current_task();
5373                    let monitor_result = monitor.process_copperlist(&ctx, #mission_mod::MONITOR_LAYOUT.view(&#mission_mod::collect_metadata(&culist)));
5374                    cl_manager.end_of_processing(clid)?;
5375                    monitor_result?;
5376                    return Ok(());
5377                }
5378                ctx.clear_current_component();
5379                ctx.clear_current_task();
5380                let monitor_result = monitor.process_copperlist(&ctx, #mission_mod::MONITOR_LAYOUT.view(&#mission_mod::collect_metadata(&culist)));
5381
5382                // here drop the payloads if we don't want them to be logged.
5383                #(#preprocess_logging_calls)*
5384
5385                #live_completed
5386                cl_manager.end_of_processing(clid)?;
5387                monitor_result?;
5388
5389                // Postprocess calls keep their component-local freeze points. Finish the
5390                // framed keyframe only after those late bridge frames have been appended.
5391                #(#postprocess_calls)*
5392                #keyframe_finish
5393                let stats = cu29::monitoring::CopperListIoStats {
5394                    raw_culist_bytes: core::mem::size_of::<CuList>() as u64 + cl_manager.last_handle_bytes,
5395                    handle_bytes: cl_manager.last_handle_bytes,
5396                    encoded_culist_bytes: cl_manager.last_encoded_bytes,
5397                    keyframe_bytes: #keyframe_bytes,
5398                    structured_log_bytes_total: ::cu29::prelude::structured_log_bytes_total(),
5399                    culistid: clid,
5400                    dropped_copperlists_total: cl_manager.dropped_copperlists_total(),
5401                    dropped_keyframes_total: #dropped_keyframes_total,
5402                };
5403                monitor.observe_copperlist_io(stats);
5404                Ok(())
5405                })();
5406                if result.is_err() {
5407                    self.copper_runtime.copperlists_manager.discard_current();
5408                }
5409                result
5410            }
5411
5412            fn restore_keyframe(&mut self, keyframe: &KeyFrame) -> CuResult<()> {
5413                let runtime = &mut self.copper_runtime;
5414                let clock_handle = runtime.clock();
5415                let clock = &clock_handle;
5416                let tasks = &mut runtime.tasks;
5417                let __cu_bridges = &mut runtime.bridges;
5418                let mut frames = cu29::curuntime::KeyFramePayloadReader::new(keyframe)?;
5419                #(#keyframe_restore_code)*
5420                frames.finish()?;
5421                Ok(())
5422            }
5423
5424            #start_all_tasks {
5425                let _ = self.log_runtime_lifecycle_event(RuntimeLifecycleEvent::MissionStarted {
5426                    mission: #mission.to_string(),
5427                });
5428                let lifecycle_clid = self.copper_runtime.copperlists_manager.last_cl_id();
5429                let mut ctx = cu29::context::CuContext::from_runtime_metadata(
5430                    self.copper_runtime.clock(),
5431                    lifecycle_clid,
5432                    self.copper_runtime.instance_id(),
5433                    self.copper_runtime.subsystem_code(),
5434                    #mission_mod::TASK_IDS,
5435                );
5436                #(#start_calls)*
5437                #keyframe_preallocation
5438                ctx.clear_current_component();
5439                ctx.clear_current_task();
5440                self.copper_runtime.monitor.start(&ctx)?;
5441                Ok(())
5442            }
5443
5444            #stop_all_tasks {
5445                // Recycle an unpublished iteration before draining completed output.
5446                self.copper_runtime.copperlists_manager.discard_current();
5447                let lifecycle_clid = self.copper_runtime.copperlists_manager.last_cl_id();
5448                let mut ctx = cu29::context::CuContext::from_runtime_metadata(
5449                    self.copper_runtime.clock(),
5450                    lifecycle_clid,
5451                    self.copper_runtime.instance_id(),
5452                    self.copper_runtime.subsystem_code(),
5453                    #mission_mod::TASK_IDS,
5454                );
5455                #(#stop_calls)*
5456                ctx.clear_current_component();
5457                ctx.clear_current_task();
5458                self.copper_runtime.monitor.stop(&ctx)?;
5459                self.copper_runtime.copperlists_manager.finish_pending()?;
5460                #keyframe_finish_pending
5461                // TODO(lifecycle): emit typed stop reasons (completed/error/panic/requested)
5462                // once panic/reporting flow is finalized for std and no-std.
5463                let _ = self.log_runtime_lifecycle_event(RuntimeLifecycleEvent::MissionStopped {
5464                    mission: #mission.to_string(),
5465                    reason: "stop_all_tasks".to_string(),
5466                });
5467                Ok(())
5468            }
5469
5470            #run {
5471                #run_body
5472            }
5473        };
5474
5475        let tasks_type = if sim_mode {
5476            quote!(CuSimTasks)
5477        } else {
5478            quote!(CuTasks)
5479        };
5480
5481        let tasks_instanciator_fn = if sim_mode {
5482            quote!(tasks_instanciator_sim)
5483        } else {
5484            quote!(tasks_instanciator)
5485        };
5486
5487        let app_impl_decl = if sim_mode {
5488            quote!(impl<S: SectionStorage + 'static, L: UnifiedLogWrite<S> + 'static> CuSimApplication<S, L> for #application_name)
5489        } else {
5490            quote!(impl<S: SectionStorage + 'static, L: UnifiedLogWrite<S> + 'static> CuApplication<S, L> for #application_name)
5491        };
5492
5493        let simstep_type_decl = if sim_mode {
5494            quote!(
5495                type Step<'z> = SimStep<'z>;
5496            )
5497        } else {
5498            quote!()
5499        };
5500
5501        let mission_id_method = if sim_mode {
5502            quote! {
5503                fn mission_id() -> Option<&'static str> {
5504                    Some(#mission)
5505                }
5506            }
5507        } else {
5508            quote!()
5509        };
5510
5511        let app_resources_thread_pools_field = if std {
5512            quote! { pub thread_pools: Vec<Option<Arc<ThreadPool>>>, }
5513        } else {
5514            quote!()
5515        };
5516
5517        let app_resources_struct = quote! {
5518            pub struct AppResources {
5519                pub config: CuConfig,
5520                pub config_source: RuntimeLifecycleConfigSource,
5521                pub resources: ResourceManager,
5522                #app_resources_thread_pools_field
5523            }
5524        };
5525
5526        let prepare_config_fn = quote! {
5527            #prepare_config_sig {
5528                let config_filename = #config_file;
5529
5530                #[cfg(target_os = "none")]
5531                ::cu29::prelude::info!("CuApp init: config file {}", config_filename);
5532                #[cfg(target_os = "none")]
5533                ::cu29::prelude::info!("CuApp init: loading config");
5534                #config_load_stmt
5535                let mut config = config;
5536                if #sim_mode { config.log_streaming = None; }
5537                #constant_override_warning
5538                #copperlist_count_check
5539                #keyframe_logging_check
5540                #logstream_topology_check
5541                #[cfg(target_os = "none")]
5542                ::cu29::prelude::info!("CuApp init: config loaded");
5543                if let Some(runtime) = &config.runtime {
5544                    #[cfg(target_os = "none")]
5545                    ::cu29::prelude::info!(
5546                        "CuApp init: rate_target_hz={}",
5547                        runtime.rate_target_hz.unwrap_or(0)
5548                    );
5549                } else {
5550                    #[cfg(target_os = "none")]
5551                    ::cu29::prelude::info!("CuApp init: rate_target_hz=none");
5552                }
5553
5554                Ok((config, config_source))
5555            }
5556        };
5557
5558        let prepare_resources_thread_pools_stmt = if std {
5559            quote! {
5560                let thread_pools = #mission_mod::thread_pools_instanciator(&config)?;
5561            }
5562        } else {
5563            quote!()
5564        };
5565        let prepare_resources_thread_pools_init = if std {
5566            quote! { thread_pools, }
5567        } else {
5568            quote!()
5569        };
5570
5571        let prepare_resources_fn = quote! {
5572            #prepare_resources_sig {
5573                let (config, config_source) = #prepare_config_call;
5574
5575                #[cfg(target_os = "none")]
5576                ::cu29::prelude::info!("CuApp init: building resources");
5577                let resources = #mission_mod::resources_instanciator(&config)?;
5578                #prepare_resources_thread_pools_stmt
5579                #[cfg(target_os = "none")]
5580                ::cu29::prelude::info!("CuApp init: resources ready");
5581
5582                Ok(AppResources {
5583                    config,
5584                    config_source,
5585                    resources,
5586                    #prepare_resources_thread_pools_init
5587                })
5588            }
5589        };
5590
5591        let no_logstream_arg = logstream_enabled.then(|| quote! { None, });
5592        let builder_logstream_arg = logstream_enabled.then(|| quote! { self.logstream, });
5593        let new_with_resources_compat_fn = if sim_mode {
5594            quote! {
5595                pub fn new_with_resources<S: SectionStorage + 'static, L: UnifiedLogWrite<S> + 'static>(
5596                    clock: RobotClock,
5597                    unified_logger: Arc<Mutex<L>>,
5598                    app_resources: AppResources,
5599                    instance_id: u32,
5600                    sim_callback: &mut impl FnMut(SimStep) -> SimOverride,
5601                ) -> CuResult<Self> {
5602                    Self::build_with_resources(
5603                        clock,
5604                        unified_logger,
5605                        app_resources,
5606                        instance_id,
5607                        #no_logstream_arg
5608                        sim_callback,
5609                    )
5610                }
5611            }
5612        } else {
5613            quote! {
5614                pub fn new_with_resources<S: SectionStorage + 'static, L: UnifiedLogWrite<S> + 'static>(
5615                    clock: RobotClock,
5616                    unified_logger: Arc<Mutex<L>>,
5617                    app_resources: AppResources,
5618                    instance_id: u32,
5619                ) -> CuResult<Self> {
5620                    Self::build_with_resources(
5621                        clock,
5622                        unified_logger,
5623                        app_resources,
5624                        instance_id,
5625                        #no_logstream_arg
5626                    )
5627                }
5628            }
5629        };
5630
5631        let build_with_resources_thread_pools_destructure = if std {
5632            quote! { thread_pools, }
5633        } else {
5634            quote!()
5635        };
5636        let build_with_resources_thread_pools_call = if std {
5637            quote! { .with_thread_pools(thread_pools) }
5638        } else {
5639            quote!()
5640        };
5641        let local_keyframe_sink_init = if local_keyframe_logging_enabled {
5642            quote! {
5643                #[cfg(target_os = "none")]
5644                ::cu29::prelude::info!("CuApp new: creating keyframes stream");
5645                let local_keyframe_sink = stream_write::<KeyFrame, S>(
5646                    unified_logger.clone(),
5647                    UnifiedLogType::FrozenTasks,
5648                    1024 * 1024 * 10, // 10 MiB
5649                )?;
5650                #[cfg(target_os = "none")]
5651                ::cu29::prelude::info!("CuApp new: keyframes stream ready");
5652            }
5653        } else {
5654            quote! {
5655                let local_keyframe_sink = cu29::curuntime::NullWriteStream;
5656            }
5657        };
5658        let configured_logstream_session = (!logstream_resource_specs.is_empty()).then(|| {
5659            quote! {
5660                let logstream_session_id = ::cu29::logstream::new_session_id();
5661                let logstream_schema = <#mission_mod::CuStampedDataSet as ::cu29::logstream::capture::CaptureDataSet>::stream_schema();
5662            }
5663        });
5664        let configured_logstream_initializers = logstream_resource_specs.iter().map(|spec| {
5665            let index = syn::Index::from(spec.destination_index);
5666            let bundle_index = spec.stream.bundle_index;
5667            let provider_path = &spec.stream.provider_path;
5668            let resource_name = LitStr::new(&spec.stream.resource_name, Span::call_site());
5669            let transport_type = &spec.stream.transport_type;
5670            let transport_ident = format_ident!("__cu_logstream_transport_{}", spec.destination_index);
5671            let continuous_ident = format_ident!("__cu_logstream_continuous_{}", spec.destination_index);
5672            let recovery_ident = format_ident!("__cu_logstream_recovery_{}", spec.destination_index);
5673            let scheduled = if let Some(feedback) = &spec.feedback {
5674                let bundle = feedback.bundle_index;
5675                let provider = &feedback.provider_path;
5676                let name = LitStr::new(&feedback.resource_name, Span::call_site());
5677                let ty = &feedback.transport_type;
5678                quote! {
5679                    let feedback_rx: #ty = resources.take(
5680                        cu29::resource::ResourceKey::<()>::new(cu29::resource::BundleIndex::new(#bundle),
5681                            cu29::resource::resource_index_by_name::<#provider>(#name)).typed::<#ty>()
5682                    )?.0;
5683                    let (mut #continuous_ident, #recovery_ident, sender_monitor) = ::cu29::logstream::scheduled_feedback_sinks::<#mission_mod::CuStampedDataSet, _>(
5684                        ::cu29::logstream::SeparateFeedback { tx: #transport_ident, feedback_rx }, sender_config,
5685                        if clock.is_mock() { RobotClock::new() } else { clock.clone() },
5686                    )?;
5687                }
5688            } else {
5689                quote! {
5690                    let (mut #continuous_ident, #recovery_ident, sender_monitor) = ::cu29::logstream::scheduled_sinks::<#mission_mod::CuStampedDataSet, _>(
5691                        #transport_ident, sender_config,
5692                        if clock.is_mock() { RobotClock::new() } else { clock.clone() },
5693                    )?;
5694                }
5695            };
5696            quote! {
5697                let destination = &config
5698                    .log_streaming
5699                    .as_ref()
5700                    .expect("compiled logstream configuration is missing")
5701                    .destinations[#index];
5702                let plan = ::cu29::logstream::LogStreamPlan::resolve(destination)
5703                    .map_err(|error| CuError::from(error.to_string()))?;
5704                let sender_config = plan
5705                    .sender_config(
5706                        ::cu29::logstream::StreamIdentity {
5707                            session_id: logstream_session_id,
5708                            sender_id: instance_id,
5709                        },
5710                        logstream_schema.clone(),
5711                    )
5712                    .map_err(|error| CuError::from(error.to_string()))?;
5713                let #transport_ident: #transport_type = resources
5714                    .take(
5715                        cu29::resource::ResourceKey::<()>::new(
5716                            cu29::resource::BundleIndex::new(#bundle_index),
5717                            cu29::resource::resource_index_by_name::<#provider_path>(#resource_name),
5718                        )
5719                        .typed::<#transport_type>(),
5720                    )?
5721                    .0;
5722                #scheduled
5723                let structured_outputs = (#continuous_ident.take_structured_log_sink(), structured_outputs);
5724                stream_monitors.push(sender_monitor.into_runtime_monitor(&destination.id,
5725                    destination.link.bitrate_bps, usize::from(destination.fec.continuous.repair_every_source_symbols)));
5726                let #continuous_ident = if logstream_schema.reconstruction.is_empty() {
5727                    #continuous_ident
5728                } else {
5729                    #continuous_ident.with_encoder(::cu29::logstream::capture::encode_capture_record_into)
5730                };
5731            }
5732        });
5733        let configured_logstream_fanouts = logstream_resource_specs.iter().map(|spec| {
5734            let continuous_ident =
5735                format_ident!("__cu_logstream_continuous_{}", spec.destination_index);
5736            let recovery_ident =
5737                format_ident!("__cu_logstream_recovery_{}", spec.destination_index);
5738            quote! {
5739                let copperlist_sink = ::cu29::fanout::StaticFanoutSink::new(
5740                    copperlist_sink,
5741                    #continuous_ident,
5742                );
5743                let keyframe_sink = ::cu29::fanout::StaticFanoutSink::new(
5744                    keyframe_sink,
5745                    #recovery_ident,
5746                );
5747            }
5748        });
5749        let configured_logstream_count = logstream_resource_specs.len();
5750        let copperlist_output_graph = if logstream_enabled {
5751            quote! {
5752                let local_copperlist_output_required = config
5753                    .logging
5754                    .as_ref()
5755                    .is_none_or(|logging| logging.enable_task_logging);
5756                let local_keyframe_output_required = config
5757                    .logging
5758                    .as_ref()
5759                    .is_none_or(|logging| {
5760                        logging.enable_task_logging && logging.enable_keyframe_logging
5761                    });
5762                let logstream_output_required = #configured_logstream_count != 0 || logstream.is_some();
5763                #[allow(unused_mut)]
5764                let mut resources = resources;
5765                let mut stream_monitors = Vec::with_capacity(#configured_logstream_count + usize::from(logstream.is_some()));
5766                let structured_outputs = ();
5767                #configured_logstream_session
5768                #(#configured_logstream_initializers)*
5769                let injected_logstream_sinks = logstream
5770                    .map(|(transport, mut sender_config)| {
5771                        sender_config.continuous.identity.sender_id = instance_id;
5772                        sender_config.recovery.finite.identity.sender_id = instance_id;
5773                        let schema = <#mission_mod::CuStampedDataSet as ::cu29::logstream::capture::CaptureDataSet>::stream_schema();
5774                        if !schema.reconstruction.is_empty() {
5775                            let manifest = ::cu29::logstream::SessionManifest::decode_record(&sender_config.recovery.manifest_record)
5776                                .map_err(|e| CuError::from(e.to_string()))?;
5777                            if manifest.application_schema != schema {
5778                                return Err(CuError::from("injected sender must use the generated reconstruction schema"));
5779                            }
5780                        }
5781                        let bitrate = sender_config.pacing.bitrate_bps;
5782                        let baseline = sender_config.continuous.repair_every_source_symbols;
5783                        let (mut copperlist, keyframe, sender_monitor) = ::cu29::logstream::scheduled_sinks::<
5784                            #mission_mod::CuStampedDataSet, _
5785                        >(transport, sender_config,
5786                            if clock.is_mock() { RobotClock::new() } else { clock.clone() })?;
5787                        stream_monitors.push(sender_monitor.into_runtime_monitor("injected", bitrate, baseline));
5788                        let structured = copperlist.take_structured_log_sink();
5789                        let copperlist = if schema.reconstruction.is_empty() { copperlist }
5790                            else { copperlist.with_encoder(::cu29::logstream::capture::encode_capture_record_into) };
5791                        Ok::<_, CuError>(((copperlist, keyframe), structured))
5792                    })
5793                    .transpose()?;
5794                let stream_monitors: Option<::std::sync::Arc<[cu29::monitoring::LogStreamMonitor]>> =
5795                    (!stream_monitors.is_empty()).then(|| stream_monitors.into());
5796                let (injected_logstream_sinks, injected_structured) = injected_logstream_sinks.unzip();
5797                let structured_outputs = (injected_structured.flatten(), structured_outputs);
5798                let (injected_logstream_sink, injected_logstream_keyframe_sink) = injected_logstream_sinks.unzip();
5799                let copperlist_sink = ::cu29::fanout::OptionalWriteStream::new(
5800                    local_copperlist_output_required.then_some(local_copperlist_sink),
5801                );
5802                let keyframe_sink = ::cu29::fanout::OptionalWriteStream::new(
5803                    local_keyframe_output_required.then_some(local_keyframe_sink),
5804                );
5805                #(#configured_logstream_fanouts)*
5806                let copperlist_sink = ::cu29::fanout::StaticFanoutSink::new(
5807                    copperlist_sink,
5808                    ::cu29::fanout::OptionalWriteStream::new(injected_logstream_sink),
5809                );
5810                let keyframe_sink = ::cu29::fanout::StaticFanoutSink::new(
5811                    keyframe_sink,
5812                    ::cu29::fanout::OptionalWriteStream::new(injected_logstream_keyframe_sink),
5813                );
5814                let output_requirements = cu29::curuntime::OutputRequirements::new(
5815                    local_copperlist_output_required || logstream_output_required,
5816                    local_keyframe_output_required || logstream_output_required,
5817                );
5818            }
5819        } else {
5820            quote! {
5821                // Downstream record demand is independent from local logging.
5822                let local_copperlist_output_required = config
5823                    .logging
5824                    .as_ref()
5825                    .is_none_or(|logging| logging.enable_task_logging);
5826                let local_keyframe_output_required = config
5827                    .logging
5828                    .as_ref()
5829                    .is_none_or(|logging| {
5830                        logging.enable_task_logging && logging.enable_keyframe_logging
5831                    });
5832                let output_requirements = cu29::curuntime::OutputRequirements::new(
5833                    local_copperlist_output_required,
5834                    local_keyframe_output_required,
5835                );
5836                let copperlist_sink = local_copperlist_sink;
5837                let keyframe_sink = local_keyframe_sink;
5838            }
5839        };
5840
5841        let monitor_instanciator = if logstream_enabled {
5842            quote! { |config: &CuConfig, metadata: cu29::monitoring::CuMonitoringMetadata, runtime: cu29::monitoring::CuMonitoringRuntime| {
5843                let runtime = match &stream_monitors {
5844                    Some(streams) => runtime.with_log_streams(streams.clone()),
5845                    None => runtime,
5846                };
5847                #mission_mod::monitor_instanciator(config, metadata, runtime)
5848            } }
5849        } else {
5850            quote! { #mission_mod::monitor_instanciator }
5851        };
5852
5853        let local_structured_logger_init = quote! {
5854            let local_structured_log_sink = ::cu29::prelude::LogStream::new(
5855                ::cu29::prelude::UnifiedLogType::StructuredLogLine,
5856                unified_logger.clone(), 4096 * 10,
5857            )?;
5858        };
5859        let (early_structured_logger_init, late_structured_logger_init) = if logstream_enabled {
5860            (
5861                quote! {},
5862                quote! {
5863                    #local_structured_logger_init
5864                    let logger_runtime = if logstream_output_required {
5865                        ::cu29::prelude::LoggerRuntime::init(
5866                            clock.clone(),
5867                            ::cu29::logstream::StructuredLogStream::new(local_structured_log_sink, structured_outputs),
5868                            None::<::cu29::prelude::NullLog>,
5869                        )
5870                    } else {
5871                        ::cu29::prelude::LoggerRuntime::init(clock.clone(), local_structured_log_sink, None::<::cu29::prelude::NullLog>)
5872                    };
5873                },
5874            )
5875        } else {
5876            (
5877                quote! {
5878                    #local_structured_logger_init
5879                    let logger_runtime = ::cu29::prelude::LoggerRuntime::init(
5880                        clock.clone(), local_structured_log_sink, None::<::cu29::prelude::NullLog>,
5881                    );
5882                },
5883                quote! {},
5884            )
5885        };
5886
5887        let build_with_resources_fn = quote! {
5888            #build_with_resources_sig {
5889                let AppResources {
5890                    config,
5891                    config_source,
5892                    resources,
5893                    #build_with_resources_thread_pools_destructure
5894                } = app_resources;
5895
5896                #early_structured_logger_init
5897
5898                // For simple cases we can say the section is just a bunch of Copper Lists.
5899                // But we can now have allocations outside of it so we can override it from the config.
5900                let mut default_section_size = size_of::<super::#mission_mod::CuList>() * 64;
5901                // Check if there is a logging configuration with section_size_mib
5902                if let Some(section_size_mib) = config.logging.as_ref().and_then(|l| l.section_size_mib) {
5903                    // Convert MiB to bytes
5904                    default_section_size = section_size_mib as usize * 1024usize * 1024usize;
5905                }
5906                #[cfg(target_os = "none")]
5907                ::cu29::prelude::info!(
5908                    "CuApp new: copperlist section size={}",
5909                    default_section_size
5910                );
5911                #[cfg(target_os = "none")]
5912                ::cu29::prelude::info!("CuApp new: creating copperlist stream");
5913                let local_copperlist_sink = stream_write::<#mission_mod::CuList, S>(
5914                    unified_logger.clone(),
5915                    UnifiedLogType::CopperList,
5916                    default_section_size,
5917                    // the 2 sizes are not directly related as we encode the CuList but we can
5918                    // assume the encoded size is close or lower than the non encoded one
5919                    // This is to be sure we have the size of at least a Culist and some.
5920                )?;
5921                #[cfg(target_os = "none")]
5922                ::cu29::prelude::info!("CuApp new: copperlist stream ready");
5923
5924                #local_keyframe_sink_init
5925
5926                #copperlist_output_graph
5927                #late_structured_logger_init
5928
5929                #[cfg(target_os = "none")]
5930                ::cu29::prelude::info!("CuApp new: creating runtime lifecycle stream");
5931                let mut local_lifecycle_sink = stream_write::<RuntimeLifecycleRecord, S>(
5932                    unified_logger.clone(),
5933                    UnifiedLogType::RuntimeLifecycle,
5934                    1024 * 64, // 64 KiB
5935                )?;
5936                #planner_resolved_stamp
5937                let effective_config_ron = config
5938                    .serialize_ron()
5939                    .unwrap_or_else(|_| "<failed to serialize config>".to_string());
5940                ::cu29::logcodec::set_effective_config_ron::<super::#mission_mod::CuStampedDataSet>(&effective_config_ron);
5941                let stack_info = RuntimeLifecycleStackInfo {
5942                    app_name: env!("CARGO_PKG_NAME").to_string(),
5943                    app_version: env!("CARGO_PKG_VERSION").to_string(),
5944                    git_commit: #git_commit_tokens,
5945                    git_dirty: #git_dirty_tokens,
5946                    subsystem_id: #application_name::subsystem().id().map(str::to_string),
5947                    subsystem_code: #application_name::subsystem().code(),
5948                    instance_id,
5949                };
5950                local_lifecycle_sink.log(&RuntimeLifecycleRecord {
5951                    timestamp: clock.now(),
5952                    event: RuntimeLifecycleEvent::Instantiated {
5953                        config_source,
5954                        effective_config_ron,
5955                        stack: stack_info,
5956                    },
5957                })?;
5958                #[cfg(target_os = "none")]
5959                ::cu29::prelude::info!("CuApp new: runtime lifecycle stream ready");
5960
5961                #[cfg(target_os = "none")]
5962                ::cu29::prelude::info!("CuApp new: building runtime");
5963                let copper_runtime = CuRuntimeBuilder::<#mission_mod::#tasks_type, #mission_mod::CuBridges, #mission_mod::CuStampedDataSet, #monitor_type, #copperlist_count_tokens, _, _, _, _, _>::new(
5964                    clock,
5965                    &config,
5966                    #mission,
5967                    CuRuntimeParts::new(
5968                        #mission_mod::#tasks_instanciator_fn,
5969                        #mission_mod::MONITORED_COMPONENTS,
5970                        #mission_mod::CULIST_COMPONENT_MAPPING,
5971                        #parallel_rt_metadata_arg
5972                        #monitor_instanciator,
5973                        #mission_mod::bridges_instanciator,
5974                    ),
5975                    copperlist_sink,
5976                    keyframe_sink,
5977                    output_requirements,
5978                )
5979                .with_subsystem(#application_name::subsystem())
5980                .with_instance_id(instance_id)
5981                .with_resources(resources)
5982                #build_with_resources_thread_pools_call
5983                .build()?;
5984                #[cfg(target_os = "none")]
5985                ::cu29::prelude::info!("CuApp new: runtime built");
5986
5987                let application = Ok(#application_name {
5988                    copper_runtime,
5989                    runtime_lifecycle_sink: Some(Box::new(local_lifecycle_sink)),
5990                    logger_runtime,
5991                });
5992
5993                #sim_callback_on_new
5994
5995                application
5996            }
5997        };
5998
5999        let app_inherent_impl = quote! {
6000            impl #application_name {
6001                const SUBSYSTEM: cu29::prelude::app::Subsystem =
6002                    cu29::prelude::app::Subsystem::new(#subsystem_id_tokens, #subsystem_code_literal);
6003
6004                #[inline]
6005                pub fn subsystem() -> cu29::prelude::app::Subsystem {
6006                    Self::SUBSYSTEM
6007                }
6008
6009                pub fn original_config() -> String {
6010                    #copper_config_content.to_string()
6011                }
6012
6013                pub fn register_reflect_types(registry: &mut cu29::reflect::TypeRegistry) {
6014                    #(#task_debug_state_registration_calls)*
6015                    #(#reflect_type_registration_calls)*
6016                }
6017
6018                /// Returns a clone of the runtime clock handle.
6019                #[inline]
6020                pub fn clock(&self) -> cu29::clock::RobotClock {
6021                    self.copper_runtime.clock()
6022                }
6023
6024                /// Log one runtime lifecycle event with the current runtime timestamp.
6025                pub fn log_runtime_lifecycle_event(
6026                    &mut self,
6027                    event: RuntimeLifecycleEvent,
6028                ) -> CuResult<()> {
6029                    let timestamp = self.copper_runtime.clock_ref().now();
6030                    let Some(sink) = self.runtime_lifecycle_sink.as_mut() else {
6031                        return Err(CuError::from("Runtime lifecycle stream is not initialized"));
6032                    };
6033                    sink.log(&RuntimeLifecycleRecord { timestamp, event })
6034                }
6035
6036                /// Convenience helper for manual execution loops to mark graceful shutdown.
6037                // TODO(lifecycle): add helper(s) for panic/error stop reporting once we wire
6038                // RuntimeLifecycleEvent::Panic across std/no-std execution models.
6039                pub fn log_shutdown_completed(&mut self) -> CuResult<()> {
6040                    self.log_runtime_lifecycle_event(RuntimeLifecycleEvent::ShutdownCompleted)
6041                }
6042
6043                #prepare_config_fn
6044                #prepare_resources_compat_fn
6045                #prepare_resources_fn
6046                #init_resources_compat_fn
6047                #new_with_resources_compat_fn
6048                #build_with_resources_fn
6049
6050                /// Mutable access to the underlying runtime (used by tools such as deterministic re-sim).
6051                #[inline]
6052                pub fn copper_runtime_mut(&mut self) -> &mut CuRuntime<#mission_mod::#tasks_type, #mission_mod::CuBridges, #mission_mod::CuStampedDataSet, #monitor_type, #copperlist_count_tokens> {
6053                    &mut self.copper_runtime
6054                }
6055            }
6056        };
6057
6058        let app_metadata_impl = quote! {
6059            impl cu29::prelude::app::CuSubsystemMetadata for #application_name {
6060                fn subsystem() -> cu29::prelude::app::Subsystem {
6061                    #application_name::subsystem()
6062                }
6063            }
6064        };
6065
6066        let app_reflect_impl = quote! {
6067            impl cu29::reflect::ReflectTaskIntrospection for #application_name {
6068                fn reflect_task(&self, task_id: &str) -> Option<&dyn cu29::reflect::Reflect> {
6069                    match task_id {
6070                        #(#task_reflect_read_arms)*
6071                        _ => None,
6072                    }
6073                }
6074
6075                fn reflect_task_mut(
6076                    &mut self,
6077                    task_id: &str,
6078                ) -> Option<&mut dyn cu29::reflect::Reflect> {
6079                    match task_id {
6080                        #(#task_reflect_write_arms)*
6081                        _ => None,
6082                    }
6083                }
6084
6085                fn register_reflect_types(registry: &mut cu29::reflect::TypeRegistry) {
6086                    #application_name::register_reflect_types(registry);
6087                }
6088
6089                fn debug_state_type_path(task_id: &str) -> Option<&'static str> {
6090                    match task_id {
6091                        #(#task_debug_state_type_path_arms)*
6092                        _ => None,
6093                    }
6094                }
6095
6096                fn with_debug_state<R>(
6097                    &self,
6098                    task_id: &str,
6099                    f: impl FnOnce(&dyn cu29::reflect::Reflect) -> R,
6100                ) -> Option<R> {
6101                    match task_id {
6102                        #(#task_debug_state_read_arms)*
6103                        _ => None,
6104                    }
6105                }
6106            }
6107        };
6108
6109        let app_runtime_copperlist_impl = quote! {
6110            impl cu29::app::CurrentRuntimeCopperList<#mission_mod::CuStampedDataSet>
6111                for #application_name
6112            {
6113                fn current_runtime_copperlist_bytes(&self) -> Option<&[u8]> {
6114                    self.copper_runtime.copperlists_manager.last_completed_encoded()
6115                }
6116
6117                fn set_current_runtime_copperlist_bytes(
6118                    &mut self,
6119                    snapshot: Option<Vec<u8>>,
6120                ) {
6121                    self.copper_runtime
6122                        .copperlists_manager
6123                        .set_last_completed_encoded(snapshot);
6124                }
6125            }
6126        };
6127
6128        #[cfg(feature = "std")]
6129        #[cfg(feature = "macro_debug")]
6130        eprintln!("[build result]");
6131        let application_impl = quote! {
6132            #app_impl_decl {
6133                #simstep_type_decl
6134
6135                fn get_original_config() -> String {
6136                    Self::original_config()
6137                }
6138
6139                #mission_id_method
6140
6141                #run_methods
6142            }
6143        };
6144
6145        let recorded_replay_app_impl = if sim_mode {
6146            Some(quote! {
6147                impl<S: SectionStorage + 'static, L: UnifiedLogWrite<S> + 'static>
6148                    CuRecordedReplayApplication<S, L> for #application_name
6149                {
6150                    type RecordedDataSet = #mission_mod::CuStampedDataSet;
6151
6152                    #[allow(deprecated)] // replays via the deprecated raw iteration on purpose
6153                    fn replay_recorded_copperlist(
6154                        &mut self,
6155                        clock_mock: &RobotClockMock,
6156                        copperlist: &CopperList<Self::RecordedDataSet>,
6157                        keyframe: Option<&KeyFrame>,
6158                    ) -> CuResult<()> {
6159                        cu29::continuity::validate_replay_continuity(
6160                            self.copper_runtime_mut().copperlists_manager.next_cl_id(),
6161                            copperlist.id,
6162                            keyframe.map(|frame| frame.culistid),
6163                        )?;
6164                        let timestamp = if let Some(keyframe) = keyframe {
6165                            if !self.copper_runtime_mut().captures_keyframe(copperlist.id) {
6166                                return Err(CuError::from(format!(
6167                                    "CopperList {} is not configured to capture a keyframe in this runtime",
6168                                    copperlist.id
6169                                )));
6170                            }
6171                            keyframe.timestamp
6172                        } else {
6173                            cu29::simulation::recorded_copperlist_timestamp(copperlist)
6174                                .ok_or_else(|| CuError::from(format!(
6175                                    "Recorded copperlist {} has no process_time.start timestamps",
6176                                    copperlist.id
6177                                )))?
6178                        };
6179                        // Recorded replay runs offline. Drain the previous output
6180                        // and align allocation only after validating this boundary.
6181                        self.copper_runtime_mut().copperlists_manager
6182                            .prepare_recorded_replay(copperlist.id)?;
6183                        self.copper_runtime_mut().keyframes_manager.finish_pending()?;
6184                        if let Some(keyframe) = keyframe {
6185                            self.copper_runtime_mut().set_forced_keyframe_timestamp(timestamp);
6186                            self.copper_runtime_mut().lock_keyframe(keyframe);
6187                        }
6188                        clock_mock.set_value(timestamp.as_nanos());
6189
6190                        let mut sim_callback = |step: SimStep<'_>| -> SimOverride {
6191                            #mission_mod::recorded_replay_step(step, copperlist)
6192                        };
6193                        <Self as CuSimApplication<S, L>>::run_one_iteration(self, &mut sim_callback)
6194                    }
6195                }
6196            })
6197        } else {
6198            None
6199        };
6200
6201        let distributed_replay_app_impl = if sim_mode {
6202            Some(quote! {
6203                impl<S: SectionStorage + 'static, L: UnifiedLogWrite<S> + 'static>
6204                    cu29::prelude::app::CuDistributedReplayApplication<S, L> for #application_name
6205                {
6206                    fn build_distributed_replay(
6207                        clock: cu29::clock::RobotClock,
6208                        unified_logger: std::sync::Arc<std::sync::Mutex<L>>,
6209                        instance_id: u32,
6210                        config_override: Option<cu29::config::CuConfig>,
6211                    ) -> CuResult<Self> {
6212                        let mut noop =
6213                            |_step: SimStep<'_>| cu29::simulation::SimOverride::ExecuteByRuntime;
6214                        let builder = Self::builder()
6215                            .with_logger::<S, L>(unified_logger)
6216                            .with_clock(clock)
6217                            .with_instance_id(instance_id);
6218                        let builder = if let Some(config_override) = config_override {
6219                            builder.with_config(config_override)
6220                        } else {
6221                            builder
6222                        };
6223                        builder.with_sim_callback(&mut noop).build_impl()
6224                    }
6225                }
6226            })
6227        } else {
6228            None
6229        };
6230
6231        let (builder_build_thread_pools_stmt, builder_build_thread_pools_init) = if std {
6232            (
6233                quote! {
6234                    let thread_pools = #mission_mod::thread_pools_instanciator(&config)?;
6235                },
6236                quote! { thread_pools, },
6237            )
6238        } else {
6239            (quote!(), quote!())
6240        };
6241
6242        let builder_prepare_config_call = if std {
6243            quote! { #application_name::prepare_config(self.instance_id, self.config_override)? }
6244        } else {
6245            quote! {{
6246                let _ = self.config_override;
6247                #application_name::prepare_config()?
6248            }}
6249        };
6250
6251        let builder_with_config_method = if std {
6252            Some(quote! {
6253                #[allow(dead_code)]
6254                pub fn with_config(mut self, config_override: CuConfig) -> Self {
6255                    self.config_override = Some(config_override);
6256                    self
6257                }
6258            })
6259        } else {
6260            None
6261        };
6262
6263        let builder_default_clock = if std {
6264            quote! { Some(RobotClock::default()) }
6265        } else {
6266            quote! { None }
6267        };
6268
6269        let builder_logstream_field = logstream_enabled.then(|| {
6270            quote! {
6271                logstream: Option<(
6272                    Box<dyn ::cu29::logstream::CuStreamTx>,
6273                    ::cu29::logstream::LogStreamSenderConfig,
6274                )>,
6275            }
6276        });
6277        let builder_logstream_init = logstream_enabled.then(|| quote! { logstream: None, });
6278        let builder_logstream_copy =
6279            logstream_enabled.then(|| quote! { logstream: self.logstream, });
6280        let builder_with_logstream_method = logstream_enabled.then(|| {
6281            quote! {
6282                /// Adds a nonblocking CopperList plus keyframe/recovery point packet resource.
6283                ///
6284                /// One sender worker owns the endpoint and schedules both semantic lanes
6285                /// against the same budget. Physical pacing uses a running RobotClock,
6286                /// even when the application's clock is mocked.
6287                pub fn with_logstream<T>(
6288                    mut self,
6289                    transport: T,
6290                    config: ::cu29::logstream::LogStreamSenderConfig,
6291                ) -> Self
6292                where
6293                    T: ::cu29::logstream::CuStreamTx + 'static,
6294                {
6295                    self.logstream = Some((Box::new(transport), config));
6296                    self
6297                }
6298            }
6299        });
6300
6301        let (
6302            builder_struct,
6303            builder_impl,
6304            builder_ctor,
6305            builder_log_path_generics,
6306            builder_sim_callback_method,
6307            builder_build_sim_callback_arg,
6308        ) = if sim_mode {
6309            (
6310                quote! {
6311                    #[allow(dead_code)]
6312                    pub struct #builder_name<'a, F, S, L, R>
6313                    where
6314                        S: SectionStorage + 'static,
6315                        L: UnifiedLogWrite<S> + 'static,
6316                        R: FnOnce(&CuConfig) -> CuResult<ResourceManager>,
6317                        F: FnMut(SimStep) -> SimOverride,
6318                    {
6319                        clock: Option<RobotClock>,
6320                        unified_logger: Arc<Mutex<L>>,
6321                        instance_id: u32,
6322                        config_override: Option<CuConfig>,
6323                        resources_factory: R,
6324                        sim_callback: Option<&'a mut F>,
6325                        #builder_logstream_field
6326                        _storage: core::marker::PhantomData<S>,
6327                    }
6328                },
6329                quote! {
6330                    impl<'a, F, S, L, R> #builder_name<'a, F, S, L, R>
6331                    where
6332                        S: SectionStorage + 'static,
6333                        L: UnifiedLogWrite<S> + 'static,
6334                        R: FnOnce(&CuConfig) -> CuResult<ResourceManager>,
6335                        F: FnMut(SimStep) -> SimOverride,
6336                },
6337                quote! {
6338                    #[allow(dead_code)]
6339                    pub fn builder<'a, F>() -> #builder_name<'a, F, cu29::prelude::NoopSectionStorage, cu29::prelude::NoopLogger, fn(&CuConfig) -> CuResult<ResourceManager>>
6340                    where
6341                        F: FnMut(SimStep) -> SimOverride,
6342                    {
6343                        #builder_name {
6344                            clock: #builder_default_clock,
6345                            unified_logger: Arc::new(Mutex::new(cu29::prelude::NoopLogger::new())),
6346                            instance_id: 0,
6347                            config_override: None,
6348                            resources_factory: #mission_mod::resources_instanciator as fn(&CuConfig) -> CuResult<ResourceManager>,
6349                            sim_callback: None,
6350                            #builder_logstream_init
6351                            _storage: core::marker::PhantomData,
6352                        }
6353                    }
6354                },
6355                quote! {'a, F, MmapSectionStorage, UnifiedLoggerWrite, R},
6356                Some(quote! {
6357                    #[allow(dead_code)]
6358                    pub fn with_sim_callback(mut self, sim_callback: &'a mut F) -> Self {
6359                        self.sim_callback = Some(sim_callback);
6360                        self
6361                    }
6362                }),
6363                Some(quote! {
6364                    self.sim_callback
6365                        .ok_or(CuError::from("Sim callback missing from builder"))?,
6366                }),
6367            )
6368        } else {
6369            (
6370                quote! {
6371                    #[allow(dead_code)]
6372                    pub struct #builder_name<S, L, R>
6373                    where
6374                        S: SectionStorage + 'static,
6375                        L: UnifiedLogWrite<S> + 'static,
6376                        R: FnOnce(&CuConfig) -> CuResult<ResourceManager>,
6377                    {
6378                        clock: Option<RobotClock>,
6379                        unified_logger: Arc<Mutex<L>>,
6380                        instance_id: u32,
6381                        config_override: Option<CuConfig>,
6382                        resources_factory: R,
6383                        #builder_logstream_field
6384                        _storage: core::marker::PhantomData<S>,
6385                    }
6386                },
6387                quote! {
6388                    impl<S, L, R> #builder_name<S, L, R>
6389                    where
6390                        S: SectionStorage + 'static,
6391                        L: UnifiedLogWrite<S> + 'static,
6392                        R: FnOnce(&CuConfig) -> CuResult<ResourceManager>,
6393                },
6394                quote! {
6395                    #[allow(dead_code)]
6396                    pub fn builder() -> #builder_name<cu29::prelude::NoopSectionStorage, cu29::prelude::NoopLogger, fn(&CuConfig) -> CuResult<ResourceManager>> {
6397                        #builder_name {
6398                            clock: #builder_default_clock,
6399                            unified_logger: Arc::new(Mutex::new(cu29::prelude::NoopLogger::new())),
6400                            instance_id: 0,
6401                            config_override: None,
6402                            resources_factory: #mission_mod::resources_instanciator as fn(&CuConfig) -> CuResult<ResourceManager>,
6403                            #builder_logstream_init
6404                            _storage: core::marker::PhantomData,
6405                        }
6406                    }
6407                },
6408                quote! {MmapSectionStorage, UnifiedLoggerWrite, R},
6409                None,
6410                None,
6411            )
6412        };
6413
6414        let builder_with_logger_generics = if sim_mode {
6415            quote! {'a, F, S2, L2, R}
6416        } else {
6417            quote! {S2, L2, R}
6418        };
6419
6420        let builder_with_resources_generics = if sim_mode {
6421            quote! {'a, F, S, L, R2}
6422        } else {
6423            quote! {S, L, R2}
6424        };
6425
6426        let builder_sim_callback_field_copy = if sim_mode {
6427            Some(quote! {
6428                sim_callback: self.sim_callback,
6429            })
6430        } else {
6431            None
6432        };
6433
6434        let builder_with_log_path_method = if std {
6435            Some(quote! {
6436                #[allow(dead_code)]
6437                pub fn with_log_path(
6438                    self,
6439                    path: impl AsRef<std::path::Path>,
6440                    slab_size: Option<usize>,
6441                ) -> CuResult<#builder_name<#builder_log_path_generics>> {
6442                    let preallocated_size = slab_size.unwrap_or(1024 * 1024 * 10);
6443                    let logger = cu29::prelude::UnifiedLoggerBuilder::new()
6444                        .write(true)
6445                        .create(true)
6446                        .file_base_name(path.as_ref())
6447                        .preallocated_size(preallocated_size)
6448                        .build()
6449                        .map_err(|e| CuError::new_with_cause("Failed to create unified logger", e))?;
6450                    let logger = match logger {
6451                        cu29::prelude::UnifiedLogger::Write(logger) => logger,
6452                        cu29::prelude::UnifiedLogger::Read(_) => {
6453                            return Err(CuError::from(
6454                                "UnifiedLoggerBuilder did not create a write-capable logger",
6455                            ));
6456                        }
6457                    };
6458                    Ok(self.with_logger::<MmapSectionStorage, UnifiedLoggerWrite>(Arc::new(Mutex::new(
6459                        logger,
6460                    ))))
6461                }
6462            })
6463        } else {
6464            None
6465        };
6466
6467        let builder_with_unified_logger_method = if std {
6468            Some(quote! {
6469                #[allow(dead_code)]
6470                pub fn with_unified_logger(
6471                    self,
6472                    unified_logger: Arc<Mutex<UnifiedLoggerWrite>>,
6473                ) -> #builder_name<#builder_log_path_generics> {
6474                    self.with_logger::<MmapSectionStorage, UnifiedLoggerWrite>(unified_logger)
6475                }
6476            })
6477        } else {
6478            None
6479        };
6480
6481        // backward compat on std non-parameterized impl.
6482        let std_application_impl = if sim_mode {
6483            // sim mode
6484            Some(quote! {
6485                        impl #application_name {
6486                            #[deprecated(
6487                                since = "1.2.0",
6488                                note = "use the typed lifecycle handle returned by `build()` instead"
6489                            )]
6490                            #[allow(deprecated)] // forwards to the deprecated raw trait method
6491                            pub fn start_all_tasks(&mut self, sim_callback: &mut impl FnMut(SimStep) -> SimOverride) -> CuResult<()> {
6492                                <Self as #app_trait<MmapSectionStorage, UnifiedLoggerWrite>>::start_all_tasks(self, sim_callback)
6493                            }
6494                            #[deprecated(
6495                                since = "1.2.0",
6496                                note = "use the typed lifecycle handle returned by `build()` instead"
6497                            )]
6498                            #[allow(deprecated)] // forwards to the deprecated raw trait method
6499                            pub fn run_one_iteration(&mut self, sim_callback: &mut impl FnMut(SimStep) -> SimOverride) -> CuResult<()> {
6500                                <Self as #app_trait<MmapSectionStorage, UnifiedLoggerWrite>>::run_one_iteration(self, sim_callback)
6501                            }
6502                            #[deprecated(
6503                                since = "1.2.0",
6504                                note = "use the typed lifecycle handle returned by `build()` instead"
6505                            )]
6506                            #[allow(deprecated)] // forwards to the deprecated raw trait method
6507                            pub fn run(&mut self, sim_callback: &mut impl FnMut(SimStep) -> SimOverride) -> CuResult<()> {
6508                                <Self as #app_trait<MmapSectionStorage, UnifiedLoggerWrite>>::run(self, sim_callback)
6509                            }
6510                            #[deprecated(
6511                                since = "1.2.0",
6512                                note = "use the typed lifecycle handle returned by `build()` instead"
6513                            )]
6514                            #[allow(deprecated)] // forwards to the deprecated raw trait method
6515                            pub fn stop_all_tasks(&mut self, sim_callback: &mut impl FnMut(SimStep) -> SimOverride) -> CuResult<()> {
6516                                <Self as #app_trait<MmapSectionStorage, UnifiedLoggerWrite>>::stop_all_tasks(self, sim_callback)
6517                            }
6518                            pub fn replay_recorded_copperlist(
6519                                &mut self,
6520                                clock_mock: &RobotClockMock,
6521                                copperlist: &CopperList<CuStampedDataSet>,
6522                                keyframe: Option<&KeyFrame>,
6523                            ) -> CuResult<()> {
6524                                <Self as CuRecordedReplayApplication<MmapSectionStorage, UnifiedLoggerWrite>>::replay_recorded_copperlist(
6525                                    self,
6526                                    clock_mock,
6527                                    copperlist,
6528                                    keyframe,
6529                                )
6530                            }
6531                        }
6532            })
6533        } else if std {
6534            // std and normal mode, we use the memory mapped starage for those
6535            Some(quote! {
6536                        impl #application_name {
6537                            #[deprecated(
6538                                since = "1.2.0",
6539                                note = "use the typed lifecycle handle returned by `build()` instead"
6540                            )]
6541                            #[allow(deprecated)] // forwards to the deprecated raw trait method
6542                            pub fn start_all_tasks(&mut self) -> CuResult<()> {
6543                                <Self as #app_trait<MmapSectionStorage, UnifiedLoggerWrite>>::start_all_tasks(self)
6544                            }
6545                            #[deprecated(
6546                                since = "1.2.0",
6547                                note = "use the typed lifecycle handle returned by `build()` instead"
6548                            )]
6549                            #[allow(deprecated)] // forwards to the deprecated raw trait method
6550                            pub fn run_one_iteration(&mut self) -> CuResult<()> {
6551                                <Self as #app_trait<MmapSectionStorage, UnifiedLoggerWrite>>::run_one_iteration(self)
6552                            }
6553                            #[deprecated(
6554                                since = "1.2.0",
6555                                note = "use the typed lifecycle handle returned by `build()` instead"
6556                            )]
6557                            #[allow(deprecated)] // forwards to the deprecated raw trait method
6558                            pub fn run(&mut self) -> CuResult<()> {
6559                                <Self as #app_trait<MmapSectionStorage, UnifiedLoggerWrite>>::run(self)
6560                            }
6561                            #[deprecated(
6562                                since = "1.2.0",
6563                                note = "use the typed lifecycle handle returned by `build()` instead"
6564                            )]
6565                            #[allow(deprecated)] // forwards to the deprecated raw trait method
6566                            pub fn stop_all_tasks(&mut self) -> CuResult<()> {
6567                                <Self as #app_trait<MmapSectionStorage, UnifiedLoggerWrite>>::stop_all_tasks(self)
6568                            }
6569                        }
6570            })
6571        } else {
6572            None // if no-std, let the user figure our the correct logger type they need to provide anyway.
6573        };
6574
6575        let (builder_build_app_return, builder_build_app_wrap) = if sim_mode {
6576            (
6577                quote! { cu29::prelude::app::CuSimAppLifecycle<S, L, #application_name> },
6578                quote! { cu29::prelude::app::CuSimAppLifecycle },
6579            )
6580        } else {
6581            (
6582                quote! { cu29::prelude::app::CuAppLifecycle<S, L, #application_name> },
6583                quote! { cu29::prelude::app::CuAppLifecycle },
6584            )
6585        };
6586
6587        let application_builder = Some(quote! {
6588            #builder_struct
6589
6590            #builder_impl
6591            {
6592                #[allow(dead_code)]
6593                pub fn with_clock(mut self, clock: RobotClock) -> Self {
6594                    self.clock = Some(clock);
6595                    self
6596                }
6597
6598                #[allow(dead_code)]
6599                pub fn with_logger<S2, L2>(
6600                    self,
6601                    unified_logger: Arc<Mutex<L2>>,
6602                ) -> #builder_name<#builder_with_logger_generics>
6603                where
6604                    S2: SectionStorage + 'static,
6605                    L2: UnifiedLogWrite<S2> + 'static,
6606                {
6607                    #builder_name {
6608                        clock: self.clock,
6609                        unified_logger,
6610                        instance_id: self.instance_id,
6611                        config_override: self.config_override,
6612                        resources_factory: self.resources_factory,
6613                        #builder_sim_callback_field_copy
6614                        #builder_logstream_copy
6615                        _storage: core::marker::PhantomData,
6616                    }
6617                }
6618
6619                #builder_with_unified_logger_method
6620
6621                #[allow(dead_code)]
6622                pub fn with_instance_id(mut self, instance_id: u32) -> Self {
6623                    self.instance_id = instance_id;
6624                    self
6625                }
6626
6627                pub fn with_resources<R2>(self, resources_factory: R2) -> #builder_name<#builder_with_resources_generics>
6628                where
6629                    R2: FnOnce(&CuConfig) -> CuResult<ResourceManager>,
6630                {
6631                    #builder_name {
6632                        clock: self.clock,
6633                        unified_logger: self.unified_logger,
6634                        instance_id: self.instance_id,
6635                        config_override: self.config_override,
6636                        resources_factory,
6637                        #builder_sim_callback_field_copy
6638                        #builder_logstream_copy
6639                        _storage: core::marker::PhantomData,
6640                    }
6641                }
6642
6643                #builder_with_config_method
6644                #builder_with_log_path_method
6645                #builder_sim_callback_method
6646                #builder_with_logstream_method
6647
6648                /// Builds the application wrapped in its compile-time checked
6649                /// lifecycle, in the `Initialized` state: start it with
6650                /// `start()` or drive the full cycle with `run_until_shutdown()`.
6651                /// The pre-typestate lifecycle methods remain callable on the
6652                /// returned handle (with deprecation warnings) until Copper 2.0.
6653                #[allow(dead_code)]
6654                pub fn build(self) -> CuResult<#builder_build_app_return> {
6655                    Ok(#builder_build_app_wrap::new(self.build_impl()?))
6656                }
6657
6658                fn build_impl(self) -> CuResult<#application_name> {
6659                    let clock = self
6660                        .clock
6661                        .ok_or(CuError::from("Clock missing from builder"))?;
6662                    let (config, config_source) = #builder_prepare_config_call;
6663                    let resources = (self.resources_factory)(&config)?;
6664                    #builder_build_thread_pools_stmt
6665                    let app_resources = AppResources {
6666                        config,
6667                        config_source,
6668                        resources,
6669                        #builder_build_thread_pools_init
6670                    };
6671                    #application_name::build_with_resources(
6672                        clock,
6673                        self.unified_logger,
6674                        app_resources,
6675                        self.instance_id,
6676                        #builder_logstream_arg
6677                        #builder_build_sim_callback_arg
6678                    )
6679                }
6680            }
6681        });
6682
6683        let app_builder_inherent_impl = quote! {
6684            impl #application_name {
6685                #builder_ctor
6686            }
6687        };
6688
6689        let sim_imports = if sim_mode {
6690            Some(quote! {
6691                use cu29::simulation::SimOverride;
6692                use cu29::simulation::CuTaskCallbackState;
6693                use cu29::simulation::CuSimSrcTask;
6694                use cu29::simulation::CuSimSrcTaskPack;
6695                use cu29::simulation::CuSimSinkTask;
6696                use cu29::simulation::CuSimBridge;
6697                use cu29::prelude::app::CuSimApplication;
6698                use cu29::prelude::app::CuRecordedReplayApplication;
6699                use cu29::cubridge::BridgeChannelSet;
6700            })
6701        } else {
6702            None
6703        };
6704
6705        let sim_tasks = if sim_mode {
6706            Some(quote! {
6707                // This is the variation with stubs for the sources and sinks in simulation mode.
6708                // Not used if the used doesn't generate Sim.
6709                pub type CuSimTasks = #task_types_tuple_sim;
6710            })
6711        } else {
6712            None
6713        };
6714
6715        let sim_inst_body = if task_sim_instances_init_code.is_empty() {
6716            quote! {
6717                let _ = (resources, thread_pools);
6718                Ok(())
6719            }
6720        } else {
6721            quote! { Ok(( #(#task_sim_instances_init_code),*, )) }
6722        };
6723
6724        let sim_tasks_instanciator = if sim_mode {
6725            Some(quote! {
6726                pub fn tasks_instanciator_sim<'c>(
6727                    all_instances_configs: Vec<Option<&'c ComponentConfig>>,
6728                    resources: &mut ResourceManager,
6729                    thread_pools: &[Option<Arc<ThreadPool>>],
6730                ) -> CuResult<CuSimTasks> {
6731                    #sim_inst_body
6732            }})
6733        } else {
6734            None
6735        };
6736
6737        let tasks_inst_body_std = if task_instances_init_code.is_empty() {
6738            quote! {
6739                let _ = (resources, thread_pools);
6740                Ok(())
6741            }
6742        } else {
6743            quote! { Ok(( #(#task_instances_init_code),*, )) }
6744        };
6745
6746        let tasks_inst_body_nostd = if task_instances_init_code.is_empty() {
6747            quote! {
6748                let _ = resources;
6749                Ok(())
6750            }
6751        } else {
6752            quote! { Ok(( #(#task_instances_init_code),*, )) }
6753        };
6754
6755        let tasks_instanciator = if std {
6756            quote! {
6757                pub fn tasks_instanciator<'c>(
6758                    all_instances_configs: Vec<Option<&'c ComponentConfig>>,
6759                    resources: &mut ResourceManager,
6760                    thread_pools: &[Option<Arc<ThreadPool>>],
6761                ) -> CuResult<CuTasks> {
6762                    #tasks_inst_body_std
6763                }
6764            }
6765        } else {
6766            // no thread pool in the no-std impl
6767            quote! {
6768                pub fn tasks_instanciator<'c>(
6769                    all_instances_configs: Vec<Option<&'c ComponentConfig>>,
6770                    resources: &mut ResourceManager,
6771                ) -> CuResult<CuTasks> {
6772                    #tasks_inst_body_nostd
6773                }
6774            }
6775        };
6776
6777        // Build the rayon thread pools declared under `runtime.thread_pools`,
6778        // indexed positionally to the config's pool order. Reserved pool ids
6779        // (such as `"rt"`, applied directly to parallel-rt stage workers) leave
6780        // a `None` slot so background-task pool indices stay aligned.
6781        let thread_pools_instanciator = if std {
6782            quote! {
6783                pub fn thread_pools_instanciator(
6784                    config: &CuConfig,
6785                ) -> CuResult<Vec<Option<Arc<ThreadPool>>>> {
6786                    let Some(runtime) = config.runtime.as_ref() else {
6787                        return Ok(Vec::new());
6788                    };
6789                    let mut pools: Vec<Option<Arc<ThreadPool>>> =
6790                        Vec::with_capacity(runtime.thread_pools.len());
6791                    for pool_spec in &runtime.thread_pools {
6792                        if pool_spec.id == cu29::config::RT_POOL {
6793                            pools.push(None);
6794                            continue;
6795                        }
6796                        let pool = cu29::thread_pool::build_pool(pool_spec)?;
6797                        pools.push(Some(Arc::new(pool)));
6798                    }
6799                    Ok(pools)
6800                }
6801            }
6802        } else {
6803            quote! {}
6804        };
6805
6806        let imports = if std {
6807            quote! {
6808                use cu29::rayon::ThreadPool;
6809                use cu29::cuasynctask::CuAsyncSrcTask;
6810                use cu29::cuasynctask::CuAsyncTask;
6811                use cu29::resource::{ResourceBindings, ResourceManager};
6812                use cu29::prelude::SectionStorage;
6813                use cu29::prelude::UnifiedLoggerWrite;
6814                use cu29::prelude::memmap::MmapSectionStorage;
6815                use cu29::__private::sync::{Arc, Mutex};
6816                use std::fmt::{Debug, Formatter};
6817                use std::fmt::Result as FmtResult;
6818                use std::mem::size_of;
6819                use std::boxed::Box;
6820                use std::sync::atomic::{AtomicBool, Ordering};
6821            }
6822        } else {
6823            quote! {
6824                use alloc::boxed::Box;
6825                use alloc::string::String;
6826                use alloc::string::ToString;
6827                use cu29::__private::sync::{Arc, Mutex};
6828                use core::sync::atomic::{AtomicBool, Ordering};
6829                use core::fmt::{Debug, Formatter};
6830                use core::fmt::Result as FmtResult;
6831                use core::mem::size_of;
6832                use cu29::prelude::SectionStorage;
6833                use cu29::resource::{ResourceBindings, ResourceManager};
6834            }
6835        };
6836
6837        let task_mapping_defs = task_resource_mappings.defs.clone();
6838        let bridge_mapping_defs = bridge_resource_mappings.defs.clone();
6839
6840        // Convert the modified struct back into a TokenStream
6841        let mission_mod_tokens = quote! {
6842            mod #mission_mod {
6843                use super::*;  // import the modules the main app did.
6844
6845                #mission_constant_contents
6846
6847                use cu29::bincode::Encode;
6848                use cu29::bincode::enc::Encoder;
6849                use cu29::bincode::error::EncodeError;
6850                use cu29::bincode::Decode;
6851                use cu29::bincode::de::Decoder;
6852                use cu29::bincode::de::DecoderImpl;
6853                use cu29::bincode::error::DecodeError;
6854                use cu29::clock::RobotClock;
6855                use cu29::clock::RobotClockMock;
6856                use cu29::config::CuConfig;
6857                use cu29::config::ComponentConfig;
6858                use cu29::curuntime::CuRuntime;
6859                use cu29::curuntime::CuRuntimeBuilder;
6860                use cu29::curuntime::CuRuntimeParts;
6861                use cu29::curuntime::KeyFrame;
6862                use cu29::curuntime::RuntimeLifecycleConfigSource;
6863                use cu29::curuntime::RuntimeLifecycleEvent;
6864                use cu29::curuntime::RuntimeLifecycleRecord;
6865                use cu29::curuntime::RuntimeLifecycleStackInfo;
6866                use cu29::CuResult;
6867                use cu29::CuError;
6868                use cu29::cutask::CuSrcTask;
6869                use cu29::cutask::CuSinkTask;
6870                use cu29::cutask::CuTask;
6871                // Anytime tasks keep their raw type in the tuple; lifecycle
6872                // calls resolve through this trait import.
6873                #[allow(unused_imports)]
6874                use cu29::cutask_anytime::CuAnytimeTask;
6875                use cu29::cutask::CuMsg;
6876                use cu29::cutask::CuMsgMetadata;
6877                use cu29::copperlist::CopperList;
6878                use cu29::monitoring::CuMonitor; // Trait import.
6879                use cu29::monitoring::CuComponentState;
6880                use cu29::monitoring::Decision;
6881                use cu29::prelude::app::CuApplication;
6882                use cu29::prelude::debug;
6883                use cu29::prelude::stream_write;
6884                use cu29::prelude::UnifiedLogType;
6885                use cu29::prelude::UnifiedLogWrite;
6886                use cu29::prelude::WriteStream;
6887
6888                #imports
6889
6890                #sim_imports
6891
6892                // Not used if a monitor is present
6893                #[allow(unused_imports)]
6894                use cu29::monitoring::NoMonitor;
6895
6896                // This is the heart of everything.
6897                // CuTasks is the list of all the tasks types.
6898                // CuList is a CopperList with the list of all the messages types as msgs.
6899                pub type CuTasks = #task_types_tuple;
6900                pub type CuBridges = #bridges_type_tokens;
6901                #sim_bridge_channel_defs
6902                #resources_module
6903                #resources_instanciator_fn
6904                #task_mapping_defs
6905                #bridge_mapping_defs
6906                #(#autogenerated_output_warnings)*
6907
6908                // One ZST per anytime node carrying its RON policy into the
6909                // type system; unset knobs const-fold away in AnytimeJob::check.
6910                #(#anytime_policy_defs)*
6911
6912                #sim_tasks
6913                #sim_support
6914                #recorded_replay_support
6915                #sim_tasks_instanciator
6916
6917                pub const TASK_IDS: &'static [&'static str] = &[#( #task_ids ),*];
6918                pub const MONITORED_COMPONENTS: &'static [cu29::monitoring::MonitorComponentMetadata] =
6919                    &[#( #monitored_component_entries ),*];
6920                pub const CULIST_COMPONENT_MAPPING: &'static [cu29::monitoring::ComponentId] =
6921                    &[#( cu29::monitoring::ComponentId::new(#culist_component_mapping) ),*];
6922                pub const MONITOR_LAYOUT: cu29::monitoring::CopperListLayout =
6923                    cu29::monitoring::CopperListLayout::new(
6924                        MONITORED_COMPONENTS,
6925                        CULIST_COMPONENT_MAPPING,
6926                    );
6927                #parallel_rt_metadata_defs
6928
6929                #[inline]
6930                pub fn monitor_component_label(
6931                    component_id: cu29::monitoring::ComponentId,
6932                ) -> &'static str {
6933                    MONITORED_COMPONENTS[component_id.index()].id()
6934                }
6935
6936                #culist_support
6937                #parallel_rt_support_tokens
6938
6939                #tasks_instanciator
6940                #thread_pools_instanciator
6941                #bridges_instanciator
6942
6943                pub fn monitor_instanciator(
6944                    config: &CuConfig,
6945                    metadata: ::cu29::monitoring::CuMonitoringMetadata,
6946                    runtime: ::cu29::monitoring::CuMonitoringRuntime,
6947                ) -> #monitor_type {
6948                    #monitor_instanciator_body
6949                }
6950
6951                // The application for this mission
6952                #app_resources_struct
6953                pub #application_struct
6954
6955                #app_inherent_impl
6956                #app_builder_inherent_impl
6957                #app_metadata_impl
6958                #app_reflect_impl
6959                #app_runtime_copperlist_impl
6960                #application_impl
6961                #live_replay_impl
6962                #recorded_replay_app_impl
6963                #distributed_replay_app_impl
6964
6965                #std_application_impl
6966
6967                #application_builder
6968            }
6969
6970        };
6971        all_missions_tokens.push(mission_mod_tokens);
6972    }
6973
6974    let default_application_tokens = if all_missions
6975        .iter()
6976        .any(|(mission_name, _)| mission_name == "default")
6977    {
6978        let default_builder = quote! {
6979            #[allow(unused_imports)]
6980            use default::#builder_name;
6981        };
6982        quote! {
6983            #default_builder
6984
6985            #[allow(unused_imports)]
6986            use default::AppResources;
6987
6988            #[allow(unused_imports)]
6989            use default::resources as app_resources;
6990
6991            #[allow(unused_imports)]
6992            use default::#application_name;
6993        }
6994    } else {
6995        quote!() // do nothing
6996    };
6997
6998    let mission_module_names = all_missions
6999        .iter()
7000        .map(|(mission, _)| {
7001            parse_str::<Ident>(mission)
7002                .expect("Could not make an identifier of the mission name")
7003                .unraw()
7004                .to_string()
7005        })
7006        .collect::<BTreeSet<_>>();
7007    let root_constant_modules = constant_modules.root_modules_except(&mission_module_names);
7008
7009    let result: proc_macro2::TokenStream = quote! {
7010        #root_constant_modules
7011        #(#all_missions_tokens)*
7012        #default_application_tokens
7013    };
7014
7015    result.into()
7016}
7017
7018fn resolve_runtime_config(args: &CopperRuntimeArgs) -> CuResult<ResolvedRuntimeConfig> {
7019    let caller_root = utils::caller_crate_root();
7020    resolve_runtime_config_with_root(args, &caller_root)
7021}
7022
7023fn resolve_runtime_config_with_root(
7024    args: &CopperRuntimeArgs,
7025    caller_root: &Path,
7026) -> CuResult<ResolvedRuntimeConfig> {
7027    let active_features = active_config_features();
7028    let active_feature_refs: Vec<_> = active_features.iter().map(String::as_str).collect();
7029    resolve_runtime_config_with_root_and_features(args, caller_root, &active_feature_refs)
7030}
7031
7032fn resolve_runtime_config_with_root_and_features(
7033    args: &CopperRuntimeArgs,
7034    caller_root: &Path,
7035    active_features: &[&str],
7036) -> CuResult<ResolvedRuntimeConfig> {
7037    let filename = config_full_path_from_root(caller_root, &args.config_path);
7038    if !Path::new(&filename).exists() {
7039        return Err(CuError::from(format!(
7040            "The configuration file `{}` does not exist. Please provide a valid path.",
7041            args.config_path
7042        )));
7043    }
7044
7045    if let Some(subsystem_id) = args.subsystem_id.as_deref() {
7046        let multi_config = cu29_runtime::config::read_multi_configuration_with_features(
7047            filename.as_str(),
7048            active_features,
7049        )
7050        .map_err(|e| {
7051            CuError::from(format!(
7052                "When `subsystem = \"{subsystem_id}\"` is provided, `config = \"{}\"` must point to a valid multi-Copper configuration: {e}",
7053                args.config_path
7054            ))
7055        })?;
7056        let subsystem = multi_config.subsystem(subsystem_id).ok_or_else(|| {
7057            CuError::from(format!(
7058                "Subsystem '{subsystem_id}' was not found in multi-Copper configuration '{}'.",
7059                args.config_path
7060            ))
7061        })?;
7062        // Bundle the include-expanded source representation. Serializing the lowered
7063        // mission graphs would lose the source task order because missions use hash maps.
7064        let (local_config, bundled_local_config_content) =
7065            read_configuration_with_resolved_ron_and_features(
7066                &subsystem.config_path,
7067                active_features,
7068            )
7069            .map_err(|e| {
7070                CuError::from(format!(
7071                    "Failed to prepare bundled local configuration for subsystem '{subsystem_id}' from '{}'.",
7072                    subsystem.config_path
7073                ))
7074                .add_cause(e.to_string().as_str())
7075            })?;
7076
7077        Ok(ResolvedRuntimeConfig {
7078            local_config,
7079            bundled_local_config_content,
7080            active_features: active_features
7081                .iter()
7082                .map(|feature| (*feature).to_string())
7083                .collect(),
7084            subsystem_id: Some(subsystem_id.to_string()),
7085            subsystem_code: subsystem.subsystem_code,
7086        })
7087    } else {
7088        let (local_config, bundled_local_config_content) =
7089            read_configuration_with_resolved_ron_and_features(filename.as_str(), active_features)?;
7090        Ok(ResolvedRuntimeConfig {
7091            local_config,
7092            bundled_local_config_content,
7093            active_features: active_features
7094                .iter()
7095                .map(|feature| (*feature).to_string())
7096                .collect(),
7097            subsystem_id: None,
7098            subsystem_code: 0,
7099        })
7100    }
7101}
7102
7103fn active_config_features() -> Vec<String> {
7104    let mut features: Vec<_> = std::env::var(COPPER_CFG_FEATURES_ENV)
7105        .unwrap_or_default()
7106        .split(',')
7107        .filter(|feature| !feature.is_empty())
7108        .map(str::to_owned)
7109        .collect();
7110    features.sort_unstable();
7111    features.dedup();
7112    features
7113}
7114
7115fn build_config_load_stmt(
7116    std_enabled: bool,
7117    application_name: &Ident,
7118    subsystem_id: Option<&str>,
7119    active_features: &[String],
7120) -> proc_macro2::TokenStream {
7121    let active_features = active_features.iter();
7122    if std_enabled {
7123        if let Some(subsystem_id) = subsystem_id {
7124            quote! {
7125                const COPPER_CFG_FEATURES: &[&str] = &[#(#active_features),*];
7126                let (config, config_source) = if let Some(overridden_config) = config_override {
7127                    debug!("CuConfig: Overridden programmatically.");
7128                    (overridden_config, RuntimeLifecycleConfigSource::ProgrammaticOverride)
7129                } else if ::std::path::Path::new(config_filename).exists() {
7130                    let subsystem_id = #application_name::subsystem()
7131                        .id()
7132                        .expect("generated multi-Copper runtime is missing a subsystem id");
7133                    debug!(
7134                        "CuConfig: Reading multi-Copper configuration from file: {} (subsystem={})",
7135                        config_filename,
7136                        subsystem_id
7137                    );
7138                    let multi_config = cu29::config::read_multi_configuration_with_features(
7139                        config_filename,
7140                        COPPER_CFG_FEATURES,
7141                    )?;
7142                    (
7143                        multi_config.resolve_subsystem_config_for_instance(subsystem_id, instance_id)?,
7144                        RuntimeLifecycleConfigSource::ExternalFile,
7145                    )
7146                } else {
7147                    let original_config = Self::original_config();
7148                    debug!(
7149                        "CuConfig: Using the bundled subsystem configuration compiled into the binary (subsystem={}).",
7150                        #subsystem_id
7151                    );
7152                    if instance_id != 0 {
7153                        debug!(
7154                            "CuConfig: runtime file '{}' is missing, so instance-specific overrides for instance_id={} cannot be resolved; using bundled subsystem defaults.",
7155                            config_filename,
7156                            instance_id
7157                        );
7158                    }
7159                    (
7160                        cu29::config::read_configuration_str(original_config, None)?,
7161                        RuntimeLifecycleConfigSource::BundledDefault,
7162                    )
7163                };
7164            }
7165        } else {
7166            quote! {
7167                const COPPER_CFG_FEATURES: &[&str] = &[#(#active_features),*];
7168                let _ = instance_id;
7169                let (config, config_source) = if let Some(overridden_config) = config_override {
7170                    debug!("CuConfig: Overridden programmatically.");
7171                    (overridden_config, RuntimeLifecycleConfigSource::ProgrammaticOverride)
7172                } else if ::std::path::Path::new(config_filename).exists() {
7173                    debug!("CuConfig: Reading configuration from file: {}", config_filename);
7174                    (
7175                        cu29::config::read_configuration_with_features(
7176                            config_filename,
7177                            COPPER_CFG_FEATURES,
7178                        )?,
7179                        RuntimeLifecycleConfigSource::ExternalFile,
7180                    )
7181                } else {
7182                    let original_config = Self::original_config();
7183                    debug!("CuConfig: Using the bundled configuration compiled into the binary.");
7184                    (
7185                        cu29::config::read_configuration_str(original_config, None)?,
7186                        RuntimeLifecycleConfigSource::BundledDefault,
7187                    )
7188                };
7189            }
7190        }
7191    } else {
7192        quote! {
7193            // Only the original config is available in no-std
7194            let original_config = Self::original_config();
7195            debug!("CuConfig: Using the bundled configuration compiled into the binary.");
7196            let config = cu29::config::read_configuration_str(original_config, None)?;
7197            let config_source = RuntimeLifecycleConfigSource::BundledDefault;
7198        }
7199    }
7200}
7201
7202fn config_full_path(config_file: &str) -> String {
7203    config_full_path_from_root(&utils::caller_crate_root(), config_file)
7204}
7205
7206fn config_full_path_from_root(caller_root: &Path, config_file: &str) -> String {
7207    let mut config_full_path = caller_root.to_path_buf();
7208    config_full_path.push(config_file);
7209    let filename = config_full_path
7210        .as_os_str()
7211        .to_str()
7212        .expect("Could not interpret the config file name");
7213    filename.to_string()
7214}
7215
7216fn read_config(config_file: &str) -> CuResult<CuConfig> {
7217    let filename = config_full_path(config_file);
7218    let active_features = active_config_features();
7219    let active_feature_refs: Vec<_> = active_features.iter().map(String::as_str).collect();
7220    read_configuration_with_features(filename.as_str(), &active_feature_refs)
7221}
7222
7223/// Bake an out-of-tree planner's build-time resolved step orders into the config.
7224///
7225/// Ship-with-copper planners are instantiated directly by the shared planner;
7226/// any other `runtime.planner` type must have been resolved by
7227/// `cu29::planner::emit_plan` in the caller's build.rs, whose artifact this
7228/// reads from `OUT_DIR`.
7229fn apply_external_plan(config: &mut CuConfig) -> CuResult<()> {
7230    let (planner_type, already_resolved) = match config.planner_config() {
7231        None => return Ok(()),
7232        Some(selection) => (
7233            selection.get_type().to_string(),
7234            selection.resolved_orders().is_some(),
7235        ),
7236    };
7237    if is_builtin_planner(&planner_type) || already_resolved {
7238        return Ok(());
7239    }
7240    let build_rs_hint = format!(
7241        "add to this crate's build.rs: cu29::planner::emit_plan::<{planner_type}>(\"<config>.ron\").unwrap()"
7242    );
7243    let shipped = BUILTIN_PLANNERS.join(", ");
7244    let out_dir = std::env::var("OUT_DIR").map_err(|_| {
7245        CuError::from(format!(
7246            "Planner '{planner_type}' is not shipped with copper (shipped: {shipped}) and no plan artifact is available (no build script?); {build_rs_hint}"
7247        ))
7248    })?;
7249    let path = std::path::Path::new(&out_dir).join(PLAN_ARTIFACT_FILE);
7250    let artifact = read_plan_artifact(&path).map_err(|e| {
7251        CuError::from(format!(
7252            "Planner '{planner_type}' is not shipped with copper (shipped: {shipped}) and reading its plan artifact failed ({e}); {build_rs_hint}"
7253        ))
7254    })?;
7255    let artifact_name = artifact
7256        .planner_type
7257        .rsplit("::")
7258        .next()
7259        .unwrap_or(&artifact.planner_type);
7260    let config_name = planner_type.rsplit("::").next().unwrap_or(&planner_type);
7261    if artifact_name != config_name {
7262        return Err(CuError::from(format!(
7263            "The plan artifact was emitted by '{}' but the config selects '{planner_type}'.",
7264            artifact.planner_type
7265        )));
7266    }
7267    let expected = config_digest(config)?;
7268    if artifact.config_digest != expected {
7269        return Err(CuError::from(format!(
7270            "The plan artifact for planner '{planner_type}' is stale; re-run the build script (touch build.rs or the config file)."
7271        )));
7272    }
7273    config.set_planner_resolved_orders(&planner_type, artifact.orders);
7274    Ok(())
7275}
7276
7277fn inferred_single_output_payload_type(
7278    task_type: &Type,
7279    task_kind: CuTaskType,
7280    is_anytime: bool,
7281) -> Type {
7282    if is_anytime {
7283        return parse_quote! {
7284            <<#task_type as cu29::cutask_anytime::CuAnytimeTask>::Output<'static> as cu29::cutask::CuSingleOutputMsg>::Payload
7285        };
7286    }
7287    match task_kind {
7288        CuTaskType::Source => parse_quote! {
7289            <<#task_type as cu29::cutask::CuSrcTask>::Output<'static> as cu29::cutask::CuSingleOutputMsg>::Payload
7290        },
7291        CuTaskType::Regular => parse_quote! {
7292            <<#task_type as cu29::cutask::CuTask>::Output<'static> as cu29::cutask::CuSingleOutputMsg>::Payload
7293        },
7294        CuTaskType::Sink => panic!("Sinks do not have output payload types"),
7295    }
7296}
7297
7298fn task_trait_for_kind(task_kind: CuTaskType) -> proc_macro2::TokenStream {
7299    match task_kind {
7300        CuTaskType::Source => quote! { cu29::cutask::CuSrcTask },
7301        CuTaskType::Regular => quote! { cu29::cutask::CuTask },
7302        CuTaskType::Sink => quote! { cu29::cutask::CuSinkTask },
7303    }
7304}
7305
7306/// Like [`task_trait_for_kind`], but resolves anytime nodes to
7307/// `CuAnytimeTask` (they stay `Regular` in the graph but implement the
7308/// anytime trait instead of `CuTask`).
7309///
7310/// Background comes first: a backgrounded node is driven through the
7311/// `CuAsyncTask` wrapper, which is a plain `CuTask` whatever it wraps.
7312fn task_trait_for_specs(task_specs: &CuTaskSpecSet, index: usize) -> proc_macro2::TokenStream {
7313    let foreground_anytime =
7314        task_specs.anytime_configs[index].is_some() && !task_specs.background_flags[index];
7315    if foreground_anytime {
7316        quote! { cu29::cutask_anytime::CuAnytimeTask }
7317    } else {
7318        task_trait_for_kind(task_specs.cutypes[index])
7319    }
7320}
7321
7322fn task_output_payload_type(
7323    graph: &CuGraph,
7324    node: &Node,
7325    task_kind: CuTaskType,
7326    task_type: &Type,
7327) -> Option<Type> {
7328    if task_kind == CuTaskType::Sink {
7329        return None;
7330    }
7331
7332    let id = node.get_id();
7333    if let Some(type_str) = graph.get_node_output_msg_type(id.as_str()) {
7334        return Some(
7335            parse_str::<Type>(type_str.as_str()).expect("Could not parse output message type."),
7336        );
7337    }
7338
7339    node.get_declared_task_kind().map(|_| {
7340        inferred_single_output_payload_type(task_type, task_kind, node.anytime().is_some())
7341    })
7342}
7343
7344#[cfg(test)]
7345fn synthesized_single_output_msg_name(
7346    task_type: &Type,
7347    task_kind: CuTaskType,
7348    is_anytime: bool,
7349) -> String {
7350    inferred_single_output_payload_type(task_type, task_kind, is_anytime)
7351        .to_token_stream()
7352        .to_string()
7353}
7354
7355struct CuTaskSpecSet {
7356    pub ids: Vec<String>,
7357    pub cutypes: Vec<CuTaskType>,
7358    pub background_flags: Vec<bool>,
7359    /// Thread pool name each task runs on when backgrounded (defaults to the
7360    /// `"background"` pool). Only meaningful where `background_flags` is true.
7361    pub background_pools: Vec<String>,
7362    /// The `anytime:` policy of each task, if any. An anytime task keeps its
7363    /// raw `CuAnytimeTask` type in the tuple (no wrapper); the policy values
7364    /// are baked into a per-node `AnytimePolicy` ZST and the emitted
7365    /// base/refine steps.
7366    pub anytime_configs: Vec<Option<AnytimeConfig>>,
7367    /// The task type inside the optional async wrapper: the anytime runner
7368    /// for a background anytime node, the declared task type for everything
7369    /// else — including every foreground task, where nothing wraps it.
7370    pub async_inner_task_types: Vec<Type>,
7371    pub logging_enabled: Vec<bool>,
7372    pub type_names: Vec<String>,
7373    pub task_types: Vec<Type>,
7374    pub instantiation_types: Vec<Type>,
7375    pub sim_task_types: Vec<Type>,
7376    pub run_in_sim_flags: Vec<bool>,
7377    #[allow(dead_code)]
7378    pub output_types: Vec<Option<Type>>,
7379    pub autogenerated_output_flags: Vec<bool>,
7380    pub node_id_to_task_index: Vec<Option<usize>>,
7381}
7382
7383impl CuTaskSpecSet {
7384    pub fn from_graph(graph: &CuGraph) -> CuResult<Self> {
7385        let all_id_nodes: Vec<(NodeId, &Node)> = graph
7386            .get_all_nodes()
7387            .into_iter()
7388            .filter(|(_, node)| node.get_flavor() == Flavor::Task)
7389            .collect();
7390
7391        let ids: Vec<String> = all_id_nodes
7392            .iter()
7393            .map(|(_, node)| node.get_id().to_string())
7394            .collect();
7395
7396        let cutypes: Vec<CuTaskType> = all_id_nodes
7397            .iter()
7398            .map(|(id, _)| find_task_type_for_id(graph, *id))
7399            .collect::<CuResult<Vec<_>>>()?;
7400
7401        let background_flags: Vec<bool> = all_id_nodes
7402            .iter()
7403            .map(|(_, node)| node.is_background())
7404            .collect();
7405
7406        let background_pools: Vec<String> = all_id_nodes
7407            .iter()
7408            .map(|(_, node)| node.background_pool().to_string())
7409            .collect();
7410
7411        let anytime_configs: Vec<Option<AnytimeConfig>> = all_id_nodes
7412            .iter()
7413            .map(|(_, node)| node.anytime().cloned())
7414            .collect();
7415
7416        let logging_enabled: Vec<bool> = all_id_nodes
7417            .iter()
7418            .map(|(_, node)| node.is_logging_enabled())
7419            .collect();
7420
7421        let type_names: Vec<String> = all_id_nodes
7422            .iter()
7423            .map(|(_, node)| node.get_type().to_string())
7424            .collect();
7425
7426        let declared_task_types: Vec<Type> = type_names
7427            .iter()
7428            .map(|name| {
7429                parse_str::<Type>(name).unwrap_or_else(|error| {
7430                    panic!("Could not transform {name} into a Task Rust type: {error}");
7431                })
7432            })
7433            .collect();
7434
7435        let output_types: Vec<Option<Type>> = all_id_nodes
7436            .iter()
7437            .zip(cutypes.iter())
7438            .zip(declared_task_types.iter())
7439            .map(|(((_, node), &task_kind), task_type)| {
7440                task_output_payload_type(graph, node, task_kind, task_type)
7441            })
7442            .collect();
7443
7444        let autogenerated_output_flags: Vec<bool> = all_id_nodes
7445            .iter()
7446            .zip(cutypes.iter())
7447            .map(|((node_id, node), &task_kind)| {
7448                task_kind != CuTaskType::Sink
7449                    && node.get_declared_task_kind().is_some()
7450                    && graph
7451                        .get_node_output_msg_types_by_id(*node_id)
7452                        .expect("missing output type lookup")
7453                        .is_empty()
7454            })
7455            .collect();
7456
7457        // A background anytime node is handed to `CuAsyncTask` wrapped in the
7458        // runner, which turns one whole job into a single `process` call.
7459        let async_inner_task_types: Vec<Type> = declared_task_types
7460            .iter()
7461            .zip(ids.iter())
7462            .zip(background_flags.iter())
7463            .zip(anytime_configs.iter())
7464            .map(|(((task_type, id), &background), anytime)| {
7465                if background && anytime.is_some() {
7466                    let policy_ident = anytime_policy_ident(id.as_str());
7467                    parse_quote!(cu29::cutask_anytime::CuAnytimeRunner<#task_type, #policy_ident>)
7468                } else {
7469                    task_type.clone()
7470                }
7471            })
7472            .collect();
7473
7474        let task_types = declared_task_types
7475            .iter()
7476            .zip(type_names.iter())
7477            .zip(cutypes.iter())
7478            .zip(background_flags.iter())
7479            .zip(output_types.iter())
7480            .zip(async_inner_task_types.iter())
7481            .zip(all_id_nodes.iter())
7482            .map(|((((((name_type, name), cutype), &background), output_type), inner_type), (_, node))| {
7483                let process_empty = node.background_process_empty();
7484                if background {
7485                    if let Some(output_type) = output_type {
7486                        match cutype {
7487                            CuTaskType::Source => {
7488                                parse_quote!(CuAsyncSrcTask<#inner_type, #output_type>)
7489                            }
7490                            CuTaskType::Regular => {
7491                                parse_quote!(CuAsyncTask<#inner_type, #output_type, #process_empty>)
7492                            }
7493                            CuTaskType::Sink => {
7494                                panic!("CuSinkTask {name} cannot be a background task, it should be a regular task.");
7495                            }
7496                        }
7497                    } else {
7498                        panic!(
7499                            "{}: If a task is background, it has to have an output",
7500                            name_type.to_token_stream()
7501                        );
7502                    }
7503                } else {
7504                    name_type.clone()
7505                }
7506            })
7507            .collect();
7508
7509        let instantiation_types = declared_task_types
7510            .iter()
7511            .zip(type_names.iter())
7512            .zip(cutypes.iter())
7513            .zip(background_flags.iter())
7514            .zip(output_types.iter())
7515            .zip(async_inner_task_types.iter())
7516            .zip(all_id_nodes.iter())
7517            .map(|((((((name_type, name), cutype), &background), output_type), inner_type), (_, node))| {
7518                let process_empty = node.background_process_empty();
7519                if background {
7520                    if let Some(output_type) = output_type {
7521                        match cutype {
7522                            CuTaskType::Source => {
7523                                parse_quote!(CuAsyncSrcTask::<#inner_type, #output_type>)
7524                            }
7525                            CuTaskType::Regular => {
7526                                parse_quote!(CuAsyncTask::<#inner_type, #output_type, #process_empty>)
7527                            }
7528                            CuTaskType::Sink => {
7529                                panic!("CuSinkTask {name} cannot be a background task, it should be a regular task.");
7530                            }
7531                        }
7532                    } else {
7533                        panic!(
7534                            "{}: If a task is background, it has to have an output",
7535                            name_type.to_token_stream()
7536                        );
7537                    }
7538                } else {
7539                    name_type.clone()
7540                }
7541            })
7542            .collect();
7543
7544        let sim_task_types = declared_task_types;
7545
7546        let run_in_sim_flags = all_id_nodes
7547            .iter()
7548            .map(|(_, node)| node.is_run_in_sim())
7549            .collect();
7550
7551        let mut node_id_to_task_index = vec![None; graph.node_count()];
7552        for (index, (node_id, _)) in all_id_nodes.iter().enumerate() {
7553            node_id_to_task_index[*node_id as usize] = Some(index);
7554        }
7555
7556        Ok(Self {
7557            ids,
7558            cutypes,
7559            background_flags,
7560            background_pools,
7561            anytime_configs,
7562            async_inner_task_types,
7563            logging_enabled,
7564            type_names,
7565            task_types,
7566            instantiation_types,
7567            sim_task_types,
7568            run_in_sim_flags,
7569            output_types,
7570            autogenerated_output_flags,
7571            node_id_to_task_index,
7572        })
7573    }
7574}
7575
7576#[derive(Clone)]
7577struct OutputPack {
7578    msg_types: Vec<Type>,
7579    msg_type_names: Vec<String>,
7580}
7581
7582impl OutputPack {
7583    fn slot_type(&self) -> Type {
7584        build_output_slot_type(&self.msg_types)
7585    }
7586
7587    fn is_multi(&self) -> bool {
7588        self.msg_types.len() > 1
7589    }
7590}
7591
7592fn build_output_slot_type(msg_types: &[Type]) -> Type {
7593    if msg_types.is_empty() {
7594        parse_quote! { () }
7595    } else if msg_types.len() == 1 {
7596        let msg_type = msg_types.first().unwrap();
7597        parse_quote! { CuMsg<#msg_type> }
7598    } else {
7599        parse_quote! { ( #( CuMsg<#msg_types> ),* ) }
7600    }
7601}
7602
7603fn flatten_slot_origin_ids(
7604    output_packs: &[OutputPack],
7605    slot_origin_ids: &[Option<String>],
7606) -> Vec<String> {
7607    let mut ids = Vec::new();
7608    for (slot, pack) in output_packs.iter().enumerate() {
7609        if pack.msg_types.is_empty() {
7610            continue;
7611        }
7612        let origin = slot_origin_ids
7613            .get(slot)
7614            .and_then(|origin| origin.as_ref())
7615            .unwrap_or_else(|| panic!("Missing slot origin id for copperlist output slot {slot}"));
7616        for _ in 0..pack.msg_types.len() {
7617            ids.push(origin.clone());
7618        }
7619    }
7620    ids
7621}
7622
7623fn flatten_task_output_specs(
7624    output_packs: &[OutputPack],
7625    slot_origin_ids: &[Option<String>],
7626) -> Vec<(String, String, Type)> {
7627    let mut specs = Vec::new();
7628    for (slot, pack) in output_packs.iter().enumerate() {
7629        if pack.msg_types.is_empty() {
7630            continue;
7631        }
7632        let origin = slot_origin_ids
7633            .get(slot)
7634            .and_then(|origin| origin.as_ref())
7635            .unwrap_or_else(|| panic!("Missing slot origin id for copperlist output slot {slot}"));
7636        for (msg_type, payload_type) in pack.msg_type_names.iter().zip(pack.msg_types.iter()) {
7637            specs.push((origin.clone(), msg_type.clone(), payload_type.clone()));
7638        }
7639    }
7640    specs
7641}
7642
7643/// Compute the per-slot [`HandleContent`] policy, reading each slot's producing task's
7644/// `NodeLogging.handle_content` from the config. Slots produced by bridges (or whose
7645/// producing task can't be located in the config) default to [`HandleContent::All`] —
7646/// matches the existing, payload-preserving behavior.
7647fn build_slot_handle_modes(
7648    cuconfig: &CuConfig,
7649    mission_label: Option<&str>,
7650    output_packs: &[OutputPack],
7651    node_output_positions: &HashMap<NodeId, usize>,
7652    task_names: &[(NodeId, String, String)],
7653) -> Vec<HandleContent> {
7654    let mut slot_modes: Vec<HandleContent> = vec![HandleContent::default(); output_packs.len()];
7655    for (node_id, task_id, _member) in task_names {
7656        let Some(pos) = node_output_positions.get(node_id) else {
7657            continue;
7658        };
7659        if let Some(node) = cuconfig.find_task_node(mission_label, task_id) {
7660            slot_modes[*pos] = node.handle_content_policy();
7661        }
7662    }
7663    slot_modes
7664}
7665
7666fn extract_output_packs(runtime_plan: &CuExecutionLoop) -> Vec<OutputPack> {
7667    let mut packs: Vec<(u32, OutputPack)> = runtime_plan
7668        .steps
7669        .iter()
7670        .filter_map(|unit| match unit {
7671            CuExecutionUnit::Step(step) => {
7672                // Refine steps reuse the base step's slot: one slot per node.
7673                if step.phase == CuStepPhase::AnytimeRefine {
7674                    return None;
7675                }
7676                let output_pack = step.output_msg_pack.as_ref()?;
7677                let msg_types: Vec<Type> = output_pack
7678                    .msg_types
7679                    .iter()
7680                    .map(|output_msg_type| {
7681                        parse_str::<Type>(output_msg_type.as_str()).unwrap_or_else(|_| {
7682                            panic!(
7683                                "Could not transform {output_msg_type} into a message Rust type."
7684                            )
7685                        })
7686                    })
7687                    .collect();
7688                Some((
7689                    output_pack.culist_index,
7690                    OutputPack {
7691                        msg_types,
7692                        msg_type_names: output_pack.msg_types.clone(),
7693                    },
7694                ))
7695            }
7696            CuExecutionUnit::Loop(_) => todo!("Needs to be implemented"),
7697        })
7698        .collect();
7699
7700    packs.sort_by_key(|(index, _)| *index);
7701    packs.into_iter().map(|(_, pack)| pack).collect()
7702}
7703
7704#[derive(Clone)]
7705struct SlotCodecBinding {
7706    payload_type: Type,
7707    task_id: String,
7708    msg_type: String,
7709    codec_type: syn::Path,
7710    codec_type_path: String,
7711}
7712
7713fn build_flat_slot_codec_bindings(
7714    cuconfig: &CuConfig,
7715    mission_label: Option<&str>,
7716    output_packs: &[OutputPack],
7717    node_output_positions: &HashMap<NodeId, usize>,
7718    task_names: &[(NodeId, String, String)],
7719) -> CuResult<Vec<Option<SlotCodecBinding>>> {
7720    let mut slot_task_ids: Vec<Option<String>> = vec![None; output_packs.len()];
7721    for (node_id, task_id, _) in task_names {
7722        let Some(output_position) = node_output_positions.get(node_id) else {
7723            continue;
7724        };
7725        slot_task_ids[*output_position] = Some(task_id.clone());
7726    }
7727
7728    let mut bindings =
7729        Vec::with_capacity(output_packs.iter().map(|pack| pack.msg_types.len()).sum());
7730    for (slot_idx, pack) in output_packs.iter().enumerate() {
7731        let task_id = slot_task_ids.get(slot_idx).and_then(|id| id.as_ref());
7732        for (port_idx, payload_type) in pack.msg_types.iter().enumerate() {
7733            let Some(task_id) = task_id else {
7734                bindings.push(None);
7735                continue;
7736            };
7737            let Some(msg_type) = pack.msg_type_names.get(port_idx) else {
7738                return Err(CuError::from(format!(
7739                    "Missing message type name for task '{task_id}' slot {slot_idx} port {port_idx}."
7740                )));
7741            };
7742
7743            let spec = cuconfig
7744                .find_task_node(mission_label, task_id)
7745                .and_then(|node| node.get_logging())
7746                .and_then(|logging| logging.codec_for_msg_type(msg_type))
7747                .map(|codec_id| {
7748                    cuconfig.find_logging_codec_spec(codec_id).ok_or_else(|| {
7749                        CuError::from(format!(
7750                            "Task '{task_id}' binds output '{msg_type}' to unknown logging codec '{codec_id}'."
7751                        ))
7752                    })
7753                })
7754                .transpose()?;
7755
7756            if let Some(spec) = spec {
7757                let codec_type = parse_str::<syn::Path>(&spec.type_).map_err(|_| {
7758                    CuError::from(format!(
7759                        "Logging codec '{}' for task '{task_id}' output '{msg_type}' is not a valid Rust type path.",
7760                        spec.type_
7761                    ))
7762                })?;
7763                bindings.push(Some(SlotCodecBinding {
7764                    payload_type: payload_type.clone(),
7765                    task_id: task_id.clone(),
7766                    msg_type: msg_type.clone(),
7767                    codec_type,
7768                    codec_type_path: spec.type_.clone(),
7769                }));
7770            } else {
7771                bindings.push(None);
7772            }
7773        }
7774    }
7775
7776    Ok(bindings)
7777}
7778
7779fn build_culist_codec_helpers(
7780    flat_codec_bindings: &[Option<SlotCodecBinding>],
7781    default_config_ron_ident: &Ident,
7782    mission_label: Option<&str>,
7783) -> (
7784    Vec<proc_macro2::TokenStream>,
7785    Vec<Option<Ident>>,
7786    Vec<Option<Ident>>,
7787) {
7788    let compressed = !cfg!(feature = "flat-copperlist-encoding");
7789    let mission_tokens = if let Some(mission) = mission_label {
7790        let lit = LitStr::new(mission, Span::call_site());
7791        quote! { Some(#lit) }
7792    } else {
7793        quote! { None }
7794    };
7795
7796    let mut helpers = Vec::new();
7797    let mut encode_helper_names = Vec::with_capacity(flat_codec_bindings.len());
7798    let mut decode_helper_names = Vec::with_capacity(flat_codec_bindings.len());
7799
7800    for (flat_idx, binding) in flat_codec_bindings.iter().enumerate() {
7801        let Some(binding) = binding else {
7802            encode_helper_names.push(None);
7803            decode_helper_names.push(None);
7804            continue;
7805        };
7806
7807        let encode_fn = format_ident!("__cu_logcodec_encode_slot_{flat_idx}");
7808        let decode_fn = format_ident!("__cu_logcodec_decode_slot_{flat_idx}");
7809        let payload_type = &binding.payload_type;
7810        let codec_type = &binding.codec_type;
7811        let task_id = LitStr::new(&binding.task_id, Span::call_site());
7812        let msg_type = LitStr::new(&binding.msg_type, Span::call_site());
7813        let codec_type_path = LitStr::new(&binding.codec_type_path, Span::call_site());
7814
7815        let helper = if compressed {
7816            quote! {
7817            fn #encode_fn<E: Encoder>(payload: &#payload_type, encoder: &mut E) -> Result<(), EncodeError> {
7818                static STATE: ::cu29::logcodec::CodecState<#codec_type> = ::cu29::logcodec::CodecState::new();
7819                let config_entry = ::cu29::logcodec::effective_config_entry::<CuStampedDataSet>(#default_config_ron_ident);
7820                ::cu29::logcodec::with_codec_for_encode(
7821                    &STATE,
7822                    config_entry,
7823                    |effective_config_ron| {
7824                        ::cu29::logcodec::instantiate_codec::<#codec_type, #payload_type>(
7825                            effective_config_ron,
7826                            #mission_tokens,
7827                            #task_id,
7828                            #msg_type,
7829                            #codec_type_path,
7830                        )
7831                    },
7832                    |codec| ::cu29::logcodec::encode_payload_with_codec(payload, codec, encoder),
7833                )
7834            }
7835
7836            fn #decode_fn<D: Decoder<Context = ()>>(decoder: &mut D) -> Result<#payload_type, DecodeError> {
7837                static STATE: ::cu29::logcodec::CodecState<#codec_type> = ::cu29::logcodec::CodecState::new();
7838                let config_entry = ::cu29::logcodec::effective_config_entry::<CuStampedDataSet>(#default_config_ron_ident);
7839                ::cu29::logcodec::with_codec_for_decode(
7840                    &STATE,
7841                    config_entry,
7842                    |effective_config_ron| {
7843                        ::cu29::logcodec::instantiate_codec::<#codec_type, #payload_type>(
7844                            effective_config_ron,
7845                            #mission_tokens,
7846                            #task_id,
7847                            #msg_type,
7848                            #codec_type_path,
7849                        )
7850                    },
7851                    |codec| ::cu29::logcodec::decode_payload_with_codec(decoder, codec),
7852                )
7853            }
7854            }
7855        } else {
7856            quote! {
7857            fn #encode_fn<E: Encoder>(msg: &CuMsg<#payload_type>, encoder: &mut E) -> Result<(), EncodeError> {
7858                static STATE: ::cu29::logcodec::CodecState<#codec_type> = ::cu29::logcodec::CodecState::new();
7859                let config_entry = ::cu29::logcodec::effective_config_entry::<CuStampedDataSet>(#default_config_ron_ident);
7860                ::cu29::logcodec::with_codec_for_encode(
7861                    &STATE,
7862                    config_entry,
7863                    |effective_config_ron| {
7864                        ::cu29::logcodec::instantiate_codec::<#codec_type, #payload_type>(
7865                            effective_config_ron,
7866                            #mission_tokens,
7867                            #task_id,
7868                            #msg_type,
7869                            #codec_type_path,
7870                        )
7871                    },
7872                    |codec| ::cu29::logcodec::encode_msg_with_codec(msg, codec, encoder),
7873                )
7874            }
7875
7876            fn #decode_fn<D: Decoder<Context = ()>>(decoder: &mut D) -> Result<CuMsg<#payload_type>, DecodeError> {
7877                static STATE: ::cu29::logcodec::CodecState<#codec_type> = ::cu29::logcodec::CodecState::new();
7878                let config_entry = ::cu29::logcodec::effective_config_entry::<CuStampedDataSet>(#default_config_ron_ident);
7879                ::cu29::logcodec::with_codec_for_decode(
7880                    &STATE,
7881                    config_entry,
7882                    |effective_config_ron| {
7883                        ::cu29::logcodec::instantiate_codec::<#codec_type, #payload_type>(
7884                            effective_config_ron,
7885                            #mission_tokens,
7886                            #task_id,
7887                            #msg_type,
7888                            #codec_type_path,
7889                        )
7890                    },
7891                    |codec| ::cu29::logcodec::decode_msg_with_codec(decoder, codec),
7892                )
7893            }
7894            }
7895        };
7896        helpers.push(helper);
7897        encode_helper_names.push(Some(encode_fn));
7898        decode_helper_names.push(Some(decode_fn));
7899    }
7900
7901    (helpers, encode_helper_names, decode_helper_names)
7902}
7903
7904fn collect_output_pack_sizes(runtime_plan: &CuExecutionLoop) -> Vec<usize> {
7905    let mut sizes: Vec<(u32, usize)> = runtime_plan
7906        .steps
7907        .iter()
7908        .filter_map(|unit| match unit {
7909            CuExecutionUnit::Step(step) => {
7910                // Refine steps reuse the base step's slot: one slot per node.
7911                if step.phase == CuStepPhase::AnytimeRefine {
7912                    return None;
7913                }
7914                step.output_msg_pack
7915                    .as_ref()
7916                    .map(|output_pack| (output_pack.culist_index, output_pack.msg_types.len()))
7917            }
7918            CuExecutionUnit::Loop(_) => todo!("Needs to be implemented"),
7919        })
7920        .collect();
7921
7922    sizes.sort_by_key(|(index, _)| *index);
7923    sizes.into_iter().map(|(_, size)| size).collect()
7924}
7925
7926fn sorted_mission_graphs(copper_config: &CuConfig) -> Vec<(String, CuGraph)> {
7927    let mut all_missions: Vec<_> = copper_config
7928        .graphs
7929        .get_all_missions_graphs()
7930        .into_iter()
7931        .collect();
7932    all_missions.sort_by(|(left, _), (right, _)| left.cmp(right));
7933    all_missions
7934}
7935
7936#[derive(Debug, Clone, PartialEq, Eq)]
7937struct CanonicalTaskInputSlot {
7938    msg_type: String,
7939    connection_orders: BTreeSet<usize>,
7940}
7941
7942#[derive(Debug, Clone, PartialEq, Eq)]
7943struct MissionTaskInput {
7944    msg_type: String,
7945    connection_order: usize,
7946}
7947
7948#[derive(Debug, Clone)]
7949struct TaskInputLayout {
7950    slots: Vec<CanonicalTaskInputSlot>,
7951    mission_slot_mappings: HashMap<String, Vec<Option<usize>>>,
7952}
7953
7954#[derive(Clone, Copy)]
7955enum AlignmentStep {
7956    Match {
7957        canonical_slot_index: usize,
7958        mission_input_index: usize,
7959    },
7960    ExistingGap {
7961        canonical_slot_index: usize,
7962    },
7963    Insert {
7964        mission_input_index: usize,
7965    },
7966}
7967
7968#[derive(Clone, Copy)]
7969enum AlignmentTransition {
7970    Match,
7971    ExistingGap,
7972    Insert,
7973}
7974
7975#[derive(Clone, Copy)]
7976struct AlignmentBackpointer {
7977    prev_i: usize,
7978    prev_j: usize,
7979    transition: AlignmentTransition,
7980}
7981
7982#[derive(Clone, Copy)]
7983struct AlignmentCell {
7984    score: i32,
7985    paths: u8,
7986    backpointer: Option<AlignmentBackpointer>,
7987}
7988
7989impl AlignmentCell {
7990    fn unreachable() -> Self {
7991        Self {
7992            score: i32::MIN,
7993            paths: 0,
7994            backpointer: None,
7995        }
7996    }
7997}
7998
7999fn collect_mission_task_inputs(
8000    graph: &CuGraph,
8001    node_id: NodeId,
8002    task_id: &str,
8003) -> CuResult<Vec<MissionTaskInput>> {
8004    let mut edge_ids = graph.get_dst_edges(node_id)?;
8005    edge_ids.sort_by_key(|edge_id| {
8006        graph
8007            .edge(*edge_id)
8008            .map(|edge| edge.order)
8009            .unwrap_or(usize::MAX)
8010    });
8011
8012    edge_ids
8013        .into_iter()
8014        .map(|edge_id| {
8015            let edge = graph.edge(edge_id).ok_or_else(|| {
8016                CuError::from(format!(
8017                    "Missing edge {edge_id} while collecting inputs for task '{task_id}'"
8018                ))
8019            })?;
8020            Ok(MissionTaskInput {
8021                msg_type: edge.msg.clone(),
8022                connection_order: edge.order,
8023            })
8024        })
8025        .collect()
8026}
8027
8028fn format_canonical_input_slots(slots: &[CanonicalTaskInputSlot]) -> String {
8029    let parts: Vec<String> = slots
8030        .iter()
8031        .map(|slot| {
8032            let orders = slot
8033                .connection_orders
8034                .iter()
8035                .map(|order| order.to_string())
8036                .collect::<Vec<_>>()
8037                .join("|");
8038            format!("{}@{}", slot.msg_type, orders)
8039        })
8040        .collect();
8041    format!("[{}]", parts.join(", "))
8042}
8043
8044fn format_mission_task_inputs(inputs: &[MissionTaskInput]) -> String {
8045    let parts: Vec<String> = inputs
8046        .iter()
8047        .map(|input| format!("{}@{}", input.msg_type, input.connection_order))
8048        .collect();
8049    format!("[{}]", parts.join(", "))
8050}
8051
8052const INPUT_MATCH_SCORE: i32 = 100;
8053const ANCHORED_INPUT_MATCH_BONUS: i32 = 1;
8054
8055fn task_input_match_score(slot: &CanonicalTaskInputSlot, input: &MissionTaskInput) -> Option<i32> {
8056    if slot.msg_type != input.msg_type {
8057        return None;
8058    }
8059
8060    let anchored_bonus = if slot.connection_orders.contains(&input.connection_order) {
8061        ANCHORED_INPUT_MATCH_BONUS
8062    } else {
8063        0
8064    };
8065
8066    Some(INPUT_MATCH_SCORE + anchored_bonus)
8067}
8068
8069fn update_alignment_cell(
8070    cell: &mut AlignmentCell,
8071    candidate_score: i32,
8072    candidate_paths: u8,
8073    backpointer: Option<AlignmentBackpointer>,
8074) {
8075    if candidate_paths == 0 {
8076        return;
8077    }
8078
8079    if candidate_score > cell.score {
8080        cell.score = candidate_score;
8081        cell.paths = candidate_paths.min(2);
8082        cell.backpointer = if candidate_paths == 1 {
8083            backpointer
8084        } else {
8085            None
8086        };
8087    } else if candidate_score == cell.score {
8088        cell.paths = cell.paths.saturating_add(candidate_paths).min(2);
8089        cell.backpointer = None;
8090    }
8091}
8092
8093fn align_task_inputs(
8094    task_id: &str,
8095    mission_name: &str,
8096    canonical_slots: &[CanonicalTaskInputSlot],
8097    mission_inputs: &[MissionTaskInput],
8098) -> CuResult<Vec<AlignmentStep>> {
8099    let canonical_len = canonical_slots.len();
8100    let mission_len = mission_inputs.len();
8101    let mut table = vec![vec![AlignmentCell::unreachable(); mission_len + 1]; canonical_len + 1];
8102    table[0][0] = AlignmentCell {
8103        score: 0,
8104        paths: 1,
8105        backpointer: None,
8106    };
8107
8108    for i in 0..=canonical_len {
8109        for j in 0..=mission_len {
8110            let cell = table[i][j];
8111            if cell.paths == 0 {
8112                continue;
8113            }
8114
8115            if i < canonical_len {
8116                update_alignment_cell(
8117                    &mut table[i + 1][j],
8118                    cell.score,
8119                    cell.paths,
8120                    if cell.paths == 1 {
8121                        Some(AlignmentBackpointer {
8122                            prev_i: i,
8123                            prev_j: j,
8124                            transition: AlignmentTransition::ExistingGap,
8125                        })
8126                    } else {
8127                        None
8128                    },
8129                );
8130            }
8131
8132            if j < mission_len {
8133                update_alignment_cell(
8134                    &mut table[i][j + 1],
8135                    cell.score,
8136                    cell.paths,
8137                    if cell.paths == 1 {
8138                        Some(AlignmentBackpointer {
8139                            prev_i: i,
8140                            prev_j: j,
8141                            transition: AlignmentTransition::Insert,
8142                        })
8143                    } else {
8144                        None
8145                    },
8146                );
8147            }
8148
8149            if i < canonical_len
8150                && j < mission_len
8151                && let Some(match_score) =
8152                    task_input_match_score(&canonical_slots[i], &mission_inputs[j])
8153            {
8154                update_alignment_cell(
8155                    &mut table[i + 1][j + 1],
8156                    cell.score + match_score,
8157                    cell.paths,
8158                    if cell.paths == 1 {
8159                        Some(AlignmentBackpointer {
8160                            prev_i: i,
8161                            prev_j: j,
8162                            transition: AlignmentTransition::Match,
8163                        })
8164                    } else {
8165                        None
8166                    },
8167                );
8168            }
8169        }
8170    }
8171
8172    let final_cell = table[canonical_len][mission_len];
8173    if final_cell.paths > 1 {
8174        return Err(CuError::from(format!(
8175            "Task '{task_id}' has ambiguous input alignment while merging mission '{mission_name}'. Existing canonical inputs {} and mission inputs {} admit multiple equally valid alignments.",
8176            format_canonical_input_slots(canonical_slots),
8177            format_mission_task_inputs(mission_inputs),
8178        )));
8179    }
8180
8181    let mut steps = Vec::new();
8182    let (mut i, mut j) = (canonical_len, mission_len);
8183    while i > 0 || j > 0 {
8184        let backpointer = table[i][j].backpointer.unwrap_or_else(|| {
8185            panic!(
8186                "Missing backpointer while aligning task '{task_id}' for mission '{mission_name}'"
8187            )
8188        });
8189
8190        match backpointer.transition {
8191            AlignmentTransition::Match => steps.push(AlignmentStep::Match {
8192                canonical_slot_index: i - 1,
8193                mission_input_index: j - 1,
8194            }),
8195            AlignmentTransition::ExistingGap => steps.push(AlignmentStep::ExistingGap {
8196                canonical_slot_index: i - 1,
8197            }),
8198            AlignmentTransition::Insert => steps.push(AlignmentStep::Insert {
8199                mission_input_index: j - 1,
8200            }),
8201        }
8202
8203        i = backpointer.prev_i;
8204        j = backpointer.prev_j;
8205    }
8206    steps.reverse();
8207    Ok(steps)
8208}
8209
8210fn merge_task_input_layout(
8211    task_id: &str,
8212    layout: &mut TaskInputLayout,
8213    mission_name: String,
8214    mission_inputs: Vec<MissionTaskInput>,
8215) -> CuResult<()> {
8216    let alignment = align_task_inputs(task_id, &mission_name, &layout.slots, &mission_inputs)?;
8217    let mut new_slots = Vec::with_capacity(alignment.len());
8218    let mut old_to_new = vec![None; layout.slots.len()];
8219    let mut mission_mapping = Vec::with_capacity(alignment.len());
8220
8221    for step in alignment {
8222        match step {
8223            AlignmentStep::Match {
8224                canonical_slot_index,
8225                mission_input_index,
8226            } => {
8227                let mut slot = layout.slots[canonical_slot_index].clone();
8228                slot.connection_orders
8229                    .insert(mission_inputs[mission_input_index].connection_order);
8230                let new_index = new_slots.len();
8231                old_to_new[canonical_slot_index] = Some(new_index);
8232                new_slots.push(slot);
8233                mission_mapping.push(Some(mission_input_index));
8234            }
8235            AlignmentStep::ExistingGap {
8236                canonical_slot_index,
8237            } => {
8238                let new_index = new_slots.len();
8239                old_to_new[canonical_slot_index] = Some(new_index);
8240                new_slots.push(layout.slots[canonical_slot_index].clone());
8241                mission_mapping.push(None);
8242            }
8243            AlignmentStep::Insert {
8244                mission_input_index,
8245            } => {
8246                new_slots.push(CanonicalTaskInputSlot {
8247                    msg_type: mission_inputs[mission_input_index].msg_type.clone(),
8248                    connection_orders: BTreeSet::from([
8249                        mission_inputs[mission_input_index].connection_order
8250                    ]),
8251                });
8252                mission_mapping.push(Some(mission_input_index));
8253            }
8254        }
8255    }
8256
8257    let mut remapped_mission_slot_mappings =
8258        HashMap::with_capacity(layout.mission_slot_mappings.len() + 1);
8259    for (existing_mission, existing_mapping) in &layout.mission_slot_mappings {
8260        let mut remapped = vec![None; new_slots.len()];
8261        for (old_slot_index, maybe_local_input_index) in existing_mapping.iter().enumerate() {
8262            let new_slot_index = old_to_new[old_slot_index].unwrap_or_else(|| {
8263                panic!("Missing remap for task '{task_id}' canonical slot {old_slot_index}")
8264            });
8265            remapped[new_slot_index] = *maybe_local_input_index;
8266        }
8267        remapped_mission_slot_mappings.insert(existing_mission.clone(), remapped);
8268    }
8269    remapped_mission_slot_mappings.insert(mission_name, mission_mapping);
8270
8271    layout.slots = new_slots;
8272    layout.mission_slot_mappings = remapped_mission_slot_mappings;
8273    Ok(())
8274}
8275
8276fn collect_task_input_layouts(
8277    all_missions: &[(String, CuGraph)],
8278) -> CuResult<HashMap<String, TaskInputLayout>> {
8279    let mut task_mission_inputs: BTreeMap<String, Vec<(String, Vec<MissionTaskInput>)>> =
8280        BTreeMap::new();
8281
8282    for (mission_name, graph) in all_missions {
8283        for (node_id, node) in graph.get_all_nodes() {
8284            if node.get_flavor() != Flavor::Task {
8285                continue;
8286            }
8287
8288            if find_task_type_for_id(graph, node_id)? == CuTaskType::Source {
8289                continue;
8290            }
8291
8292            let task_id = node.get_id().to_string();
8293            let mission_inputs = collect_mission_task_inputs(graph, node_id, task_id.as_str())?;
8294            task_mission_inputs
8295                .entry(task_id)
8296                .or_default()
8297                .push((mission_name.clone(), mission_inputs));
8298        }
8299    }
8300
8301    let mut layouts = HashMap::new();
8302    for (task_id, mission_inputs) in task_mission_inputs {
8303        let mut mission_iter = mission_inputs.into_iter();
8304        let Some((first_mission, first_inputs)) = mission_iter.next() else {
8305            continue;
8306        };
8307
8308        let slots: Vec<CanonicalTaskInputSlot> = first_inputs
8309            .iter()
8310            .map(|input| CanonicalTaskInputSlot {
8311                msg_type: input.msg_type.clone(),
8312                connection_orders: BTreeSet::from([input.connection_order]),
8313            })
8314            .collect();
8315        let mut mission_slot_mappings = HashMap::new();
8316        mission_slot_mappings.insert(
8317            first_mission,
8318            (0..first_inputs.len()).map(Some).collect::<Vec<_>>(),
8319        );
8320
8321        let mut layout = TaskInputLayout {
8322            slots,
8323            mission_slot_mappings,
8324        };
8325        for (mission_name, mission_inputs) in mission_iter {
8326            merge_task_input_layout(&task_id, &mut layout, mission_name, mission_inputs)?;
8327        }
8328
8329        layouts.insert(task_id, layout);
8330    }
8331
8332    Ok(layouts)
8333}
8334
8335struct GeneratedTaskInput {
8336    setup: proc_macro2::TokenStream,
8337    expr: proc_macro2::TokenStream,
8338}
8339
8340fn present_task_input_expr(
8341    input: &cu29_runtime::curuntime::CuInputMsg,
8342    output_pack_sizes: &[usize],
8343) -> proc_macro2::TokenStream {
8344    let input_index = int2sliceindex(input.culist_index);
8345    let output_size = output_pack_sizes
8346        .get(input.culist_index as usize)
8347        .copied()
8348        .unwrap_or_else(|| {
8349            panic!(
8350                "Missing output pack size for culist index {}",
8351                input.culist_index
8352            )
8353        });
8354    if output_size > 1 {
8355        let port_index = syn::Index::from(input.src_port);
8356        quote! { &msgs.#input_index.#port_index }
8357    } else {
8358        quote! { &msgs.#input_index }
8359    }
8360}
8361
8362fn generate_task_input_binding(
8363    step: &CuExecutionStep,
8364    mission_name: &str,
8365    output_pack_sizes: &[usize],
8366    task_input_layouts: &HashMap<String, TaskInputLayout>,
8367) -> GeneratedTaskInput {
8368    let task_id = step.node.get_id().to_string();
8369    let layout = task_input_layouts
8370        .get(&task_id)
8371        .unwrap_or_else(|| panic!("Missing canonical input layout for task '{task_id}'"));
8372    let slot_mapping = layout
8373        .mission_slot_mappings
8374        .get(mission_name)
8375        .unwrap_or_else(|| {
8376            panic!("Missing input slot mapping for task '{task_id}' in mission '{mission_name}'")
8377        });
8378
8379    let mut setup = Vec::new();
8380    let mut refs = Vec::new();
8381
8382    for (slot_index, slot) in layout.slots.iter().enumerate() {
8383        if let Some(input_index) = slot_mapping.get(slot_index).copied().flatten() {
8384            let input = step.input_msg_indices_types.get(input_index).unwrap_or_else(|| {
8385                panic!(
8386                    "Task '{task_id}' mission '{mission_name}' input slot {slot_index} mapped to missing input index {input_index}"
8387                )
8388            });
8389            refs.push(present_task_input_expr(input, output_pack_sizes));
8390            continue;
8391        }
8392
8393        let empty_input_ident = format_ident!("__cu_missing_input_{slot_index}");
8394        let input_ty: Type = parse_str(slot.msg_type.as_str()).unwrap_or_else(|err| {
8395            panic!(
8396                "Could not parse canonical input message type '{}' for task '{}': {err}",
8397                slot.msg_type, task_id
8398            )
8399        });
8400        setup.push(quote! {
8401            let #empty_input_ident = cu29::cutask::CuMsg::<#input_ty>::new(None);
8402        });
8403        refs.push(quote! { &#empty_input_ident });
8404    }
8405
8406    let expr = match refs.len() {
8407        0 => quote! { &() },
8408        1 => refs
8409            .into_iter()
8410            .next()
8411            .expect("single input expression missing"),
8412        _ => quote! { &( #(#refs),* ) },
8413    };
8414
8415    GeneratedTaskInput {
8416        setup: quote! { #(#setup)* },
8417        expr,
8418    }
8419}
8420
8421/// Builds the tuple of the CuList as a tuple off all the output slots.
8422fn build_culist_tuple(slot_types: &[Type]) -> TypeTuple {
8423    if slot_types.is_empty() {
8424        parse_quote! { () }
8425    } else {
8426        parse_quote! { ( #( #slot_types ),*, ) }
8427    }
8428}
8429
8430/// Builds the canonical generated encoding: one common metadata block followed
8431/// by the payload bytes selected for the local log view.
8432fn build_compressed_culist_tuple_encode(
8433    output_packs: &[OutputPack],
8434    encode_helper_names: &[Option<Ident>],
8435    slot_handle_modes: &[HandleContent],
8436    cumsg_count: usize,
8437    capture_slots: Option<&[bool]>,
8438) -> ItemImpl {
8439    let mut flat_idx = 0usize;
8440    let mut metadata_refs = Vec::with_capacity(cumsg_count);
8441    let mut original_idents = Vec::with_capacity(cumsg_count);
8442    let mut captured_idents = Vec::with_capacity(cumsg_count);
8443    let mut presence_initializers = Vec::with_capacity(cumsg_count);
8444    let mut payload_encoders = Vec::with_capacity(cumsg_count);
8445
8446    for (slot_idx, pack) in output_packs.iter().enumerate() {
8447        let slot_index = syn::Index::from(slot_idx);
8448        let mode = slot_handle_modes.get(slot_idx).copied().unwrap_or_default();
8449        for (port_idx, payload_ty) in pack.msg_types.iter().enumerate() {
8450            let access = if pack.is_multi() {
8451                let port_index = syn::Index::from(port_idx);
8452                quote! { self.0.#slot_index.#port_index }
8453            } else {
8454                quote! { self.0.#slot_index }
8455            };
8456            let original_ident = format_ident!("__cu_original_payload_{flat_idx}");
8457            let captured_ident = format_ident!("__cu_captured_payload_{flat_idx}");
8458            let helper = encode_helper_names[flat_idx].clone();
8459            let cache_index = flat_idx;
8460            flat_idx += 1;
8461
8462            metadata_refs.push(quote! {
8463                ::cu29::copperlist_codec::CommonMetadataRef::new(
8464                    &#access.tov,
8465                    &#access.metadata,
8466                )
8467            });
8468            original_idents.push(original_ident.clone());
8469            captured_idents.push(captured_ident.clone());
8470
8471            let captured_value = if capture_slots.is_some_and(|slots| !slots[slot_idx]) {
8472                quote! { false }
8473            } else if mode == HandleContent::default() {
8474                quote! { #original_ident }
8475            } else {
8476                let mode_u8 = mode as u8;
8477                quote! {
8478                    match #access.payload() {
8479                        Some(__cu_payload) => {
8480                            use ::cu29::pool::PayloadDefaultHandlePolicyApply as _;
8481                            use ::cu29::pool::PayloadDefaultLoggingPolicy as _;
8482                            __cu_payload.apply_handle_content_policy(
8483                                ::cu29::config::HandleContent::from_u8(#mode_u8),
8484                            );
8485                            __cu_payload.payload_should_log()
8486                        }
8487                        None => false,
8488                    }
8489                }
8490            };
8491            let awareness_check = if mode == HandleContent::default() {
8492                quote! {}
8493            } else {
8494                quote! {
8495                    const _: fn() = || {
8496                        fn assert_aware<__T: ::cu29::pool::HandleContentAware + ?::core::marker::Sized>() {}
8497                        assert_aware::<#payload_ty>();
8498                    };
8499                }
8500            };
8501            presence_initializers.push(quote! {
8502                #awareness_check
8503                let #original_ident = #access.payload().is_some();
8504                let #captured_ident = #captured_value;
8505            });
8506
8507            let encode_payload = if let Some(helper) = helper {
8508                quote! { #helper(__cu_payload, encoder)?; }
8509            } else {
8510                quote! {
8511                    ::cu29::copperlist_codec::encode_payload(__cu_payload, encoder)?;
8512                }
8513            };
8514            payload_encoders.push(quote! {
8515                if #captured_ident {
8516                    __cu_capture.select_slot(#cache_index);
8517                    let __cu_payload = #access.payload().ok_or(
8518                        EncodeError::Other("CopperList captured-payload plane mismatch")
8519                    )?;
8520                    #encode_payload
8521                }
8522            });
8523        }
8524    }
8525
8526    let (impl_header, method) = if capture_slots.is_some() {
8527        (
8528            quote! { impl CuStampedDataSet },
8529            format_ident!("__encode_capture"),
8530        )
8531    } else {
8532        (
8533            quote! { impl Encode for CuStampedDataSet },
8534            format_ident!("encode"),
8535        )
8536    };
8537    parse_quote! {
8538        #impl_header {
8539            fn #method<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
8540                let __cu_capture = cu29::monitoring::start_copperlist_io_capture(&self.1);
8541                #(#presence_initializers)*
8542                let __cu_metadata = [#(#metadata_refs),*];
8543                let __cu_original_payload_presence = [#(#original_idents),*];
8544                let __cu_captured_payload_presence = [#(#captured_idents),*];
8545                ::cu29::copperlist_codec::encode_common_metadata::<#cumsg_count, _>(
8546                    &__cu_metadata,
8547                    &__cu_original_payload_presence,
8548                    &__cu_captured_payload_presence,
8549                    encoder,
8550                )?;
8551                #(#payload_encoders)*
8552                Ok(())
8553            }
8554        }
8555    }
8556}
8557
8558fn build_compressed_culist_tuple_decode(
8559    output_packs: &[OutputPack],
8560    slot_types: &[Type],
8561    cumsg_count: usize,
8562    decode_helper_names: &[Option<Ident>],
8563) -> ItemImpl {
8564    let mut flat_idx = 0usize;
8565    let mut decode_fields = Vec::with_capacity(slot_types.len());
8566
8567    for pack in output_packs {
8568        if pack.is_multi() {
8569            let mut fields = Vec::with_capacity(pack.msg_types.len());
8570            for payload_ty in &pack.msg_types {
8571                let metadata_index = flat_idx;
8572                let helper = decode_helper_names[flat_idx].clone();
8573                flat_idx += 1;
8574                let decode_payload = if let Some(helper) = helper {
8575                    quote! { #helper(decoder)? }
8576                } else {
8577                    quote! { ::cu29::copperlist_codec::decode_payload::<#payload_ty, _>(decoder)? }
8578                };
8579                fields.push(quote! {
8580                    {
8581                        let __cu_slot = __cu_metadata.take_slot(#metadata_index);
8582                        let __cu_payload = if __cu_slot.captured_payload_present {
8583                            Some(#decode_payload)
8584                        } else {
8585                            None
8586                        };
8587                        ::cu29::copperlist_codec::restore_msg(__cu_payload, __cu_slot)
8588                    }
8589                });
8590            }
8591            decode_fields.push(quote! { ( #(#fields),* ) });
8592        } else {
8593            let payload_ty = pack
8594                .msg_types
8595                .first()
8596                .expect("single-port pack must have a payload type");
8597            let metadata_index = flat_idx;
8598            let helper = decode_helper_names[flat_idx].clone();
8599            flat_idx += 1;
8600            let decode_payload = if let Some(helper) = helper {
8601                quote! { #helper(decoder)? }
8602            } else {
8603                quote! { ::cu29::copperlist_codec::decode_payload::<#payload_ty, _>(decoder)? }
8604            };
8605            decode_fields.push(quote! {
8606                {
8607                    let __cu_slot = __cu_metadata.take_slot(#metadata_index);
8608                    let __cu_payload = if __cu_slot.captured_payload_present {
8609                        Some(#decode_payload)
8610                    } else {
8611                        None
8612                    };
8613                    ::cu29::copperlist_codec::restore_msg(__cu_payload, __cu_slot)
8614                }
8615            });
8616        }
8617    }
8618
8619    parse_quote! {
8620        impl Decode<()> for CuStampedDataSet {
8621            fn decode<D: Decoder<Context=()>>(decoder: &mut D) -> Result<Self, DecodeError> {
8622                let mut __cu_metadata =
8623                    ::cu29::copperlist_codec::decode_common_metadata::<#cumsg_count, _>(decoder)?;
8624                Ok(CuStampedDataSet(
8625                    (
8626                        #(#decode_fields),*,
8627                    ),
8628                    cu29::monitoring::CuMsgIoCache::<#cumsg_count>::default(),
8629                ))
8630            }
8631        }
8632    }
8633}
8634
8635/// This is the bincode encoding part of the CuStampedDataSet
8636fn build_culist_tuple_encode(
8637    output_packs: &[OutputPack],
8638    encode_helper_names: &[Option<Ident>],
8639    slot_handle_modes: &[HandleContent],
8640) -> ItemImpl {
8641    let mut flat_idx = 0usize;
8642    let mut encode_fields = Vec::new();
8643
8644    for (slot_idx, pack) in output_packs.iter().enumerate() {
8645        let slot_index = syn::Index::from(slot_idx);
8646        let mode = slot_handle_modes.get(slot_idx).copied();
8647
8648        if pack.is_multi() {
8649            for (port_idx, payload_ty) in pack.msg_types.iter().enumerate() {
8650                let port_index = syn::Index::from(port_idx);
8651                let cache_index = flat_idx;
8652                let encode_helper = encode_helper_names[flat_idx].clone();
8653                flat_idx += 1;
8654                let normal_encode = if let Some(helper) = encode_helper {
8655                    quote! { #helper(&self.0.#slot_index.#port_index, encoder)?; }
8656                } else {
8657                    quote! { self.0.#slot_index.#port_index.encode(encoder)?; }
8658                };
8659                let slot_access = quote! { self.0.#slot_index.#port_index };
8660                let slot_block =
8661                    build_per_slot_encode_block(mode, payload_ty, &slot_access, &normal_encode);
8662                encode_fields.push(quote! {
8663                    __cu_capture.select_slot(#cache_index);
8664                    #slot_block
8665                });
8666            }
8667        } else {
8668            let cache_index = flat_idx;
8669            let encode_helper = encode_helper_names[flat_idx].clone();
8670            flat_idx += 1;
8671            let normal_encode = if let Some(helper) = encode_helper {
8672                quote! { #helper(&self.0.#slot_index, encoder)?; }
8673            } else {
8674                quote! { self.0.#slot_index.encode(encoder)?; }
8675            };
8676            let slot_access = quote! { self.0.#slot_index };
8677            let payload_ty = pack
8678                .msg_types
8679                .first()
8680                .expect("single-port pack must have a payload type");
8681            let slot_block =
8682                build_per_slot_encode_block(mode, payload_ty, &slot_access, &normal_encode);
8683            encode_fields.push(quote! {
8684                __cu_capture.select_slot(#cache_index);
8685                #slot_block
8686            });
8687        }
8688    }
8689
8690    parse_quote! {
8691        impl Encode for CuStampedDataSet {
8692            fn encode<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
8693                let __cu_capture = cu29::monitoring::start_copperlist_io_capture(&self.1);
8694                #(#encode_fields)*
8695                Ok(())
8696            }
8697        }
8698    }
8699}
8700
8701/// Build the per-slot encode block. Mode `All` returns the existing encode call
8702/// (zero codegen change). Non-default modes wrap it with a `HandleContentAware`
8703/// bound check on the payload type and an autoref-specialized policy check that
8704/// routes to `encode_metadata_only` when the payload says skip.
8705fn build_per_slot_encode_block(
8706    mode: Option<HandleContent>,
8707    payload_ty: &Type,
8708    slot_access: &proc_macro2::TokenStream,
8709    normal_encode: &proc_macro2::TokenStream,
8710) -> proc_macro2::TokenStream {
8711    let mode = match mode {
8712        Some(m) if m != HandleContent::default() => m,
8713        _ => return normal_encode.clone(),
8714    };
8715    let mode_u8 = mode as u8;
8716    quote! {
8717        {
8718            // Catches the silent-no-op footgun: non-default handle_content on an
8719            // unmarked payload fails here with `HandleContentAware not satisfied`.
8720            const _: fn() = || {
8721                fn assert_aware<__T: ::cu29::pool::HandleContentAware + ?::core::marker::Sized>() {}
8722                assert_aware::<#payload_ty>();
8723            };
8724            use ::cu29::pool::PayloadDefaultHandlePolicyApply as _;
8725            use ::cu29::pool::PayloadDefaultLoggingPolicy as _;
8726            // Stamp the source's configured policy on whatever handles live in the
8727            // payload, then ask the (now-policy-aware) payload whether to log.
8728            let __cu_should_log = match #slot_access.payload() {
8729                Some(__cu_p) => {
8730                    __cu_p.apply_handle_content_policy(
8731                        ::cu29::config::HandleContent::from_u8(#mode_u8),
8732                    );
8733                    __cu_p.payload_should_log()
8734                }
8735                None => false,
8736            };
8737            if __cu_should_log {
8738                #normal_encode
8739            } else {
8740                ::cu29::cutask::encode_metadata_only(&#slot_access, encoder)?;
8741            }
8742        }
8743    }
8744}
8745
8746/// This is the bincode decoding part of the CuStampedDataSet
8747fn build_culist_tuple_decode(
8748    output_packs: &[OutputPack],
8749    slot_types: &[Type],
8750    cumsg_count: usize,
8751    decode_helper_names: &[Option<Ident>],
8752) -> ItemImpl {
8753    let mut flat_idx = 0usize;
8754    let mut decode_fields = Vec::with_capacity(slot_types.len());
8755    for (slot_idx, pack) in output_packs.iter().enumerate() {
8756        let slot_type = &slot_types[slot_idx];
8757        if pack.is_multi() {
8758            let mut slot_fields = Vec::with_capacity(pack.msg_types.len());
8759            for _ in 0..pack.msg_types.len() {
8760                let decode_helper = decode_helper_names[flat_idx].clone();
8761                flat_idx += 1;
8762                if let Some(decode_helper) = decode_helper {
8763                    slot_fields.push(quote! { #decode_helper(decoder)? });
8764                } else {
8765                    let msg_type = &pack.msg_types[slot_fields.len()];
8766                    slot_fields.push(quote! { <CuMsg<#msg_type> as Decode<()>>::decode(decoder)? });
8767                }
8768            }
8769            decode_fields.push(quote! { ( #(#slot_fields),* ) });
8770        } else if let Some(decode_helper) = decode_helper_names[flat_idx].clone() {
8771            flat_idx += 1;
8772            decode_fields.push(quote! { #decode_helper(decoder)? });
8773        } else {
8774            flat_idx += 1;
8775            decode_fields.push(quote! { <#slot_type as Decode<()>>::decode(decoder)? });
8776        }
8777    }
8778
8779    parse_quote! {
8780        impl Decode<()> for CuStampedDataSet {
8781            fn decode<D: Decoder<Context=()>>(decoder: &mut D) -> Result<Self, DecodeError> {
8782                Ok(CuStampedDataSet(
8783                    (
8784                        #(#decode_fields),*,
8785                    ),
8786                    cu29::monitoring::CuMsgIoCache::<#cumsg_count>::default(),
8787                ))
8788            }
8789        }
8790    }
8791}
8792
8793fn build_culist_erasedcumsgs(output_packs: &[OutputPack]) -> ItemImpl {
8794    let mut casted_fields: Vec<proc_macro2::TokenStream> = Vec::new();
8795    for (idx, pack) in output_packs.iter().enumerate() {
8796        let slot_index = syn::Index::from(idx);
8797        if pack.is_multi() {
8798            for port_idx in 0..pack.msg_types.len() {
8799                let port_index = syn::Index::from(port_idx);
8800                casted_fields.push(quote! {
8801                    &self.0.#slot_index.#port_index as &dyn ErasedCuStampedData
8802                });
8803            }
8804        } else {
8805            casted_fields.push(quote! { &self.0.#slot_index as &dyn ErasedCuStampedData });
8806        }
8807    }
8808    parse_quote! {
8809        impl ErasedCuStampedDataSet for CuStampedDataSet {
8810            fn cumsgs(&self) -> Vec<&dyn ErasedCuStampedData> {
8811                vec![
8812                    #(#casted_fields),*
8813                ]
8814            }
8815        }
8816    }
8817}
8818
8819fn build_culist_tuple_debug(slot_types: &[Type]) -> ItemImpl {
8820    let indices: Vec<usize> = (0..slot_types.len()).collect();
8821
8822    let debug_fields: Vec<_> = indices
8823        .iter()
8824        .map(|i| {
8825            let idx = syn::Index::from(*i);
8826            quote! { .field(&self.0.#idx) }
8827        })
8828        .collect();
8829
8830    parse_quote! {
8831        impl Debug for CuStampedDataSet {
8832            fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
8833                f.debug_tuple("CuStampedDataSet")
8834                    #(#debug_fields)*
8835                    .finish()
8836            }
8837        }
8838    }
8839}
8840
8841/// This is the serde serialization part of the CuStampedDataSet
8842fn build_culist_tuple_serialize(slot_types: &[Type]) -> ItemImpl {
8843    let indices: Vec<usize> = (0..slot_types.len()).collect();
8844    let tuple_len = slot_types.len();
8845
8846    // Generate the serialization for each tuple field
8847    let serialize_fields: Vec<_> = indices
8848        .iter()
8849        .map(|i| {
8850            let idx = syn::Index::from(*i);
8851            quote! { &self.0.#idx }
8852        })
8853        .collect();
8854
8855    parse_quote! {
8856        impl cu29::serde::ser::Serialize for CuStampedDataSet {
8857            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
8858            where
8859                S: cu29::serde::Serializer,
8860            {
8861                use cu29::serde::ser::SerializeTuple;
8862                let mut tuple = serializer.serialize_tuple(#tuple_len)?;
8863                #(tuple.serialize_element(#serialize_fields)?;)*
8864                tuple.end()
8865            }
8866        }
8867    }
8868}
8869
8870/// This is the default implementation for CuStampedDataSet
8871fn build_culist_tuple_default(slot_types: &[Type], cumsg_count: usize) -> ItemImpl {
8872    let default_fields: Vec<_> = slot_types
8873        .iter()
8874        .map(|slot_type| quote! { <#slot_type as Default>::default() })
8875        .collect();
8876
8877    parse_quote! {
8878        impl Default for CuStampedDataSet {
8879            fn default() -> CuStampedDataSet
8880            {
8881                CuStampedDataSet(
8882                    (
8883                        #(#default_fields),*,
8884                    ),
8885                    cu29::monitoring::CuMsgIoCache::<#cumsg_count>::default(),
8886                )
8887            }
8888        }
8889    }
8890}
8891
8892fn collect_bridge_channel_usage(graph: &CuGraph) -> HashMap<BridgeChannelKey, String> {
8893    let mut usage = HashMap::new();
8894    for cnx in graph.edges() {
8895        if let Some(channel) = &cnx.src_channel {
8896            let key = BridgeChannelKey {
8897                bridge_id: cnx.src.clone(),
8898                channel_id: channel.clone(),
8899                direction: BridgeChannelDirection::Rx,
8900            };
8901            usage
8902                .entry(key)
8903                .and_modify(|msg| {
8904                    if msg != &cnx.msg {
8905                        panic!(
8906                            "Bridge '{}' channel '{}' is used with incompatible message types: {} vs {}",
8907                            cnx.src, channel, msg, cnx.msg
8908                        );
8909                    }
8910                })
8911                .or_insert(cnx.msg.clone());
8912        }
8913        if let Some(channel) = &cnx.dst_channel {
8914            let key = BridgeChannelKey {
8915                bridge_id: cnx.dst.clone(),
8916                channel_id: channel.clone(),
8917                direction: BridgeChannelDirection::Tx,
8918            };
8919            usage
8920                .entry(key)
8921                .and_modify(|msg| {
8922                    if msg != &cnx.msg {
8923                        panic!(
8924                            "Bridge '{}' channel '{}' is used with incompatible message types: {} vs {}",
8925                            cnx.dst, channel, msg, cnx.msg
8926                        );
8927                    }
8928                })
8929                .or_insert(cnx.msg.clone());
8930        }
8931    }
8932    usage
8933}
8934
8935fn build_bridge_specs(
8936    config: &CuConfig,
8937    graph: &CuGraph,
8938    channel_usage: &HashMap<BridgeChannelKey, String>,
8939) -> Vec<BridgeSpec> {
8940    let mut specs = Vec::new();
8941    for (bridge_index, bridge_cfg) in config.bridges.iter().enumerate() {
8942        if graph.get_node_id_by_name(bridge_cfg.id.as_str()).is_none() {
8943            continue;
8944        }
8945
8946        let type_path = parse_str::<Type>(bridge_cfg.type_.as_str()).unwrap_or_else(|err| {
8947            panic!(
8948                "Could not parse bridge type '{}' for '{}': {err}",
8949                bridge_cfg.type_, bridge_cfg.id
8950            )
8951        });
8952
8953        let mut rx_channels = Vec::new();
8954        let mut tx_channels = Vec::new();
8955
8956        for (channel_index, channel) in bridge_cfg.channels.iter().enumerate() {
8957            match channel {
8958                BridgeChannelConfigRepresentation::Rx { id, .. } => {
8959                    let key = BridgeChannelKey {
8960                        bridge_id: bridge_cfg.id.clone(),
8961                        channel_id: id.clone(),
8962                        direction: BridgeChannelDirection::Rx,
8963                    };
8964                    if let Some(msg_type) = channel_usage.get(&key) {
8965                        let msg_type_name = msg_type.clone();
8966                        let msg_type = parse_str::<Type>(msg_type).unwrap_or_else(|err| {
8967                            panic!(
8968                                "Could not parse message type '{msg_type}' for bridge '{}' channel '{}': {err}",
8969                                bridge_cfg.id, id
8970                            )
8971                        });
8972                        let const_ident =
8973                            Ident::new(&config_id_to_bridge_const(id.as_str()), Span::call_site());
8974                        rx_channels.push(BridgeChannelSpec {
8975                            id: id.clone(),
8976                            const_ident,
8977                            msg_type,
8978                            msg_type_name,
8979                            config_index: channel_index,
8980                            plan_node_id: None,
8981                            culist_index: None,
8982                            monitor_index: None,
8983                        });
8984                    }
8985                }
8986                BridgeChannelConfigRepresentation::Tx { id, .. } => {
8987                    let key = BridgeChannelKey {
8988                        bridge_id: bridge_cfg.id.clone(),
8989                        channel_id: id.clone(),
8990                        direction: BridgeChannelDirection::Tx,
8991                    };
8992                    if let Some(msg_type) = channel_usage.get(&key) {
8993                        let msg_type_name = msg_type.clone();
8994                        let msg_type = parse_str::<Type>(msg_type).unwrap_or_else(|err| {
8995                            panic!(
8996                                "Could not parse message type '{msg_type}' for bridge '{}' channel '{}': {err}",
8997                                bridge_cfg.id, id
8998                            )
8999                        });
9000                        let const_ident =
9001                            Ident::new(&config_id_to_bridge_const(id.as_str()), Span::call_site());
9002                        tx_channels.push(BridgeChannelSpec {
9003                            id: id.clone(),
9004                            const_ident,
9005                            msg_type,
9006                            msg_type_name,
9007                            config_index: channel_index,
9008                            plan_node_id: None,
9009                            culist_index: None,
9010                            monitor_index: None,
9011                        });
9012                    }
9013                }
9014            }
9015        }
9016
9017        if rx_channels.is_empty() && tx_channels.is_empty() {
9018            continue;
9019        }
9020
9021        specs.push(BridgeSpec {
9022            id: bridge_cfg.id.clone(),
9023            type_path,
9024            run_in_sim: bridge_cfg.is_run_in_sim(),
9025            config_index: bridge_index,
9026            tuple_index: 0,
9027            monitor_index: None,
9028            rx_channels,
9029            tx_channels,
9030        });
9031    }
9032
9033    for (tuple_index, spec) in specs.iter_mut().enumerate() {
9034        spec.tuple_index = tuple_index;
9035    }
9036
9037    specs
9038}
9039
9040fn collect_task_names(graph: &CuGraph) -> Vec<(NodeId, String, String)> {
9041    graph
9042        .get_all_nodes()
9043        .iter()
9044        .filter(|(_, node)| node.get_flavor() == Flavor::Task)
9045        .map(|(node_id, node)| {
9046            (
9047                *node_id,
9048                node.get_id().to_string(),
9049                config_id_to_struct_member(node.get_id().as_str()),
9050            )
9051        })
9052        .collect()
9053}
9054
9055#[derive(Clone, Copy)]
9056enum ResourceOwner {
9057    Task(usize),
9058    Bridge(usize),
9059}
9060
9061#[derive(Clone)]
9062struct ResourceKeySpec {
9063    bundle_index: usize,
9064    provider_path: syn::Path,
9065    resource_name: String,
9066    binding_name: String,
9067    owner: ResourceOwner,
9068}
9069
9070fn parse_resource_path(path: &str) -> CuResult<(String, String)> {
9071    let (bundle_id, name) = path.split_once('.').ok_or_else(|| {
9072        CuError::from(format!(
9073            "Resource '{path}' is missing a bundle prefix (expected bundle.resource)"
9074        ))
9075    })?;
9076
9077    if bundle_id.is_empty() || name.is_empty() {
9078        return Err(CuError::from(format!(
9079            "Resource '{path}' must use the 'bundle.resource' format"
9080        )));
9081    }
9082
9083    Ok((bundle_id.to_string(), name.to_string()))
9084}
9085
9086fn collect_resource_specs(
9087    graph: &CuGraph,
9088    task_specs: &CuTaskSpecSet,
9089    bridge_specs: &[BridgeSpec],
9090    bundle_specs: &[BundleSpec],
9091) -> CuResult<Vec<ResourceKeySpec>> {
9092    let mut bridge_lookup: BTreeMap<String, usize> = BTreeMap::new();
9093    for (idx, spec) in bridge_specs.iter().enumerate() {
9094        bridge_lookup.insert(spec.id.clone(), idx);
9095    }
9096
9097    let mut bundle_lookup: HashMap<String, (usize, syn::Path)> = HashMap::new();
9098    for (index, bundle) in bundle_specs.iter().enumerate() {
9099        bundle_lookup.insert(bundle.id.clone(), (index, bundle.provider_path.clone()));
9100    }
9101
9102    let mut specs = Vec::new();
9103
9104    for (node_id, node) in graph.get_all_nodes() {
9105        let resources = node.get_resources();
9106        if let Some(resources) = resources {
9107            let task_index = task_specs.node_id_to_task_index[node_id as usize];
9108            let owner = if let Some(task_index) = task_index {
9109                ResourceOwner::Task(task_index)
9110            } else if node.get_flavor() == Flavor::Bridge {
9111                let bridge_index = bridge_lookup.get(&node.get_id()).ok_or_else(|| {
9112                    CuError::from(format!(
9113                        "Resource mapping attached to unknown bridge node '{}'",
9114                        node.get_id()
9115                    ))
9116                })?;
9117                ResourceOwner::Bridge(*bridge_index)
9118            } else {
9119                return Err(CuError::from(format!(
9120                    "Resource mapping attached to non-task node '{}'",
9121                    node.get_id()
9122                )));
9123            };
9124
9125            for (binding_name, path) in resources {
9126                let (bundle_id, resource_name) = parse_resource_path(path)?;
9127                let (bundle_index, provider_path) =
9128                    bundle_lookup.get(&bundle_id).ok_or_else(|| {
9129                        CuError::from(format!(
9130                            "Resource '{}' references unknown bundle '{}'",
9131                            path, bundle_id
9132                        ))
9133                    })?;
9134                specs.push(ResourceKeySpec {
9135                    bundle_index: *bundle_index,
9136                    provider_path: provider_path.clone(),
9137                    resource_name,
9138                    binding_name: binding_name.clone(),
9139                    owner,
9140                });
9141            }
9142        }
9143    }
9144
9145    Ok(specs)
9146}
9147
9148fn build_bundle_list<'a>(config: &'a CuConfig, mission: &str) -> Vec<&'a ResourceBundleConfig> {
9149    config
9150        .resources
9151        .iter()
9152        .filter(|bundle| {
9153            bundle
9154                .missions
9155                .as_ref()
9156                .is_none_or(|missions| missions.iter().any(|m| m == mission))
9157        })
9158        .collect()
9159}
9160
9161struct BundleSpec {
9162    inputs: Vec<(String, String)>,
9163    id: String,
9164    provider_path: syn::Path,
9165}
9166
9167struct LogStreamResourceSpec {
9168    destination_index: usize,
9169    stream: LogStreamEndpointSpec,
9170    feedback: Option<LogStreamEndpointSpec>,
9171}
9172
9173struct LogStreamEndpointSpec {
9174    bundle_index: usize,
9175    provider_path: syn::Path,
9176    resource_name: String,
9177    transport_type: syn::Type,
9178}
9179
9180fn build_logstream_resource_specs(
9181    config: &CuConfig,
9182    mission: &str,
9183    component_resources: &[ResourceKeySpec],
9184) -> CuResult<Vec<LogStreamResourceSpec>> {
9185    let Some(streaming) = &config.log_streaming else {
9186        return Ok(Vec::new());
9187    };
9188    let bundle_specs = build_bundle_specs(config, mission)?;
9189    let bundle_lookup: HashMap<_, _> = bundle_specs
9190        .iter()
9191        .enumerate()
9192        .map(|(index, bundle)| (bundle.id.as_str(), (index, &bundle.provider_path)))
9193        .collect();
9194    let endpoint = |destination_id: &str,
9195                    transport: &cu29_runtime::config::LogStreamTransportConfig|
9196     -> CuResult<LogStreamEndpointSpec> {
9197        let (bundle_id, resource_name) = parse_resource_path(&transport.resource)?;
9198        let (bundle_index, provider_path) = bundle_lookup.get(bundle_id.as_str()).ok_or_else(|| CuError::from(format!(
9199            "Log-stream destination '{destination_id}' references resource bundle '{bundle_id}' which is not active in mission '{mission}'"
9200        )))?;
9201        if component_resources.iter().any(|resource| {
9202            resource.bundle_index == *bundle_index && resource.resource_name == resource_name
9203        }) {
9204            return Err(CuError::from(format!(
9205                "Log-stream destination '{destination_id}' and a task or bridge both require exclusive resource '{}'",
9206                transport.resource
9207            )));
9208        }
9209        if bundle_specs.iter().any(|bundle| {
9210            bundle
9211                .inputs
9212                .iter()
9213                .any(|(_, path)| path == &transport.resource)
9214        }) {
9215            return Err(CuError::from(format!(
9216                "Log-stream destination '{destination_id}' and a resource bundle both require exclusive resource '{}'",
9217                transport.resource
9218            )));
9219        }
9220        let transport_type = parse_str::<Type>(&transport.type_).map_err(|error| CuError::from(format!(
9221            "Log-stream destination '{destination_id}' transport type '{}' is not a valid Rust type: {error}", transport.type_
9222        )))?;
9223        Ok(LogStreamEndpointSpec {
9224            bundle_index: *bundle_index,
9225            provider_path: (*provider_path).clone(),
9226            resource_name,
9227            transport_type,
9228        })
9229    };
9230    streaming
9231        .destinations
9232        .iter()
9233        .enumerate()
9234        .map(|(destination_index, destination)| {
9235            Ok(LogStreamResourceSpec {
9236                destination_index,
9237                stream: endpoint(&destination.id, &destination.transport)?,
9238                feedback: destination
9239                    .feedback
9240                    .as_ref()
9241                    .map(|f| endpoint(&destination.id, &f.transport))
9242                    .transpose()?,
9243            })
9244        })
9245        .collect()
9246}
9247
9248fn build_bundle_specs(config: &CuConfig, mission: &str) -> CuResult<Vec<BundleSpec>> {
9249    build_bundle_list(config, mission)
9250        .into_iter()
9251        .map(|bundle| {
9252            let provider_path: syn::Path =
9253                syn::parse_str(bundle.provider.as_str()).map_err(|err| {
9254                    CuError::from(format!(
9255                        "Failed to parse provider path '{}' for bundle '{}': {err}",
9256                        bundle.provider, bundle.id
9257                    ))
9258                })?;
9259            let mut inputs: Vec<_> = bundle
9260                .resources
9261                .iter()
9262                .flat_map(|map| map.iter())
9263                .map(|(name, path)| (name.clone(), path.clone()))
9264                .collect();
9265            inputs.sort();
9266            Ok(BundleSpec {
9267                inputs,
9268                id: bundle.id.clone(),
9269                provider_path,
9270            })
9271        })
9272        .collect()
9273}
9274
9275/// Compute construction order while preserving declaration-based resource keys.
9276fn resource_bundle_build_order(bundles: &[BundleSpec]) -> CuResult<Vec<usize>> {
9277    let mut lookup = HashMap::new();
9278    for (index, bundle) in bundles.iter().enumerate() {
9279        if lookup.insert(bundle.id.as_str(), index).is_some() {
9280            return Err(CuError::from(format!(
9281                "Duplicate resource bundle '{}'",
9282                bundle.id
9283            )));
9284        }
9285    }
9286    let dependencies = bundles.iter().map(|bundle| {
9287        bundle.inputs.iter().map(|(_, path)| {
9288            let (source, _) = parse_resource_path(path)?;
9289            lookup.get(source.as_str()).copied().ok_or_else(|| CuError::from(format!(
9290                "Resource bundle '{}' depends on '{}' which is not active in this mission", bundle.id, source
9291            )))
9292        }).collect::<CuResult<Vec<_>>>()
9293    }).collect::<CuResult<Vec<_>>>()?;
9294    let mut built = vec![false; bundles.len()];
9295    let mut order = Vec::with_capacity(bundles.len());
9296    while order.len() < bundles.len() {
9297        let Some(index) = (0..bundles.len()).find(|&index| {
9298            !built[index]
9299                && dependencies[index]
9300                    .iter()
9301                    .all(|&dependency| built[dependency])
9302        }) else {
9303            let cycle = bundles
9304                .iter()
9305                .enumerate()
9306                .filter(|(index, _)| !built[*index])
9307                .map(|(_, bundle)| bundle.id.as_str())
9308                .collect::<Vec<_>>()
9309                .join(", ");
9310            return Err(CuError::from(format!(
9311                "Resource dependency cycle involving: {cycle}"
9312            )));
9313        };
9314        built[index] = true;
9315        order.push(index);
9316    }
9317    Ok(order)
9318}
9319
9320#[cfg(test)]
9321mod resource_stack_tests {
9322    use super::*;
9323    type InputSpecs<'a> = &'a [(&'a str, &'a str)];
9324    fn specs(entries: &[(&str, InputSpecs<'_>)]) -> Vec<BundleSpec> {
9325        entries
9326            .iter()
9327            .map(|(id, inputs)| BundleSpec {
9328                id: (*id).into(),
9329                provider_path: syn::parse_str("Provider").unwrap(),
9330                inputs: inputs
9331                    .iter()
9332                    .map(|(name, path)| ((*name).into(), (*path).into()))
9333                    .collect(),
9334            })
9335            .collect()
9336    }
9337    #[test]
9338    fn resource_dependencies_validate_and_keep_indices() {
9339        assert_eq!(
9340            resource_bundle_build_order(&specs(&[
9341                ("stream", &[("serial", "radio.serial")]),
9342                ("board", &[]),
9343                ("radio", &[("serial", "board.uart")]),
9344            ]))
9345            .unwrap(),
9346            [1, 2, 0]
9347        );
9348        for invalid in [
9349            specs(&[("a", &[("x", "a.x")])]),
9350            specs(&[("a", &[("x", "b.x")]), ("b", &[("x", "a.x")])]),
9351            specs(&[("a", &[("x", "inactive.x")])]),
9352            specs(&[("a", &[]), ("a", &[])]),
9353        ] {
9354            assert!(resource_bundle_build_order(&invalid).is_err());
9355        }
9356    }
9357}
9358
9359fn build_resources_module(
9360    bundle_specs: &[BundleSpec],
9361) -> CuResult<(proc_macro2::TokenStream, proc_macro2::TokenStream)> {
9362    let bundle_consts = bundle_specs.iter().enumerate().map(|(index, bundle)| {
9363        let const_ident = Ident::new(
9364            &config_id_to_bridge_const(bundle.id.as_str()),
9365            Span::call_site(),
9366        );
9367        quote! { pub const #const_ident: BundleIndex = BundleIndex::new(#index); }
9368    });
9369
9370    let resources_module = quote! {
9371        pub mod resources {
9372            #![allow(dead_code)]
9373            use cu29::resource::BundleIndex;
9374
9375            pub mod bundles {
9376                use super::BundleIndex;
9377                #(#bundle_consts)*
9378            }
9379        }
9380    };
9381
9382    let bundle_counts = bundle_specs.iter().map(|bundle| {
9383        let provider_path = &bundle.provider_path;
9384        quote! { <#provider_path as cu29::resource::ResourceBundleDecl>::Id::COUNT }
9385    });
9386
9387    let order = resource_bundle_build_order(bundle_specs)?;
9388    let mut validations = Vec::new();
9389    let mut bundle_inits = Vec::new();
9390    for index in order {
9391        let bundle = &bundle_specs[index];
9392        let bundle_id = LitStr::new(&bundle.id, Span::call_site());
9393        let provider_path = &bundle.provider_path;
9394        let input_count = bundle.inputs.len();
9395        let checks = bundle.inputs.iter().map(|(name, path)| {
9396            quote! {
9397                bundle_cfg.resources.as_ref().and_then(|map| map.get(#name))
9398                    .is_some_and(|value| value == #path)
9399            }
9400        });
9401        validations.push(quote! {
9402            let bundle_cfg = config.resources.iter().find(|b| b.id == #bundle_id)
9403                .ok_or_else(|| cu29::CuError::from(concat!("Missing resource bundle: ", #bundle_id)))?;
9404            if bundle_cfg.resources.as_ref().map_or(0, |map| map.len()) != #input_count
9405                #(|| !(#checks))* {
9406                return Err(cu29::CuError::from(concat!("Resource input topology differs from compiled configuration: ", #bundle_id)));
9407            }
9408        });
9409        let mut entries = Vec::new();
9410        for (name, path) in &bundle.inputs {
9411            let (source_id, slot) = parse_resource_path(path)?;
9412            let source_index = bundle_specs
9413                .iter()
9414                .position(|source| source.id == source_id)
9415                .expect("validated resource dependency");
9416            let source_provider = &bundle_specs[source_index].provider_path;
9417            entries.push(quote! {
9418                (#name, cu29::resource::ResourceKey::new(
9419                    cu29::resource::BundleIndex::new(#source_index),
9420                    cu29::resource::resource_index_by_name::<#source_provider>(#slot),
9421                ))
9422            });
9423        }
9424        bundle_inits.push(quote! {
9425            {
9426                const INPUT_KEYS: [cu29::resource::ResourceKey;
9427                    <#provider_path as cu29::resource::ResourceBundle>::INPUT_NAMES.len()] =
9428                    cu29::resource::resource_input_keys(
9429                        <#provider_path as cu29::resource::ResourceBundle>::INPUT_NAMES,
9430                        &[#(#entries),*],
9431                    );
9432                let bundle_cfg = config.resources.iter().find(|b| b.id == #bundle_id)
9433                    .expect("validated resource bundle");
9434                let bundle_ctx = cu29::resource::BundleContext::<#provider_path>::new(
9435                    cu29::resource::BundleIndex::new(#index), #bundle_id,
9436                ).with_input_keys(&INPUT_KEYS);
9437                <#provider_path as cu29::resource::ResourceBundle>::build(
9438                    bundle_ctx, bundle_cfg.config.as_ref(), &mut manager,
9439                )?;
9440            }
9441        });
9442    }
9443
9444    let resources_instanciator = quote! {
9445        pub fn resources_instanciator(config: &CuConfig) -> CuResult<cu29::resource::ResourceManager> {
9446            #(#validations)*
9447            let bundle_counts: &[usize] = &[ #(#bundle_counts),* ];
9448            let mut manager = cu29::resource::ResourceManager::new(bundle_counts);
9449            #(#bundle_inits)*
9450            Ok(manager)
9451        }
9452    };
9453
9454    Ok((resources_module, resources_instanciator))
9455}
9456
9457struct ResourceMappingTokens {
9458    defs: proc_macro2::TokenStream,
9459    refs: Vec<proc_macro2::TokenStream>,
9460}
9461
9462fn build_task_resource_mappings(
9463    resource_specs: &[ResourceKeySpec],
9464    task_specs: &CuTaskSpecSet,
9465    sim_mode: bool,
9466) -> CuResult<ResourceMappingTokens> {
9467    let mut per_task: Vec<Vec<&ResourceKeySpec>> = vec![Vec::new(); task_specs.ids.len()];
9468
9469    for spec in resource_specs {
9470        let ResourceOwner::Task(task_index) = spec.owner else {
9471            continue;
9472        };
9473        if sim_mode
9474            && !task_specs.run_in_sim_flags[task_index]
9475            && task_specs.cutypes[task_index] != CuTaskType::Regular
9476        {
9477            continue;
9478        }
9479        per_task
9480            .get_mut(task_index)
9481            .ok_or_else(|| {
9482                CuError::from(format!(
9483                    "Resource '{}' mapped to invalid task index {}",
9484                    spec.binding_name, task_index
9485                ))
9486            })?
9487            .push(spec);
9488    }
9489
9490    let mut mapping_defs = Vec::new();
9491    let mut mapping_refs = Vec::new();
9492
9493    for (idx, entries) in per_task.iter().enumerate() {
9494        if entries.is_empty() {
9495            mapping_refs.push(quote! { None });
9496            continue;
9497        }
9498
9499        // A backgrounded task binds the resources of what the wrapper wraps —
9500        // the runner for an anytime node, the task itself otherwise.
9501        let binding_task_type = &task_specs.async_inner_task_types[idx];
9502
9503        let binding_trait = task_trait_for_specs(task_specs, idx);
9504
9505        let entries_ident = format_ident!("TASK{}_RES_ENTRIES", idx);
9506        let map_ident = format_ident!("TASK{}_RES_MAPPING", idx);
9507        let binding_type = quote! {
9508            <<#binding_task_type as #binding_trait>::Resources<'_> as ResourceBindings>::Binding
9509        };
9510        let entry_tokens = entries.iter().map(|spec| {
9511            let binding_ident = Ident::new(
9512                &config_id_to_enum(spec.binding_name.as_str()),
9513                Span::call_site(),
9514            );
9515            let resource_name = LitStr::new(spec.resource_name.as_str(), Span::call_site());
9516            let bundle_index = spec.bundle_index;
9517            let provider_path = &spec.provider_path;
9518            quote! {
9519                (#binding_type::#binding_ident, cu29::resource::ResourceKey::new(
9520                    cu29::resource::BundleIndex::new(#bundle_index),
9521                    cu29::resource::resource_index_by_name::<#provider_path>(#resource_name),
9522                ))
9523            }
9524        });
9525
9526        mapping_defs.push(quote! {
9527            const #entries_ident: &[(#binding_type, cu29::resource::ResourceKey)] = &[ #(#entry_tokens),* ];
9528            const #map_ident: cu29::resource::ResourceBindingMap<#binding_type> =
9529                cu29::resource::ResourceBindingMap::new(#entries_ident);
9530        });
9531        mapping_refs.push(quote! { Some(&#map_ident) });
9532    }
9533
9534    Ok(ResourceMappingTokens {
9535        defs: quote! { #(#mapping_defs)* },
9536        refs: mapping_refs,
9537    })
9538}
9539
9540fn build_bridge_resource_mappings(
9541    resource_specs: &[ResourceKeySpec],
9542    bridge_specs: &[BridgeSpec],
9543    sim_mode: bool,
9544) -> ResourceMappingTokens {
9545    let mut per_bridge: Vec<Vec<&ResourceKeySpec>> = vec![Vec::new(); bridge_specs.len()];
9546
9547    for spec in resource_specs {
9548        let ResourceOwner::Bridge(bridge_index) = spec.owner else {
9549            continue;
9550        };
9551        if sim_mode && !bridge_specs[bridge_index].run_in_sim {
9552            continue;
9553        }
9554        per_bridge[bridge_index].push(spec);
9555    }
9556
9557    let mut mapping_defs = Vec::new();
9558    let mut mapping_refs = Vec::new();
9559
9560    for (idx, entries) in per_bridge.iter().enumerate() {
9561        if entries.is_empty() {
9562            mapping_refs.push(quote! { None });
9563            continue;
9564        }
9565
9566        let bridge_type = &bridge_specs[idx].type_path;
9567        let binding_type = quote! {
9568            <<#bridge_type as cu29::cubridge::CuBridge>::Resources<'_> as ResourceBindings>::Binding
9569        };
9570        let entries_ident = format_ident!("BRIDGE{}_RES_ENTRIES", idx);
9571        let map_ident = format_ident!("BRIDGE{}_RES_MAPPING", idx);
9572        let entry_tokens = entries.iter().map(|spec| {
9573            let binding_ident = Ident::new(
9574                &config_id_to_enum(spec.binding_name.as_str()),
9575                Span::call_site(),
9576            );
9577            let resource_name = LitStr::new(spec.resource_name.as_str(), Span::call_site());
9578            let bundle_index = spec.bundle_index;
9579            let provider_path = &spec.provider_path;
9580            quote! {
9581                (#binding_type::#binding_ident, cu29::resource::ResourceKey::new(
9582                    cu29::resource::BundleIndex::new(#bundle_index),
9583                    cu29::resource::resource_index_by_name::<#provider_path>(#resource_name),
9584                ))
9585            }
9586        });
9587
9588        mapping_defs.push(quote! {
9589            const #entries_ident: &[(#binding_type, cu29::resource::ResourceKey)] = &[ #(#entry_tokens),* ];
9590            const #map_ident: cu29::resource::ResourceBindingMap<#binding_type> =
9591                cu29::resource::ResourceBindingMap::new(#entries_ident);
9592        });
9593        mapping_refs.push(quote! { Some(&#map_ident) });
9594    }
9595
9596    ResourceMappingTokens {
9597        defs: quote! { #(#mapping_defs)* },
9598        refs: mapping_refs,
9599    }
9600}
9601
9602fn build_execution_plan(
9603    config: &CuConfig,
9604    graph: &CuGraph,
9605    mission: &str,
9606    bridge_specs: &mut [BridgeSpec],
9607) -> CuResult<(
9608    CuExecutionLoop,
9609    Vec<ExecutionEntity>,
9610    HashMap<NodeId, NodeId>,
9611)> {
9612    let assembled = match config.planner_resolved_order(mission) {
9613        Some(step_keys) => assemble_runtime_plan_from_step_keys(config, graph, step_keys)?,
9614        None => assemble_runtime_plan(config, graph)?,
9615    };
9616    let mut exec_entities = Vec::with_capacity(assembled.entities.len());
9617    for (plan_node_id, entity) in assembled.entities.iter().enumerate() {
9618        let kind = match entity.kind {
9619            PlanEntityKind::Task { task_index, .. } => ExecutionEntityKind::Task { task_index },
9620            PlanEntityKind::BridgeRx {
9621                bridge_config_index,
9622                channel_config_index,
9623            } => {
9624                let bridge_index = bridge_specs
9625                    .iter()
9626                    .position(|bridge| bridge.config_index == bridge_config_index)
9627                    .expect("shared planner returned an unknown bridge");
9628                let channel_index = bridge_specs[bridge_index]
9629                    .rx_channels
9630                    .iter()
9631                    .position(|channel| channel.config_index == channel_config_index)
9632                    .expect("shared planner returned an unknown bridge rx channel");
9633                bridge_specs[bridge_index].rx_channels[channel_index].plan_node_id =
9634                    Some(plan_node_id as NodeId);
9635                ExecutionEntityKind::BridgeRx {
9636                    bridge_index,
9637                    channel_index,
9638                }
9639            }
9640            PlanEntityKind::BridgeTx {
9641                bridge_config_index,
9642                channel_config_index,
9643            } => {
9644                let bridge_index = bridge_specs
9645                    .iter()
9646                    .position(|bridge| bridge.config_index == bridge_config_index)
9647                    .expect("shared planner returned an unknown bridge");
9648                let channel_index = bridge_specs[bridge_index]
9649                    .tx_channels
9650                    .iter()
9651                    .position(|channel| channel.config_index == channel_config_index)
9652                    .expect("shared planner returned an unknown bridge tx channel");
9653                bridge_specs[bridge_index].tx_channels[channel_index].plan_node_id =
9654                    Some(plan_node_id as NodeId);
9655                ExecutionEntityKind::BridgeTx {
9656                    bridge_index,
9657                    channel_index,
9658                }
9659            }
9660        };
9661        exec_entities.push(ExecutionEntity { kind });
9662    }
9663    let plan_to_original = assembled
9664        .plan_to_original
9665        .iter()
9666        .enumerate()
9667        .filter_map(|(plan_node_id, original)| {
9668            original.map(|original| (plan_node_id as NodeId, original))
9669        })
9670        .collect();
9671    Ok((assembled.execution, exec_entities, plan_to_original))
9672}
9673
9674fn collect_culist_metadata(
9675    runtime_plan: &CuExecutionLoop,
9676    exec_entities: &[ExecutionEntity],
9677    bridge_specs: &mut [BridgeSpec],
9678    plan_to_original: &HashMap<NodeId, NodeId>,
9679) -> (Vec<usize>, HashMap<NodeId, usize>) {
9680    let mut culist_order = Vec::new();
9681    let mut node_output_positions = HashMap::new();
9682
9683    for unit in &runtime_plan.steps {
9684        if let CuExecutionUnit::Step(step) = unit
9685            && step.phase != CuStepPhase::AnytimeRefine // refine steps reuse the base step's slot
9686            && let Some(output_pack) = &step.output_msg_pack
9687        {
9688            let output_idx = output_pack.culist_index;
9689            culist_order.push(output_idx as usize);
9690            match &exec_entities[step.node_id as usize].kind {
9691                ExecutionEntityKind::Task { .. } => {
9692                    if let Some(original_node_id) = plan_to_original.get(&step.node_id) {
9693                        node_output_positions.insert(*original_node_id, output_idx as usize);
9694                    }
9695                }
9696                ExecutionEntityKind::BridgeRx {
9697                    bridge_index,
9698                    channel_index,
9699                } => {
9700                    bridge_specs[*bridge_index].rx_channels[*channel_index].culist_index =
9701                        Some(output_idx as usize);
9702                }
9703                ExecutionEntityKind::BridgeTx {
9704                    bridge_index,
9705                    channel_index,
9706                } => {
9707                    bridge_specs[*bridge_index].tx_channels[*channel_index].culist_index =
9708                        Some(output_idx as usize);
9709                }
9710            }
9711        }
9712    }
9713
9714    (culist_order, node_output_positions)
9715}
9716
9717fn build_monitor_culist_component_mapping(
9718    runtime_plan: &CuExecutionLoop,
9719    exec_entities: &[ExecutionEntity],
9720    bridge_specs: &[BridgeSpec],
9721) -> Result<Vec<usize>, String> {
9722    let mut mapping = Vec::new();
9723    for unit in &runtime_plan.steps {
9724        if let CuExecutionUnit::Step(step) = unit
9725            && step.phase != CuStepPhase::AnytimeRefine // refine steps reuse the base step's slot
9726            && step.output_msg_pack.is_some()
9727        {
9728            let Some(entity) = exec_entities.get(step.node_id as usize) else {
9729                return Err(format!(
9730                    "Missing execution entity for plan node {} while building monitor mapping",
9731                    step.node_id
9732                ));
9733            };
9734            let component_index = match &entity.kind {
9735                ExecutionEntityKind::Task { task_index } => *task_index,
9736                ExecutionEntityKind::BridgeRx {
9737                    bridge_index,
9738                    channel_index,
9739                } => bridge_specs
9740                    .get(*bridge_index)
9741                    .and_then(|spec| spec.rx_channels.get(*channel_index))
9742                    .and_then(|channel| channel.monitor_index)
9743                    .ok_or_else(|| {
9744                        format!(
9745                            "Missing monitor index for bridge rx {}:{}",
9746                            bridge_index, channel_index
9747                        )
9748                    })?,
9749                ExecutionEntityKind::BridgeTx {
9750                    bridge_index,
9751                    channel_index,
9752                } => bridge_specs
9753                    .get(*bridge_index)
9754                    .and_then(|spec| spec.tx_channels.get(*channel_index))
9755                    .and_then(|channel| channel.monitor_index)
9756                    .ok_or_else(|| {
9757                        format!(
9758                            "Missing monitor index for bridge tx {}:{}",
9759                            bridge_index, channel_index
9760                        )
9761                    })?,
9762            };
9763            mapping.push(component_index);
9764        }
9765    }
9766    Ok(mapping)
9767}
9768
9769fn build_parallel_rt_stage_entries(
9770    runtime_plan: &CuExecutionLoop,
9771    exec_entities: &[ExecutionEntity],
9772    task_specs: &CuTaskSpecSet,
9773    bridge_specs: &[BridgeSpec],
9774) -> CuResult<Vec<proc_macro2::TokenStream>> {
9775    let mut entries = Vec::new();
9776
9777    for unit in &runtime_plan.steps {
9778        let CuExecutionUnit::Step(step) = unit else {
9779            todo!("parallel runtime metadata for nested loops is not implemented yet")
9780        };
9781
9782        // An anytime node is one parallel-rt stage: its base and refine steps
9783        // all run inside the base step's stage, on the same worker thread
9784        // (memory locality), so refine steps produce no stage of their own.
9785        if step.phase == CuStepPhase::AnytimeRefine {
9786            continue;
9787        }
9788
9789        let entity = exec_entities.get(step.node_id as usize).ok_or_else(|| {
9790            CuError::from(format!(
9791                "Missing execution entity for runtime plan node {} while building parallel runtime metadata",
9792                step.node_id
9793            ))
9794        })?;
9795
9796        let (label, kind_tokens, component_index) = match &entity.kind {
9797            ExecutionEntityKind::Task { task_index } => (
9798                task_specs
9799                    .ids
9800                    .get(*task_index)
9801                    .cloned()
9802                    .ok_or_else(|| {
9803                        CuError::from(format!(
9804                            "Missing task id for task index {} while building parallel runtime metadata",
9805                            task_index
9806                        ))
9807                    })?,
9808                quote! { cu29::parallel_rt::ParallelRtStageKind::Task },
9809                *task_index,
9810            ),
9811            ExecutionEntityKind::BridgeRx {
9812                bridge_index,
9813                channel_index,
9814            } => {
9815                let bridge = bridge_specs.get(*bridge_index).ok_or_else(|| {
9816                    CuError::from(format!(
9817                        "Missing bridge spec {} while building parallel runtime metadata",
9818                        bridge_index
9819                    ))
9820                })?;
9821                let channel = bridge.rx_channels.get(*channel_index).ok_or_else(|| {
9822                    CuError::from(format!(
9823                        "Missing bridge rx channel {}:{} while building parallel runtime metadata",
9824                        bridge_index, channel_index
9825                    ))
9826                })?;
9827                let component_index = channel.monitor_index.ok_or_else(|| {
9828                    CuError::from(format!(
9829                        "Missing monitor index for bridge rx {}:{} while building parallel runtime metadata",
9830                        bridge_index, channel_index
9831                    ))
9832                })?;
9833                (
9834                    format!("bridge::{}::rx::{}", bridge.id, channel.id),
9835                    quote! { cu29::parallel_rt::ParallelRtStageKind::BridgeRx },
9836                    component_index,
9837                )
9838            }
9839            ExecutionEntityKind::BridgeTx {
9840                bridge_index,
9841                channel_index,
9842            } => {
9843                let bridge = bridge_specs.get(*bridge_index).ok_or_else(|| {
9844                    CuError::from(format!(
9845                        "Missing bridge spec {} while building parallel runtime metadata",
9846                        bridge_index
9847                    ))
9848                })?;
9849                let channel = bridge.tx_channels.get(*channel_index).ok_or_else(|| {
9850                    CuError::from(format!(
9851                        "Missing bridge tx channel {}:{} while building parallel runtime metadata",
9852                        bridge_index, channel_index
9853                    ))
9854                })?;
9855                let component_index = channel.monitor_index.ok_or_else(|| {
9856                    CuError::from(format!(
9857                        "Missing monitor index for bridge tx {}:{} while building parallel runtime metadata",
9858                        bridge_index, channel_index
9859                    ))
9860                })?;
9861                (
9862                    format!("bridge::{}::tx::{}", bridge.id, channel.id),
9863                    quote! { cu29::parallel_rt::ParallelRtStageKind::BridgeTx },
9864                    component_index,
9865                )
9866            }
9867        };
9868
9869        let node_id = step.node_id;
9870        entries.push(quote! {
9871            cu29::parallel_rt::ParallelRtStageMetadata::new(
9872                #label,
9873                #kind_tokens,
9874                #node_id,
9875                cu29::monitoring::ComponentId::new(#component_index),
9876            )
9877        });
9878    }
9879
9880    Ok(entries)
9881}
9882
9883#[allow(dead_code)]
9884fn build_monitored_ids(task_ids: &[String], bridge_specs: &mut [BridgeSpec]) -> Vec<String> {
9885    let mut names = task_ids.to_vec();
9886    for spec in bridge_specs.iter_mut() {
9887        spec.monitor_index = Some(names.len());
9888        names.push(format!("bridge::{}", spec.id));
9889        for channel in spec.rx_channels.iter_mut() {
9890            channel.monitor_index = Some(names.len());
9891            names.push(format!("bridge::{}::rx::{}", spec.id, channel.id));
9892        }
9893        for channel in spec.tx_channels.iter_mut() {
9894            channel.monitor_index = Some(names.len());
9895            names.push(format!("bridge::{}::tx::{}", spec.id, channel.id));
9896        }
9897    }
9898    names
9899}
9900
9901fn wrap_process_step_tokens(
9902    wrap_process_step: bool,
9903    body: proc_macro2::TokenStream,
9904) -> proc_macro2::TokenStream {
9905    if wrap_process_step {
9906        quote! {{
9907            let __cu_process_step_result: cu29::curuntime::ProcessStepResult = (|| {
9908                #body
9909                Ok(cu29::curuntime::ProcessStepOutcome::Continue)
9910            })();
9911            __cu_process_step_result
9912        }}
9913    } else {
9914        body
9915    }
9916}
9917
9918fn abort_process_step_tokens(wrap_process_step: bool) -> proc_macro2::TokenStream {
9919    if wrap_process_step {
9920        quote! {
9921            return Ok(cu29::curuntime::ProcessStepOutcome::AbortCopperList);
9922        }
9923    } else {
9924        quote! {
9925            __cu_abort_copperlist = true;
9926            break '__cu_process_steps;
9927        }
9928    }
9929}
9930
9931fn parallel_task_lifecycle_tokens(
9932    task_trait: proc_macro2::TokenStream,
9933    task_type: &Type,
9934    component_index: usize,
9935    mission_mod: &Ident,
9936    task_instance: &proc_macro2::TokenStream,
9937    placement: ParallelLifecyclePlacement,
9938) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
9939    let rt_guard = rtsan_guard_tokens();
9940    let abort_process_step = abort_process_step_tokens(true);
9941
9942    let preprocess_alloc_open = alloc_scope_open_tokens();
9943    let preprocess_alloc_close = alloc_scope_close_tokens(
9944        quote! { monitor },
9945        quote! { #component_index },
9946        quote! { CuComponentState::Preprocess },
9947    );
9948    let postprocess_alloc_open = alloc_scope_open_tokens();
9949    let postprocess_alloc_close = alloc_scope_close_tokens(
9950        quote! { monitor },
9951        quote! { #component_index },
9952        quote! { CuComponentState::Postprocess },
9953    );
9954    let preprocess = if placement.preprocess {
9955        quote! {
9956            execution_probe.record(cu29::monitoring::ExecutionMarker {
9957                component_id: cu29::monitoring::ComponentId::new(#component_index),
9958                step: CuComponentState::Preprocess,
9959                culistid: Some(clid),
9960            });
9961            ctx.set_current_task(#component_index);
9962            #preprocess_alloc_open
9963            let maybe_error = {
9964                #rt_guard
9965                <#task_type as #task_trait>::preprocess(&mut #task_instance, &ctx)
9966            };
9967            #preprocess_alloc_close
9968            if let Err(error) = maybe_error {
9969                let decision = monitor.process_error(
9970                    cu29::monitoring::ComponentId::new(#component_index),
9971                    CuComponentState::Preprocess,
9972                    &error,
9973                );
9974                match decision {
9975                    Decision::Abort => {
9976                        debug!(ctx,
9977                            "Preprocess: ABORT decision from monitoring. Component '{}' errored out during preprocess. Aborting CopperList {}.",
9978                            #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index)),
9979                            clid
9980                        );
9981                        #abort_process_step
9982                    }
9983                    Decision::Ignore => {
9984                        debug!(ctx,
9985                            "Preprocess: IGNORE decision from monitoring. Component '{}' errored out during preprocess. The runtime will continue.",
9986                            #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index))
9987                        );
9988                    }
9989                    Decision::Shutdown => {
9990                        debug!(ctx,
9991                            "Preprocess: SHUTDOWN decision from monitoring. Component '{}' errored out during preprocess. The runtime cannot continue.",
9992                            #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index))
9993                        );
9994                        return Err(CuError::new_with_cause(
9995                            "Component errored out during preprocess.",
9996                            error,
9997                        ));
9998                    }
9999                }
10000            }
10001        }
10002    } else {
10003        quote! {}
10004    };
10005
10006    let postprocess = if placement.postprocess {
10007        quote! {
10008            execution_probe.record(cu29::monitoring::ExecutionMarker {
10009                component_id: cu29::monitoring::ComponentId::new(#component_index),
10010                step: CuComponentState::Postprocess,
10011                culistid: Some(clid),
10012            });
10013            ctx.set_current_task(#component_index);
10014            #postprocess_alloc_open
10015            let maybe_error = {
10016                #rt_guard
10017                <#task_type as #task_trait>::postprocess(&mut #task_instance, &ctx)
10018            };
10019            #postprocess_alloc_close
10020            if let Err(error) = maybe_error {
10021                let decision = monitor.process_error(
10022                    cu29::monitoring::ComponentId::new(#component_index),
10023                    CuComponentState::Postprocess,
10024                    &error,
10025                );
10026                match decision {
10027                    Decision::Abort => {
10028                        debug!(ctx,
10029                            "Postprocess: ABORT decision from monitoring. Component '{}' errored out during postprocess. Continuing with the completed CopperList.",
10030                            #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index))
10031                        );
10032                    }
10033                    Decision::Ignore => {
10034                        debug!(ctx,
10035                            "Postprocess: IGNORE decision from monitoring. Component '{}' errored out during postprocess. The runtime will continue.",
10036                            #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index))
10037                        );
10038                    }
10039                    Decision::Shutdown => {
10040                        debug!(ctx,
10041                            "Postprocess: SHUTDOWN decision from monitoring. Component '{}' errored out during postprocess. The runtime cannot continue.",
10042                            #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index))
10043                        );
10044                        return Err(CuError::new_with_cause(
10045                            "Component errored out during postprocess.",
10046                            error,
10047                        ));
10048                    }
10049                }
10050            }
10051        }
10052    } else {
10053        quote! {}
10054    };
10055
10056    (preprocess, postprocess)
10057}
10058
10059fn parallel_bridge_lifecycle_tokens(
10060    bridge_type: &Type,
10061    component_index: usize,
10062    mission_mod: &Ident,
10063    placement: ParallelLifecyclePlacement,
10064    keyframe_logging_enabled: bool,
10065) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
10066    let rt_guard = rtsan_guard_tokens();
10067    let abort_process_step = abort_process_step_tokens(true);
10068    let freeze_bridge = keyframe_freeze_bridge_tokens(keyframe_logging_enabled);
10069
10070    let preprocess_alloc_open = alloc_scope_open_tokens();
10071    let preprocess_alloc_close = alloc_scope_close_tokens(
10072        quote! { monitor },
10073        quote! { #component_index },
10074        quote! { CuComponentState::Preprocess },
10075    );
10076    let postprocess_alloc_open = alloc_scope_open_tokens();
10077    let postprocess_alloc_close = alloc_scope_close_tokens(
10078        quote! { monitor },
10079        quote! { #component_index },
10080        quote! { CuComponentState::Postprocess },
10081    );
10082    let preprocess = if placement.preprocess {
10083        quote! {
10084            execution_probe.record(cu29::monitoring::ExecutionMarker {
10085                component_id: cu29::monitoring::ComponentId::new(#component_index),
10086                step: CuComponentState::Preprocess,
10087                culistid: Some(clid),
10088            });
10089            ctx.set_current_component(#component_index);
10090            ctx.clear_current_task();
10091            #preprocess_alloc_open
10092            let maybe_error = {
10093                #rt_guard
10094                <#bridge_type as cu29::cubridge::CuBridge>::preprocess(bridge, &ctx)
10095            };
10096            #preprocess_alloc_close
10097            if let Err(error) = maybe_error {
10098                let decision = monitor.process_error(
10099                    cu29::monitoring::ComponentId::new(#component_index),
10100                    CuComponentState::Preprocess,
10101                    &error,
10102                );
10103                match decision {
10104                    Decision::Abort => {
10105                        debug!(ctx,
10106                            "Preprocess: ABORT decision from monitoring. Component '{}' errored out during preprocess. Aborting CopperList {}.",
10107                            #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index)),
10108                            clid
10109                        );
10110                        #abort_process_step
10111                    }
10112                    Decision::Ignore => {
10113                        debug!(ctx,
10114                            "Preprocess: IGNORE decision from monitoring. Component '{}' errored out during preprocess. The runtime will continue.",
10115                            #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index))
10116                        );
10117                    }
10118                    Decision::Shutdown => {
10119                        debug!(ctx,
10120                            "Preprocess: SHUTDOWN decision from monitoring. Component '{}' errored out during preprocess. The runtime cannot continue.",
10121                            #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index))
10122                        );
10123                        return Err(CuError::new_with_cause(
10124                            "Component errored out during preprocess.",
10125                            error,
10126                        ));
10127                    }
10128                }
10129            }
10130        }
10131    } else {
10132        quote! {}
10133    };
10134
10135    let postprocess = if placement.postprocess {
10136        quote! {
10137            #freeze_bridge
10138            execution_probe.record(cu29::monitoring::ExecutionMarker {
10139                component_id: cu29::monitoring::ComponentId::new(#component_index),
10140                step: CuComponentState::Postprocess,
10141                culistid: Some(clid),
10142            });
10143            ctx.set_current_component(#component_index);
10144            ctx.clear_current_task();
10145            #postprocess_alloc_open
10146            let maybe_error = {
10147                #rt_guard
10148                <#bridge_type as cu29::cubridge::CuBridge>::postprocess(bridge, &ctx)
10149            };
10150            #postprocess_alloc_close
10151            if let Err(error) = maybe_error {
10152                let decision = monitor.process_error(
10153                    cu29::monitoring::ComponentId::new(#component_index),
10154                    CuComponentState::Postprocess,
10155                    &error,
10156                );
10157                match decision {
10158                    Decision::Abort => {
10159                        debug!(ctx,
10160                            "Postprocess: ABORT decision from monitoring. Component '{}' errored out during postprocess. Continuing with the completed CopperList.",
10161                            #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index))
10162                        );
10163                    }
10164                    Decision::Ignore => {
10165                        debug!(ctx,
10166                            "Postprocess: IGNORE decision from monitoring. Component '{}' errored out during postprocess. The runtime will continue.",
10167                            #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index))
10168                        );
10169                    }
10170                    Decision::Shutdown => {
10171                        debug!(ctx,
10172                            "Postprocess: SHUTDOWN decision from monitoring. Component '{}' errored out during postprocess. The runtime cannot continue.",
10173                            #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index))
10174                        );
10175                        return Err(CuError::new_with_cause(
10176                            "Component errored out during postprocess.",
10177                            error,
10178                        ));
10179                    }
10180                }
10181            }
10182        }
10183    } else {
10184        quote! {}
10185    };
10186
10187    (preprocess, postprocess)
10188}
10189
10190/// Mission-module name of the `AnytimePolicy` ZST emitted for one anytime node.
10191fn anytime_policy_ident(task_id: &str) -> Ident {
10192    format_ident!("__CuAnytimePolicy_{}", config_id_to_struct_member(task_id))
10193}
10194
10195/// Name of the hoisted `Option<AnytimeJob>` local for one anytime node — the
10196/// only value crossing its base/refine steps (is the job still live).
10197fn anytime_job_ident(task_id: &str) -> Ident {
10198    format_ident!("__cu_anytime_job_{}", config_id_to_struct_member(task_id))
10199}
10200
10201fn anytime_ms_to_nanos(ms: f64) -> u64 {
10202    (ms * 1_000_000.0).round() as u64
10203}
10204
10205/// The `__cu_any_now` binding shared by the base and refine emitters: one clock
10206/// read per base call and per refine quantum. Without a time knob every timed
10207/// check const-folds away and `now` only feeds the debug-only elapsed, so the
10208/// read is skipped entirely (`CuTime` subtraction saturates, so the zero
10209/// stand-in is safe).
10210fn anytime_now_binding_tokens(anytime: &AnytimeConfig) -> proc_macro2::TokenStream {
10211    if anytime.time_budget_ms.is_some() || anytime.max_age_ms.is_some() {
10212        quote! { let __cu_any_now = clock.now(); }
10213    } else {
10214        quote! { let __cu_any_now = cu29::clock::CuTime::default(); }
10215    }
10216}
10217
10218fn anytime_option_duration_tokens(ms: Option<f64>) -> proc_macro2::TokenStream {
10219    match ms {
10220        Some(ms) => {
10221            let nanos = anytime_ms_to_nanos(ms);
10222            quote! { Some(cu29::clock::CuDuration(#nanos)) }
10223        }
10224        None => quote! { None },
10225    }
10226}
10227
10228/// One `AnytimePolicy` ZST per anytime node, its RON policy baked in as
10229/// consts and overrides so every unset knob const-folds away.
10230///
10231/// A quality knob (`quality_target`/`quality_floor`/`max_stall`) pins the
10232/// implementation to the shared `Quality` scale, which is the compile-time
10233/// gate rejecting those knobs on `Quality = ()` tasks (the job's
10234/// `AnytimePolicy<T::Quality>` bound fails). Without quality knobs the
10235/// implementation is generic over any comparable quality.
10236fn build_anytime_policy_defs(task_specs: &CuTaskSpecSet) -> Vec<proc_macro2::TokenStream> {
10237    task_specs
10238        .anytime_configs
10239        .iter()
10240        .enumerate()
10241        .filter_map(|(index, anytime)| {
10242            let anytime = anytime.as_ref()?;
10243            let policy_ident = anytime_policy_ident(&task_specs.ids[index]);
10244            let time_budget = anytime_option_duration_tokens(anytime.time_budget_ms);
10245            let max_age = anytime_option_duration_tokens(anytime.max_age_ms);
10246            let max_stall = match anytime.max_stall {
10247                Some(stall) => quote! { Some(#stall) },
10248                None => quote! { None },
10249            };
10250            // Only the background runner reads MAX_REFINES: a foreground node
10251            // carries the count as the number of refine steps in its plan.
10252            let max_refines = match anytime.max_refines {
10253                Some(refines) => quote! { Some(#refines) },
10254                None => quote! { None },
10255            };
10256            let consts = quote! {
10257                const TIME_BUDGET: Option<cu29::clock::CuDuration> = #time_budget;
10258                const MAX_AGE: Option<cu29::clock::CuDuration> = #max_age;
10259                const MAX_STALL: Option<u32> = #max_stall;
10260                const MAX_REFINES: Option<u32> = #max_refines;
10261            };
10262            let has_quality_knob = anytime.quality_target.is_some()
10263                || anytime.quality_floor.is_some()
10264                || anytime.max_stall.is_some();
10265            let policy_impl = if has_quality_knob {
10266                let target_met = anytime.quality_target.map(|target| {
10267                    quote! {
10268                        #[inline(always)]
10269                        fn target_met(q: cu29::cutask_anytime::Quality) -> bool {
10270                            q >= cu29::cutask_anytime::quality_from_f32(#target)
10271                        }
10272                    }
10273                });
10274                let below_floor = anytime.quality_floor.map(|floor| {
10275                    quote! {
10276                        #[inline(always)]
10277                        fn below_floor(q: cu29::cutask_anytime::Quality) -> bool {
10278                            // NaN fails closed: an unordered quality counts as below the floor.
10279                            q.partial_cmp(&cu29::cutask_anytime::quality_from_f32(#floor))
10280                                .is_none_or(core::cmp::Ordering::is_lt)
10281                        }
10282                    }
10283                });
10284                quote! {
10285                    impl cu29::cutask_anytime::AnytimePolicy<cu29::cutask_anytime::Quality>
10286                        for #policy_ident
10287                    {
10288                        #consts
10289                        #target_met
10290                        #below_floor
10291                    }
10292                }
10293            } else {
10294                quote! {
10295                    impl<Q: Copy + PartialOrd> cu29::cutask_anytime::AnytimePolicy<Q>
10296                        for #policy_ident
10297                    {
10298                        #consts
10299                    }
10300                }
10301            };
10302            Some(quote! {
10303                #[allow(non_camel_case_types)]
10304                pub struct #policy_ident;
10305                #policy_impl
10306            })
10307        })
10308        .collect()
10309}
10310
10311/// The hoisted per-node job local, declared above the plan splice: step bodies
10312/// can be wrapped in closures, so the local cannot live inside a step. The
10313/// `Option` answers the single genuinely dynamic cross-step question — is the
10314/// job still live; nothing else crosses steps.
10315fn anytime_job_local_tokens(task_specs: &CuTaskSpecSet, index: usize) -> proc_macro2::TokenStream {
10316    let task_type = &task_specs.task_types[index];
10317    let policy_ident = anytime_policy_ident(&task_specs.ids[index]);
10318    let job_ident = anytime_job_ident(&task_specs.ids[index]);
10319    quote! {
10320        #[allow(non_snake_case, unused_mut)]
10321        let mut #job_ident: Option<
10322            cu29::cutask_anytime::AnytimeJob<
10323                <#task_type as cu29::cutask_anytime::CuAnytimeTask>::Quality,
10324                #policy_ident,
10325            >,
10326        > = None;
10327    }
10328}
10329
10330fn build_anytime_job_locals(task_specs: &CuTaskSpecSet) -> Vec<proc_macro2::TokenStream> {
10331    task_specs
10332        .anytime_configs
10333        .iter()
10334        .enumerate()
10335        // A background anytime node keeps its single `Whole` step and runs its
10336        // job inside the runner, so it has no plan-level job to carry.
10337        .filter(|(index, anytime)| anytime.is_some() && !task_specs.background_flags[*index])
10338        .map(|(index, _)| anytime_job_local_tokens(task_specs, index))
10339        .collect()
10340}
10341
10342/// The output-slot cast trait/fn pair shared by the source, regular, and
10343/// anytime step emitters: `kind_label`/`fn_prefix` keep each emitter's
10344/// diagnostic idents, `task_trait` is the trait whose `Output` the slot must
10345/// match. Each emitted block is its own scope, so the fixed names never
10346/// collide.
10347fn output_slot_cast_tokens(
10348    task_hint: &str,
10349    kind_label: &str,
10350    fn_prefix: &str,
10351    task_trait: &proc_macro2::TokenStream,
10352) -> (proc_macro2::TokenStream, Ident) {
10353    let trait_ident = format_ident!(
10354        "__CuOutputSlotMustMatchTaskOutput__{}_{}__Add_dst___nc___connections_for_unused_outputs",
10355        kind_label,
10356        task_hint
10357    );
10358    let fn_ident = format_ident!(
10359        "__cu_{}_output_slot_or_add_dst___nc___for_unused_outputs__task_{}",
10360        fn_prefix,
10361        task_hint
10362    );
10363    let defs = quote! {
10364        #[allow(non_camel_case_types)]
10365        trait #trait_ident<Expected> {
10366            fn __cu_cast_output_slot(slot: &mut Self) -> &mut Expected;
10367        }
10368        impl<T> #trait_ident<T> for T {
10369            fn __cu_cast_output_slot(slot: &mut Self) -> &mut T {
10370                slot
10371            }
10372        }
10373
10374        fn #fn_ident<'a, Task, Slot>(
10375            _task: &Task,
10376            slot: &'a mut Slot,
10377        ) -> &'a mut Task::Output<'static>
10378        where
10379            Task: #task_trait,
10380            Slot: #trait_ident<Task::Output<'static>>,
10381        {
10382            <Slot as #trait_ident<Task::Output<'static>>>::__cu_cast_output_slot(slot)
10383        }
10384    };
10385    (defs, fn_ident)
10386}
10387
10388/// Anytime flavor of [`output_slot_cast_tokens`].
10389fn anytime_slot_cast_tokens(task_hint: &str) -> (proc_macro2::TokenStream, Ident) {
10390    output_slot_cast_tokens(
10391        task_hint,
10392        "AnytimeTask",
10393        "anytime",
10394        &quote! { cu29::cutask_anytime::CuAnytimeTask },
10395    )
10396}
10397
10398/// The process-error monitoring match shared by every task step emitter
10399/// (source, sink, regular, and anytime base/refine blocks).
10400fn process_monitoring_action_tokens(
10401    tid: usize,
10402    mission_mod: &Ident,
10403    output_culist_index: &syn::Index,
10404    output_clear_payload: &proc_macro2::TokenStream,
10405    wrap_process_step: bool,
10406) -> proc_macro2::TokenStream {
10407    let abort_process_step = abort_process_step_tokens(wrap_process_step);
10408    quote! {
10409        debug!(ctx, "Component {}: Error during process: {}", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#tid)), &error);
10410        let decision = monitor.process_error(cu29::monitoring::ComponentId::new(#tid), CuComponentState::Process, &error);
10411        match decision {
10412            Decision::Abort => {
10413                debug!(ctx, "Process: ABORT decision from monitoring. Component '{}' errored out \
10414                        during process. Skipping the processing of CL {}.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#tid)), clid);
10415                #abort_process_step
10416            }
10417            Decision::Ignore => {
10418                debug!(ctx, "Process: IGNORE decision from monitoring. Component '{}' errored out \
10419                        during process. The runtime will continue with a forced empty message.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#tid)));
10420                let cumsg_output = &mut msgs.#output_culist_index;
10421                #output_clear_payload
10422            }
10423            Decision::Shutdown => {
10424                debug!(ctx, "Process: SHUTDOWN decision from monitoring. Component '{}' errored out \
10425                        during process. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#tid)));
10426                return Err(CuError::new_with_cause("Component errored out during process.", error));
10427            }
10428        }
10429    }
10430}
10431
10432/// The sim-callback `doit` gate shared by the sink, regular, and anytime-base
10433/// step emitters. `input_binding` is the `let cumsg_input = <expr>;` line;
10434/// sources have none and pass `()` to the callback state.
10435fn process_sim_callback_tokens(
10436    sim_mode: bool,
10437    enum_name: &Ident,
10438    input_binding: Option<&proc_macro2::TokenStream>,
10439    output_culist_index: &syn::Index,
10440    monitoring_action: &proc_macro2::TokenStream,
10441) -> proc_macro2::TokenStream {
10442    if !sim_mode {
10443        return quote! { let doit = true; };
10444    }
10445    let (input_line, state_input) = match input_binding {
10446        Some(binding) => ((*binding).clone(), quote! { cumsg_input }),
10447        None => (quote! {}, quote! { () }),
10448    };
10449    quote! {
10450        let doit = {
10451            #input_line
10452            let cumsg_output = &mut msgs.#output_culist_index;
10453            let state = CuTaskCallbackState::Process(#state_input, cumsg_output);
10454            let ovr = sim_callback(SimStep::#enum_name(state));
10455
10456            if let SimOverride::Errored(reason) = ovr  {
10457                let error: CuError = reason.into();
10458                #monitoring_action
10459                false
10460            }
10461            else {
10462                ovr == SimOverride::ExecuteByRuntime
10463            }
10464        };
10465    }
10466}
10467
10468/// Emits the `[base <node>]` block of a foreground anytime node: the standard
10469/// step wrapper (probe, keyframe freeze, sim callback, monitoring, alloc
10470/// scopes) around the DOA age check — emitted only when `max_age_ms` is
10471/// configured — and one `base()` call. On `Improved` the job lands in the
10472/// node's hoisted local; `Converged`/`Aborted` finish on the spot and the
10473/// local stays `None`, so every refine block no-ops through its liveness test.
10474///
10475/// An anytime node is single-output, so the monitoring clear is the one-slot
10476/// form. The job local needs no explicit kill on error: a job is only
10477/// (re)stored on an `Ok` status, so an erroring block leaves it `None`/taken
10478/// and later quanta no-op.
10479///
10480/// Returns the block plus the payload-drop logging tokens (same contract as
10481/// `generate_task_execution_tokens`).
10482fn generate_anytime_base_block(
10483    step: &CuExecutionStep,
10484    task_index: usize,
10485    task_specs: &CuTaskSpecSet,
10486    ctx: &StepGenerationContext<'_>,
10487    task_instance: &proc_macro2::TokenStream,
10488) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
10489    let tid = task_index;
10490    let anytime = task_specs.anytime_configs[tid]
10491        .as_ref()
10492        .expect("anytime base block emitted for a task without an anytime policy");
10493    let now_binding = anytime_now_binding_tokens(anytime);
10494    let task_id = &task_specs.ids[tid];
10495    let policy_ident = anytime_policy_ident(task_id);
10496    let job_ident = anytime_job_ident(task_id);
10497    let enum_name = Ident::new(&config_id_to_enum(task_id), Span::call_site());
10498    let task_hint = config_id_to_struct_member(task_id);
10499    let (slot_cast_defs, slot_cast_fn) = anytime_slot_cast_tokens(&task_hint);
10500    let rt_guard = rtsan_guard_tokens();
10501    let mission_mod = ctx.mission_mod;
10502    let freeze_task = keyframe_freeze_task_tokens(ctx.keyframe_logging_enabled, task_instance);
10503
10504    let comment_str = format!(
10505        "DEBUG ->> {} ({:?}/{:?}) Id:{} I:{:?} O:{:?}",
10506        step.node.get_id(),
10507        step.task_type,
10508        step.phase,
10509        step.node_id,
10510        step.input_msg_indices_types,
10511        step.output_msg_pack
10512    );
10513    let comment_tokens = quote! {{
10514        let _ = stringify!(#comment_str);
10515    }};
10516
10517    let output_pack = step
10518        .output_msg_pack
10519        .as_ref()
10520        .expect("Anytime task should have an output message pack.");
10521    let output_culist_index = int2sliceindex(output_pack.culist_index);
10522    let monitoring_action = process_monitoring_action_tokens(
10523        tid,
10524        mission_mod,
10525        &output_culist_index,
10526        &quote! { cumsg_output.clear_payload(); },
10527        ctx.wrap_process_step,
10528    );
10529
10530    let GeneratedTaskInput {
10531        setup: task_input_setup,
10532        expr: task_input_expr,
10533    } = generate_task_input_binding(
10534        step,
10535        ctx.mission_name,
10536        ctx.output_pack_sizes,
10537        ctx.task_input_layouts,
10538    );
10539
10540    // The sim callback fires once per node, here in the base block, with the
10541    // standard Process state. On ExecutedBySim the job local simply stays
10542    // `None` — no refine-side sim machinery exists or is needed.
10543    let call_sim_callback = process_sim_callback_tokens(
10544        ctx.sim_mode,
10545        &enum_name,
10546        Some(&quote! { let cumsg_input = #task_input_expr; }),
10547        &output_culist_index,
10548        &monitoring_action,
10549    );
10550
10551    // Anchor extraction and the DOA skip exist in the text only when max_age
10552    // is configured; otherwise the anchor degenerates to the budget anchor.
10553    let base_call = quote! {
10554        match #task_instance.base(&ctx, cumsg_input, cumsg_output) {
10555            Ok(cu29::cutask_anytime::AnytimeStatus::Improved(q)) => {
10556                #job_ident = Some(cu29::cutask_anytime::AnytimeJob::new(__cu_any_now, __cu_any_anchor, q));
10557                Ok(())
10558            }
10559            Ok(cu29::cutask_anytime::AnytimeStatus::Converged(q)) => {
10560                let __cu_any_job: cu29::cutask_anytime::AnytimeJob<_, #policy_ident> =
10561                    cu29::cutask_anytime::AnytimeJob::new(__cu_any_now, __cu_any_anchor, q);
10562                // The job's clock read is reused: `finish` spends `now` only on
10563                // the debug-only elapsed, so a terminal base pays no second read.
10564                let __cu_any_outcome = __cu_any_job.finish(
10565                    __cu_any_now,
10566                    cu29::cutask_anytime::AnytimeStopCause::Converged,
10567                    0u32,
10568                    cumsg_output,
10569                );
10570                debug!(ctx, "Anytime task {}: {} after {} refinement(s).", #task_id, __cu_any_outcome.stop.label(), __cu_any_outcome.iterations);
10571                Ok(())
10572            }
10573            Ok(cu29::cutask_anytime::AnytimeStatus::Aborted) => {
10574                let __cu_any_outcome = cu29::cutask_anytime::abort_at_base(__cu_any_now, __cu_any_now, cumsg_output);
10575                debug!(ctx, "Anytime task {}: {} after {} refinement(s).", #task_id, __cu_any_outcome.stop.label(), __cu_any_outcome.iterations);
10576                Ok(())
10577            }
10578            Err(error) => Err(error),
10579        }
10580    };
10581    let base_dispatch = if let Some(max_age_ms) = anytime.max_age_ms {
10582        let max_age_nanos = anytime_ms_to_nanos(max_age_ms);
10583        quote! {
10584            let __cu_any_anchor = cu29::cutask_anytime::anchor_from_tov(cumsg_input.tov, __cu_any_now);
10585            if __cu_any_now >= __cu_any_anchor + cu29::clock::CuDuration(#max_age_nanos) {
10586                let __cu_any_outcome = cu29::cutask_anytime::skip_stale(cumsg_output);
10587                debug!(ctx, "Anytime task {}: input dead on arrival, job skipped.", #task_id);
10588                let _ = __cu_any_outcome;
10589                Ok(())
10590            } else {
10591                #base_call
10592            }
10593        }
10594    } else {
10595        quote! {
10596            let __cu_any_anchor = __cu_any_now;
10597            #base_call
10598        }
10599    };
10600
10601    let alloc_open = alloc_scope_open_tokens();
10602    let alloc_close = alloc_scope_close_tokens(
10603        quote! { monitor },
10604        quote! { #tid },
10605        quote! { CuComponentState::Process },
10606    );
10607
10608    let block = quote! {
10609        {
10610            #comment_tokens
10611            // One snapshot per copperlist, before the job: refine blocks must
10612            // not re-freeze.
10613            #freeze_task
10614            #task_input_setup
10615            #call_sim_callback
10616            let cumsg_input = #task_input_expr;
10617            let cumsg_output = &mut msgs.#output_culist_index;
10618            let maybe_error = if doit {
10619                execution_probe.record(cu29::monitoring::ExecutionMarker {
10620                    component_id: cu29::monitoring::ComponentId::new(#tid),
10621                    step: CuComponentState::Process,
10622                    culistid: Some(clid),
10623                });
10624                #slot_cast_defs
10625                if cumsg_output.metadata.process_time.start.is_none() {
10626                    cumsg_output.metadata.process_time.start = cu29::curuntime::perf_now(clock).into();
10627                }
10628                #alloc_open
10629                let result = {
10630                    let cumsg_output = #slot_cast_fn(&#task_instance, cumsg_output);
10631                    #rt_guard
10632                    ctx.set_current_task(#tid);
10633                    #now_binding
10634                    #base_dispatch
10635                };
10636                // A live job means a refine block runs and stamps `end`; only
10637                // a job that terminated at the base site stamps it here.
10638                if #job_ident.is_none() {
10639                    cumsg_output.metadata.process_time.end = cu29::curuntime::perf_now(clock).into();
10640                }
10641                #alloc_close
10642                result
10643            } else {
10644                Ok(())
10645            };
10646            if let Err(error) = maybe_error {
10647                #monitoring_action
10648            }
10649        }
10650    };
10651
10652    let logging_tokens = if !task_specs.logging_enabled[tid] {
10653        quote! {
10654            let cumsg_output = &mut culist.msgs.0.#output_culist_index;
10655            cumsg_output.clear_payload();
10656        }
10657    } else {
10658        quote!()
10659    };
10660
10661    (block, logging_tokens)
10662}
10663
10664/// Emits the `k`-th of `total` refine blocks of a foreground anytime node:
10665/// liveness test on the hoisted local, then the configured between-quanta
10666/// `check()`, then one `refine()` quantum. `k` and last-ness are generation
10667/// time facts — the runtime carries no counter; the LAST block never puts the
10668/// job back and stops a still-live job with `MaxRefines` purely by position.
10669fn generate_anytime_refine_block(
10670    step: &CuExecutionStep,
10671    task_index: usize,
10672    task_specs: &CuTaskSpecSet,
10673    ctx: &StepGenerationContext<'_>,
10674    task_instance: &proc_macro2::TokenStream,
10675    k: u32,
10676    total: u32,
10677) -> proc_macro2::TokenStream {
10678    let tid = task_index;
10679    let task_id = &task_specs.ids[tid];
10680    let job_ident = anytime_job_ident(task_id);
10681    let task_hint = config_id_to_struct_member(task_id);
10682    let (slot_cast_defs, slot_cast_fn) = anytime_slot_cast_tokens(&task_hint);
10683    let rt_guard = rtsan_guard_tokens();
10684
10685    let comment_str = format!(
10686        "DEBUG ->> {} ({:?}/{:?} {}/{}) Id:{} O:{:?}",
10687        step.node.get_id(),
10688        step.task_type,
10689        step.phase,
10690        k,
10691        total,
10692        step.node_id,
10693        step.output_msg_pack
10694    );
10695    let comment_tokens = quote! {{
10696        let _ = stringify!(#comment_str);
10697    }};
10698
10699    let output_pack = step
10700        .output_msg_pack
10701        .as_ref()
10702        .expect("Anytime refine step should carry the base step's output pack.");
10703    let output_culist_index = int2sliceindex(output_pack.culist_index);
10704    let monitoring_action = process_monitoring_action_tokens(
10705        tid,
10706        ctx.mission_mod,
10707        &output_culist_index,
10708        &quote! { cumsg_output.clear_payload(); },
10709        ctx.wrap_process_step,
10710    );
10711
10712    // A live job at block k has run exactly k-1 quanta — every earlier block
10713    // either ran its quantum or killed the job — so these are plain literals.
10714    let iters_on_check_stop = k - 1;
10715    let iters_with_quantum = k;
10716    let finish_log = quote! {
10717        debug!(ctx, "Anytime task {}: {} after {} refinement(s).", #task_id, __cu_any_outcome.stop.label(), __cu_any_outcome.iterations);
10718    };
10719    let anytime = task_specs.anytime_configs[tid]
10720        .as_ref()
10721        .expect("anytime refine block emitted for a task without an anytime policy");
10722    let now_binding = anytime_now_binding_tokens(anytime);
10723    // Only the Improved tail differs between blocks: put the job back while
10724    // the plan has more quanta; the LAST block stops a still-live job with
10725    // MaxRefines BY POSITION — the plan has no more quanta for it.
10726    let improved_tail = if k < total {
10727        quote! {
10728            #job_ident = Some(__cu_any_job); // still live: put it back
10729            Ok(())
10730        }
10731    } else {
10732        quote! {
10733            let __cu_any_outcome = __cu_any_job.finish(__cu_any_now, cu29::cutask_anytime::AnytimeStopCause::MaxRefines, #iters_with_quantum, cumsg_output);
10734            #finish_log
10735            Ok(())
10736        }
10737    };
10738    let quantum = quote! {
10739        #now_binding
10740        if let Some(__cu_any_cause) = __cu_any_job.check(__cu_any_now) {
10741            let __cu_any_outcome = __cu_any_job.finish(__cu_any_now, __cu_any_cause, #iters_on_check_stop, cumsg_output);
10742            #finish_log
10743            Ok(())
10744        } else {
10745            match #task_instance.refine(&ctx, cumsg_output) {
10746                Ok(cu29::cutask_anytime::AnytimeStatus::Improved(q)) => {
10747                    __cu_any_job.record(q);
10748                    #improved_tail
10749                }
10750                Ok(cu29::cutask_anytime::AnytimeStatus::Converged(q)) => {
10751                    __cu_any_job.record(q);
10752                    let __cu_any_outcome = __cu_any_job.finish(__cu_any_now, cu29::cutask_anytime::AnytimeStopCause::Converged, #iters_with_quantum, cumsg_output);
10753                    #finish_log
10754                    Ok(())
10755                }
10756                Ok(cu29::cutask_anytime::AnytimeStatus::Aborted) => {
10757                    let __cu_any_outcome = __cu_any_job.finish(__cu_any_now, cu29::cutask_anytime::AnytimeStopCause::Aborted, #iters_with_quantum, cumsg_output);
10758                    #finish_log
10759                    Ok(())
10760                }
10761                Err(error) => Err(error),
10762            }
10763        }
10764    };
10765
10766    let alloc_open = alloc_scope_open_tokens();
10767    let alloc_close = alloc_scope_close_tokens(
10768        quote! { monitor },
10769        quote! { #tid },
10770        quote! { CuComponentState::Process },
10771    );
10772
10773    quote! {
10774        {
10775            #comment_tokens
10776            // Liveness test: an earlier block that finished (or errored) the
10777            // job left the local empty, so this quantum no-ops.
10778            if let Some(mut __cu_any_job) = #job_ident.take() {
10779                execution_probe.record(cu29::monitoring::ExecutionMarker {
10780                    component_id: cu29::monitoring::ComponentId::new(#tid),
10781                    step: CuComponentState::Process,
10782                    culistid: Some(clid),
10783                });
10784                let cumsg_output = &mut msgs.#output_culist_index;
10785                #slot_cast_defs
10786                #alloc_open
10787                let maybe_error = {
10788                    let cumsg_output = #slot_cast_fn(&#task_instance, cumsg_output);
10789                    #rt_guard
10790                    ctx.set_current_task(#tid);
10791                    #quantum
10792                };
10793                // A still-live job means a later block runs and stamps `end`;
10794                // only the job's terminal quantum pays the clock read.
10795                if #job_ident.is_none() {
10796                    cumsg_output.metadata.process_time.end = cu29::curuntime::perf_now(clock).into();
10797                }
10798                #alloc_close
10799                if let Err(error) = maybe_error {
10800                    // No explicit job kill needed: the job was taken and is
10801                    // only put back on Ok(Improved), so later quanta no-op.
10802                    #monitoring_action
10803                }
10804            }
10805        }
10806    }
10807}
10808
10809#[derive(Clone, Copy)]
10810struct StepGenerationContext<'a> {
10811    output_pack_sizes: &'a [usize],
10812    task_input_layouts: &'a HashMap<String, TaskInputLayout>,
10813    mission_name: &'a str,
10814    sim_mode: bool,
10815    keyframe_logging_enabled: bool,
10816    mission_mod: &'a Ident,
10817    lifecycle_placement: ParallelLifecyclePlacement,
10818    wrap_process_step: bool,
10819}
10820
10821impl<'a> StepGenerationContext<'a> {
10822    #[allow(clippy::too_many_arguments)]
10823    fn new(
10824        output_pack_sizes: &'a [usize],
10825        task_input_layouts: &'a HashMap<String, TaskInputLayout>,
10826        mission_name: &'a str,
10827        sim_mode: bool,
10828        keyframe_logging_enabled: bool,
10829        mission_mod: &'a Ident,
10830        lifecycle_placement: ParallelLifecyclePlacement,
10831        wrap_process_step: bool,
10832    ) -> Self {
10833        Self {
10834            output_pack_sizes,
10835            task_input_layouts,
10836            mission_name,
10837            sim_mode,
10838            keyframe_logging_enabled,
10839            mission_mod,
10840            lifecycle_placement,
10841            wrap_process_step,
10842        }
10843    }
10844}
10845
10846struct TaskExecutionTokens {
10847    setup: proc_macro2::TokenStream,
10848    instance: proc_macro2::TokenStream,
10849}
10850
10851fn keyframe_freeze_task_tokens(
10852    enabled: bool,
10853    task_instance: &proc_macro2::TokenStream,
10854) -> proc_macro2::TokenStream {
10855    if enabled {
10856        quote! { kf_manager.freeze_task(clid, &#task_instance)?; }
10857    } else {
10858        quote! {}
10859    }
10860}
10861
10862fn keyframe_freeze_bridge_tokens(enabled: bool) -> proc_macro2::TokenStream {
10863    if enabled {
10864        quote! { kf_manager.freeze_any(clid, bridge)?; }
10865    } else {
10866        quote! {}
10867    }
10868}
10869
10870impl TaskExecutionTokens {
10871    fn new(setup: proc_macro2::TokenStream, instance: proc_macro2::TokenStream) -> Self {
10872        Self { setup, instance }
10873    }
10874}
10875
10876fn generate_task_execution_tokens(
10877    step: &CuExecutionStep,
10878    task_index: usize,
10879    task_specs: &CuTaskSpecSet,
10880    runtime_task_type: &Type,
10881    ctx: StepGenerationContext<'_>,
10882    task_tokens: TaskExecutionTokens,
10883) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
10884    let StepGenerationContext {
10885        output_pack_sizes,
10886        task_input_layouts,
10887        mission_name,
10888        sim_mode,
10889        keyframe_logging_enabled,
10890        mission_mod,
10891        lifecycle_placement,
10892        wrap_process_step,
10893    } = ctx;
10894    let TaskExecutionTokens {
10895        setup: task_setup,
10896        instance: task_instance,
10897    } = task_tokens;
10898    let freeze_task = keyframe_freeze_task_tokens(keyframe_logging_enabled, &task_instance);
10899    let comment_str = format!(
10900        "DEBUG ->> {} ({:?}) Id:{} I:{:?} O:{:?}",
10901        step.node.get_id(),
10902        step.task_type,
10903        step.node_id,
10904        step.input_msg_indices_types,
10905        step.output_msg_pack
10906    );
10907    let comment_tokens = quote! {{
10908        let _ = stringify!(#comment_str);
10909    }};
10910    let tid = task_index;
10911    let task_enum_name = config_id_to_enum(&task_specs.ids[tid]);
10912    let enum_name = Ident::new(&task_enum_name, Span::call_site());
10913    let task_hint = config_id_to_struct_member(&task_specs.ids[tid]);
10914    let rt_guard = rtsan_guard_tokens();
10915    let run_in_sim_flag = task_specs.run_in_sim_flags[tid];
10916    let (parallel_task_preprocess, parallel_task_postprocess) = parallel_task_lifecycle_tokens(
10917        task_trait_for_specs(task_specs, tid),
10918        runtime_task_type,
10919        tid,
10920        mission_mod,
10921        &task_instance,
10922        lifecycle_placement,
10923    );
10924    let maybe_sim_tick = if sim_mode && !run_in_sim_flag {
10925        quote! {
10926            if !doit {
10927                #task_instance.sim_tick();
10928            }
10929        }
10930    } else {
10931        quote!()
10932    };
10933
10934    let output_pack = step
10935        .output_msg_pack
10936        .as_ref()
10937        .expect("Task should have an output message pack.");
10938    let output_culist_index = int2sliceindex(output_pack.culist_index);
10939    let output_ports: Vec<syn::Index> = (0..output_pack.msg_types.len())
10940        .map(syn::Index::from)
10941        .collect();
10942    let output_clear_payload = if output_ports.len() == 1 {
10943        quote! { cumsg_output.clear_payload(); }
10944    } else {
10945        quote! { #(cumsg_output.#output_ports.clear_payload();)* }
10946    };
10947    let output_start_time = if output_ports.len() == 1 {
10948        quote! {
10949            if cumsg_output.metadata.process_time.start.is_none() {
10950                cumsg_output.metadata.process_time.start = cu29::curuntime::perf_now(clock).into();
10951            }
10952        }
10953    } else {
10954        quote! {
10955            let start_time = cu29::curuntime::perf_now(clock).into();
10956            #( if cumsg_output.#output_ports.metadata.process_time.start.is_none() {
10957                cumsg_output.#output_ports.metadata.process_time.start = start_time;
10958            } )*
10959        }
10960    };
10961    let output_end_time = if output_ports.len() == 1 {
10962        quote! {
10963            if cumsg_output.metadata.process_time.end.is_none() {
10964                cumsg_output.metadata.process_time.end = cu29::curuntime::perf_now(clock).into();
10965            }
10966        }
10967    } else {
10968        quote! {
10969            let end_time = cu29::curuntime::perf_now(clock).into();
10970            #( if cumsg_output.#output_ports.metadata.process_time.end.is_none() {
10971                cumsg_output.#output_ports.metadata.process_time.end = end_time;
10972            } )*
10973        }
10974    };
10975
10976    match step.task_type {
10977        CuTaskType::Source => {
10978            let monitoring_action = process_monitoring_action_tokens(
10979                tid,
10980                mission_mod,
10981                &output_culist_index,
10982                &output_clear_payload,
10983                wrap_process_step,
10984            );
10985
10986            let call_sim_callback = process_sim_callback_tokens(
10987                sim_mode,
10988                &enum_name,
10989                None,
10990                &output_culist_index,
10991                &monitoring_action,
10992            );
10993
10994            let logging_tokens = if !task_specs.logging_enabled[tid] {
10995                quote! {
10996                    let mut cumsg_output = &mut culist.msgs.0.#output_culist_index;
10997                    #output_clear_payload
10998                }
10999            } else {
11000                quote!()
11001            };
11002            let alloc_open = alloc_scope_open_tokens();
11003            let alloc_close = alloc_scope_close_tokens(
11004                quote! { monitor },
11005                quote! { #tid },
11006                quote! { CuComponentState::Process },
11007            );
11008            let (slot_cast_defs, source_slot_match_fn_ident) = output_slot_cast_tokens(
11009                &task_hint,
11010                "Task",
11011                "source",
11012                &quote! { cu29::cutask::CuSrcTask },
11013            );
11014            let source_process_tokens = quote! {
11015                #slot_cast_defs
11016
11017                #output_start_time
11018                #alloc_open
11019                let result = {
11020                    let cumsg_output = #source_slot_match_fn_ident::<
11021                        _,
11022                        _,
11023                    >(&#task_instance, cumsg_output);
11024                    #rt_guard
11025                    ctx.set_current_task(#tid);
11026                    #task_instance.process(&ctx, cumsg_output)
11027                };
11028                #output_end_time
11029                #alloc_close
11030                result
11031            };
11032
11033            (
11034                wrap_process_step_tokens(
11035                    wrap_process_step,
11036                    quote! {
11037                        #task_setup
11038                        #parallel_task_preprocess
11039                        #comment_tokens
11040                        #freeze_task
11041                        #call_sim_callback
11042                        let cumsg_output = &mut msgs.#output_culist_index;
11043                        #maybe_sim_tick
11044                        let maybe_error = if doit {
11045                            execution_probe.record(cu29::monitoring::ExecutionMarker {
11046                                component_id: cu29::monitoring::ComponentId::new(#tid),
11047                                step: CuComponentState::Process,
11048                                culistid: Some(clid),
11049                            });
11050                            #source_process_tokens
11051                        } else {
11052                            Ok(())
11053                        };
11054                        if let Err(error) = maybe_error {
11055                            #monitoring_action
11056                        }
11057                        #parallel_task_postprocess
11058                    },
11059                ),
11060                logging_tokens,
11061            )
11062        }
11063        CuTaskType::Sink => {
11064            let GeneratedTaskInput {
11065                setup: task_input_setup,
11066                expr: task_input_expr,
11067            } = generate_task_input_binding(
11068                step,
11069                mission_name,
11070                output_pack_sizes,
11071                task_input_layouts,
11072            );
11073
11074            let monitoring_action = process_monitoring_action_tokens(
11075                tid,
11076                mission_mod,
11077                &output_culist_index,
11078                &output_clear_payload,
11079                wrap_process_step,
11080            );
11081
11082            let call_sim_callback = process_sim_callback_tokens(
11083                sim_mode,
11084                &enum_name,
11085                Some(&quote! { let cumsg_input = #task_input_expr; }),
11086                &output_culist_index,
11087                &monitoring_action,
11088            );
11089
11090            let alloc_open = alloc_scope_open_tokens();
11091            let alloc_close = alloc_scope_close_tokens(
11092                quote! { monitor },
11093                quote! { #tid },
11094                quote! { CuComponentState::Process },
11095            );
11096            (
11097                wrap_process_step_tokens(
11098                    wrap_process_step,
11099                    quote! {
11100                        #task_setup
11101                        #parallel_task_preprocess
11102                        #comment_tokens
11103                        #freeze_task
11104                        #task_input_setup
11105                        #call_sim_callback
11106                        let cumsg_input = #task_input_expr;
11107                        let cumsg_output = &mut msgs.#output_culist_index;
11108                        let maybe_error = if doit {
11109                            execution_probe.record(cu29::monitoring::ExecutionMarker {
11110                                component_id: cu29::monitoring::ComponentId::new(#tid),
11111                                step: CuComponentState::Process,
11112                                culistid: Some(clid),
11113                            });
11114                            #output_start_time
11115                            #alloc_open
11116                            let result = {
11117                                #rt_guard
11118                                ctx.set_current_task(#tid);
11119                                #task_instance.process(&ctx, cumsg_input)
11120                            };
11121                            #output_end_time
11122                            #alloc_close
11123                            result
11124                        } else {
11125                            Ok(())
11126                        };
11127                        if let Err(error) = maybe_error {
11128                            #monitoring_action
11129                        }
11130                        #parallel_task_postprocess
11131                    },
11132                ),
11133                quote! {},
11134            )
11135        }
11136        CuTaskType::Regular => {
11137            let GeneratedTaskInput {
11138                setup: task_input_setup,
11139                expr: task_input_expr,
11140            } = generate_task_input_binding(
11141                step,
11142                mission_name,
11143                output_pack_sizes,
11144                task_input_layouts,
11145            );
11146
11147            let monitoring_action = process_monitoring_action_tokens(
11148                tid,
11149                mission_mod,
11150                &output_culist_index,
11151                &output_clear_payload,
11152                wrap_process_step,
11153            );
11154
11155            let call_sim_callback = process_sim_callback_tokens(
11156                sim_mode,
11157                &enum_name,
11158                Some(&quote! { let cumsg_input = #task_input_expr; }),
11159                &output_culist_index,
11160                &monitoring_action,
11161            );
11162
11163            let logging_tokens = if !task_specs.logging_enabled[tid] {
11164                quote! {
11165                    let mut cumsg_output = &mut culist.msgs.0.#output_culist_index;
11166                    #output_clear_payload
11167                }
11168            } else {
11169                quote!()
11170            };
11171            let alloc_open = alloc_scope_open_tokens();
11172            let alloc_close = alloc_scope_close_tokens(
11173                quote! { monitor },
11174                quote! { #tid },
11175                quote! { CuComponentState::Process },
11176            );
11177            let (slot_cast_defs, regular_slot_match_fn_ident) = output_slot_cast_tokens(
11178                &task_hint,
11179                "Task",
11180                "task",
11181                &quote! { cu29::cutask::CuTask },
11182            );
11183            let live_process_completed = (sim_mode && cfg!(feature = "logstream")).then(|| quote! {
11184                let _ = sim_callback(SimStep::#enum_name(CuTaskCallbackState::ProcessCompleted(cumsg_output)));
11185            });
11186            let regular_process_tokens = quote! {
11187                #slot_cast_defs
11188
11189                #output_start_time
11190                #alloc_open
11191                let result = {
11192                    let cumsg_output = #regular_slot_match_fn_ident::<
11193                        _,
11194                        _,
11195                    >(&#task_instance, cumsg_output);
11196                    #rt_guard
11197                    ctx.set_current_task(#tid);
11198                    #task_instance.process(&ctx, cumsg_input, cumsg_output)
11199                };
11200                #output_end_time
11201                #live_process_completed
11202                #alloc_close
11203                result
11204            };
11205
11206            (
11207                wrap_process_step_tokens(
11208                    wrap_process_step,
11209                    quote! {
11210                        #task_setup
11211                        #parallel_task_preprocess
11212                        #comment_tokens
11213                        #freeze_task
11214                        #task_input_setup
11215                        #call_sim_callback
11216                        let cumsg_input = #task_input_expr;
11217                        let cumsg_output = &mut msgs.#output_culist_index;
11218                        let maybe_error = if doit {
11219                            execution_probe.record(cu29::monitoring::ExecutionMarker {
11220                                component_id: cu29::monitoring::ComponentId::new(#tid),
11221                                step: CuComponentState::Process,
11222                                culistid: Some(clid),
11223                            });
11224                            #regular_process_tokens
11225                        } else {
11226                            Ok(())
11227                        };
11228                        if let Err(error) = maybe_error {
11229                            #monitoring_action
11230                        }
11231                        #parallel_task_postprocess
11232                    },
11233                ),
11234                logging_tokens,
11235            )
11236        }
11237    }
11238}
11239
11240fn generate_bridge_rx_execution_tokens(
11241    step: &CuExecutionStep,
11242    bridge_spec: &BridgeSpec,
11243    channel_index: usize,
11244    ctx: StepGenerationContext<'_>,
11245    bridge_setup: proc_macro2::TokenStream,
11246) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
11247    let StepGenerationContext {
11248        output_pack_sizes: _,
11249        task_input_layouts: _,
11250        mission_name: _,
11251        sim_mode,
11252        keyframe_logging_enabled,
11253        mission_mod,
11254        lifecycle_placement,
11255        wrap_process_step,
11256    } = ctx;
11257    let rt_guard = rtsan_guard_tokens();
11258    let abort_process_step = abort_process_step_tokens(wrap_process_step);
11259    let channel = &bridge_spec.rx_channels[channel_index];
11260    let output_pack = step
11261        .output_msg_pack
11262        .as_ref()
11263        .expect("Bridge Rx channel missing output pack");
11264    let port_index = output_pack
11265        .msg_types
11266        .iter()
11267        .position(|msg| msg == &channel.msg_type_name)
11268        .unwrap_or_else(|| {
11269            panic!(
11270                "Bridge Rx channel '{}' missing output port for '{}'",
11271                channel.id, channel.msg_type_name
11272            )
11273        });
11274    let culist_index_ts = int2sliceindex(output_pack.culist_index);
11275    let output_ref = if output_pack.msg_types.len() == 1 {
11276        quote! { &mut msgs.#culist_index_ts }
11277    } else {
11278        let port_index = syn::Index::from(port_index);
11279        quote! { &mut msgs.#culist_index_ts.#port_index }
11280    };
11281    let monitor_index = syn::Index::from(
11282        channel
11283            .monitor_index
11284            .expect("Bridge Rx channel missing monitor index"),
11285    );
11286    let bridge_type = runtime_bridge_type_for_spec(bridge_spec, sim_mode);
11287    let (parallel_bridge_preprocess, parallel_bridge_postprocess) =
11288        parallel_bridge_lifecycle_tokens(
11289            &bridge_type,
11290            bridge_spec
11291                .monitor_index
11292                .expect("Bridge missing monitor index for lifecycle"),
11293            mission_mod,
11294            lifecycle_placement,
11295            keyframe_logging_enabled,
11296        );
11297    let const_ident = &channel.const_ident;
11298    let enum_ident = Ident::new(
11299        &config_id_to_enum(&format!("{}_rx_{}", bridge_spec.id, channel.id)),
11300        Span::call_site(),
11301    );
11302
11303    let call_sim_callback = if sim_mode {
11304        quote! {
11305            let doit = {
11306                let state = SimStep::#enum_ident {
11307                    channel: &<#bridge_type as cu29::cubridge::CuBridge>::Rx::#const_ident,
11308                    msg: cumsg_output,
11309                };
11310                let ovr = sim_callback(state);
11311                if let SimOverride::Errored(reason) = ovr {
11312                    let error: CuError = reason.into();
11313                    let decision = monitor.process_error(cu29::monitoring::ComponentId::new(#monitor_index), CuComponentState::Process, &error);
11314                    match decision {
11315                        Decision::Abort => {
11316                            debug!(ctx, "Process: ABORT decision from monitoring. Component '{}' errored out during process. Skipping the processing of CL {}.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)), clid);
11317                            #abort_process_step
11318                        }
11319                        Decision::Ignore => {
11320                            debug!(ctx, "Process: IGNORE decision from monitoring. Component '{}' errored out during process. The runtime will continue with a forced empty message.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
11321                            cumsg_output.clear_payload();
11322                            false
11323                        }
11324                        Decision::Shutdown => {
11325                            debug!(ctx, "Process: SHUTDOWN decision from monitoring. Component '{}' errored out during process. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
11326                            return Err(CuError::new_with_cause("Component errored out during process.", error));
11327                        }
11328                    }
11329                } else {
11330                    ovr == SimOverride::ExecuteByRuntime
11331                }
11332            };
11333        }
11334    } else {
11335        quote! { let doit = true; }
11336    };
11337    let alloc_open = alloc_scope_open_tokens();
11338    let alloc_close = alloc_scope_close_tokens(
11339        quote! { monitor },
11340        quote! { #monitor_index },
11341        quote! { CuComponentState::Process },
11342    );
11343    (
11344        wrap_process_step_tokens(
11345            wrap_process_step,
11346            quote! {
11347                #bridge_setup
11348                #parallel_bridge_preprocess
11349                let cumsg_output = #output_ref;
11350                #call_sim_callback
11351                if doit {
11352                    execution_probe.record(cu29::monitoring::ExecutionMarker {
11353                        component_id: cu29::monitoring::ComponentId::new(#monitor_index),
11354                        step: CuComponentState::Process,
11355                        culistid: Some(clid),
11356                    });
11357                    cumsg_output.metadata.process_time.start = cu29::curuntime::perf_now(clock).into();
11358                    #alloc_open
11359                    let maybe_error = {
11360                        #rt_guard
11361                        ctx.set_current_component(#monitor_index);
11362                        ctx.clear_current_task();
11363                        bridge.receive(
11364                            &ctx,
11365                            &<#bridge_type as cu29::cubridge::CuBridge>::Rx::#const_ident,
11366                            cumsg_output,
11367                        )
11368                    };
11369                    cumsg_output.metadata.process_time.end = cu29::curuntime::perf_now(clock).into();
11370                    #alloc_close
11371                    if let Err(error) = maybe_error {
11372                        let decision = monitor.process_error(cu29::monitoring::ComponentId::new(#monitor_index), CuComponentState::Process, &error);
11373                        match decision {
11374                            Decision::Abort => {
11375                                debug!(ctx, "Process: ABORT decision from monitoring. Component '{}' errored out during process. Skipping the processing of CL {}.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)), clid);
11376                                #abort_process_step
11377                            }
11378                            Decision::Ignore => {
11379                                debug!(ctx, "Process: IGNORE decision from monitoring. Component '{}' errored out during process. The runtime will continue with a forced empty message.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
11380                                cumsg_output.clear_payload();
11381                            }
11382                            Decision::Shutdown => {
11383                                debug!(ctx, "Process: SHUTDOWN decision from monitoring. Component '{}' errored out during process. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
11384                                return Err(CuError::new_with_cause("Component errored out during process.", error));
11385                            }
11386                        }
11387                    }
11388                }
11389                #parallel_bridge_postprocess
11390            },
11391        ),
11392        quote! {},
11393    )
11394}
11395
11396fn generate_bridge_tx_execution_tokens(
11397    step: &CuExecutionStep,
11398    bridge_spec: &BridgeSpec,
11399    channel_index: usize,
11400    ctx: StepGenerationContext<'_>,
11401    bridge_setup: proc_macro2::TokenStream,
11402) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
11403    let StepGenerationContext {
11404        output_pack_sizes,
11405        task_input_layouts: _,
11406        mission_name: _,
11407        sim_mode,
11408        keyframe_logging_enabled,
11409        mission_mod,
11410        lifecycle_placement,
11411        wrap_process_step,
11412    } = ctx;
11413    let rt_guard = rtsan_guard_tokens();
11414    let abort_process_step = abort_process_step_tokens(wrap_process_step);
11415    let channel = &bridge_spec.tx_channels[channel_index];
11416    let monitor_index = syn::Index::from(
11417        channel
11418            .monitor_index
11419            .expect("Bridge Tx channel missing monitor index"),
11420    );
11421    let input = step
11422        .input_msg_indices_types
11423        .first()
11424        .expect("Bridge Tx channel should have exactly one input");
11425    let input_index = int2sliceindex(input.culist_index);
11426    let output_size = output_pack_sizes
11427        .get(input.culist_index as usize)
11428        .copied()
11429        .unwrap_or_else(|| {
11430            panic!(
11431                "Missing output pack size for culist index {}",
11432                input.culist_index
11433            )
11434        });
11435    let input_ref = if output_size > 1 {
11436        let port_index = syn::Index::from(input.src_port);
11437        quote! { &mut msgs.#input_index.#port_index }
11438    } else {
11439        quote! { &mut msgs.#input_index }
11440    };
11441    let output_pack = step
11442        .output_msg_pack
11443        .as_ref()
11444        .expect("Bridge Tx channel missing output pack");
11445    if output_pack.msg_types.len() != 1 {
11446        panic!(
11447            "Bridge Tx channel '{}' expected a single output message slot, got {}",
11448            channel.id,
11449            output_pack.msg_types.len()
11450        );
11451    }
11452    let output_index = int2sliceindex(output_pack.culist_index);
11453    let output_ref = quote! { &mut msgs.#output_index };
11454    let bridge_type = runtime_bridge_type_for_spec(bridge_spec, sim_mode);
11455    let (parallel_bridge_preprocess, parallel_bridge_postprocess) =
11456        parallel_bridge_lifecycle_tokens(
11457            &bridge_type,
11458            bridge_spec
11459                .monitor_index
11460                .expect("Bridge missing monitor index for lifecycle"),
11461            mission_mod,
11462            lifecycle_placement,
11463            keyframe_logging_enabled,
11464        );
11465    let const_ident = &channel.const_ident;
11466    let enum_ident = Ident::new(
11467        &config_id_to_enum(&format!("{}_tx_{}", bridge_spec.id, channel.id)),
11468        Span::call_site(),
11469    );
11470
11471    let call_sim_callback = if sim_mode {
11472        quote! {
11473            let doit = {
11474                let state = SimStep::#enum_ident {
11475                    channel: &<#bridge_type as cu29::cubridge::CuBridge>::Tx::#const_ident,
11476                    msg: &*cumsg_input,
11477                    output: cumsg_output,
11478                };
11479                let ovr = sim_callback(state);
11480                if let SimOverride::Errored(reason) = ovr  {
11481                    let error: CuError = reason.into();
11482                    let decision = monitor.process_error(cu29::monitoring::ComponentId::new(#monitor_index), CuComponentState::Process, &error);
11483                    match decision {
11484                        Decision::Abort => {
11485                            debug!(ctx, "Process: ABORT decision from monitoring. Component '{}' errored out during process. Skipping the processing of CL {}.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)), clid);
11486                            #abort_process_step
11487                        }
11488                        Decision::Ignore => {
11489                            debug!(ctx, "Process: IGNORE decision from monitoring. Component '{}' errored out during process. The runtime will continue with a forced empty message.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
11490                            false
11491                        }
11492                        Decision::Shutdown => {
11493                            debug!(ctx, "Process: SHUTDOWN decision from monitoring. Component '{}' errored out during process. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
11494                            return Err(CuError::new_with_cause("Component errored out during process.", error));
11495                        }
11496                    }
11497                } else {
11498                    ovr == SimOverride::ExecuteByRuntime
11499                }
11500            };
11501        }
11502    } else {
11503        quote! { let doit = true; }
11504    };
11505    let alloc_open = alloc_scope_open_tokens();
11506    let alloc_close = alloc_scope_close_tokens(
11507        quote! { monitor },
11508        quote! { #monitor_index },
11509        quote! { CuComponentState::Process },
11510    );
11511    (
11512        wrap_process_step_tokens(
11513            wrap_process_step,
11514            quote! {
11515                #bridge_setup
11516                #parallel_bridge_preprocess
11517                let cumsg_input = #input_ref;
11518                let cumsg_output = #output_ref;
11519                let bridge_channel = &<#bridge_type as cu29::cubridge::CuBridge>::Tx::#const_ident;
11520                #call_sim_callback
11521                if doit {
11522                    execution_probe.record(cu29::monitoring::ExecutionMarker {
11523                        component_id: cu29::monitoring::ComponentId::new(#monitor_index),
11524                        step: CuComponentState::Process,
11525                        culistid: Some(clid),
11526                    });
11527                    cumsg_output.metadata.process_time.start = cu29::curuntime::perf_now(clock).into();
11528                    #alloc_open
11529                    let maybe_error = if bridge_channel.should_send(cumsg_input.payload().is_some()) {
11530                        {
11531                            #rt_guard
11532                            ctx.set_current_component(#monitor_index);
11533                            ctx.clear_current_task();
11534                            bridge.send(
11535                                &ctx,
11536                                bridge_channel,
11537                                &*cumsg_input,
11538                            )
11539                        }
11540                    } else {
11541                        Ok(())
11542                    };
11543                    #alloc_close
11544                    if let Err(error) = maybe_error {
11545                        let decision = monitor.process_error(cu29::monitoring::ComponentId::new(#monitor_index), CuComponentState::Process, &error);
11546                        match decision {
11547                            Decision::Abort => {
11548                                debug!(ctx, "Process: ABORT decision from monitoring. Component '{}' errored out during process. Skipping the processing of CL {}.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)), clid);
11549                                #abort_process_step
11550                            }
11551                            Decision::Ignore => {
11552                                debug!(ctx, "Process: IGNORE decision from monitoring. Component '{}' errored out during process. The runtime will continue with a forced empty message.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
11553                            }
11554                            Decision::Shutdown => {
11555                                debug!(ctx, "Process: SHUTDOWN decision from monitoring. Component '{}' errored out during process. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#monitor_index)));
11556                                return Err(CuError::new_with_cause("Component errored out during process.", error));
11557                            }
11558                        }
11559                    }
11560                    cumsg_output.metadata.process_time.end = cu29::curuntime::perf_now(clock).into();
11561                }
11562                #parallel_bridge_postprocess
11563            },
11564        ),
11565        quote! {},
11566    )
11567}
11568
11569#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
11570enum BridgeChannelDirection {
11571    Rx,
11572    Tx,
11573}
11574
11575#[derive(Clone, Debug, PartialEq, Eq, Hash)]
11576struct BridgeChannelKey {
11577    bridge_id: String,
11578    channel_id: String,
11579    direction: BridgeChannelDirection,
11580}
11581
11582#[derive(Clone)]
11583struct BridgeChannelSpec {
11584    id: String,
11585    const_ident: Ident,
11586    #[allow(dead_code)]
11587    msg_type: Type,
11588    msg_type_name: String,
11589    config_index: usize,
11590    plan_node_id: Option<NodeId>,
11591    culist_index: Option<usize>,
11592    monitor_index: Option<usize>,
11593}
11594
11595#[derive(Clone)]
11596struct BridgeSpec {
11597    id: String,
11598    type_path: Type,
11599    run_in_sim: bool,
11600    config_index: usize,
11601    tuple_index: usize,
11602    monitor_index: Option<usize>,
11603    rx_channels: Vec<BridgeChannelSpec>,
11604    tx_channels: Vec<BridgeChannelSpec>,
11605}
11606
11607#[derive(Clone, Copy, Debug, Default)]
11608struct ParallelLifecyclePlacement {
11609    preprocess: bool,
11610    postprocess: bool,
11611}
11612
11613#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
11614enum ParallelLifecycleKey {
11615    Task(usize),
11616    Bridge(usize),
11617}
11618
11619fn build_parallel_lifecycle_placements(
11620    culist_plan: &CuExecutionLoop,
11621    culist_exec_entities: &[ExecutionEntity],
11622) -> Vec<ParallelLifecyclePlacement> {
11623    let step_keys: Vec<Option<ParallelLifecycleKey>> = culist_plan
11624        .steps
11625        .iter()
11626        .map(|unit| match unit {
11627            CuExecutionUnit::Step(step) => {
11628                // Anytime refine steps emit no parallel stage (they run inside
11629                // the base step's stage), so they must not claim the task's
11630                // postprocess placement.
11631                if step.phase == CuStepPhase::AnytimeRefine {
11632                    return None;
11633                }
11634                match &culist_exec_entities[step.node_id as usize].kind {
11635                    ExecutionEntityKind::Task { task_index } => {
11636                        Some(ParallelLifecycleKey::Task(*task_index))
11637                    }
11638                    ExecutionEntityKind::BridgeRx { bridge_index, .. }
11639                    | ExecutionEntityKind::BridgeTx { bridge_index, .. } => {
11640                        Some(ParallelLifecycleKey::Bridge(*bridge_index))
11641                    }
11642                }
11643            }
11644            CuExecutionUnit::Loop(_) => None,
11645        })
11646        .collect();
11647
11648    let mut placements = vec![ParallelLifecyclePlacement::default(); step_keys.len()];
11649    let mut seen_forward = std::collections::HashSet::new();
11650    for (index, key) in step_keys.iter().enumerate() {
11651        let Some(key) = key else {
11652            continue;
11653        };
11654        if seen_forward.insert(*key) {
11655            placements[index].preprocess = true;
11656        }
11657    }
11658
11659    let mut seen_reverse = std::collections::HashSet::new();
11660    for (index, key) in step_keys.iter().enumerate().rev() {
11661        let Some(key) = key else {
11662            continue;
11663        };
11664        if seen_reverse.insert(*key) {
11665            placements[index].postprocess = true;
11666        }
11667    }
11668
11669    placements
11670}
11671
11672fn sim_bridge_channel_set_idents(bridge_tuple_index: usize) -> (Ident, Ident, Ident, Ident) {
11673    (
11674        format_ident!("__CuSimBridge{}TxChannels", bridge_tuple_index),
11675        format_ident!("__CuSimBridge{}TxId", bridge_tuple_index),
11676        format_ident!("__CuSimBridge{}RxChannels", bridge_tuple_index),
11677        format_ident!("__CuSimBridge{}RxId", bridge_tuple_index),
11678    )
11679}
11680
11681fn runtime_bridge_type_for_spec(bridge_spec: &BridgeSpec, sim_mode: bool) -> Type {
11682    if sim_mode && !bridge_spec.run_in_sim {
11683        let (tx_set_ident, _tx_id_ident, rx_set_ident, _rx_id_ident) =
11684            sim_bridge_channel_set_idents(bridge_spec.tuple_index);
11685        let tx_type: Type = if bridge_spec.tx_channels.is_empty() {
11686            parse_quote!(cu29::simulation::CuNoBridgeChannels)
11687        } else {
11688            parse_quote!(#tx_set_ident)
11689        };
11690        let rx_type: Type = if bridge_spec.rx_channels.is_empty() {
11691            parse_quote!(cu29::simulation::CuNoBridgeChannels)
11692        } else {
11693            parse_quote!(#rx_set_ident)
11694        };
11695        parse_quote!(cu29::simulation::CuSimBridge<#tx_type, #rx_type>)
11696    } else {
11697        bridge_spec.type_path.clone()
11698    }
11699}
11700
11701fn runtime_task_type_for_index(
11702    task_specs: &CuTaskSpecSet,
11703    graph: &CuGraph,
11704    index: usize,
11705    sim_mode: bool,
11706) -> Type {
11707    let task_id = &task_specs.ids[index];
11708    let declared_task_type = &task_specs.sim_task_types[index];
11709    let background = task_specs.background_flags[index];
11710    let run_in_sim = task_specs.run_in_sim_flags[index];
11711    let output_type = &task_specs.output_types[index];
11712
11713    match task_specs.cutypes[index] {
11714        CuTaskType::Source => {
11715            if sim_mode && !run_in_sim {
11716                let msg_types = graph
11717                    .get_node_output_msg_types(task_id.as_str())
11718                    .unwrap_or_else(|| {
11719                        panic!(
11720                            "CuSrcTask {task_id} should have an outgoing connection with a valid output msg type"
11721                        )
11722                    });
11723                let sim_task_name = if msg_types.len() == 1 {
11724                    format!("CuSimSrcTask<{}>", msg_types[0])
11725                } else {
11726                    let messages = msg_types
11727                        .iter()
11728                        .map(|msg_type| format!("cu29::prelude::CuMsg<{msg_type}>"))
11729                        .collect::<Vec<_>>()
11730                        .join(", ");
11731                    format!("CuSimSrcTaskPack<({messages})>")
11732                };
11733                parse_str(sim_task_name.as_str()).unwrap_or_else(|_| {
11734                    panic!("Could not build the placeholder for simulation: {sim_task_name}")
11735                })
11736            } else if background {
11737                if let Some(out_ty) = output_type {
11738                    parse_quote!(CuAsyncSrcTask<#declared_task_type, #out_ty>)
11739                } else {
11740                    panic!("{task_id}: If a source is background, it has to have an output");
11741                }
11742            } else {
11743                declared_task_type.clone()
11744            }
11745        }
11746        CuTaskType::Regular => {
11747            if background {
11748                if output_type.is_some() {
11749                    task_specs.task_types[index].clone()
11750                } else {
11751                    panic!("{task_id}: If a task is background, it has to have an output");
11752                }
11753            } else {
11754                // run_in_sim has no effect for regular tasks; they always run as themselves in sim.
11755                declared_task_type.clone()
11756            }
11757        }
11758        CuTaskType::Sink => {
11759            if background {
11760                panic!(
11761                    "CuSinkTask {task_id} cannot be a background task, it should be a regular task."
11762                );
11763            }
11764
11765            if sim_mode && !run_in_sim {
11766                let msg_types = graph.get_node_input_msg_types(task_id.as_str()).unwrap_or_else(|| {
11767                    panic!(
11768                        "CuSinkTask {task_id} should have an incoming connection with a valid input msg type"
11769                    )
11770                });
11771                let msg_type = if msg_types.len() == 1 {
11772                    format!("({},)", msg_types[0])
11773                } else {
11774                    format!("({})", msg_types.join(", "))
11775                };
11776                let sim_task_name = format!("CuSimSinkTask<{msg_type}>");
11777                parse_str(sim_task_name.as_str()).unwrap_or_else(|_| {
11778                    panic!("Could not build the placeholder for simulation: {sim_task_name}")
11779                })
11780            } else {
11781                declared_task_type.clone()
11782            }
11783        }
11784    }
11785}
11786
11787#[derive(Clone)]
11788struct ExecutionEntity {
11789    kind: ExecutionEntityKind,
11790}
11791
11792#[derive(Clone)]
11793enum ExecutionEntityKind {
11794    Task {
11795        task_index: usize,
11796    },
11797    BridgeRx {
11798        bridge_index: usize,
11799        channel_index: usize,
11800    },
11801    BridgeTx {
11802        bridge_index: usize,
11803        channel_index: usize,
11804    },
11805}
11806
11807#[cfg(test)]
11808mod tests {
11809    use super::*;
11810    use std::fs;
11811    use std::path::{Path, PathBuf};
11812
11813    fn unique_test_dir(name: &str) -> PathBuf {
11814        let nanos = std::time::SystemTime::now()
11815            .duration_since(std::time::UNIX_EPOCH)
11816            .expect("system clock before unix epoch")
11817            .as_nanos();
11818        std::env::temp_dir().join(format!("cu29_derive_{name}_{nanos}"))
11819    }
11820
11821    fn write_file(path: &Path, content: &str) {
11822        if let Some(parent) = path.parent() {
11823            fs::create_dir_all(parent).expect("create parent dirs");
11824        }
11825        fs::write(path, content).expect("write file");
11826    }
11827
11828    #[test]
11829    fn disabled_keyframe_capture_emits_no_freeze_calls() {
11830        let task = quote! { tasks.0 };
11831        assert!(keyframe_freeze_task_tokens(false, &task).is_empty());
11832        assert!(keyframe_freeze_bridge_tokens(false).is_empty());
11833        assert!(
11834            keyframe_freeze_task_tokens(true, &task)
11835                .to_string()
11836                .contains("freeze_task")
11837        );
11838        assert!(
11839            keyframe_freeze_bridge_tokens(true)
11840                .to_string()
11841                .contains("freeze_any")
11842        );
11843    }
11844
11845    // Compile fixtures assert diagnostic codes or short messages at the failing line.
11846    #[test]
11847    fn test_compile_fail() {
11848        use ui_test::custom_flags::edition::Edition;
11849        use ui_test::custom_flags::rustfix::RustfixMode;
11850        use ui_test::dependencies::DependencyBuilder;
11851        use ui_test::{Config, default_file_filter, default_per_file_config, run_tests_generic};
11852
11853        let root = Path::new(env!("CARGO_MANIFEST_DIR"));
11854        let target_dir = std::env::var_os("CARGO_TARGET_DIR")
11855            .map(PathBuf::from)
11856            .unwrap_or_else(|| root.join("../../target"));
11857        let log_index_dir = target_dir.join("ui/derive/log_index/a/b/c");
11858        fs::create_dir_all(&log_index_dir).expect("create compile-test log index directory");
11859
11860        let mut dependencies = DependencyBuilder {
11861            crate_manifest_path: root.join("tests/dependencies/Cargo.toml"),
11862            // Lockfiles are generated locally and are not committed in this workspace.
11863            bless_lockfile: true,
11864            ..DependencyBuilder::default()
11865        };
11866        // Share Cargo's normal artifact cache between the fail and pass suites.
11867        dependencies.program.out_dir_flag = None;
11868        dependencies.program.envs.push((
11869            "LOG_INDEX_DIR".into(),
11870            Some(log_index_dir.clone().into_os_string()),
11871        ));
11872
11873        let mut config = Config::rustc(root.join("tests/compile_fail"));
11874        config.out_dir = target_dir.join("ui/derive");
11875        config.output_conflict_handling = ui_test::ignore_output_conflict;
11876        config
11877            .program
11878            .envs
11879            .push(("LOG_INDEX_DIR".into(), Some(log_index_dir.into_os_string())));
11880        let defaults = config.comment_defaults.base();
11881        defaults.custom.remove("edition");
11882        defaults.custom.remove("rustfix");
11883        defaults.add_custom("edition", Edition("2024".into()));
11884        defaults.add_custom("rustfix", RustfixMode::Disabled);
11885        defaults.add_custom("dependencies", dependencies);
11886
11887        let mut pass = config.clone();
11888        pass.root_dir = build_compile_pass_umbrella();
11889        run_tests_generic(
11890            vec![config, pass],
11891            default_file_filter,
11892            default_per_file_config,
11893            ui_test::status_emitter::Text::quiet(),
11894        )
11895        .expect("compile tests failed");
11896    }
11897
11898    fn build_compile_pass_umbrella() -> std::path::PathBuf {
11899        use std::fmt::Write as _;
11900        let pass_dir = std::path::Path::new("tests/compile_pass");
11901        let mut entries: Vec<std::path::PathBuf> = Vec::new();
11902        for sub in std::fs::read_dir(pass_dir).expect("read tests/compile_pass") {
11903            let sub = sub.expect("read tests/compile_pass entry");
11904            if !sub
11905                .file_type()
11906                .expect("stat tests/compile_pass entry")
11907                .is_dir()
11908            {
11909                continue;
11910            }
11911            for file in std::fs::read_dir(sub.path()).expect("read compile_pass subdir") {
11912                let p = file.expect("read compile_pass subdir entry").path();
11913                if p.extension().and_then(|x| x.to_str()) == Some("rs") {
11914                    entries.push(p);
11915                }
11916            }
11917        }
11918        entries.sort();
11919
11920        let mut src =
11921            String::from("//@check-pass\n#![allow(dead_code, unused_imports, non_snake_case)]\n");
11922        for p in &entries {
11923            let abs = std::fs::canonicalize(p)
11924                .unwrap_or_else(|e| panic!("canonicalize {}: {e}", p.display()));
11925            let subdir = abs
11926                .parent()
11927                .and_then(|d| d.file_name())
11928                .map(|s| s.to_string_lossy().into_owned())
11929                .unwrap_or_default();
11930            let stem = abs
11931                .file_stem()
11932                .expect("compile_pass file has stem")
11933                .to_string_lossy()
11934                .into_owned();
11935            let mod_name = format!("{subdir}_{stem}").replace('-', "_");
11936            writeln!(
11937                src,
11938                "#[path = {:?}] mod compile_pass_{};",
11939                abs.to_string_lossy(),
11940                mod_name
11941            )
11942            .expect("write to String");
11943        }
11944        src.push_str("fn main() {}\n");
11945
11946        // Keep the umbrella outside `tests/` so cargo doesn't pick it up as an
11947        // integration test.
11948        let target_dir = std::env::var_os("CARGO_TARGET_DIR")
11949            .map(std::path::PathBuf::from)
11950            .unwrap_or_else(|| std::path::PathBuf::from("../../target"));
11951        let umbrella_dir = target_dir.join("generated");
11952        std::fs::create_dir_all(&umbrella_dir)
11953            .unwrap_or_else(|e| panic!("create {}: {e}", umbrella_dir.display()));
11954        let umbrella = umbrella_dir.join("compile_pass_umbrella.rs");
11955        if std::fs::read_to_string(&umbrella).ok().as_deref() != Some(src.as_str()) {
11956            std::fs::write(&umbrella, &src)
11957                .unwrap_or_else(|e| panic!("write {}: {e}", umbrella.display()));
11958        }
11959        std::fs::canonicalize(&umbrella)
11960            .unwrap_or_else(|e| panic!("canonicalize {}: {e}", umbrella.display()))
11961    }
11962
11963    #[test]
11964    fn runtime_plan_keeps_nc_order_for_non_first_connected_output() {
11965        use super::*;
11966        use cu29::config::CuConfig;
11967        use cu29::curuntime::{CuExecutionUnit, compute_runtime_plan};
11968
11969        let config: CuConfig =
11970            read_config("tests/config/multi_output_source_non_first_connected_valid.ron")
11971                .expect("failed to read test config");
11972        let graph = config.get_graph(None).expect("missing graph");
11973        let src_id = graph.get_node_id_by_name("src").expect("missing src node");
11974
11975        let runtime = compute_runtime_plan(graph).expect("runtime plan failed");
11976        let src_step = runtime
11977            .steps
11978            .iter()
11979            .find_map(|step| match step {
11980                CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
11981                _ => None,
11982            })
11983            .expect("missing source step");
11984
11985        assert_eq!(
11986            src_step.output_msg_pack.as_ref().unwrap().msg_types,
11987            vec!["i32", "bool"]
11988        );
11989    }
11990
11991    #[test]
11992    fn matching_task_ids_are_flattened_per_output_message() {
11993        use super::*;
11994        use cu29::config::CuConfig;
11995
11996        let config: CuConfig =
11997            read_config("tests/config/multi_output_source_non_first_connected_valid.ron")
11998                .expect("failed to read test config");
11999        let graph = config.get_graph(None).expect("missing graph");
12000        let channel_usage = collect_bridge_channel_usage(graph);
12001        let mut bridge_specs = build_bridge_specs(&config, graph, &channel_usage);
12002        let (runtime_plan, exec_entities, plan_to_original) =
12003            build_execution_plan(&config, graph, "default", &mut bridge_specs)
12004                .expect("runtime plan failed");
12005        let output_packs = extract_output_packs(&runtime_plan);
12006        let task_names = collect_task_names(graph);
12007        let (_, node_output_positions) = collect_culist_metadata(
12008            &runtime_plan,
12009            &exec_entities,
12010            &mut bridge_specs,
12011            &plan_to_original,
12012        );
12013
12014        // Rebuild per-slot origin ids like `gen_culist_support` does.
12015        let mut slot_origin_ids: Vec<Option<String>> = vec![None; output_packs.len()];
12016        for (node_id, task_id, _) in task_names {
12017            let output_position = node_output_positions
12018                .get(&node_id)
12019                .unwrap_or_else(|| panic!("Task {task_id} (node id: {node_id}) not found"));
12020            slot_origin_ids[*output_position] = Some(task_id);
12021        }
12022
12023        let flattened_ids = flatten_slot_origin_ids(&output_packs, &slot_origin_ids);
12024
12025        // src emits two messages (i32 + bool), both map to src.
12026        // sink contributes its own output slot (CuMsg<()>), mapped to sink.
12027        assert_eq!(
12028            flattened_ids,
12029            vec!["src".to_string(), "src".to_string(), "sink".to_string()]
12030        );
12031    }
12032
12033    #[test]
12034    fn bridge_resources_are_collected() {
12035        use super::*;
12036        use cu29::config::{CuGraph, Flavor, Node};
12037        use std::collections::HashMap;
12038        use syn::parse_str;
12039
12040        let mut graph = CuGraph::default();
12041        let mut node = Node::new_with_flavor("radio", "bridge::Dummy", Flavor::Bridge);
12042        let mut res = HashMap::new();
12043        res.insert("serial".to_string(), "fc.serial0".to_string());
12044        node.set_resources(Some(res));
12045        graph.add_node(node).expect("bridge node");
12046
12047        let task_specs = CuTaskSpecSet::from_graph(&graph).expect("task specs");
12048        let bridge_spec = BridgeSpec {
12049            id: "radio".to_string(),
12050            type_path: parse_str("bridge::Dummy").unwrap(),
12051            run_in_sim: true,
12052            config_index: 0,
12053            tuple_index: 0,
12054            monitor_index: None,
12055            rx_channels: Vec::new(),
12056            tx_channels: Vec::new(),
12057        };
12058
12059        let mut config = cu29::config::CuConfig::default();
12060        config.resources.push(ResourceBundleConfig {
12061            resources: None,
12062            id: "fc".to_string(),
12063            provider: "board::Bundle".to_string(),
12064            config: None,
12065            missions: None,
12066        });
12067        let bundle_specs = build_bundle_specs(&config, "default").expect("bundle specs");
12068        let specs = collect_resource_specs(&graph, &task_specs, &[bridge_spec], &bundle_specs)
12069            .expect("collect specs");
12070        assert_eq!(specs.len(), 1);
12071        assert!(matches!(specs[0].owner, ResourceOwner::Bridge(0)));
12072        assert_eq!(specs[0].binding_name, "serial");
12073        assert_eq!(specs[0].bundle_index, 0);
12074        assert_eq!(specs[0].resource_name, "serial0");
12075    }
12076
12077    #[test]
12078    fn copper_runtime_args_parse_subsystem_mode() {
12079        use super::*;
12080        use quote::quote;
12081
12082        let args = CopperRuntimeArgs::parse_tokens(quote!(
12083            config = "multi_copper.ron",
12084            subsystem = "ping",
12085            sim_mode,
12086            ignore_resources
12087        ))
12088        .expect("parse runtime args");
12089
12090        assert_eq!(args.config_path, "multi_copper.ron");
12091        assert_eq!(args.subsystem_id.as_deref(), Some("ping"));
12092        assert!(args.sim_mode);
12093        assert!(args.ignore_resources);
12094    }
12095
12096    #[test]
12097    fn resolve_runtime_config_from_multi_config_selects_local_subsystem() {
12098        use super::*;
12099
12100        let root = unique_test_dir("multi_runtime_resolve");
12101        let alpha_config = root.join("alpha.ron");
12102        let beta_base_config = root.join("beta_base.ron");
12103        let beta_config = root.join("beta.ron");
12104        let network_config = root.join("multi.ron");
12105
12106        write_file(
12107            &alpha_config,
12108            r#"
12109(
12110    tasks: [
12111        (id: "src", type: "AlphaSource", run_in_sim: true),
12112        (id: "sink", type: "AlphaSink", run_in_sim: true),
12113    ],
12114    cnx: [
12115        (src: "src", dst: "sink", msg: "u32"),
12116    ],
12117)
12118"#,
12119        );
12120        write_file(
12121            &beta_base_config,
12122            r#"
12123(
12124    tasks: [
12125        (id: "src", type: "BetaSource", run_in_sim: true),
12126    ],
12127)
12128"#,
12129        );
12130        write_file(
12131            &beta_config,
12132            r#"
12133(
12134    includes: [
12135        (path: "beta_base.ron", params: {}),
12136    ],
12137    tasks: [
12138        (id: "sink", type: "BetaSink", run_in_sim: true),
12139    ],
12140    cnx: [
12141        (src: "src", dst: "sink", msg: "u64"),
12142    ],
12143)
12144"#,
12145        );
12146        write_file(
12147            &network_config,
12148            r#"
12149(
12150    subsystems: [
12151        (id: "beta", config: "beta.ron"),
12152        (id: "alpha", config: "alpha.ron"),
12153    ],
12154    interconnects: [],
12155)
12156"#,
12157        );
12158
12159        let args = CopperRuntimeArgs {
12160            config_path: "multi.ron".to_string(),
12161            subsystem_id: Some("beta".to_string()),
12162            sim_mode: false,
12163            ignore_resources: false,
12164        };
12165
12166        let resolved =
12167            resolve_runtime_config_with_root(&args, &root).expect("resolve multi runtime config");
12168
12169        assert_eq!(resolved.subsystem_id.as_deref(), Some("beta"));
12170        assert_eq!(resolved.subsystem_code, 1);
12171        let graph = resolved
12172            .local_config
12173            .get_graph(None)
12174            .expect("resolved local config graph");
12175        assert!(graph.get_node_id_by_name("src").is_some());
12176        assert!(resolved.bundled_local_config_content.contains("BetaSource"));
12177        assert!(
12178            !resolved
12179                .bundled_local_config_content
12180                .contains("beta_base.ron")
12181        );
12182
12183        let bundled = CuConfig::deserialize_ron(&resolved.bundled_local_config_content)
12184            .expect("bundled subsystem config must not need include path resolution");
12185        let bundled_graph = bundled.get_graph(None).expect("bundled graph");
12186        assert!(bundled_graph.get_node_id_by_name("src").is_some());
12187        assert!(bundled_graph.get_node_id_by_name("sink").is_some());
12188        assert_eq!(bundled_graph.edge_count(), 1);
12189    }
12190
12191    #[test]
12192    fn resolve_runtime_config_bundles_resolved_single_config() {
12193        use super::*;
12194
12195        let root = unique_test_dir("single_runtime_resolve");
12196        let base_config = root.join("base.ron");
12197        let app_config = root.join("app.ron");
12198
12199        write_file(
12200            &base_config,
12201            r#"
12202(
12203    tasks: [
12204        (id: "src", type: "IncludedSource", run_in_sim: true),
12205    ],
12206)
12207"#,
12208        );
12209        write_file(
12210            &app_config,
12211            r#"
12212(
12213    includes: [
12214        (path: "base.ron", params: {}),
12215    ],
12216    tasks: [
12217        (id: "sink", type: "LocalSink", run_in_sim: true),
12218    ],
12219    cnx: [
12220        (src: "src", dst: "sink", msg: "u32"),
12221    ],
12222)
12223"#,
12224        );
12225
12226        let args = CopperRuntimeArgs {
12227            config_path: "app.ron".to_string(),
12228            subsystem_id: None,
12229            sim_mode: false,
12230            ignore_resources: false,
12231        };
12232
12233        let resolved =
12234            resolve_runtime_config_with_root(&args, &root).expect("resolve single runtime config");
12235
12236        assert!(
12237            resolved
12238                .bundled_local_config_content
12239                .contains("IncludedSource")
12240        );
12241        assert!(!resolved.bundled_local_config_content.contains("base.ron"));
12242        let bundled = CuConfig::deserialize_ron(&resolved.bundled_local_config_content)
12243            .expect("bundled config must not need include path resolution");
12244        let graph = bundled.get_graph(None).expect("bundled graph");
12245        assert!(graph.get_node_id_by_name("src").is_some());
12246        assert!(graph.get_node_id_by_name("sink").is_some());
12247        assert_eq!(graph.edge_count(), 1);
12248    }
12249
12250    #[test]
12251    fn resolve_runtime_config_uses_forwarded_features_for_codegen_and_reload() {
12252        use super::*;
12253
12254        let root = unique_test_dir("feature_runtime_resolve");
12255        let camera_config = root.join("camera.ron");
12256        let app_config = root.join("app.ron");
12257
12258        write_file(
12259            &camera_config,
12260            r#"
12261(
12262    tasks: [
12263        (id: "camera", type: "target_camera::Camera"),
12264        (id: "sink", type: "tasks::CameraSink"),
12265    ],
12266    cnx: [
12267        (src: "camera", dst: "sink", msg: "target_camera::Frame"),
12268    ],
12269)
12270"#,
12271        );
12272        write_file(
12273            &app_config,
12274            r#"
12275(
12276    tasks: [],
12277    cnx: [],
12278    includes: [
12279        (path: "camera.ron", when: Feature("camera")),
12280    ],
12281)
12282"#,
12283        );
12284
12285        let args = CopperRuntimeArgs {
12286            config_path: "app.ron".to_string(),
12287            subsystem_id: None,
12288            sim_mode: false,
12289            ignore_resources: false,
12290        };
12291
12292        let without_camera =
12293            resolve_runtime_config_with_root_and_features(&args, &root, &[]).unwrap();
12294        assert!(without_camera.active_features.is_empty());
12295        assert!(
12296            !without_camera
12297                .bundled_local_config_content
12298                .contains("target_camera")
12299        );
12300
12301        let with_camera =
12302            resolve_runtime_config_with_root_and_features(&args, &root, &["camera"]).unwrap();
12303        assert_eq!(with_camera.active_features, ["camera"]);
12304        assert!(
12305            with_camera
12306                .bundled_local_config_content
12307                .contains("target_camera::Camera")
12308        );
12309        assert!(
12310            with_camera
12311                .bundled_local_config_content
12312                .contains("target_camera::Frame")
12313        );
12314        assert!(!with_camera.bundled_local_config_content.contains("Feature"));
12315    }
12316
12317    #[test]
12318    fn resolve_multi_runtime_config_uses_forwarded_features() {
12319        use super::*;
12320
12321        let root = unique_test_dir("feature_multi_runtime_resolve");
12322        write_file(
12323            &root.join("camera.ron"),
12324            r#"
12325(
12326    tasks: [
12327        (id: "camera", type: "target_camera::Camera"),
12328        (id: "sink", type: "tasks::CameraSink"),
12329    ],
12330    cnx: [
12331        (src: "camera", dst: "sink", msg: "target_camera::Frame"),
12332    ],
12333)
12334"#,
12335        );
12336        write_file(
12337            &root.join("robot.ron"),
12338            r#"
12339(
12340    tasks: [],
12341    cnx: [],
12342    includes: [
12343        (path: "camera.ron", when: Feature("camera")),
12344    ],
12345)
12346"#,
12347        );
12348        write_file(
12349            &root.join("multi.ron"),
12350            r#"
12351(
12352    subsystems: [
12353        (id: "robot", config: "robot.ron"),
12354    ],
12355    interconnects: [],
12356)
12357"#,
12358        );
12359
12360        let args = CopperRuntimeArgs {
12361            config_path: "multi.ron".to_string(),
12362            subsystem_id: Some("robot".to_string()),
12363            sim_mode: false,
12364            ignore_resources: false,
12365        };
12366
12367        let without_camera =
12368            resolve_runtime_config_with_root_and_features(&args, &root, &[]).unwrap();
12369        assert_eq!(
12370            without_camera
12371                .local_config
12372                .get_graph(None)
12373                .unwrap()
12374                .node_count(),
12375            0
12376        );
12377
12378        let with_camera =
12379            resolve_runtime_config_with_root_and_features(&args, &root, &["camera"]).unwrap();
12380        assert_eq!(
12381            with_camera
12382                .local_config
12383                .get_graph(None)
12384                .unwrap()
12385                .node_count(),
12386            2
12387        );
12388        assert!(
12389            with_camera
12390                .bundled_local_config_content
12391                .contains("target_camera::Frame")
12392        );
12393    }
12394
12395    #[test]
12396    fn resolve_runtime_config_preserves_mission_task_order_in_bundle() {
12397        use super::*;
12398
12399        let root = unique_test_dir("mission_runtime_resolve_order");
12400        let base_config = root.join("base.ron");
12401        let app_config = root.join("app.ron");
12402
12403        write_file(
12404            &base_config,
12405            r#"
12406(
12407    tasks: [
12408        (id: "c", type: "TaskC", missions: ["one", "two"]),
12409    ],
12410)
12411"#,
12412        );
12413        write_file(
12414            &app_config,
12415            r#"
12416(
12417    includes: [
12418        (path: "base.ron", params: {}),
12419    ],
12420    missions: [
12421        (id: "one"),
12422        (id: "two"),
12423    ],
12424    tasks: [
12425        (id: "a", type: "TaskA", missions: ["two"]),
12426        (id: "b", type: "TaskB", missions: ["one"]),
12427    ],
12428)
12429"#,
12430        );
12431
12432        let args = CopperRuntimeArgs {
12433            config_path: "app.ron".to_string(),
12434            subsystem_id: None,
12435            sim_mode: false,
12436            ignore_resources: false,
12437        };
12438        let resolved = resolve_runtime_config_with_root(&args, &root)
12439            .expect("resolve mission config with includes");
12440        let bundled = CuConfig::deserialize_ron(&resolved.bundled_local_config_content)
12441            .expect("bundled mission config");
12442
12443        let task_order = |config: &CuConfig, mission: &str| {
12444            config
12445                .get_graph(Some(mission))
12446                .expect("mission graph")
12447                .get_all_nodes()
12448                .into_iter()
12449                .filter(|(_, node)| node.get_flavor() == Flavor::Task)
12450                .map(|(_, node)| node.get_id())
12451                .collect::<Vec<_>>()
12452        };
12453
12454        assert_eq!(task_order(&resolved.local_config, "one"), vec!["b", "c"]);
12455        assert_eq!(task_order(&resolved.local_config, "two"), vec!["a", "c"]);
12456        assert_eq!(
12457            task_order(&bundled, "one"),
12458            task_order(&resolved.local_config, "one")
12459        );
12460        assert_eq!(
12461            task_order(&bundled, "two"),
12462            task_order(&resolved.local_config, "two")
12463        );
12464        assert!(!resolved.bundled_local_config_content.contains("base.ron"));
12465    }
12466
12467    #[test]
12468    fn resolve_runtime_config_rejects_missing_subsystem() {
12469        use super::*;
12470
12471        let root = unique_test_dir("multi_runtime_missing_subsystem");
12472        let alpha_config = root.join("alpha.ron");
12473        let network_config = root.join("multi.ron");
12474
12475        write_file(
12476            &alpha_config,
12477            r#"
12478(
12479    tasks: [
12480        (id: "src", type: "AlphaSource", run_in_sim: true),
12481        (id: "sink", type: "AlphaSink", run_in_sim: true),
12482    ],
12483    cnx: [
12484        (src: "src", dst: "sink", msg: "u32"),
12485    ],
12486)
12487"#,
12488        );
12489        write_file(
12490            &network_config,
12491            r#"
12492(
12493    subsystems: [
12494        (id: "alpha", config: "alpha.ron"),
12495    ],
12496    interconnects: [],
12497)
12498"#,
12499        );
12500
12501        let args = CopperRuntimeArgs {
12502            config_path: "multi.ron".to_string(),
12503            subsystem_id: Some("missing".to_string()),
12504            sim_mode: false,
12505            ignore_resources: false,
12506        };
12507
12508        let err = resolve_runtime_config_with_root(&args, &root).expect_err("missing subsystem");
12509        assert!(err.to_string().contains("Subsystem 'missing'"));
12510    }
12511
12512    #[test]
12513    fn synthesized_single_output_type_name_parses_for_source_and_regular_tasks() {
12514        use super::*;
12515
12516        let src_ty: Type = parse_quote!(SingleSource);
12517        let regular_ty: Type = parse_quote!(RegularTask);
12518
12519        let src_name = synthesized_single_output_msg_name(&src_ty, CuTaskType::Source, false);
12520        let regular_name =
12521            synthesized_single_output_msg_name(&regular_ty, CuTaskType::Regular, false);
12522        let anytime_name =
12523            synthesized_single_output_msg_name(&regular_ty, CuTaskType::Regular, true);
12524
12525        parse_str::<Type>(src_name.as_str()).expect("source payload type should parse");
12526        parse_str::<Type>(regular_name.as_str()).expect("regular payload type should parse");
12527        parse_str::<Type>(anytime_name.as_str()).expect("anytime payload type should parse");
12528    }
12529}