Skip to main content

cu29_derive/
lib.rs

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