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}
63
64#[derive(Clone, Debug, Serialize, Deserialize)]
65pub struct RuntimeEventSite {
66    pub kind: RuntimeEventKind,
67    pub template: Option<String>,
68    /// Fully elaborated module-instance scope that emitted this event.
69    pub scope: Option<String>,
70    pub arg_widths: Vec<usize>,
71    pub arg_signed: Vec<bool>,
72    pub arg_is_string: Vec<bool>,
73}
74
75/// Runtime activation recipe for one combinational event site.
76///
77/// Expression trees used to emit the event have already been lowered into
78/// SIR.  The runtime only retains the persistent-state ranges needed to detect
79/// whether the corresponding combinational process must be observed again.
80#[derive(Clone, Debug, Serialize, Deserialize)]
81pub struct RuntimeCombObserver<A> {
82    pub site_id: u32,
83    pub activation_group: u32,
84    pub sensitivity: Vec<VarAtomBase<A>>,
85    pub written_inputs: Vec<A>,
86}
87
88#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
89pub struct InitialStateWriteRun {
90    pub bit_offset: usize,
91    pub bit_width: usize,
92    pub value_bytes: Vec<u8>,
93    pub mask_bytes: Vec<u8>,
94}
95
96#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
97pub enum InitialStateData {
98    Packed {
99        value: BigUint,
100        mask: BigUint,
101        written_mask: BigUint,
102    },
103    Writes(Vec<InitialStateWriteRun>),
104}
105
106#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
107pub struct InitialStateValue<A> {
108    pub address: A,
109    pub data: InitialStateData,
110}
111
112#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
113pub struct RuntimeErrorInfo<A> {
114    pub message: String,
115    pub signals: Vec<A>,
116}
117
118/// Source-independent runtime diagnostics and observable event descriptions.
119#[derive(Clone, Debug)]
120pub struct RuntimeSchema<A> {
121    pub runtime_errors: HashMap<i64, RuntimeErrorInfo<A>>,
122    pub runtime_event_sites: Vec<RuntimeEventSite>,
123    pub comb_observers: Vec<RuntimeCombObserver<A>>,
124    /// Persistent state read directly by host-side testbench execution. These
125    /// are optimization roots even when no SIR instruction loads them.
126    pub testbench_read_roots: HashSet<A>,
127    /// Bit ranges written by RTL execution units. External component outputs
128    /// may not overlap these ranges because that would create multiple drivers.
129    pub rtl_writes: HashSet<VarAtomBase<A>>,
130}
131
132impl<A> Default for RuntimeSchema<A> {
133    fn default() -> Self {
134        Self {
135            runtime_errors: HashMap::default(),
136            runtime_event_sites: Vec::new(),
137            comb_observers: Vec::new(),
138            testbench_read_roots: HashSet::default(),
139            rtl_writes: HashSet::default(),
140        }
141    }
142}
143
144/// Source-independent event-domain topology after elaboration.
145///
146/// Addresses are already flattened. Source paths and frontend IDs used only
147/// for diagnostics or lookup deliberately live outside this structure.
148#[derive(Clone, Debug)]
149pub struct EventTopology<A> {
150    /// Alias event address to the canonical event-domain address.
151    pub aliases: HashMap<A, A>,
152    /// Canonical event domains in evaluation order.
153    pub ordered_events: Vec<A>,
154    /// Canonical clocks whose value may be changed by another event domain.
155    pub cascaded_events: BTreeSet<A>,
156    /// Canonical asynchronous/synchronous reset to its canonical clock.
157    pub reset_clocks: HashMap<A, A>,
158}
159
160impl<A> Default for EventTopology<A> {
161    fn default() -> Self {
162        Self {
163            aliases: HashMap::default(),
164            ordered_events: Vec::new(),
165            cascaded_events: BTreeSet::new(),
166            reset_clocks: HashMap::default(),
167        }
168    }
169}
170
171impl<A: Copy + Eq + std::hash::Hash> EventTopology<A> {
172    pub fn canonical(&self, address: A) -> A {
173        self.aliases.get(&address).copied().unwrap_or(address)
174    }
175
176    pub fn len(&self) -> usize {
177        self.ordered_events.len()
178    }
179
180    pub fn is_empty(&self) -> bool {
181        self.ordered_events.is_empty()
182    }
183}
184
185/// Backend-neutral semantic design data after hierarchy flattening.
186///
187/// This is intentionally not a frontend lookup table: every state object is
188/// keyed by its flattened semantic address, and no source-language AST or path
189/// type is retained.
190#[derive(Clone, Debug)]
191pub struct ElaboratedDesign<A> {
192    pub state_objects: HashMap<A, VariableMetadata>,
193    pub events: EventTopology<A>,
194    pub initial_state: Vec<InitialStateValue<A>>,
195}
196
197impl<A> Default for ElaboratedDesign<A> {
198    fn default() -> Self {
199        Self {
200            state_objects: HashMap::default(),
201            events: EventTopology::default(),
202            initial_state: Vec::new(),
203        }
204    }
205}
206
207#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
208pub enum BinaryOp {
209    Add,
210    Sub,
211    Mul,
212    DivU,
213    DivS,
214    RemU,
215    RemS,
216    And,
217    Or,
218    Xor,
219    Shl, // Logical Shift Left (<<)
220    Shr, // Logical Shift Right (>>)
221    Sar, // Arithmetic Shift Right (>>>)
222    Eq,
223    Ne,
224    EqCase,
225    NeCase,
226    LtU,
227    LtS, // Less Than (Unsigned / Signed)
228    LeU,
229    LeS, // Less Equal
230    GtU,
231    GtS, // Greater Than
232    GeU,
233    GeS, // Greater Equal
234    LogicAnd,
235    LogicOr,
236    EqWildcard,
237    NeWildcard,
238}
239
240impl BinaryOp {
241    /// Whether the operation is commutative (a op b == b op a).
242    pub fn is_commutative(&self) -> bool {
243        matches!(
244            self,
245            BinaryOp::Add
246                | BinaryOp::Mul
247                | BinaryOp::And
248                | BinaryOp::Or
249                | BinaryOp::Xor
250                | BinaryOp::Eq
251                | BinaryOp::Ne
252                | BinaryOp::EqCase
253                | BinaryOp::NeCase
254                | BinaryOp::LogicAnd
255                | BinaryOp::LogicOr
256        )
257    }
258}
259
260impl fmt::Display for BinaryOp {
261    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262        let op_str = match self {
263            BinaryOp::Add => "Add",
264            BinaryOp::Sub => "Sub",
265            BinaryOp::Mul => "Mul",
266            BinaryOp::DivU => "DivU",
267            BinaryOp::DivS => "DivS",
268            BinaryOp::RemU => "RemU",
269            BinaryOp::RemS => "RemS",
270            BinaryOp::And => "And",
271            BinaryOp::Or => "Or",
272            BinaryOp::Xor => "Xor",
273            BinaryOp::Shl => "Shl",
274            BinaryOp::Shr => "Shr",
275            BinaryOp::Sar => "Sar",
276            BinaryOp::Eq => "Eq",
277            BinaryOp::Ne => "Ne",
278            BinaryOp::EqCase => "EqCase",
279            BinaryOp::NeCase => "NeCase",
280            BinaryOp::LtU => "LtU",
281            BinaryOp::LtS => "LtS",
282            BinaryOp::LeU => "LeU",
283            BinaryOp::LeS => "LeS",
284            BinaryOp::GtU => "GtU",
285            BinaryOp::GtS => "GtS",
286            BinaryOp::GeU => "GeU",
287            BinaryOp::GeS => "GeS",
288            BinaryOp::LogicAnd => "LogicAnd",
289            BinaryOp::LogicOr => "LogicOr",
290            BinaryOp::EqWildcard => "EqWildcard",
291            BinaryOp::NeWildcard => "NeWildcard",
292        };
293        write!(f, "{}", op_str)
294    }
295}
296
297#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
298pub enum UnaryOp {
299    Ident,
300    /// Convert a four-state value to two-state form. Unknown bits become zero:
301    /// `(value, mask) -> (value & !mask, 0)`.
302    ToTwoState,
303    Minus,
304    BitNot,
305    LogicNot,
306    And,
307    Or,
308    Xor,
309    PopCount,
310    CountLeadingZeros,
311    CountTrailingZeros,
312}
313
314impl UnaryOp {
315    /// Return the canonical result width for an operand of `operand_width` bits.
316    ///
317    /// Bit-count operations return a value in `0..=operand_width`, which needs
318    /// `ceil(log2(operand_width + 1))` bits.  Computing that as the bit length
319    /// of `operand_width` avoids overflowing when the operand width is
320    /// `usize::MAX`.
321    pub fn result_width(self, operand_width: usize) -> usize {
322        match self {
323            UnaryOp::LogicNot | UnaryOp::And | UnaryOp::Or | UnaryOp::Xor => 1,
324            UnaryOp::Ident | UnaryOp::ToTwoState | UnaryOp::Minus | UnaryOp::BitNot => {
325                operand_width
326            }
327            UnaryOp::PopCount | UnaryOp::CountLeadingZeros | UnaryOp::CountTrailingZeros => {
328                usize::BITS as usize - operand_width.leading_zeros() as usize
329            }
330        }
331    }
332}
333
334impl fmt::Display for UnaryOp {
335    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
336        let op_str = match self {
337            UnaryOp::Ident => "Ident",
338            UnaryOp::ToTwoState => "ToTwoState",
339            UnaryOp::Minus => "Minus",
340            UnaryOp::BitNot => "BitNot",
341            UnaryOp::LogicNot => "LogicNot",
342            UnaryOp::And => "And",
343            UnaryOp::Or => "Or",
344            UnaryOp::Xor => "Xor",
345            UnaryOp::PopCount => "PopCount",
346            UnaryOp::CountLeadingZeros => "CountLeadingZeros",
347            UnaryOp::CountTrailingZeros => "CountTrailingZeros",
348        };
349        write!(f, "{}", op_str)
350    }
351}
352
353#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
354pub struct BitAccess {
355    pub lsb: usize,
356    pub msb: usize,
357}
358impl BitAccess {
359    pub fn new(lsb: usize, msb: usize) -> Self {
360        debug_assert!(lsb <= msb, "lsb must be less than or equal to msb");
361        Self { lsb, msb }
362    }
363    pub fn overlaps(&self, other: &Self) -> bool {
364        !(self.msb < other.lsb || other.msb < self.lsb)
365    }
366
367    /// Calculates the atomic bit ranges for a given access range and a set of boundaries.
368    pub fn calculate_atoms(&self, bounds: &BTreeSet<usize>) -> Vec<Self> {
369        use std::ops::Bound::*;
370        let mut atoms = Vec::new();
371        let mut current_lsb = self.lsb;
372
373        // Iterate through the boundaries that are within the access range
374        // Excluded(lsb) to Included(msb) handles lsb == msb case naturally (returns empty iterator)
375        for &bound in bounds.range((Excluded(self.lsb), Included(self.msb))) {
376            atoms.push(Self::new(current_lsb, bound - 1));
377            current_lsb = bound;
378        }
379
380        // Add the last atom
381        if current_lsb <= self.msb {
382            atoms.push(Self::new(current_lsb, self.msb));
383        }
384
385        atoms
386    }
387}
388#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
389pub struct VarAtomBase<A> {
390    pub id: A,
391    pub access: BitAccess,
392}
393impl<A> VarAtomBase<A> {
394    pub fn new(id: A, lsb: usize, msb: usize) -> Self {
395        Self {
396            id,
397            access: BitAccess { lsb, msb },
398        }
399    }
400}
401impl fmt::Display for BitAccess {
402    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
403        if self.lsb == self.msb {
404            write!(f, "[{}]", self.lsb)
405        } else {
406            write!(f, "[{}:{}]", self.msb, self.lsb)
407        }
408    }
409}
410
411impl<A> fmt::Display for VarAtomBase<A>
412where
413    A: fmt::Display + std::hash::Hash + Eq,
414{
415    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
416        write!(f, "{}{}", self.id, self.access)
417    }
418}
419
420#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
421pub struct ModuleId(pub usize);
422
423impl fmt::Display for ModuleId {
424    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
425        write!(f, "mod{}", self.0)
426    }
427}
428
429#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
430pub struct InstanceId(pub usize);
431
432impl fmt::Display for InstanceId {
433    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
434        write!(f, "inst{}", self.0)
435    }
436}
437
438/// Dense source-independent identity of one flattened state object.
439///
440/// Frontends assign this identity during design projection. Source variable
441/// IDs must not cross into SIR optimization, layout, or backend code.
442#[derive(
443    Debug, Clone, Copy, Default, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize,
444)]
445pub struct StateObjectId(pub u32);
446
447impl StateObjectId {
448    pub const fn from_raw(value: u32) -> Self {
449        Self(value)
450    }
451}
452
453impl fmt::Display for StateObjectId {
454    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
455        write!(f, "state{}", self.0)
456    }
457}
458
459#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
460pub struct AbsoluteAddrBase<V> {
461    pub instance_id: InstanceId,
462    pub var_id: V,
463}
464
465impl<V: fmt::Display> fmt::Display for AbsoluteAddrBase<V> {
466    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
467        write!(f, "AbsoluteAddr({}, {})", self.instance_id, self.var_id)
468    }
469}
470
471pub const STABLE_REGION: u32 = 0;
472pub const WORKING_REGION: u32 = 1;
473pub const SPARSE_WORKING_REGION: u32 = 2;
474
475#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
476pub struct RegionedVarAddrBase<V> {
477    pub region: u32,
478    pub var_id: V,
479}
480
481impl<V: fmt::Display> fmt::Display for RegionedVarAddrBase<V> {
482    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
483        write!(
484            f,
485            "RegionedVarAddr(region={}, {})",
486            self.region, self.var_id
487        )
488    }
489}
490
491#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
492pub struct RegionedAbsoluteAddrBase<V> {
493    pub region: u32,
494    pub instance_id: InstanceId,
495    pub var_id: V,
496}
497
498pub type StateAddr = AbsoluteAddrBase<StateObjectId>;
499pub type RegionedStateAddr = RegionedAbsoluteAddrBase<StateObjectId>;
500
501impl<V: Copy> RegionedAbsoluteAddrBase<V> {
502    pub fn from_absolute_addr(region: u32, addr: AbsoluteAddrBase<V>) -> Self {
503        Self {
504            region,
505            instance_id: addr.instance_id,
506            var_id: addr.var_id,
507        }
508    }
509
510    pub fn absolute_addr(&self) -> AbsoluteAddrBase<V> {
511        AbsoluteAddrBase {
512            instance_id: self.instance_id,
513            var_id: self.var_id,
514        }
515    }
516}
517
518impl<V: fmt::Display> fmt::Display for RegionedAbsoluteAddrBase<V> {
519    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
520        write!(
521            f,
522            "RegionedAbsoluteAddr(region={}, {}, {})",
523            self.region, self.instance_id, self.var_id
524        )
525    }
526}
527
528#[cfg(test)]
529mod tests {
530    use super::*;
531
532    #[test]
533    fn bit_access_splits_only_at_internal_boundaries() {
534        let access = BitAccess::new(4, 11);
535        let bounds = [0, 4, 7, 12, 20].into_iter().collect();
536
537        assert_eq!(
538            access.calculate_atoms(&bounds),
539            vec![BitAccess::new(4, 6), BitAccess::new(7, 11)]
540        );
541    }
542
543    #[test]
544    fn design_ids_and_addresses_have_stable_display() {
545        let address = AbsoluteAddrBase {
546            instance_id: InstanceId(42),
547            var_id: 7,
548        };
549
550        assert_eq!(ModuleId(3).to_string(), "mod3");
551        assert_eq!(InstanceId(42).to_string(), "inst42");
552        assert_eq!(StateObjectId(7).to_string(), "state7");
553        assert_eq!(address.to_string(), "AbsoluteAddr(inst42, 7)");
554    }
555
556    #[test]
557    fn regioned_address_round_trips_semantic_identity() {
558        let address = AbsoluteAddrBase {
559            instance_id: InstanceId(2),
560            var_id: 9,
561        };
562        let regioned = RegionedAbsoluteAddrBase::from_absolute_addr(WORKING_REGION, address);
563
564        assert_eq!(regioned.absolute_addr(), address);
565        assert_eq!(regioned.region, WORKING_REGION);
566    }
567
568    #[test]
569    fn semantic_operator_contracts_are_source_independent() {
570        assert!(BinaryOp::Add.is_commutative());
571        assert!(!BinaryOp::Sub.is_commutative());
572        assert_eq!(UnaryOp::LogicNot.result_width(128), 1);
573        assert_eq!(UnaryOp::PopCount.result_width(128), 8);
574    }
575
576    #[test]
577    fn initial_state_and_runtime_error_schemas_accept_design_owned_ids() {
578        let initial = InitialStateValue {
579            address: AbsoluteAddrBase {
580                instance_id: InstanceId(1),
581                var_id: 7u32,
582            },
583            data: InitialStateData::Writes(vec![InitialStateWriteRun {
584                bit_offset: 3,
585                bit_width: 5,
586                value_bytes: vec![0x15],
587                mask_bytes: vec![0],
588            }]),
589        };
590        let error = RuntimeErrorInfo {
591            message: "failed".to_string(),
592            signals: vec![initial.address],
593        };
594        let mut runtime = RuntimeSchema::default();
595        runtime.runtime_errors.insert(1, error.clone());
596        runtime.runtime_event_sites.push(RuntimeEventSite {
597            kind: RuntimeEventKind::AssertFatal,
598            template: Some("failed".to_string()),
599            scope: None,
600            arg_widths: Vec::new(),
601            arg_signed: Vec::new(),
602            arg_is_string: Vec::new(),
603        });
604        runtime.comb_observers.push(RuntimeCombObserver {
605            site_id: 0,
606            activation_group: 0,
607            sensitivity: vec![VarAtomBase {
608                id: initial.address,
609                access: BitAccess { lsb: 3, msb: 7 },
610            }],
611            written_inputs: vec![initial.address],
612        });
613        runtime.testbench_read_roots.insert(initial.address);
614
615        assert_eq!(error.signals, vec![initial.address]);
616        assert!(matches!(initial.data, InitialStateData::Writes(_)));
617        assert_eq!(runtime.runtime_errors[&1], error);
618        assert_eq!(runtime.runtime_event_sites.len(), 1);
619        assert_eq!(runtime.comb_observers[0].sensitivity[0].id, initial.address);
620        assert!(runtime.testbench_read_roots.contains(&initial.address));
621    }
622
623    #[test]
624    fn variable_metadata_preserves_elaborated_shape_and_domain() {
625        let metadata = VariableMetadata {
626            width: 32,
627            is_4state: true,
628            kind: DomainKind::Other,
629            type_kind: PortTypeKind::Logic,
630            array_dims: vec![4],
631        };
632
633        assert_eq!(metadata.width, 32);
634        assert_eq!(metadata.array_dims, vec![4]);
635    }
636
637    #[test]
638    fn elaborated_design_uses_flat_addresses_and_canonical_event_topology() {
639        let mut design = ElaboratedDesign::<u32>::default();
640        design.state_objects.insert(
641            10,
642            VariableMetadata {
643                width: 1,
644                is_4state: false,
645                kind: DomainKind::ClockPosedge,
646                type_kind: PortTypeKind::Clock,
647                array_dims: Vec::new(),
648            },
649        );
650        design.events.aliases.insert(11, 10);
651        design.events.ordered_events.push(10);
652
653        assert_eq!(design.events.canonical(11), 10);
654        assert_eq!(design.events.canonical(12), 12);
655        assert_eq!(design.events.len(), 1);
656        assert!(!design.events.is_empty());
657        assert_eq!(design.state_objects[&10].width, 1);
658    }
659}