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}
67fn 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
80fn 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 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#[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
399struct 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 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 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 #msgs_types_tuple_encode
931 #msgs_types_tuple_decode
932
933 #msgs_types_tuple_debug
935
936 #msgs_types_tuple_serialize
938
939 #msgs_types_tuple_default
941
942 #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 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 !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 let mut variants = plan_enum;
1077
1078 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 #[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 !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 #[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 #[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 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 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
1551fn 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 pub background_result_lags: Vec<Option<u32>>,
1615 pub background_pools: Vec<String>,
1618 pub anytime_configs: Vec<Option<AnytimeConfig>>,
1623 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 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
1907fn 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 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 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
2685fn 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
2694fn 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
2901fn 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
2967fn 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 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 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
3012fn 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
3110fn 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 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
3139fn 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
3549fn 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 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 && 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 && 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 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
4498fn anytime_policy_ident(task_id: &str) -> Ident {
4500 format_ident!("__CuAnytimePolicy_{}", config_id_to_struct_member(task_id))
4501}
4502
4503fn 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
4513fn 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
4536fn 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 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 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
4619fn 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 .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
4650fn 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
4696fn anytime_slot_cast_tokens(task_hint: &str) -> (proc_macro2::TokenStream, Ident) {
4698 output_slot_cast_tokens(
4699 task_hint,
4700 "AnytimeTask",
4701 "anytime",
4702 "e! { cu29::cutask_anytime::CuAnytimeTask },
4703 )
4704}
4705
4706fn 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
4740fn 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
4776fn 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 "e! { 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 let call_sim_callback = process_sim_callback_tokens(
4852 ctx.sim_mode,
4853 &enum_name,
4854 Some("e! { let cumsg_input = #task_input_expr; }),
4855 &output_culist_index,
4856 &monitoring_action,
4857 );
4858
4859 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 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 #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 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
4972fn 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 "e! { cumsg_output.clear_payload(); },
5017 ctx.wrap_process_step,
5018 );
5019
5020 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 let improved_tail = if k < total {
5035 quote! {
5036 #job_ident = Some(__cu_any_job); 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 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 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 #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 "e! { 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("e! { 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("e! { 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", ®ular_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 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 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;