Skip to main content

celox/
ir.rs

1use crate::HashMap;
2pub use celox_design::PortTypeKind;
3pub(crate) use celox_design::{
4    AbsoluteAddrBase, BitAccess, InstanceId, ModuleId, RegionedAbsoluteAddrBase,
5    RegionedVarAddrBase, RuntimeSchema, SPARSE_WORKING_REGION, STABLE_REGION, VarAtomBase,
6    WORKING_REGION,
7};
8#[cfg(test)]
9pub(crate) use celox_design::{BinaryOp, UnaryOp};
10#[cfg(feature = "host-runtime")]
11pub(crate) use celox_design::{
12    InitialStateData, InitialStateWriteRun, RuntimeEventKind, RuntimeEventSite,
13};
14pub use celox_frontend_core::shared::{
15    FrontendLookup, InstancePath, SourceAddr, SourceVarId, VariableInfo, VariableKind,
16};
17#[cfg(all(
18    feature = "host-runtime",
19    any(
20        target_arch = "x86_64",
21        feature = "arm64-codegen",
22        target_arch = "aarch64"
23    )
24))]
25use celox_runtime::{
26    DesignReflection, ReflectionScope, ReflectionScopeId, ReflectionSignal, ReflectionSignalId,
27    SignalDirection,
28};
29#[cfg(test)]
30pub(crate) use celox_sir::{BasicBlock, SIRValue, inline_single_predecessor_jumps};
31pub(crate) use celox_sir::{
32    BlockId, ExecutionUnit, RegisterId, RegisterType, SIRInstruction, SIROffset, SIRTerminator,
33    collect_exact_zero_registers,
34};
35use celox_testbench::TestbenchProgram;
36use std::{fmt, ops::Deref};
37
38/// Source-independent identity of one elaborated state object.
39pub type AbsoluteAddr = celox_design::StateAddr;
40/// Source-independent state identity qualified by its storage region.
41pub type RegionedAbsoluteAddr = celox_design::RegionedStateAddr;
42pub type SirProgram = celox_sir::SirProgram<AbsoluteAddr, RegionedAbsoluteAddr>;
43
44/// Source-facing metadata for one flattened runtime state object.
45///
46/// Storage metadata remains canonical in [`RuntimeDesign::semantic`]; this
47/// record only retains the hierarchy and source properties needed for lookup,
48/// diagnostics, testbench integration, and reflection.
49#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
50pub struct RuntimeVariable {
51    pub address: AbsoluteAddr,
52    pub source_id: SourceVarId,
53    pub path: Vec<String>,
54    pub var_kind: VariableKind,
55    pub signed: bool,
56    pub packed_dims: Vec<usize>,
57}
58
59/// One elaborated runtime instance with direct state-address indices.
60#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
61pub struct RuntimeInstance {
62    pub id: InstanceId,
63    pub module_id: ModuleId,
64    pub module_name: String,
65    pub path: InstancePath,
66    pub display_path: Vec<String>,
67    state_addresses: Vec<AbsoluteAddr>,
68    source_variables: HashMap<SourceVarId, AbsoluteAddr>,
69    path_index: HashMap<Vec<String>, Option<AbsoluteAddr>>,
70}
71
72impl RuntimeInstance {
73    pub fn state_addresses(&self) -> &[AbsoluteAddr] {
74        &self.state_addresses
75    }
76
77    pub fn resolves_path_to(&self, path: &[String], address: AbsoluteAddr) -> bool {
78        self.path_index.get(path) == Some(&Some(address))
79    }
80}
81
82/// Canonical runtime design model after frontend scheduling.
83///
84/// The semantic state table, hierarchy, paths, and source-facing variable
85/// properties are projected into this model once. The compiler can then drop
86/// [`FrontendLookup`] instead of retaining it beside a duplicate state table.
87#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
88pub struct RuntimeDesign {
89    semantic: celox_design::ElaboratedDesign<AbsoluteAddr>,
90    instances: HashMap<InstanceId, RuntimeInstance>,
91    instance_ids: HashMap<InstancePath, InstanceId>,
92    variables: HashMap<AbsoluteAddr, RuntimeVariable>,
93}
94
95impl std::ops::Deref for RuntimeDesign {
96    type Target = celox_design::ElaboratedDesign<AbsoluteAddr>;
97
98    fn deref(&self) -> &Self::Target {
99        &self.semantic
100    }
101}
102
103impl RuntimeDesign {
104    fn from_projection(
105        semantic: celox_design::ElaboratedDesign<AbsoluteAddr>,
106        frontend: FrontendLookup,
107    ) -> Result<Self, DesignProjectionError> {
108        let expected_count = frontend
109            .instance_module
110            .values()
111            .map(|module_id| frontend.module_variables[module_id].len())
112            .sum::<usize>();
113        if semantic.state_objects.len() != expected_count {
114            return Err(DesignProjectionError::StateObjectCount {
115                design: semantic.state_objects.len(),
116                frontend: expected_count,
117            });
118        }
119
120        let mut instances = HashMap::default();
121        let mut variables = HashMap::default();
122        for (path, &instance_id) in &frontend.instance_ids {
123            let module_id = frontend.instance_module[&instance_id];
124            let module_variables = &frontend.module_variables[&module_id];
125            let display_path = frontend.instance_path_segments(path);
126            let mut state_addresses = Vec::with_capacity(module_variables.len());
127            let mut source_variables = HashMap::default();
128
129            for info in module_variables.values() {
130                let source_address = SourceAddr {
131                    instance_id,
132                    var_id: info.id,
133                };
134                let Some(address) = frontend.state_address(&source_address) else {
135                    return Err(DesignProjectionError::MissingStateProjection { source_address });
136                };
137                let Some(metadata) = semantic.state_objects.get(&address) else {
138                    return Err(DesignProjectionError::MissingStateObject { address });
139                };
140                if metadata != &info.metadata {
141                    return Err(DesignProjectionError::MetadataMismatch { address });
142                }
143
144                state_addresses.push(address);
145                source_variables.insert(info.id, address);
146                variables.insert(
147                    address,
148                    RuntimeVariable {
149                        address,
150                        source_id: info.id,
151                        path: info.path.clone(),
152                        var_kind: info.var_kind,
153                        signed: info.signed,
154                        packed_dims: info.packed_dims.clone(),
155                    },
156                );
157            }
158
159            state_addresses.sort_unstable();
160            let path_index = frontend.module_var_path_index[&module_id]
161                .iter()
162                .map(|(path, source_id)| {
163                    (
164                        path.clone(),
165                        source_id.and_then(|source_id| source_variables.get(&source_id).copied()),
166                    )
167                })
168                .collect();
169            instances.insert(
170                instance_id,
171                RuntimeInstance {
172                    id: instance_id,
173                    module_id,
174                    module_name: frontend
175                        .module_names
176                        .get(&module_id)
177                        .cloned()
178                        .unwrap_or_else(|| module_id.to_string()),
179                    path: path.clone(),
180                    display_path,
181                    state_addresses,
182                    source_variables,
183                    path_index,
184                },
185            );
186        }
187
188        let design = Self {
189            semantic,
190            instances,
191            instance_ids: frontend.instance_ids,
192            variables,
193        };
194        design
195            .validate()
196            .map_err(|reason| DesignProjectionError::InvalidRuntimeDesign { reason })?;
197        Ok(design)
198    }
199
200    pub fn semantic(&self) -> &celox_design::ElaboratedDesign<AbsoluteAddr> {
201        &self.semantic
202    }
203
204    pub fn instances(&self) -> impl Iterator<Item = &RuntimeInstance> {
205        self.instances.values()
206    }
207
208    pub fn instance(&self, id: InstanceId) -> Option<&RuntimeInstance> {
209        self.instances.get(&id)
210    }
211
212    pub fn instance_at_path(&self, path: &InstancePath) -> Option<&RuntimeInstance> {
213        self.instance_ids
214            .get(path)
215            .and_then(|instance_id| self.instances.get(instance_id))
216    }
217
218    pub fn root_instance(&self) -> Option<&RuntimeInstance> {
219        self.instance_at_path(&InstancePath(Vec::new()))
220    }
221
222    pub fn variable(&self, address: &AbsoluteAddr) -> Option<&RuntimeVariable> {
223        self.variables.get(address)
224    }
225
226    pub fn instance_variable(
227        &self,
228        instance_id: InstanceId,
229        source_id: SourceVarId,
230    ) -> Option<&RuntimeVariable> {
231        let address = self
232            .instances
233            .get(&instance_id)?
234            .source_variables
235            .get(&source_id)?;
236        self.variables.get(address)
237    }
238
239    pub fn variable_info(&self, address: &AbsoluteAddr) -> Option<VariableInfo> {
240        let variable = self.variables.get(address)?;
241        Some(VariableInfo {
242            id: variable.source_id,
243            path: variable.path.clone(),
244            var_kind: variable.var_kind,
245            signed: variable.signed,
246            metadata: self.semantic.state_objects.get(address)?.clone(),
247            packed_dims: variable.packed_dims.clone(),
248        })
249    }
250
251    pub fn get_path(&self, address: &AbsoluteAddr) -> String {
252        let Some(variable) = self.variables.get(address) else {
253            return address.to_string();
254        };
255        let Some(instance) = self.instances.get(&address.instance_id) else {
256            return address.to_string();
257        };
258        instance
259            .display_path
260            .iter()
261            .chain(&variable.path)
262            .cloned()
263            .collect::<Vec<_>>()
264            .join(".")
265    }
266
267    pub(crate) fn validate(&self) -> Result<(), String> {
268        if self.variables.len() != self.semantic.state_objects.len() {
269            return Err(format!(
270                "state variable count differs: design={} runtime={}",
271                self.semantic.state_objects.len(),
272                self.variables.len()
273            ));
274        }
275        if self.instance_ids.len() != self.instances.len() {
276            return Err(format!(
277                "instance count differs: paths={} instances={}",
278                self.instance_ids.len(),
279                self.instances.len()
280            ));
281        }
282
283        for (path, instance_id) in &self.instance_ids {
284            let instance = self.instances.get(instance_id).ok_or_else(|| {
285                format!("instance path {path:?} references missing {instance_id}")
286            })?;
287            if instance.path != *path || instance.id != *instance_id {
288                return Err(format!("instance path index disagrees for {instance_id}"));
289            }
290        }
291
292        for (instance_id, instance) in &self.instances {
293            if instance.id != *instance_id {
294                return Err(format!("instance map key disagrees for {instance_id}"));
295            }
296            if self.instance_ids.get(&instance.path) != Some(instance_id) {
297                return Err(format!("missing path index for {instance_id}"));
298            }
299            if instance.state_addresses.len() != instance.source_variables.len() {
300                return Err(format!(
301                    "state/source variable count differs for {instance_id}"
302                ));
303            }
304            if instance
305                .state_addresses
306                .windows(2)
307                .any(|addresses| addresses[0] >= addresses[1])
308            {
309                return Err(format!(
310                    "state addresses are not strictly sorted for {instance_id}"
311                ));
312            }
313
314            for address in &instance.state_addresses {
315                if address.instance_id != *instance_id {
316                    return Err(format!(
317                        "state address {address} belongs to another instance"
318                    ));
319                }
320                if !self.semantic.state_objects.contains_key(address) {
321                    return Err(format!("state address {address} has no semantic metadata"));
322                }
323                let variable = self
324                    .variables
325                    .get(address)
326                    .ok_or_else(|| format!("state address {address} has no runtime variable"))?;
327                if variable.address != *address
328                    || instance.source_variables.get(&variable.source_id) != Some(address)
329                {
330                    return Err(format!("source index disagrees for {address}"));
331                }
332            }
333
334            for (path, address) in &instance.path_index {
335                let Some(address) = address else {
336                    continue;
337                };
338                let variable = self
339                    .variables
340                    .get(address)
341                    .ok_or_else(|| format!("path index references missing {address}"))?;
342                if address.instance_id != *instance_id || variable.path != *path {
343                    return Err(format!("path index disagrees for {address}"));
344                }
345            }
346        }
347
348        for address in self.variables.keys() {
349            let instance = self
350                .instances
351                .get(&address.instance_id)
352                .ok_or_else(|| format!("runtime variable {address} references missing instance"))?;
353            if instance.state_addresses.binary_search(address).is_err() {
354                return Err(format!(
355                    "runtime variable {address} is not indexed by instance"
356                ));
357            }
358        }
359
360        Ok(())
361    }
362
363    #[cfg(feature = "host-runtime")]
364    pub(crate) fn take_initial_state(
365        &mut self,
366    ) -> Vec<celox_design::InitialStateValue<AbsoluteAddr>> {
367        std::mem::take(&mut self.semantic.initial_state)
368    }
369
370    #[cfg(feature = "host-runtime")]
371    pub(crate) fn restore_initial_state(
372        &mut self,
373        initial_state: Vec<celox_design::InitialStateValue<AbsoluteAddr>>,
374    ) {
375        self.semantic.initial_state = initial_state;
376    }
377}
378
379/// Error returned by [`RuntimeProgram::get_addr`] when a path-based variable lookup fails.
380#[derive(Debug, Clone, thiserror::Error)]
381pub enum AddrLookupError {
382    #[error("Instance not found: {path}")]
383    InstanceNotFound { path: String },
384    #[error("Variable not found: {path}")]
385    VariableNotFound { path: String },
386    #[error("Ambiguous variable path: {path} — multiple variables share this path")]
387    AmbiguousPath { path: String },
388}
389
390/// Internal consistency failure while consuming the frontend projection into
391/// the canonical runtime design.
392#[derive(Debug, Clone, thiserror::Error)]
393pub(crate) enum DesignProjectionError {
394    #[error("state object count differs: design={design} frontend={frontend}")]
395    StateObjectCount { design: usize, frontend: usize },
396    #[error("missing state projection for {source_address}")]
397    MissingStateProjection { source_address: SourceAddr },
398    #[error("missing flattened state object {address}")]
399    MissingStateObject { address: AbsoluteAddr },
400    #[error("metadata differs for flattened state object {address}")]
401    MetadataMismatch { address: AbsoluteAddr },
402    #[error("invalid normalized runtime design: {reason}")]
403    InvalidRuntimeDesign { reason: String },
404}
405
406#[cfg(feature = "host-runtime")]
407pub type InitialMemoryWriteRun = InitialStateWriteRun;
408#[cfg(feature = "host-runtime")]
409pub type InitialMemoryData = InitialStateData;
410pub type RuntimeErrorInfo<Addr = AbsoluteAddr> = celox_design::RuntimeErrorInfo<Addr>;
411
412/// Source-independent metadata retained while a compiled design is executing.
413///
414/// Compiler-only SIR and layout requirements are deliberately absent. A
415/// backend can therefore discard the compiler artifact after code generation.
416#[derive(Clone)]
417pub struct RuntimeProgram {
418    pub design: RuntimeDesign,
419    pub runtime_schema: RuntimeSchema<AbsoluteAddr>,
420    pub testbench: Option<TestbenchProgram<AbsoluteAddr>>,
421}
422
423/// Lowered SIR whose backend-independent optimization pipeline has not run.
424#[derive(Clone, Debug)]
425pub struct UnoptimizedSir {
426    pub sir: SirProgram,
427    pub layout_requirements: celox_state_layout::LayoutRequirements<AbsoluteAddr>,
428    pub runtime: RuntimeProgram,
429}
430
431impl UnoptimizedSir {
432    pub(crate) fn new(sir: SirProgram, runtime: RuntimeProgram) -> Self {
433        Self {
434            sir,
435            layout_requirements: Default::default(),
436            runtime,
437        }
438    }
439
440    pub(crate) fn into_optimized(self) -> OptimizedSir {
441        OptimizedSir::new(self.sir, self.runtime, self.layout_requirements)
442    }
443}
444
445impl Deref for UnoptimizedSir {
446    type Target = RuntimeProgram;
447
448    fn deref(&self) -> &Self::Target {
449        &self.runtime
450    }
451}
452
453/// A pre-layout compiler artifact whose SIR optimization pipeline has
454/// completed successfully.
455///
456/// Construction is restricted to the compiler driver. Physical layout can
457/// only be finalized from this phase, preventing unoptimized SIR from
458/// accidentally entering a backend.
459#[derive(Clone, Debug)]
460pub struct OptimizedSir {
461    pub sir: SirProgram,
462    pub layout_requirements: celox_state_layout::LayoutRequirements<AbsoluteAddr>,
463    pub(crate) runtime: RuntimeProgram,
464}
465
466impl OptimizedSir {
467    pub(crate) fn new(
468        sir: SirProgram,
469        runtime: RuntimeProgram,
470        layout_requirements: celox_state_layout::LayoutRequirements<AbsoluteAddr>,
471    ) -> Self {
472        Self {
473            sir,
474            layout_requirements,
475            runtime,
476        }
477    }
478
479    #[cfg(all(
480        feature = "host-runtime",
481        any(
482            target_arch = "x86_64",
483            feature = "arm64-codegen",
484            target_arch = "aarch64"
485        )
486    ))]
487    pub(crate) fn into_runtime(self) -> RuntimeProgram {
488        self.runtime
489    }
490}
491
492impl Deref for OptimizedSir {
493    type Target = RuntimeProgram;
494
495    fn deref(&self) -> &Self::Target {
496        &self.runtime
497    }
498}
499
500/// Optimized SIR whose physical state layout has been finalized.
501///
502/// Backend code generation accepts this artifact instead of a bare SIR value,
503/// making it impossible to enter code generation before layout construction.
504#[derive(Clone, Debug)]
505pub struct LaidOutProgram {
506    pub sir: SirProgram,
507    pub(crate) runtime: RuntimeProgram,
508    layout: crate::backend::MemoryLayout,
509}
510
511impl LaidOutProgram {
512    pub fn layout(&self) -> &crate::backend::MemoryLayout {
513        &self.layout
514    }
515
516    pub fn runtime(&self) -> &RuntimeProgram {
517        &self.runtime
518    }
519
520    pub fn into_runtime(self) -> RuntimeProgram {
521        self.runtime
522    }
523}
524
525impl Deref for LaidOutProgram {
526    type Target = RuntimeProgram;
527
528    fn deref(&self) -> &Self::Target {
529        &self.runtime
530    }
531}
532
533impl fmt::Debug for RuntimeProgram {
534    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
535        f.debug_struct("RuntimeProgram")
536            .field("num_events", &self.design.events.len())
537            .finish_non_exhaustive()
538    }
539}
540
541impl OptimizedSir {
542    /// Finalize the physical state layout and consume the optimized program.
543    pub fn into_laid_out(self, four_state: bool) -> LaidOutProgram {
544        self.into_laid_out_with_mode(
545            four_state,
546            crate::backend::memory_layout::MemoryLayoutMode::Packed,
547        )
548    }
549
550    pub fn into_laid_out_with_mode(
551        self,
552        four_state: bool,
553        mode: crate::backend::memory_layout::MemoryLayoutMode,
554    ) -> LaidOutProgram {
555        let mut program = self;
556        if !program.runtime_schema.comb_observers.is_empty()
557            && !program.layout_requirements.is_empty()
558        {
559            let observed_written: crate::HashSet<AbsoluteAddr> = program
560                .runtime_schema
561                .comb_observers
562                .iter()
563                .flat_map(|observer| observer.written_inputs.iter().copied())
564                .collect();
565            program
566                .layout_requirements
567                .state_aliases_mut()
568                .retain(|alias_addr, _| !observed_written.contains(alias_addr));
569            program
570                .layout_requirements
571                .state_aliases_mut()
572                .retain(|alias_addr, _| {
573                    !comb_capture_enable_needs_unaliased_old_value(
574                        &program.sir.eval_comb,
575                        *alias_addr,
576                    )
577                });
578        }
579        crate::optimizer::sir::retain_final_identity_aliases(&mut program, four_state);
580        let layout = crate::backend::MemoryLayout::build(&program, four_state, mode);
581
582        // Remove identity Stores for aliases validated by the layout
583        if !program.layout_requirements.is_empty() {
584            let aliased: crate::HashMap<AbsoluteAddr, AbsoluteAddr> = program
585                .layout_requirements
586                .state_aliases()
587                .iter()
588                .filter(|(alias_addr, canonical_addr)| {
589                    layout
590                        .offsets
591                        .get(alias_addr)
592                        .zip(layout.offsets.get(canonical_addr))
593                        .is_some_and(|(a, c)| a == c)
594                })
595                .map(|(&alias, &canonical)| (alias, canonical))
596                .collect();
597            if !aliased.is_empty() {
598                crate::optimizer::sir::remove_final_identity_alias_stores(
599                    &mut program,
600                    &aliased,
601                    four_state,
602                );
603            }
604        }
605        rebuild_rtl_writes(&mut program);
606        program.layout_requirements.clear();
607        let OptimizedSir {
608            sir,
609            runtime,
610            layout_requirements,
611        } = program;
612        debug_assert!(layout_requirements.is_empty());
613        LaidOutProgram {
614            sir,
615            runtime,
616            layout,
617        }
618    }
619}
620
621fn rebuild_rtl_writes(program: &mut OptimizedSir) {
622    let mut rtl_writes = crate::HashSet::default();
623    for unit in program
624        .sir
625        .eval_comb
626        .iter()
627        .chain(program.sir.eval_apply_ffs.values().flatten())
628        .chain(program.sir.eval_comb_apply_ffs.values().flatten())
629        .chain(program.sir.eval_only_ffs.values().flatten())
630        .chain(program.sir.apply_ffs.values().flatten())
631    {
632        for block in unit.blocks.values() {
633            for instruction in &block.instructions {
634                let (address, offset, width) = match instruction {
635                    SIRInstruction::Store(address, offset, width, ..)
636                    | SIRInstruction::Commit(_, address, offset, width, _) => {
637                        (address.absolute_addr(), offset, *width)
638                    }
639                    _ => continue,
640                };
641                let access = offset
642                    .constant_bit_offset()
643                    .and_then(|lsb| {
644                        width
645                            .checked_sub(1)
646                            .and_then(|tail| lsb.checked_add(tail))
647                            .map(|msb| BitAccess::new(lsb, msb))
648                    })
649                    .or_else(|| {
650                        program
651                            .runtime
652                            .design
653                            .state_objects
654                            .get(&address)
655                            .and_then(|object| object.width.checked_sub(1))
656                            .map(|msb| BitAccess::new(0, msb))
657                    });
658                if let Some(access) = access {
659                    rtl_writes.insert(VarAtomBase {
660                        id: address,
661                        access,
662                    });
663                }
664            }
665        }
666    }
667    program.runtime.runtime_schema.rtl_writes = rtl_writes;
668}
669
670impl RuntimeProgram {
671    #[cfg(all(
672        feature = "host-runtime",
673        any(
674            target_arch = "x86_64",
675            feature = "arm64-codegen",
676            target_arch = "aarch64"
677        )
678    ))]
679    pub(crate) fn build_design_reflection(
680        &self,
681        layout: &crate::backend::MemoryLayout,
682    ) -> DesignReflection {
683        struct ScopeSource {
684            instance_id: InstanceId,
685            name: String,
686            full_name: String,
687            parent_name: Option<String>,
688            module_name: String,
689        }
690
691        let root_name = self
692            .design
693            .root_instance()
694            .expect("top-level instance exists")
695            .module_name
696            .clone();
697
698        let mut scope_sources = self
699            .design
700            .instances()
701            .map(|instance| {
702                let segments = &instance.display_path;
703                let name = segments
704                    .last()
705                    .cloned()
706                    .unwrap_or_else(|| root_name.clone());
707                let full_name = if segments.is_empty() {
708                    root_name.clone()
709                } else {
710                    format!("{root_name}.{}", segments.join("."))
711                };
712                let parent_name = (!segments.is_empty()).then(|| {
713                    if segments.len() == 1 {
714                        root_name.clone()
715                    } else {
716                        format!("{root_name}.{}", segments[..segments.len() - 1].join("."))
717                    }
718                });
719                ScopeSource {
720                    instance_id: instance.id,
721                    name,
722                    full_name,
723                    parent_name,
724                    module_name: instance.module_name.clone(),
725                }
726            })
727            .collect::<Vec<_>>();
728        scope_sources.sort_by(|left, right| left.full_name.cmp(&right.full_name));
729
730        let scope_ids = scope_sources
731            .iter()
732            .enumerate()
733            .map(|(index, scope)| {
734                (
735                    scope.full_name.clone(),
736                    ReflectionScopeId(u32::try_from(index).expect("scope count exceeds u32")),
737                )
738            })
739            .collect::<HashMap<_, _>>();
740        let instance_scopes = scope_sources
741            .iter()
742            .enumerate()
743            .map(|(index, scope)| {
744                (
745                    scope.instance_id,
746                    ReflectionScopeId(u32::try_from(index).expect("scope count exceeds u32")),
747                )
748            })
749            .collect::<HashMap<_, _>>();
750        let mut scopes = scope_sources
751            .iter()
752            .map(|scope| ReflectionScope {
753                name: scope.name.clone(),
754                full_name: scope.full_name.clone(),
755                module_name: scope.module_name.clone(),
756                parent: scope.parent_name.as_ref().map(|parent| scope_ids[parent]),
757                children: Vec::new(),
758                signals: Vec::new(),
759            })
760            .collect::<Vec<_>>();
761        let child_parents = scopes
762            .iter()
763            .enumerate()
764            .filter_map(|(index, scope)| {
765                scope.parent.map(|parent| {
766                    (
767                        parent,
768                        ReflectionScopeId(u32::try_from(index).expect("scope count exceeds u32")),
769                    )
770                })
771            })
772            .collect::<Vec<_>>();
773        for (parent, child) in child_parents {
774            scopes[parent.0 as usize].children.push(child);
775        }
776
777        let mut signals = Vec::new();
778        for scope in &scope_sources {
779            let instance = self.design.instance(scope.instance_id).unwrap();
780            for state_address in instance.state_addresses() {
781                let variable = self.design.variable(state_address).unwrap();
782                if matches!(
783                    variable.var_kind,
784                    VariableKind::Parameter | VariableKind::Constant
785                ) {
786                    continue;
787                }
788                if instance.path_index.get(&variable.path) != Some(&Some(*state_address)) {
789                    continue;
790                }
791                let metadata = &self.design.state_objects[state_address];
792                let name = variable.path.join(".");
793                let array_layout =
794                    layout
795                        .unpacked_arrays
796                        .get(state_address)
797                        .map(|array| SignalArrayLayout {
798                            element_width: array.element_width,
799                            element_count: array.element_count,
800                            element_stride: array.element_stride,
801                            plane_size: array.plane_size,
802                        });
803                let direction = match variable.var_kind {
804                    VariableKind::Input => SignalDirection::Input,
805                    VariableKind::Output => SignalDirection::Output,
806                    VariableKind::Inout => SignalDirection::Inout,
807                    _ => SignalDirection::Internal,
808                };
809                signals.push(ReflectionSignal {
810                    full_name: format!("{}.{}", scope.full_name, name),
811                    name,
812                    parent: instance_scopes[&scope.instance_id],
813                    state_address: *state_address,
814                    signal: SignalRef {
815                        offset: layout.offsets[state_address],
816                        width: layout.widths[state_address],
817                        is_4state: layout.is_4states[state_address],
818                        array_layout,
819                    },
820                    direction,
821                    domain_kind: metadata.kind,
822                    signed: variable.signed,
823                    packed_dims: variable.packed_dims.clone(),
824                    unpacked_dims: metadata.array_dims.clone(),
825                    type_kind: metadata.type_kind,
826                });
827            }
828        }
829        signals.sort_by(|left, right| left.full_name.cmp(&right.full_name));
830        for (index, signal) in signals.iter().enumerate() {
831            scopes[signal.parent.0 as usize]
832                .signals
833                .push(ReflectionSignalId(
834                    u32::try_from(index).expect("signal count exceeds u32"),
835                ));
836        }
837        let reflection = DesignReflection::new(scopes, signals);
838        debug_assert!(reflection.validate().is_ok());
839        reflection
840    }
841
842    pub(crate) fn from_scheduled(
843        scheduled: celox_frontend_core::ScheduledRtl,
844    ) -> Result<(SirProgram, Self), DesignProjectionError> {
845        let design = RuntimeDesign::from_projection(scheduled.design, scheduled.frontend_lookup)?;
846        Ok((
847            scheduled.sir,
848            Self {
849                design,
850                runtime_schema: scheduled.runtime_schema,
851                testbench: None,
852            },
853        ))
854    }
855
856    pub fn get_addr(
857        &self,
858        instance_path: &[(&str, usize)],
859        var_path: &[&str],
860    ) -> Result<AbsoluteAddr, AddrLookupError> {
861        let instance_path: Vec<(String, usize)> = instance_path
862            .iter()
863            .map(|(name, index)| ((*name).to_string(), *index))
864            .collect();
865        let instance = self
866            .design
867            .instance_at_path(&InstancePath(instance_path.clone()))
868            .ok_or_else(|| AddrLookupError::InstanceNotFound {
869                path: instance_path
870                    .iter()
871                    .map(|(s, i)| format!("{}[{}]", s, i))
872                    .collect::<Vec<_>>()
873                    .join("."),
874            })?;
875        let target_path = var_path
876            .iter()
877            .map(|segment| (*segment).to_string())
878            .collect::<Vec<_>>();
879        let path_str = var_path.join(".");
880        let entry = instance.path_index.get(&target_path).ok_or_else(|| {
881            AddrLookupError::VariableNotFound {
882                path: path_str.clone(),
883            }
884        })?;
885        entry
886            .as_ref()
887            .copied()
888            .ok_or(AddrLookupError::AmbiguousPath { path: path_str })
889    }
890
891    pub fn get_path(&self, addr: &AbsoluteAddr) -> String {
892        self.design.get_path(addr)
893    }
894
895    pub fn get_variable_info(&self, addr: &AbsoluteAddr) -> Option<VariableInfo> {
896        self.design.variable_info(addr)
897    }
898
899    pub fn num_events(&self) -> usize {
900        self.design.events.len()
901    }
902}
903
904impl OptimizedSir {
905    /// Collect the set of `AbsoluteAddr` values that are accessed in the working
906    /// region (region != STABLE). These are the only variables that need working
907    /// region space.
908    pub fn collect_working_region_addrs(&self) -> crate::HashSet<AbsoluteAddr> {
909        let mut addrs = crate::HashSet::default();
910
911        let scan_units =
912            |units: &HashMap<AbsoluteAddr, Vec<ExecutionUnit<RegionedAbsoluteAddr>>>,
913             addrs: &mut crate::HashSet<AbsoluteAddr>| {
914                for eu_list in units.values() {
915                    for eu in eu_list {
916                        for block in eu.blocks.values() {
917                            for inst in &block.instructions {
918                                match inst {
919                                    SIRInstruction::Store(addr, _, _, _, _, _)
920                                        if addr.region == WORKING_REGION =>
921                                    {
922                                        addrs.insert(addr.absolute_addr());
923                                    }
924                                    SIRInstruction::Commit(src, dst, _, _, _) => {
925                                        if src.region == WORKING_REGION {
926                                            addrs.insert(src.absolute_addr());
927                                        }
928                                        if dst.region == WORKING_REGION {
929                                            addrs.insert(dst.absolute_addr());
930                                        }
931                                    }
932                                    _ => {}
933                                }
934                            }
935                        }
936                    }
937                }
938            };
939
940        scan_units(&self.sir.eval_apply_ffs, &mut addrs);
941        scan_units(&self.sir.eval_comb_apply_ffs, &mut addrs);
942        scan_units(&self.sir.eval_only_ffs, &mut addrs);
943        scan_units(&self.sir.apply_ffs, &mut addrs);
944
945        addrs
946    }
947
948    pub fn collect_sparse_working_region_addrs(&self) -> crate::HashSet<AbsoluteAddr> {
949        let mut addrs = crate::HashSet::default();
950        for units in self
951            .sir
952            .eval_apply_ffs
953            .values()
954            .chain(self.sir.eval_comb_apply_ffs.values())
955            .chain(self.sir.eval_only_ffs.values())
956        {
957            for eu in units {
958                for block in eu.blocks.values() {
959                    for inst in &block.instructions {
960                        if let SIRInstruction::Store(addr, _, _, _, _, _) = inst
961                            && addr.region == SPARSE_WORKING_REGION
962                        {
963                            addrs.insert(addr.absolute_addr());
964                        }
965                    }
966                }
967            }
968        }
969        addrs
970    }
971}
972
973fn comb_capture_enable_needs_unaliased_old_value(
974    units: &[ExecutionUnit<RegionedAbsoluteAddr>],
975    alias_addr: AbsoluteAddr,
976) -> bool {
977    for eu in units {
978        for block in eu.blocks.values() {
979            let mut last_store = None;
980            for inst in &block.instructions {
981                match inst {
982                    SIRInstruction::Store(addr, _, _, _, _, comb_capture_sites) => {
983                        let abs = addr.absolute_addr();
984                        if abs == alias_addr && !comb_capture_sites.is_empty() {
985                            return true;
986                        }
987                        last_store = Some(abs);
988                    }
989                    SIRInstruction::CombCaptureEnableIfChanged { sites, .. } => {
990                        if !sites.is_empty() && last_store == Some(alias_addr) {
991                            return true;
992                        }
993                        last_store = None;
994                    }
995                    _ => {
996                        last_store = None;
997                    }
998                }
999            }
1000        }
1001    }
1002    false
1003}
1004
1005pub(crate) mod verify {
1006    pub(crate) use celox_sir::verify::*;
1007}
1008pub use celox_slt::{GlueAddrBase, GlueBlockBase};
1009
1010pub use celox_frontend_core::TraceSimModule as SimModule;
1011#[cfg(all(
1012    feature = "host-runtime",
1013    any(
1014        target_arch = "x86_64",
1015        feature = "arm64-codegen",
1016        target_arch = "aarch64"
1017    )
1018))]
1019pub(crate) use celox_runtime::SignalArrayLayout;
1020pub use celox_runtime::SignalRef;
1021
1022#[cfg(test)]
1023mod tests {
1024    use super::*;
1025
1026    #[test]
1027    fn exact_zero_analysis_collapses_repeated_concat_dependencies() {
1028        let zero = RegisterId(0);
1029        let wide_zero = RegisterId(1);
1030        let sliced_zero = RegisterId(2);
1031        let nonzero = RegisterId(3);
1032        let mixed = RegisterId(4);
1033        let eu: ExecutionUnit<()> = ExecutionUnit {
1034            entry_block_id: BlockId(0),
1035            blocks: [(
1036                BlockId(0),
1037                BasicBlock {
1038                    id: BlockId(0),
1039                    params: vec![],
1040                    instructions: vec![
1041                        SIRInstruction::Imm(zero, SIRValue::new(0u8)),
1042                        SIRInstruction::Concat(wide_zero, vec![zero; 4096]),
1043                        SIRInstruction::Slice(sliced_zero, wide_zero, 0, 64),
1044                        SIRInstruction::Imm(nonzero, SIRValue::new(1u8)),
1045                        SIRInstruction::Concat(mixed, vec![zero, nonzero]),
1046                    ],
1047                    terminator: SIRTerminator::Return,
1048                },
1049            )]
1050            .into_iter()
1051            .collect(),
1052            register_map: HashMap::default(),
1053        };
1054
1055        let zeros = collect_exact_zero_registers(&eu, [sliced_zero, mixed]);
1056        assert!(zeros.contains(&zero));
1057        assert!(zeros.contains(&wide_zero));
1058        assert!(zeros.contains(&sliced_zero));
1059        assert!(!zeros.contains(&nonzero));
1060        assert!(!zeros.contains(&mixed));
1061    }
1062
1063    #[test]
1064    fn test_sirvalue_display() {
1065        let val = SIRValue::new(42u64);
1066        let display = format!("{}", val);
1067        assert!(display.contains("SIRValue"));
1068        assert!(display.contains("0x2a")); // 42 in hex
1069    }
1070
1071    #[test]
1072    fn test_absoluteaddr_display() {
1073        let addr = AbsoluteAddr {
1074            instance_id: InstanceId(0),
1075            var_id: celox_design::StateObjectId(0),
1076        };
1077        let display = format!("{}", addr);
1078        assert!(display.contains("AbsoluteAddr"));
1079        assert!(display.contains("inst0"));
1080        assert!(display.contains("state0"));
1081    }
1082
1083    #[test]
1084    fn test_glueaddr_display() {
1085        let parent_addr =
1086            celox_frontend_veryl::GlueAddr::Parent(veryl_analyzer::ir::VarId::default());
1087        let parent_display = format!("{}", parent_addr);
1088        assert!(parent_display.contains("GlueAddr::Parent"));
1089        assert!(parent_display.contains("var0"));
1090
1091        let child_addr =
1092            celox_frontend_veryl::GlueAddr::Child(veryl_analyzer::ir::VarId::default());
1093        let child_display = format!("{}", child_addr);
1094        assert!(child_display.contains("GlueAddr::Child"));
1095        assert!(child_display.contains("var0"));
1096    }
1097
1098    #[test]
1099    fn test_instanceid_display() {
1100        let id = InstanceId(42);
1101        let display = format!("{}", id);
1102        assert_eq!(display, "inst42");
1103    }
1104
1105    #[test]
1106    fn test_binaryop_display() {
1107        assert_eq!(format!("{}", BinaryOp::Add), "Add");
1108        assert_eq!(format!("{}", BinaryOp::Sub), "Sub");
1109        assert_eq!(format!("{}", BinaryOp::Mul), "Mul");
1110        assert_eq!(format!("{}", BinaryOp::Xor), "Xor");
1111    }
1112
1113    #[test]
1114    fn test_unaryop_display() {
1115        assert_eq!(format!("{}", UnaryOp::Minus), "Minus");
1116        assert_eq!(format!("{}", UnaryOp::LogicNot), "LogicNot");
1117        assert_eq!(format!("{}", UnaryOp::BitNot), "BitNot");
1118        assert_eq!(format!("{}", UnaryOp::PopCount), "PopCount");
1119        assert_eq!(
1120            format!("{}", UnaryOp::CountLeadingZeros),
1121            "CountLeadingZeros"
1122        );
1123        assert_eq!(
1124            format!("{}", UnaryOp::CountTrailingZeros),
1125            "CountTrailingZeros"
1126        );
1127    }
1128
1129    #[test]
1130    fn bit_count_result_width_represents_operand_width() {
1131        for (operand_width, expected) in [
1132            (0, 0),
1133            (1, 1),
1134            (2, 2),
1135            (3, 2),
1136            (8, 4),
1137            (usize::MAX, usize::BITS as usize),
1138        ] {
1139            for op in [
1140                UnaryOp::PopCount,
1141                UnaryOp::CountLeadingZeros,
1142                UnaryOp::CountTrailingZeros,
1143            ] {
1144                assert_eq!(op.result_width(operand_width), expected, "{op}");
1145            }
1146        }
1147    }
1148
1149    #[test]
1150    fn bit_count_unary_ops_roundtrip_through_serde() {
1151        for op in [
1152            UnaryOp::PopCount,
1153            UnaryOp::CountLeadingZeros,
1154            UnaryOp::CountTrailingZeros,
1155        ] {
1156            let encoded = serde_json::to_string(&op).unwrap();
1157            let decoded: UnaryOp = serde_json::from_str(&encoded).unwrap();
1158            assert_eq!(decoded, op);
1159        }
1160    }
1161
1162    #[test]
1163    fn test_sirinstruction_display() {
1164        // Test Imm instruction
1165        let imm: SIRInstruction<i32> = SIRInstruction::Imm(RegisterId(0), SIRValue::new(42u64));
1166        let imm_display = format!("{}", imm);
1167        assert!(imm_display.contains("r0"));
1168        assert!(imm_display.contains("SIRValue"));
1169
1170        // Test Binary instruction
1171        let binary: SIRInstruction<i32> =
1172            SIRInstruction::Binary(RegisterId(0), RegisterId(1), BinaryOp::Add, RegisterId(2));
1173        let binary_display = format!("{}", binary);
1174        assert!(binary_display.contains("r0"));
1175        assert!(binary_display.contains("r1"));
1176        assert!(binary_display.contains("r2"));
1177        assert!(binary_display.contains("Add"));
1178
1179        // Test Unary instruction
1180        let unary: SIRInstruction<i32> =
1181            SIRInstruction::Unary(RegisterId(0), UnaryOp::Minus, RegisterId(1));
1182        let unary_display = format!("{}", unary);
1183        assert!(unary_display.contains("r0"));
1184        assert!(unary_display.contains("r1"));
1185        assert!(unary_display.contains("Minus"));
1186    }
1187
1188    #[test]
1189    fn test_sirterminator_display() {
1190        // Test Jump
1191        let jump = SIRTerminator::Jump(BlockId(1), vec![RegisterId(0), RegisterId(1)]);
1192        let jump_display = format!("{}", jump);
1193        assert!(jump_display.contains("Jump"));
1194        assert!(jump_display.contains("b1"));
1195
1196        // Test Return
1197        let ret = SIRTerminator::Return;
1198        let ret_display = format!("{}", ret);
1199        assert_eq!(ret_display, "Return");
1200
1201        // Test Branch
1202        let branch = SIRTerminator::Branch {
1203            cond: RegisterId(0),
1204            true_block: (BlockId(1), vec![]),
1205            false_block: (BlockId(2), vec![]),
1206        };
1207        let branch_display = format!("{}", branch);
1208        assert!(branch_display.contains("Branch"));
1209        assert!(branch_display.contains("b1"));
1210        assert!(branch_display.contains("b2"));
1211    }
1212
1213    #[test]
1214    fn test_basicblock_display() {
1215        let _block: BasicBlock<i32> = BasicBlock {
1216            id: BlockId(0),
1217            params: vec![RegisterId(0), RegisterId(1)],
1218            instructions: vec![
1219                SIRInstruction::Imm(RegisterId(2), SIRValue::new(42u64)),
1220                SIRInstruction::Binary(RegisterId(3), RegisterId(0), BinaryOp::Add, RegisterId(2)),
1221            ],
1222            terminator: SIRTerminator::Return,
1223        };
1224
1225        let block_display = format!("{}", _block);
1226        assert!(block_display.contains("b0:"));
1227        assert!(block_display.contains("params:"));
1228        assert!(block_display.contains("r0"));
1229        assert!(block_display.contains("r1"));
1230        assert!(block_display.contains("Add"));
1231        assert!(block_display.contains("Return"));
1232    }
1233
1234    #[test]
1235    fn single_predecessor_inlining_rewrites_dominated_parameter_uses() {
1236        let mut eu: ExecutionUnit<()> = ExecutionUnit {
1237            entry_block_id: BlockId(0),
1238            blocks: [
1239                BasicBlock {
1240                    id: BlockId(0),
1241                    params: vec![RegisterId(0)],
1242                    instructions: Vec::new(),
1243                    terminator: SIRTerminator::Jump(BlockId(1), vec![RegisterId(0)]),
1244                },
1245                BasicBlock {
1246                    id: BlockId(1),
1247                    params: vec![RegisterId(1)],
1248                    instructions: Vec::new(),
1249                    terminator: SIRTerminator::Jump(BlockId(2), Vec::new()),
1250                },
1251                BasicBlock {
1252                    id: BlockId(2),
1253                    params: Vec::new(),
1254                    instructions: vec![SIRInstruction::Unary(
1255                        RegisterId(2),
1256                        UnaryOp::Ident,
1257                        RegisterId(1),
1258                    )],
1259                    terminator: SIRTerminator::Return,
1260                },
1261            ]
1262            .into_iter()
1263            .map(|block| (block.id, block))
1264            .collect(),
1265            register_map: (0..3)
1266                .map(|register| {
1267                    (
1268                        RegisterId(register),
1269                        RegisterType::Bit {
1270                            width: 8,
1271                            signed: false,
1272                        },
1273                    )
1274                })
1275                .collect(),
1276        };
1277        eu.verify_result().unwrap();
1278
1279        assert!(inline_single_predecessor_jumps(&mut eu).unwrap());
1280        eu.verify_result().unwrap();
1281        assert_eq!(eu.blocks.len(), 1);
1282        assert!(matches!(
1283            eu.blocks[&BlockId(0)].instructions.as_slice(),
1284            [SIRInstruction::Unary(
1285                RegisterId(2),
1286                UnaryOp::Ident,
1287                RegisterId(0)
1288            )]
1289        ));
1290    }
1291
1292    #[test]
1293    fn single_predecessor_inlining_handles_deep_linear_cfg() {
1294        const BLOCK_COUNT: usize = 20_000;
1295
1296        let mut eu: ExecutionUnit<()> = ExecutionUnit {
1297            entry_block_id: BlockId(0),
1298            blocks: (0..BLOCK_COUNT)
1299                .map(|index| {
1300                    let id = BlockId(index);
1301                    let terminator = if index + 1 == BLOCK_COUNT {
1302                        SIRTerminator::Return
1303                    } else {
1304                        SIRTerminator::Jump(BlockId(index + 1), Vec::new())
1305                    };
1306                    (
1307                        id,
1308                        BasicBlock {
1309                            id,
1310                            params: Vec::new(),
1311                            instructions: Vec::new(),
1312                            terminator,
1313                        },
1314                    )
1315                })
1316                .collect(),
1317            register_map: crate::HashMap::default(),
1318        };
1319        eu.verify_result().unwrap();
1320
1321        assert!(inline_single_predecessor_jumps(&mut eu).unwrap());
1322        assert_eq!(eu.blocks.len(), 1);
1323        assert_eq!(eu.blocks[&BlockId(0)].terminator, SIRTerminator::Return);
1324        eu.verify_result().unwrap();
1325    }
1326}