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