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