Skip to main content

celox_design/
lib.rs

1//! Source-language-independent design identities and semantic vocabulary.
2
3use fxhash::{FxHashMap as HashMap, FxHashSet as HashSet};
4use num_bigint::BigUint;
5use serde::{Deserialize, Serialize};
6use std::{collections::BTreeSet, fmt};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9pub enum DomainKind {
10    ClockPosedge,
11    ClockNegedge,
12    ResetAsyncHigh,
13    ResetAsyncLow,
14    Other,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
18pub struct TriggerIdWithKind {
19    pub kind: DomainKind,
20    pub id: usize,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
24pub enum PortTypeKind {
25    Clock,
26    ResetAsyncHigh,
27    ResetAsyncLow,
28    ResetSyncHigh,
29    ResetSyncLow,
30    Logic,
31    Bit,
32    Other,
33}
34
35/// Source-independent metadata for one elaborated design variable.
36///
37/// Source IDs, source paths, and declaration syntax belong to the frontend and
38/// deliberately are not part of this type.
39#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
40pub struct VariableMetadata {
41    pub width: usize,
42    pub is_4state: bool,
43    pub kind: DomainKind,
44    pub type_kind: PortTypeKind,
45    /// Per-dimension sizes for array ports (for example, `[4]` for `logic<32>[4]`).
46    /// Empty means scalar.
47    pub array_dims: Vec<usize>,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
51pub struct TriggerSet<A> {
52    pub clock: A,
53    pub resets: Vec<A>,
54}
55
56#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
57pub enum RuntimeEventKind {
58    Display,
59    Write,
60    AssertContinue,
61    AssertFatal,
62    /// `$finish` executed by the design (IEEE 1800-2023 20.2).
63    Finish,
64}
65
66/// How a runtime event site sizes its formatted arguments.
67#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
68pub enum DisplaySizing {
69    /// The formatting of Veryl's simulator: radices at the argument's width,
70    /// decimals at their minimum, and C-style field widths that never
71    /// truncate.
72    #[default]
73    Veryl,
74    /// Automatic sizing and field widths of IEEE 1800-2023 21.2.1.2.
75    Ieee,
76}
77
78#[derive(Clone, Debug, Serialize, Deserialize)]
79pub struct RuntimeEventSite {
80    pub kind: RuntimeEventKind,
81    pub template: Option<String>,
82    #[serde(default)]
83    pub sizing: DisplaySizing,
84    /// Fully elaborated module-instance scope that emitted this event.
85    pub scope: Option<String>,
86    pub arg_widths: Vec<usize>,
87    pub arg_signed: Vec<bool>,
88    pub arg_is_string: Vec<bool>,
89}
90
91/// A function in the host's C ABI that the design calls, such as a
92/// SystemVerilog DPI-C import. `ExternCall` instructions index the design's
93/// table of these.
94#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
95pub struct ExternFunction {
96    /// The C symbol the function is linked by.
97    pub name: String,
98    /// The prototype every call of `name` must share (IEEE 1800-2023
99    /// 35.5.4).
100    pub signature: ExternSignature,
101}
102
103/// The prototype of an [`ExternFunction`].
104#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
105pub struct ExternSignature {
106    /// Whether the function is declared `pure`.
107    pub pure: bool,
108    /// The result, or `None` for a `void` function.
109    pub result: Option<ExternType>,
110    pub arguments: Vec<ExternType>,
111}
112
113/// A type an [`ExternFunction`] passes or returns.
114#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
115pub enum ExternType {
116    /// `bit`, passed as `svBit`.
117    Bit,
118    /// `logic`, passed as `svLogic`.
119    Logic,
120    /// `byte`, `shortint`, `int` or `longint`, passed as the C integer of
121    /// that width.
122    Integer { width: usize, signed: bool },
123}
124
125impl fmt::Display for ExternType {
126    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127        let (name, signed) = match *self {
128            ExternType::Bit => return f.write_str("bit"),
129            ExternType::Logic => return f.write_str("logic"),
130            ExternType::Integer { width: 8, signed } => ("byte", signed),
131            ExternType::Integer { width: 16, signed } => ("shortint", signed),
132            ExternType::Integer { width: 32, signed } => ("int", signed),
133            ExternType::Integer { width: 64, signed } => ("longint", signed),
134            ExternType::Integer { width, signed } => {
135                return write!(f, "{}{width}", if signed { "i" } else { "u" });
136            }
137        };
138        f.write_str(name)?;
139        if !signed {
140            f.write_str(" unsigned")?;
141        }
142        Ok(())
143    }
144}
145
146impl fmt::Display for ExternFunction {
147    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148        let signature = &self.signature;
149        if signature.pure {
150            f.write_str("pure ")?;
151        }
152        f.write_str("function ")?;
153        match signature.result {
154            Some(result) => write!(f, "{result}")?,
155            None => f.write_str("void")?,
156        }
157        write!(f, " {}(", self.name)?;
158        for (index, argument) in signature.arguments.iter().enumerate() {
159            if index > 0 {
160                f.write_str(", ")?;
161            }
162            write!(f, "input {argument}")?;
163        }
164        f.write_str(")")
165    }
166}
167
168/// Runtime activation recipe for one combinational event site.
169///
170/// Expression trees used to emit the event have already been lowered into
171/// SIR.  The runtime only retains the persistent-state ranges needed to detect
172/// whether the corresponding combinational process must be observed again.
173#[derive(Clone, Debug, Serialize, Deserialize)]
174pub struct RuntimeCombObserver<A> {
175    pub site_id: u32,
176    pub activation_group: u32,
177    pub sensitivity: Vec<VarAtomBase<A>>,
178    pub written_inputs: Vec<A>,
179}
180
181#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
182pub struct InitialStateWriteRun {
183    pub bit_offset: usize,
184    pub bit_width: usize,
185    pub value_bytes: Vec<u8>,
186    pub mask_bytes: Vec<u8>,
187}
188
189#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
190pub enum InitialStateData {
191    Packed {
192        value: BigUint,
193        mask: BigUint,
194        written_mask: BigUint,
195    },
196    Writes(Vec<InitialStateWriteRun>),
197}
198
199#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
200pub struct InitialStateValue<A> {
201    pub address: A,
202    pub data: InitialStateData,
203}
204
205#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
206pub struct RuntimeErrorInfo<A> {
207    pub message: String,
208    pub signals: Vec<A>,
209}
210
211/// How a process kernel returned control to the runtime, as stored in its
212/// [`ProcessSlots::status`] slot.
213#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
214pub enum ProcessStatus {
215    /// Resume after the number of time units stored in [`ProcessSlots::delay`].
216    Delay,
217    /// The process ran to the end of its body.
218    Done,
219    /// The process requested the end of the simulation.
220    Finish,
221}
222
223impl ProcessStatus {
224    /// Encoding in the status slot. Zero is never written, so a kernel that
225    /// returned without reporting a status is detected.
226    pub const fn code(self) -> u8 {
227        match self {
228            Self::Delay => 1,
229            Self::Done => 2,
230            Self::Finish => 3,
231        }
232    }
233
234    pub const fn from_code(code: u8) -> Option<Self> {
235        match code {
236            1 => Some(Self::Delay),
237            2 => Some(Self::Done),
238            3 => Some(Self::Finish),
239            _ => None,
240        }
241    }
242}
243
244/// State through which one process kernel exchanges control with the runtime.
245///
246/// A process kernel is a resumable function. On entry it reads `resume` to
247/// find where it stopped; before returning it stores the next resume point,
248/// a [`ProcessStatus`] code in `status` and, for a delay, the number of time
249/// units in `delay`. All three are ordinary two-state state objects, so
250/// checkpoints capture a suspended process.
251#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
252pub struct ProcessSlots<A> {
253    pub resume: A,
254    pub status: A,
255    pub delay: A,
256}
257
258impl<A> ProcessSlots<A> {
259    pub fn map<B>(self, mut map: impl FnMut(A) -> B) -> ProcessSlots<B> {
260        ProcessSlots {
261            resume: map(self.resume),
262            status: map(self.status),
263            delay: map(self.delay),
264        }
265    }
266
267    pub fn iter(&self) -> impl Iterator<Item = &A> {
268        [&self.resume, &self.status, &self.delay].into_iter()
269    }
270}
271
272/// Width of [`ProcessSlots::status`].
273pub const PROCESS_STATUS_WIDTH: usize = 8;
274/// Width of [`ProcessSlots::delay`].
275pub const PROCESS_DELAY_WIDTH: usize = 64;
276
277/// Controls which combinational stores dead store elimination preserves.
278///
279/// The policy is part of the runtime contract: host-side handles resolve
280/// only to state the policy guarantees to keep, so a design that happens to
281/// read a signal today cannot make a testbench depend on it.
282#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
283pub enum DeadStorePolicy {
284    /// Keep all stores (no dead store elimination). Default for user-facing builds.
285    #[default]
286    Off,
287    /// Eliminate stores except those explicitly marked live via `live_signal()`
288    /// and those loaded by execution units.
289    PreserveListedSignals,
290    /// Eliminate stores except those to top-module ports and those loaded by EUs.
291    PreserveTopPorts,
292    /// Eliminate stores except those to ports of *all* instances and those loaded by EUs.
293    PreserveAllPorts,
294}
295
296/// Source-independent runtime diagnostics and observable event descriptions.
297#[derive(Clone, Debug)]
298pub struct RuntimeSchema<A> {
299    pub runtime_errors: HashMap<i64, RuntimeErrorInfo<A>>,
300    pub runtime_event_sites: Vec<RuntimeEventSite>,
301    /// Extern functions called by `ExternCall`, by index.
302    pub extern_functions: Vec<ExternFunction>,
303    pub comb_observers: Vec<RuntimeCombObserver<A>>,
304    /// Persistent state read directly by host-side testbench execution. These
305    /// are optimization roots even when no SIR instruction loads them.
306    pub testbench_read_roots: HashSet<A>,
307    /// Bit ranges written by RTL execution units. External component outputs
308    /// may not overlap these ranges because that would create multiple drivers.
309    pub rtl_writes: HashSet<VarAtomBase<A>>,
310    /// State objects written by combinational logic. Their values are
311    /// derived from the other state, so state files recompute rather than
312    /// compare them.
313    pub comb_writes: HashSet<A>,
314    /// Dead store elimination policy the program was compiled with. Together
315    /// with `live_signals` and the design it decides which combinationally
316    /// written state the host may observe.
317    pub dead_store_policy: DeadStorePolicy,
318    /// State explicitly declared observable for dead store elimination.
319    pub live_signals: HashSet<A>,
320    /// Control slots of the process kernels, in kernel order.
321    pub processes: Vec<ProcessSlots<A>>,
322}
323
324impl<A> Default for RuntimeSchema<A> {
325    fn default() -> Self {
326        Self {
327            runtime_errors: HashMap::default(),
328            runtime_event_sites: Vec::new(),
329            extern_functions: Vec::new(),
330            comb_observers: Vec::new(),
331            testbench_read_roots: HashSet::default(),
332            rtl_writes: HashSet::default(),
333            comb_writes: HashSet::default(),
334            dead_store_policy: DeadStorePolicy::Off,
335            live_signals: HashSet::default(),
336            processes: Vec::new(),
337        }
338    }
339}
340
341/// Source-independent event-domain topology after elaboration.
342///
343/// Addresses are already flattened. Source paths and frontend IDs used only
344/// for diagnostics or lookup deliberately live outside this structure.
345#[derive(Clone, Debug, Serialize, Deserialize)]
346#[serde(bound(
347    serialize = "A: Serialize + Eq + std::hash::Hash + Ord",
348    deserialize = "A: Deserialize<'de> + Eq + std::hash::Hash + Ord"
349))]
350pub struct EventTopology<A> {
351    /// Alias event address to the canonical event-domain address.
352    pub aliases: HashMap<A, A>,
353    /// Canonical event domains in evaluation order.
354    pub ordered_events: Vec<A>,
355    /// Canonical clocks whose value may be changed by another event domain.
356    pub cascaded_events: BTreeSet<A>,
357    /// Canonical asynchronous/synchronous reset to its canonical clock.
358    pub reset_clocks: HashMap<A, A>,
359}
360
361impl<A> Default for EventTopology<A> {
362    fn default() -> Self {
363        Self {
364            aliases: HashMap::default(),
365            ordered_events: Vec::new(),
366            cascaded_events: BTreeSet::new(),
367            reset_clocks: HashMap::default(),
368        }
369    }
370}
371
372impl<A: Copy + Eq + std::hash::Hash> EventTopology<A> {
373    pub fn canonical(&self, address: A) -> A {
374        self.aliases.get(&address).copied().unwrap_or(address)
375    }
376
377    pub fn len(&self) -> usize {
378        self.ordered_events.len()
379    }
380
381    pub fn is_empty(&self) -> bool {
382        self.ordered_events.is_empty()
383    }
384}
385
386/// Backend-neutral semantic design data after hierarchy flattening.
387///
388/// This is intentionally not a frontend lookup table: every state object is
389/// keyed by its flattened semantic address, and no source-language AST or path
390/// type is retained.
391#[derive(Clone, Debug, Serialize, Deserialize)]
392#[serde(bound(
393    serialize = "A: Serialize + Eq + std::hash::Hash + Ord",
394    deserialize = "A: Deserialize<'de> + Eq + std::hash::Hash + Ord"
395))]
396pub struct ElaboratedDesign<A> {
397    pub state_objects: HashMap<A, VariableMetadata>,
398    pub events: EventTopology<A>,
399    pub initial_state: Vec<InitialStateValue<A>>,
400}
401
402impl<A> Default for ElaboratedDesign<A> {
403    fn default() -> Self {
404        Self {
405            state_objects: HashMap::default(),
406            events: EventTopology::default(),
407            initial_state: Vec::new(),
408        }
409    }
410}
411
412impl<A: Copy + Eq + std::hash::Hash> ElaboratedDesign<A> {
413    /// Effective kinds of canonical event domains. Infer only unambiguous
414    /// one-bit alias edges; declaration metadata and signal types stay intact.
415    /// Opposite edges on one net require separate dispatch domains and retain
416    /// their existing canonical kind here.
417    pub fn event_kinds(&self) -> HashMap<A, DomainKind> {
418        let mut inferred: HashMap<A, Option<DomainKind>> = HashMap::default();
419        for (address, metadata) in &self.state_objects {
420            if metadata.width != 1
421                || !matches!(
422                    metadata.kind,
423                    DomainKind::ClockPosedge
424                        | DomainKind::ClockNegedge
425                        | DomainKind::ResetAsyncHigh
426                        | DomainKind::ResetAsyncLow
427                )
428            {
429                continue;
430            }
431            let canonical = self.events.canonical(*address);
432            let kind = inferred.entry(canonical).or_insert(Some(metadata.kind));
433            if kind.is_some_and(|kind| kind != metadata.kind) {
434                *kind = None;
435            }
436        }
437        self.events
438            .ordered_events
439            .iter()
440            .filter_map(|address| {
441                let metadata = self.state_objects.get(address)?;
442                let kind = if metadata.kind == DomainKind::Other && metadata.width == 1 {
443                    inferred
444                        .get(address)
445                        .copied()
446                        .flatten()
447                        .unwrap_or(metadata.kind)
448                } else {
449                    metadata.kind
450                };
451                Some((*address, kind))
452            })
453            .collect()
454    }
455}
456
457#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
458pub enum BinaryOp {
459    Add,
460    Sub,
461    Mul,
462    DivU,
463    DivS,
464    RemU,
465    RemS,
466    And,
467    Or,
468    Xor,
469    Shl, // Logical Shift Left (<<)
470    Shr, // Logical Shift Right (>>)
471    Sar, // Arithmetic Shift Right (>>>)
472    Eq,
473    Ne,
474    EqCase,
475    NeCase,
476    LtU,
477    LtS, // Less Than (Unsigned / Signed)
478    LeU,
479    LeS, // Less Equal
480    GtU,
481    GtS, // Greater Than
482    GeU,
483    GeS, // Greater Equal
484    LogicAnd,
485    LogicOr,
486    EqWildcard,
487    NeWildcard,
488}
489
490impl BinaryOp {
491    /// Whether the operation is commutative (a op b == b op a).
492    pub fn is_commutative(&self) -> bool {
493        matches!(
494            self,
495            BinaryOp::Add
496                | BinaryOp::Mul
497                | BinaryOp::And
498                | BinaryOp::Or
499                | BinaryOp::Xor
500                | BinaryOp::Eq
501                | BinaryOp::Ne
502                | BinaryOp::EqCase
503                | BinaryOp::NeCase
504                | BinaryOp::LogicAnd
505                | BinaryOp::LogicOr
506        )
507    }
508}
509
510impl fmt::Display for BinaryOp {
511    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
512        let op_str = match self {
513            BinaryOp::Add => "Add",
514            BinaryOp::Sub => "Sub",
515            BinaryOp::Mul => "Mul",
516            BinaryOp::DivU => "DivU",
517            BinaryOp::DivS => "DivS",
518            BinaryOp::RemU => "RemU",
519            BinaryOp::RemS => "RemS",
520            BinaryOp::And => "And",
521            BinaryOp::Or => "Or",
522            BinaryOp::Xor => "Xor",
523            BinaryOp::Shl => "Shl",
524            BinaryOp::Shr => "Shr",
525            BinaryOp::Sar => "Sar",
526            BinaryOp::Eq => "Eq",
527            BinaryOp::Ne => "Ne",
528            BinaryOp::EqCase => "EqCase",
529            BinaryOp::NeCase => "NeCase",
530            BinaryOp::LtU => "LtU",
531            BinaryOp::LtS => "LtS",
532            BinaryOp::LeU => "LeU",
533            BinaryOp::LeS => "LeS",
534            BinaryOp::GtU => "GtU",
535            BinaryOp::GtS => "GtS",
536            BinaryOp::GeU => "GeU",
537            BinaryOp::GeS => "GeS",
538            BinaryOp::LogicAnd => "LogicAnd",
539            BinaryOp::LogicOr => "LogicOr",
540            BinaryOp::EqWildcard => "EqWildcard",
541            BinaryOp::NeWildcard => "NeWildcard",
542        };
543        write!(f, "{}", op_str)
544    }
545}
546
547#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
548pub enum UnaryOp {
549    Ident,
550    /// Convert a four-state value to two-state form. Unknown bits become zero:
551    /// `(value, mask) -> (value & !mask, 0)`.
552    ToTwoState,
553    Minus,
554    BitNot,
555    LogicNot,
556    And,
557    Or,
558    Xor,
559    PopCount,
560    CountLeadingZeros,
561    CountTrailingZeros,
562}
563
564impl UnaryOp {
565    /// Return the canonical result width for an operand of `operand_width` bits.
566    ///
567    /// Bit-count operations return a value in `0..=operand_width`, which needs
568    /// `ceil(log2(operand_width + 1))` bits.  Computing that as the bit length
569    /// of `operand_width` avoids overflowing when the operand width is
570    /// `usize::MAX`.
571    pub fn result_width(self, operand_width: usize) -> usize {
572        match self {
573            UnaryOp::LogicNot | UnaryOp::And | UnaryOp::Or | UnaryOp::Xor => 1,
574            UnaryOp::Ident | UnaryOp::ToTwoState | UnaryOp::Minus | UnaryOp::BitNot => {
575                operand_width
576            }
577            UnaryOp::PopCount | UnaryOp::CountLeadingZeros | UnaryOp::CountTrailingZeros => {
578                usize::BITS as usize - operand_width.leading_zeros() as usize
579            }
580        }
581    }
582}
583
584impl fmt::Display for UnaryOp {
585    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
586        let op_str = match self {
587            UnaryOp::Ident => "Ident",
588            UnaryOp::ToTwoState => "ToTwoState",
589            UnaryOp::Minus => "Minus",
590            UnaryOp::BitNot => "BitNot",
591            UnaryOp::LogicNot => "LogicNot",
592            UnaryOp::And => "And",
593            UnaryOp::Or => "Or",
594            UnaryOp::Xor => "Xor",
595            UnaryOp::PopCount => "PopCount",
596            UnaryOp::CountLeadingZeros => "CountLeadingZeros",
597            UnaryOp::CountTrailingZeros => "CountTrailingZeros",
598        };
599        write!(f, "{}", op_str)
600    }
601}
602
603#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
604pub struct BitAccess {
605    pub lsb: usize,
606    pub msb: usize,
607}
608impl BitAccess {
609    pub fn new(lsb: usize, msb: usize) -> Self {
610        debug_assert!(lsb <= msb, "lsb must be less than or equal to msb");
611        Self { lsb, msb }
612    }
613    pub fn overlaps(&self, other: &Self) -> bool {
614        !(self.msb < other.lsb || other.msb < self.lsb)
615    }
616
617    /// Calculates the atomic bit ranges for a given access range and a set of boundaries.
618    pub fn calculate_atoms(&self, bounds: &BTreeSet<usize>) -> Vec<Self> {
619        use std::ops::Bound::*;
620        let mut atoms = Vec::new();
621        let mut current_lsb = self.lsb;
622
623        // Iterate through the boundaries that are within the access range
624        // Excluded(lsb) to Included(msb) handles lsb == msb case naturally (returns empty iterator)
625        for &bound in bounds.range((Excluded(self.lsb), Included(self.msb))) {
626            atoms.push(Self::new(current_lsb, bound - 1));
627            current_lsb = bound;
628        }
629
630        // Add the last atom
631        if current_lsb <= self.msb {
632            atoms.push(Self::new(current_lsb, self.msb));
633        }
634
635        atoms
636    }
637}
638#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
639pub struct VarAtomBase<A> {
640    pub id: A,
641    pub access: BitAccess,
642}
643impl<A> VarAtomBase<A> {
644    pub fn new(id: A, lsb: usize, msb: usize) -> Self {
645        Self {
646            id,
647            access: BitAccess { lsb, msb },
648        }
649    }
650}
651impl fmt::Display for BitAccess {
652    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
653        if self.lsb == self.msb {
654            write!(f, "[{}]", self.lsb)
655        } else {
656            write!(f, "[{}:{}]", self.msb, self.lsb)
657        }
658    }
659}
660
661impl<A> fmt::Display for VarAtomBase<A>
662where
663    A: fmt::Display + std::hash::Hash + Eq,
664{
665    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
666        write!(f, "{}{}", self.id, self.access)
667    }
668}
669
670#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
671pub struct ModuleId(pub usize);
672
673impl fmt::Display for ModuleId {
674    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
675        write!(f, "mod{}", self.0)
676    }
677}
678
679#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
680pub struct InstanceId(pub usize);
681
682impl fmt::Display for InstanceId {
683    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
684        write!(f, "inst{}", self.0)
685    }
686}
687
688/// Dense source-independent identity of one flattened state object.
689///
690/// Frontends assign this identity during design projection. Source variable
691/// IDs must not cross into SIR optimization, layout, or backend code.
692#[derive(
693    Debug, Clone, Copy, Default, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize,
694)]
695pub struct StateObjectId(pub u32);
696
697impl StateObjectId {
698    pub const fn from_raw(value: u32) -> Self {
699        Self(value)
700    }
701}
702
703impl fmt::Display for StateObjectId {
704    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
705        write!(f, "state{}", self.0)
706    }
707}
708
709#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
710pub struct AbsoluteAddrBase<V> {
711    pub instance_id: InstanceId,
712    pub var_id: V,
713}
714
715impl<V: fmt::Display> fmt::Display for AbsoluteAddrBase<V> {
716    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
717        write!(f, "AbsoluteAddr({}, {})", self.instance_id, self.var_id)
718    }
719}
720
721pub const STABLE_REGION: u32 = 0;
722pub const WORKING_REGION: u32 = 1;
723pub const SPARSE_WORKING_REGION: u32 = 2;
724
725#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
726pub struct RegionedVarAddrBase<V> {
727    pub region: u32,
728    pub var_id: V,
729}
730
731impl<V: fmt::Display> fmt::Display for RegionedVarAddrBase<V> {
732    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
733        write!(
734            f,
735            "RegionedVarAddr(region={}, {})",
736            self.region, self.var_id
737        )
738    }
739}
740
741#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
742pub struct RegionedAbsoluteAddrBase<V> {
743    pub region: u32,
744    pub instance_id: InstanceId,
745    pub var_id: V,
746}
747
748pub type StateAddr = AbsoluteAddrBase<StateObjectId>;
749pub type RegionedStateAddr = RegionedAbsoluteAddrBase<StateObjectId>;
750
751impl<V: Copy> RegionedAbsoluteAddrBase<V> {
752    pub fn from_absolute_addr(region: u32, addr: AbsoluteAddrBase<V>) -> Self {
753        Self {
754            region,
755            instance_id: addr.instance_id,
756            var_id: addr.var_id,
757        }
758    }
759
760    pub fn absolute_addr(&self) -> AbsoluteAddrBase<V> {
761        AbsoluteAddrBase {
762            instance_id: self.instance_id,
763            var_id: self.var_id,
764        }
765    }
766}
767
768impl<V: fmt::Display> fmt::Display for RegionedAbsoluteAddrBase<V> {
769    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
770        write!(
771            f,
772            "RegionedAbsoluteAddr(region={}, {}, {})",
773            self.region, self.instance_id, self.var_id
774        )
775    }
776}
777
778#[cfg(test)]
779mod tests {
780    use super::*;
781
782    #[test]
783    fn bit_access_splits_only_at_internal_boundaries() {
784        let access = BitAccess::new(4, 11);
785        let bounds = [0, 4, 7, 12, 20].into_iter().collect();
786
787        assert_eq!(
788            access.calculate_atoms(&bounds),
789            vec![BitAccess::new(4, 6), BitAccess::new(7, 11)]
790        );
791    }
792
793    #[test]
794    fn design_ids_and_addresses_have_stable_display() {
795        let address = AbsoluteAddrBase {
796            instance_id: InstanceId(42),
797            var_id: 7,
798        };
799
800        assert_eq!(ModuleId(3).to_string(), "mod3");
801        assert_eq!(InstanceId(42).to_string(), "inst42");
802        assert_eq!(StateObjectId(7).to_string(), "state7");
803        assert_eq!(address.to_string(), "AbsoluteAddr(inst42, 7)");
804    }
805
806    #[test]
807    fn regioned_address_round_trips_semantic_identity() {
808        let address = AbsoluteAddrBase {
809            instance_id: InstanceId(2),
810            var_id: 9,
811        };
812        let regioned = RegionedAbsoluteAddrBase::from_absolute_addr(WORKING_REGION, address);
813
814        assert_eq!(regioned.absolute_addr(), address);
815        assert_eq!(regioned.region, WORKING_REGION);
816    }
817
818    #[test]
819    fn semantic_operator_contracts_are_source_independent() {
820        assert!(BinaryOp::Add.is_commutative());
821        assert!(!BinaryOp::Sub.is_commutative());
822        assert_eq!(UnaryOp::LogicNot.result_width(128), 1);
823        assert_eq!(UnaryOp::PopCount.result_width(128), 8);
824    }
825
826    #[test]
827    fn initial_state_and_runtime_error_schemas_accept_design_owned_ids() {
828        let initial = InitialStateValue {
829            address: AbsoluteAddrBase {
830                instance_id: InstanceId(1),
831                var_id: 7u32,
832            },
833            data: InitialStateData::Writes(vec![InitialStateWriteRun {
834                bit_offset: 3,
835                bit_width: 5,
836                value_bytes: vec![0x15],
837                mask_bytes: vec![0],
838            }]),
839        };
840        let error = RuntimeErrorInfo {
841            message: "failed".to_string(),
842            signals: vec![initial.address],
843        };
844        let mut runtime = RuntimeSchema::default();
845        runtime.runtime_errors.insert(1, error.clone());
846        runtime.runtime_event_sites.push(RuntimeEventSite {
847            kind: RuntimeEventKind::AssertFatal,
848            template: Some("failed".to_string()),
849            sizing: DisplaySizing::Veryl,
850            scope: None,
851            arg_widths: Vec::new(),
852            arg_signed: Vec::new(),
853            arg_is_string: Vec::new(),
854        });
855        runtime.comb_observers.push(RuntimeCombObserver {
856            site_id: 0,
857            activation_group: 0,
858            sensitivity: vec![VarAtomBase {
859                id: initial.address,
860                access: BitAccess { lsb: 3, msb: 7 },
861            }],
862            written_inputs: vec![initial.address],
863        });
864        runtime.testbench_read_roots.insert(initial.address);
865
866        assert_eq!(error.signals, vec![initial.address]);
867        assert!(matches!(initial.data, InitialStateData::Writes(_)));
868        assert_eq!(runtime.runtime_errors[&1], error);
869        assert_eq!(runtime.runtime_event_sites.len(), 1);
870        assert_eq!(runtime.comb_observers[0].sensitivity[0].id, initial.address);
871        assert!(runtime.testbench_read_roots.contains(&initial.address));
872    }
873
874    #[test]
875    fn variable_metadata_preserves_elaborated_shape_and_domain() {
876        let metadata = VariableMetadata {
877            width: 32,
878            is_4state: true,
879            kind: DomainKind::Other,
880            type_kind: PortTypeKind::Logic,
881            array_dims: vec![4],
882        };
883
884        assert_eq!(metadata.width, 32);
885        assert_eq!(metadata.array_dims, vec![4]);
886    }
887
888    #[test]
889    fn elaborated_design_uses_flat_addresses_and_canonical_event_topology() {
890        let mut design = ElaboratedDesign::<u32>::default();
891        design.state_objects.insert(
892            10,
893            VariableMetadata {
894                width: 1,
895                is_4state: false,
896                kind: DomainKind::ClockPosedge,
897                type_kind: PortTypeKind::Clock,
898                array_dims: Vec::new(),
899            },
900        );
901        design.events.aliases.insert(11, 10);
902        design.events.ordered_events.push(10);
903
904        assert_eq!(design.events.canonical(11), 10);
905        assert_eq!(design.events.canonical(12), 12);
906        assert_eq!(design.events.len(), 1);
907        assert!(!design.events.is_empty());
908        assert_eq!(design.state_objects[&10].width, 1);
909    }
910    #[test]
911    fn alias_event_kinds_are_separate_from_declaration_metadata() {
912        let mut design = ElaboratedDesign::<u32>::default();
913        for (address, kind) in [
914            (10, DomainKind::Other),
915            (11, DomainKind::ClockPosedge),
916            (20, DomainKind::Other),
917            (21, DomainKind::ClockNegedge),
918            (30, DomainKind::Other),
919            (31, DomainKind::ClockPosedge),
920            (32, DomainKind::ClockNegedge),
921        ] {
922            design.state_objects.insert(
923                address,
924                VariableMetadata {
925                    width: 1,
926                    is_4state: false,
927                    kind,
928                    type_kind: PortTypeKind::Logic,
929                    array_dims: Vec::new(),
930                },
931            );
932        }
933        design.events.ordered_events = vec![10, 20, 30];
934        for (alias, canonical) in [(11, 10), (21, 20), (31, 30), (32, 30)] {
935            design.events.aliases.insert(alias, canonical);
936        }
937        let kinds = design.event_kinds();
938        assert_eq!(kinds[&10], DomainKind::ClockPosedge);
939        assert_eq!(kinds[&20], DomainKind::ClockNegedge);
940        assert_eq!(
941            kinds[&30],
942            DomainKind::Other,
943            "conflicting aliases do not select one edge"
944        );
945        assert_eq!(design.state_objects[&10].kind, DomainKind::Other);
946        assert_eq!(design.state_objects[&20].type_kind, PortTypeKind::Logic);
947    }
948}