Skip to main content

cu29_derive/
lib.rs

1#[cfg(feature = "clock-sync")]
2mod clock_sync;
3mod live_twin;
4use proc_macro::TokenStream;
5use quote::{ToTokens, format_ident, quote};
6use std::collections::{BTreeMap, BTreeSet, HashMap};
7use std::path::Path;
8use std::process::Command;
9use syn::Fields::{Named, Unnamed};
10use syn::ext::IdentExt;
11use syn::meta::parser;
12use syn::parse::Parser;
13use syn::{
14    Field, Fields, ItemImpl, ItemStruct, LitStr, Type, TypeTuple, parse_macro_input, parse_quote,
15    parse_str,
16};
17
18use crate::utils::{config_id_to_bridge_const, config_id_to_enum, config_id_to_struct_member};
19use cu29_build::COPPER_CFG_FEATURES_ENV;
20use cu29_runtime::config::CuConfig;
21use cu29_runtime::config::DEFAULT_MISSION_ID;
22use cu29_runtime::config::{
23    AnytimeConfig, BridgeChannelConfigRepresentation, ConfigGraphs, CuGraph, Flavor, HandleContent,
24    Node, NodeId, PlannerKind, RT_POOL, ResourceBundleConfig, SchedulingPolicy, TaskKind,
25    read_configuration_with_features, read_configuration_with_resolved_ron_and_features,
26};
27use cu29_runtime::curuntime::{
28    CuExecutionLoop, CuExecutionStep, CuExecutionUnit, CuStepPhase, CuTaskType,
29    find_task_type_for_id,
30};
31use cu29_runtime::planner::{
32    CuPlanBackground, CuPlanBackgroundResult, CuPlanPlacement, CuPlanThread,
33    DEFAULT_COPPERLIST_COUNT, LanePlan, PlanEntityKind, assemble_runtime_plan_for_mission,
34};
35use cu29_traits::{CuError, CuResult};
36use proc_macro2::{Ident, Span};
37
38mod bundle_resources;
39mod constants;
40mod resources;
41mod safety;
42mod utils;
43
44use constants::build_constant_modules;
45
46#[inline]
47fn int2sliceindex(i: u32) -> syn::Index {
48    syn::Index::from(i as usize)
49}
50
51#[inline(always)]
52fn return_error(msg: String) -> TokenStream {
53    syn::Error::new(Span::call_site(), msg)
54        .to_compile_error()
55        .into()
56}
57
58fn rtsan_guard_tokens() -> proc_macro2::TokenStream {
59    if cfg!(feature = "rtsan") {
60        quote! {
61            let _rt_guard = ::cu29::rtsan::ScopedSanitizeRealtime::default();
62        }
63    } else {
64        quote! {}
65    }
66}
67/// Opens a heap-allocation accounting scope (binds `__cu_alloc_scope`) iff the
68/// `memory_monitoring` feature is enabled. Otherwise expands to nothing, so a
69/// default build emits zero extra code per task step.
70fn alloc_scope_open_tokens() -> proc_macro2::TokenStream {
71    if cfg!(feature = "memory_monitoring") {
72        quote! {
73            let __cu_alloc_scope = cu29::monitoring::ScopedAllocCounter::new();
74        }
75    } else {
76        quote! {}
77    }
78}
79
80/// Forwards the scope's accumulated delta to `monitor.observe_alloc(...)` and
81/// drops the scope. `monitor_expr` is whatever expression reaches the monitor
82/// in the current code block (`monitor` for preprocess/process/postprocess,
83/// `self.copper_runtime.monitor` for start/stop). `component_index` and `step`
84/// are the tokens to be passed through to `ComponentId::new(...)` and the
85/// `CuComponentState` variant.
86fn alloc_scope_close_tokens(
87    monitor_expr: proc_macro2::TokenStream,
88    component_index: proc_macro2::TokenStream,
89    step: proc_macro2::TokenStream,
90) -> proc_macro2::TokenStream {
91    if cfg!(feature = "memory_monitoring") {
92        quote! {
93            #monitor_expr.observe_alloc(
94                cu29::monitoring::ComponentId::new(#component_index),
95                #step,
96                __cu_alloc_scope.allocated(),
97                __cu_alloc_scope.deallocated(),
98            );
99        }
100    } else {
101        quote! {}
102    }
103}
104
105fn git_output_trimmed(repo_root: &Path, args: &[&str]) -> Option<String> {
106    let output = Command::new("git")
107        .arg("-C")
108        .arg(repo_root)
109        .args(args)
110        .output()
111        .ok()?;
112    if !output.status.success() {
113        return None;
114    }
115    let stdout = String::from_utf8(output.stdout).ok()?;
116    Some(stdout.trim().to_string())
117}
118
119fn detect_git_info(repo_root: &Path) -> (Option<String>, Option<bool>) {
120    let in_repo = git_output_trimmed(repo_root, &["rev-parse", "--is-inside-work-tree"])
121        .is_some_and(|value| value == "true");
122    if !in_repo {
123        return (None, None);
124    }
125
126    let commit = git_output_trimmed(repo_root, &["rev-parse", "HEAD"]).filter(|s| !s.is_empty());
127    // Porcelain output is empty when tree is clean.
128    let dirty = git_output_trimmed(repo_root, &["status", "--porcelain"]).map(|s| !s.is_empty());
129    (commit, dirty)
130}
131
132#[derive(Debug, Clone)]
133struct CopperRuntimeArgs {
134    config_path: String,
135    subsystem_id: Option<String>,
136    sim_mode: bool,
137    ignore_resources: bool,
138}
139
140impl CopperRuntimeArgs {
141    fn parse_tokens(args: proc_macro2::TokenStream) -> Result<Self, syn::Error> {
142        let mut config_file: Option<LitStr> = None;
143        let mut subsystem_id: Option<LitStr> = None;
144        let mut sim_mode = false;
145        let mut ignore_resources = false;
146
147        let parser = parser(|meta| {
148            if meta.path.is_ident("config") {
149                config_file = Some(meta.value()?.parse()?);
150                Ok(())
151            } else if meta.path.is_ident("subsystem") {
152                subsystem_id = Some(meta.value()?.parse()?);
153                Ok(())
154            } else if meta.path.is_ident("sim_mode") {
155                if meta.input.peek(syn::Token![=]) {
156                    meta.input.parse::<syn::Token![=]>()?;
157                    let value: syn::LitBool = meta.input.parse()?;
158                    sim_mode = value.value();
159                } else {
160                    sim_mode = true;
161                }
162                Ok(())
163            } else if meta.path.is_ident("ignore_resources") {
164                if meta.input.peek(syn::Token![=]) {
165                    meta.input.parse::<syn::Token![=]>()?;
166                    let value: syn::LitBool = meta.input.parse()?;
167                    ignore_resources = value.value();
168                } else {
169                    ignore_resources = true;
170                }
171                Ok(())
172            } else {
173                Err(meta.error("unsupported property"))
174            }
175        });
176
177        parser.parse2(args)?;
178
179        let config_path = config_file
180            .ok_or_else(|| {
181                syn::Error::new(
182                    Span::call_site(),
183                    "Expected config file attribute like #[copper_runtime(config = \"path\")]",
184                )
185            })?
186            .value();
187
188        Ok(Self {
189            config_path,
190            subsystem_id: subsystem_id.map(|value| value.value()),
191            sim_mode,
192            ignore_resources,
193        })
194    }
195}
196
197#[derive(Debug)]
198struct ResolvedRuntimeConfig {
199    local_config: CuConfig,
200    bundled_local_config_content: String,
201    active_features: Vec<String>,
202    subsystem_id: Option<String>,
203    subsystem_code: u16,
204}
205
206#[proc_macro]
207pub fn resources(input: TokenStream) -> TokenStream {
208    resources::resources(input)
209}
210
211#[proc_macro]
212pub fn bundle_resources(input: TokenStream) -> TokenStream {
213    bundle_resources::bundle_resources(input)
214}
215
216#[proc_macro_attribute]
217pub fn safety_case(args: TokenStream, input: TokenStream) -> TokenStream {
218    safety::expand(args, input)
219}
220
221/// Generates the CopperList content type from a config.
222/// gen_cumsgs!("path/to/config.toml")
223/// It will create a new type called CuStampedDataSet you can pass to the log reader for decoding:
224#[proc_macro]
225pub fn gen_cumsgs(config_path_lit: TokenStream) -> TokenStream {
226    #[cfg(feature = "std")]
227    let std = true;
228
229    #[cfg(not(feature = "std"))]
230    let std = false;
231    let config = parse_macro_input!(config_path_lit as LitStr).value();
232    if !std::path::Path::new(&config_full_path(&config)).exists() {
233        return return_error(format!(
234            "The configuration file `{config}` does not exist. Please provide a valid path."
235        ));
236    }
237    #[cfg(feature = "macro_debug")]
238    eprintln!("[gen culist support with {config:?}]");
239    let cuconfig = match read_config(&config) {
240        Ok(cuconfig) => cuconfig,
241        Err(e) => return return_error(e.to_string()),
242    };
243
244    let extra_imports = if !std {
245        quote! {
246            use core::fmt::Debug;
247            use core::fmt::Formatter;
248            use core::fmt::Result as FmtResult;
249            use alloc::vec;
250            use alloc::vec::Vec;
251        }
252    } else {
253        quote! {
254            use std::fmt::Debug;
255            use std::fmt::Formatter;
256            use std::fmt::Result as FmtResult;
257        }
258    };
259
260    let common_imports = quote! {
261        use cu29::bincode::Encode;
262        use cu29::bincode::enc::Encoder;
263        use cu29::bincode::error::EncodeError;
264        use cu29::bincode::Decode;
265        use cu29::bincode::de::Decoder;
266        use cu29::bincode::error::DecodeError;
267        use cu29::copperlist::CopperList;
268        use cu29::prelude::ErasedCuStampedData;
269        use cu29::prelude::ErasedCuStampedDataSet;
270        use cu29::prelude::MatchingTasks;
271        use cu29::prelude::CuMsg;
272        use cu29::prelude::CuMsgMetadata;
273        use cu29::prelude::CuListZeroedInit;
274        use cu29::prelude::CuCompactString;
275        #extra_imports
276    };
277
278    let with_uses = match &cuconfig.graphs {
279        ConfigGraphs::Simple(graph) => {
280            let support = match build_gen_cumsgs_support(&cuconfig, graph, None) {
281                Ok(support) => support,
282                Err(e) => return return_error(e.to_string()),
283            };
284
285            quote! {
286                mod cumsgs {
287                    #common_imports
288                    #support
289                }
290                use cumsgs::CuStampedDataSet;
291                type CuMsgs=CuStampedDataSet;
292            }
293        }
294        ConfigGraphs::Missions(graphs) => {
295            let mut missions: Vec<_> = graphs.iter().collect();
296            missions.sort_by(|a, b| a.0.cmp(b.0));
297
298            let mut mission_modules = Vec::<proc_macro2::TokenStream>::new();
299            for (mission, graph) in missions {
300                let mission_mod = match parse_str::<Ident>(mission.as_str()) {
301                    Ok(id) => id,
302                    Err(_) => {
303                        return return_error(format!(
304                            "Mission '{mission}' is not a valid Rust identifier for gen_cumsgs output."
305                        ));
306                    }
307                };
308
309                let support = match build_gen_cumsgs_support(&cuconfig, graph, Some(mission)) {
310                    Ok(support) => support,
311                    Err(e) => return return_error(e.to_string()),
312                };
313
314                mission_modules.push(quote! {
315                    pub mod #mission_mod {
316                        #common_imports
317                        #support
318                    }
319                });
320            }
321
322            let default_exports = if graphs.contains_key("default") {
323                quote! {
324                    use cumsgs::default::CuStampedDataSet;
325                    type CuMsgs=CuStampedDataSet;
326                }
327            } else {
328                quote! {}
329            };
330
331            quote! {
332                mod cumsgs {
333                    #(#mission_modules)*
334                }
335                #default_exports
336            }
337        }
338    };
339    with_uses.into()
340}
341
342fn build_gen_cumsgs_support(
343    cuconfig: &CuConfig,
344    graph: &CuGraph,
345    mission_label: Option<&str>,
346) -> CuResult<proc_macro2::TokenStream> {
347    let channel_usage = collect_bridge_channel_usage(graph);
348    let mut bridge_specs = build_bridge_specs(cuconfig, graph, &channel_usage);
349    let (culist_plan, exec_entities, plan_to_original) = build_execution_plan(
350        cuconfig,
351        graph,
352        mission_label.unwrap_or(DEFAULT_MISSION_ID),
353        &mut bridge_specs,
354    )
355    .map_err(|e| {
356        if let Some(mission) = mission_label {
357            CuError::from(format!(
358                "Could not compute copperlist plan for mission '{mission}': {e}"
359            ))
360        } else {
361            CuError::from(format!("Could not compute copperlist plan: {e}"))
362        }
363    })?;
364    let task_names = collect_task_names(graph);
365    let (culist_order, node_output_positions) = collect_culist_metadata(
366        &culist_plan,
367        &exec_entities,
368        &mut bridge_specs,
369        &plan_to_original,
370    );
371
372    #[cfg(feature = "macro_debug")]
373    if let Some(mission) = mission_label {
374        eprintln!(
375            "[The CuStampedDataSet matching tasks ids for mission '{mission}' are {:?}]",
376            culist_order
377        );
378    } else {
379        eprintln!(
380            "[The CuStampedDataSet matching tasks ids are {:?}]",
381            culist_order
382        );
383    }
384
385    Ok(gen_culist_support(
386        CuListSupportOptions {
387            cuconfig,
388            distributed: false,
389        },
390        mission_label,
391        &culist_plan,
392        &culist_order,
393        &node_output_positions,
394        &task_names,
395        &bridge_specs,
396    ))
397}
398
399/// Build the inner support of the copper list.
400struct CuListSupportOptions<'a> {
401    cuconfig: &'a CuConfig,
402    distributed: bool,
403}
404
405fn gen_culist_support(
406    options: CuListSupportOptions<'_>,
407    mission_label: Option<&str>,
408    runtime_plan: &CuExecutionLoop,
409    culist_indices_in_plan_order: &[usize],
410    node_output_positions: &HashMap<NodeId, usize>,
411    task_names: &[(NodeId, String, String)],
412    bridge_specs: &[BridgeSpec],
413) -> proc_macro2::TokenStream {
414    let CuListSupportOptions {
415        cuconfig,
416        distributed,
417    } = options;
418    #[cfg(feature = "macro_debug")]
419    eprintln!("[Extract msgs types]");
420    let output_packs = extract_output_packs(runtime_plan);
421    let slot_types: Vec<Type> = output_packs.iter().map(|pack| pack.slot_type()).collect();
422
423    let culist_size = output_packs.len();
424
425    #[cfg(feature = "macro_debug")]
426    eprintln!("[build the copperlist struct]");
427    let msgs_types_tuple: Type = if distributed {
428        parse_quote!(CuMessageRegions)
429    } else {
430        let tuple = build_culist_tuple(&slot_types);
431        parse_quote!(#tuple)
432    };
433    let isolated_regions = if distributed {
434        quote! {
435            #[repr(C)]
436            pub struct CuMessageRegions(
437                #(pub cu29::copperlist::MessageRegion<#slot_types>),*
438            );
439        }
440    } else {
441        quote! {}
442    };
443    let dataset_alignment = distributed.then(|| quote!(#[repr(align(128))]));
444    let cumsg_count: usize = output_packs.iter().map(|pack| pack.msg_types.len()).sum();
445    let flat_codec_bindings = build_flat_slot_codec_bindings(
446        cuconfig,
447        mission_label,
448        &output_packs,
449        node_output_positions,
450        task_names,
451    )
452    .unwrap_or_else(|err| panic!("Could not resolve log codec bindings: {err}"));
453    let default_config_ron_ident = format_ident!("__CU_LOGCODEC_DEFAULT_CONFIG_RON");
454    let default_config_ron = cuconfig
455        .serialize_ron()
456        .unwrap_or_else(|_| "<failed to serialize config>".to_string());
457    let default_config_ron_lit = LitStr::new(&default_config_ron, Span::call_site());
458    let (codec_helper_fns, encode_helper_names, decode_helper_names) = build_culist_codec_helpers(
459        &flat_codec_bindings,
460        &default_config_ron_ident,
461        mission_label,
462    );
463    let default_config_ron_const = if flat_codec_bindings.iter().any(Option::is_some) {
464        quote! {
465            const #default_config_ron_ident: &str = #default_config_ron_lit;
466        }
467    } else {
468        quote! {}
469    };
470
471    #[cfg(feature = "macro_debug")]
472    eprintln!("[build the copperlist tuple bincode support]");
473    let slot_handle_modes = build_slot_handle_modes(
474        cuconfig,
475        mission_label,
476        &output_packs,
477        node_output_positions,
478        task_names,
479    );
480    let compressed = !cfg!(feature = "flat-copperlist-encoding");
481    let (msgs_types_tuple_encode, msgs_types_tuple_decode) = if compressed {
482        (
483            build_compressed_culist_tuple_encode(
484                &output_packs,
485                &encode_helper_names,
486                &slot_handle_modes,
487                cumsg_count,
488                None,
489            ),
490            build_compressed_culist_tuple_decode(
491                &output_packs,
492                &slot_types,
493                cumsg_count,
494                &decode_helper_names,
495                distributed,
496            ),
497        )
498    } else {
499        (
500            build_culist_tuple_encode(&output_packs, &encode_helper_names, &slot_handle_modes),
501            build_culist_tuple_decode(
502                &output_packs,
503                &slot_types,
504                cumsg_count,
505                &decode_helper_names,
506                distributed,
507            ),
508        )
509    };
510
511    #[cfg(feature = "macro_debug")]
512    eprintln!("[build the copperlist tuple debug support]");
513    let msgs_types_tuple_debug = build_culist_tuple_debug(&slot_types);
514
515    #[cfg(feature = "macro_debug")]
516    eprintln!("[build the copperlist tuple serialize support]");
517    let msgs_types_tuple_serialize = build_culist_tuple_serialize(&slot_types);
518
519    #[cfg(feature = "macro_debug")]
520    eprintln!("[build the default tuple support]");
521    let msgs_types_tuple_default =
522        build_culist_tuple_default(&slot_types, cumsg_count, distributed);
523
524    #[cfg(feature = "macro_debug")]
525    eprintln!("[build erasedcumsgs]");
526
527    let erasedmsg_trait_impl = build_culist_erasedcumsgs(&output_packs, distributed);
528
529    let metadata_accessors: Vec<proc_macro2::TokenStream> = culist_indices_in_plan_order
530        .iter()
531        .map(|idx| {
532            let slot_index = syn::Index::from(*idx);
533            let pack = output_packs
534                .get(*idx)
535                .unwrap_or_else(|| panic!("Missing output pack for index {idx}"));
536            if pack.is_multi() {
537                quote! { &culist.msgs.0.#slot_index.0.metadata }
538            } else {
539                quote! { &culist.msgs.0.#slot_index.metadata }
540            }
541        })
542        .collect();
543    let mut zeroed_init_tokens: Vec<proc_macro2::TokenStream> = Vec::new();
544    let mut in_place_init_tokens: Vec<proc_macro2::TokenStream> = Vec::new();
545    for idx in culist_indices_in_plan_order {
546        let slot_index = syn::Index::from(*idx);
547        let pack = output_packs
548            .get(*idx)
549            .unwrap_or_else(|| panic!("Missing output pack for index {idx}"));
550        let region_value = distributed.then(|| quote!(.value));
551        if pack.is_multi() {
552            for port_idx in 0..pack.msg_types.len() {
553                let port_index = syn::Index::from(port_idx);
554                in_place_init_tokens.push(quote! {
555                    CuMsg::init_in_place(core::ptr::addr_of_mut!((*dst).0.#slot_index #region_value .#port_index));
556                });
557                zeroed_init_tokens.push(quote! {
558                    self.0.#slot_index.#port_index.metadata.status_txt = CuCompactString::default();
559                    self.0.#slot_index.#port_index.metadata.process_time.start =
560                        cu29::clock::OptionCuTime::none();
561                    self.0.#slot_index.#port_index.metadata.process_time.end =
562                        cu29::clock::OptionCuTime::none();
563                    self.0.#slot_index.#port_index.metadata.origin = None;
564                });
565            }
566        } else {
567            in_place_init_tokens.push(quote! {
568                CuMsg::init_in_place(core::ptr::addr_of_mut!((*dst).0.#slot_index #region_value));
569            });
570            zeroed_init_tokens.push(quote! {
571                self.0.#slot_index.metadata.status_txt = CuCompactString::default();
572                self.0.#slot_index.metadata.process_time.start = cu29::clock::OptionCuTime::none();
573                self.0.#slot_index.metadata.process_time.end = cu29::clock::OptionCuTime::none();
574                self.0.#slot_index.metadata.origin = None;
575            });
576        }
577    }
578    let collect_metadata_function = quote! {
579        pub fn collect_metadata<'a>(culist: &'a CuList) -> [&'a CuMsgMetadata; #culist_size] {
580            [#( #metadata_accessors, )*]
581        }
582    };
583
584    let payload_bytes_accumulators: Vec<proc_macro2::TokenStream> = culist_indices_in_plan_order
585        .iter()
586        .scan(0usize, |flat_idx, idx| {
587            let slot_index = syn::Index::from(*idx);
588            let pack = output_packs
589                .get(*idx)
590                .unwrap_or_else(|| panic!("Missing output pack for index {idx}"));
591            if pack.is_multi() {
592                let iter = (0..pack.msg_types.len()).map(|port_idx| {
593                    let port_index = syn::Index::from(port_idx);
594                    let cache_index = syn::Index::from(*flat_idx);
595                    *flat_idx += 1;
596                    quote! {
597                        if let Some(payload) = culist.msgs.0.#slot_index.#port_index.payload() {
598                            let cached = culist.msgs.1.get(#cache_index);
599                            let io = if cached.present {
600                                cu29::monitoring::PayloadIoStats {
601                                    resident_bytes: cached.resident_bytes as usize,
602                                    encoded_bytes: cached.encoded_bytes as usize,
603                                    handle_bytes: cached.handle_bytes as usize,
604                                }
605                            } else {
606                                cu29::monitoring::payload_io_stats(payload)?
607                            };
608                            raw += io.resident_bytes;
609                            handles += io.handle_bytes;
610                        }
611                    }
612                });
613                Some(quote! { #(#iter)* })
614            } else {
615                let cache_index = syn::Index::from(*flat_idx);
616                *flat_idx += 1;
617                Some(quote! {
618                    if let Some(payload) = culist.msgs.0.#slot_index.payload() {
619                        let cached = culist.msgs.1.get(#cache_index);
620                        let io = if cached.present {
621                            cu29::monitoring::PayloadIoStats {
622                                resident_bytes: cached.resident_bytes as usize,
623                                encoded_bytes: cached.encoded_bytes as usize,
624                                handle_bytes: cached.handle_bytes as usize,
625                            }
626                        } else {
627                            cu29::monitoring::payload_io_stats(payload)?
628                        };
629                        raw += io.resident_bytes;
630                        handles += io.handle_bytes;
631                    }
632                })
633            }
634        })
635        .collect();
636
637    let payload_raw_bytes_accumulators: Vec<proc_macro2::TokenStream> = output_packs
638        .iter()
639        .enumerate()
640        .scan(0usize, |flat_idx, (slot_idx, pack)| {
641            let slot_index = syn::Index::from(slot_idx);
642            if pack.is_multi() {
643                let iter = (0..pack.msg_types.len()).map(|port_idx| {
644                    let port_index = syn::Index::from(port_idx);
645                    let cache_index = syn::Index::from(*flat_idx);
646                    *flat_idx += 1;
647                    quote! {
648                        if let Some(payload) = self.0.#slot_index.#port_index.payload() {
649                            let cached = self.1.get(#cache_index);
650                            bytes.push(if cached.present {
651                                Some(cached.resident_bytes)
652                            } else {
653                                cu29::monitoring::payload_io_stats(payload)
654                                    .ok()
655                                    .map(|io| io.resident_bytes as u64)
656                            });
657                        } else {
658                            bytes.push(None);
659                        }
660                    }
661                });
662                Some(quote! { #(#iter)* })
663            } else {
664                let cache_index = syn::Index::from(*flat_idx);
665                *flat_idx += 1;
666                Some(quote! {
667                    if let Some(payload) = self.0.#slot_index.payload() {
668                        let cached = self.1.get(#cache_index);
669                        bytes.push(if cached.present {
670                            Some(cached.resident_bytes)
671                        } else {
672                            cu29::monitoring::payload_io_stats(payload)
673                                .ok()
674                                .map(|io| io.resident_bytes as u64)
675                        });
676                    } else {
677                        bytes.push(None);
678                    }
679                })
680            }
681        })
682        .collect();
683
684    let compute_payload_bytes_fn = quote! {
685        pub fn compute_payload_bytes(culist: &CuList) -> cu29::prelude::CuResult<(u64, u64)> {
686            let mut raw: usize = 0;
687            let mut handles: usize = 0;
688            #(#payload_bytes_accumulators)*
689            Ok((raw as u64, handles as u64))
690        }
691    };
692
693    let payload_raw_bytes_impl = quote! {
694        impl ::cu29::CuPayloadRawBytes for CuStampedDataSet {
695            fn payload_raw_bytes(&self) -> Vec<Option<u64>> {
696                let mut bytes: Vec<Option<u64>> = Vec::with_capacity(#cumsg_count);
697                #(#payload_raw_bytes_accumulators)*
698                bytes
699            }
700        }
701    };
702
703    let mut slot_origin_ids: Vec<Option<String>> = vec![None; output_packs.len()];
704    let mut slot_task_names: Vec<Option<String>> = vec![None; output_packs.len()];
705
706    let mut methods = Vec::new();
707    for (node_id, task_id, member_name) in task_names {
708        let output_position = node_output_positions.get(node_id).unwrap_or_else(|| {
709            panic!("Task {task_id} (node id: {node_id}) not found in execution order")
710        });
711        let pack = output_packs
712            .get(*output_position)
713            .unwrap_or_else(|| panic!("Missing output pack for task {task_id}"));
714        let slot_index = syn::Index::from(*output_position);
715        slot_origin_ids[*output_position] = Some(task_id.clone());
716        slot_task_names[*output_position] = Some(member_name.clone());
717
718        if pack.msg_types.len() == 1 {
719            let fn_name = format_ident!("get_{}_output", member_name);
720            let payload_type = pack.msg_types.first().unwrap();
721            methods.push(quote! {
722                #[allow(dead_code)]
723                pub fn #fn_name(&self) -> &CuMsg<#payload_type> {
724                    &self.0.#slot_index
725                }
726            });
727        } else {
728            let outputs_fn = format_ident!("get_{}_outputs", member_name);
729            let slot_type = pack.slot_type();
730            for (port_idx, payload_type) in pack.msg_types.iter().enumerate() {
731                let fn_name = format_ident!("get_{}_output_{}", member_name, port_idx);
732                let port_index = syn::Index::from(port_idx);
733                methods.push(quote! {
734                    #[allow(dead_code)]
735                    pub fn #fn_name(&self) -> &CuMsg<#payload_type> {
736                        &self.0.#slot_index.#port_index
737                    }
738                });
739            }
740            methods.push(quote! {
741                #[allow(dead_code)]
742                pub fn #outputs_fn(&self) -> &#slot_type {
743                    &self.0.#slot_index
744                }
745            });
746        }
747    }
748
749    for spec in bridge_specs {
750        for channel in &spec.rx_channels {
751            if let Some(culist_index) = channel.culist_index {
752                let origin_id = format!("bridge::{}::rx::{}", spec.id, channel.id);
753                let Some(existing_slot) = slot_origin_ids.get_mut(culist_index) else {
754                    panic!(
755                        "Bridge origin '{origin_id}' points to out-of-range copperlist slot {culist_index}"
756                    );
757                };
758                if let Some(existing) = existing_slot.as_ref() {
759                    panic!(
760                        "Duplicate slot origin assignment for slot {culist_index}: '{existing}' and '{origin_id}'"
761                    );
762                }
763                *existing_slot = Some(origin_id.clone());
764                let Some(slot_name) = slot_task_names.get_mut(culist_index) else {
765                    panic!(
766                        "Bridge origin '{origin_id}' points to out-of-range name slot {culist_index}"
767                    );
768                };
769                *slot_name = Some(origin_id);
770            }
771        }
772        for channel in &spec.tx_channels {
773            if let Some(culist_index) = channel.culist_index {
774                let origin_id = format!("bridge::{}::tx::{}", spec.id, channel.id);
775                let Some(existing_slot) = slot_origin_ids.get_mut(culist_index) else {
776                    panic!(
777                        "Bridge origin '{origin_id}' points to out-of-range copperlist slot {culist_index}"
778                    );
779                };
780                if let Some(existing) = existing_slot.as_ref() {
781                    panic!(
782                        "Duplicate slot origin assignment for slot {culist_index}: '{existing}' and '{origin_id}'"
783                    );
784                }
785                *existing_slot = Some(origin_id.clone());
786                let Some(slot_name) = slot_task_names.get_mut(culist_index) else {
787                    panic!(
788                        "Bridge origin '{origin_id}' points to out-of-range name slot {culist_index}"
789                    );
790                };
791                *slot_name = Some(origin_id);
792            }
793        }
794    }
795
796    let task_name_literals = flatten_slot_origin_ids(&output_packs, &slot_origin_ids);
797    let task_output_specs = flatten_task_output_specs(&output_packs, &slot_origin_ids);
798    let task_output_spec_literals: Vec<proc_macro2::TokenStream> = task_output_specs
799        .iter()
800        .map(|(task_id, msg_type, payload_type)| {
801            let task_id = LitStr::new(task_id, Span::call_site());
802            let msg_type = LitStr::new(msg_type, Span::call_site());
803            quote! {
804                cu29::TaskOutputSpec::new::<#payload_type>(#task_id, #msg_type)
805            }
806        })
807        .collect();
808
809    let catalog_description = if cfg!(feature = "self-describing-logs") {
810        let mut slots = Vec::new();
811        let mut error = None;
812        for (index, (task_id, msg_type, payload_type)) in task_output_specs.iter().enumerate() {
813            let captured = (cfg!(feature = "logstream")
814                || cuconfig
815                    .logging
816                    .as_ref()
817                    .is_none_or(|logging| logging.enable_task_logging))
818                && cuconfig
819                    .find_task_node(mission_label, task_id)
820                    .is_none_or(|node| node.is_logging_enabled());
821            let payload = if !captured {
822                quote! { None }
823            } else if let Some(codec) = &flat_codec_bindings[index] {
824                error = Some(format!(
825                    "Task '{task_id}' output '{msg_type}' uses logging codec '{}', which needs a catalog description of its encoded representation.",
826                    codec.codec_type_path
827                ));
828                quote! { None }
829            } else {
830                quote! { Some(cu29::bincode::value_decode::ValueDecodeRef::of::<#payload_type>()) }
831            };
832            slots.push(quote! { cu29::catalog_stream::CatalogSlot { task_id: #task_id, msg_type: #msg_type, payload: #payload } });
833        }
834        let error = match error {
835            Some(error) => quote! { Some(#error) },
836            None => quote! { None },
837        };
838        quote! {
839            #[allow(dead_code)]
840            pub const VALUE_DECODE_CATALOG_MISSION: cu29::catalog_stream::CatalogMission = cu29::catalog_stream::CatalogMission {
841                slots: &[#(#slots),*],
842            };
843            #[allow(dead_code)]
844            pub const VALUE_DECODE_CATALOG_ERROR: Option<&'static str> = #error;
845        }
846    } else {
847        quote! {}
848    };
849
850    // Generate bridge channel getter methods
851    for spec in bridge_specs {
852        for channel in &spec.rx_channels {
853            if let Some(culist_index) = channel.culist_index {
854                let slot_index = syn::Index::from(culist_index);
855                let bridge_name = config_id_to_struct_member(spec.id.as_str());
856                let channel_name = config_id_to_struct_member(channel.id.as_str());
857                let fn_name = format_ident!("get_{}_rx_{}", bridge_name, channel_name);
858                let msg_type = &channel.msg_type;
859
860                methods.push(quote! {
861                    #[allow(dead_code)]
862                    pub fn #fn_name(&self) -> &CuMsg<#msg_type> {
863                        &self.0.#slot_index
864                    }
865                });
866            }
867        }
868    }
869
870    let capture_support = if cfg!(feature = "logstream") {
871        live_twin::dataset_support(
872            runtime_plan,
873            &output_packs,
874            &encode_helper_names,
875            &slot_handle_modes,
876            cumsg_count,
877            &flat_codec_bindings,
878        )
879    } else {
880        quote! {}
881    };
882
883    // This generates a way to get the metadata of every single message of a culist at low cost
884    quote! {
885        #capture_support
886        #catalog_description
887        #collect_metadata_function
888        #compute_payload_bytes_fn
889        #default_config_ron_const
890        #(#codec_helper_fns)*
891
892        #isolated_regions
893        #dataset_alignment
894        pub struct CuStampedDataSet(pub #msgs_types_tuple, cu29::monitoring::CuMsgIoCache<#cumsg_count>);
895
896        pub type CuList = CopperList<CuStampedDataSet>;
897
898        const TASK_OUTPUT_SPECS: &[cu29::TaskOutputSpec] = &[
899            #(#task_output_spec_literals),*
900        ];
901
902        impl CuStampedDataSet {
903            #(#methods)*
904
905            #[allow(dead_code)]
906            fn get_tuple(&self) -> &#msgs_types_tuple {
907                &self.0
908            }
909
910            #[allow(dead_code)]
911            fn get_tuple_mut(&mut self) -> &mut #msgs_types_tuple {
912                &mut self.0
913            }
914        }
915
916        #payload_raw_bytes_impl
917        impl MatchingTasks for CuStampedDataSet {
918            #[allow(dead_code)]
919            fn get_all_task_ids() -> &'static [&'static str] {
920                &[#(#task_name_literals),*]
921            }
922
923            #[allow(dead_code)]
924            fn get_output_specs() -> &'static [cu29::TaskOutputSpec] {
925                TASK_OUTPUT_SPECS
926            }
927        }
928
929        // Adds the bincode support for the copper list tuple
930        #msgs_types_tuple_encode
931        #msgs_types_tuple_decode
932
933        // Adds the debug support
934        #msgs_types_tuple_debug
935
936        // Adds the serialization support
937        #msgs_types_tuple_serialize
938
939        // Adds the default support
940        #msgs_types_tuple_default
941
942        // Adds the type erased CuStampedDataSet support (to help generic serialized conversions)
943        #erasedmsg_trait_impl
944
945        impl CuListZeroedInit for CuStampedDataSet {
946            const DISTRIBUTED: bool = #distributed;
947
948            fn init_in_place(storage: &mut core::mem::MaybeUninit<Self>) -> &mut Self {
949                let dst = storage.as_mut_ptr();
950                // SAFETY: The pool supplies uninitialized dataset storage. Every
951                // message and the I/O cache are written before it is borrowed.
952                unsafe {
953                    #(#in_place_init_tokens)*
954                    core::ptr::addr_of_mut!((*dst).1)
955                        .write(cu29::monitoring::CuMsgIoCache::default());
956                    storage.assume_init_mut()
957                }
958            }
959
960            fn init_zeroed(&mut self) {
961                self.1.clear();
962                #(#zeroed_init_tokens)*
963            }
964        }
965    }
966}
967
968fn gen_sim_support(
969    runtime_plan: &CuExecutionLoop,
970    exec_entities: &[ExecutionEntity],
971    bridge_specs: &[BridgeSpec],
972) -> proc_macro2::TokenStream {
973    #[cfg(feature = "macro_debug")]
974    eprintln!("[Sim: Build SimEnum]");
975    let plan_enum: Vec<proc_macro2::TokenStream> = runtime_plan
976        .steps
977        .iter()
978        .filter(|unit| {
979            // The sim callback fires once per node, in the base step; refine
980            // steps get no SimStep variant of their own.
981            !matches!(unit, CuExecutionUnit::Step(step) if step.phase == CuStepPhase::AnytimeRefine)
982        })
983        .map(|unit| match unit {
984            CuExecutionUnit::Step(step) => match &exec_entities[step.node_id as usize].kind {
985                ExecutionEntityKind::Task { .. } => {
986                    let enum_entry_name = config_id_to_enum(step.node.get_id().as_str());
987                    let enum_ident = Ident::new(&enum_entry_name, Span::call_site());
988                    let inputs: Vec<Type> = step
989                        .input_msg_indices_types
990                        .iter()
991                        .map(|input| {
992                            parse_str::<Type>(format!("CuMsg<{}>", input.msg_type).as_str()).unwrap()
993                        })
994                        .collect();
995                    let output: Option<Type> = step.output_msg_pack.as_ref().map(|pack| {
996                        let msg_types: Vec<Type> = pack
997                            .msg_types
998                            .iter()
999                            .map(|msg_type| {
1000                                parse_str::<Type>(msg_type.as_str()).unwrap_or_else(|_| {
1001                                    panic!("Could not transform {msg_type} into a message Rust type.")
1002                                })
1003                            })
1004                            .collect();
1005                        build_output_slot_type(&msg_types)
1006                    });
1007                    let no_output = parse_str::<Type>("CuMsg<()>").unwrap();
1008                    let output = output.as_ref().unwrap_or(&no_output);
1009
1010                    let inputs_type = if inputs.is_empty() {
1011                        quote! { () }
1012                    } else if inputs.len() == 1 {
1013                        let input = inputs.first().unwrap();
1014                        quote! { &'a #input }
1015                    } else {
1016                        quote! { &'a (#(&'a #inputs),*) }
1017                    };
1018
1019                    quote! {
1020                        #enum_ident(CuTaskCallbackState<#inputs_type, &'a mut #output>)
1021                    }
1022                }
1023                ExecutionEntityKind::BridgeRx { bridge_index, channel_index } => {
1024                    let bridge_spec = &bridge_specs[*bridge_index];
1025                    let channel = &bridge_spec.rx_channels[*channel_index];
1026                    let enum_entry_name = config_id_to_enum(&format!("{}_rx_{}", bridge_spec.id, channel.id));
1027                    let enum_ident = Ident::new(&enum_entry_name, Span::call_site());
1028                    let channel_type: Type = parse_str::<Type>(channel.msg_type_name.as_str()).unwrap();
1029                    let bridge_type = runtime_bridge_type_for_spec(bridge_spec, true);
1030                    let _const_ident = &channel.const_ident;
1031                    quote! {
1032                        #enum_ident {
1033                            channel: &'static cu29::cubridge::BridgeChannel<< <#bridge_type as cu29::cubridge::CuBridge>::Rx as cu29::cubridge::BridgeChannelSet >::Id, #channel_type>,
1034                            msg: &'a mut CuMsg<#channel_type>,
1035                        }
1036                    }
1037                }
1038                ExecutionEntityKind::BridgeTx { bridge_index, channel_index } => {
1039                    let bridge_spec = &bridge_specs[*bridge_index];
1040                    let channel = &bridge_spec.tx_channels[*channel_index];
1041                    let enum_entry_name = config_id_to_enum(&format!("{}_tx_{}", bridge_spec.id, channel.id));
1042                    let enum_ident = Ident::new(&enum_entry_name, Span::call_site());
1043                    let channel_type: Type = parse_str::<Type>(channel.msg_type_name.as_str()).unwrap();
1044                    let output_pack = step
1045                        .output_msg_pack
1046                        .as_ref()
1047                        .expect("Bridge Tx channel missing output pack for sim support");
1048                    let output_types: Vec<Type> = output_pack
1049                        .msg_types
1050                        .iter()
1051                        .map(|msg_type| {
1052                            parse_str::<Type>(msg_type.as_str()).unwrap_or_else(|_| {
1053                                panic!("Could not transform {msg_type} into a message Rust type.")
1054                            })
1055                        })
1056                        .collect();
1057                    let output_type = build_output_slot_type(&output_types);
1058                    let bridge_type = runtime_bridge_type_for_spec(bridge_spec, true);
1059                    let _const_ident = &channel.const_ident;
1060                    quote! {
1061                        #enum_ident {
1062                            channel: &'static cu29::cubridge::BridgeChannel<< <#bridge_type as cu29::cubridge::CuBridge>::Tx as cu29::cubridge::BridgeChannelSet >::Id, #channel_type>,
1063                            msg: &'a CuMsg<#channel_type>,
1064                            output: &'a mut #output_type,
1065                        }
1066                    }
1067                }
1068            },
1069            CuExecutionUnit::Loop(_) => {
1070                todo!("Needs to be implemented")
1071            }
1072        })
1073        .collect();
1074
1075    // bridge lifecycle variants (one per bridge)
1076    let mut variants = plan_enum;
1077
1078    // add bridge lifecycle variants
1079    for bridge_spec in bridge_specs {
1080        let enum_entry_name = config_id_to_enum(&format!("{}_bridge", bridge_spec.id));
1081        let enum_ident = Ident::new(&enum_entry_name, Span::call_site());
1082        variants.push(quote! {
1083            #enum_ident(cu29::simulation::CuBridgeLifecycleState)
1084        });
1085    }
1086
1087    if cfg!(feature = "logstream") {
1088        variants.push(quote! { CopperListCompleted(&'a mut CopperList<CuStampedDataSet>) });
1089    }
1090    variants.push(quote! { __Phantom(core::marker::PhantomData<&'a ()>) });
1091    quote! {
1092        // not used if sim is not generated but this is ok.
1093        #[allow(dead_code, unused_lifetimes)]
1094        pub enum SimStep<'a> {
1095            #(#variants),*
1096        }
1097    }
1098}
1099
1100fn gen_recorded_replay_support(
1101    runtime_plan: &CuExecutionLoop,
1102    exec_entities: &[ExecutionEntity],
1103    bridge_specs: &[BridgeSpec],
1104) -> proc_macro2::TokenStream {
1105    let replay_arms: Vec<proc_macro2::TokenStream> = runtime_plan
1106        .steps
1107        .iter()
1108        .filter(|unit| {
1109            // One replay arm per node: refine steps have no SimStep variant.
1110            !matches!(unit, CuExecutionUnit::Step(step) if step.phase == CuStepPhase::AnytimeRefine)
1111        })
1112        .filter_map(|unit| match unit {
1113            CuExecutionUnit::Step(step) => match &exec_entities[step.node_id as usize].kind {
1114                ExecutionEntityKind::Task { .. } => {
1115                    let enum_entry_name = config_id_to_enum(step.node.get_id().as_str());
1116                    let enum_ident = Ident::new(&enum_entry_name, Span::call_site());
1117                    let output_pack = step
1118                        .output_msg_pack
1119                        .as_ref()
1120                        .expect("Task step missing output pack for recorded replay");
1121                    let culist_index = int2sliceindex(output_pack.culist_index);
1122                    Some(quote! {
1123                        SimStep::#enum_ident(CuTaskCallbackState::Process(_, output)) => {
1124                            *output = recorded.msgs.0.#culist_index.clone();
1125                            SimOverride::ExecutedBySim
1126                        }
1127                    })
1128                }
1129                ExecutionEntityKind::BridgeRx {
1130                    bridge_index,
1131                    channel_index,
1132                } => {
1133                    let bridge_spec = &bridge_specs[*bridge_index];
1134                    let channel = &bridge_spec.rx_channels[*channel_index];
1135                    let enum_entry_name =
1136                        config_id_to_enum(&format!("{}_rx_{}", bridge_spec.id, channel.id));
1137                    let enum_ident = Ident::new(&enum_entry_name, Span::call_site());
1138                    let output_pack = step
1139                        .output_msg_pack
1140                        .as_ref()
1141                        .expect("Bridge Rx channel missing output pack for recorded replay");
1142                    let port_index = output_pack
1143                        .msg_types
1144                        .iter()
1145                        .position(|msg| msg == &channel.msg_type_name)
1146                        .unwrap_or_else(|| {
1147                            panic!(
1148                                "Bridge Rx channel '{}' missing output port for '{}'",
1149                                channel.id, channel.msg_type_name
1150                            )
1151                        });
1152                    let culist_index = int2sliceindex(output_pack.culist_index);
1153                    let recorded_slot = if output_pack.msg_types.len() == 1 {
1154                        quote! { recorded.msgs.0.#culist_index.clone() }
1155                    } else {
1156                        let port_index = syn::Index::from(port_index);
1157                        quote! { recorded.msgs.0.#culist_index.#port_index.clone() }
1158                    };
1159                    Some(quote! {
1160                        SimStep::#enum_ident { msg, .. } => {
1161                            *msg = #recorded_slot;
1162                            SimOverride::ExecutedBySim
1163                        }
1164                    })
1165                }
1166                ExecutionEntityKind::BridgeTx {
1167                    bridge_index,
1168                    channel_index,
1169                } => {
1170                    let bridge_spec = &bridge_specs[*bridge_index];
1171                    let channel = &bridge_spec.tx_channels[*channel_index];
1172                    let enum_entry_name =
1173                        config_id_to_enum(&format!("{}_tx_{}", bridge_spec.id, channel.id));
1174                    let enum_ident = Ident::new(&enum_entry_name, Span::call_site());
1175                    let output_pack = step
1176                        .output_msg_pack
1177                        .as_ref()
1178                        .expect("Bridge Tx channel missing output pack for recorded replay");
1179                    let culist_index = int2sliceindex(output_pack.culist_index);
1180                    Some(quote! {
1181                        SimStep::#enum_ident { output, .. } => {
1182                            *output = recorded.msgs.0.#culist_index.clone();
1183                            SimOverride::ExecutedBySim
1184                        }
1185                    })
1186                }
1187            },
1188            CuExecutionUnit::Loop(_) => None,
1189        })
1190        .collect();
1191    let debug_replay_arms: Vec<proc_macro2::TokenStream> =
1192        runtime_plan
1193            .steps
1194            .iter()
1195            .filter_map(|unit| match unit {
1196                CuExecutionUnit::Step(step) => match &exec_entities[step.node_id as usize].kind {
1197                    ExecutionEntityKind::Task { .. } => {
1198                        if step.task_type == CuTaskType::Regular {
1199                            return None;
1200                        }
1201                        let enum_entry_name = config_id_to_enum(step.node.get_id().as_str());
1202                        let enum_ident = Ident::new(&enum_entry_name, Span::call_site());
1203                        let output_pack = step
1204                            .output_msg_pack
1205                            .as_ref()
1206                            .expect("Task step missing output pack for recorded debug replay");
1207                        let culist_index = int2sliceindex(output_pack.culist_index);
1208                        Some(quote! {
1209                            SimStep::#enum_ident(CuTaskCallbackState::Process(_, output)) => {
1210                                *output = recorded.msgs.0.#culist_index.clone();
1211                                SimOverride::ExecutedBySim
1212                            }
1213                        })
1214                    }
1215                    ExecutionEntityKind::BridgeRx {
1216                        bridge_index,
1217                        channel_index,
1218                    } => {
1219                        let bridge_spec = &bridge_specs[*bridge_index];
1220                        let channel = &bridge_spec.rx_channels[*channel_index];
1221                        let enum_entry_name =
1222                            config_id_to_enum(&format!("{}_rx_{}", bridge_spec.id, channel.id));
1223                        let enum_ident = Ident::new(&enum_entry_name, Span::call_site());
1224                        let output_pack = step.output_msg_pack.as_ref().expect(
1225                            "Bridge Rx channel missing output pack for recorded debug replay",
1226                        );
1227                        let port_index = output_pack
1228                            .msg_types
1229                            .iter()
1230                            .position(|msg| msg == &channel.msg_type_name)
1231                            .unwrap_or_else(|| {
1232                                panic!(
1233                                    "Bridge Rx channel '{}' missing output port for '{}'",
1234                                    channel.id, channel.msg_type_name
1235                                )
1236                            });
1237                        let culist_index = int2sliceindex(output_pack.culist_index);
1238                        let recorded_slot = if output_pack.msg_types.len() == 1 {
1239                            quote! { recorded.msgs.0.#culist_index.clone() }
1240                        } else {
1241                            let port_index = syn::Index::from(port_index);
1242                            quote! { recorded.msgs.0.#culist_index.#port_index.clone() }
1243                        };
1244                        Some(quote! {
1245                            SimStep::#enum_ident { msg, .. } => {
1246                                *msg = #recorded_slot;
1247                                SimOverride::ExecutedBySim
1248                            }
1249                        })
1250                    }
1251                    ExecutionEntityKind::BridgeTx {
1252                        bridge_index,
1253                        channel_index,
1254                    } => {
1255                        let bridge_spec = &bridge_specs[*bridge_index];
1256                        let channel = &bridge_spec.tx_channels[*channel_index];
1257                        let enum_entry_name =
1258                            config_id_to_enum(&format!("{}_tx_{}", bridge_spec.id, channel.id));
1259                        let enum_ident = Ident::new(&enum_entry_name, Span::call_site());
1260                        let output_pack = step.output_msg_pack.as_ref().expect(
1261                            "Bridge Tx channel missing output pack for recorded debug replay",
1262                        );
1263                        let culist_index = int2sliceindex(output_pack.culist_index);
1264                        Some(quote! {
1265                            SimStep::#enum_ident { output, .. } => {
1266                                *output = recorded.msgs.0.#culist_index.clone();
1267                                SimOverride::ExecutedBySim
1268                            }
1269                        })
1270                    }
1271                },
1272                CuExecutionUnit::Loop(_) => None,
1273            })
1274            .collect();
1275
1276    quote! {
1277        /// Exact-output replay callback: every recorded task and bridge output is
1278        /// copied from the CopperList and the runtime implementation is skipped.
1279        ///
1280        /// This is for deterministic log reproduction, not for debugger state replay.
1281        #[allow(dead_code)]
1282        pub fn recorded_replay_step<'a>(
1283            step: SimStep<'a>,
1284            recorded: &CopperList<CuStampedDataSet>,
1285        ) -> SimOverride {
1286            match step {
1287                #(#replay_arms),*,
1288                _ => SimOverride::ExecuteByRuntime,
1289            }
1290        }
1291
1292        /// Debugger state replay callback: recorded external inputs are injected,
1293        /// regular Copper tasks execute normally, and external sink/bridge effects
1294        /// are suppressed.
1295        ///
1296        /// This preserves task-state evolution after restoring an intra-CL keyframe.
1297        #[allow(dead_code)]
1298        pub fn recorded_debug_replay_step<'a>(
1299            step: SimStep<'a>,
1300            recorded: &CopperList<CuStampedDataSet>,
1301        ) -> SimOverride {
1302            match step {
1303                #(#debug_replay_arms),*,
1304                _ => SimOverride::ExecuteByRuntime,
1305            }
1306        }
1307    }
1308}
1309
1310include!("runtime_macro.rs");
1311fn resolve_runtime_config(args: &CopperRuntimeArgs) -> CuResult<ResolvedRuntimeConfig> {
1312    let caller_root = utils::caller_crate_root();
1313    resolve_runtime_config_with_root(args, &caller_root)
1314}
1315
1316fn resolve_runtime_config_with_root(
1317    args: &CopperRuntimeArgs,
1318    caller_root: &Path,
1319) -> CuResult<ResolvedRuntimeConfig> {
1320    let active_features = active_config_features();
1321    let active_feature_refs: Vec<_> = active_features.iter().map(String::as_str).collect();
1322    resolve_runtime_config_with_root_and_features(args, caller_root, &active_feature_refs)
1323}
1324
1325fn resolve_runtime_config_with_root_and_features(
1326    args: &CopperRuntimeArgs,
1327    caller_root: &Path,
1328    active_features: &[&str],
1329) -> CuResult<ResolvedRuntimeConfig> {
1330    let filename = config_full_path_from_root(caller_root, &args.config_path);
1331    if !Path::new(&filename).exists() {
1332        return Err(CuError::from(format!(
1333            "The configuration file `{}` does not exist. Please provide a valid path.",
1334            args.config_path
1335        )));
1336    }
1337
1338    if let Some(subsystem_id) = args.subsystem_id.as_deref() {
1339        let multi_config = cu29_runtime::config::read_multi_configuration_with_features(
1340            filename.as_str(),
1341            active_features,
1342        )
1343        .map_err(|e| {
1344            CuError::from(format!(
1345                "When `subsystem = \"{subsystem_id}\"` is provided, `config = \"{}\"` must point to a valid multi-Copper configuration: {e}",
1346                args.config_path
1347            ))
1348        })?;
1349        let subsystem = multi_config.subsystem(subsystem_id).ok_or_else(|| {
1350            CuError::from(format!(
1351                "Subsystem '{subsystem_id}' was not found in multi-Copper configuration '{}'.",
1352                args.config_path
1353            ))
1354        })?;
1355        // Bundle the include-expanded source representation. Serializing the lowered
1356        // mission graphs would lose the source task order because missions use hash maps.
1357        let (local_config, bundled_local_config_content) =
1358            read_configuration_with_resolved_ron_and_features(
1359                &subsystem.config_path,
1360                active_features,
1361            )
1362            .map_err(|e| {
1363                CuError::from(format!(
1364                    "Failed to prepare bundled local configuration for subsystem '{subsystem_id}' from '{}'.",
1365                    subsystem.config_path
1366                ))
1367                .add_cause(e.to_string().as_str())
1368            })?;
1369
1370        Ok(ResolvedRuntimeConfig {
1371            local_config,
1372            bundled_local_config_content,
1373            active_features: active_features
1374                .iter()
1375                .map(|feature| (*feature).to_string())
1376                .collect(),
1377            subsystem_id: Some(subsystem_id.to_string()),
1378            subsystem_code: subsystem.subsystem_code,
1379        })
1380    } else {
1381        let (local_config, bundled_local_config_content) =
1382            read_configuration_with_resolved_ron_and_features(filename.as_str(), active_features)?;
1383        Ok(ResolvedRuntimeConfig {
1384            local_config,
1385            bundled_local_config_content,
1386            active_features: active_features
1387                .iter()
1388                .map(|feature| (*feature).to_string())
1389                .collect(),
1390            subsystem_id: None,
1391            subsystem_code: 0,
1392        })
1393    }
1394}
1395
1396fn active_config_features() -> Vec<String> {
1397    let mut features: Vec<_> = std::env::var(COPPER_CFG_FEATURES_ENV)
1398        .unwrap_or_default()
1399        .split(',')
1400        .filter(|feature| !feature.is_empty())
1401        .map(str::to_owned)
1402        .collect();
1403    features.sort_unstable();
1404    features.dedup();
1405    features
1406}
1407
1408fn build_config_load_stmt(
1409    std_enabled: bool,
1410    application_name: &Ident,
1411    subsystem_id: Option<&str>,
1412    active_features: &[String],
1413) -> proc_macro2::TokenStream {
1414    let active_features = active_features.iter();
1415    if std_enabled {
1416        if let Some(subsystem_id) = subsystem_id {
1417            quote! {
1418                const COPPER_CFG_FEATURES: &[&str] = &[#(#active_features),*];
1419                let (config, config_source) = if let Some(overridden_config) = config_override {
1420                    debug!("CuConfig: Overridden programmatically.");
1421                    (overridden_config, RuntimeLifecycleConfigSource::ProgrammaticOverride)
1422                } else if ::std::path::Path::new(config_filename).exists() {
1423                    let subsystem_id = #application_name::subsystem()
1424                        .id()
1425                        .expect("generated multi-Copper runtime is missing a subsystem id");
1426                    debug!(
1427                        "CuConfig: Reading multi-Copper configuration from file: {} (subsystem={})",
1428                        config_filename,
1429                        subsystem_id
1430                    );
1431                    let multi_config = cu29::config::read_multi_configuration_with_features(
1432                        config_filename,
1433                        COPPER_CFG_FEATURES,
1434                    )?;
1435                    (
1436                        multi_config.resolve_subsystem_config_for_instance(subsystem_id, instance_id)?,
1437                        RuntimeLifecycleConfigSource::ExternalFile,
1438                    )
1439                } else {
1440                    let original_config = Self::original_config();
1441                    debug!(
1442                        "CuConfig: Using the bundled subsystem configuration compiled into the binary (subsystem={}).",
1443                        #subsystem_id
1444                    );
1445                    if instance_id != 0 {
1446                        debug!(
1447                            "CuConfig: runtime file '{}' is missing, so instance-specific overrides for instance_id={} cannot be resolved; using bundled subsystem defaults.",
1448                            config_filename,
1449                            instance_id
1450                        );
1451                    }
1452                    (
1453                        cu29::config::read_configuration_str(original_config, None)?,
1454                        RuntimeLifecycleConfigSource::BundledDefault,
1455                    )
1456                };
1457            }
1458        } else {
1459            quote! {
1460                const COPPER_CFG_FEATURES: &[&str] = &[#(#active_features),*];
1461                let _ = instance_id;
1462                let (config, config_source) = if let Some(overridden_config) = config_override {
1463                    debug!("CuConfig: Overridden programmatically.");
1464                    (overridden_config, RuntimeLifecycleConfigSource::ProgrammaticOverride)
1465                } else if ::std::path::Path::new(config_filename).exists() {
1466                    debug!("CuConfig: Reading configuration from file: {}", config_filename);
1467                    (
1468                        cu29::config::read_configuration_with_features(
1469                            config_filename,
1470                            COPPER_CFG_FEATURES,
1471                        )?,
1472                        RuntimeLifecycleConfigSource::ExternalFile,
1473                    )
1474                } else {
1475                    let original_config = Self::original_config();
1476                    debug!("CuConfig: Using the bundled configuration compiled into the binary.");
1477                    (
1478                        cu29::config::read_configuration_str(original_config, None)?,
1479                        RuntimeLifecycleConfigSource::BundledDefault,
1480                    )
1481                };
1482            }
1483        }
1484    } else {
1485        quote! {
1486            // Only the original config is available in no-std
1487            let original_config = Self::original_config();
1488            debug!("CuConfig: Using the bundled configuration compiled into the binary.");
1489            let config = cu29::config::read_configuration_str(original_config, None)?;
1490            let config_source = RuntimeLifecycleConfigSource::BundledDefault;
1491        }
1492    }
1493}
1494
1495fn config_full_path(config_file: &str) -> String {
1496    config_full_path_from_root(&utils::caller_crate_root(), config_file)
1497}
1498
1499fn config_full_path_from_root(caller_root: &Path, config_file: &str) -> String {
1500    let mut config_full_path = caller_root.to_path_buf();
1501    config_full_path.push(config_file);
1502    let filename = config_full_path
1503        .as_os_str()
1504        .to_str()
1505        .expect("Could not interpret the config file name");
1506    filename.to_string()
1507}
1508
1509fn read_config(config_file: &str) -> CuResult<CuConfig> {
1510    let filename = config_full_path(config_file);
1511    let active_features = active_config_features();
1512    let active_feature_refs: Vec<_> = active_features.iter().map(String::as_str).collect();
1513    read_configuration_with_features(filename.as_str(), &active_feature_refs)
1514}
1515
1516fn inferred_single_output_payload_type(
1517    task_type: &Type,
1518    task_kind: CuTaskType,
1519    is_anytime: bool,
1520    is_stateless: bool,
1521) -> Type {
1522    if is_anytime {
1523        return parse_quote! {
1524            <<#task_type as cu29::cutask_anytime::CuAnytimeTask>::Output<'static> as cu29::cutask::CuSingleOutputMsg>::Payload
1525        };
1526    }
1527    if is_stateless {
1528        return parse_quote! {
1529            <<#task_type as cu29::cutask::CuStatelessTask>::Output<'static> as cu29::cutask::CuSingleOutputMsg>::Payload
1530        };
1531    }
1532    match task_kind {
1533        CuTaskType::Source => parse_quote! {
1534            <<#task_type as cu29::cutask::CuSrcTask>::Output<'static> as cu29::cutask::CuSingleOutputMsg>::Payload
1535        },
1536        CuTaskType::Regular => parse_quote! {
1537            <<#task_type as cu29::cutask::CuTask>::Output<'static> as cu29::cutask::CuSingleOutputMsg>::Payload
1538        },
1539        CuTaskType::Sink => panic!("Sinks do not have output payload types"),
1540    }
1541}
1542
1543fn task_trait_for_kind(task_kind: CuTaskType) -> proc_macro2::TokenStream {
1544    match task_kind {
1545        CuTaskType::Source => quote! { cu29::cutask::CuSrcTask },
1546        CuTaskType::Regular => quote! { cu29::cutask::CuTask },
1547        CuTaskType::Sink => quote! { cu29::cutask::CuSinkTask },
1548    }
1549}
1550
1551/// Like [`task_trait_for_kind`], but resolves stateless and anytime nodes to
1552/// their declared task traits while they retain the regular graph role.
1553///
1554/// Background anytime nodes are driven through `CuAsyncTask`, which is a plain
1555/// `CuTask`; stateless nodes reject background and anytime configuration.
1556fn task_trait_for_specs(task_specs: &CuTaskSpecSet, index: usize) -> proc_macro2::TokenStream {
1557    if task_specs.stateless_flags[index] {
1558        return quote! { cu29::cutask::CuStatelessTask };
1559    }
1560    let foreground_anytime =
1561        task_specs.anytime_configs[index].is_some() && !task_specs.background_flags[index];
1562    if foreground_anytime {
1563        quote! { cu29::cutask_anytime::CuAnytimeTask }
1564    } else {
1565        task_trait_for_kind(task_specs.cutypes[index])
1566    }
1567}
1568
1569fn task_output_payload_type(
1570    graph: &CuGraph,
1571    node: &Node,
1572    task_kind: CuTaskType,
1573    task_type: &Type,
1574) -> Option<Type> {
1575    if task_kind == CuTaskType::Sink {
1576        return None;
1577    }
1578
1579    let id = node.get_id();
1580    if let Some(type_str) = graph.get_node_output_msg_type(id.as_str()) {
1581        return Some(
1582            parse_str::<Type>(type_str.as_str()).expect("Could not parse output message type."),
1583        );
1584    }
1585
1586    node.get_declared_task_kind().map(|_| {
1587        inferred_single_output_payload_type(
1588            task_type,
1589            task_kind,
1590            node.anytime().is_some(),
1591            node.get_declared_task_kind() == Some(TaskKind::Stateless),
1592        )
1593    })
1594}
1595
1596#[cfg(test)]
1597fn synthesized_single_output_msg_name(
1598    task_type: &Type,
1599    task_kind: CuTaskType,
1600    is_anytime: bool,
1601    is_stateless: bool,
1602) -> String {
1603    inferred_single_output_payload_type(task_type, task_kind, is_anytime, is_stateless)
1604        .to_token_stream()
1605        .to_string()
1606}
1607
1608struct CuTaskSpecSet {
1609    pub ids: Vec<String>,
1610    pub cutypes: Vec<CuTaskType>,
1611    pub stateless_flags: Vec<bool>,
1612    pub background_flags: Vec<bool>,
1613    /// Fixed CopperList lag selected for a deterministic background result.
1614    pub background_result_lags: Vec<Option<u32>>,
1615    /// Thread pool name each task runs on when backgrounded (defaults to the
1616    /// `"background"` pool). Only meaningful where `background_flags` is true.
1617    pub background_pools: Vec<String>,
1618    /// The `anytime:` policy of each task, if any. An anytime task keeps its
1619    /// raw `CuAnytimeTask` type in the tuple (no wrapper); the policy values
1620    /// are baked into a per-node `AnytimePolicy` ZST and the emitted
1621    /// base/refine steps.
1622    pub anytime_configs: Vec<Option<AnytimeConfig>>,
1623    /// The task type inside the optional async wrapper: the anytime runner
1624    /// for a background anytime node, the declared task type for everything
1625    /// else — including every foreground task, where nothing wraps it.
1626    pub async_inner_task_types: Vec<Type>,
1627    pub logging_enabled: Vec<bool>,
1628    pub type_names: Vec<String>,
1629    pub task_types: Vec<Type>,
1630    pub instantiation_types: Vec<Type>,
1631    pub sim_task_types: Vec<Type>,
1632    pub run_in_sim_flags: Vec<bool>,
1633    #[allow(dead_code)]
1634    pub output_types: Vec<Option<Type>>,
1635    pub autogenerated_output_flags: Vec<bool>,
1636    pub node_id_to_task_index: Vec<Option<usize>>,
1637}
1638
1639impl CuTaskSpecSet {
1640    pub fn from_graph(graph: &CuGraph) -> CuResult<Self> {
1641        let all_id_nodes: Vec<(NodeId, &Node)> = graph
1642            .get_all_nodes()
1643            .into_iter()
1644            .filter(|(_, node)| node.get_flavor() == Flavor::Task)
1645            .collect();
1646
1647        let ids: Vec<String> = all_id_nodes
1648            .iter()
1649            .map(|(_, node)| node.get_id().to_string())
1650            .collect();
1651
1652        let cutypes: Vec<CuTaskType> = all_id_nodes
1653            .iter()
1654            .map(|(id, _)| find_task_type_for_id(graph, *id))
1655            .collect::<CuResult<Vec<_>>>()?;
1656
1657        let stateless_flags: Vec<bool> = all_id_nodes
1658            .iter()
1659            .map(|(_, node)| node.get_declared_task_kind() == Some(TaskKind::Stateless))
1660            .collect();
1661
1662        let background_flags: Vec<bool> = all_id_nodes
1663            .iter()
1664            .map(|(_, node)| node.is_background())
1665            .collect();
1666
1667        let background_pools: Vec<String> = all_id_nodes
1668            .iter()
1669            .map(|(_, node)| node.background_pool().to_string())
1670            .collect();
1671
1672        let anytime_configs: Vec<Option<AnytimeConfig>> = all_id_nodes
1673            .iter()
1674            .map(|(_, node)| node.anytime().cloned())
1675            .collect();
1676
1677        let logging_enabled: Vec<bool> = all_id_nodes
1678            .iter()
1679            .map(|(_, node)| node.is_logging_enabled())
1680            .collect();
1681
1682        let type_names: Vec<String> = all_id_nodes
1683            .iter()
1684            .map(|(_, node)| node.get_type().to_string())
1685            .collect();
1686
1687        let declared_task_types: Vec<Type> = type_names
1688            .iter()
1689            .map(|name| {
1690                parse_str::<Type>(name).unwrap_or_else(|error| {
1691                    panic!("Could not transform {name} into a Task Rust type: {error}");
1692                })
1693            })
1694            .collect();
1695
1696        let output_types: Vec<Option<Type>> = all_id_nodes
1697            .iter()
1698            .zip(cutypes.iter())
1699            .zip(declared_task_types.iter())
1700            .map(|(((_, node), &task_kind), task_type)| {
1701                task_output_payload_type(graph, node, task_kind, task_type)
1702            })
1703            .collect();
1704
1705        let autogenerated_output_flags: Vec<bool> = all_id_nodes
1706            .iter()
1707            .zip(cutypes.iter())
1708            .map(|((node_id, node), &task_kind)| {
1709                task_kind != CuTaskType::Sink
1710                    && node.get_declared_task_kind().is_some()
1711                    && graph
1712                        .get_node_output_msg_types_by_id(*node_id)
1713                        .expect("missing output type lookup")
1714                        .is_empty()
1715            })
1716            .collect();
1717
1718        // A background anytime node is handed to `CuAsyncTask` wrapped in the
1719        // runner, which turns one whole job into a single `process` call.
1720        let async_inner_task_types: Vec<Type> = declared_task_types
1721            .iter()
1722            .zip(ids.iter())
1723            .zip(background_flags.iter())
1724            .zip(anytime_configs.iter())
1725            .map(|(((task_type, id), &background), anytime)| {
1726                if background && anytime.is_some() {
1727                    let policy_ident = anytime_policy_ident(id.as_str());
1728                    parse_quote!(cu29::cutask_anytime::CuAnytimeRunner<#task_type, #policy_ident>)
1729                } else {
1730                    task_type.clone()
1731                }
1732            })
1733            .collect();
1734
1735        let task_types = declared_task_types
1736            .iter()
1737            .zip(type_names.iter())
1738            .zip(cutypes.iter())
1739            .zip(background_flags.iter())
1740            .zip(output_types.iter())
1741            .zip(async_inner_task_types.iter())
1742            .zip(all_id_nodes.iter())
1743            .map(|((((((name_type, name), cutype), &background), output_type), inner_type), (_, node))| {
1744                let process_empty = node.background_process_empty();
1745                if background {
1746                    if let Some(output_type) = output_type {
1747                        match cutype {
1748                            CuTaskType::Source => {
1749                                parse_quote!(CuAsyncSrcTask<#inner_type, #output_type>)
1750                            }
1751                            CuTaskType::Regular => {
1752                                parse_quote!(CuAsyncTask<#inner_type, #output_type, #process_empty>)
1753                            }
1754                            CuTaskType::Sink => {
1755                                panic!("CuSinkTask {name} cannot be a background task, it should be a regular task.");
1756                            }
1757                        }
1758                    } else {
1759                        panic!(
1760                            "{}: If a task is background, it has to have an output",
1761                            name_type.to_token_stream()
1762                        );
1763                    }
1764                } else {
1765                    name_type.clone()
1766                }
1767            })
1768            .collect();
1769
1770        let instantiation_types = declared_task_types
1771            .iter()
1772            .zip(type_names.iter())
1773            .zip(cutypes.iter())
1774            .zip(background_flags.iter())
1775            .zip(output_types.iter())
1776            .zip(async_inner_task_types.iter())
1777            .zip(all_id_nodes.iter())
1778            .map(|((((((name_type, name), cutype), &background), output_type), inner_type), (_, node))| {
1779                let process_empty = node.background_process_empty();
1780                if background {
1781                    if let Some(output_type) = output_type {
1782                        match cutype {
1783                            CuTaskType::Source => {
1784                                parse_quote!(CuAsyncSrcTask::<#inner_type, #output_type>)
1785                            }
1786                            CuTaskType::Regular => {
1787                                parse_quote!(CuAsyncTask::<#inner_type, #output_type, #process_empty>)
1788                            }
1789                            CuTaskType::Sink => {
1790                                panic!("CuSinkTask {name} cannot be a background task, it should be a regular task.");
1791                            }
1792                        }
1793                    } else {
1794                        panic!(
1795                            "{}: If a task is background, it has to have an output",
1796                            name_type.to_token_stream()
1797                        );
1798                    }
1799                } else {
1800                    name_type.clone()
1801                }
1802            })
1803            .collect();
1804
1805        let sim_task_types = declared_task_types;
1806
1807        let run_in_sim_flags = all_id_nodes
1808            .iter()
1809            .map(|(_, node)| node.is_run_in_sim())
1810            .collect();
1811
1812        let mut node_id_to_task_index = vec![None; graph.node_count()];
1813        for (index, (node_id, _)) in all_id_nodes.iter().enumerate() {
1814            node_id_to_task_index[*node_id as usize] = Some(index);
1815        }
1816        let background_result_lags = vec![None; ids.len()];
1817
1818        Ok(Self {
1819            ids,
1820            cutypes,
1821            stateless_flags,
1822            background_flags,
1823            background_result_lags,
1824            background_pools,
1825            anytime_configs,
1826            async_inner_task_types,
1827            logging_enabled,
1828            type_names,
1829            task_types,
1830            instantiation_types,
1831            sim_task_types,
1832            run_in_sim_flags,
1833            output_types,
1834            autogenerated_output_flags,
1835            node_id_to_task_index,
1836        })
1837    }
1838}
1839
1840#[derive(Clone)]
1841struct OutputPack {
1842    msg_types: Vec<Type>,
1843    msg_type_names: Vec<String>,
1844}
1845
1846impl OutputPack {
1847    fn slot_type(&self) -> Type {
1848        build_output_slot_type(&self.msg_types)
1849    }
1850
1851    fn is_multi(&self) -> bool {
1852        self.msg_types.len() > 1
1853    }
1854}
1855
1856fn build_output_slot_type(msg_types: &[Type]) -> Type {
1857    if msg_types.is_empty() {
1858        parse_quote! { () }
1859    } else if msg_types.len() == 1 {
1860        let msg_type = msg_types.first().unwrap();
1861        parse_quote! { CuMsg<#msg_type> }
1862    } else {
1863        parse_quote! { ( #( CuMsg<#msg_types> ),* ) }
1864    }
1865}
1866
1867fn flatten_slot_origin_ids(
1868    output_packs: &[OutputPack],
1869    slot_origin_ids: &[Option<String>],
1870) -> Vec<String> {
1871    let mut ids = Vec::new();
1872    for (slot, pack) in output_packs.iter().enumerate() {
1873        if pack.msg_types.is_empty() {
1874            continue;
1875        }
1876        let origin = slot_origin_ids
1877            .get(slot)
1878            .and_then(|origin| origin.as_ref())
1879            .unwrap_or_else(|| panic!("Missing slot origin id for copperlist output slot {slot}"));
1880        for _ in 0..pack.msg_types.len() {
1881            ids.push(origin.clone());
1882        }
1883    }
1884    ids
1885}
1886
1887fn flatten_task_output_specs(
1888    output_packs: &[OutputPack],
1889    slot_origin_ids: &[Option<String>],
1890) -> Vec<(String, String, Type)> {
1891    let mut specs = Vec::new();
1892    for (slot, pack) in output_packs.iter().enumerate() {
1893        if pack.msg_types.is_empty() {
1894            continue;
1895        }
1896        let origin = slot_origin_ids
1897            .get(slot)
1898            .and_then(|origin| origin.as_ref())
1899            .unwrap_or_else(|| panic!("Missing slot origin id for copperlist output slot {slot}"));
1900        for (msg_type, payload_type) in pack.msg_type_names.iter().zip(pack.msg_types.iter()) {
1901            specs.push((origin.clone(), msg_type.clone(), payload_type.clone()));
1902        }
1903    }
1904    specs
1905}
1906
1907/// Compute the per-slot [`HandleContent`] policy, reading each slot's producing task's
1908/// `NodeLogging.handle_content` from the config. Slots produced by bridges (or whose
1909/// producing task can't be located in the config) default to [`HandleContent::All`] —
1910/// matches the existing, payload-preserving behavior.
1911fn build_slot_handle_modes(
1912    cuconfig: &CuConfig,
1913    mission_label: Option<&str>,
1914    output_packs: &[OutputPack],
1915    node_output_positions: &HashMap<NodeId, usize>,
1916    task_names: &[(NodeId, String, String)],
1917) -> Vec<HandleContent> {
1918    let mut slot_modes: Vec<HandleContent> = vec![HandleContent::default(); output_packs.len()];
1919    for (node_id, task_id, _member) in task_names {
1920        let Some(pos) = node_output_positions.get(node_id) else {
1921            continue;
1922        };
1923        if let Some(node) = cuconfig.find_task_node(mission_label, task_id) {
1924            slot_modes[*pos] = node.handle_content_policy();
1925        }
1926    }
1927    slot_modes
1928}
1929
1930fn extract_output_packs(runtime_plan: &CuExecutionLoop) -> Vec<OutputPack> {
1931    let mut packs: Vec<(u32, OutputPack)> = runtime_plan
1932        .steps
1933        .iter()
1934        .filter_map(|unit| match unit {
1935            CuExecutionUnit::Step(step) => {
1936                // Refine steps reuse the base step's slot: one slot per node.
1937                if step.phase == CuStepPhase::AnytimeRefine {
1938                    return None;
1939                }
1940                let output_pack = step.output_msg_pack.as_ref()?;
1941                let msg_types: Vec<Type> = output_pack
1942                    .msg_types
1943                    .iter()
1944                    .map(|output_msg_type| {
1945                        parse_str::<Type>(output_msg_type.as_str()).unwrap_or_else(|_| {
1946                            panic!(
1947                                "Could not transform {output_msg_type} into a message Rust type."
1948                            )
1949                        })
1950                    })
1951                    .collect();
1952                Some((
1953                    output_pack.culist_index,
1954                    OutputPack {
1955                        msg_types,
1956                        msg_type_names: output_pack.msg_types.clone(),
1957                    },
1958                ))
1959            }
1960            CuExecutionUnit::Loop(_) => todo!("Needs to be implemented"),
1961        })
1962        .collect();
1963
1964    packs.sort_by_key(|(index, _)| *index);
1965    packs.into_iter().map(|(_, pack)| pack).collect()
1966}
1967
1968#[derive(Clone)]
1969struct SlotCodecBinding {
1970    payload_type: Type,
1971    task_id: String,
1972    msg_type: String,
1973    codec_type: syn::Path,
1974    codec_type_path: String,
1975}
1976
1977fn build_flat_slot_codec_bindings(
1978    cuconfig: &CuConfig,
1979    mission_label: Option<&str>,
1980    output_packs: &[OutputPack],
1981    node_output_positions: &HashMap<NodeId, usize>,
1982    task_names: &[(NodeId, String, String)],
1983) -> CuResult<Vec<Option<SlotCodecBinding>>> {
1984    let mut slot_task_ids: Vec<Option<String>> = vec![None; output_packs.len()];
1985    for (node_id, task_id, _) in task_names {
1986        let Some(output_position) = node_output_positions.get(node_id) else {
1987            continue;
1988        };
1989        slot_task_ids[*output_position] = Some(task_id.clone());
1990    }
1991
1992    let mut bindings =
1993        Vec::with_capacity(output_packs.iter().map(|pack| pack.msg_types.len()).sum());
1994    for (slot_idx, pack) in output_packs.iter().enumerate() {
1995        let task_id = slot_task_ids.get(slot_idx).and_then(|id| id.as_ref());
1996        for (port_idx, payload_type) in pack.msg_types.iter().enumerate() {
1997            let Some(task_id) = task_id else {
1998                bindings.push(None);
1999                continue;
2000            };
2001            let Some(msg_type) = pack.msg_type_names.get(port_idx) else {
2002                return Err(CuError::from(format!(
2003                    "Missing message type name for task '{task_id}' slot {slot_idx} port {port_idx}."
2004                )));
2005            };
2006
2007            let spec = cuconfig
2008                .find_task_node(mission_label, task_id)
2009                .and_then(|node| node.get_logging())
2010                .and_then(|logging| logging.codec_for_msg_type(msg_type))
2011                .map(|codec_id| {
2012                    cuconfig.find_logging_codec_spec(codec_id).ok_or_else(|| {
2013                        CuError::from(format!(
2014                            "Task '{task_id}' binds output '{msg_type}' to unknown logging codec '{codec_id}'."
2015                        ))
2016                    })
2017                })
2018                .transpose()?;
2019
2020            if let Some(spec) = spec {
2021                let codec_type = parse_str::<syn::Path>(&spec.type_).map_err(|_| {
2022                    CuError::from(format!(
2023                        "Logging codec '{}' for task '{task_id}' output '{msg_type}' is not a valid Rust type path.",
2024                        spec.type_
2025                    ))
2026                })?;
2027                bindings.push(Some(SlotCodecBinding {
2028                    payload_type: payload_type.clone(),
2029                    task_id: task_id.clone(),
2030                    msg_type: msg_type.clone(),
2031                    codec_type,
2032                    codec_type_path: spec.type_.clone(),
2033                }));
2034            } else {
2035                bindings.push(None);
2036            }
2037        }
2038    }
2039
2040    Ok(bindings)
2041}
2042
2043fn build_culist_codec_helpers(
2044    flat_codec_bindings: &[Option<SlotCodecBinding>],
2045    default_config_ron_ident: &Ident,
2046    mission_label: Option<&str>,
2047) -> (
2048    Vec<proc_macro2::TokenStream>,
2049    Vec<Option<Ident>>,
2050    Vec<Option<Ident>>,
2051) {
2052    let compressed = !cfg!(feature = "flat-copperlist-encoding");
2053    let mission_tokens = if let Some(mission) = mission_label {
2054        let lit = LitStr::new(mission, Span::call_site());
2055        quote! { Some(#lit) }
2056    } else {
2057        quote! { None }
2058    };
2059
2060    let mut helpers = Vec::new();
2061    let mut encode_helper_names = Vec::with_capacity(flat_codec_bindings.len());
2062    let mut decode_helper_names = Vec::with_capacity(flat_codec_bindings.len());
2063
2064    for (flat_idx, binding) in flat_codec_bindings.iter().enumerate() {
2065        let Some(binding) = binding else {
2066            encode_helper_names.push(None);
2067            decode_helper_names.push(None);
2068            continue;
2069        };
2070
2071        let encode_fn = format_ident!("__cu_logcodec_encode_slot_{flat_idx}");
2072        let decode_fn = format_ident!("__cu_logcodec_decode_slot_{flat_idx}");
2073        let payload_type = &binding.payload_type;
2074        let codec_type = &binding.codec_type;
2075        let task_id = LitStr::new(&binding.task_id, Span::call_site());
2076        let msg_type = LitStr::new(&binding.msg_type, Span::call_site());
2077        let codec_type_path = LitStr::new(&binding.codec_type_path, Span::call_site());
2078
2079        let helper = if compressed {
2080            quote! {
2081            fn #encode_fn<E: Encoder>(payload: &#payload_type, encoder: &mut E) -> Result<(), EncodeError> {
2082                static STATE: ::cu29::logcodec::CodecState<#codec_type> = ::cu29::logcodec::CodecState::new();
2083                let config_entry = ::cu29::logcodec::effective_config_entry::<CuStampedDataSet>(#default_config_ron_ident);
2084                ::cu29::logcodec::with_codec_for_encode(
2085                    &STATE,
2086                    config_entry,
2087                    |effective_config_ron| {
2088                        ::cu29::logcodec::instantiate_codec::<#codec_type, #payload_type>(
2089                            effective_config_ron,
2090                            #mission_tokens,
2091                            #task_id,
2092                            #msg_type,
2093                            #codec_type_path,
2094                        )
2095                    },
2096                    |codec| ::cu29::logcodec::encode_payload_with_codec(payload, codec, encoder),
2097                )
2098            }
2099
2100            fn #decode_fn<D: Decoder<Context = ()>>(decoder: &mut D) -> Result<#payload_type, DecodeError> {
2101                static STATE: ::cu29::logcodec::CodecState<#codec_type> = ::cu29::logcodec::CodecState::new();
2102                let config_entry = ::cu29::logcodec::effective_config_entry::<CuStampedDataSet>(#default_config_ron_ident);
2103                ::cu29::logcodec::with_codec_for_decode(
2104                    &STATE,
2105                    config_entry,
2106                    |effective_config_ron| {
2107                        ::cu29::logcodec::instantiate_codec::<#codec_type, #payload_type>(
2108                            effective_config_ron,
2109                            #mission_tokens,
2110                            #task_id,
2111                            #msg_type,
2112                            #codec_type_path,
2113                        )
2114                    },
2115                    |codec| ::cu29::logcodec::decode_payload_with_codec(decoder, codec),
2116                )
2117            }
2118            }
2119        } else {
2120            quote! {
2121            fn #encode_fn<E: Encoder>(msg: &CuMsg<#payload_type>, encoder: &mut E) -> Result<(), EncodeError> {
2122                static STATE: ::cu29::logcodec::CodecState<#codec_type> = ::cu29::logcodec::CodecState::new();
2123                let config_entry = ::cu29::logcodec::effective_config_entry::<CuStampedDataSet>(#default_config_ron_ident);
2124                ::cu29::logcodec::with_codec_for_encode(
2125                    &STATE,
2126                    config_entry,
2127                    |effective_config_ron| {
2128                        ::cu29::logcodec::instantiate_codec::<#codec_type, #payload_type>(
2129                            effective_config_ron,
2130                            #mission_tokens,
2131                            #task_id,
2132                            #msg_type,
2133                            #codec_type_path,
2134                        )
2135                    },
2136                    |codec| ::cu29::logcodec::encode_msg_with_codec(msg, codec, encoder),
2137                )
2138            }
2139
2140            fn #decode_fn<D: Decoder<Context = ()>>(decoder: &mut D) -> Result<CuMsg<#payload_type>, DecodeError> {
2141                static STATE: ::cu29::logcodec::CodecState<#codec_type> = ::cu29::logcodec::CodecState::new();
2142                let config_entry = ::cu29::logcodec::effective_config_entry::<CuStampedDataSet>(#default_config_ron_ident);
2143                ::cu29::logcodec::with_codec_for_decode(
2144                    &STATE,
2145                    config_entry,
2146                    |effective_config_ron| {
2147                        ::cu29::logcodec::instantiate_codec::<#codec_type, #payload_type>(
2148                            effective_config_ron,
2149                            #mission_tokens,
2150                            #task_id,
2151                            #msg_type,
2152                            #codec_type_path,
2153                        )
2154                    },
2155                    |codec| ::cu29::logcodec::decode_msg_with_codec(decoder, codec),
2156                )
2157            }
2158            }
2159        };
2160        helpers.push(helper);
2161        encode_helper_names.push(Some(encode_fn));
2162        decode_helper_names.push(Some(decode_fn));
2163    }
2164
2165    (helpers, encode_helper_names, decode_helper_names)
2166}
2167
2168fn collect_output_pack_sizes(runtime_plan: &CuExecutionLoop) -> Vec<usize> {
2169    let mut sizes: Vec<(u32, usize)> = runtime_plan
2170        .steps
2171        .iter()
2172        .filter_map(|unit| match unit {
2173            CuExecutionUnit::Step(step) => {
2174                // Refine steps reuse the base step's slot: one slot per node.
2175                if step.phase == CuStepPhase::AnytimeRefine {
2176                    return None;
2177                }
2178                step.output_msg_pack
2179                    .as_ref()
2180                    .map(|output_pack| (output_pack.culist_index, output_pack.msg_types.len()))
2181            }
2182            CuExecutionUnit::Loop(_) => todo!("Needs to be implemented"),
2183        })
2184        .collect();
2185
2186    sizes.sort_by_key(|(index, _)| *index);
2187    sizes.into_iter().map(|(_, size)| size).collect()
2188}
2189
2190fn sorted_mission_graphs(copper_config: &CuConfig) -> Vec<(String, CuGraph)> {
2191    let mut all_missions: Vec<_> = copper_config
2192        .graphs
2193        .get_all_missions_graphs()
2194        .into_iter()
2195        .collect();
2196    all_missions.sort_by(|(left, _), (right, _)| left.cmp(right));
2197    all_missions
2198}
2199
2200#[derive(Debug, Clone, PartialEq, Eq)]
2201struct CanonicalTaskInputSlot {
2202    msg_type: String,
2203    connection_orders: BTreeSet<usize>,
2204}
2205
2206#[derive(Debug, Clone, PartialEq, Eq)]
2207struct MissionTaskInput {
2208    msg_type: String,
2209    connection_order: usize,
2210}
2211
2212#[derive(Debug, Clone)]
2213struct TaskInputLayout {
2214    slots: Vec<CanonicalTaskInputSlot>,
2215    mission_slot_mappings: HashMap<String, Vec<Option<usize>>>,
2216}
2217
2218#[derive(Clone, Copy)]
2219enum AlignmentStep {
2220    Match {
2221        canonical_slot_index: usize,
2222        mission_input_index: usize,
2223    },
2224    ExistingGap {
2225        canonical_slot_index: usize,
2226    },
2227    Insert {
2228        mission_input_index: usize,
2229    },
2230}
2231
2232#[derive(Clone, Copy)]
2233enum AlignmentTransition {
2234    Match,
2235    ExistingGap,
2236    Insert,
2237}
2238
2239#[derive(Clone, Copy)]
2240struct AlignmentBackpointer {
2241    prev_i: usize,
2242    prev_j: usize,
2243    transition: AlignmentTransition,
2244}
2245
2246#[derive(Clone, Copy)]
2247struct AlignmentCell {
2248    score: i32,
2249    paths: u8,
2250    backpointer: Option<AlignmentBackpointer>,
2251}
2252
2253impl AlignmentCell {
2254    fn unreachable() -> Self {
2255        Self {
2256            score: i32::MIN,
2257            paths: 0,
2258            backpointer: None,
2259        }
2260    }
2261}
2262
2263fn collect_mission_task_inputs(
2264    graph: &CuGraph,
2265    node_id: NodeId,
2266    task_id: &str,
2267) -> CuResult<Vec<MissionTaskInput>> {
2268    let mut edge_ids = graph.get_dst_edges(node_id)?;
2269    edge_ids.sort_by_key(|edge_id| {
2270        graph
2271            .edge(*edge_id)
2272            .map(|edge| edge.order)
2273            .unwrap_or(usize::MAX)
2274    });
2275
2276    edge_ids
2277        .into_iter()
2278        .map(|edge_id| {
2279            let edge = graph.edge(edge_id).ok_or_else(|| {
2280                CuError::from(format!(
2281                    "Missing edge {edge_id} while collecting inputs for task '{task_id}'"
2282                ))
2283            })?;
2284            Ok(MissionTaskInput {
2285                msg_type: edge.msg.clone(),
2286                connection_order: edge.order,
2287            })
2288        })
2289        .collect()
2290}
2291
2292fn format_canonical_input_slots(slots: &[CanonicalTaskInputSlot]) -> String {
2293    let parts: Vec<String> = slots
2294        .iter()
2295        .map(|slot| {
2296            let orders = slot
2297                .connection_orders
2298                .iter()
2299                .map(|order| order.to_string())
2300                .collect::<Vec<_>>()
2301                .join("|");
2302            format!("{}@{}", slot.msg_type, orders)
2303        })
2304        .collect();
2305    format!("[{}]", parts.join(", "))
2306}
2307
2308fn format_mission_task_inputs(inputs: &[MissionTaskInput]) -> String {
2309    let parts: Vec<String> = inputs
2310        .iter()
2311        .map(|input| format!("{}@{}", input.msg_type, input.connection_order))
2312        .collect();
2313    format!("[{}]", parts.join(", "))
2314}
2315
2316const INPUT_MATCH_SCORE: i32 = 100;
2317const ANCHORED_INPUT_MATCH_BONUS: i32 = 1;
2318
2319fn task_input_match_score(slot: &CanonicalTaskInputSlot, input: &MissionTaskInput) -> Option<i32> {
2320    if slot.msg_type != input.msg_type {
2321        return None;
2322    }
2323
2324    let anchored_bonus = if slot.connection_orders.contains(&input.connection_order) {
2325        ANCHORED_INPUT_MATCH_BONUS
2326    } else {
2327        0
2328    };
2329
2330    Some(INPUT_MATCH_SCORE + anchored_bonus)
2331}
2332
2333fn update_alignment_cell(
2334    cell: &mut AlignmentCell,
2335    candidate_score: i32,
2336    candidate_paths: u8,
2337    backpointer: Option<AlignmentBackpointer>,
2338) {
2339    if candidate_paths == 0 {
2340        return;
2341    }
2342
2343    if candidate_score > cell.score {
2344        cell.score = candidate_score;
2345        cell.paths = candidate_paths.min(2);
2346        cell.backpointer = if candidate_paths == 1 {
2347            backpointer
2348        } else {
2349            None
2350        };
2351    } else if candidate_score == cell.score {
2352        cell.paths = cell.paths.saturating_add(candidate_paths).min(2);
2353        cell.backpointer = None;
2354    }
2355}
2356
2357fn align_task_inputs(
2358    task_id: &str,
2359    mission_name: &str,
2360    canonical_slots: &[CanonicalTaskInputSlot],
2361    mission_inputs: &[MissionTaskInput],
2362) -> CuResult<Vec<AlignmentStep>> {
2363    let canonical_len = canonical_slots.len();
2364    let mission_len = mission_inputs.len();
2365    let mut table = vec![vec![AlignmentCell::unreachable(); mission_len + 1]; canonical_len + 1];
2366    table[0][0] = AlignmentCell {
2367        score: 0,
2368        paths: 1,
2369        backpointer: None,
2370    };
2371
2372    for i in 0..=canonical_len {
2373        for j in 0..=mission_len {
2374            let cell = table[i][j];
2375            if cell.paths == 0 {
2376                continue;
2377            }
2378
2379            if i < canonical_len {
2380                update_alignment_cell(
2381                    &mut table[i + 1][j],
2382                    cell.score,
2383                    cell.paths,
2384                    if cell.paths == 1 {
2385                        Some(AlignmentBackpointer {
2386                            prev_i: i,
2387                            prev_j: j,
2388                            transition: AlignmentTransition::ExistingGap,
2389                        })
2390                    } else {
2391                        None
2392                    },
2393                );
2394            }
2395
2396            if j < mission_len {
2397                update_alignment_cell(
2398                    &mut table[i][j + 1],
2399                    cell.score,
2400                    cell.paths,
2401                    if cell.paths == 1 {
2402                        Some(AlignmentBackpointer {
2403                            prev_i: i,
2404                            prev_j: j,
2405                            transition: AlignmentTransition::Insert,
2406                        })
2407                    } else {
2408                        None
2409                    },
2410                );
2411            }
2412
2413            if i < canonical_len
2414                && j < mission_len
2415                && let Some(match_score) =
2416                    task_input_match_score(&canonical_slots[i], &mission_inputs[j])
2417            {
2418                update_alignment_cell(
2419                    &mut table[i + 1][j + 1],
2420                    cell.score + match_score,
2421                    cell.paths,
2422                    if cell.paths == 1 {
2423                        Some(AlignmentBackpointer {
2424                            prev_i: i,
2425                            prev_j: j,
2426                            transition: AlignmentTransition::Match,
2427                        })
2428                    } else {
2429                        None
2430                    },
2431                );
2432            }
2433        }
2434    }
2435
2436    let final_cell = table[canonical_len][mission_len];
2437    if final_cell.paths > 1 {
2438        return Err(CuError::from(format!(
2439            "Task '{task_id}' has ambiguous input alignment while merging mission '{mission_name}'. Existing canonical inputs {} and mission inputs {} admit multiple equally valid alignments.",
2440            format_canonical_input_slots(canonical_slots),
2441            format_mission_task_inputs(mission_inputs),
2442        )));
2443    }
2444
2445    let mut steps = Vec::new();
2446    let (mut i, mut j) = (canonical_len, mission_len);
2447    while i > 0 || j > 0 {
2448        let backpointer = table[i][j].backpointer.unwrap_or_else(|| {
2449            panic!(
2450                "Missing backpointer while aligning task '{task_id}' for mission '{mission_name}'"
2451            )
2452        });
2453
2454        match backpointer.transition {
2455            AlignmentTransition::Match => steps.push(AlignmentStep::Match {
2456                canonical_slot_index: i - 1,
2457                mission_input_index: j - 1,
2458            }),
2459            AlignmentTransition::ExistingGap => steps.push(AlignmentStep::ExistingGap {
2460                canonical_slot_index: i - 1,
2461            }),
2462            AlignmentTransition::Insert => steps.push(AlignmentStep::Insert {
2463                mission_input_index: j - 1,
2464            }),
2465        }
2466
2467        i = backpointer.prev_i;
2468        j = backpointer.prev_j;
2469    }
2470    steps.reverse();
2471    Ok(steps)
2472}
2473
2474fn merge_task_input_layout(
2475    task_id: &str,
2476    layout: &mut TaskInputLayout,
2477    mission_name: String,
2478    mission_inputs: Vec<MissionTaskInput>,
2479) -> CuResult<()> {
2480    let alignment = align_task_inputs(task_id, &mission_name, &layout.slots, &mission_inputs)?;
2481    let mut new_slots = Vec::with_capacity(alignment.len());
2482    let mut old_to_new = vec![None; layout.slots.len()];
2483    let mut mission_mapping = Vec::with_capacity(alignment.len());
2484
2485    for step in alignment {
2486        match step {
2487            AlignmentStep::Match {
2488                canonical_slot_index,
2489                mission_input_index,
2490            } => {
2491                let mut slot = layout.slots[canonical_slot_index].clone();
2492                slot.connection_orders
2493                    .insert(mission_inputs[mission_input_index].connection_order);
2494                let new_index = new_slots.len();
2495                old_to_new[canonical_slot_index] = Some(new_index);
2496                new_slots.push(slot);
2497                mission_mapping.push(Some(mission_input_index));
2498            }
2499            AlignmentStep::ExistingGap {
2500                canonical_slot_index,
2501            } => {
2502                let new_index = new_slots.len();
2503                old_to_new[canonical_slot_index] = Some(new_index);
2504                new_slots.push(layout.slots[canonical_slot_index].clone());
2505                mission_mapping.push(None);
2506            }
2507            AlignmentStep::Insert {
2508                mission_input_index,
2509            } => {
2510                new_slots.push(CanonicalTaskInputSlot {
2511                    msg_type: mission_inputs[mission_input_index].msg_type.clone(),
2512                    connection_orders: BTreeSet::from([
2513                        mission_inputs[mission_input_index].connection_order
2514                    ]),
2515                });
2516                mission_mapping.push(Some(mission_input_index));
2517            }
2518        }
2519    }
2520
2521    let mut remapped_mission_slot_mappings =
2522        HashMap::with_capacity(layout.mission_slot_mappings.len() + 1);
2523    for (existing_mission, existing_mapping) in &layout.mission_slot_mappings {
2524        let mut remapped = vec![None; new_slots.len()];
2525        for (old_slot_index, maybe_local_input_index) in existing_mapping.iter().enumerate() {
2526            let new_slot_index = old_to_new[old_slot_index].unwrap_or_else(|| {
2527                panic!("Missing remap for task '{task_id}' canonical slot {old_slot_index}")
2528            });
2529            remapped[new_slot_index] = *maybe_local_input_index;
2530        }
2531        remapped_mission_slot_mappings.insert(existing_mission.clone(), remapped);
2532    }
2533    remapped_mission_slot_mappings.insert(mission_name, mission_mapping);
2534
2535    layout.slots = new_slots;
2536    layout.mission_slot_mappings = remapped_mission_slot_mappings;
2537    Ok(())
2538}
2539
2540fn collect_task_input_layouts(
2541    all_missions: &[(String, CuGraph)],
2542) -> CuResult<HashMap<String, TaskInputLayout>> {
2543    let mut task_mission_inputs: BTreeMap<String, Vec<(String, Vec<MissionTaskInput>)>> =
2544        BTreeMap::new();
2545
2546    for (mission_name, graph) in all_missions {
2547        for (node_id, node) in graph.get_all_nodes() {
2548            if node.get_flavor() != Flavor::Task {
2549                continue;
2550            }
2551
2552            if find_task_type_for_id(graph, node_id)? == CuTaskType::Source {
2553                continue;
2554            }
2555
2556            let task_id = node.get_id().to_string();
2557            let mission_inputs = collect_mission_task_inputs(graph, node_id, task_id.as_str())?;
2558            task_mission_inputs
2559                .entry(task_id)
2560                .or_default()
2561                .push((mission_name.clone(), mission_inputs));
2562        }
2563    }
2564
2565    let mut layouts = HashMap::new();
2566    for (task_id, mission_inputs) in task_mission_inputs {
2567        let mut mission_iter = mission_inputs.into_iter();
2568        let Some((first_mission, first_inputs)) = mission_iter.next() else {
2569            continue;
2570        };
2571
2572        let slots: Vec<CanonicalTaskInputSlot> = first_inputs
2573            .iter()
2574            .map(|input| CanonicalTaskInputSlot {
2575                msg_type: input.msg_type.clone(),
2576                connection_orders: BTreeSet::from([input.connection_order]),
2577            })
2578            .collect();
2579        let mut mission_slot_mappings = HashMap::new();
2580        mission_slot_mappings.insert(
2581            first_mission,
2582            (0..first_inputs.len()).map(Some).collect::<Vec<_>>(),
2583        );
2584
2585        let mut layout = TaskInputLayout {
2586            slots,
2587            mission_slot_mappings,
2588        };
2589        for (mission_name, mission_inputs) in mission_iter {
2590            merge_task_input_layout(&task_id, &mut layout, mission_name, mission_inputs)?;
2591        }
2592
2593        layouts.insert(task_id, layout);
2594    }
2595
2596    Ok(layouts)
2597}
2598
2599struct GeneratedTaskInput {
2600    setup: proc_macro2::TokenStream,
2601    expr: proc_macro2::TokenStream,
2602}
2603
2604fn present_task_input_expr(
2605    input: &cu29_runtime::curuntime::CuInputMsg,
2606    output_pack_sizes: &[usize],
2607) -> proc_macro2::TokenStream {
2608    let input_index = int2sliceindex(input.culist_index);
2609    let output_size = output_pack_sizes
2610        .get(input.culist_index as usize)
2611        .copied()
2612        .unwrap_or_else(|| {
2613            panic!(
2614                "Missing output pack size for culist index {}",
2615                input.culist_index
2616            )
2617        });
2618    if output_size > 1 {
2619        let port_index = syn::Index::from(input.src_port);
2620        quote! { __cu_input!(#input_index, #port_index) }
2621    } else {
2622        quote! { __cu_input!(#input_index) }
2623    }
2624}
2625
2626fn generate_task_input_binding(
2627    step: &CuExecutionStep,
2628    mission_name: &str,
2629    output_pack_sizes: &[usize],
2630    task_input_layouts: &HashMap<String, TaskInputLayout>,
2631) -> GeneratedTaskInput {
2632    let task_id = step.node.get_id().to_string();
2633    let layout = task_input_layouts
2634        .get(&task_id)
2635        .unwrap_or_else(|| panic!("Missing canonical input layout for task '{task_id}'"));
2636    let slot_mapping = layout
2637        .mission_slot_mappings
2638        .get(mission_name)
2639        .unwrap_or_else(|| {
2640            panic!("Missing input slot mapping for task '{task_id}' in mission '{mission_name}'")
2641        });
2642
2643    let mut setup = Vec::new();
2644    let mut refs = Vec::new();
2645
2646    for (slot_index, slot) in layout.slots.iter().enumerate() {
2647        if let Some(input_index) = slot_mapping.get(slot_index).copied().flatten() {
2648            let input = step.input_msg_indices_types.get(input_index).unwrap_or_else(|| {
2649                panic!(
2650                    "Task '{task_id}' mission '{mission_name}' input slot {slot_index} mapped to missing input index {input_index}"
2651                )
2652            });
2653            refs.push(present_task_input_expr(input, output_pack_sizes));
2654            continue;
2655        }
2656
2657        let empty_input_ident = format_ident!("__cu_missing_input_{slot_index}");
2658        let input_ty: Type = parse_str(slot.msg_type.as_str()).unwrap_or_else(|err| {
2659            panic!(
2660                "Could not parse canonical input message type '{}' for task '{}': {err}",
2661                slot.msg_type, task_id
2662            )
2663        });
2664        setup.push(quote! {
2665            let #empty_input_ident = cu29::cutask::CuMsg::<#input_ty>::new(None);
2666        });
2667        refs.push(quote! { &#empty_input_ident });
2668    }
2669
2670    let expr = match refs.len() {
2671        0 => quote! { &() },
2672        1 => refs
2673            .into_iter()
2674            .next()
2675            .expect("single input expression missing"),
2676        _ => quote! { &( #(#refs),* ) },
2677    };
2678
2679    GeneratedTaskInput {
2680        setup: quote! { #(#setup)* },
2681        expr,
2682    }
2683}
2684
2685/// Builds the tuple of the CuList as a tuple off all the output slots.
2686fn build_culist_tuple(slot_types: &[Type]) -> TypeTuple {
2687    if slot_types.is_empty() {
2688        parse_quote! { () }
2689    } else {
2690        parse_quote! { ( #( #slot_types ),*, ) }
2691    }
2692}
2693
2694/// Builds the canonical generated encoding: one common metadata block followed
2695/// by the payload bytes selected for the local log view.
2696fn build_compressed_culist_tuple_encode(
2697    output_packs: &[OutputPack],
2698    encode_helper_names: &[Option<Ident>],
2699    slot_handle_modes: &[HandleContent],
2700    cumsg_count: usize,
2701    capture_slots: Option<&[bool]>,
2702) -> ItemImpl {
2703    let mut flat_idx = 0usize;
2704    let mut metadata_refs = Vec::with_capacity(cumsg_count);
2705    let mut original_idents = Vec::with_capacity(cumsg_count);
2706    let mut captured_idents = Vec::with_capacity(cumsg_count);
2707    let mut presence_initializers = Vec::with_capacity(cumsg_count);
2708    let mut payload_encoders = Vec::with_capacity(cumsg_count);
2709
2710    for (slot_idx, pack) in output_packs.iter().enumerate() {
2711        let slot_index = syn::Index::from(slot_idx);
2712        let mode = slot_handle_modes.get(slot_idx).copied().unwrap_or_default();
2713        for (port_idx, payload_ty) in pack.msg_types.iter().enumerate() {
2714            let access = if pack.is_multi() {
2715                let port_index = syn::Index::from(port_idx);
2716                quote! { self.0.#slot_index.#port_index }
2717            } else {
2718                quote! { self.0.#slot_index }
2719            };
2720            let original_ident = format_ident!("__cu_original_payload_{flat_idx}");
2721            let captured_ident = format_ident!("__cu_captured_payload_{flat_idx}");
2722            let helper = encode_helper_names[flat_idx].clone();
2723            let cache_index = flat_idx;
2724            flat_idx += 1;
2725
2726            metadata_refs.push(quote! {
2727                ::cu29::copperlist_codec::CommonMetadataRef::new(
2728                    &#access.tov,
2729                    &#access.metadata,
2730                )
2731            });
2732            original_idents.push(original_ident.clone());
2733            captured_idents.push(captured_ident.clone());
2734
2735            let captured_value = if capture_slots.is_some_and(|slots| !slots[slot_idx]) {
2736                quote! { false }
2737            } else if mode == HandleContent::default() {
2738                quote! { #original_ident }
2739            } else {
2740                let mode_u8 = mode as u8;
2741                quote! {
2742                    match #access.payload() {
2743                        Some(__cu_payload) => {
2744                            use ::cu29::pool::PayloadDefaultHandlePolicyApply as _;
2745                            use ::cu29::pool::PayloadDefaultLoggingPolicy as _;
2746                            __cu_payload.apply_handle_content_policy(
2747                                ::cu29::config::HandleContent::from_u8(#mode_u8),
2748                            );
2749                            __cu_payload.payload_should_log()
2750                        }
2751                        None => false,
2752                    }
2753                }
2754            };
2755            let awareness_check = if mode == HandleContent::default() {
2756                quote! {}
2757            } else {
2758                quote! {
2759                    const _: fn() = || {
2760                        fn assert_aware<__T: ::cu29::pool::HandleContentAware + ?::core::marker::Sized>() {}
2761                        assert_aware::<#payload_ty>();
2762                    };
2763                }
2764            };
2765            presence_initializers.push(quote! {
2766                #awareness_check
2767                let #original_ident = #access.payload().is_some();
2768                let #captured_ident = #captured_value;
2769            });
2770
2771            let encode_payload = if let Some(helper) = helper {
2772                quote! { #helper(__cu_payload, encoder)?; }
2773            } else {
2774                quote! {
2775                    ::cu29::copperlist_codec::encode_payload(__cu_payload, encoder)?;
2776                }
2777            };
2778            payload_encoders.push(quote! {
2779                if #captured_ident {
2780                    __cu_capture.select_slot(#cache_index);
2781                    let __cu_payload = #access.payload().ok_or(
2782                        EncodeError::Other("CopperList captured-payload plane mismatch")
2783                    )?;
2784                    #encode_payload
2785                }
2786            });
2787        }
2788    }
2789
2790    let (impl_header, method) = if capture_slots.is_some() {
2791        (
2792            quote! { impl CuStampedDataSet },
2793            format_ident!("__encode_capture"),
2794        )
2795    } else {
2796        (
2797            quote! { impl Encode for CuStampedDataSet },
2798            format_ident!("encode"),
2799        )
2800    };
2801    parse_quote! {
2802        #impl_header {
2803            fn #method<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
2804                let __cu_capture = cu29::monitoring::start_copperlist_io_capture(&self.1);
2805                #(#presence_initializers)*
2806                let __cu_metadata = [#(#metadata_refs),*];
2807                let __cu_original_payload_presence = [#(#original_idents),*];
2808                let __cu_captured_payload_presence = [#(#captured_idents),*];
2809                ::cu29::copperlist_codec::encode_common_metadata::<#cumsg_count, _>(
2810                    &__cu_metadata,
2811                    &__cu_original_payload_presence,
2812                    &__cu_captured_payload_presence,
2813                    encoder,
2814                )?;
2815                #(#payload_encoders)*
2816                Ok(())
2817            }
2818        }
2819    }
2820}
2821
2822fn build_compressed_culist_tuple_decode(
2823    output_packs: &[OutputPack],
2824    slot_types: &[Type],
2825    cumsg_count: usize,
2826    decode_helper_names: &[Option<Ident>],
2827    distributed: bool,
2828) -> ItemImpl {
2829    let mut flat_idx = 0usize;
2830    let mut decode_fields = Vec::with_capacity(slot_types.len());
2831
2832    for pack in output_packs {
2833        if pack.is_multi() {
2834            let mut fields = Vec::with_capacity(pack.msg_types.len());
2835            for payload_ty in &pack.msg_types {
2836                let metadata_index = flat_idx;
2837                let helper = decode_helper_names[flat_idx].clone();
2838                flat_idx += 1;
2839                let decode_payload = if let Some(helper) = helper {
2840                    quote! { #helper(decoder)? }
2841                } else {
2842                    quote! { ::cu29::copperlist_codec::decode_payload::<#payload_ty, _>(decoder)? }
2843                };
2844                fields.push(quote! {
2845                    {
2846                        let __cu_slot = __cu_metadata.take_slot(#metadata_index);
2847                        let __cu_payload = if __cu_slot.captured_payload_present {
2848                            Some(#decode_payload)
2849                        } else {
2850                            None
2851                        };
2852                        ::cu29::copperlist_codec::restore_msg(__cu_payload, __cu_slot)
2853                    }
2854                });
2855            }
2856            decode_fields.push(quote! { ( #(#fields),* ) });
2857        } else {
2858            let payload_ty = pack
2859                .msg_types
2860                .first()
2861                .expect("single-port pack must have a payload type");
2862            let metadata_index = flat_idx;
2863            let helper = decode_helper_names[flat_idx].clone();
2864            flat_idx += 1;
2865            let decode_payload = if let Some(helper) = helper {
2866                quote! { #helper(decoder)? }
2867            } else {
2868                quote! { ::cu29::copperlist_codec::decode_payload::<#payload_ty, _>(decoder)? }
2869            };
2870            decode_fields.push(quote! {
2871                {
2872                    let __cu_slot = __cu_metadata.take_slot(#metadata_index);
2873                    let __cu_payload = if __cu_slot.captured_payload_present {
2874                        Some(#decode_payload)
2875                    } else {
2876                        None
2877                    };
2878                    ::cu29::copperlist_codec::restore_msg(__cu_payload, __cu_slot)
2879                }
2880            });
2881        }
2882    }
2883
2884    let constructor = distributed.then(|| quote!(CuMessageRegions));
2885    parse_quote! {
2886        impl Decode<()> for CuStampedDataSet {
2887            fn decode<D: Decoder<Context=()>>(decoder: &mut D) -> Result<Self, DecodeError> {
2888                let mut __cu_metadata =
2889                    ::cu29::copperlist_codec::decode_common_metadata::<#cumsg_count, _>(decoder)?;
2890                Ok(CuStampedDataSet(
2891                    #constructor (
2892                        #((#decode_fields).into()),*,
2893                    ),
2894                    cu29::monitoring::CuMsgIoCache::<#cumsg_count>::default(),
2895                ))
2896            }
2897        }
2898    }
2899}
2900
2901/// This is the bincode encoding part of the CuStampedDataSet
2902fn build_culist_tuple_encode(
2903    output_packs: &[OutputPack],
2904    encode_helper_names: &[Option<Ident>],
2905    slot_handle_modes: &[HandleContent],
2906) -> ItemImpl {
2907    let mut flat_idx = 0usize;
2908    let mut encode_fields = Vec::new();
2909
2910    for (slot_idx, pack) in output_packs.iter().enumerate() {
2911        let slot_index = syn::Index::from(slot_idx);
2912        let mode = slot_handle_modes.get(slot_idx).copied();
2913
2914        if pack.is_multi() {
2915            for (port_idx, payload_ty) in pack.msg_types.iter().enumerate() {
2916                let port_index = syn::Index::from(port_idx);
2917                let cache_index = flat_idx;
2918                let encode_helper = encode_helper_names[flat_idx].clone();
2919                flat_idx += 1;
2920                let normal_encode = if let Some(helper) = encode_helper {
2921                    quote! { #helper(&self.0.#slot_index.#port_index, encoder)?; }
2922                } else {
2923                    quote! { self.0.#slot_index.#port_index.encode(encoder)?; }
2924                };
2925                let slot_access = quote! { self.0.#slot_index.#port_index };
2926                let slot_block =
2927                    build_per_slot_encode_block(mode, payload_ty, &slot_access, &normal_encode);
2928                encode_fields.push(quote! {
2929                    __cu_capture.select_slot(#cache_index);
2930                    #slot_block
2931                });
2932            }
2933        } else {
2934            let cache_index = flat_idx;
2935            let encode_helper = encode_helper_names[flat_idx].clone();
2936            flat_idx += 1;
2937            let normal_encode = if let Some(helper) = encode_helper {
2938                quote! { #helper(&self.0.#slot_index, encoder)?; }
2939            } else {
2940                quote! { self.0.#slot_index.encode(encoder)?; }
2941            };
2942            let slot_access = quote! { self.0.#slot_index };
2943            let payload_ty = pack
2944                .msg_types
2945                .first()
2946                .expect("single-port pack must have a payload type");
2947            let slot_block =
2948                build_per_slot_encode_block(mode, payload_ty, &slot_access, &normal_encode);
2949            encode_fields.push(quote! {
2950                __cu_capture.select_slot(#cache_index);
2951                #slot_block
2952            });
2953        }
2954    }
2955
2956    parse_quote! {
2957        impl Encode for CuStampedDataSet {
2958            fn encode<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
2959                let __cu_capture = cu29::monitoring::start_copperlist_io_capture(&self.1);
2960                #(#encode_fields)*
2961                Ok(())
2962            }
2963        }
2964    }
2965}
2966
2967/// Build the per-slot encode block. Mode `All` returns the existing encode call
2968/// (zero codegen change). Non-default modes wrap it with a `HandleContentAware`
2969/// bound check on the payload type and an autoref-specialized policy check that
2970/// routes to `encode_metadata_only` when the payload says skip.
2971fn build_per_slot_encode_block(
2972    mode: Option<HandleContent>,
2973    payload_ty: &Type,
2974    slot_access: &proc_macro2::TokenStream,
2975    normal_encode: &proc_macro2::TokenStream,
2976) -> proc_macro2::TokenStream {
2977    let mode = match mode {
2978        Some(m) if m != HandleContent::default() => m,
2979        _ => return normal_encode.clone(),
2980    };
2981    let mode_u8 = mode as u8;
2982    quote! {
2983        {
2984            // Catches the silent-no-op footgun: non-default handle_content on an
2985            // unmarked payload fails here with `HandleContentAware not satisfied`.
2986            const _: fn() = || {
2987                fn assert_aware<__T: ::cu29::pool::HandleContentAware + ?::core::marker::Sized>() {}
2988                assert_aware::<#payload_ty>();
2989            };
2990            use ::cu29::pool::PayloadDefaultHandlePolicyApply as _;
2991            use ::cu29::pool::PayloadDefaultLoggingPolicy as _;
2992            // Stamp the source's configured policy on whatever handles live in the
2993            // payload, then ask the (now-policy-aware) payload whether to log.
2994            let __cu_should_log = match #slot_access.payload() {
2995                Some(__cu_p) => {
2996                    __cu_p.apply_handle_content_policy(
2997                        ::cu29::config::HandleContent::from_u8(#mode_u8),
2998                    );
2999                    __cu_p.payload_should_log()
3000                }
3001                None => false,
3002            };
3003            if __cu_should_log {
3004                #normal_encode
3005            } else {
3006                ::cu29::cutask::encode_metadata_only(&#slot_access, encoder)?;
3007            }
3008        }
3009    }
3010}
3011
3012/// This is the bincode decoding part of the CuStampedDataSet
3013fn build_culist_tuple_decode(
3014    output_packs: &[OutputPack],
3015    slot_types: &[Type],
3016    cumsg_count: usize,
3017    decode_helper_names: &[Option<Ident>],
3018    distributed: bool,
3019) -> ItemImpl {
3020    let mut flat_idx = 0usize;
3021    let mut decode_fields = Vec::with_capacity(slot_types.len());
3022    for (slot_idx, pack) in output_packs.iter().enumerate() {
3023        let slot_type = &slot_types[slot_idx];
3024        if pack.is_multi() {
3025            let mut slot_fields = Vec::with_capacity(pack.msg_types.len());
3026            for _ in 0..pack.msg_types.len() {
3027                let decode_helper = decode_helper_names[flat_idx].clone();
3028                flat_idx += 1;
3029                if let Some(decode_helper) = decode_helper {
3030                    slot_fields.push(quote! { #decode_helper(decoder)? });
3031                } else {
3032                    let msg_type = &pack.msg_types[slot_fields.len()];
3033                    slot_fields.push(quote! { <CuMsg<#msg_type> as Decode<()>>::decode(decoder)? });
3034                }
3035            }
3036            decode_fields.push(quote! { ( #(#slot_fields),* ) });
3037        } else if let Some(decode_helper) = decode_helper_names[flat_idx].clone() {
3038            flat_idx += 1;
3039            decode_fields.push(quote! { #decode_helper(decoder)? });
3040        } else {
3041            flat_idx += 1;
3042            decode_fields.push(quote! { <#slot_type as Decode<()>>::decode(decoder)? });
3043        }
3044    }
3045
3046    let constructor = distributed.then(|| quote!(CuMessageRegions));
3047    parse_quote! {
3048        impl Decode<()> for CuStampedDataSet {
3049            fn decode<D: Decoder<Context=()>>(decoder: &mut D) -> Result<Self, DecodeError> {
3050                Ok(CuStampedDataSet(
3051                    #constructor (
3052                        #((#decode_fields).into()),*,
3053                    ),
3054                    cu29::monitoring::CuMsgIoCache::<#cumsg_count>::default(),
3055                ))
3056            }
3057        }
3058    }
3059}
3060
3061fn build_culist_erasedcumsgs(output_packs: &[OutputPack], distributed: bool) -> ItemImpl {
3062    let mut casted_fields: Vec<proc_macro2::TokenStream> = Vec::new();
3063    for (idx, pack) in output_packs.iter().enumerate() {
3064        let slot_index = syn::Index::from(idx);
3065        if pack.is_multi() {
3066            for port_idx in 0..pack.msg_types.len() {
3067                let port_index = syn::Index::from(port_idx);
3068                casted_fields.push(quote! {
3069                    &self.0.#slot_index.#port_index as &dyn ErasedCuStampedData
3070                });
3071            }
3072        } else {
3073            let value = distributed.then(|| quote!(.value));
3074            casted_fields.push(quote! { &self.0.#slot_index #value as &dyn ErasedCuStampedData });
3075        }
3076    }
3077    parse_quote! {
3078        impl ErasedCuStampedDataSet for CuStampedDataSet {
3079            fn cumsgs(&self) -> Vec<&dyn ErasedCuStampedData> {
3080                vec![
3081                    #(#casted_fields),*
3082                ]
3083            }
3084        }
3085    }
3086}
3087
3088fn build_culist_tuple_debug(slot_types: &[Type]) -> ItemImpl {
3089    let indices: Vec<usize> = (0..slot_types.len()).collect();
3090
3091    let debug_fields: Vec<_> = indices
3092        .iter()
3093        .map(|i| {
3094            let idx = syn::Index::from(*i);
3095            quote! { .field(&self.0.#idx) }
3096        })
3097        .collect();
3098
3099    parse_quote! {
3100        impl Debug for CuStampedDataSet {
3101            fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
3102                f.debug_tuple("CuStampedDataSet")
3103                    #(#debug_fields)*
3104                    .finish()
3105            }
3106        }
3107    }
3108}
3109
3110/// This is the serde serialization part of the CuStampedDataSet
3111fn build_culist_tuple_serialize(slot_types: &[Type]) -> ItemImpl {
3112    let indices: Vec<usize> = (0..slot_types.len()).collect();
3113    let tuple_len = slot_types.len();
3114
3115    // Generate the serialization for each tuple field
3116    let serialize_fields: Vec<_> = indices
3117        .iter()
3118        .map(|i| {
3119            let idx = syn::Index::from(*i);
3120            quote! { &self.0.#idx }
3121        })
3122        .collect();
3123
3124    parse_quote! {
3125        impl cu29::serde::ser::Serialize for CuStampedDataSet {
3126            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
3127            where
3128                S: cu29::serde::Serializer,
3129            {
3130                use cu29::serde::ser::SerializeTuple;
3131                let mut tuple = serializer.serialize_tuple(#tuple_len)?;
3132                #(tuple.serialize_element(#serialize_fields)?;)*
3133                tuple.end()
3134            }
3135        }
3136    }
3137}
3138
3139/// This is the default implementation for CuStampedDataSet
3140fn build_culist_tuple_default(
3141    slot_types: &[Type],
3142    cumsg_count: usize,
3143    distributed: bool,
3144) -> ItemImpl {
3145    let default_fields: Vec<_> = slot_types
3146        .iter()
3147        .map(|slot_type| quote! { <#slot_type as Default>::default() })
3148        .collect();
3149
3150    let constructor = distributed.then(|| quote!(CuMessageRegions));
3151    parse_quote! {
3152        impl Default for CuStampedDataSet {
3153            fn default() -> CuStampedDataSet
3154            {
3155                CuStampedDataSet(
3156                    #constructor (
3157                        #((#default_fields).into()),*,
3158                    ),
3159                    cu29::monitoring::CuMsgIoCache::<#cumsg_count>::default(),
3160                )
3161            }
3162        }
3163    }
3164}
3165
3166fn collect_bridge_channel_usage(graph: &CuGraph) -> HashMap<BridgeChannelKey, String> {
3167    let mut usage = HashMap::new();
3168    for cnx in graph.edges() {
3169        if let Some(channel) = &cnx.src_channel {
3170            let key = BridgeChannelKey {
3171                bridge_id: cnx.src.clone(),
3172                channel_id: channel.clone(),
3173                direction: BridgeChannelDirection::Rx,
3174            };
3175            usage
3176                .entry(key)
3177                .and_modify(|msg| {
3178                    if msg != &cnx.msg {
3179                        panic!(
3180                            "Bridge '{}' channel '{}' is used with incompatible message types: {} vs {}",
3181                            cnx.src, channel, msg, cnx.msg
3182                        );
3183                    }
3184                })
3185                .or_insert(cnx.msg.clone());
3186        }
3187        if let Some(channel) = &cnx.dst_channel {
3188            let key = BridgeChannelKey {
3189                bridge_id: cnx.dst.clone(),
3190                channel_id: channel.clone(),
3191                direction: BridgeChannelDirection::Tx,
3192            };
3193            usage
3194                .entry(key)
3195                .and_modify(|msg| {
3196                    if msg != &cnx.msg {
3197                        panic!(
3198                            "Bridge '{}' channel '{}' is used with incompatible message types: {} vs {}",
3199                            cnx.dst, channel, msg, cnx.msg
3200                        );
3201                    }
3202                })
3203                .or_insert(cnx.msg.clone());
3204        }
3205    }
3206    usage
3207}
3208
3209fn build_bridge_specs(
3210    config: &CuConfig,
3211    graph: &CuGraph,
3212    channel_usage: &HashMap<BridgeChannelKey, String>,
3213) -> Vec<BridgeSpec> {
3214    let mut specs = Vec::new();
3215    for (bridge_index, bridge_cfg) in config.bridges.iter().enumerate() {
3216        if graph.get_node_id_by_name(bridge_cfg.id.as_str()).is_none() {
3217            continue;
3218        }
3219
3220        let type_path = parse_str::<Type>(bridge_cfg.type_.as_str()).unwrap_or_else(|err| {
3221            panic!(
3222                "Could not parse bridge type '{}' for '{}': {err}",
3223                bridge_cfg.type_, bridge_cfg.id
3224            )
3225        });
3226
3227        let mut rx_channels = Vec::new();
3228        let mut tx_channels = Vec::new();
3229
3230        for (channel_index, channel) in bridge_cfg.channels.iter().enumerate() {
3231            match channel {
3232                BridgeChannelConfigRepresentation::Rx { id, .. } => {
3233                    let key = BridgeChannelKey {
3234                        bridge_id: bridge_cfg.id.clone(),
3235                        channel_id: id.clone(),
3236                        direction: BridgeChannelDirection::Rx,
3237                    };
3238                    if let Some(msg_type) = channel_usage.get(&key) {
3239                        let msg_type_name = msg_type.clone();
3240                        let msg_type = parse_str::<Type>(msg_type).unwrap_or_else(|err| {
3241                            panic!(
3242                                "Could not parse message type '{msg_type}' for bridge '{}' channel '{}': {err}",
3243                                bridge_cfg.id, id
3244                            )
3245                        });
3246                        let const_ident =
3247                            Ident::new(&config_id_to_bridge_const(id.as_str()), Span::call_site());
3248                        rx_channels.push(BridgeChannelSpec {
3249                            id: id.clone(),
3250                            const_ident,
3251                            msg_type,
3252                            msg_type_name,
3253                            config_index: channel_index,
3254                            plan_node_id: None,
3255                            culist_index: None,
3256                            monitor_index: None,
3257                        });
3258                    }
3259                }
3260                BridgeChannelConfigRepresentation::Tx { id, .. } => {
3261                    let key = BridgeChannelKey {
3262                        bridge_id: bridge_cfg.id.clone(),
3263                        channel_id: id.clone(),
3264                        direction: BridgeChannelDirection::Tx,
3265                    };
3266                    if let Some(msg_type) = channel_usage.get(&key) {
3267                        let msg_type_name = msg_type.clone();
3268                        let msg_type = parse_str::<Type>(msg_type).unwrap_or_else(|err| {
3269                            panic!(
3270                                "Could not parse message type '{msg_type}' for bridge '{}' channel '{}': {err}",
3271                                bridge_cfg.id, id
3272                            )
3273                        });
3274                        let const_ident =
3275                            Ident::new(&config_id_to_bridge_const(id.as_str()), Span::call_site());
3276                        tx_channels.push(BridgeChannelSpec {
3277                            id: id.clone(),
3278                            const_ident,
3279                            msg_type,
3280                            msg_type_name,
3281                            config_index: channel_index,
3282                            plan_node_id: None,
3283                            culist_index: None,
3284                            monitor_index: None,
3285                        });
3286                    }
3287                }
3288            }
3289        }
3290
3291        if rx_channels.is_empty() && tx_channels.is_empty() {
3292            continue;
3293        }
3294
3295        specs.push(BridgeSpec {
3296            id: bridge_cfg.id.clone(),
3297            type_path,
3298            run_in_sim: bridge_cfg.is_run_in_sim(),
3299            config_index: bridge_index,
3300            tuple_index: 0,
3301            monitor_index: None,
3302            rx_channels,
3303            tx_channels,
3304        });
3305    }
3306
3307    for (tuple_index, spec) in specs.iter_mut().enumerate() {
3308        spec.tuple_index = tuple_index;
3309    }
3310
3311    specs
3312}
3313
3314fn collect_task_names(graph: &CuGraph) -> Vec<(NodeId, String, String)> {
3315    graph
3316        .get_all_nodes()
3317        .iter()
3318        .filter(|(_, node)| node.get_flavor() == Flavor::Task)
3319        .map(|(node_id, node)| {
3320            (
3321                *node_id,
3322                node.get_id().to_string(),
3323                config_id_to_struct_member(node.get_id().as_str()),
3324            )
3325        })
3326        .collect()
3327}
3328
3329#[derive(Clone, Copy)]
3330enum ResourceOwner {
3331    Task(usize),
3332    Bridge(usize),
3333}
3334
3335#[derive(Clone)]
3336struct ResourceKeySpec {
3337    bundle_index: usize,
3338    provider_path: syn::Path,
3339    resource_name: String,
3340    binding_name: String,
3341    owner: ResourceOwner,
3342}
3343
3344fn parse_resource_path(path: &str) -> CuResult<(String, String)> {
3345    let (bundle_id, name) = path.split_once('.').ok_or_else(|| {
3346        CuError::from(format!(
3347            "Resource '{path}' is missing a bundle prefix (expected bundle.resource)"
3348        ))
3349    })?;
3350
3351    if bundle_id.is_empty() || name.is_empty() {
3352        return Err(CuError::from(format!(
3353            "Resource '{path}' must use the 'bundle.resource' format"
3354        )));
3355    }
3356
3357    Ok((bundle_id.to_string(), name.to_string()))
3358}
3359
3360fn collect_resource_specs(
3361    graph: &CuGraph,
3362    task_specs: &CuTaskSpecSet,
3363    bridge_specs: &[BridgeSpec],
3364    bundle_specs: &[BundleSpec],
3365) -> CuResult<Vec<ResourceKeySpec>> {
3366    let mut bridge_lookup: BTreeMap<String, usize> = BTreeMap::new();
3367    for (idx, spec) in bridge_specs.iter().enumerate() {
3368        bridge_lookup.insert(spec.id.clone(), idx);
3369    }
3370
3371    let mut bundle_lookup: HashMap<String, (usize, syn::Path)> = HashMap::new();
3372    for (index, bundle) in bundle_specs.iter().enumerate() {
3373        bundle_lookup.insert(bundle.id.clone(), (index, bundle.provider_path.clone()));
3374    }
3375
3376    let mut specs = Vec::new();
3377
3378    for (node_id, node) in graph.get_all_nodes() {
3379        let resources = node.get_resources();
3380        if let Some(resources) = resources {
3381            let task_index = task_specs.node_id_to_task_index[node_id as usize];
3382            let owner = if let Some(task_index) = task_index {
3383                ResourceOwner::Task(task_index)
3384            } else if node.get_flavor() == Flavor::Bridge {
3385                let bridge_index = bridge_lookup.get(&node.get_id()).ok_or_else(|| {
3386                    CuError::from(format!(
3387                        "Resource mapping attached to unknown bridge node '{}'",
3388                        node.get_id()
3389                    ))
3390                })?;
3391                ResourceOwner::Bridge(*bridge_index)
3392            } else {
3393                return Err(CuError::from(format!(
3394                    "Resource mapping attached to non-task node '{}'",
3395                    node.get_id()
3396                )));
3397            };
3398
3399            for (binding_name, path) in resources {
3400                let (bundle_id, resource_name) = parse_resource_path(path)?;
3401                let (bundle_index, provider_path) =
3402                    bundle_lookup.get(&bundle_id).ok_or_else(|| {
3403                        CuError::from(format!(
3404                            "Resource '{}' references unknown bundle '{}'",
3405                            path, bundle_id
3406                        ))
3407                    })?;
3408                specs.push(ResourceKeySpec {
3409                    bundle_index: *bundle_index,
3410                    provider_path: provider_path.clone(),
3411                    resource_name,
3412                    binding_name: binding_name.clone(),
3413                    owner,
3414                });
3415            }
3416        }
3417    }
3418
3419    Ok(specs)
3420}
3421
3422fn build_bundle_list<'a>(config: &'a CuConfig, mission: &str) -> Vec<&'a ResourceBundleConfig> {
3423    config
3424        .resources
3425        .iter()
3426        .filter(|bundle| {
3427            bundle
3428                .missions
3429                .as_ref()
3430                .is_none_or(|missions| missions.iter().any(|m| m == mission))
3431        })
3432        .collect()
3433}
3434
3435struct BundleSpec {
3436    inputs: Vec<(String, String)>,
3437    id: String,
3438    provider_path: syn::Path,
3439}
3440
3441struct LogStreamResourceSpec {
3442    destination_index: usize,
3443    stream: LogStreamEndpointSpec,
3444    feedback: Option<LogStreamEndpointSpec>,
3445}
3446
3447struct LogStreamEndpointSpec {
3448    bundle_index: usize,
3449    provider_path: syn::Path,
3450    resource_name: String,
3451    transport_type: syn::Type,
3452}
3453
3454fn build_logstream_resource_specs(
3455    config: &CuConfig,
3456    mission: &str,
3457    component_resources: &[ResourceKeySpec],
3458) -> CuResult<Vec<LogStreamResourceSpec>> {
3459    let Some(streaming) = &config.log_streaming else {
3460        return Ok(Vec::new());
3461    };
3462    let bundle_specs = build_bundle_specs(config, mission)?;
3463    let bundle_lookup: HashMap<_, _> = bundle_specs
3464        .iter()
3465        .enumerate()
3466        .map(|(index, bundle)| (bundle.id.as_str(), (index, &bundle.provider_path)))
3467        .collect();
3468    let endpoint = |destination_id: &str,
3469                    transport: &cu29_runtime::config::LogStreamTransportConfig|
3470     -> CuResult<LogStreamEndpointSpec> {
3471        let (bundle_id, resource_name) = parse_resource_path(&transport.resource)?;
3472        let (bundle_index, provider_path) = bundle_lookup.get(bundle_id.as_str()).ok_or_else(|| CuError::from(format!(
3473            "Log-stream destination '{destination_id}' references resource bundle '{bundle_id}' which is not active in mission '{mission}'"
3474        )))?;
3475        if component_resources.iter().any(|resource| {
3476            resource.bundle_index == *bundle_index && resource.resource_name == resource_name
3477        }) {
3478            return Err(CuError::from(format!(
3479                "Log-stream destination '{destination_id}' and a task or bridge both require exclusive resource '{}'",
3480                transport.resource
3481            )));
3482        }
3483        if bundle_specs.iter().any(|bundle| {
3484            bundle
3485                .inputs
3486                .iter()
3487                .any(|(_, path)| path == &transport.resource)
3488        }) {
3489            return Err(CuError::from(format!(
3490                "Log-stream destination '{destination_id}' and a resource bundle both require exclusive resource '{}'",
3491                transport.resource
3492            )));
3493        }
3494        let transport_type = parse_str::<Type>(&transport.type_).map_err(|error| CuError::from(format!(
3495            "Log-stream destination '{destination_id}' transport type '{}' is not a valid Rust type: {error}", transport.type_
3496        )))?;
3497        Ok(LogStreamEndpointSpec {
3498            bundle_index: *bundle_index,
3499            provider_path: (*provider_path).clone(),
3500            resource_name,
3501            transport_type,
3502        })
3503    };
3504    streaming
3505        .destinations
3506        .iter()
3507        .enumerate()
3508        .map(|(destination_index, destination)| {
3509            Ok(LogStreamResourceSpec {
3510                destination_index,
3511                stream: endpoint(&destination.id, &destination.transport)?,
3512                feedback: destination
3513                    .feedback
3514                    .as_ref()
3515                    .map(|f| endpoint(&destination.id, &f.transport))
3516                    .transpose()?,
3517            })
3518        })
3519        .collect()
3520}
3521
3522fn build_bundle_specs(config: &CuConfig, mission: &str) -> CuResult<Vec<BundleSpec>> {
3523    build_bundle_list(config, mission)
3524        .into_iter()
3525        .map(|bundle| {
3526            let provider_path: syn::Path =
3527                syn::parse_str(bundle.provider.as_str()).map_err(|err| {
3528                    CuError::from(format!(
3529                        "Failed to parse provider path '{}' for bundle '{}': {err}",
3530                        bundle.provider, bundle.id
3531                    ))
3532                })?;
3533            let mut inputs: Vec<_> = bundle
3534                .resources
3535                .iter()
3536                .flat_map(|map| map.iter())
3537                .map(|(name, path)| (name.clone(), path.clone()))
3538                .collect();
3539            inputs.sort();
3540            Ok(BundleSpec {
3541                inputs,
3542                id: bundle.id.clone(),
3543                provider_path,
3544            })
3545        })
3546        .collect()
3547}
3548
3549/// Compute construction order while preserving declaration-based resource keys.
3550fn resource_bundle_build_order(bundles: &[BundleSpec]) -> CuResult<Vec<usize>> {
3551    let mut lookup = HashMap::new();
3552    for (index, bundle) in bundles.iter().enumerate() {
3553        if lookup.insert(bundle.id.as_str(), index).is_some() {
3554            return Err(CuError::from(format!(
3555                "Duplicate resource bundle '{}'",
3556                bundle.id
3557            )));
3558        }
3559    }
3560    let dependencies = bundles.iter().map(|bundle| {
3561        bundle.inputs.iter().map(|(_, path)| {
3562            let (source, _) = parse_resource_path(path)?;
3563            lookup.get(source.as_str()).copied().ok_or_else(|| CuError::from(format!(
3564                "Resource bundle '{}' depends on '{}' which is not active in this mission", bundle.id, source
3565            )))
3566        }).collect::<CuResult<Vec<_>>>()
3567    }).collect::<CuResult<Vec<_>>>()?;
3568    let mut built = vec![false; bundles.len()];
3569    let mut order = Vec::with_capacity(bundles.len());
3570    while order.len() < bundles.len() {
3571        let Some(index) = (0..bundles.len()).find(|&index| {
3572            !built[index]
3573                && dependencies[index]
3574                    .iter()
3575                    .all(|&dependency| built[dependency])
3576        }) else {
3577            let cycle = bundles
3578                .iter()
3579                .enumerate()
3580                .filter(|(index, _)| !built[*index])
3581                .map(|(_, bundle)| bundle.id.as_str())
3582                .collect::<Vec<_>>()
3583                .join(", ");
3584            return Err(CuError::from(format!(
3585                "Resource dependency cycle involving: {cycle}"
3586            )));
3587        };
3588        built[index] = true;
3589        order.push(index);
3590    }
3591    Ok(order)
3592}
3593
3594#[cfg(test)]
3595mod resource_stack_tests {
3596    use super::*;
3597    type InputSpecs<'a> = &'a [(&'a str, &'a str)];
3598    fn specs(entries: &[(&str, InputSpecs<'_>)]) -> Vec<BundleSpec> {
3599        entries
3600            .iter()
3601            .map(|(id, inputs)| BundleSpec {
3602                id: (*id).into(),
3603                provider_path: syn::parse_str("Provider").unwrap(),
3604                inputs: inputs
3605                    .iter()
3606                    .map(|(name, path)| ((*name).into(), (*path).into()))
3607                    .collect(),
3608            })
3609            .collect()
3610    }
3611    #[test]
3612    fn resource_dependencies_validate_and_keep_indices() {
3613        assert_eq!(
3614            resource_bundle_build_order(&specs(&[
3615                ("stream", &[("serial", "radio.serial")]),
3616                ("board", &[]),
3617                ("radio", &[("serial", "board.uart")]),
3618            ]))
3619            .unwrap(),
3620            [1, 2, 0]
3621        );
3622        for invalid in [
3623            specs(&[("a", &[("x", "a.x")])]),
3624            specs(&[("a", &[("x", "b.x")]), ("b", &[("x", "a.x")])]),
3625            specs(&[("a", &[("x", "inactive.x")])]),
3626            specs(&[("a", &[]), ("a", &[])]),
3627        ] {
3628            assert!(resource_bundle_build_order(&invalid).is_err());
3629        }
3630    }
3631}
3632
3633fn build_resources_module(
3634    bundle_specs: &[BundleSpec],
3635) -> CuResult<(proc_macro2::TokenStream, proc_macro2::TokenStream)> {
3636    let bundle_consts = bundle_specs.iter().enumerate().map(|(index, bundle)| {
3637        let const_ident = Ident::new(
3638            &config_id_to_bridge_const(bundle.id.as_str()),
3639            Span::call_site(),
3640        );
3641        quote! { pub const #const_ident: BundleIndex = BundleIndex::new(#index); }
3642    });
3643
3644    let resources_module = quote! {
3645        pub mod resources {
3646            #![allow(dead_code)]
3647            use cu29::resource::BundleIndex;
3648
3649            pub mod bundles {
3650                use super::BundleIndex;
3651                #(#bundle_consts)*
3652            }
3653        }
3654    };
3655
3656    let bundle_counts = bundle_specs.iter().map(|bundle| {
3657        let provider_path = &bundle.provider_path;
3658        quote! { <#provider_path as cu29::resource::ResourceBundleDecl>::Id::COUNT }
3659    });
3660
3661    let order = resource_bundle_build_order(bundle_specs)?;
3662    let mut validations = Vec::new();
3663    let mut bundle_inits = Vec::new();
3664    for index in order {
3665        let bundle = &bundle_specs[index];
3666        let bundle_id = LitStr::new(&bundle.id, Span::call_site());
3667        let provider_path = &bundle.provider_path;
3668        let input_count = bundle.inputs.len();
3669        let checks = bundle.inputs.iter().map(|(name, path)| {
3670            quote! {
3671                bundle_cfg.resources.as_ref().and_then(|map| map.get(#name))
3672                    .is_some_and(|value| value == #path)
3673            }
3674        });
3675        validations.push(quote! {
3676            let bundle_cfg = config.resources.iter().find(|b| b.id == #bundle_id)
3677                .ok_or_else(|| cu29::CuError::from(concat!("Missing resource bundle: ", #bundle_id)))?;
3678            if bundle_cfg.resources.as_ref().map_or(0, |map| map.len()) != #input_count
3679                #(|| !(#checks))* {
3680                return Err(cu29::CuError::from(concat!("Resource input topology differs from compiled configuration: ", #bundle_id)));
3681            }
3682        });
3683        let mut entries = Vec::new();
3684        for (name, path) in &bundle.inputs {
3685            let (source_id, slot) = parse_resource_path(path)?;
3686            let source_index = bundle_specs
3687                .iter()
3688                .position(|source| source.id == source_id)
3689                .expect("validated resource dependency");
3690            let source_provider = &bundle_specs[source_index].provider_path;
3691            entries.push(quote! {
3692                (#name, cu29::resource::ResourceKey::new(
3693                    cu29::resource::BundleIndex::new(#source_index),
3694                    cu29::resource::resource_index_by_name::<#source_provider>(#slot),
3695                ))
3696            });
3697        }
3698        bundle_inits.push(quote! {
3699            {
3700                const INPUT_KEYS: [cu29::resource::ResourceKey;
3701                    <#provider_path as cu29::resource::ResourceBundle>::INPUT_NAMES.len()] =
3702                    cu29::resource::resource_input_keys(
3703                        <#provider_path as cu29::resource::ResourceBundle>::INPUT_NAMES,
3704                        &[#(#entries),*],
3705                    );
3706                let bundle_cfg = config.resources.iter().find(|b| b.id == #bundle_id)
3707                    .expect("validated resource bundle");
3708                let bundle_ctx = cu29::resource::BundleContext::<#provider_path>::new(
3709                    cu29::resource::BundleIndex::new(#index), #bundle_id,
3710                ).with_input_keys(&INPUT_KEYS);
3711                <#provider_path as cu29::resource::ResourceBundle>::build(
3712                    bundle_ctx, bundle_cfg.config.as_ref(), &mut manager,
3713                )?;
3714            }
3715        });
3716    }
3717
3718    let resources_instanciator = quote! {
3719        pub fn resources_instanciator(config: &CuConfig) -> CuResult<cu29::resource::ResourceManager> {
3720            #(#validations)*
3721            let bundle_counts: &[usize] = &[ #(#bundle_counts),* ];
3722            let mut manager = cu29::resource::ResourceManager::new(bundle_counts);
3723            #(#bundle_inits)*
3724            Ok(manager)
3725        }
3726    };
3727
3728    Ok((resources_module, resources_instanciator))
3729}
3730
3731struct ResourceMappingTokens {
3732    defs: proc_macro2::TokenStream,
3733    refs: Vec<proc_macro2::TokenStream>,
3734}
3735
3736fn build_task_resource_mappings(
3737    resource_specs: &[ResourceKeySpec],
3738    task_specs: &CuTaskSpecSet,
3739    sim_mode: bool,
3740) -> CuResult<ResourceMappingTokens> {
3741    let mut per_task: Vec<Vec<&ResourceKeySpec>> = vec![Vec::new(); task_specs.ids.len()];
3742
3743    for spec in resource_specs {
3744        let ResourceOwner::Task(task_index) = spec.owner else {
3745            continue;
3746        };
3747        if sim_mode
3748            && !task_specs.run_in_sim_flags[task_index]
3749            && task_specs.cutypes[task_index] != CuTaskType::Regular
3750        {
3751            continue;
3752        }
3753        per_task
3754            .get_mut(task_index)
3755            .ok_or_else(|| {
3756                CuError::from(format!(
3757                    "Resource '{}' mapped to invalid task index {}",
3758                    spec.binding_name, task_index
3759                ))
3760            })?
3761            .push(spec);
3762    }
3763
3764    let mut mapping_defs = Vec::new();
3765    let mut mapping_refs = Vec::new();
3766
3767    for (idx, entries) in per_task.iter().enumerate() {
3768        if entries.is_empty() {
3769            mapping_refs.push(quote! { None });
3770            continue;
3771        }
3772
3773        // A backgrounded task binds the resources of what the wrapper wraps —
3774        // the runner for an anytime node, the task itself otherwise.
3775        let binding_task_type = &task_specs.async_inner_task_types[idx];
3776
3777        let binding_trait = task_trait_for_specs(task_specs, idx);
3778
3779        let entries_ident = format_ident!("TASK{}_RES_ENTRIES", idx);
3780        let map_ident = format_ident!("TASK{}_RES_MAPPING", idx);
3781        let binding_type = quote! {
3782            <<#binding_task_type as #binding_trait>::Resources<'_> as ResourceBindings>::Binding
3783        };
3784        let entry_tokens = entries.iter().map(|spec| {
3785            let binding_ident = Ident::new(
3786                &config_id_to_enum(spec.binding_name.as_str()),
3787                Span::call_site(),
3788            );
3789            let resource_name = LitStr::new(spec.resource_name.as_str(), Span::call_site());
3790            let bundle_index = spec.bundle_index;
3791            let provider_path = &spec.provider_path;
3792            quote! {
3793                (#binding_type::#binding_ident, cu29::resource::ResourceKey::new(
3794                    cu29::resource::BundleIndex::new(#bundle_index),
3795                    cu29::resource::resource_index_by_name::<#provider_path>(#resource_name),
3796                ))
3797            }
3798        });
3799
3800        mapping_defs.push(quote! {
3801            const #entries_ident: &[(#binding_type, cu29::resource::ResourceKey)] = &[ #(#entry_tokens),* ];
3802            const #map_ident: cu29::resource::ResourceBindingMap<#binding_type> =
3803                cu29::resource::ResourceBindingMap::new(#entries_ident);
3804        });
3805        mapping_refs.push(quote! { Some(&#map_ident) });
3806    }
3807
3808    Ok(ResourceMappingTokens {
3809        defs: quote! { #(#mapping_defs)* },
3810        refs: mapping_refs,
3811    })
3812}
3813
3814fn build_bridge_resource_mappings(
3815    resource_specs: &[ResourceKeySpec],
3816    bridge_specs: &[BridgeSpec],
3817    sim_mode: bool,
3818) -> ResourceMappingTokens {
3819    let mut per_bridge: Vec<Vec<&ResourceKeySpec>> = vec![Vec::new(); bridge_specs.len()];
3820
3821    for spec in resource_specs {
3822        let ResourceOwner::Bridge(bridge_index) = spec.owner else {
3823            continue;
3824        };
3825        if sim_mode && !bridge_specs[bridge_index].run_in_sim {
3826            continue;
3827        }
3828        per_bridge[bridge_index].push(spec);
3829    }
3830
3831    let mut mapping_defs = Vec::new();
3832    let mut mapping_refs = Vec::new();
3833
3834    for (idx, entries) in per_bridge.iter().enumerate() {
3835        if entries.is_empty() {
3836            mapping_refs.push(quote! { None });
3837            continue;
3838        }
3839
3840        let bridge_type = &bridge_specs[idx].type_path;
3841        let binding_type = quote! {
3842            <<#bridge_type as cu29::cubridge::CuBridge>::Resources<'_> as ResourceBindings>::Binding
3843        };
3844        let entries_ident = format_ident!("BRIDGE{}_RES_ENTRIES", idx);
3845        let map_ident = format_ident!("BRIDGE{}_RES_MAPPING", idx);
3846        let entry_tokens = entries.iter().map(|spec| {
3847            let binding_ident = Ident::new(
3848                &config_id_to_enum(spec.binding_name.as_str()),
3849                Span::call_site(),
3850            );
3851            let resource_name = LitStr::new(spec.resource_name.as_str(), Span::call_site());
3852            let bundle_index = spec.bundle_index;
3853            let provider_path = &spec.provider_path;
3854            quote! {
3855                (#binding_type::#binding_ident, cu29::resource::ResourceKey::new(
3856                    cu29::resource::BundleIndex::new(#bundle_index),
3857                    cu29::resource::resource_index_by_name::<#provider_path>(#resource_name),
3858                ))
3859            }
3860        });
3861
3862        mapping_defs.push(quote! {
3863            const #entries_ident: &[(#binding_type, cu29::resource::ResourceKey)] = &[ #(#entry_tokens),* ];
3864            const #map_ident: cu29::resource::ResourceBindingMap<#binding_type> =
3865                cu29::resource::ResourceBindingMap::new(#entries_ident);
3866        });
3867        mapping_refs.push(quote! { Some(&#map_ident) });
3868    }
3869
3870    ResourceMappingTokens {
3871        defs: quote! { #(#mapping_defs)* },
3872        refs: mapping_refs,
3873    }
3874}
3875
3876struct ExecutionSchedule {
3877    background: Vec<CuPlanBackground>,
3878    lanes: Option<LanePlan>,
3879}
3880
3881type ExecutionPlanBuild = (
3882    CuExecutionLoop,
3883    Vec<ExecutionEntity>,
3884    HashMap<NodeId, NodeId>,
3885    ExecutionSchedule,
3886);
3887
3888fn build_execution_plan(
3889    config: &CuConfig,
3890    graph: &CuGraph,
3891    mission: &str,
3892    bridge_specs: &mut [BridgeSpec],
3893) -> CuResult<(
3894    CuExecutionLoop,
3895    Vec<ExecutionEntity>,
3896    HashMap<NodeId, NodeId>,
3897)> {
3898    build_execution_plan_with_schedule(config, graph, mission, bridge_specs)
3899        .map(|(execution, entities, plan_to_original, _)| (execution, entities, plan_to_original))
3900}
3901
3902fn build_execution_plan_with_schedule(
3903    config: &CuConfig,
3904    graph: &CuGraph,
3905    mission: &str,
3906    bridge_specs: &mut [BridgeSpec],
3907) -> CuResult<ExecutionPlanBuild> {
3908    let assembled = assemble_runtime_plan_for_mission(config, graph, mission)?;
3909    let mut exec_entities = Vec::with_capacity(assembled.entities.len());
3910    for (plan_node_id, entity) in assembled.entities.iter().enumerate() {
3911        let kind = match entity.kind {
3912            PlanEntityKind::Task { task_index, .. } => ExecutionEntityKind::Task { task_index },
3913            PlanEntityKind::BridgeRx {
3914                bridge_config_index,
3915                channel_config_index,
3916            } => {
3917                let bridge_index = bridge_specs
3918                    .iter()
3919                    .position(|bridge| bridge.config_index == bridge_config_index)
3920                    .expect("shared planner returned an unknown bridge");
3921                let channel_index = bridge_specs[bridge_index]
3922                    .rx_channels
3923                    .iter()
3924                    .position(|channel| channel.config_index == channel_config_index)
3925                    .expect("shared planner returned an unknown bridge rx channel");
3926                bridge_specs[bridge_index].rx_channels[channel_index].plan_node_id =
3927                    Some(plan_node_id as NodeId);
3928                ExecutionEntityKind::BridgeRx {
3929                    bridge_index,
3930                    channel_index,
3931                }
3932            }
3933            PlanEntityKind::BridgeTx {
3934                bridge_config_index,
3935                channel_config_index,
3936            } => {
3937                let bridge_index = bridge_specs
3938                    .iter()
3939                    .position(|bridge| bridge.config_index == bridge_config_index)
3940                    .expect("shared planner returned an unknown bridge");
3941                let channel_index = bridge_specs[bridge_index]
3942                    .tx_channels
3943                    .iter()
3944                    .position(|channel| channel.config_index == channel_config_index)
3945                    .expect("shared planner returned an unknown bridge tx channel");
3946                bridge_specs[bridge_index].tx_channels[channel_index].plan_node_id =
3947                    Some(plan_node_id as NodeId);
3948                ExecutionEntityKind::BridgeTx {
3949                    bridge_index,
3950                    channel_index,
3951                }
3952            }
3953        };
3954        exec_entities.push(ExecutionEntity { kind });
3955    }
3956    let plan_to_original = assembled
3957        .plan_to_original
3958        .iter()
3959        .enumerate()
3960        .filter_map(|(plan_node_id, original)| {
3961            original.map(|original| (plan_node_id as NodeId, original))
3962        })
3963        .collect();
3964    let schedule = ExecutionSchedule {
3965        background: assembled.background,
3966        lanes: assembled.lanes,
3967    };
3968    Ok((
3969        assembled.execution,
3970        exec_entities,
3971        plan_to_original,
3972        schedule,
3973    ))
3974}
3975
3976fn collect_culist_metadata(
3977    runtime_plan: &CuExecutionLoop,
3978    exec_entities: &[ExecutionEntity],
3979    bridge_specs: &mut [BridgeSpec],
3980    plan_to_original: &HashMap<NodeId, NodeId>,
3981) -> (Vec<usize>, HashMap<NodeId, usize>) {
3982    let mut culist_order = Vec::new();
3983    let mut node_output_positions = HashMap::new();
3984
3985    for unit in &runtime_plan.steps {
3986        if let CuExecutionUnit::Step(step) = unit
3987            && step.phase != CuStepPhase::AnytimeRefine // refine steps reuse the base step's slot
3988            && let Some(output_pack) = &step.output_msg_pack
3989        {
3990            let output_idx = output_pack.culist_index;
3991            culist_order.push(output_idx as usize);
3992            match &exec_entities[step.node_id as usize].kind {
3993                ExecutionEntityKind::Task { .. } => {
3994                    if let Some(original_node_id) = plan_to_original.get(&step.node_id) {
3995                        node_output_positions.insert(*original_node_id, output_idx as usize);
3996                    }
3997                }
3998                ExecutionEntityKind::BridgeRx {
3999                    bridge_index,
4000                    channel_index,
4001                } => {
4002                    bridge_specs[*bridge_index].rx_channels[*channel_index].culist_index =
4003                        Some(output_idx as usize);
4004                }
4005                ExecutionEntityKind::BridgeTx {
4006                    bridge_index,
4007                    channel_index,
4008                } => {
4009                    bridge_specs[*bridge_index].tx_channels[*channel_index].culist_index =
4010                        Some(output_idx as usize);
4011                }
4012            }
4013        }
4014    }
4015
4016    (culist_order, node_output_positions)
4017}
4018
4019fn build_monitor_culist_component_mapping(
4020    runtime_plan: &CuExecutionLoop,
4021    exec_entities: &[ExecutionEntity],
4022    bridge_specs: &[BridgeSpec],
4023) -> Result<Vec<usize>, String> {
4024    let mut mapping = Vec::new();
4025    for unit in &runtime_plan.steps {
4026        if let CuExecutionUnit::Step(step) = unit
4027            && step.phase != CuStepPhase::AnytimeRefine // refine steps reuse the base step's slot
4028            && step.output_msg_pack.is_some()
4029        {
4030            let Some(entity) = exec_entities.get(step.node_id as usize) else {
4031                return Err(format!(
4032                    "Missing execution entity for plan node {} while building monitor mapping",
4033                    step.node_id
4034                ));
4035            };
4036            let component_index = match &entity.kind {
4037                ExecutionEntityKind::Task { task_index } => *task_index,
4038                ExecutionEntityKind::BridgeRx {
4039                    bridge_index,
4040                    channel_index,
4041                } => bridge_specs
4042                    .get(*bridge_index)
4043                    .and_then(|spec| spec.rx_channels.get(*channel_index))
4044                    .and_then(|channel| channel.monitor_index)
4045                    .ok_or_else(|| {
4046                        format!(
4047                            "Missing monitor index for bridge rx {}:{}",
4048                            bridge_index, channel_index
4049                        )
4050                    })?,
4051                ExecutionEntityKind::BridgeTx {
4052                    bridge_index,
4053                    channel_index,
4054                } => bridge_specs
4055                    .get(*bridge_index)
4056                    .and_then(|spec| spec.tx_channels.get(*channel_index))
4057                    .and_then(|channel| channel.monitor_index)
4058                    .ok_or_else(|| {
4059                        format!(
4060                            "Missing monitor index for bridge tx {}:{}",
4061                            bridge_index, channel_index
4062                        )
4063                    })?,
4064            };
4065            mapping.push(component_index);
4066        }
4067    }
4068    Ok(mapping)
4069}
4070
4071fn build_parallel_rt_stage_entries(
4072    runtime_plan: &CuExecutionLoop,
4073    exec_entities: &[ExecutionEntity],
4074    task_specs: &CuTaskSpecSet,
4075    bridge_specs: &[BridgeSpec],
4076) -> CuResult<Vec<proc_macro2::TokenStream>> {
4077    let mut entries = Vec::new();
4078
4079    for unit in &runtime_plan.steps {
4080        let CuExecutionUnit::Step(step) = unit else {
4081            todo!("parallel runtime metadata for nested loops is not implemented yet")
4082        };
4083
4084        // An anytime node is one parallel-rt stage: its base and refine steps
4085        // all run inside the base step's stage, on the same worker thread
4086        // (memory locality), so refine steps produce no stage of their own.
4087        if step.phase == CuStepPhase::AnytimeRefine {
4088            continue;
4089        }
4090
4091        let entity = exec_entities.get(step.node_id as usize).ok_or_else(|| {
4092            CuError::from(format!(
4093                "Missing execution entity for runtime plan node {} while building parallel runtime metadata",
4094                step.node_id
4095            ))
4096        })?;
4097
4098        let (label, kind_tokens, component_index) = match &entity.kind {
4099            ExecutionEntityKind::Task { task_index } => (
4100                task_specs
4101                    .ids
4102                    .get(*task_index)
4103                    .cloned()
4104                    .ok_or_else(|| {
4105                        CuError::from(format!(
4106                            "Missing task id for task index {} while building parallel runtime metadata",
4107                            task_index
4108                        ))
4109                    })?,
4110                quote! { cu29::parallel_rt::ParallelRtStageKind::Task },
4111                *task_index,
4112            ),
4113            ExecutionEntityKind::BridgeRx {
4114                bridge_index,
4115                channel_index,
4116            } => {
4117                let bridge = bridge_specs.get(*bridge_index).ok_or_else(|| {
4118                    CuError::from(format!(
4119                        "Missing bridge spec {} while building parallel runtime metadata",
4120                        bridge_index
4121                    ))
4122                })?;
4123                let channel = bridge.rx_channels.get(*channel_index).ok_or_else(|| {
4124                    CuError::from(format!(
4125                        "Missing bridge rx channel {}:{} while building parallel runtime metadata",
4126                        bridge_index, channel_index
4127                    ))
4128                })?;
4129                let component_index = channel.monitor_index.ok_or_else(|| {
4130                    CuError::from(format!(
4131                        "Missing monitor index for bridge rx {}:{} while building parallel runtime metadata",
4132                        bridge_index, channel_index
4133                    ))
4134                })?;
4135                (
4136                    format!("bridge::{}::rx::{}", bridge.id, channel.id),
4137                    quote! { cu29::parallel_rt::ParallelRtStageKind::BridgeRx },
4138                    component_index,
4139                )
4140            }
4141            ExecutionEntityKind::BridgeTx {
4142                bridge_index,
4143                channel_index,
4144            } => {
4145                let bridge = bridge_specs.get(*bridge_index).ok_or_else(|| {
4146                    CuError::from(format!(
4147                        "Missing bridge spec {} while building parallel runtime metadata",
4148                        bridge_index
4149                    ))
4150                })?;
4151                let channel = bridge.tx_channels.get(*channel_index).ok_or_else(|| {
4152                    CuError::from(format!(
4153                        "Missing bridge tx channel {}:{} while building parallel runtime metadata",
4154                        bridge_index, channel_index
4155                    ))
4156                })?;
4157                let component_index = channel.monitor_index.ok_or_else(|| {
4158                    CuError::from(format!(
4159                        "Missing monitor index for bridge tx {}:{} while building parallel runtime metadata",
4160                        bridge_index, channel_index
4161                    ))
4162                })?;
4163                (
4164                    format!("bridge::{}::tx::{}", bridge.id, channel.id),
4165                    quote! { cu29::parallel_rt::ParallelRtStageKind::BridgeTx },
4166                    component_index,
4167                )
4168            }
4169        };
4170
4171        let node_id = step.node_id;
4172        entries.push(quote! {
4173            cu29::parallel_rt::ParallelRtStageMetadata::new(
4174                #label,
4175                #kind_tokens,
4176                #node_id,
4177                cu29::monitoring::ComponentId::new(#component_index),
4178            )
4179        });
4180    }
4181
4182    Ok(entries)
4183}
4184
4185#[allow(dead_code)]
4186fn build_monitored_ids(task_ids: &[String], bridge_specs: &mut [BridgeSpec]) -> Vec<String> {
4187    let mut names = task_ids.to_vec();
4188    for spec in bridge_specs.iter_mut() {
4189        spec.monitor_index = Some(names.len());
4190        names.push(format!("bridge::{}", spec.id));
4191        for channel in spec.rx_channels.iter_mut() {
4192            channel.monitor_index = Some(names.len());
4193            names.push(format!("bridge::{}::rx::{}", spec.id, channel.id));
4194        }
4195        for channel in spec.tx_channels.iter_mut() {
4196            channel.monitor_index = Some(names.len());
4197            names.push(format!("bridge::{}::tx::{}", spec.id, channel.id));
4198        }
4199    }
4200    names
4201}
4202
4203fn wrap_process_step_tokens(
4204    wrap_process_step: bool,
4205    body: proc_macro2::TokenStream,
4206) -> proc_macro2::TokenStream {
4207    if wrap_process_step {
4208        quote! {{
4209            let __cu_process_step_result: cu29::curuntime::ProcessStepResult = (|| {
4210                #body
4211                Ok(cu29::curuntime::ProcessStepOutcome::Continue)
4212            })();
4213            __cu_process_step_result
4214        }}
4215    } else {
4216        body
4217    }
4218}
4219
4220fn abort_process_step_tokens(wrap_process_step: bool) -> proc_macro2::TokenStream {
4221    if wrap_process_step {
4222        quote! {
4223            return Ok(cu29::curuntime::ProcessStepOutcome::AbortCopperList);
4224        }
4225    } else {
4226        quote! {
4227            __cu_abort_copperlist = true;
4228            break '__cu_process_steps;
4229        }
4230    }
4231}
4232
4233fn parallel_task_lifecycle_tokens(
4234    task_trait: proc_macro2::TokenStream,
4235    task_type: &Type,
4236    stateless: bool,
4237    component_index: usize,
4238    mission_mod: &Ident,
4239    task_instance: &proc_macro2::TokenStream,
4240    placement: ParallelLifecyclePlacement,
4241) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
4242    let rt_guard = rtsan_guard_tokens();
4243    let abort_process_step = abort_process_step_tokens(true);
4244
4245    let preprocess_alloc_open = alloc_scope_open_tokens();
4246    let preprocess_alloc_close = alloc_scope_close_tokens(
4247        quote! { monitor },
4248        quote! { #component_index },
4249        quote! { CuComponentState::Preprocess },
4250    );
4251    let postprocess_alloc_open = alloc_scope_open_tokens();
4252    let postprocess_alloc_close = alloc_scope_close_tokens(
4253        quote! { monitor },
4254        quote! { #component_index },
4255        quote! { CuComponentState::Postprocess },
4256    );
4257    let task_borrow = if stateless {
4258        quote! { &#task_instance }
4259    } else {
4260        quote! { &mut #task_instance }
4261    };
4262    let preprocess = if placement.preprocess {
4263        quote! {
4264            execution_probe.record(cu29::monitoring::ExecutionMarker {
4265                component_id: cu29::monitoring::ComponentId::new(#component_index),
4266                step: CuComponentState::Preprocess,
4267                culistid: Some(clid),
4268            });
4269            ctx.set_current_task(#component_index);
4270            #preprocess_alloc_open
4271            let maybe_error = {
4272                #rt_guard
4273                <#task_type as #task_trait>::preprocess(#task_borrow, &ctx)
4274            };
4275            #preprocess_alloc_close
4276            if let Err(error) = maybe_error {
4277                let decision = monitor.process_error(
4278                    cu29::monitoring::ComponentId::new(#component_index),
4279                    CuComponentState::Preprocess,
4280                    &error,
4281                );
4282                match decision {
4283                    Decision::Abort => {
4284                        debug!(ctx,
4285                            "Preprocess: ABORT decision from monitoring. Component '{}' errored out during preprocess. Aborting CopperList {}.",
4286                            #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index)),
4287                            clid
4288                        );
4289                        #abort_process_step
4290                    }
4291                    Decision::Ignore => {
4292                        debug!(ctx,
4293                            "Preprocess: IGNORE decision from monitoring. Component '{}' errored out during preprocess. The runtime will continue.",
4294                            #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index))
4295                        );
4296                    }
4297                    Decision::Shutdown => {
4298                        debug!(ctx,
4299                            "Preprocess: SHUTDOWN decision from monitoring. Component '{}' errored out during preprocess. The runtime cannot continue.",
4300                            #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index))
4301                        );
4302                        return Err(CuError::new_with_cause(
4303                            "Component errored out during preprocess.",
4304                            error,
4305                        ));
4306                    }
4307                }
4308            }
4309        }
4310    } else {
4311        quote! {}
4312    };
4313
4314    let postprocess = if placement.postprocess {
4315        quote! {
4316            execution_probe.record(cu29::monitoring::ExecutionMarker {
4317                component_id: cu29::monitoring::ComponentId::new(#component_index),
4318                step: CuComponentState::Postprocess,
4319                culistid: Some(clid),
4320            });
4321            ctx.set_current_task(#component_index);
4322            #postprocess_alloc_open
4323            let maybe_error = {
4324                #rt_guard
4325                <#task_type as #task_trait>::postprocess(#task_borrow, &ctx)
4326            };
4327            #postprocess_alloc_close
4328            if let Err(error) = maybe_error {
4329                let decision = monitor.process_error(
4330                    cu29::monitoring::ComponentId::new(#component_index),
4331                    CuComponentState::Postprocess,
4332                    &error,
4333                );
4334                match decision {
4335                    Decision::Abort => {
4336                        debug!(ctx,
4337                            "Postprocess: ABORT decision from monitoring. Component '{}' errored out during postprocess. Continuing with the completed CopperList.",
4338                            #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index))
4339                        );
4340                    }
4341                    Decision::Ignore => {
4342                        debug!(ctx,
4343                            "Postprocess: IGNORE decision from monitoring. Component '{}' errored out during postprocess. The runtime will continue.",
4344                            #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index))
4345                        );
4346                    }
4347                    Decision::Shutdown => {
4348                        debug!(ctx,
4349                            "Postprocess: SHUTDOWN decision from monitoring. Component '{}' errored out during postprocess. The runtime cannot continue.",
4350                            #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index))
4351                        );
4352                        return Err(CuError::new_with_cause(
4353                            "Component errored out during postprocess.",
4354                            error,
4355                        ));
4356                    }
4357                }
4358            }
4359        }
4360    } else {
4361        quote! {}
4362    };
4363
4364    (preprocess, postprocess)
4365}
4366
4367fn parallel_bridge_lifecycle_tokens(
4368    bridge_type: &Type,
4369    component_index: usize,
4370    mission_mod: &Ident,
4371    placement: ParallelLifecyclePlacement,
4372    keyframe_logging_enabled: bool,
4373) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
4374    let rt_guard = rtsan_guard_tokens();
4375    let abort_process_step = abort_process_step_tokens(true);
4376    let freeze_bridge = keyframe_freeze_bridge_tokens(keyframe_logging_enabled);
4377
4378    let preprocess_alloc_open = alloc_scope_open_tokens();
4379    let preprocess_alloc_close = alloc_scope_close_tokens(
4380        quote! { monitor },
4381        quote! { #component_index },
4382        quote! { CuComponentState::Preprocess },
4383    );
4384    let postprocess_alloc_open = alloc_scope_open_tokens();
4385    let postprocess_alloc_close = alloc_scope_close_tokens(
4386        quote! { monitor },
4387        quote! { #component_index },
4388        quote! { CuComponentState::Postprocess },
4389    );
4390    let preprocess = if placement.preprocess {
4391        quote! {
4392            execution_probe.record(cu29::monitoring::ExecutionMarker {
4393                component_id: cu29::monitoring::ComponentId::new(#component_index),
4394                step: CuComponentState::Preprocess,
4395                culistid: Some(clid),
4396            });
4397            ctx.set_current_component(#component_index);
4398            ctx.clear_current_task();
4399            #preprocess_alloc_open
4400            let maybe_error = {
4401                #rt_guard
4402                <#bridge_type as cu29::cubridge::CuBridge>::preprocess(bridge, &ctx)
4403            };
4404            #preprocess_alloc_close
4405            if let Err(error) = maybe_error {
4406                let decision = monitor.process_error(
4407                    cu29::monitoring::ComponentId::new(#component_index),
4408                    CuComponentState::Preprocess,
4409                    &error,
4410                );
4411                match decision {
4412                    Decision::Abort => {
4413                        debug!(ctx,
4414                            "Preprocess: ABORT decision from monitoring. Component '{}' errored out during preprocess. Aborting CopperList {}.",
4415                            #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index)),
4416                            clid
4417                        );
4418                        #abort_process_step
4419                    }
4420                    Decision::Ignore => {
4421                        debug!(ctx,
4422                            "Preprocess: IGNORE decision from monitoring. Component '{}' errored out during preprocess. The runtime will continue.",
4423                            #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index))
4424                        );
4425                    }
4426                    Decision::Shutdown => {
4427                        debug!(ctx,
4428                            "Preprocess: SHUTDOWN decision from monitoring. Component '{}' errored out during preprocess. The runtime cannot continue.",
4429                            #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index))
4430                        );
4431                        return Err(CuError::new_with_cause(
4432                            "Component errored out during preprocess.",
4433                            error,
4434                        ));
4435                    }
4436                }
4437            }
4438        }
4439    } else {
4440        quote! {}
4441    };
4442
4443    let postprocess = if placement.postprocess {
4444        quote! {
4445            #freeze_bridge
4446            execution_probe.record(cu29::monitoring::ExecutionMarker {
4447                component_id: cu29::monitoring::ComponentId::new(#component_index),
4448                step: CuComponentState::Postprocess,
4449                culistid: Some(clid),
4450            });
4451            ctx.set_current_component(#component_index);
4452            ctx.clear_current_task();
4453            #postprocess_alloc_open
4454            let maybe_error = {
4455                #rt_guard
4456                <#bridge_type as cu29::cubridge::CuBridge>::postprocess(bridge, &ctx)
4457            };
4458            #postprocess_alloc_close
4459            if let Err(error) = maybe_error {
4460                let decision = monitor.process_error(
4461                    cu29::monitoring::ComponentId::new(#component_index),
4462                    CuComponentState::Postprocess,
4463                    &error,
4464                );
4465                match decision {
4466                    Decision::Abort => {
4467                        debug!(ctx,
4468                            "Postprocess: ABORT decision from monitoring. Component '{}' errored out during postprocess. Continuing with the completed CopperList.",
4469                            #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index))
4470                        );
4471                    }
4472                    Decision::Ignore => {
4473                        debug!(ctx,
4474                            "Postprocess: IGNORE decision from monitoring. Component '{}' errored out during postprocess. The runtime will continue.",
4475                            #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index))
4476                        );
4477                    }
4478                    Decision::Shutdown => {
4479                        debug!(ctx,
4480                            "Postprocess: SHUTDOWN decision from monitoring. Component '{}' errored out during postprocess. The runtime cannot continue.",
4481                            #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#component_index))
4482                        );
4483                        return Err(CuError::new_with_cause(
4484                            "Component errored out during postprocess.",
4485                            error,
4486                        ));
4487                    }
4488                }
4489            }
4490        }
4491    } else {
4492        quote! {}
4493    };
4494
4495    (preprocess, postprocess)
4496}
4497
4498/// Mission-module name of the `AnytimePolicy` ZST emitted for one anytime node.
4499fn anytime_policy_ident(task_id: &str) -> Ident {
4500    format_ident!("__CuAnytimePolicy_{}", config_id_to_struct_member(task_id))
4501}
4502
4503/// Name of the hoisted `Option<AnytimeJob>` local for one anytime node — the
4504/// only value crossing its base/refine steps (is the job still live).
4505fn anytime_job_ident(task_id: &str) -> Ident {
4506    format_ident!("__cu_anytime_job_{}", config_id_to_struct_member(task_id))
4507}
4508
4509fn anytime_ms_to_nanos(ms: f64) -> u64 {
4510    (ms * 1_000_000.0).round() as u64
4511}
4512
4513/// The `__cu_any_now` binding shared by the base and refine emitters: one clock
4514/// read per base call and per refine quantum. Without a time knob every timed
4515/// check const-folds away and `now` only feeds the debug-only elapsed, so the
4516/// read is skipped entirely (`CuTime` subtraction saturates, so the zero
4517/// stand-in is safe).
4518fn anytime_now_binding_tokens(anytime: &AnytimeConfig) -> proc_macro2::TokenStream {
4519    if anytime.time_budget_ms.is_some() || anytime.max_age_ms.is_some() {
4520        quote! { let __cu_any_now = clock.now(); }
4521    } else {
4522        quote! { let __cu_any_now = cu29::clock::CuTime::default(); }
4523    }
4524}
4525
4526fn anytime_option_duration_tokens(ms: Option<f64>) -> proc_macro2::TokenStream {
4527    match ms {
4528        Some(ms) => {
4529            let nanos = anytime_ms_to_nanos(ms);
4530            quote! { Some(cu29::clock::CuDuration(#nanos)) }
4531        }
4532        None => quote! { None },
4533    }
4534}
4535
4536/// One `AnytimePolicy` ZST per anytime node, its RON policy baked in as
4537/// consts and overrides so every unset knob const-folds away.
4538///
4539/// A quality knob (`quality_target`/`quality_floor`/`max_stall`) pins the
4540/// implementation to the shared `Quality` scale, which is the compile-time
4541/// gate rejecting those knobs on `Quality = ()` tasks (the job's
4542/// `AnytimePolicy<T::Quality>` bound fails). Without quality knobs the
4543/// implementation is generic over any comparable quality.
4544fn build_anytime_policy_defs(task_specs: &CuTaskSpecSet) -> Vec<proc_macro2::TokenStream> {
4545    task_specs
4546        .anytime_configs
4547        .iter()
4548        .enumerate()
4549        .filter_map(|(index, anytime)| {
4550            let anytime = anytime.as_ref()?;
4551            let policy_ident = anytime_policy_ident(&task_specs.ids[index]);
4552            let time_budget = anytime_option_duration_tokens(anytime.time_budget_ms);
4553            let max_age = anytime_option_duration_tokens(anytime.max_age_ms);
4554            let max_stall = match anytime.max_stall {
4555                Some(stall) => quote! { Some(#stall) },
4556                None => quote! { None },
4557            };
4558            // Only the background runner reads MAX_REFINES: a foreground node
4559            // carries the count as the number of refine steps in its plan.
4560            let max_refines = match anytime.max_refines {
4561                Some(refines) => quote! { Some(#refines) },
4562                None => quote! { None },
4563            };
4564            let consts = quote! {
4565                const TIME_BUDGET: Option<cu29::clock::CuDuration> = #time_budget;
4566                const MAX_AGE: Option<cu29::clock::CuDuration> = #max_age;
4567                const MAX_STALL: Option<u32> = #max_stall;
4568                const MAX_REFINES: Option<u32> = #max_refines;
4569            };
4570            let has_quality_knob = anytime.quality_target.is_some()
4571                || anytime.quality_floor.is_some()
4572                || anytime.max_stall.is_some();
4573            let policy_impl = if has_quality_knob {
4574                let target_met = anytime.quality_target.map(|target| {
4575                    quote! {
4576                        #[inline(always)]
4577                        fn target_met(q: cu29::cutask_anytime::Quality) -> bool {
4578                            q >= cu29::cutask_anytime::quality_from_f32(#target)
4579                        }
4580                    }
4581                });
4582                let below_floor = anytime.quality_floor.map(|floor| {
4583                    quote! {
4584                        #[inline(always)]
4585                        fn below_floor(q: cu29::cutask_anytime::Quality) -> bool {
4586                            // NaN fails closed: an unordered quality counts as below the floor.
4587                            q.partial_cmp(&cu29::cutask_anytime::quality_from_f32(#floor))
4588                                .is_none_or(core::cmp::Ordering::is_lt)
4589                        }
4590                    }
4591                });
4592                quote! {
4593                    impl cu29::cutask_anytime::AnytimePolicy<cu29::cutask_anytime::Quality>
4594                        for #policy_ident
4595                    {
4596                        #consts
4597                        #target_met
4598                        #below_floor
4599                    }
4600                }
4601            } else {
4602                quote! {
4603                    impl<Q: Copy + PartialOrd> cu29::cutask_anytime::AnytimePolicy<Q>
4604                        for #policy_ident
4605                    {
4606                        #consts
4607                    }
4608                }
4609            };
4610            Some(quote! {
4611                #[allow(non_camel_case_types)]
4612                pub struct #policy_ident;
4613                #policy_impl
4614            })
4615        })
4616        .collect()
4617}
4618
4619/// The hoisted per-node job local, declared above the plan splice: step bodies
4620/// can be wrapped in closures, so the local cannot live inside a step. The
4621/// `Option` answers the single genuinely dynamic cross-step question — is the
4622/// job still live; nothing else crosses steps.
4623fn anytime_job_local_tokens(task_specs: &CuTaskSpecSet, index: usize) -> proc_macro2::TokenStream {
4624    let task_type = &task_specs.task_types[index];
4625    let policy_ident = anytime_policy_ident(&task_specs.ids[index]);
4626    let job_ident = anytime_job_ident(&task_specs.ids[index]);
4627    quote! {
4628        #[allow(non_snake_case, unused_mut)]
4629        let mut #job_ident: Option<
4630            cu29::cutask_anytime::AnytimeJob<
4631                <#task_type as cu29::cutask_anytime::CuAnytimeTask>::Quality,
4632                #policy_ident,
4633            >,
4634        > = None;
4635    }
4636}
4637
4638fn build_anytime_job_locals(task_specs: &CuTaskSpecSet) -> Vec<proc_macro2::TokenStream> {
4639    task_specs
4640        .anytime_configs
4641        .iter()
4642        .enumerate()
4643        // A background anytime node keeps its single `Whole` step and runs its
4644        // job inside the runner, so it has no plan-level job to carry.
4645        .filter(|(index, anytime)| anytime.is_some() && !task_specs.background_flags[*index])
4646        .map(|(index, _)| anytime_job_local_tokens(task_specs, index))
4647        .collect()
4648}
4649
4650/// The output-slot cast trait/fn pair shared by the source, regular, and
4651/// anytime step emitters: `kind_label`/`fn_prefix` keep each emitter's
4652/// diagnostic idents, `task_trait` is the trait whose `Output` the slot must
4653/// match. Each emitted block is its own scope, so the fixed names never
4654/// collide.
4655fn output_slot_cast_tokens(
4656    task_hint: &str,
4657    kind_label: &str,
4658    fn_prefix: &str,
4659    task_trait: &proc_macro2::TokenStream,
4660) -> (proc_macro2::TokenStream, Ident) {
4661    let trait_ident = format_ident!(
4662        "__CuOutputSlotMustMatchTaskOutput__{}_{}__Add_dst___nc___connections_for_unused_outputs",
4663        kind_label,
4664        task_hint
4665    );
4666    let fn_ident = format_ident!(
4667        "__cu_{}_output_slot_or_add_dst___nc___for_unused_outputs__task_{}",
4668        fn_prefix,
4669        task_hint
4670    );
4671    let defs = quote! {
4672        #[allow(non_camel_case_types)]
4673        trait #trait_ident<Expected> {
4674            fn __cu_cast_output_slot(slot: &mut Self) -> &mut Expected;
4675        }
4676        impl<T> #trait_ident<T> for T {
4677            fn __cu_cast_output_slot(slot: &mut Self) -> &mut T {
4678                slot
4679            }
4680        }
4681
4682        fn #fn_ident<'a, Task, Slot>(
4683            _task: &Task,
4684            slot: &'a mut Slot,
4685        ) -> &'a mut Task::Output<'static>
4686        where
4687            Task: #task_trait,
4688            Slot: #trait_ident<Task::Output<'static>>,
4689        {
4690            <Slot as #trait_ident<Task::Output<'static>>>::__cu_cast_output_slot(slot)
4691        }
4692    };
4693    (defs, fn_ident)
4694}
4695
4696/// Anytime flavor of [`output_slot_cast_tokens`].
4697fn anytime_slot_cast_tokens(task_hint: &str) -> (proc_macro2::TokenStream, Ident) {
4698    output_slot_cast_tokens(
4699        task_hint,
4700        "AnytimeTask",
4701        "anytime",
4702        &quote! { cu29::cutask_anytime::CuAnytimeTask },
4703    )
4704}
4705
4706/// The process-error monitoring match shared by every task step emitter
4707/// (source, sink, regular, and anytime base/refine blocks).
4708fn process_monitoring_action_tokens(
4709    tid: usize,
4710    mission_mod: &Ident,
4711    output_culist_index: &syn::Index,
4712    output_clear_payload: &proc_macro2::TokenStream,
4713    wrap_process_step: bool,
4714) -> proc_macro2::TokenStream {
4715    let abort_process_step = abort_process_step_tokens(wrap_process_step);
4716    quote! {
4717        debug!(ctx, "Component {}: Error during process: {}", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#tid)), &error);
4718        let decision = monitor.process_error(cu29::monitoring::ComponentId::new(#tid), CuComponentState::Process, &error);
4719        match decision {
4720            Decision::Abort => {
4721                debug!(ctx, "Process: ABORT decision from monitoring. Component '{}' errored out \
4722                        during process. Skipping the processing of CL {}.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#tid)), clid);
4723                #abort_process_step
4724            }
4725            Decision::Ignore => {
4726                debug!(ctx, "Process: IGNORE decision from monitoring. Component '{}' errored out \
4727                        during process. The runtime will continue with a forced empty message.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#tid)));
4728                let cumsg_output = __cu_output!(#output_culist_index);
4729                #output_clear_payload
4730            }
4731            Decision::Shutdown => {
4732                debug!(ctx, "Process: SHUTDOWN decision from monitoring. Component '{}' errored out \
4733                        during process. The runtime cannot continue.", #mission_mod::monitor_component_label(cu29::monitoring::ComponentId::new(#tid)));
4734                return Err(CuError::new_with_cause("Component errored out during process.", error));
4735            }
4736        }
4737    }
4738}
4739
4740/// The sim-callback `doit` gate shared by the sink, regular, and anytime-base
4741/// step emitters. `input_binding` is the `let cumsg_input = <expr>;` line;
4742/// sources have none and pass `()` to the callback state.
4743fn process_sim_callback_tokens(
4744    sim_mode: bool,
4745    enum_name: &Ident,
4746    input_binding: Option<&proc_macro2::TokenStream>,
4747    output_culist_index: &syn::Index,
4748    monitoring_action: &proc_macro2::TokenStream,
4749) -> proc_macro2::TokenStream {
4750    if !sim_mode {
4751        return quote! { let doit = true; };
4752    }
4753    let (input_line, state_input) = match input_binding {
4754        Some(binding) => ((*binding).clone(), quote! { cumsg_input }),
4755        None => (quote! {}, quote! { () }),
4756    };
4757    quote! {
4758        let doit = {
4759            #input_line
4760            let cumsg_output = __cu_output!(#output_culist_index);
4761            let state = CuTaskCallbackState::Process(#state_input, cumsg_output);
4762            let ovr = sim_callback(SimStep::#enum_name(state));
4763
4764            if let SimOverride::Errored(reason) = ovr  {
4765                let error: CuError = reason.into();
4766                #monitoring_action
4767                false
4768            }
4769            else {
4770                ovr == SimOverride::ExecuteByRuntime
4771            }
4772        };
4773    }
4774}
4775
4776/// Emits the `[base <node>]` block of a foreground anytime node: the standard
4777/// step wrapper (probe, keyframe freeze, sim callback, monitoring, alloc
4778/// scopes) around the DOA age check — emitted only when `max_age_ms` is
4779/// configured — and one `base()` call. On `Improved` the job lands in the
4780/// node's hoisted local; `Converged`/`Aborted` finish on the spot and the
4781/// local stays `None`, so every refine block no-ops through its liveness test.
4782///
4783/// An anytime node is single-output, so the monitoring clear is the one-slot
4784/// form. The job local needs no explicit kill on error: a job is only
4785/// (re)stored on an `Ok` status, so an erroring block leaves it `None`/taken
4786/// and later quanta no-op.
4787///
4788/// Returns the block plus the payload-drop logging tokens (same contract as
4789/// `generate_task_execution_tokens`).
4790fn generate_anytime_base_block(
4791    step: &CuExecutionStep,
4792    task_index: usize,
4793    task_specs: &CuTaskSpecSet,
4794    ctx: &StepGenerationContext<'_>,
4795    task_instance: &proc_macro2::TokenStream,
4796) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
4797    let tid = task_index;
4798    let anytime = task_specs.anytime_configs[tid]
4799        .as_ref()
4800        .expect("anytime base block emitted for a task without an anytime policy");
4801    let now_binding = anytime_now_binding_tokens(anytime);
4802    let task_id = &task_specs.ids[tid];
4803    let policy_ident = anytime_policy_ident(task_id);
4804    let job_ident = anytime_job_ident(task_id);
4805    let enum_name = Ident::new(&config_id_to_enum(task_id), Span::call_site());
4806    let task_hint = config_id_to_struct_member(task_id);
4807    let (slot_cast_defs, slot_cast_fn) = anytime_slot_cast_tokens(&task_hint);
4808    let rt_guard = rtsan_guard_tokens();
4809    let mission_mod = ctx.mission_mod;
4810    let freeze_task = keyframe_freeze_task_tokens(ctx.keyframe_logging_enabled, task_instance);
4811
4812    let comment_str = format!(
4813        "DEBUG ->> {} ({:?}/{:?}) Id:{} I:{:?} O:{:?}",
4814        step.node.get_id(),
4815        step.task_type,
4816        step.phase,
4817        step.node_id,
4818        step.input_msg_indices_types,
4819        step.output_msg_pack
4820    );
4821    let comment_tokens = quote! {{
4822        let _ = stringify!(#comment_str);
4823    }};
4824
4825    let output_pack = step
4826        .output_msg_pack
4827        .as_ref()
4828        .expect("Anytime task should have an output message pack.");
4829    let output_culist_index = int2sliceindex(output_pack.culist_index);
4830    let monitoring_action = process_monitoring_action_tokens(
4831        tid,
4832        mission_mod,
4833        &output_culist_index,
4834        &quote! { cumsg_output.clear_payload(); },
4835        ctx.wrap_process_step,
4836    );
4837
4838    let GeneratedTaskInput {
4839        setup: task_input_setup,
4840        expr: task_input_expr,
4841    } = generate_task_input_binding(
4842        step,
4843        ctx.mission_name,
4844        ctx.output_pack_sizes,
4845        ctx.task_input_layouts,
4846    );
4847
4848    // The sim callback fires once per node, here in the base block, with the
4849    // standard Process state. On ExecutedBySim the job local simply stays
4850    // `None` — no refine-side sim machinery exists or is needed.
4851    let call_sim_callback = process_sim_callback_tokens(
4852        ctx.sim_mode,
4853        &enum_name,
4854        Some(&quote! { let cumsg_input = #task_input_expr; }),
4855        &output_culist_index,
4856        &monitoring_action,
4857    );
4858
4859    // Anchor extraction and the DOA skip exist in the text only when max_age
4860    // is configured; otherwise the anchor degenerates to the budget anchor.
4861    let base_call = quote! {
4862        match #task_instance.base(&ctx, cumsg_input, cumsg_output) {
4863            Ok(cu29::cutask_anytime::AnytimeStatus::Improved(q)) => {
4864                #job_ident = Some(cu29::cutask_anytime::AnytimeJob::new(__cu_any_now, __cu_any_anchor, q));
4865                Ok(())
4866            }
4867            Ok(cu29::cutask_anytime::AnytimeStatus::Converged(q)) => {
4868                let __cu_any_job: cu29::cutask_anytime::AnytimeJob<_, #policy_ident> =
4869                    cu29::cutask_anytime::AnytimeJob::new(__cu_any_now, __cu_any_anchor, q);
4870                // The job's clock read is reused: `finish` spends `now` only on
4871                // the debug-only elapsed, so a terminal base pays no second read.
4872                let __cu_any_outcome = __cu_any_job.finish(
4873                    __cu_any_now,
4874                    cu29::cutask_anytime::AnytimeStopCause::Converged,
4875                    0u32,
4876                    cumsg_output,
4877                );
4878                debug!(ctx, "Anytime task {}: {} after {} refinement(s).", #task_id, __cu_any_outcome.stop.label(), __cu_any_outcome.iterations);
4879                Ok(())
4880            }
4881            Ok(cu29::cutask_anytime::AnytimeStatus::Aborted) => {
4882                let __cu_any_outcome = cu29::cutask_anytime::abort_at_base(__cu_any_now, __cu_any_now, cumsg_output);
4883                debug!(ctx, "Anytime task {}: {} after {} refinement(s).", #task_id, __cu_any_outcome.stop.label(), __cu_any_outcome.iterations);
4884                Ok(())
4885            }
4886            Err(error) => Err(error),
4887        }
4888    };
4889    let base_dispatch = if let Some(max_age_ms) = anytime.max_age_ms {
4890        let max_age_nanos = anytime_ms_to_nanos(max_age_ms);
4891        quote! {
4892            let __cu_any_anchor = cu29::cutask_anytime::anchor_from_tov(cumsg_input.tov, __cu_any_now);
4893            if __cu_any_now >= __cu_any_anchor + cu29::clock::CuDuration(#max_age_nanos) {
4894                let __cu_any_outcome = cu29::cutask_anytime::skip_stale(cumsg_output);
4895                debug!(ctx, "Anytime task {}: input dead on arrival, job skipped.", #task_id);
4896                let _ = __cu_any_outcome;
4897                Ok(())
4898            } else {
4899                #base_call
4900            }
4901        }
4902    } else {
4903        quote! {
4904            let __cu_any_anchor = __cu_any_now;
4905            #base_call
4906        }
4907    };
4908
4909    let alloc_open = alloc_scope_open_tokens();
4910    let alloc_close = alloc_scope_close_tokens(
4911        quote! { monitor },
4912        quote! { #tid },
4913        quote! { CuComponentState::Process },
4914    );
4915
4916    let block = quote! {
4917        {
4918            #comment_tokens
4919            // One snapshot per copperlist, before the job: refine blocks must
4920            // not re-freeze.
4921            #freeze_task
4922            #task_input_setup
4923            #call_sim_callback
4924            let cumsg_input = #task_input_expr;
4925            let cumsg_output = __cu_output!(#output_culist_index);
4926            let maybe_error = if doit {
4927                execution_probe.record(cu29::monitoring::ExecutionMarker {
4928                    component_id: cu29::monitoring::ComponentId::new(#tid),
4929                    step: CuComponentState::Process,
4930                    culistid: Some(clid),
4931                });
4932                #slot_cast_defs
4933                if cumsg_output.metadata.process_time.start.is_none() {
4934                    cumsg_output.metadata.process_time.start = cu29::curuntime::perf_now(clock).into();
4935                }
4936                #alloc_open
4937                let result = {
4938                    let cumsg_output = #slot_cast_fn(&#task_instance, cumsg_output);
4939                    #rt_guard
4940                    ctx.set_current_task(#tid);
4941                    #now_binding
4942                    #base_dispatch
4943                };
4944                // A live job means a refine block runs and stamps `end`; only
4945                // a job that terminated at the base site stamps it here.
4946                if #job_ident.is_none() {
4947                    cumsg_output.metadata.process_time.end = cu29::curuntime::perf_now(clock).into();
4948                }
4949                #alloc_close
4950                result
4951            } else {
4952                Ok(())
4953            };
4954            if let Err(error) = maybe_error {
4955                #monitoring_action
4956            }
4957        }
4958    };
4959
4960    let logging_tokens = if !task_specs.logging_enabled[tid] {
4961        quote! {
4962            let cumsg_output = &mut culist.msgs.0.#output_culist_index;
4963            cumsg_output.clear_payload();
4964        }
4965    } else {
4966        quote!()
4967    };
4968
4969    (block, logging_tokens)
4970}
4971
4972/// Emits the `k`-th of `total` refine blocks of a foreground anytime node:
4973/// liveness test on the hoisted local, then the configured between-quanta
4974/// `check()`, then one `refine()` quantum. `k` and last-ness are generation
4975/// time facts — the runtime carries no counter; the LAST block never puts the
4976/// job back and stops a still-live job with `MaxRefines` purely by position.
4977fn generate_anytime_refine_block(
4978    step: &CuExecutionStep,
4979    task_index: usize,
4980    task_specs: &CuTaskSpecSet,
4981    ctx: &StepGenerationContext<'_>,
4982    task_instance: &proc_macro2::TokenStream,
4983    k: u32,
4984    total: u32,
4985) -> proc_macro2::TokenStream {
4986    let tid = task_index;
4987    let task_id = &task_specs.ids[tid];
4988    let job_ident = anytime_job_ident(task_id);
4989    let task_hint = config_id_to_struct_member(task_id);
4990    let (slot_cast_defs, slot_cast_fn) = anytime_slot_cast_tokens(&task_hint);
4991    let rt_guard = rtsan_guard_tokens();
4992
4993    let comment_str = format!(
4994        "DEBUG ->> {} ({:?}/{:?} {}/{}) Id:{} O:{:?}",
4995        step.node.get_id(),
4996        step.task_type,
4997        step.phase,
4998        k,
4999        total,
5000        step.node_id,
5001        step.output_msg_pack
5002    );
5003    let comment_tokens = quote! {{
5004        let _ = stringify!(#comment_str);
5005    }};
5006
5007    let output_pack = step
5008        .output_msg_pack
5009        .as_ref()
5010        .expect("Anytime refine step should carry the base step's output pack.");
5011    let output_culist_index = int2sliceindex(output_pack.culist_index);
5012    let monitoring_action = process_monitoring_action_tokens(
5013        tid,
5014        ctx.mission_mod,
5015        &output_culist_index,
5016        &quote! { cumsg_output.clear_payload(); },
5017        ctx.wrap_process_step,
5018    );
5019
5020    // A live job at block k has run exactly k-1 quanta — every earlier block
5021    // either ran its quantum or killed the job — so these are plain literals.
5022    let iters_on_check_stop = k - 1;
5023    let iters_with_quantum = k;
5024    let finish_log = quote! {
5025        debug!(ctx, "Anytime task {}: {} after {} refinement(s).", #task_id, __cu_any_outcome.stop.label(), __cu_any_outcome.iterations);
5026    };
5027    let anytime = task_specs.anytime_configs[tid]
5028        .as_ref()
5029        .expect("anytime refine block emitted for a task without an anytime policy");
5030    let now_binding = anytime_now_binding_tokens(anytime);
5031    // Only the Improved tail differs between blocks: put the job back while
5032    // the plan has more quanta; the LAST block stops a still-live job with
5033    // MaxRefines BY POSITION — the plan has no more quanta for it.
5034    let improved_tail = if k < total {
5035        quote! {
5036            #job_ident = Some(__cu_any_job); // still live: put it back
5037            Ok(())
5038        }
5039    } else {
5040        quote! {
5041            let __cu_any_outcome = __cu_any_job.finish(__cu_any_now, cu29::cutask_anytime::AnytimeStopCause::MaxRefines, #iters_with_quantum, cumsg_output);
5042            #finish_log
5043            Ok(())
5044        }
5045    };
5046    let quantum = quote! {
5047        #now_binding
5048        if let Some(__cu_any_cause) = __cu_any_job.check(__cu_any_now) {
5049            let __cu_any_outcome = __cu_any_job.finish(__cu_any_now, __cu_any_cause, #iters_on_check_stop, cumsg_output);
5050            #finish_log
5051            Ok(())
5052        } else {
5053            match #task_instance.refine(&ctx, cumsg_output) {
5054                Ok(cu29::cutask_anytime::AnytimeStatus::Improved(q)) => {
5055                    __cu_any_job.record(q);
5056                    #improved_tail
5057                }
5058                Ok(cu29::cutask_anytime::AnytimeStatus::Converged(q)) => {
5059                    __cu_any_job.record(q);
5060                    let __cu_any_outcome = __cu_any_job.finish(__cu_any_now, cu29::cutask_anytime::AnytimeStopCause::Converged, #iters_with_quantum, cumsg_output);
5061                    #finish_log
5062                    Ok(())
5063                }
5064                Ok(cu29::cutask_anytime::AnytimeStatus::Aborted) => {
5065                    let __cu_any_outcome = __cu_any_job.finish(__cu_any_now, cu29::cutask_anytime::AnytimeStopCause::Aborted, #iters_with_quantum, cumsg_output);
5066                    #finish_log
5067                    Ok(())
5068                }
5069                Err(error) => Err(error),
5070            }
5071        }
5072    };
5073
5074    let alloc_open = alloc_scope_open_tokens();
5075    let alloc_close = alloc_scope_close_tokens(
5076        quote! { monitor },
5077        quote! { #tid },
5078        quote! { CuComponentState::Process },
5079    );
5080
5081    quote! {
5082        {
5083            #comment_tokens
5084            // Liveness test: an earlier block that finished (or errored) the
5085            // job left the local empty, so this quantum no-ops.
5086            if let Some(mut __cu_any_job) = #job_ident.take() {
5087                execution_probe.record(cu29::monitoring::ExecutionMarker {
5088                    component_id: cu29::monitoring::ComponentId::new(#tid),
5089                    step: CuComponentState::Process,
5090                    culistid: Some(clid),
5091                });
5092                let cumsg_output = __cu_output!(#output_culist_index);
5093                #slot_cast_defs
5094                #alloc_open
5095                let maybe_error = {
5096                    let cumsg_output = #slot_cast_fn(&#task_instance, cumsg_output);
5097                    #rt_guard
5098                    ctx.set_current_task(#tid);
5099                    #quantum
5100                };
5101                // A still-live job means a later block runs and stamps `end`;
5102                // only the job's terminal quantum pays the clock read.
5103                if #job_ident.is_none() {
5104                    cumsg_output.metadata.process_time.end = cu29::curuntime::perf_now(clock).into();
5105                }
5106                #alloc_close
5107                if let Err(error) = maybe_error {
5108                    // No explicit job kill needed: the job was taken and is
5109                    // only put back on Ok(Improved), so later quanta no-op.
5110                    #monitoring_action
5111                }
5112            }
5113        }
5114    }
5115}
5116
5117#[derive(Clone, Copy)]
5118struct StepGenerationContext<'a> {
5119    output_pack_sizes: &'a [usize],
5120    task_input_layouts: &'a HashMap<String, TaskInputLayout>,
5121    mission_name: &'a str,
5122    sim_mode: bool,
5123    keyframe_logging_enabled: bool,
5124    mission_mod: &'a Ident,
5125    lifecycle_placement: ParallelLifecyclePlacement,
5126    wrap_process_step: bool,
5127}
5128
5129impl<'a> StepGenerationContext<'a> {
5130    #[allow(clippy::too_many_arguments)]
5131    fn new(
5132        output_pack_sizes: &'a [usize],
5133        task_input_layouts: &'a HashMap<String, TaskInputLayout>,
5134        mission_name: &'a str,
5135        sim_mode: bool,
5136        keyframe_logging_enabled: bool,
5137        mission_mod: &'a Ident,
5138        lifecycle_placement: ParallelLifecyclePlacement,
5139        wrap_process_step: bool,
5140    ) -> Self {
5141        Self {
5142            output_pack_sizes,
5143            task_input_layouts,
5144            mission_name,
5145            sim_mode,
5146            keyframe_logging_enabled,
5147            mission_mod,
5148            lifecycle_placement,
5149            wrap_process_step,
5150        }
5151    }
5152}
5153
5154struct TaskExecutionTokens {
5155    setup: proc_macro2::TokenStream,
5156    instance: proc_macro2::TokenStream,
5157}
5158
5159fn keyframe_freeze_task_tokens(
5160    enabled: bool,
5161    task_instance: &proc_macro2::TokenStream,
5162) -> proc_macro2::TokenStream {
5163    if enabled {
5164        quote! { kf_manager.freeze_task(clid, &#task_instance)?; }
5165    } else {
5166        quote! {}
5167    }
5168}
5169
5170fn keyframe_freeze_bridge_tokens(enabled: bool) -> proc_macro2::TokenStream {
5171    if enabled {
5172        quote! { kf_manager.freeze_any(clid, bridge)?; }
5173    } else {
5174        quote! {}
5175    }
5176}
5177
5178impl TaskExecutionTokens {
5179    fn new(setup: proc_macro2::TokenStream, instance: proc_macro2::TokenStream) -> Self {
5180        Self { setup, instance }
5181    }
5182}
5183
5184fn generate_task_execution_tokens(
5185    step: &CuExecutionStep,
5186    task_index: usize,
5187    task_specs: &CuTaskSpecSet,
5188    runtime_task_type: &Type,
5189    ctx: StepGenerationContext<'_>,
5190    task_tokens: TaskExecutionTokens,
5191) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
5192    let StepGenerationContext {
5193        output_pack_sizes,
5194        task_input_layouts,
5195        mission_name,
5196        sim_mode,
5197        keyframe_logging_enabled,
5198        mission_mod,
5199        lifecycle_placement,
5200        wrap_process_step,
5201    } = ctx;
5202    let TaskExecutionTokens {
5203        setup: task_setup,
5204        instance: task_instance,
5205    } = task_tokens;
5206    let keyframe_state = if task_specs.stateless_flags[task_index] {
5207        quote! { cu29::cutask::CuStatelessTaskState }
5208    } else {
5209        task_instance.clone()
5210    };
5211    let freeze_task = keyframe_freeze_task_tokens(keyframe_logging_enabled, &keyframe_state);
5212    let comment_str = format!(
5213        "DEBUG ->> {} ({:?}) Id:{} I:{:?} O:{:?}",
5214        step.node.get_id(),
5215        step.task_type,
5216        step.node_id,
5217        step.input_msg_indices_types,
5218        step.output_msg_pack
5219    );
5220    let comment_tokens = quote! {{
5221        let _ = stringify!(#comment_str);
5222    }};
5223    let tid = task_index;
5224    let task_enum_name = config_id_to_enum(&task_specs.ids[tid]);
5225    let enum_name = Ident::new(&task_enum_name, Span::call_site());
5226    let task_hint = config_id_to_struct_member(&task_specs.ids[tid]);
5227    let rt_guard = rtsan_guard_tokens();
5228    let run_in_sim_flag = task_specs.run_in_sim_flags[tid];
5229    let (parallel_task_preprocess, parallel_task_postprocess) = parallel_task_lifecycle_tokens(
5230        task_trait_for_specs(task_specs, tid),
5231        runtime_task_type,
5232        task_specs.stateless_flags[tid],
5233        tid,
5234        mission_mod,
5235        &task_instance,
5236        lifecycle_placement,
5237    );
5238    let lag_wait = if task_specs.background_result_lags[tid].is_some() && !sim_mode {
5239        quote! { #task_instance.wait_for_job()?; }
5240    } else {
5241        quote! {}
5242    };
5243    let maybe_sim_tick = if sim_mode && !run_in_sim_flag {
5244        quote! {
5245            if !doit {
5246                #task_instance.sim_tick();
5247            }
5248        }
5249    } else {
5250        quote!()
5251    };
5252
5253    let output_pack = step
5254        .output_msg_pack
5255        .as_ref()
5256        .expect("Task should have an output message pack.");
5257    let output_culist_index = int2sliceindex(output_pack.culist_index);
5258    let output_ports: Vec<syn::Index> = (0..output_pack.msg_types.len())
5259        .map(syn::Index::from)
5260        .collect();
5261    let output_clear_payload = if output_ports.len() == 1 {
5262        quote! { cumsg_output.clear_payload(); }
5263    } else {
5264        quote! { #(cumsg_output.#output_ports.clear_payload();)* }
5265    };
5266    let output_start_time = if output_ports.len() == 1 {
5267        quote! {
5268            if cumsg_output.metadata.process_time.start.is_none() {
5269                cumsg_output.metadata.process_time.start = cu29::curuntime::perf_now(clock).into();
5270            }
5271        }
5272    } else {
5273        quote! {
5274            let start_time = cu29::curuntime::perf_now(clock).into();
5275            #( if cumsg_output.#output_ports.metadata.process_time.start.is_none() {
5276                cumsg_output.#output_ports.metadata.process_time.start = start_time;
5277            } )*
5278        }
5279    };
5280    let output_end_time = if output_ports.len() == 1 {
5281        quote! {
5282            if cumsg_output.metadata.process_time.end.is_none() {
5283                cumsg_output.metadata.process_time.end = cu29::curuntime::perf_now(clock).into();
5284            }
5285        }
5286    } else {
5287        quote! {
5288            let end_time = cu29::curuntime::perf_now(clock).into();
5289            #( if cumsg_output.#output_ports.metadata.process_time.end.is_none() {
5290                cumsg_output.#output_ports.metadata.process_time.end = end_time;
5291            } )*
5292        }
5293    };
5294
5295    match step.task_type {
5296        CuTaskType::Source => {
5297            let monitoring_action = process_monitoring_action_tokens(
5298                tid,
5299                mission_mod,
5300                &output_culist_index,
5301                &output_clear_payload,
5302                wrap_process_step,
5303            );
5304
5305            let call_sim_callback = process_sim_callback_tokens(
5306                sim_mode,
5307                &enum_name,
5308                None,
5309                &output_culist_index,
5310                &monitoring_action,
5311            );
5312
5313            let logging_tokens = if !task_specs.logging_enabled[tid] {
5314                quote! {
5315                    let mut cumsg_output = &mut culist.msgs.0.#output_culist_index;
5316                    #output_clear_payload
5317                }
5318            } else {
5319                quote!()
5320            };
5321            let alloc_open = alloc_scope_open_tokens();
5322            let alloc_close = alloc_scope_close_tokens(
5323                quote! { monitor },
5324                quote! { #tid },
5325                quote! { CuComponentState::Process },
5326            );
5327            let (slot_cast_defs, source_slot_match_fn_ident) = output_slot_cast_tokens(
5328                &task_hint,
5329                "Task",
5330                "source",
5331                &quote! { cu29::cutask::CuSrcTask },
5332            );
5333            let source_process_tokens = quote! {
5334                #slot_cast_defs
5335
5336                #output_start_time
5337                #alloc_open
5338                let result = {
5339                    let cumsg_output = #source_slot_match_fn_ident::<
5340                        _,
5341                        _,
5342                    >(&#task_instance, cumsg_output);
5343                    #rt_guard
5344                    ctx.set_current_task(#tid);
5345                    #lag_wait
5346                    #task_instance.process(&ctx, cumsg_output)
5347                };
5348                #output_end_time
5349                #alloc_close
5350                result
5351            };
5352
5353            (
5354                wrap_process_step_tokens(
5355                    wrap_process_step,
5356                    quote! {
5357                        #task_setup
5358                        #parallel_task_preprocess
5359                        #comment_tokens
5360                        #freeze_task
5361                        #call_sim_callback
5362                        let cumsg_output = __cu_output!(#output_culist_index);
5363                        #maybe_sim_tick
5364                        let maybe_error = if doit {
5365                            execution_probe.record(cu29::monitoring::ExecutionMarker {
5366                                component_id: cu29::monitoring::ComponentId::new(#tid),
5367                                step: CuComponentState::Process,
5368                                culistid: Some(clid),
5369                            });
5370                            #source_process_tokens
5371                        } else {
5372                            Ok(())
5373                        };
5374                        if let Err(error) = maybe_error {
5375                            #monitoring_action
5376                        }
5377                        #parallel_task_postprocess
5378                    },
5379                ),
5380                logging_tokens,
5381            )
5382        }
5383        CuTaskType::Sink => {
5384            let GeneratedTaskInput {
5385                setup: task_input_setup,
5386                expr: task_input_expr,
5387            } = generate_task_input_binding(
5388                step,
5389                mission_name,
5390                output_pack_sizes,
5391                task_input_layouts,
5392            );
5393
5394            let monitoring_action = process_monitoring_action_tokens(
5395                tid,
5396                mission_mod,
5397                &output_culist_index,
5398                &output_clear_payload,
5399                wrap_process_step,
5400            );
5401
5402            let call_sim_callback = process_sim_callback_tokens(
5403                sim_mode,
5404                &enum_name,
5405                Some(&quote! { let cumsg_input = #task_input_expr; }),
5406                &output_culist_index,
5407                &monitoring_action,
5408            );
5409
5410            let alloc_open = alloc_scope_open_tokens();
5411            let alloc_close = alloc_scope_close_tokens(
5412                quote! { monitor },
5413                quote! { #tid },
5414                quote! { CuComponentState::Process },
5415            );
5416            (
5417                wrap_process_step_tokens(
5418                    wrap_process_step,
5419                    quote! {
5420                        #task_setup
5421                        #parallel_task_preprocess
5422                        #comment_tokens
5423                        #freeze_task
5424                        #task_input_setup
5425                        #call_sim_callback
5426                        let cumsg_input = #task_input_expr;
5427                        let cumsg_output = __cu_output!(#output_culist_index);
5428                        let maybe_error = if doit {
5429                            execution_probe.record(cu29::monitoring::ExecutionMarker {
5430                                component_id: cu29::monitoring::ComponentId::new(#tid),
5431                                step: CuComponentState::Process,
5432                                culistid: Some(clid),
5433                            });
5434                            #output_start_time
5435                            #alloc_open
5436                            let result = {
5437                                #rt_guard
5438                                ctx.set_current_task(#tid);
5439                                #task_instance.process(&ctx, cumsg_input)
5440                            };
5441                            #output_end_time
5442                            #alloc_close
5443                            result
5444                        } else {
5445                            Ok(())
5446                        };
5447                        if let Err(error) = maybe_error {
5448                            #monitoring_action
5449                        }
5450                        #parallel_task_postprocess
5451                    },
5452                ),
5453                quote! {},
5454            )
5455        }
5456        CuTaskType::Regular => {
5457            let GeneratedTaskInput {
5458                setup: task_input_setup,
5459                expr: task_input_expr,
5460            } = generate_task_input_binding(
5461                step,
5462                mission_name,
5463                output_pack_sizes,
5464                task_input_layouts,
5465            );
5466
5467            let monitoring_action = process_monitoring_action_tokens(
5468                tid,
5469                mission_mod,
5470                &output_culist_index,
5471                &output_clear_payload,
5472                wrap_process_step,
5473            );
5474
5475            let call_sim_callback = process_sim_callback_tokens(
5476                sim_mode,
5477                &enum_name,
5478                Some(&quote! { let cumsg_input = #task_input_expr; }),
5479                &output_culist_index,
5480                &monitoring_action,
5481            );
5482
5483            let logging_tokens = if !task_specs.logging_enabled[tid] {
5484                quote! {
5485                    let mut cumsg_output = &mut culist.msgs.0.#output_culist_index;
5486                    #output_clear_payload
5487                }
5488            } else {
5489                quote!()
5490            };
5491            let alloc_open = alloc_scope_open_tokens();
5492            let alloc_close = alloc_scope_close_tokens(
5493                quote! { monitor },
5494                quote! { #tid },
5495                quote! { CuComponentState::Process },
5496            );
5497            let regular_trait = task_trait_for_specs(task_specs, tid);
5498            let (slot_cast_defs, regular_slot_match_fn_ident) =
5499                output_slot_cast_tokens(&task_hint, "Task", "task", &regular_trait);
5500            let live_process_completed = (sim_mode && cfg!(feature = "logstream")).then(|| quote! {
5501                let _ = sim_callback(SimStep::#enum_name(CuTaskCallbackState::ProcessCompleted(cumsg_output)));
5502            });
5503            let regular_process_tokens = quote! {
5504                #slot_cast_defs
5505
5506                #output_start_time
5507                #alloc_open
5508                let result = {
5509                    let cumsg_output = #regular_slot_match_fn_ident::<
5510                        _,
5511                        _,
5512                    >(&#task_instance, cumsg_output);
5513                    #rt_guard
5514                    ctx.set_current_task(#tid);
5515                    #lag_wait
5516                    #task_instance.process(&ctx, cumsg_input, cumsg_output)
5517                };
5518                #output_end_time
5519                #live_process_completed
5520                #alloc_close
5521                result
5522            };
5523
5524            (
5525                wrap_process_step_tokens(
5526                    wrap_process_step,
5527                    quote! {
5528                        #task_setup
5529                        #parallel_task_preprocess
5530                        #comment_tokens
5531                        #freeze_task
5532                        #task_input_setup
5533                        #call_sim_callback
5534                        let cumsg_input = #task_input_expr;
5535                        let cumsg_output = __cu_output!(#output_culist_index);
5536                        let maybe_error = if doit {
5537                            execution_probe.record(cu29::monitoring::ExecutionMarker {
5538                                component_id: cu29::monitoring::ComponentId::new(#tid),
5539                                step: CuComponentState::Process,
5540                                culistid: Some(clid),
5541                            });
5542                            #regular_process_tokens
5543                        } else {
5544                            Ok(())
5545                        };
5546                        if let Err(error) = maybe_error {
5547                            #monitoring_action
5548                        }
5549                        #parallel_task_postprocess
5550                    },
5551                ),
5552                logging_tokens,
5553            )
5554        }
5555    }
5556}
5557
5558fn generate_bridge_rx_execution_tokens(
5559    step: &CuExecutionStep,
5560    bridge_spec: &BridgeSpec,
5561    channel_index: usize,
5562    ctx: StepGenerationContext<'_>,
5563    bridge_setup: proc_macro2::TokenStream,
5564) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
5565    let StepGenerationContext {
5566        output_pack_sizes: _,
5567        task_input_layouts: _,
5568        mission_name: _,
5569        sim_mode,
5570        keyframe_logging_enabled,
5571        mission_mod,
5572        lifecycle_placement,
5573        wrap_process_step,
5574    } = ctx;
5575    let rt_guard = rtsan_guard_tokens();
5576    let abort_process_step = abort_process_step_tokens(wrap_process_step);
5577    let channel = &bridge_spec.rx_channels[channel_index];
5578    let output_pack = step
5579        .output_msg_pack
5580        .as_ref()
5581        .expect("Bridge Rx channel missing output pack");
5582    let port_index = output_pack
5583        .msg_types
5584        .iter()
5585        .position(|msg| msg == &channel.msg_type_name)
5586        .unwrap_or_else(|| {
5587            panic!(
5588                "Bridge Rx channel '{}' missing output port for '{}'",
5589                channel.id, channel.msg_type_name
5590            )
5591        });
5592    let culist_index_ts = int2sliceindex(output_pack.culist_index);
5593    let output_ref = if output_pack.msg_types.len() == 1 {
5594        quote! { __cu_output!(#culist_index_ts) }
5595    } else {
5596        let port_index = syn::Index::from(port_index);
5597        quote! { __cu_output!(#culist_index_ts, #port_index) }
5598    };
5599    let monitor_index = syn::Index::from(
5600        channel
5601            .monitor_index
5602            .expect("Bridge Rx channel missing monitor index"),
5603    );
5604    let bridge_type = runtime_bridge_type_for_spec(bridge_spec, sim_mode);
5605    let (parallel_bridge_preprocess, parallel_bridge_postprocess) =
5606        parallel_bridge_lifecycle_tokens(
5607            &bridge_type,
5608            bridge_spec
5609                .monitor_index
5610                .expect("Bridge missing monitor index for lifecycle"),
5611            mission_mod,
5612            lifecycle_placement,
5613            keyframe_logging_enabled,
5614        );
5615    let const_ident = &channel.const_ident;
5616    let enum_ident = Ident::new(
5617        &config_id_to_enum(&format!("{}_rx_{}", bridge_spec.id, channel.id)),
5618        Span::call_site(),
5619    );
5620
5621    let call_sim_callback = if sim_mode {
5622        quote! {
5623            let doit = {
5624                let state = SimStep::#enum_ident {
5625                    channel: &<#bridge_type as cu29::cubridge::CuBridge>::Rx::#const_ident,
5626                    msg: cumsg_output,
5627                };
5628                let ovr = sim_callback(state);
5629                if let SimOverride::Errored(reason) = ovr {
5630                    let error: CuError = reason.into();
5631                    let decision = monitor.process_error(cu29::monitoring::ComponentId::new(#monitor_index), CuComponentState::Process, &error);
5632                    match decision {
5633                        Decision::Abort => {
5634                            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);
5635                            #abort_process_step
5636                        }
5637                        Decision::Ignore => {
5638                            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)));
5639                            cumsg_output.clear_payload();
5640                            false
5641                        }
5642                        Decision::Shutdown => {
5643                            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)));
5644                            return Err(CuError::new_with_cause("Component errored out during process.", error));
5645                        }
5646                    }
5647                } else {
5648                    ovr == SimOverride::ExecuteByRuntime
5649                }
5650            };
5651        }
5652    } else {
5653        quote! { let doit = true; }
5654    };
5655    let alloc_open = alloc_scope_open_tokens();
5656    let alloc_close = alloc_scope_close_tokens(
5657        quote! { monitor },
5658        quote! { #monitor_index },
5659        quote! { CuComponentState::Process },
5660    );
5661    (
5662        wrap_process_step_tokens(
5663            wrap_process_step,
5664            quote! {
5665                #bridge_setup
5666                #parallel_bridge_preprocess
5667                let cumsg_output = #output_ref;
5668                #call_sim_callback
5669                if doit {
5670                    execution_probe.record(cu29::monitoring::ExecutionMarker {
5671                        component_id: cu29::monitoring::ComponentId::new(#monitor_index),
5672                        step: CuComponentState::Process,
5673                        culistid: Some(clid),
5674                    });
5675                    cumsg_output.metadata.process_time.start = cu29::curuntime::perf_now(clock).into();
5676                    #alloc_open
5677                    let maybe_error = {
5678                        #rt_guard
5679                        ctx.set_current_component(#monitor_index);
5680                        ctx.clear_current_task();
5681                        bridge.receive(
5682                            &ctx,
5683                            &<#bridge_type as cu29::cubridge::CuBridge>::Rx::#const_ident,
5684                            cumsg_output,
5685                        )
5686                    };
5687                    cumsg_output.metadata.process_time.end = cu29::curuntime::perf_now(clock).into();
5688                    #alloc_close
5689                    if let Err(error) = maybe_error {
5690                        let decision = monitor.process_error(cu29::monitoring::ComponentId::new(#monitor_index), CuComponentState::Process, &error);
5691                        match decision {
5692                            Decision::Abort => {
5693                                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);
5694                                #abort_process_step
5695                            }
5696                            Decision::Ignore => {
5697                                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)));
5698                                cumsg_output.clear_payload();
5699                            }
5700                            Decision::Shutdown => {
5701                                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)));
5702                                return Err(CuError::new_with_cause("Component errored out during process.", error));
5703                            }
5704                        }
5705                    }
5706                }
5707                #parallel_bridge_postprocess
5708            },
5709        ),
5710        quote! {},
5711    )
5712}
5713
5714fn generate_bridge_tx_execution_tokens(
5715    step: &CuExecutionStep,
5716    bridge_spec: &BridgeSpec,
5717    channel_index: usize,
5718    ctx: StepGenerationContext<'_>,
5719    bridge_setup: proc_macro2::TokenStream,
5720) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
5721    let StepGenerationContext {
5722        output_pack_sizes,
5723        task_input_layouts: _,
5724        mission_name: _,
5725        sim_mode,
5726        keyframe_logging_enabled,
5727        mission_mod,
5728        lifecycle_placement,
5729        wrap_process_step,
5730    } = ctx;
5731    let rt_guard = rtsan_guard_tokens();
5732    let abort_process_step = abort_process_step_tokens(wrap_process_step);
5733    let channel = &bridge_spec.tx_channels[channel_index];
5734    let monitor_index = syn::Index::from(
5735        channel
5736            .monitor_index
5737            .expect("Bridge Tx channel missing monitor index"),
5738    );
5739    let input = step
5740        .input_msg_indices_types
5741        .first()
5742        .expect("Bridge Tx channel should have exactly one input");
5743    let input_index = int2sliceindex(input.culist_index);
5744    let output_size = output_pack_sizes
5745        .get(input.culist_index as usize)
5746        .copied()
5747        .unwrap_or_else(|| {
5748            panic!(
5749                "Missing output pack size for culist index {}",
5750                input.culist_index
5751            )
5752        });
5753    let input_ref = if output_size > 1 {
5754        let port_index = syn::Index::from(input.src_port);
5755        quote! { __cu_input!(#input_index, #port_index) }
5756    } else {
5757        quote! { __cu_input!(#input_index) }
5758    };
5759    let output_pack = step
5760        .output_msg_pack
5761        .as_ref()
5762        .expect("Bridge Tx channel missing output pack");
5763    if output_pack.msg_types.len() != 1 {
5764        panic!(
5765            "Bridge Tx channel '{}' expected a single output message slot, got {}",
5766            channel.id,
5767            output_pack.msg_types.len()
5768        );
5769    }
5770    let output_index = int2sliceindex(output_pack.culist_index);
5771    let output_ref = quote! { __cu_output!(#output_index) };
5772    let bridge_type = runtime_bridge_type_for_spec(bridge_spec, sim_mode);
5773    let (parallel_bridge_preprocess, parallel_bridge_postprocess) =
5774        parallel_bridge_lifecycle_tokens(
5775            &bridge_type,
5776            bridge_spec
5777                .monitor_index
5778                .expect("Bridge missing monitor index for lifecycle"),
5779            mission_mod,
5780            lifecycle_placement,
5781            keyframe_logging_enabled,
5782        );
5783    let const_ident = &channel.const_ident;
5784    let enum_ident = Ident::new(
5785        &config_id_to_enum(&format!("{}_tx_{}", bridge_spec.id, channel.id)),
5786        Span::call_site(),
5787    );
5788
5789    let call_sim_callback = if sim_mode {
5790        quote! {
5791            let doit = {
5792                let state = SimStep::#enum_ident {
5793                    channel: &<#bridge_type as cu29::cubridge::CuBridge>::Tx::#const_ident,
5794                    msg: &*cumsg_input,
5795                    output: cumsg_output,
5796                };
5797                let ovr = sim_callback(state);
5798                if let SimOverride::Errored(reason) = ovr  {
5799                    let error: CuError = reason.into();
5800                    let decision = monitor.process_error(cu29::monitoring::ComponentId::new(#monitor_index), CuComponentState::Process, &error);
5801                    match decision {
5802                        Decision::Abort => {
5803                            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);
5804                            #abort_process_step
5805                        }
5806                        Decision::Ignore => {
5807                            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)));
5808                            false
5809                        }
5810                        Decision::Shutdown => {
5811                            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)));
5812                            return Err(CuError::new_with_cause("Component errored out during process.", error));
5813                        }
5814                    }
5815                } else {
5816                    ovr == SimOverride::ExecuteByRuntime
5817                }
5818            };
5819        }
5820    } else {
5821        quote! { let doit = true; }
5822    };
5823    let alloc_open = alloc_scope_open_tokens();
5824    let alloc_close = alloc_scope_close_tokens(
5825        quote! { monitor },
5826        quote! { #monitor_index },
5827        quote! { CuComponentState::Process },
5828    );
5829    (
5830        wrap_process_step_tokens(
5831            wrap_process_step,
5832            quote! {
5833                #bridge_setup
5834                #parallel_bridge_preprocess
5835                let cumsg_input = #input_ref;
5836                let cumsg_output = #output_ref;
5837                let bridge_channel = &<#bridge_type as cu29::cubridge::CuBridge>::Tx::#const_ident;
5838                #call_sim_callback
5839                if doit {
5840                    execution_probe.record(cu29::monitoring::ExecutionMarker {
5841                        component_id: cu29::monitoring::ComponentId::new(#monitor_index),
5842                        step: CuComponentState::Process,
5843                        culistid: Some(clid),
5844                    });
5845                    cumsg_output.metadata.process_time.start = cu29::curuntime::perf_now(clock).into();
5846                    #alloc_open
5847                    let maybe_error = if bridge_channel.should_send(cumsg_input.payload().is_some()) {
5848                        {
5849                            #rt_guard
5850                            ctx.set_current_component(#monitor_index);
5851                            ctx.clear_current_task();
5852                            bridge.send(
5853                                &ctx,
5854                                bridge_channel,
5855                                &*cumsg_input,
5856                            )
5857                        }
5858                    } else {
5859                        Ok(())
5860                    };
5861                    #alloc_close
5862                    if let Err(error) = maybe_error {
5863                        let decision = monitor.process_error(cu29::monitoring::ComponentId::new(#monitor_index), CuComponentState::Process, &error);
5864                        match decision {
5865                            Decision::Abort => {
5866                                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);
5867                                #abort_process_step
5868                            }
5869                            Decision::Ignore => {
5870                                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)));
5871                            }
5872                            Decision::Shutdown => {
5873                                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)));
5874                                return Err(CuError::new_with_cause("Component errored out during process.", error));
5875                            }
5876                        }
5877                    }
5878                    cumsg_output.metadata.process_time.end = cu29::curuntime::perf_now(clock).into();
5879                }
5880                #parallel_bridge_postprocess
5881            },
5882        ),
5883        quote! {},
5884    )
5885}
5886
5887#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
5888enum BridgeChannelDirection {
5889    Rx,
5890    Tx,
5891}
5892
5893#[derive(Clone, Debug, PartialEq, Eq, Hash)]
5894struct BridgeChannelKey {
5895    bridge_id: String,
5896    channel_id: String,
5897    direction: BridgeChannelDirection,
5898}
5899
5900#[derive(Clone)]
5901struct BridgeChannelSpec {
5902    id: String,
5903    const_ident: Ident,
5904    #[allow(dead_code)]
5905    msg_type: Type,
5906    msg_type_name: String,
5907    config_index: usize,
5908    plan_node_id: Option<NodeId>,
5909    culist_index: Option<usize>,
5910    monitor_index: Option<usize>,
5911}
5912
5913#[derive(Clone)]
5914struct BridgeSpec {
5915    id: String,
5916    type_path: Type,
5917    run_in_sim: bool,
5918    config_index: usize,
5919    tuple_index: usize,
5920    monitor_index: Option<usize>,
5921    rx_channels: Vec<BridgeChannelSpec>,
5922    tx_channels: Vec<BridgeChannelSpec>,
5923}
5924
5925#[derive(Clone, Copy, Debug, Default)]
5926struct ParallelLifecyclePlacement {
5927    preprocess: bool,
5928    postprocess: bool,
5929}
5930
5931#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
5932enum ParallelLifecycleKey {
5933    Task(usize),
5934    Bridge(usize),
5935}
5936
5937fn build_parallel_lifecycle_placements(
5938    culist_plan: &CuExecutionLoop,
5939    culist_exec_entities: &[ExecutionEntity],
5940) -> Vec<ParallelLifecyclePlacement> {
5941    let step_keys: Vec<Option<ParallelLifecycleKey>> = culist_plan
5942        .steps
5943        .iter()
5944        .map(|unit| match unit {
5945            CuExecutionUnit::Step(step) => {
5946                // Anytime refine steps emit no parallel stage (they run inside
5947                // the base step's stage), so they must not claim the task's
5948                // postprocess placement.
5949                if step.phase == CuStepPhase::AnytimeRefine {
5950                    return None;
5951                }
5952                match &culist_exec_entities[step.node_id as usize].kind {
5953                    ExecutionEntityKind::Task { task_index } => {
5954                        Some(ParallelLifecycleKey::Task(*task_index))
5955                    }
5956                    ExecutionEntityKind::BridgeRx { bridge_index, .. }
5957                    | ExecutionEntityKind::BridgeTx { bridge_index, .. } => {
5958                        Some(ParallelLifecycleKey::Bridge(*bridge_index))
5959                    }
5960                }
5961            }
5962            CuExecutionUnit::Loop(_) => None,
5963        })
5964        .collect();
5965
5966    let mut placements = vec![ParallelLifecyclePlacement::default(); step_keys.len()];
5967    let mut seen_forward = std::collections::HashSet::new();
5968    for (index, key) in step_keys.iter().enumerate() {
5969        let Some(key) = key else {
5970            continue;
5971        };
5972        if seen_forward.insert(*key) {
5973            placements[index].preprocess = true;
5974        }
5975    }
5976
5977    let mut seen_reverse = std::collections::HashSet::new();
5978    for (index, key) in step_keys.iter().enumerate().rev() {
5979        let Some(key) = key else {
5980            continue;
5981        };
5982        if seen_reverse.insert(*key) {
5983            placements[index].postprocess = true;
5984        }
5985    }
5986
5987    placements
5988}
5989
5990struct LaneExecutorTokens {
5991    worker_spawns: Vec<proc_macro2::TokenStream>,
5992    dispatcher_scheduling: proc_macro2::TokenStream,
5993    completion_ready: proc_macro2::TokenStream,
5994    copperlists_per_cycle: proc_macro2::Literal,
5995    max_in_flight: proc_macro2::Literal,
5996    workers: proc_macro2::Literal,
5997}
5998
5999fn scheduling_policy_tokens(policy: SchedulingPolicy) -> proc_macro2::TokenStream {
6000    match policy {
6001        SchedulingPolicy::Fair => quote! { cu29::config::SchedulingPolicy::Fair },
6002        SchedulingPolicy::Nice(nice) => quote! { cu29::config::SchedulingPolicy::Nice(#nice) },
6003        SchedulingPolicy::Fifo { priority } => {
6004            quote! { cu29::config::SchedulingPolicy::Fifo { priority: #priority } }
6005        }
6006        SchedulingPolicy::RoundRobin { priority } => {
6007            quote! { cu29::config::SchedulingPolicy::RoundRobin { priority: #priority } }
6008        }
6009    }
6010}
6011
6012fn plan_thread_spec_tokens(
6013    id: &str,
6014    cpu: Option<usize>,
6015    policy: SchedulingPolicy,
6016) -> proc_macro2::TokenStream {
6017    let affinity = match cpu {
6018        Some(cpu) => quote! { Some(vec![#cpu]) },
6019        None => quote! { None },
6020    };
6021    let policy = scheduling_policy_tokens(policy);
6022    quote! {
6023        cu29::config::ThreadPoolConfig {
6024            id: #id.to_string(),
6025            threads: 1,
6026            affinity: #affinity,
6027            policy: #policy,
6028            on_error: cu29::config::OnError::Strict,
6029        }
6030    }
6031}
6032
6033fn build_lane_executor_tokens(
6034    lane_plan: &LanePlan,
6035    culist_plan: &CuExecutionLoop,
6036    stage_idents: &[Ident],
6037    mission_mod: &Ident,
6038    keyframe_logging_enabled: bool,
6039) -> CuResult<LaneExecutorTokens> {
6040    let compiled = cu29_runtime::planner::DistributedSchedule::compile(lane_plan, culist_plan)?;
6041    if compiled.stages != stage_idents.len() {
6042        return Err(CuError::from(
6043            "Worker executor stages do not match the generated execution plan",
6044        ));
6045    }
6046
6047    let mut worker_spawns = Vec::with_capacity(lane_plan.workers.len());
6048    let copperlists_per_cycle = u64::from(lane_plan.copperlists_per_cycle);
6049    for (worker_index, worker) in lane_plan.workers.iter().enumerate() {
6050        let CuPlanPlacement::Thread { cpu, policy } = &worker.placement else {
6051            return Err(CuError::from(format!(
6052                "Worker executor requires thread placement for '{}'",
6053                worker.id
6054            )));
6055        };
6056        let worker_id = worker.id.as_str();
6057        let spec = plan_thread_spec_tokens(worker_id, *cpu, *policy);
6058        let worker_length = compiled.worker_lengths[worker_index];
6059        let mut blocks = Vec::new();
6060
6061        for &index in &worker.occurrences {
6062            if compiled.base_of[index].is_some() {
6063                continue;
6064            }
6065            let occurrence = lane_plan.occurrences[index];
6066            let stage = compiled.stage_of_step[occurrence.step]
6067                .expect("executable occurrence must have a generated stage");
6068            let stage_ident = &stage_idents[stage];
6069            let copperlist_offset = u64::from(occurrence.copperlist);
6070            let waits: Vec<_> = compiled
6071                .dependencies
6072                .iter()
6073                .filter(|(_, to, _)| *to == index)
6074                .map(|(from, _, lag)| {
6075                    let from_worker = compiled.owner[*from];
6076                    let from_length = compiled.worker_lengths[from_worker];
6077                    let from_progress = compiled.progress[*from];
6078                    let lag = u64::from(*lag);
6079                    quote! {
6080                        if cycle >= #lag {
6081                            let required_progress =
6082                                (cycle - #lag) * #from_length + #from_progress;
6083                            if !lanes.wait_progress(#from_worker, required_progress) {
6084                                break '__cu_worker;
6085                            }
6086                        }
6087                    }
6088                })
6089                .collect();
6090            let dependent_workers: BTreeSet<usize> = compiled
6091                .dependencies
6092                .iter()
6093                .filter(|(from, _, _)| *from == index)
6094                .map(|(_, to, _)| compiled.owner[*to])
6095                .collect();
6096            let progress = compiled.progress[index];
6097            let terminal = compiled.finish_counts[index] != 0;
6098            let publish = (!dependent_workers.is_empty() || terminal).then(|| {
6099                let dependent_workers = dependent_workers.iter();
6100                quote! {
6101                    lanes.publish_progress(
6102                        #worker_index,
6103                        cycle * #worker_length + #progress,
6104                        &[#(#dependent_workers),*],
6105                        #terminal,
6106                    );
6107                }
6108            });
6109            let keyframe_fields = keyframe_logging_enabled.then(|| {
6110                quote! {
6111                    let (keyframe_ptr, keyframe_len) = lanes.keyframe_capture(clid);
6112                }
6113            });
6114            let keyframe_runtime_fields = keyframe_logging_enabled.then(|| {
6115                quote! { keyframe_ptr, keyframe_len, }
6116            });
6117
6118            blocks.push(quote! {
6119                {
6120                    let clid = first_clid + cycle * #copperlists_per_cycle + #copperlist_offset;
6121                    let Some(culist_ptr) = lanes.wait_admitted(clid) else {
6122                        break '__cu_worker;
6123                    };
6124                    #(#waits)*
6125                    if lanes.is_shut_down() {
6126                        break '__cu_worker;
6127                    }
6128                    let outcome = if lanes.is_aborted(clid) {
6129                        Ok(cu29::curuntime::ProcessStepOutcome::Continue)
6130                    } else {
6131                        match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
6132                            let execution_probe = unsafe { execution_probe_ptr.as_ref() };
6133                            let monitor = unsafe { monitor_ptr.as_ref() };
6134                            #keyframe_fields
6135                            let mut step_rt = #mission_mod::ParallelProcessStepRuntime {
6136                                clock: &clock,
6137                                execution_probe,
6138                                monitor,
6139                                task_ptrs: &task_ptrs,
6140                                bridge_ptrs: &bridge_ptrs,
6141                                #keyframe_runtime_fields
6142                                culist: culist_ptr.cast::<CuList>(),
6143                                clid,
6144                                ctx: cu29::context::CuContext::from_runtime_metadata(
6145                                    clock.clone(), clid, instance_id, subsystem_code,
6146                                    #mission_mod::TASK_IDS,
6147                                ),
6148                            };
6149                            #stage_ident(&mut step_rt)
6150                        })) {
6151                            Ok(outcome) => outcome,
6152                            Err(payload) => Err(CuError::from(format!(
6153                                "Panic while processing CopperList #{} on worker '{}': {}",
6154                                clid,
6155                                #worker_id,
6156                                cu29::monitoring::panic_payload_to_string(payload.as_ref()),
6157                            ))),
6158                        }
6159                    };
6160                    match outcome {
6161                        Ok(cu29::curuntime::ProcessStepOutcome::Continue) => {}
6162                        Ok(cu29::curuntime::ProcessStepOutcome::AbortCopperList) => {
6163                            lanes.abort(clid);
6164                        }
6165                        Err(error) => {
6166                            lanes.publish_error(clid, error);
6167                            break '__cu_worker;
6168                        }
6169                    }
6170                    #publish
6171                }
6172            });
6173        }
6174
6175        worker_spawns.push(quote! {
6176            {
6177                let lanes = std::sync::Arc::clone(&lanes);
6178                let clock = clock.clone();
6179                let execution_probe_ptr = execution_probe_ptr;
6180                let monitor_ptr = monitor_ptr;
6181                let task_ptrs = task_ptrs;
6182                let bridge_ptrs = bridge_ptrs;
6183                lane_handles.push(scope.spawn(move || {
6184                    lanes.register_worker(#worker_index);
6185                    let first_clid = lanes.first_clid();
6186                    let spec = #spec;
6187                    if let Err(error) = cu29::thread_pool::apply_current_thread_scheduling(&spec, 0) {
6188                        lanes.publish_error(
6189                            first_clid,
6190                            CuError::new_with_cause(
6191                                &format!("Worker '{}' could not apply its placement", #worker_id),
6192                                error,
6193                            ),
6194                        );
6195                        return;
6196                    }
6197                    let mut cycle = 0u64;
6198                    '__cu_worker: loop {
6199                        #(#blocks)*
6200                        cycle += 1;
6201                    }
6202                }));
6203            }
6204        });
6205    }
6206
6207    let completion_arms = compiled
6208        .completion
6209        .iter()
6210        .enumerate()
6211        .map(|(offset, entries)| {
6212            let checks = entries.iter().map(|(worker, progress)| {
6213                let worker_length = compiled.worker_lengths[*worker];
6214                quote! {
6215                    && lanes.worker_reached(#worker, cycle * #worker_length + #progress)
6216                }
6217            });
6218            quote! { #offset => true #(#checks)*, }
6219        });
6220    let completion_ready = quote! {
6221        {
6222            let cycle = lanes.cycle_of(next_commit_clid);
6223            match ((next_commit_clid - lanes.first_clid()) % #copperlists_per_cycle) as usize {
6224                #(#completion_arms)*
6225                _ => unreachable!("CopperList offset outside exact schedule cycle"),
6226            }
6227        }
6228    };
6229    let dispatcher_scheduling = match &lane_plan.dispatcher {
6230        Some(CuPlanThread { cpu, policy }) => {
6231            let spec = plan_thread_spec_tokens("dispatcher", *cpu, *policy);
6232            quote! {
6233                {
6234                    let spec = #spec;
6235                    cu29::thread_pool::apply_current_thread_scheduling(&spec, 0)?;
6236                }
6237            }
6238        }
6239        None => quote! {},
6240    };
6241
6242    Ok(LaneExecutorTokens {
6243        worker_spawns,
6244        dispatcher_scheduling,
6245        completion_ready,
6246        copperlists_per_cycle: proc_macro2::Literal::u64_unsuffixed(copperlists_per_cycle),
6247        max_in_flight: proc_macro2::Literal::usize_unsuffixed(lane_plan.max_in_flight as usize),
6248        workers: proc_macro2::Literal::usize_unsuffixed(lane_plan.workers.len()),
6249    })
6250}
6251
6252fn sim_bridge_channel_set_idents(bridge_tuple_index: usize) -> (Ident, Ident, Ident, Ident) {
6253    (
6254        format_ident!("__CuSimBridge{}TxChannels", bridge_tuple_index),
6255        format_ident!("__CuSimBridge{}TxId", bridge_tuple_index),
6256        format_ident!("__CuSimBridge{}RxChannels", bridge_tuple_index),
6257        format_ident!("__CuSimBridge{}RxId", bridge_tuple_index),
6258    )
6259}
6260
6261fn runtime_bridge_type_for_spec(bridge_spec: &BridgeSpec, sim_mode: bool) -> Type {
6262    if sim_mode && !bridge_spec.run_in_sim {
6263        let (tx_set_ident, _tx_id_ident, rx_set_ident, _rx_id_ident) =
6264            sim_bridge_channel_set_idents(bridge_spec.tuple_index);
6265        let tx_type: Type = if bridge_spec.tx_channels.is_empty() {
6266            parse_quote!(cu29::simulation::CuNoBridgeChannels)
6267        } else {
6268            parse_quote!(#tx_set_ident)
6269        };
6270        let rx_type: Type = if bridge_spec.rx_channels.is_empty() {
6271            parse_quote!(cu29::simulation::CuNoBridgeChannels)
6272        } else {
6273            parse_quote!(#rx_set_ident)
6274        };
6275        parse_quote!(cu29::simulation::CuSimBridge<#tx_type, #rx_type>)
6276    } else {
6277        bridge_spec.type_path.clone()
6278    }
6279}
6280
6281fn runtime_task_type_for_index(
6282    task_specs: &CuTaskSpecSet,
6283    graph: &CuGraph,
6284    index: usize,
6285    sim_mode: bool,
6286) -> Type {
6287    let task_id = &task_specs.ids[index];
6288    let declared_task_type = &task_specs.sim_task_types[index];
6289    let background = task_specs.background_flags[index];
6290    let run_in_sim = task_specs.run_in_sim_flags[index];
6291    let output_type = &task_specs.output_types[index];
6292
6293    match task_specs.cutypes[index] {
6294        CuTaskType::Source => {
6295            if sim_mode && !run_in_sim {
6296                let msg_types = graph
6297                    .get_node_output_msg_types(task_id.as_str())
6298                    .unwrap_or_else(|| {
6299                        panic!(
6300                            "CuSrcTask {task_id} should have an outgoing connection with a valid output msg type"
6301                        )
6302                    });
6303                let sim_task_name = if msg_types.len() == 1 {
6304                    format!("CuSimSrcTask<{}>", msg_types[0])
6305                } else {
6306                    let messages = msg_types
6307                        .iter()
6308                        .map(|msg_type| format!("cu29::prelude::CuMsg<{msg_type}>"))
6309                        .collect::<Vec<_>>()
6310                        .join(", ");
6311                    format!("CuSimSrcTaskPack<({messages})>")
6312                };
6313                parse_str(sim_task_name.as_str()).unwrap_or_else(|_| {
6314                    panic!("Could not build the placeholder for simulation: {sim_task_name}")
6315                })
6316            } else if background {
6317                if let Some(out_ty) = output_type {
6318                    parse_quote!(CuAsyncSrcTask<#declared_task_type, #out_ty>)
6319                } else {
6320                    panic!("{task_id}: If a source is background, it has to have an output");
6321                }
6322            } else {
6323                declared_task_type.clone()
6324            }
6325        }
6326        CuTaskType::Regular => {
6327            if background {
6328                if output_type.is_some() {
6329                    task_specs.task_types[index].clone()
6330                } else {
6331                    panic!("{task_id}: If a task is background, it has to have an output");
6332                }
6333            } else {
6334                // run_in_sim has no effect for regular tasks; they always run as themselves in sim.
6335                declared_task_type.clone()
6336            }
6337        }
6338        CuTaskType::Sink => {
6339            if background {
6340                panic!(
6341                    "CuSinkTask {task_id} cannot be a background task, it should be a regular task."
6342                );
6343            }
6344
6345            if sim_mode && !run_in_sim {
6346                let msg_types = graph.get_node_input_msg_types(task_id.as_str()).unwrap_or_else(|| {
6347                    panic!(
6348                        "CuSinkTask {task_id} should have an incoming connection with a valid input msg type"
6349                    )
6350                });
6351                let msg_type = if msg_types.len() == 1 {
6352                    format!("({},)", msg_types[0])
6353                } else {
6354                    format!("({})", msg_types.join(", "))
6355                };
6356                let sim_task_name = format!("CuSimSinkTask<{msg_type}>");
6357                parse_str(sim_task_name.as_str()).unwrap_or_else(|_| {
6358                    panic!("Could not build the placeholder for simulation: {sim_task_name}")
6359                })
6360            } else {
6361                declared_task_type.clone()
6362            }
6363        }
6364    }
6365}
6366
6367#[derive(Clone)]
6368struct ExecutionEntity {
6369    kind: ExecutionEntityKind,
6370}
6371
6372#[derive(Clone)]
6373enum ExecutionEntityKind {
6374    Task {
6375        task_index: usize,
6376    },
6377    BridgeRx {
6378        bridge_index: usize,
6379        channel_index: usize,
6380    },
6381    BridgeTx {
6382        bridge_index: usize,
6383        channel_index: usize,
6384    },
6385}
6386
6387#[cfg(test)]
6388mod tests;