Skip to main content

celox_frontend_core/symbolic/
assembly.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::symbolic::artifact::{
4    RelocationModule, SimModule, SymbolicGlueAddr as GlueAddr, SymbolicRtl,
5};
6use crate::{
7    FrontendLookup, FrontendTrace, FrontendTraceOptions, FusedSirOptimizationHints, HashMap,
8    HashSet, InstancePath, ParserError, ScheduledRtl, ScheduledRtlOutput, SourceAddr,
9    SourceLocation, SourceVarId, VariableInfo, flattening,
10};
11use celox_design::{
12    BitAccess, DomainKind, ElaboratedDesign, EventTopology, InitialStateValue, InstanceId,
13    ModuleId, RegionedAbsoluteAddrBase, RegionedStateAddr, RuntimeCombObserver, RuntimeErrorInfo,
14    RuntimeEventKind, RuntimeEventSite, RuntimeSchema, STABLE_REGION, StateAddr, StateObjectId,
15    TriggerSet, VarAtomBase, VariableMetadata,
16};
17use celox_sir::{BasicBlock, ExecutionUnit, SIRInstruction, SIRTerminator, SirProgram};
18use celox_slt::{
19    CombObserver, FfAccessSummary, LogicPath, LogicPathId, LogicPathTarget, NodeId, SLTNodeArena,
20    scheduler::{self, SchedulerError},
21};
22
23type AbsoluteAddr = SourceAddr;
24type RegionedAbsoluteAddr = RegionedAbsoluteAddrBase<SourceVarId>;
25type RegionedVarAddr = celox_design::RegionedVarAddrBase<SourceVarId>;
26
27/// Runtime IDs assigned while a module instance is relocated into the global
28/// design. Source adapters use these IDs only when rebuilding an optimized FF
29/// action inside the shared comb/FF scheduler.
30#[derive(Clone, Debug)]
31pub struct FfRuntimeRelocation {
32    pub error_codes: HashMap<i64, i64>,
33    pub event_site_base: u32,
34}
35
36/// Source-neutral description of one FF action offered to an adapter-specific
37/// lowering implementation.
38#[derive(Clone)]
39pub struct FusedFfAction {
40    pub id: usize,
41    pub instance_id: InstanceId,
42    pub module_id: ModuleId,
43    pub trigger: TriggerSet<SourceVarId>,
44    pub summary: FfAccessSummary<RegionedAbsoluteAddrBase<SourceVarId>>,
45    pub runtime: FfRuntimeRelocation,
46}
47
48/// Adapter hook for source-aware FF lowering used by the optional fused
49/// comb/FF optimization. The scheduler and all identities crossing this trait
50/// remain source neutral.
51pub trait FusedFfLoweringFactory {
52    fn create(
53        &self,
54        actions: Vec<FusedFfAction>,
55    ) -> Result<
56        Box<
57            dyn scheduler::ClockFfLowering<
58                    RegionedAbsoluteAddrBase<SourceVarId>,
59                    Error = ParserError,
60                > + '_,
61        >,
62        ParserError,
63    >;
64}
65
66fn elaborated_scope_name(
67    root_name: &str,
68    path: &InstancePath,
69    expanded: &HashMap<InstancePath, InstanceId>,
70    indexed_instances: &HashSet<InstanceId>,
71) -> String {
72    let mut prefix = Vec::with_capacity(path.0.len());
73    let segments = path
74        .0
75        .iter()
76        .map(|(name, index)| {
77            prefix.push((name.clone(), *index));
78            let indexed = expanded
79                .get(&InstancePath(prefix.clone()))
80                .is_some_and(|id| indexed_instances.contains(id));
81            if indexed {
82                format!("{name}[{index}]")
83            } else {
84                name.clone()
85            }
86        })
87        .collect::<Vec<_>>();
88    if segments.is_empty() {
89        root_name.to_string()
90    } else {
91        format!("{root_name}.{}", segments.join("."))
92    }
93}
94
95fn flatten_with_trace(
96    module: &SimModule,
97    path: &InstancePath,
98    instance_ids: &HashMap<InstancePath, InstanceId>,
99    global_boundaries: &HashMap<AbsoluteAddr, BTreeSet<usize>>,
100    unpacked_element_widths: &HashMap<AbsoluteAddr, usize>,
101    arena: &mut SLTNodeArena<AbsoluteAddr>,
102    trace_opts: &FrontendTraceOptions,
103    mut trace: Option<&mut FrontendTrace>,
104) -> Result<RelocationModule, celox_slt::SLTNodeFactsError> {
105    let flattened = flattening::flatten_module(
106        module,
107        path,
108        instance_ids,
109        global_boundaries,
110        unpacked_element_widths,
111        arena,
112    )?;
113
114    if let Some(trace) = trace.as_deref_mut()
115        && trace_opts.pre_atomized_comb_blocks
116    {
117        match &mut trace.pre_atomized_comb_blocks {
118            Some((blocks, trace_arena)) => {
119                blocks.extend(flattened.pre_atomized_comb_blocks);
120                *trace_arena = arena.clone();
121            }
122            slot @ None => *slot = Some((flattened.pre_atomized_comb_blocks, arena.clone())),
123        }
124    }
125
126    if let Some(trace) = trace
127        && trace_opts.atomized_comb_blocks
128    {
129        match &mut trace.atomized_comb_blocks {
130            Some((blocks, trace_arena)) => {
131                blocks.extend(flattened.relocation.comb_blocks.iter().cloned());
132                *trace_arena = arena.clone();
133            }
134            slot @ None => {
135                *slot = Some((flattened.relocation.comb_blocks.clone(), arena.clone()));
136            }
137        }
138    }
139
140    Ok(flattened.relocation)
141}
142
143fn remap_for_fold_runtime_event_sites<A: std::hash::Hash + Eq + Clone>(
144    arena: &mut SLTNodeArena<A>,
145    start: usize,
146    runtime_event_site_map: &HashMap<u32, u32>,
147) -> Result<(), ParserError> {
148    arena
149        .remap_for_fold_effect_sites(start..arena.len(), |site_id, fatal_error_code| {
150            Ok(runtime_event_site_map.get(&site_id).map(|&global_site| {
151                (
152                    global_site,
153                    fatal_error_code.map(|_| i64::from(global_site)),
154                )
155            }))
156        })
157        .map_err(|error| {
158            ParserError::illegal_context(
159                "ForFold runtime-event relocation",
160                error.to_string(),
161                None,
162            )
163        })
164}
165
166fn create_absolute_addr(
167    instance_path: &[(String, usize)],
168    var_path: &[String],
169    instance_modules: &HashMap<InstanceId, ModuleId>,
170    modules: &HashMap<ModuleId, SimModule>,
171    expanded: &HashMap<InstancePath, InstanceId>,
172) -> AbsoluteAddr {
173    let instance_path = InstancePath(instance_path.to_vec());
174    let instance_id = expanded[&instance_path];
175    let module_id = instance_modules[&instance_id];
176    let module = &modules[&module_id];
177    let var_id = *module
178        .variables
179        .iter()
180        .find(|(_, variable)| variable.path == var_path)
181        .unwrap()
182        .0;
183    AbsoluteAddr {
184        instance_id,
185        var_id,
186    }
187}
188fn parse_ignored_loops(
189    ignored_loops: &[(
190        (Vec<(String, usize)>, Vec<String>),
191        (Vec<(String, usize)>, Vec<String>),
192    )],
193    instance_modules: &HashMap<InstanceId, ModuleId>,
194    modules: &HashMap<ModuleId, SimModule>,
195    expanded: &HashMap<InstancePath, InstanceId>,
196) -> HashSet<(AbsoluteAddr, AbsoluteAddr)> {
197    let mut res = HashSet::default();
198
199    for ((from_instance_path, from_var_path), (to_instance_path, to_var_path)) in ignored_loops {
200        let from = create_absolute_addr(
201            from_instance_path,
202            from_var_path,
203            instance_modules,
204            modules,
205            expanded,
206        );
207        let to = create_absolute_addr(
208            to_instance_path,
209            to_var_path,
210            instance_modules,
211            modules,
212            expanded,
213        );
214        res.insert((from, to));
215    }
216    res
217}
218fn parse_true_loops(
219    true_loops: &[(
220        (Vec<(String, usize)>, Vec<String>),
221        (Vec<(String, usize)>, Vec<String>),
222        usize,
223    )],
224    instance_modules: &HashMap<InstanceId, ModuleId>,
225    modules: &HashMap<ModuleId, SimModule>,
226    expanded: &HashMap<InstancePath, InstanceId>,
227) -> HashMap<(AbsoluteAddr, AbsoluteAddr), usize> {
228    let mut res = HashMap::default();
229
230    for ((from_instance_path, from_var_path), (to_instance_path, to_var_path), max_iter) in
231        true_loops
232    {
233        let from = create_absolute_addr(
234            from_instance_path,
235            from_var_path,
236            instance_modules,
237            modules,
238            expanded,
239        );
240        let to = create_absolute_addr(
241            to_instance_path,
242            to_var_path,
243            instance_modules,
244            modules,
245            expanded,
246        );
247        res.insert((from, to), *max_iter);
248    }
249    res
250}
251
252fn scheduler_source_locations(
253    error: &SchedulerError<AbsoluteAddr>,
254    modules: &HashMap<ModuleId, SimModule>,
255    instance_modules: &HashMap<InstanceId, ModuleId>,
256) -> Vec<SourceLocation> {
257    let blocks = match error {
258        SchedulerError::CombinationalLoop { blocks } => blocks,
259        SchedulerError::MultipleDriver { blocks } => blocks,
260        SchedulerError::InvalidDependencyGraph => return Vec::new(),
261    };
262    let mut seen = HashSet::default();
263    blocks
264        .iter()
265        .filter_map(|block| {
266            let addr = block.target.var()?.id;
267            if !seen.insert(addr) {
268                return None;
269            }
270            let module_id = instance_modules.get(&addr.instance_id)?;
271            let module = modules.get(module_id)?;
272            let var = module.variables.get(&addr.var_id)?;
273            var.source.clone()
274        })
275        .collect()
276}
277
278pub fn schedule_symbolic_rtl(
279    symbolic: SymbolicRtl,
280    fused_ff_factory: Option<&dyn FusedFfLoweringFactory>,
281    ignored_loops: &[(
282        (Vec<(String, usize)>, Vec<String>),
283        (Vec<(String, usize)>, Vec<String>),
284    )],
285    true_loops: &[(
286        (Vec<(String, usize)>, Vec<String>),
287        (Vec<(String, usize)>, Vec<String>),
288        usize,
289    )],
290    four_state: bool,
291    trace_opts: &FrontendTraceOptions,
292    mut trace: Option<&mut FrontendTrace>,
293) -> Result<ScheduledRtlOutput, ParserError> {
294    let SymbolicRtl {
295        modules,
296        module_names,
297        root_id,
298    } = symbolic;
299    let flatten_timing = trace_opts.phase_timing;
300    macro_rules! timed_sub {
301        ($label:expr, $body:expr) => {{
302            if flatten_timing {
303                let start = std::time::Instant::now();
304                let result = $body;
305                tracing::debug!("[flatten] {}: {:?}", $label, start.elapsed());
306                result
307            } else {
308                $body
309            }
310        }};
311    }
312
313    if let Some(t) = trace.as_deref_mut()
314        && trace_opts.sim_modules
315    {
316        t.sim_modules = Some(modules.clone());
317    }
318
319    let (expanded, instance_modules, indexed_instances) =
320        timed_sub!("expand_hierarchy", expand_hierarchy(&root_id, &modules));
321    let global_boundaries = timed_sub!(
322        "propagate_boundaries",
323        propagate_boundaries(&expanded, &instance_modules, &modules)
324    );
325    let unpacked_element_widths = instance_modules
326        .iter()
327        .flat_map(|(&instance_id, &module_id)| {
328            modules[&module_id]
329                .variables
330                .iter()
331                .filter_map(move |(&var_id, variable)| {
332                    let element_count = variable
333                        .metadata
334                        .array_dims
335                        .iter()
336                        .try_fold(1usize, |total, &dim| total.checked_mul(dim))?;
337                    let element_width = variable.metadata.width.checked_div(element_count)?;
338                    (element_count > 1 && element_width > 0).then_some((
339                        AbsoluteAddr {
340                            instance_id,
341                            var_id,
342                        },
343                        element_width,
344                    ))
345                })
346        })
347        .collect::<HashMap<_, _>>();
348
349    let clock_domains = timed_sub!(
350        "unify_clock_domains",
351        unify_clock_domains(&expanded, &instance_modules, &modules)
352    );
353    let (
354        mut global_arena,
355        mut eval_apply_ffs,
356        mut eval_only_ffs,
357        mut apply_ffs,
358        _ff_access_summaries,
359        ff_runtime_relocations,
360        mut comb_blocks,
361        mut comb_observers,
362        mut runtime_errors,
363        runtime_event_sites,
364        next_runtime_error_code,
365    ) = timed_sub!(
366        "relocate_units",
367        relocate_units(
368            &expanded,
369            &instance_modules,
370            &modules,
371            &module_names[&root_id],
372            &indexed_instances,
373            &global_boundaries,
374            &unpacked_element_widths,
375            &clock_domains,
376            trace_opts,
377            &mut trace,
378        )
379    )?;
380    let ignored_loops = parse_ignored_loops(ignored_loops, &instance_modules, &modules, &expanded);
381    let true_loops = parse_true_loops(true_loops, &instance_modules, &modules, &expanded);
382
383    // Build reset -> clock mapping with AbsoluteAddr
384    let mut reset_clock_map: HashMap<AbsoluteAddr, AbsoluteAddr> = HashMap::default();
385    for id in expanded.values() {
386        let module_id = &instance_modules[id];
387        let sim_module = &modules[module_id];
388        for (reset_var_id, clock_var_id) in &sim_module.reset_clock_map {
389            let reset_addr = AbsoluteAddr {
390                instance_id: *id,
391                var_id: *reset_var_id,
392            };
393            let clock_addr = AbsoluteAddr {
394                instance_id: *id,
395                var_id: *clock_var_id,
396            };
397            // Use canonical clock domain if available
398            let canonical_clock = clock_domains
399                .get(&clock_addr)
400                .copied()
401                .unwrap_or(clock_addr);
402            let canonical_reset = clock_domains
403                .get(&reset_addr)
404                .copied()
405                .unwrap_or(reset_addr);
406            reset_clock_map.insert(canonical_reset, canonical_clock);
407        }
408    }
409
410    let (topological_clocks, cascaded_clocks) = timed_sub!(
411        "analyze_clock_dependencies",
412        analyze_clock_dependencies(
413            &mut eval_apply_ffs,
414            &mut eval_only_ffs,
415            &mut apply_ffs,
416            &comb_blocks,
417            &global_arena,
418            &clock_domains,
419            &expanded,
420            &instance_modules,
421            &modules,
422        )
423    );
424
425    if let Some(t) = trace.as_deref_mut()
426        && trace_opts.flattened_comb_blocks
427    {
428        t.flattened_comb_blocks = Some((comb_blocks.clone(), global_arena.clone()));
429    }
430
431    // Constant variable inlining: detect variables whose every LogicPath
432    // is a constant, then replace all Input references with Constant nodes.
433    // This eliminates Store→Load roundtrips for compile-time constants
434    // (e.g. genvar-expanded parity-check matrices).
435    celox_slt::const_inline::inline_constant_variables(&mut comb_blocks, &mut global_arena)?;
436    apply_always_comb_previous_source_ordering(&mut comb_blocks);
437
438    let var_widths: HashMap<AbsoluteAddr, usize> = instance_modules
439        .iter()
440        .flat_map(|(&inst_id, &mod_id)| {
441            modules[&mod_id].variables.iter().map(move |(var_id, var)| {
442                (
443                    AbsoluteAddr {
444                        instance_id: inst_id,
445                        var_id: *var_id,
446                    },
447                    var.metadata.width,
448                )
449            })
450        })
451        .collect();
452    let var_signedness: HashMap<AbsoluteAddr, bool> = instance_modules
453        .iter()
454        .flat_map(|(&inst_id, &mod_id)| {
455            modules[&mod_id].variables.iter().map(move |(var_id, var)| {
456                (
457                    AbsoluteAddr {
458                        instance_id: inst_id,
459                        var_id: *var_id,
460                    },
461                    var.signed,
462                )
463            })
464        })
465        .collect();
466
467    build_comb_observer_capture_paths(
468        &mut comb_blocks,
469        &mut comb_observers,
470        &runtime_event_sites,
471        &mut global_arena,
472    )?;
473    for (site_id, site) in runtime_event_sites.iter().enumerate() {
474        if !matches!(site.kind, RuntimeEventKind::AssertFatal) {
475            continue;
476        }
477        runtime_errors
478            .entry(site_id as i64)
479            .or_insert_with(|| RuntimeErrorInfo {
480                message: site
481                    .template
482                    .clone()
483                    .unwrap_or_else(|| "assertion failed".to_string()),
484                signals: Vec::new(),
485            });
486    }
487
488    celox_slt::verify_symbolic_roots(
489        &global_arena,
490        &comb_blocks,
491        &comb_observers,
492        &var_widths,
493        &var_signedness,
494    )
495    .map_err(|error| ParserError::SltVerify {
496        phase: "after flattening symbolic logic",
497        error,
498    })?;
499
500    let fused_inputs = if fused_ff_factory.is_some() {
501        let actions = build_fused_ff_actions(
502            &modules,
503            &instance_modules,
504            &clock_domains,
505            &ff_runtime_relocations,
506        );
507        let mut clock_arena = SLTNodeArena::<RegionedAbsoluteAddr>::new();
508        let mut clock_node_cache = HashMap::default();
509        let clock_comb_blocks = comb_blocks
510            .iter()
511            .map(|path| {
512                path.map_addr(
513                    &global_arena,
514                    &mut clock_arena,
515                    &mut clock_node_cache,
516                    &|addr| RegionedAbsoluteAddr::from_absolute_addr(STABLE_REGION, *addr),
517                )
518            })
519            .collect::<Result<Vec<_>, _>>()?;
520        let clock_var_widths = var_widths
521            .iter()
522            .map(|(&addr, &width)| {
523                (
524                    RegionedAbsoluteAddr::from_absolute_addr(STABLE_REGION, addr),
525                    width,
526                )
527            })
528            .collect::<HashMap<_, _>>();
529        let clock_unpacked_element_widths = unpacked_element_widths
530            .iter()
531            .map(|(&addr, &element_width)| {
532                (
533                    RegionedAbsoluteAddr::from_absolute_addr(STABLE_REGION, addr),
534                    element_width,
535                )
536            })
537            .collect::<HashMap<_, _>>();
538        let clock_ignored_loops = ignored_loops
539            .iter()
540            .map(|&(from, to)| {
541                (
542                    RegionedAbsoluteAddr::from_absolute_addr(STABLE_REGION, from),
543                    RegionedAbsoluteAddr::from_absolute_addr(STABLE_REGION, to),
544                )
545            })
546            .collect::<HashSet<_>>();
547        let clock_true_loops = true_loops
548            .iter()
549            .map(|(&(from, to), &limit)| {
550                (
551                    (
552                        RegionedAbsoluteAddr::from_absolute_addr(STABLE_REGION, from),
553                        RegionedAbsoluteAddr::from_absolute_addr(STABLE_REGION, to),
554                    ),
555                    limit,
556                )
557            })
558            .collect::<HashMap<_, _>>();
559        Some((
560            actions,
561            clock_arena,
562            clock_comb_blocks,
563            clock_var_widths,
564            clock_unpacked_element_widths,
565            clock_ignored_loops,
566            clock_true_loops,
567        ))
568    } else {
569        None
570    };
571
572    let sched_start = flatten_timing.then(std::time::Instant::now);
573    let schedule = match scheduler::sort_with_unpacked_element_widths(
574        comb_blocks,
575        &global_arena,
576        &ignored_loops,
577        &true_loops,
578        four_state,
579        &var_widths,
580        &unpacked_element_widths,
581        next_runtime_error_code,
582    ) {
583        Ok(schedule) => schedule,
584        Err(error) => {
585            let (err_vars, err_path_idx) = module_variables(&modules);
586            let frontend_lookup = FrontendLookup {
587                instance_ids: expanded.clone(),
588                instance_module: instance_modules.clone(),
589                indexed_instances: indexed_instances.clone(),
590                module_variables: err_vars,
591                module_var_path_index: err_path_idx,
592                module_names: module_names.clone(),
593                source_to_state: HashMap::default(),
594                state_to_source: HashMap::default(),
595                event_aliases: HashMap::default(),
596            };
597            let source_locations = scheduler_source_locations(&error, &modules, &instance_modules);
598            let mut target_arena = SLTNodeArena::new();
599            let error = error.map_addr(&global_arena, &mut target_arena, &|addr| {
600                frontend_lookup.get_path(addr)
601            })?;
602            return Err(if source_locations.is_empty() {
603                ParserError::Scheduler(error)
604            } else {
605                ParserError::SchedulerWithLocation {
606                    error,
607                    source_locations,
608                }
609            });
610        }
611    };
612    if let Some(s) = sched_start {
613        tracing::debug!("[flatten] scheduler::sort: {:?}", s.elapsed());
614    }
615    runtime_errors.extend(schedule.runtime_errors);
616    let schduled: Vec<ExecutionUnit<RegionedAbsoluteAddr>> = schedule
617        .execution_units
618        .into_iter()
619        .map(|eu| ExecutionUnit {
620            entry_block_id: eu.entry_block_id,
621            blocks: eu
622                .blocks
623                .into_iter()
624                .map(|(id, bb)| {
625                    (
626                        id,
627                        BasicBlock {
628                            id: bb.id,
629                            params: bb.params,
630                            instructions: bb
631                                .instructions
632                                .into_iter()
633                                .map(|inst| {
634                                    inst.into_map_addr(|addr| RegionedAbsoluteAddr {
635                                        region: STABLE_REGION,
636                                        instance_id: addr.instance_id,
637                                        var_id: addr.var_id,
638                                    })
639                                })
640                                .collect(),
641                            terminator: bb.terminator,
642                        },
643                    )
644                })
645                .collect(),
646            register_map: eu.register_map,
647        })
648        .collect();
649    let eval_comb = schduled.clone();
650    let mut eval_comb_apply_ffs = HashMap::default();
651    let mut fused_direct_ff_writes = HashMap::default();
652    if let (
653        Some(factory),
654        Some((
655            actions,
656            clock_arena,
657            clock_comb_blocks,
658            clock_var_widths,
659            clock_unpacked_element_widths,
660            clock_ignored_loops,
661            clock_true_loops,
662        )),
663    ) = (fused_ff_factory, fused_inputs)
664    {
665        let mut fused_schedule_cache = HashMap::<
666            Vec<usize>,
667            (
668                Vec<ExecutionUnit<RegionedAbsoluteAddr>>,
669                Vec<VarAtomBase<RegionedAbsoluteAddr>>,
670            ),
671        >::default();
672        for (trigger, actions) in actions {
673            let action_ids = actions.iter().map(|action| action.id).collect::<Vec<_>>();
674            if let Some((units, direct_ff_writes)) = fused_schedule_cache.get(&action_ids) {
675                eval_comb_apply_ffs.insert(trigger, units.clone());
676                fused_direct_ff_writes.insert(trigger, direct_ff_writes.clone());
677                continue;
678            }
679            let mut ff_lowering = factory.create(actions)?;
680            let fused_start = flatten_timing.then(std::time::Instant::now);
681            let fused = match scheduler::sort_clock(
682                clock_comb_blocks.clone(),
683                &clock_arena,
684                &clock_ignored_loops,
685                &clock_true_loops,
686                four_state,
687                &clock_var_widths,
688                &clock_unpacked_element_widths,
689                next_runtime_error_code,
690                ff_lowering.as_mut(),
691            ) {
692                Ok(schedule) => schedule,
693                Err(scheduler::ClockSortError::Lowering(error)) => return Err(error),
694                Err(scheduler::ClockSortError::Scheduler(error)) => {
695                    let mut error_arena = SLTNodeArena::new();
696                    let error =
697                        error.map_addr(&clock_arena, &mut error_arena, &|addr| addr.to_string())?;
698                    return Err(ParserError::Scheduler(error));
699                }
700            };
701            if let Some(start) = fused_start {
702                tracing::debug!("[flatten] scheduler::sort_clock: {:?}", start.elapsed());
703            }
704            let direct_ff_writes = fused.direct_ff_writes;
705            let units = fused.execution_units;
706            fused_schedule_cache.insert(action_ids, (units.clone(), direct_ff_writes.clone()));
707            eval_comb_apply_ffs.insert(trigger, units);
708            fused_direct_ff_writes.insert(trigger, direct_ff_writes);
709        }
710    }
711
712    if let Some(t) = trace
713        && trace_opts.scheduled_units
714    {
715        t.scheduled_units = Some(schduled.clone());
716    }
717
718    // The unified function is the normal fast path.  Split evaluator/apply
719    // functions are needed only when scheduling can evaluate several active
720    // domains or must cascade through a derived clock.
721    let active_ff_domains = eval_apply_ffs
722        .values()
723        .filter(|units| !units.is_empty())
724        .count();
725    let needs_split_path = active_ff_domains > 1 || !cascaded_clocks.is_empty();
726    let (eval_only_ffs, apply_ffs) = if needs_split_path {
727        (eval_only_ffs, apply_ffs)
728    } else {
729        (HashMap::default(), HashMap::default())
730    };
731
732    let (mod_vars, mod_path_idx) = module_variables(&modules);
733    let initial_memory_values: Vec<InitialStateValue<AbsoluteAddr>> = instance_modules
734        .iter()
735        .flat_map(|(&instance_id, module_id)| {
736            modules[module_id]
737                .initial_memory_values
738                .iter()
739                .map(move |init| InitialStateValue {
740                    address: AbsoluteAddr {
741                        instance_id,
742                        var_id: init.address,
743                    },
744                    data: init.data.clone(),
745                })
746        })
747        .collect();
748    let state_objects: HashMap<AbsoluteAddr, VariableMetadata> = instance_modules
749        .iter()
750        .flat_map(|(&instance_id, module_id)| {
751            modules[module_id]
752                .variables
753                .iter()
754                .map(move |(&var_id, variable)| {
755                    (
756                        AbsoluteAddr {
757                            instance_id,
758                            var_id,
759                        },
760                        variable.metadata.clone(),
761                    )
762                })
763        })
764        .collect();
765    let runtime_comb_observers: Vec<RuntimeCombObserver<AbsoluteAddr>> = comb_observers
766        .iter()
767        .map(|observer| RuntimeCombObserver {
768            site_id: observer.site_id,
769            activation_group: observer.activation_group,
770            sensitivity: observer.sensitivity.clone(),
771            written_inputs: observer.written_inputs.clone(),
772        })
773        .collect();
774    let source_sir = SirProgram {
775        eval_apply_ffs,
776        eval_comb_apply_ffs,
777        eval_only_ffs,
778        apply_ffs,
779        eval_comb,
780    };
781    let mut source_addresses = state_objects.keys().copied().collect::<Vec<_>>();
782    source_addresses.sort_unstable();
783    let mut source_to_state = HashMap::default();
784    for (index, source) in source_addresses.into_iter().enumerate() {
785        let object = StateObjectId(u32::try_from(index).map_err(|_| {
786            ParserError::illegal_context(
787                "design state projection",
788                "flattened state object count exceeds u32",
789                None,
790            )
791        })?);
792        let state = StateAddr {
793            instance_id: source.instance_id,
794            var_id: object,
795        };
796        source_to_state.insert(source, state);
797    }
798    let project = |source: AbsoluteAddr| source_to_state[&source];
799    let project_regioned = |source: RegionedAbsoluteAddr| RegionedStateAddr {
800        region: source.region,
801        instance_id: source.instance_id,
802        var_id: source_to_state[&source.absolute_addr()].var_id,
803    };
804
805    let sir = source_sir.into_map_addr(project, project_regioned);
806    let state_objects: HashMap<StateAddr, VariableMetadata> = state_objects
807        .into_iter()
808        .map(|(address, metadata)| (project(address), metadata))
809        .collect();
810    let initial_state = initial_memory_values
811        .into_iter()
812        .map(|initial| InitialStateValue {
813            address: project(initial.address),
814            data: initial.data,
815        })
816        .collect();
817    let events = EventTopology {
818        aliases: clock_domains
819            .into_iter()
820            .map(|(alias, canonical)| (project(alias), project(canonical)))
821            .collect(),
822        ordered_events: topological_clocks.into_iter().map(project).collect(),
823        cascaded_events: cascaded_clocks.into_iter().map(project).collect(),
824        reset_clocks: reset_clock_map
825            .into_iter()
826            .map(|(reset, clock)| (project(reset), project(clock)))
827            .collect(),
828    };
829    let event_aliases = events.aliases.clone();
830    let runtime_errors = runtime_errors
831        .into_iter()
832        .map(|(code, info)| {
833            (
834                code,
835                RuntimeErrorInfo {
836                    message: info.message,
837                    signals: info.signals.into_iter().map(project).collect(),
838                },
839            )
840        })
841        .collect();
842    let comb_observers = runtime_comb_observers
843        .into_iter()
844        .map(|observer| RuntimeCombObserver {
845            site_id: observer.site_id,
846            activation_group: observer.activation_group,
847            sensitivity: observer
848                .sensitivity
849                .into_iter()
850                .map(|atom| VarAtomBase {
851                    id: project(atom.id),
852                    access: atom.access,
853                })
854                .collect(),
855            written_inputs: observer.written_inputs.into_iter().map(project).collect(),
856        })
857        .collect();
858    let direct_ff_writes = fused_direct_ff_writes
859        .into_iter()
860        .map(|(source, writes)| {
861            (
862                project(source),
863                writes
864                    .into_iter()
865                    .map(|write| VarAtomBase {
866                        id: project_regioned(write.id),
867                        access: write.access,
868                    })
869                    .collect(),
870            )
871        })
872        .collect();
873
874    let mut rtl_writes = HashSet::default();
875    for unit in sir
876        .eval_comb
877        .iter()
878        .chain(sir.eval_apply_ffs.values().flatten())
879        .chain(sir.eval_comb_apply_ffs.values().flatten())
880        .chain(sir.eval_only_ffs.values().flatten())
881        .chain(sir.apply_ffs.values().flatten())
882    {
883        for block in unit.blocks.values() {
884            for instruction in &block.instructions {
885                let (address, offset, width) = match instruction {
886                    SIRInstruction::Store(address, offset, width, ..)
887                    | SIRInstruction::Commit(_, address, offset, width, _) => {
888                        (address.absolute_addr(), offset, *width)
889                    }
890                    _ => continue,
891                };
892                let access = offset
893                    .constant_bit_offset()
894                    .and_then(|lsb| {
895                        width
896                            .checked_sub(1)
897                            .and_then(|tail| lsb.checked_add(tail))
898                            .map(|msb| BitAccess::new(lsb, msb))
899                    })
900                    .or_else(|| {
901                        state_objects
902                            .get(&address)
903                            .and_then(|object| object.width.checked_sub(1))
904                            .map(|msb| BitAccess::new(0, msb))
905                    });
906                if let Some(access) = access {
907                    rtl_writes.insert(VarAtomBase {
908                        id: address,
909                        access,
910                    });
911                }
912            }
913        }
914    }
915
916    let state_to_source = source_to_state
917        .iter()
918        .map(|(source, state)| (*state, *source))
919        .collect();
920
921    let scheduled = ScheduledRtl {
922        sir,
923        design: ElaboratedDesign {
924            state_objects,
925            events,
926            initial_state,
927        },
928        frontend_lookup: FrontendLookup {
929            instance_ids: expanded,
930            instance_module: instance_modules,
931            indexed_instances,
932            module_variables: mod_vars,
933            module_var_path_index: mod_path_idx,
934            module_names,
935            source_to_state,
936            state_to_source,
937            event_aliases,
938        },
939        runtime_schema: RuntimeSchema {
940            runtime_errors,
941            runtime_event_sites,
942            comb_observers,
943            testbench_read_roots: Default::default(),
944            rtl_writes,
945        },
946    };
947
948    Ok(ScheduledRtlOutput {
949        scheduled,
950        fused_optimization_hints: FusedSirOptimizationHints { direct_ff_writes },
951    })
952}
953
954fn module_variables(
955    modules: &HashMap<ModuleId, SimModule>,
956) -> (
957    HashMap<ModuleId, HashMap<SourceVarId, VariableInfo>>,
958    HashMap<ModuleId, HashMap<Vec<String>, Option<SourceVarId>>>,
959) {
960    let mut res = HashMap::default();
961    let mut path_index = HashMap::default();
962    for (id, module) in modules {
963        let mut variables = HashMap::default();
964        let mut paths: HashMap<Vec<String>, Option<SourceVarId>> = HashMap::default();
965        for (&source_id, variable) in &module.variables {
966            // Only module-scope variables are externally addressable. Locals
967            // may share the same VarPath, but must not make a legal
968            // hierarchical or public lookup appear ambiguous.
969            if variable.module_affiliated {
970                match paths.entry(variable.path.clone()) {
971                    std::collections::hash_map::Entry::Vacant(e) => {
972                        e.insert(Some(source_id));
973                    }
974                    std::collections::hash_map::Entry::Occupied(mut e) => {
975                        // Duplicate visible VarPath — mark as ambiguous.
976                        e.insert(None);
977                    }
978                }
979            }
980            variables.insert(
981                source_id,
982                VariableInfo {
983                    id: source_id,
984                    path: variable.path.clone(),
985                    var_kind: variable.kind,
986                    signed: variable.signed,
987                    packed_dims: variable.packed_dims.clone(),
988                    metadata: variable.metadata.clone(),
989                },
990            );
991        }
992        res.insert(*id, variables);
993        path_index.insert(*id, paths);
994    }
995    (res, path_index)
996}
997
998fn expand_hierarchy(
999    top: &ModuleId,
1000    modules: &HashMap<ModuleId, SimModule>,
1001) -> (
1002    HashMap<InstancePath, InstanceId>,
1003    HashMap<InstanceId, ModuleId>,
1004    HashSet<InstanceId>,
1005) {
1006    let mut expanded = HashMap::default();
1007    let mut instance_modules = HashMap::default();
1008    let mut indexed_instances = HashSet::default();
1009    let mut instance_id = 0;
1010    let path = vec![];
1011    let id = InstanceId(instance_id);
1012    instance_modules.insert(id, *top);
1013    expanded.insert(InstancePath(path.clone()), id);
1014    instance_id += 1;
1015    expand(
1016        top,
1017        path,
1018        modules,
1019        &mut expanded,
1020        &mut instance_modules,
1021        &mut indexed_instances,
1022        &mut instance_id,
1023    );
1024    (expanded, instance_modules, indexed_instances)
1025}
1026
1027fn extend_boundaries(
1028    boundaries: &mut HashMap<AbsoluteAddr, BTreeSet<usize>>,
1029    source: AbsoluteAddr,
1030    target: AbsoluteAddr,
1031) -> bool {
1032    if source == target || boundaries.get(&source).is_none_or(BTreeSet::is_empty) {
1033        return false;
1034    }
1035    boundaries.entry(target).or_default();
1036    let [Some(source), Some(target)] = boundaries.get_disjoint_mut([&source, &target]) else {
1037        unreachable!("distinct boundary keys were inserted before lookup");
1038    };
1039    let old_len = target.len();
1040    target.extend(source.iter().copied());
1041    target.len() != old_len
1042}
1043
1044fn propagate_boundaries(
1045    expanded: &HashMap<InstancePath, InstanceId>,
1046    instance_modules: &HashMap<InstanceId, ModuleId>,
1047    modules: &HashMap<ModuleId, SimModule>,
1048) -> HashMap<AbsoluteAddr, BTreeSet<usize>> {
1049    let mut current_boundaries = HashMap::default();
1050
1051    // Initialize with local boundaries
1052    for id in expanded.values() {
1053        let module_id = &instance_modules[id];
1054        let sim_module = &modules[module_id];
1055        for (var_id, boundaries) in &sim_module.comb_boundaries {
1056            let addr = AbsoluteAddr {
1057                instance_id: *id,
1058                var_id: *var_id,
1059            };
1060            current_boundaries.insert(addr, boundaries.clone());
1061        }
1062    }
1063
1064    // Propagate boundaries
1065    let mut changed = true;
1066    while changed {
1067        changed = false;
1068        for (path, id) in expanded {
1069            let module_id = &instance_modules[id];
1070            let sim_module = &modules[module_id];
1071
1072            for (inst_name, glue_blocks) in &sim_module.glue_blocks {
1073                for (idx, glue_block) in glue_blocks.iter().enumerate() {
1074                    let mut child_path = path.0.clone();
1075                    child_path.push((inst_name.clone(), idx));
1076                    let child_id = expanded[&InstancePath(child_path)];
1077
1078                    // Propagate from Parent to Child (Input Ports)
1079                    for (parent_vars, child_addr) in &glue_block.input_ports {
1080                        if let Some(target) = child_addr.target.var()
1081                            && let GlueAddr::Child(child_var_id) = target.id
1082                        {
1083                            let child_abs = AbsoluteAddr {
1084                                instance_id: child_id,
1085                                var_id: child_var_id,
1086                            };
1087
1088                            // Collect boundaries from all parent variables connected to this port
1089                            for parent_var in parent_vars {
1090                                let parent_abs = AbsoluteAddr {
1091                                    instance_id: *id,
1092                                    var_id: *parent_var,
1093                                };
1094                                changed |= extend_boundaries(
1095                                    &mut current_boundaries,
1096                                    parent_abs,
1097                                    child_abs,
1098                                );
1099                            }
1100                        }
1101                    }
1102
1103                    // Propagate from Child to Parent (Output Ports)
1104                    for (parent_vars, logic_path) in &glue_block.output_ports {
1105                        // logic_path.target is Parent. logic_path.sources contains Child.
1106                        for source in &logic_path.sources {
1107                            if let GlueAddr::Child(child_var_id) = source.id {
1108                                let child_abs = AbsoluteAddr {
1109                                    instance_id: child_id,
1110                                    var_id: child_var_id,
1111                                };
1112
1113                                // Child -> Parent
1114                                for parent_var in parent_vars {
1115                                    let parent_abs = AbsoluteAddr {
1116                                        instance_id: *id,
1117                                        var_id: *parent_var,
1118                                    };
1119                                    changed |= extend_boundaries(
1120                                        &mut current_boundaries,
1121                                        child_abs,
1122                                        parent_abs,
1123                                    );
1124                                }
1125
1126                                // Parent -> Child (Sink -> Source propagation)
1127                                // If the parent wire connected to this output has boundaries (e.g. used in slices),
1128                                // those boundaries should propagate to the child output port so it drives them appropriately.
1129                                for parent_var in parent_vars {
1130                                    let parent_abs = AbsoluteAddr {
1131                                        instance_id: *id,
1132                                        var_id: *parent_var,
1133                                    };
1134                                    changed |= extend_boundaries(
1135                                        &mut current_boundaries,
1136                                        parent_abs,
1137                                        child_abs,
1138                                    );
1139                                }
1140                            }
1141                        }
1142                    }
1143                }
1144            }
1145        }
1146    }
1147    current_boundaries
1148}
1149
1150fn expand(
1151    target: &ModuleId,
1152    path: Vec<(String, usize)>,
1153    modules: &HashMap<ModuleId, SimModule>,
1154    expanded: &mut HashMap<InstancePath, InstanceId>,
1155    instance_modules: &mut HashMap<InstanceId, ModuleId>,
1156    indexed_instances: &mut HashSet<InstanceId>,
1157    instance_id: &mut usize,
1158) {
1159    let module = &modules[target];
1160    for (inst_name, gbs) in &module.glue_blocks {
1161        // Generate loops can elaborate several scalar declarations under the
1162        // same flattened name. Keep those scopes distinct until generate
1163        // hierarchy segments become part of InstancePath.
1164        let indexed = module.indexed_instance_names.contains(inst_name) || gbs.len() > 1;
1165        for (idx, gb) in gbs.iter().enumerate() {
1166            let mut path = path.clone();
1167            path.push((inst_name.clone(), idx));
1168            let id = InstanceId(*instance_id);
1169            expanded.insert(InstancePath(path.clone()), id);
1170            instance_modules.insert(id, gb.module_id);
1171            if indexed {
1172                indexed_instances.insert(id);
1173            }
1174            *instance_id += 1;
1175            expand(
1176                &gb.module_id,
1177                path,
1178                modules,
1179                expanded,
1180                instance_modules,
1181                indexed_instances,
1182                instance_id,
1183            );
1184        }
1185    }
1186}
1187
1188fn relocate_executation_unit_with_errors<A, B>(
1189    eu: &ExecutionUnit<A>,
1190    f: &impl Fn(&A) -> B,
1191    runtime_error_codes: &HashMap<i64, i64>,
1192    runtime_event_sites: &HashMap<u32, u32>,
1193) -> ExecutionUnit<B> {
1194    ExecutionUnit {
1195        entry_block_id: eu.entry_block_id,
1196        blocks: eu
1197            .blocks
1198            .iter()
1199            .map(|(id, block)| {
1200                (
1201                    *id,
1202                    BasicBlock {
1203                        id: block.id,
1204                        instructions: block
1205                            .instructions
1206                            .iter()
1207                            .map(|inst| match inst {
1208                                SIRInstruction::RuntimeEvent { site_id, args } => {
1209                                    SIRInstruction::RuntimeEvent {
1210                                        site_id: runtime_event_sites
1211                                            .get(site_id)
1212                                            .copied()
1213                                            .unwrap_or(*site_id),
1214                                        args: args.clone(),
1215                                    }
1216                                }
1217                                SIRInstruction::CombCaptureEvent {
1218                                    site_id,
1219                                    args,
1220                                    fatal_error_code,
1221                                    consume_enabled,
1222                                } => SIRInstruction::CombCaptureEvent {
1223                                    site_id: runtime_event_sites
1224                                        .get(site_id)
1225                                        .copied()
1226                                        .unwrap_or(*site_id),
1227                                    args: args.clone(),
1228                                    fatal_error_code: *fatal_error_code,
1229                                    consume_enabled: *consume_enabled,
1230                                },
1231                                _ => inst.map_addr(f),
1232                            })
1233                            .collect(),
1234                        params: block.params.clone(),
1235                        terminator: match block.terminator {
1236                            SIRTerminator::Error(code) => SIRTerminator::Error(
1237                                runtime_error_codes.get(&code).copied().unwrap_or(code),
1238                            ),
1239                            ref terminator => terminator.clone(),
1240                        },
1241                    },
1242                )
1243            })
1244            .collect(),
1245        register_map: eu.register_map.clone(),
1246    }
1247}
1248
1249fn unify_clock_domains(
1250    expanded: &HashMap<InstancePath, InstanceId>,
1251    instance_modules: &HashMap<InstanceId, ModuleId>,
1252    modules: &HashMap<ModuleId, SimModule>,
1253) -> HashMap<AbsoluteAddr, AbsoluteAddr> {
1254    let mut drive_graph: HashMap<AbsoluteAddr, Vec<AbsoluteAddr>> = HashMap::default();
1255
1256    for (path, id) in expanded {
1257        let module_id = &instance_modules[id];
1258        let sim_module = &modules[module_id];
1259
1260        // Internal aliases (e.g. `assign clk_internal = clk_port;`)
1261        for logic_path in &sim_module.comb_blocks {
1262            // Only unify direct aliases, not complex logic like gated clocks
1263            if logic_path.sources.len() == 1 {
1264                let expr_node = sim_module.arena.get(logic_path.expr);
1265                let is_alias = matches!(
1266                    expr_node,
1267                    celox_slt::SLTNode::Input { .. } | celox_slt::SLTNode::Slice { .. }
1268                );
1269                if is_alias {
1270                    let Some(target) = logic_path.target.var() else {
1271                        continue;
1272                    };
1273                    let target_abs = AbsoluteAddr {
1274                        instance_id: *id,
1275                        var_id: target.id,
1276                    };
1277                    let source_abs = AbsoluteAddr {
1278                        instance_id: *id,
1279                        var_id: logic_path.sources.iter().next().unwrap().id,
1280                    };
1281                    drive_graph.entry(source_abs).or_default().push(target_abs);
1282                }
1283            }
1284        }
1285        for (inst_name, glue_blocks) in &sim_module.glue_blocks {
1286            for (idx, glue_block) in glue_blocks.iter().enumerate() {
1287                let mut child_path = path.0.clone();
1288                child_path.push((inst_name.clone(), idx));
1289                let child_id = expanded[&InstancePath(child_path)];
1290
1291                // Inputs: Parent -> Child (Parent drives Child)
1292                for (parent_vars, logic_path) in &glue_block.input_ports {
1293                    if let Some(target) = logic_path.target.var()
1294                        && let GlueAddr::Child(child_var_id) = target.id
1295                    {
1296                        let child_abs = AbsoluteAddr {
1297                            instance_id: child_id,
1298                            var_id: child_var_id,
1299                        };
1300                        for parent_var in parent_vars {
1301                            let parent_abs = AbsoluteAddr {
1302                                instance_id: *id,
1303                                var_id: *parent_var,
1304                            };
1305                            drive_graph.entry(parent_abs).or_default().push(child_abs);
1306                        }
1307                    }
1308                }
1309                // Outputs: Child -> Parent (Child drives Parent)
1310                for (parent_vars, logic_path) in &glue_block.output_ports {
1311                    for parent_var in parent_vars {
1312                        let parent_abs = AbsoluteAddr {
1313                            instance_id: *id,
1314                            var_id: *parent_var,
1315                        };
1316                        for source in &logic_path.sources {
1317                            if let GlueAddr::Child(child_var_id) = source.id {
1318                                let child_abs = AbsoluteAddr {
1319                                    instance_id: child_id,
1320                                    var_id: child_var_id,
1321                                };
1322                                drive_graph.entry(child_abs).or_default().push(parent_abs);
1323                            }
1324                        }
1325                    }
1326                }
1327            }
1328        }
1329    }
1330
1331    // Resolve Canonical Clock Domains: Find the root driver for each connected component
1332    let mut clock_domains: HashMap<AbsoluteAddr, AbsoluteAddr> = HashMap::default();
1333
1334    // Reverse the drive graph to find roots (Sink -> Sources)
1335    let mut reverse_drive_graph: HashMap<AbsoluteAddr, Vec<AbsoluteAddr>> = HashMap::default();
1336    for (src, sinks) in &drive_graph {
1337        for sink in sinks {
1338            reverse_drive_graph.entry(*sink).or_default().push(*src);
1339        }
1340    }
1341
1342    // Collect all unique addresses involved in any drive relationship
1343    let mut all_addrs = HashSet::default();
1344    for src in drive_graph.keys() {
1345        all_addrs.insert(*src);
1346    }
1347    for sinks in drive_graph.values() {
1348        for sink in sinks {
1349            all_addrs.insert(*sink);
1350        }
1351    }
1352
1353    // Assign each address its canonical root driver
1354    for addr in all_addrs {
1355        let mut current = addr;
1356        let mut visited = HashSet::default();
1357        // Traverse upwards towards the root driver
1358        while let Some(sources) = reverse_drive_graph.get(&current) {
1359            if sources.is_empty() {
1360                break;
1361            }
1362            // In a valid hardware design, a clock net usually has 1 driver.
1363            // If multiple, we just pick the first for canonicalization.
1364            let next = sources[0];
1365            if visited.contains(&next) {
1366                break; // Prevent infinite loop in case of bad combinational loop
1367            }
1368            visited.insert(next);
1369            current = next;
1370        }
1371        clock_domains.insert(addr, current);
1372    }
1373    clock_domains
1374}
1375
1376fn build_fused_ff_actions(
1377    modules: &HashMap<ModuleId, SimModule>,
1378    instance_modules: &HashMap<InstanceId, ModuleId>,
1379    clock_domains: &HashMap<AbsoluteAddr, AbsoluteAddr>,
1380    runtime_relocations: &HashMap<InstanceId, FfRuntimeRelocation>,
1381) -> HashMap<AbsoluteAddr, Vec<FusedFfAction>> {
1382    let mut instances = instance_modules.iter().collect::<Vec<_>>();
1383    instances.sort_unstable_by_key(|(instance, _)| instance.0);
1384    let mut result = HashMap::<AbsoluteAddr, Vec<FusedFfAction>>::default();
1385    let mut next_action_id = 0usize;
1386
1387    for (&instance_id, &module_id) in instances {
1388        let module = &modules[&module_id];
1389        let mut summaries = module.ff_access_summaries.iter().collect::<Vec<_>>();
1390        summaries.sort_unstable_by_key(|(trigger, _)| (*trigger).clone());
1391        for (trigger, summary) in summaries {
1392            let relocate = |address: RegionedVarAddr| RegionedAbsoluteAddr {
1393                region: address.region,
1394                instance_id,
1395                var_id: address.var_id,
1396            };
1397            let summary = FfAccessSummary {
1398                reads: summary
1399                    .reads
1400                    .iter()
1401                    .map(|read| VarAtomBase {
1402                        id: relocate(read.id),
1403                        access: read.access,
1404                    })
1405                    .collect(),
1406                writes: summary
1407                    .writes
1408                    .iter()
1409                    .map(|write| VarAtomBase {
1410                        id: RegionedAbsoluteAddr {
1411                            region: STABLE_REGION,
1412                            instance_id,
1413                            var_id: write.id.var_id,
1414                        },
1415                        access: write.access,
1416                    })
1417                    .collect(),
1418                dynamic_writes: summary
1419                    .dynamic_writes
1420                    .iter()
1421                    .map(|address| RegionedAbsoluteAddr {
1422                        region: STABLE_REGION,
1423                        instance_id,
1424                        var_id: address.var_id,
1425                    })
1426                    .collect(),
1427            };
1428            let action = FusedFfAction {
1429                id: next_action_id,
1430                instance_id,
1431                module_id,
1432                trigger: trigger.clone(),
1433                summary,
1434                runtime: runtime_relocations[&instance_id].clone(),
1435            };
1436            next_action_id += 1;
1437            let clock = AbsoluteAddr {
1438                instance_id,
1439                var_id: trigger.clock,
1440            };
1441            let clock = clock_domains.get(&clock).copied().unwrap_or(clock);
1442            result.entry(clock).or_default().push(action.clone());
1443            for &reset_id in &trigger.resets {
1444                let reset = AbsoluteAddr {
1445                    instance_id,
1446                    var_id: reset_id,
1447                };
1448                let reset = clock_domains.get(&reset).copied().unwrap_or(reset);
1449                result.entry(reset).or_default().push(action.clone());
1450            }
1451        }
1452    }
1453    result
1454}
1455
1456fn relocate_units(
1457    expanded: &HashMap<InstancePath, InstanceId>,
1458    instance_modules: &HashMap<InstanceId, ModuleId>,
1459    modules: &HashMap<ModuleId, SimModule>,
1460    root_name: &str,
1461    indexed_instances: &HashSet<InstanceId>,
1462    global_boundaries: &HashMap<AbsoluteAddr, std::collections::BTreeSet<usize>>,
1463    unpacked_element_widths: &HashMap<AbsoluteAddr, usize>,
1464    clock_domains: &HashMap<AbsoluteAddr, AbsoluteAddr>,
1465    trace_opts: &crate::FrontendTraceOptions,
1466    trace: &mut Option<&mut crate::FrontendTrace>,
1467) -> Result<
1468    (
1469        SLTNodeArena<AbsoluteAddr>,
1470        HashMap<AbsoluteAddr, Vec<ExecutionUnit<RegionedAbsoluteAddr>>>,
1471        HashMap<AbsoluteAddr, Vec<ExecutionUnit<RegionedAbsoluteAddr>>>,
1472        HashMap<AbsoluteAddr, Vec<ExecutionUnit<RegionedAbsoluteAddr>>>,
1473        HashMap<AbsoluteAddr, Vec<FfAccessSummary<RegionedAbsoluteAddr>>>,
1474        HashMap<InstanceId, FfRuntimeRelocation>,
1475        Vec<celox_slt::LogicPath<AbsoluteAddr>>,
1476        Vec<CombObserver<AbsoluteAddr>>,
1477        HashMap<i64, RuntimeErrorInfo<AbsoluteAddr>>,
1478        Vec<RuntimeEventSite>,
1479        i64,
1480    ),
1481    ParserError,
1482> {
1483    let mut global_arena = SLTNodeArena::<AbsoluteAddr>::new();
1484    let mut eval_apply_ffs: HashMap<AbsoluteAddr, Vec<ExecutionUnit<RegionedAbsoluteAddr>>> =
1485        HashMap::default();
1486    let mut eval_only_ffs: HashMap<AbsoluteAddr, Vec<ExecutionUnit<RegionedAbsoluteAddr>>> =
1487        HashMap::default();
1488    let mut apply_ffs: HashMap<AbsoluteAddr, Vec<ExecutionUnit<RegionedAbsoluteAddr>>> =
1489        HashMap::default();
1490    let mut ff_access_summaries: HashMap<AbsoluteAddr, Vec<FfAccessSummary<RegionedAbsoluteAddr>>> =
1491        HashMap::default();
1492    let mut ff_runtime_relocations = HashMap::default();
1493    let mut comb_blocks = Vec::new();
1494    let mut comb_observers = Vec::new();
1495    let mut runtime_errors = HashMap::default();
1496    let mut runtime_event_sites = Vec::new();
1497    let mut next_runtime_error_code = 2000;
1498
1499    for (path, id) in expanded {
1500        let module_id = &instance_modules[id];
1501        let sim_module = &modules[module_id];
1502        let runtime_event_site_base = u32::try_from(runtime_event_sites.len()).map_err(|_| {
1503            ParserError::illegal_context(
1504                "FF runtime-event relocation",
1505                "runtime event site count exceeds u32",
1506                None,
1507            )
1508        })?;
1509        let relocate_ff_summary = |summary: &FfAccessSummary<RegionedVarAddr>| {
1510            let relocate_addr = |addr: RegionedVarAddr| RegionedAbsoluteAddr {
1511                region: addr.region,
1512                instance_id: *id,
1513                var_id: addr.var_id,
1514            };
1515            FfAccessSummary {
1516                reads: summary
1517                    .reads
1518                    .iter()
1519                    .map(|read| VarAtomBase {
1520                        id: relocate_addr(read.id),
1521                        access: read.access,
1522                    })
1523                    .collect(),
1524                writes: summary
1525                    .writes
1526                    .iter()
1527                    .map(|write| VarAtomBase {
1528                        id: relocate_addr(write.id),
1529                        access: write.access,
1530                    })
1531                    .collect(),
1532                dynamic_writes: summary
1533                    .dynamic_writes
1534                    .iter()
1535                    .copied()
1536                    .map(relocate_addr)
1537                    .collect(),
1538            }
1539        };
1540        for (trigger_set, summary) in &sim_module.ff_access_summaries {
1541            let clock_addr = AbsoluteAddr {
1542                instance_id: *id,
1543                var_id: trigger_set.clock,
1544            };
1545            let canonical_clock = clock_domains
1546                .get(&clock_addr)
1547                .copied()
1548                .unwrap_or(clock_addr);
1549            ff_access_summaries
1550                .entry(canonical_clock)
1551                .or_default()
1552                .push(relocate_ff_summary(summary));
1553            for &reset in &trigger_set.resets {
1554                let reset_addr = AbsoluteAddr {
1555                    instance_id: *id,
1556                    var_id: reset,
1557                };
1558                let canonical_reset = clock_domains
1559                    .get(&reset_addr)
1560                    .copied()
1561                    .unwrap_or(reset_addr);
1562                ff_access_summaries
1563                    .entry(canonical_reset)
1564                    .or_default()
1565                    .push(relocate_ff_summary(summary));
1566            }
1567        }
1568        let mut runtime_error_codes = HashMap::default();
1569        for (&local_code, info) in &sim_module.runtime_errors {
1570            let global_code = next_runtime_error_code;
1571            next_runtime_error_code += 1;
1572            runtime_error_codes.insert(local_code, global_code);
1573            runtime_errors.insert(
1574                global_code,
1575                RuntimeErrorInfo {
1576                    message: info.message.clone(),
1577                    signals: info
1578                        .signals
1579                        .iter()
1580                        .filter(|var_id| sim_module.variables.contains_key(var_id))
1581                        .map(|&var_id| AbsoluteAddr {
1582                            instance_id: *id,
1583                            var_id,
1584                        })
1585                        .collect(),
1586                },
1587            );
1588        }
1589        ff_runtime_relocations.insert(
1590            *id,
1591            FfRuntimeRelocation {
1592                error_codes: runtime_error_codes.clone(),
1593                event_site_base: runtime_event_site_base,
1594            },
1595        );
1596        let mut runtime_event_site_map = HashMap::default();
1597        let scope = elaborated_scope_name(root_name, path, expanded, indexed_instances);
1598        for (local_site, site) in sim_module.runtime_event_sites.iter().enumerate() {
1599            let global_site = runtime_event_sites.len() as u32;
1600            runtime_event_site_map.insert(local_site as u32, global_site);
1601            let mut site = site.clone();
1602            site.scope = Some(scope.clone());
1603            runtime_event_sites.push(site);
1604        }
1605
1606        let arena_start = global_arena.len();
1607        let mut relocated_module = flatten_with_trace(
1608            sim_module,
1609            path,
1610            expanded,
1611            global_boundaries,
1612            unpacked_element_widths,
1613            &mut global_arena,
1614            trace_opts,
1615            trace.as_deref_mut(),
1616        )?;
1617        remap_for_fold_runtime_event_sites(
1618            &mut global_arena,
1619            arena_start,
1620            &runtime_event_site_map,
1621        )?;
1622        for observer in &mut relocated_module.comb_observers {
1623            observer.site_id = runtime_event_site_map[&observer.site_id];
1624            observer.activation_group = runtime_event_site_map[&observer.activation_group];
1625        }
1626        comb_blocks.extend(relocated_module.comb_blocks);
1627        comb_observers.extend(relocated_module.comb_observers);
1628
1629        // Relocate sequential blocks for this instance
1630        for (trigger_set, eu) in &sim_module.eval_apply_ff_blocks {
1631            let clock_addr = AbsoluteAddr {
1632                instance_id: *id,
1633                var_id: trigger_set.clock,
1634            };
1635            let canonical_addr = clock_domains
1636                .get(&clock_addr)
1637                .copied()
1638                .unwrap_or(clock_addr);
1639
1640            eval_apply_ffs.entry(canonical_addr).or_default().push(
1641                relocate_executation_unit_with_errors(
1642                    eu,
1643                    &|addr| RegionedAbsoluteAddr {
1644                        region: addr.region,
1645                        instance_id: *id,
1646                        var_id: addr.var_id,
1647                    },
1648                    &runtime_error_codes,
1649                    &runtime_event_site_map,
1650                ),
1651            );
1652
1653            for &reset in &trigger_set.resets {
1654                let reset_addr = AbsoluteAddr {
1655                    instance_id: *id,
1656                    var_id: reset,
1657                };
1658                let canonical_addr = clock_domains
1659                    .get(&reset_addr)
1660                    .copied()
1661                    .unwrap_or(reset_addr);
1662                eval_apply_ffs.entry(canonical_addr).or_default().push(
1663                    relocate_executation_unit_with_errors(
1664                        eu,
1665                        &|addr| RegionedAbsoluteAddr {
1666                            region: addr.region,
1667                            instance_id: *id,
1668                            var_id: addr.var_id,
1669                        },
1670                        &runtime_error_codes,
1671                        &runtime_event_site_map,
1672                    ),
1673                );
1674            }
1675        }
1676
1677        for (trigger_set, eu) in &sim_module.eval_only_ff_blocks {
1678            let clock_addr = AbsoluteAddr {
1679                instance_id: *id,
1680                var_id: trigger_set.clock,
1681            };
1682            let canonical_addr = clock_domains
1683                .get(&clock_addr)
1684                .copied()
1685                .unwrap_or(clock_addr);
1686            eval_only_ffs.entry(canonical_addr).or_default().push(
1687                relocate_executation_unit_with_errors(
1688                    eu,
1689                    &|addr| RegionedAbsoluteAddr {
1690                        region: addr.region,
1691                        instance_id: *id,
1692                        var_id: addr.var_id,
1693                    },
1694                    &runtime_error_codes,
1695                    &runtime_event_site_map,
1696                ),
1697            );
1698
1699            for &reset in &trigger_set.resets {
1700                let reset_addr = AbsoluteAddr {
1701                    instance_id: *id,
1702                    var_id: reset,
1703                };
1704                let canonical_addr = clock_domains
1705                    .get(&reset_addr)
1706                    .copied()
1707                    .unwrap_or(reset_addr);
1708                eval_only_ffs.entry(canonical_addr).or_default().push(
1709                    relocate_executation_unit_with_errors(
1710                        eu,
1711                        &|addr| RegionedAbsoluteAddr {
1712                            region: addr.region,
1713                            instance_id: *id,
1714                            var_id: addr.var_id,
1715                        },
1716                        &runtime_error_codes,
1717                        &runtime_event_site_map,
1718                    ),
1719                );
1720            }
1721        }
1722
1723        for (trigger_set, eu) in &sim_module.apply_ff_blocks {
1724            let clock_addr = AbsoluteAddr {
1725                instance_id: *id,
1726                var_id: trigger_set.clock,
1727            };
1728            let canonical_addr = clock_domains
1729                .get(&clock_addr)
1730                .copied()
1731                .unwrap_or(clock_addr);
1732            apply_ffs.entry(canonical_addr).or_default().push(
1733                relocate_executation_unit_with_errors(
1734                    eu,
1735                    &|addr| RegionedAbsoluteAddr {
1736                        region: addr.region,
1737                        instance_id: *id,
1738                        var_id: addr.var_id,
1739                    },
1740                    &runtime_error_codes,
1741                    &runtime_event_site_map,
1742                ),
1743            );
1744
1745            for &reset in &trigger_set.resets {
1746                let reset_addr = AbsoluteAddr {
1747                    instance_id: *id,
1748                    var_id: reset,
1749                };
1750                let canonical_addr = clock_domains
1751                    .get(&reset_addr)
1752                    .copied()
1753                    .unwrap_or(reset_addr);
1754                apply_ffs.entry(canonical_addr).or_default().push(
1755                    relocate_executation_unit_with_errors(
1756                        eu,
1757                        &|addr| RegionedAbsoluteAddr {
1758                            region: addr.region,
1759                            instance_id: *id,
1760                            var_id: addr.var_id,
1761                        },
1762                        &runtime_error_codes,
1763                        &runtime_event_site_map,
1764                    ),
1765                );
1766            }
1767        }
1768    }
1769    Ok((
1770        global_arena,
1771        eval_apply_ffs,
1772        eval_only_ffs,
1773        apply_ffs,
1774        ff_access_summaries,
1775        ff_runtime_relocations,
1776        comb_blocks,
1777        comb_observers,
1778        runtime_errors,
1779        runtime_event_sites,
1780        next_runtime_error_code,
1781    ))
1782}
1783
1784fn build_comb_observer_capture_paths(
1785    comb_blocks: &mut Vec<LogicPath<AbsoluteAddr>>,
1786    observers: &mut [CombObserver<AbsoluteAddr>],
1787    sites: &[RuntimeEventSite],
1788    arena: &mut SLTNodeArena<AbsoluteAddr>,
1789) -> Result<(), ParserError> {
1790    if observers.is_empty() {
1791        return Ok(());
1792    }
1793
1794    annotate_comb_capture_enable_sites(comb_blocks, observers);
1795
1796    let mut group_members: HashMap<u32, Vec<usize>> = HashMap::default();
1797    for (idx, observer) in observers.iter().enumerate() {
1798        group_members
1799            .entry(observer.activation_group)
1800            .or_default()
1801            .push(idx);
1802    }
1803    let mut emitted_group_triggers = HashSet::default();
1804    let mut previous_primary_capture_path: Option<LogicPathId> = None;
1805    let mut previous_trigger_capture_path: Option<LogicPathId> = None;
1806    for observer_idx in 0..observers.len() {
1807        let observer = &observers[observer_idx];
1808        let has_statement_position_dependency =
1809            observer_has_statement_position_dependency(comb_blocks, observer);
1810        let order_before = observer_order_before(comb_blocks, observer);
1811        let order_after = observer_order_after(comb_blocks, observer);
1812        let trigger_paths = if has_statement_position_dependency {
1813            observer_trigger_paths(comb_blocks, observer)
1814        } else {
1815            Vec::new()
1816        };
1817        if observer.captured_in_loop {
1818            let Some(loop_runner) = observer.loop_runner else {
1819                continue;
1820            };
1821            let sources: HashSet<_> = observer
1822                .sensitivity
1823                .iter()
1824                .copied()
1825                .filter(|atom| !observer_written_input_overlaps(observer, atom))
1826                .filter(|atom| !observer_statement_position_overlaps(comb_blocks, observer, atom))
1827                .collect();
1828            let path_id = LogicPathId(comb_blocks.len());
1829            if let Some(prev) = previous_primary_capture_path {
1830                comb_blocks[prev.0].order_before.insert(path_id);
1831            }
1832            for idx in &order_after {
1833                comb_blocks[idx.0].order_before.insert(path_id);
1834            }
1835            comb_blocks.push(LogicPath {
1836                target: LogicPathTarget::CombCaptureEvent {
1837                    site_id: observer.site_id,
1838                    guard: None,
1839                    emit_on_true: true,
1840                    args: Vec::new(),
1841                    loop_runner: Some(loop_runner),
1842                    fatal_error_code: None,
1843                    consume_enabled: true,
1844                },
1845                sources,
1846                previous_sources: HashSet::default(),
1847                address_sources: HashSet::default(),
1848                local_inputs: observer.local_inputs.clone(),
1849                order_before: order_before.clone(),
1850                comb_capture_enable_sites: Vec::new(),
1851                comb_capture_enable_always: false,
1852                pre_lower_nodes: Vec::new(),
1853                expr: loop_runner,
1854            });
1855            previous_primary_capture_path = Some(path_id);
1856            for trigger_idx in trigger_paths {
1857                let Some(trigger_target) = comb_blocks[trigger_idx.0].target.var().copied() else {
1858                    continue;
1859                };
1860                let trigger_order_before =
1861                    direct_consumers_of_path_target(comb_blocks, trigger_idx);
1862                let path_id = LogicPathId(comb_blocks.len());
1863                if let Some(prev) = previous_trigger_capture_path {
1864                    comb_blocks[prev.0].order_before.insert(path_id);
1865                }
1866                comb_blocks[trigger_idx.0].order_before.insert(path_id);
1867                comb_blocks.push(LogicPath {
1868                    target: LogicPathTarget::CombCaptureEvent {
1869                        site_id: observer.site_id,
1870                        guard: None,
1871                        emit_on_true: true,
1872                        args: Vec::new(),
1873                        loop_runner: Some(loop_runner),
1874                        fatal_error_code: None,
1875                        consume_enabled: true,
1876                    },
1877                    sources: std::iter::once(trigger_target).collect(),
1878                    previous_sources: HashSet::default(),
1879                    address_sources: HashSet::default(),
1880                    local_inputs: observer.local_inputs.clone(),
1881                    order_before: trigger_order_before,
1882                    comb_capture_enable_sites: Vec::new(),
1883                    comb_capture_enable_always: false,
1884                    pre_lower_nodes: Vec::new(),
1885                    expr: loop_runner,
1886                });
1887                previous_trigger_capture_path = Some(path_id);
1888            }
1889            continue;
1890        }
1891        let local_input_ids: HashSet<_> = observer
1892            .local_inputs
1893            .iter()
1894            .map(|(addr, _)| *addr)
1895            .collect();
1896        let mut sources: HashSet<_> = observer
1897            .observed_inputs
1898            .iter()
1899            .copied()
1900            .filter(|atom| !observer_written_input_overlaps(observer, atom))
1901            .filter(|atom| !local_input_ids.contains(&atom.id))
1902            .filter(|atom| !observer_statement_position_overlaps(comb_blocks, observer, atom))
1903            .collect();
1904        for (_, node) in &observer.local_inputs {
1905            let mut local_sources = HashSet::default();
1906            crate::flattening::collect_inputs(*node, arena, &mut local_sources);
1907            sources.extend(
1908                local_sources
1909                    .into_iter()
1910                    .filter(|atom| !observer_written_input_overlaps(observer, atom))
1911                    .filter(|atom| !local_input_ids.contains(&atom.id))
1912                    .filter(|atom| {
1913                        !observer_statement_position_overlaps(comb_blocks, observer, atom)
1914                    }),
1915            );
1916        }
1917        let expr = match observer.guard.or_else(|| observer.args.first().copied()) {
1918            Some(expr) => expr,
1919            None => arena.alloc(celox_slt::SLTNode::Constant(
1920                num_bigint::BigUint::from(1u8),
1921                num_bigint::BigUint::from(0u8),
1922                1,
1923                false,
1924            ))?,
1925        };
1926        let emit_on_true = matches!(
1927            sites[observer.site_id as usize].kind,
1928            RuntimeEventKind::Display | RuntimeEventKind::Write
1929        );
1930        let fatal_error_code = matches!(
1931            sites[observer.site_id as usize].kind,
1932            RuntimeEventKind::AssertFatal
1933        )
1934        .then_some(observer.site_id as i64);
1935        let pre_lower_nodes = observer_pre_lower_nodes(observer, arena);
1936        for idx in &order_after {
1937            comb_blocks[idx.0]
1938                .pre_lower_nodes
1939                .extend(pre_lower_nodes.iter().copied());
1940        }
1941        let path_id = LogicPathId(comb_blocks.len());
1942        if let Some(prev) = previous_primary_capture_path {
1943            comb_blocks[prev.0].order_before.insert(path_id);
1944        }
1945        for idx in &order_after {
1946            comb_blocks[idx.0].order_before.insert(path_id);
1947        }
1948        comb_blocks.push(LogicPath {
1949            target: LogicPathTarget::CombCaptureEvent {
1950                site_id: observer.site_id,
1951                guard: observer.guard,
1952                emit_on_true,
1953                args: observer.args.clone(),
1954                loop_runner: None,
1955                fatal_error_code,
1956                consume_enabled: !trigger_paths.is_empty(),
1957            },
1958            sources,
1959            previous_sources: HashSet::default(),
1960            address_sources: HashSet::default(),
1961            local_inputs: observer.local_inputs.clone(),
1962            order_before,
1963            comb_capture_enable_sites: Vec::new(),
1964            comb_capture_enable_always: false,
1965            pre_lower_nodes: Vec::new(),
1966            expr,
1967        });
1968        previous_primary_capture_path = Some(path_id);
1969        for trigger_idx in trigger_paths {
1970            if !emitted_group_triggers.insert((observer.activation_group, trigger_idx)) {
1971                continue;
1972            }
1973            let Some(trigger_target) = comb_blocks[trigger_idx.0].target.var().copied() else {
1974                continue;
1975            };
1976            let trigger_order_before = direct_consumers_of_path_target(comb_blocks, trigger_idx);
1977            for &member_idx in &group_members[&observer.activation_group] {
1978                let member = &observers[member_idx];
1979                let member_emit_on_true = matches!(
1980                    sites[member.site_id as usize].kind,
1981                    RuntimeEventKind::Display | RuntimeEventKind::Write
1982                );
1983                let member_fatal_error_code = matches!(
1984                    sites[member.site_id as usize].kind,
1985                    RuntimeEventKind::AssertFatal
1986                )
1987                .then_some(member.site_id as i64);
1988                let member_expr = match member
1989                    .loop_runner
1990                    .or(member.guard)
1991                    .or_else(|| member.args.first().copied())
1992                {
1993                    Some(expr) => expr,
1994                    None => arena.alloc(celox_slt::SLTNode::Constant(
1995                        num_bigint::BigUint::from(1u8),
1996                        num_bigint::BigUint::from(0u8),
1997                        1,
1998                        false,
1999                    ))?,
2000                };
2001                let path_id = LogicPathId(comb_blocks.len());
2002                if let Some(prev) = previous_trigger_capture_path {
2003                    comb_blocks[prev.0].order_before.insert(path_id);
2004                }
2005                comb_blocks[trigger_idx.0].order_before.insert(path_id);
2006                comb_blocks.push(LogicPath {
2007                    target: LogicPathTarget::CombCaptureEvent {
2008                        site_id: member.site_id,
2009                        guard: member.guard,
2010                        emit_on_true: member_emit_on_true,
2011                        args: member.args.clone(),
2012                        loop_runner: member.loop_runner,
2013                        fatal_error_code: member_fatal_error_code,
2014                        consume_enabled: true,
2015                    },
2016                    sources: std::iter::once(trigger_target).collect(),
2017                    previous_sources: HashSet::default(),
2018                    address_sources: HashSet::default(),
2019                    local_inputs: member.local_inputs.clone(),
2020                    order_before: trigger_order_before.clone(),
2021                    comb_capture_enable_sites: Vec::new(),
2022                    comb_capture_enable_always: false,
2023                    pre_lower_nodes: Vec::new(),
2024                    expr: member_expr,
2025                });
2026                previous_trigger_capture_path = Some(path_id);
2027            }
2028        }
2029    }
2030    Ok(())
2031}
2032
2033fn apply_always_comb_previous_source_ordering(comb_blocks: &mut [LogicPath<AbsoluteAddr>]) {
2034    let targets: Vec<_> = comb_blocks
2035        .iter()
2036        .map(|path| path.target.var().copied())
2037        .collect();
2038
2039    for (idx, path) in comb_blocks.iter_mut().enumerate() {
2040        if path.previous_sources.is_empty() {
2041            continue;
2042        }
2043
2044        let previous_sources = path.previous_sources.clone();
2045        let address_sources = path.address_sources.clone();
2046        path.sources.retain(|source| {
2047            let is_previous = previous_sources.iter().any(|previous| {
2048                previous.id == source.id && previous.access.overlaps(&source.access)
2049            });
2050            let is_address = address_sources
2051                .iter()
2052                .any(|address| address.id == source.id && address.access.overlaps(&source.access));
2053            !is_previous || is_address
2054        });
2055
2056        let mut order_before = Vec::new();
2057        for (target_idx, target) in targets.iter().enumerate() {
2058            if target_idx == idx {
2059                continue;
2060            }
2061            let Some(target) = target else {
2062                continue;
2063            };
2064            if previous_sources.iter().any(|previous| {
2065                previous.id == target.id && previous.access.overlaps(&target.access)
2066            }) {
2067                order_before.push(LogicPathId(target_idx));
2068            }
2069        }
2070        path.order_before.extend(order_before);
2071    }
2072}
2073
2074fn observer_written_input_overlaps(
2075    observer: &CombObserver<AbsoluteAddr>,
2076    atom: &VarAtomBase<AbsoluteAddr>,
2077) -> bool {
2078    observer
2079        .written_input_atoms
2080        .iter()
2081        .any(|written| written.id == atom.id && written.access.overlaps(&atom.access))
2082}
2083
2084fn observer_statement_position_overlaps(
2085    paths: &[LogicPath<AbsoluteAddr>],
2086    observer: &CombObserver<AbsoluteAddr>,
2087    atom: &VarAtomBase<AbsoluteAddr>,
2088) -> bool {
2089    atom_overlaps_any(atom, observer_affected_by_preceding_writes(paths, observer))
2090}
2091
2092fn observer_has_statement_position_dependency(
2093    paths: &[LogicPath<AbsoluteAddr>],
2094    observer: &CombObserver<AbsoluteAddr>,
2095) -> bool {
2096    if observer.preceding_writes.is_empty() {
2097        return false;
2098    }
2099    let affected = observer_affected_by_preceding_writes(paths, observer);
2100    observer
2101        .position_inputs
2102        .iter()
2103        .chain(observer.observed_inputs.iter())
2104        .any(|input| atom_overlaps_any(input, &affected))
2105}
2106
2107fn observer_trigger_paths(
2108    paths: &[LogicPath<AbsoluteAddr>],
2109    observer: &CombObserver<AbsoluteAddr>,
2110) -> Vec<LogicPathId> {
2111    let mut seen_targets = HashSet::default();
2112    let affected = observer_affected_by_preceding_writes(paths, observer);
2113    paths
2114        .iter()
2115        .enumerate()
2116        .filter_map(|(idx, path)| {
2117            let target = path.target.var()?;
2118            if observer_written_input_overlaps(observer, target) {
2119                return None;
2120            }
2121            let matches_observer_operand = observer
2122                .position_inputs
2123                .iter()
2124                .chain(observer.observed_inputs.iter())
2125                .any(|atom| target.id == atom.id && target.access.overlaps(&atom.access));
2126            if !matches_observer_operand
2127                || !atom_overlaps_any(target, &affected)
2128                || !seen_targets.insert((target.id, target.access.lsb, target.access.msb))
2129            {
2130                return None;
2131            }
2132            Some(LogicPathId(idx))
2133        })
2134        .collect()
2135}
2136
2137fn observer_pre_lower_nodes(
2138    observer: &CombObserver<AbsoluteAddr>,
2139    arena: &SLTNodeArena<AbsoluteAddr>,
2140) -> Vec<NodeId> {
2141    let local_input_ids: HashSet<_> = observer.local_inputs.iter().map(|(id, _)| *id).collect();
2142    let mut nodes = Vec::with_capacity(observer.args.len() + usize::from(observer.guard.is_some()));
2143    if let Some(guard) = observer.guard {
2144        nodes.push(guard);
2145    }
2146    nodes.extend(observer.args.iter().copied());
2147    nodes.extend(observer.local_inputs.iter().filter_map(|(_, node)| {
2148        matches!(arena.get(*node), celox_slt::SLTNode::Capture { .. }).then_some(*node)
2149    }));
2150    nodes
2151        .into_iter()
2152        .filter(|node| {
2153            let mut inputs = HashSet::default();
2154            crate::flattening::collect_inputs(*node, arena, &mut inputs);
2155            // A capture independent of local bindings can be materialized at
2156            // its statement position. Bound captures still need the event's
2157            // environment, unless they contain an earlier formal snapshot.
2158            // Constants need no early materialization.
2159            !inputs.is_empty()
2160                && (capture_contains_nested_snapshot(*node, arena)
2161                    || inputs
2162                        .iter()
2163                        .all(|input| !local_input_ids.contains(&input.id)))
2164        })
2165        .collect()
2166}
2167
2168fn capture_contains_nested_snapshot(node: NodeId, arena: &SLTNodeArena<AbsoluteAddr>) -> bool {
2169    let celox_slt::SLTNode::Capture { expr, .. } = arena.get(node) else {
2170        return false;
2171    };
2172    let mut work = vec![*expr];
2173    let mut visited = HashSet::default();
2174    while let Some(node) = work.pop() {
2175        if !visited.insert(node) {
2176            continue;
2177        }
2178        match arena.get(node) {
2179            celox_slt::SLTNode::Capture { .. } => return true,
2180            celox_slt::SLTNode::Input { index, .. } => {
2181                work.extend(index.iter().map(|entry| entry.node));
2182            }
2183            celox_slt::SLTNode::Constant(..) => {}
2184            celox_slt::SLTNode::Binary(lhs, _, rhs) => {
2185                work.push(*lhs);
2186                work.push(*rhs);
2187            }
2188            celox_slt::SLTNode::Unary(_, inner) => work.push(*inner),
2189            celox_slt::SLTNode::Mux {
2190                cond,
2191                then_expr,
2192                else_expr,
2193            } => {
2194                work.push(*cond);
2195                work.push(*then_expr);
2196                work.push(*else_expr);
2197            }
2198            celox_slt::SLTNode::Concat(parts) => {
2199                work.extend(parts.iter().map(|(part, _)| *part));
2200            }
2201            celox_slt::SLTNode::Slice { expr, .. } => work.push(*expr),
2202            celox_slt::SLTNode::ForFold {
2203                start,
2204                end,
2205                result,
2206                initials,
2207                updates,
2208                effects,
2209                continue_cond,
2210                ..
2211            } => {
2212                if let celox_slt::SLTLoopBound::Expr(node) = start {
2213                    work.push(*node);
2214                }
2215                if let celox_slt::SLTLoopBound::Expr(node) = end {
2216                    work.push(*node);
2217                }
2218                if let celox_slt::SLTForFoldResult::Transient { initial, update } = result {
2219                    work.push(*initial);
2220                    work.push(*update);
2221                }
2222                work.extend(initials.iter().map(|state| state.expr));
2223                work.extend(updates.iter().map(|state| state.expr));
2224                for effect in effects {
2225                    match effect {
2226                        celox_slt::SLTForEffect::Event { guard, args, .. } => {
2227                            work.extend(*guard);
2228                            work.extend(args.iter().copied());
2229                        }
2230                        celox_slt::SLTForEffect::Runner(runner) => work.push(*runner),
2231                    }
2232                }
2233                work.push(*continue_cond);
2234            }
2235            celox_slt::SLTNode::ForFoldGroup {
2236                entry_guard,
2237                states,
2238                ..
2239            } => {
2240                work.push(*entry_guard);
2241                for state in states {
2242                    work.push(state.initial);
2243                    work.push(state.update);
2244                }
2245            }
2246        }
2247    }
2248    false
2249}
2250
2251fn annotate_comb_capture_enable_sites(
2252    comb_blocks: &mut [LogicPath<AbsoluteAddr>],
2253    observers: &[CombObserver<AbsoluteAddr>],
2254) {
2255    let mut group_sites: HashMap<u32, Vec<u32>> = HashMap::default();
2256    for observer in observers {
2257        group_sites
2258            .entry(observer.activation_group)
2259            .or_default()
2260            .push(observer.site_id);
2261    }
2262    for observer in observers {
2263        for atom in &observer.sensitivity {
2264            for path in comb_blocks.iter_mut() {
2265                let Some(target) = path.target.var() else {
2266                    continue;
2267                };
2268                if target.id == atom.id && target.access.overlaps(&atom.access) {
2269                    for site_id in &group_sites[&observer.activation_group] {
2270                        if !path.comb_capture_enable_sites.contains(site_id) {
2271                            path.comb_capture_enable_sites.push(*site_id);
2272                        }
2273                    }
2274                }
2275            }
2276        }
2277    }
2278}
2279
2280fn observer_order_after(
2281    paths: &[LogicPath<AbsoluteAddr>],
2282    observer: &CombObserver<AbsoluteAddr>,
2283) -> HashSet<LogicPathId> {
2284    let mut result = HashSet::default();
2285    if !observer_has_statement_position_dependency(paths, observer) {
2286        return result;
2287    }
2288    for written in &observer.preceding_writes {
2289        for (idx, path) in paths.iter().enumerate() {
2290            let Some(target) = path.target.var() else {
2291                continue;
2292            };
2293            if target.id == written.id && target.access.overlaps(&written.access) {
2294                result.insert(LogicPathId(idx));
2295            }
2296        }
2297    }
2298    result
2299}
2300
2301fn observer_order_before(
2302    paths: &[LogicPath<AbsoluteAddr>],
2303    observer: &CombObserver<AbsoluteAddr>,
2304) -> HashSet<LogicPathId> {
2305    let preceding_writes = observer.preceding_writes.iter().collect::<Vec<_>>();
2306    let affected_by_preceding_writes = observer_has_statement_position_dependency(paths, observer)
2307        .then(|| observer_affected_by_preceding_writes(paths, observer));
2308    let mut result = HashSet::default();
2309    for (idx, path) in paths.iter().enumerate() {
2310        let Some(target) = path.target.var() else {
2311            continue;
2312        };
2313        let already_written = preceding_writes
2314            .iter()
2315            .any(|written| target.id == written.id && target.access.overlaps(&written.access));
2316        let is_later_observed_write = observer_written_input_overlaps(observer, target);
2317        let is_later_affected_write = affected_by_preceding_writes
2318            .as_ref()
2319            .is_some_and(|affected| atom_overlaps_any(target, affected));
2320        if !already_written && (is_later_observed_write || is_later_affected_write) {
2321            result.insert(LogicPathId(idx));
2322        }
2323    }
2324    result
2325}
2326
2327/// Place a trigger-capture between the write which activated it and each
2328/// immediate dataflow consumer of that write. Transitive consumers remain
2329/// ordered by the ordinary LogicPath dependency graph.
2330fn direct_consumers_of_path_target(
2331    paths: &[LogicPath<AbsoluteAddr>],
2332    trigger: LogicPathId,
2333) -> HashSet<LogicPathId> {
2334    let Some(target) = paths.get(trigger.0).and_then(|path| path.target.var()) else {
2335        return HashSet::default();
2336    };
2337    paths
2338        .iter()
2339        .enumerate()
2340        .filter_map(|(index, path)| {
2341            (index != trigger.0
2342                && path
2343                    .sources
2344                    .iter()
2345                    .any(|source| source.id == target.id && source.access.overlaps(&target.access)))
2346            .then_some(LogicPathId(index))
2347        })
2348        .collect()
2349}
2350
2351fn observer_affected_by_preceding_writes(
2352    paths: &[LogicPath<AbsoluteAddr>],
2353    observer: &CombObserver<AbsoluteAddr>,
2354) -> HashSet<VarAtomBase<AbsoluteAddr>> {
2355    let mut affected: HashSet<VarAtomBase<AbsoluteAddr>> =
2356        observer.preceding_writes.iter().copied().collect();
2357    let mut changed = true;
2358    while changed {
2359        changed = false;
2360        for path in paths {
2361            let Some(target) = path.target.var() else {
2362                continue;
2363            };
2364            if !path
2365                .sources
2366                .iter()
2367                .any(|source| atom_overlaps_any(source, &affected))
2368            {
2369                continue;
2370            }
2371            if affected.insert(*target) {
2372                changed = true;
2373            }
2374        }
2375    }
2376    affected
2377}
2378
2379fn atom_overlaps_any<A: Eq + std::hash::Hash + Copy>(
2380    atom: &VarAtomBase<A>,
2381    atoms: impl IntoIterator<Item = impl std::borrow::Borrow<VarAtomBase<A>>>,
2382) -> bool {
2383    atoms.into_iter().any(|other| {
2384        let other = other.borrow();
2385        atom.id == other.id && atom.access.overlaps(&other.access)
2386    })
2387}
2388
2389fn analyze_clock_dependencies(
2390    eval_apply_ffs: &mut HashMap<AbsoluteAddr, Vec<ExecutionUnit<RegionedAbsoluteAddr>>>,
2391    eval_only_ffs: &mut HashMap<AbsoluteAddr, Vec<ExecutionUnit<RegionedAbsoluteAddr>>>,
2392    apply_ffs: &mut HashMap<AbsoluteAddr, Vec<ExecutionUnit<RegionedAbsoluteAddr>>>,
2393    comb_blocks: &[LogicPath<AbsoluteAddr>],
2394    arena: &SLTNodeArena<AbsoluteAddr>,
2395    clock_domains: &HashMap<AbsoluteAddr, AbsoluteAddr>,
2396    expanded: &HashMap<InstancePath, InstanceId>,
2397    instance_modules: &HashMap<InstanceId, ModuleId>,
2398    modules: &HashMap<ModuleId, SimModule>,
2399) -> (Vec<AbsoluteAddr>, BTreeSet<AbsoluteAddr>) {
2400    // Build static clock dependency graph & Topo Sort
2401    let mut clock_deps: BTreeMap<AbsoluteAddr, BTreeSet<AbsoluteAddr>> = BTreeMap::new();
2402    let mut unique_clocks: BTreeSet<AbsoluteAddr> = BTreeSet::new();
2403
2404    // 1. Identify all variables written by FFs (direct sequential outputs)
2405    let mut ff_outputs: BTreeSet<AbsoluteAddr> = BTreeSet::new();
2406    unique_clocks.extend(eval_apply_ffs.keys().copied());
2407
2408    // Include event-typed signals even when no FF is directly driven by them.
2409    // A testbench clock may only feed a combinationally gated clock, in which
2410    // case it would otherwise be absent from the dependency graph entirely.
2411    for id in expanded.values() {
2412        let module_id = &instance_modules[id];
2413        let sim_module = &modules[module_id];
2414        for (var_id, var) in &sim_module.variables {
2415            let kind = var.metadata.kind;
2416            if !matches!(
2417                kind,
2418                DomainKind::ClockPosedge
2419                    | DomainKind::ClockNegedge
2420                    | DomainKind::ResetAsyncHigh
2421                    | DomainKind::ResetAsyncLow
2422            ) {
2423                continue;
2424            }
2425            let addr = AbsoluteAddr {
2426                instance_id: *id,
2427                var_id: *var_id,
2428            };
2429            let canonical = clock_domains.get(&addr).copied().unwrap_or(addr);
2430            unique_clocks.insert(canonical);
2431            eval_apply_ffs.entry(canonical).or_default();
2432            eval_only_ffs.entry(canonical).or_default();
2433            apply_ffs.entry(canonical).or_default();
2434        }
2435    }
2436
2437    for (domain_clock, eus) in &*eval_apply_ffs {
2438        for eu in eus {
2439            for bb in eu.blocks.values() {
2440                for inst in &bb.instructions {
2441                    if let SIRInstruction::Store(target_addr, ..) = inst {
2442                        // Direct sequential dependency: the target is driven by this clock
2443                        let abs = target_addr.absolute_addr();
2444                        let canonical_target = clock_domains.get(&abs).copied().unwrap_or(abs);
2445
2446                        ff_outputs.insert(abs);
2447
2448                        if canonical_target != *domain_clock
2449                            && unique_clocks.contains(&canonical_target)
2450                        {
2451                            clock_deps
2452                                .entry(canonical_target)
2453                                .or_default()
2454                                .insert(*domain_clock);
2455                        }
2456                    }
2457                }
2458            }
2459        }
2460    }
2461
2462    // 2. Build combinational dependency graph (target -> sources)
2463    let acd_timing = tracing::enabled!(tracing::Level::DEBUG);
2464    let acd_start = acd_timing.then(std::time::Instant::now);
2465    let mut comb_deps: BTreeMap<AbsoluteAddr, BTreeSet<AbsoluteAddr>> = BTreeMap::new();
2466    for path in comb_blocks {
2467        let Some(target) = path.target.var() else {
2468            continue;
2469        };
2470        let target_abs = target.id;
2471        let mut sources = HashSet::default();
2472        crate::flattening::collect_inputs(path.expr, arena, &mut sources);
2473        for source in sources {
2474            comb_deps.entry(target_abs).or_default().insert(source.id);
2475        }
2476    }
2477    if let Some(s) = acd_start {
2478        tracing::debug!(
2479            "[acd] comb_deps build ({} blocks): {:?}",
2480            comb_deps.len(),
2481            s.elapsed()
2482        );
2483    }
2484
2485    // Record clock-to-clock dependencies through combinational logic.  FF
2486    // propagation below finds divided clocks, but a plain gated clock such as
2487    // `gated_clk = clk & enable` has no FF source and needs this separate walk.
2488    fn collect_upstream_clocks(
2489        node: AbsoluteAddr,
2490        target_clock: AbsoluteAddr,
2491        comb_deps: &BTreeMap<AbsoluteAddr, BTreeSet<AbsoluteAddr>>,
2492        clock_domains: &HashMap<AbsoluteAddr, AbsoluteAddr>,
2493        clocks: &BTreeSet<AbsoluteAddr>,
2494        visited: &mut BTreeSet<AbsoluteAddr>,
2495        found: &mut BTreeSet<AbsoluteAddr>,
2496    ) {
2497        if !visited.insert(node) {
2498            return;
2499        }
2500        let Some(sources) = comb_deps.get(&node) else {
2501            return;
2502        };
2503        for source in sources {
2504            let canonical = clock_domains.get(source).copied().unwrap_or(*source);
2505            if canonical != target_clock && clocks.contains(&canonical) {
2506                found.insert(canonical);
2507            }
2508            collect_upstream_clocks(
2509                *source,
2510                target_clock,
2511                comb_deps,
2512                clock_domains,
2513                clocks,
2514                visited,
2515                found,
2516            );
2517        }
2518    }
2519
2520    for target_clock in &unique_clocks {
2521        let mut sources = BTreeSet::new();
2522        collect_upstream_clocks(
2523            *target_clock,
2524            *target_clock,
2525            &comb_deps,
2526            clock_domains,
2527            &unique_clocks,
2528            &mut BTreeSet::new(),
2529            &mut sources,
2530        );
2531        if !sources.is_empty() {
2532            clock_deps.entry(*target_clock).or_default().extend(sources);
2533        }
2534    }
2535
2536    // 3. Propagate FF outputs through combinational graph to find all derived variables
2537    let fp_start = acd_timing.then(std::time::Instant::now);
2538    let mut derived_from_ff: BTreeSet<AbsoluteAddr> = ff_outputs.clone();
2539    let mut changed = true;
2540    let mut fp_rounds = 0u32;
2541    while changed {
2542        changed = false;
2543        fp_rounds += 1;
2544        for (target, sources) in &comb_deps {
2545            if !derived_from_ff.contains(target) {
2546                // If any source is derived from an FF, the target is too
2547                if sources.iter().any(|s| derived_from_ff.contains(s)) {
2548                    derived_from_ff.insert(*target);
2549                    changed = true;
2550                }
2551            }
2552        }
2553    }
2554    if let Some(s) = fp_start {
2555        tracing::debug!(
2556            "[acd] fixpoint: {fp_rounds} rounds, {} entries, {:?}",
2557            comb_deps.len(),
2558            s.elapsed()
2559        );
2560    }
2561
2562    // 4. Any clock domain that is derived from an FF is a cascaded clock!
2563    // We add them to a special "pseudo-domain" or just add themselves to trigger cascade marking.
2564    for clk in &unique_clocks {
2565        if derived_from_ff.contains(clk) {
2566            // Self-dependency ensures it appears in `clock_deps.keys()`
2567            clock_deps.entry(*clk).or_default().insert(*clk);
2568        }
2569    }
2570
2571    // Topologically sort the clock domains
2572    // Sources (no dependencies) should be evaluated first.
2573    let mut topological_clocks = Vec::new();
2574    let mut visited = BTreeSet::new();
2575    let mut temp_visited = BTreeSet::new();
2576
2577    fn topo_visit(
2578        node: AbsoluteAddr,
2579        deps: &BTreeMap<AbsoluteAddr, BTreeSet<AbsoluteAddr>>,
2580        visited: &mut BTreeSet<AbsoluteAddr>,
2581        temp_visited: &mut BTreeSet<AbsoluteAddr>,
2582        result: &mut Vec<AbsoluteAddr>,
2583    ) {
2584        if visited.contains(&node) {
2585            return;
2586        }
2587        if temp_visited.contains(&node) {
2588            // Cycle detected in clock generation, ignore and break cycle for now
2589            return;
2590        }
2591        temp_visited.insert(node);
2592
2593        if let Some(node_deps) = deps.get(&node) {
2594            for &dep in node_deps {
2595                topo_visit(dep, deps, visited, temp_visited, result);
2596            }
2597        }
2598
2599        temp_visited.remove(&node);
2600        visited.insert(node);
2601        result.push(node);
2602    }
2603
2604    // Ensure all unique clocks mapped in eval_apply_ffs are present in the topo sort
2605    for &clk in &unique_clocks {
2606        if !visited.contains(&clk) {
2607            topo_visit(
2608                clk,
2609                &clock_deps,
2610                &mut visited,
2611                &mut temp_visited,
2612                &mut topological_clocks,
2613            );
2614        }
2615    }
2616
2617    // Include other potential event signals (like synchronous resets) so they can be scheduled
2618    for id in expanded.values() {
2619        let module_id = &instance_modules[id];
2620        let sim_module = &modules[module_id];
2621        for (var_id, var) in &sim_module.variables {
2622            let kind = var.metadata.kind;
2623            let is_trigger = matches!(
2624                kind,
2625                DomainKind::ClockPosedge
2626                    | DomainKind::ClockNegedge
2627                    | DomainKind::ResetAsyncHigh
2628                    | DomainKind::ResetAsyncLow
2629            );
2630            if is_trigger {
2631                let addr = AbsoluteAddr {
2632                    instance_id: *id,
2633                    var_id: *var_id,
2634                };
2635                let canonical = clock_domains.get(&addr).copied().unwrap_or(addr);
2636                // Add empty execution units so it becomes a valid event domain for scheduling
2637                eval_apply_ffs.entry(canonical).or_default();
2638                eval_only_ffs.entry(canonical).or_default();
2639                apply_ffs.entry(canonical).or_default();
2640
2641                if !visited.contains(&canonical) {
2642                    topo_visit(
2643                        canonical,
2644                        &clock_deps,
2645                        &mut visited,
2646                        &mut temp_visited,
2647                        &mut topological_clocks,
2648                    );
2649                }
2650            }
2651        }
2652    }
2653
2654    let mut cascaded_clocks: BTreeSet<AbsoluteAddr> = BTreeSet::new();
2655    for (target, sources) in &clock_deps {
2656        cascaded_clocks.insert(*target);
2657        for source in sources {
2658            cascaded_clocks.insert(*source);
2659        }
2660    }
2661
2662    (topological_clocks, cascaded_clocks)
2663}