Skip to main content

celox_testbench/
lib.rs

1//! Source-independent testbench bytecode contracts.
2//!
3//! Frontends translate source-language operators into this vocabulary before
4//! execution. No Veryl AST, parser ID, simulator backend, or runtime handle is
5//! part of the bytecode representation.
6
7use num_bigint::BigUint;
8use serde::{Deserialize, Serialize};
9use std::path::PathBuf;
10
11mod format;
12mod vm;
13pub use format::{DisplayFormatArg, format_display_arg};
14pub use vm::{CompiledExpr, TestbenchValue};
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
17pub struct StateLocation<A> {
18    pub address: A,
19    pub byte_offset: usize,
20}
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
23pub enum TestbenchOperator {
24    Add,
25    Sub,
26    Mul,
27    Div,
28    Rem,
29    Pow,
30    BitAnd,
31    BitOr,
32    BitXor,
33    BitXnor,
34    BitNand,
35    BitNor,
36    LogicShiftL,
37    LogicShiftR,
38    ArithShiftL,
39    ArithShiftR,
40    Eq,
41    EqWildcard,
42    Ne,
43    NeWildcard,
44    Less,
45    LessEq,
46    Greater,
47    GreaterEq,
48    LogicAnd,
49    LogicOr,
50    LogicNot,
51    BitNot,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55pub struct SourceLocation {
56    pub file: String,
57    pub line: u32,
58    pub column: u32,
59}
60
61#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
62pub enum AssertMessage<Argument> {
63    Formatted {
64        template: String,
65        args: Vec<Argument>,
66    },
67    DynamicArgs(Vec<Argument>),
68}
69
70#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
71pub enum ClockCount<Expression> {
72    Static(u64),
73    Dynamic(Expression),
74}
75
76#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
77pub enum LoopBound<Expression> {
78    Static(usize),
79    Dynamic {
80        expr: Expression,
81        width: usize,
82        signed: bool,
83    },
84}
85
86#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
87pub enum ComponentParameterValue {
88    Bits { words: Vec<u64>, width: u32 },
89    String(String),
90}
91
92#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
93pub struct ComponentConnection {
94    pub port: String,
95    pub group: Option<String>,
96    pub member: Option<String>,
97    pub input: bool,
98    pub has_output: bool,
99    pub is_clock: bool,
100    pub is_reset: bool,
101    pub width: u32,
102}
103
104#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
105pub struct TestbenchComponent {
106    pub instance: String,
107    pub component: String,
108    pub params: Vec<(String, ComponentParameterValue)>,
109    pub connections: Vec<ComponentConnection>,
110    pub is_var_form: bool,
111    pub source: Option<SourceLocation>,
112}
113
114#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
115pub struct ComponentLibrary {
116    pub export: String,
117    pub type_name: String,
118    pub path: PathBuf,
119}
120
121#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
122pub struct ComponentConnectionBinding<Event, Signal, Expression> {
123    pub port: String,
124    pub input: Option<Expression>,
125    /// Direct mask source for a variable/select input connection. Runtime
126    /// hosts evaluate `input` for the value and use this target to preserve
127    /// the corresponding four-state mask.
128    pub input_target: Option<TestbenchTarget<Signal, Expression>>,
129    pub output: Option<TestbenchTarget<Signal, Expression>>,
130    pub output_rtl_driven: bool,
131    pub event: Option<Event>,
132}
133
134#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
135pub struct ComponentBinding<Event, Signal, Expression> {
136    pub instance: String,
137    pub connections: Vec<ComponentConnectionBinding<Event, Signal, Expression>>,
138}
139
140/// Source-independent testbench control program.
141///
142/// Frontends instantiate this with semantic state/event identities and
143/// unbound expressions. Runtime binding instantiates the same contract with
144/// backend event/signal handles and executable expressions.
145#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
146pub enum TestbenchStatement<Event, Signal, Expression, Argument, Target = Signal> {
147    ClockNext {
148        clock_event: Event,
149        count: ClockCount<Expression>,
150    },
151    ResetAssert {
152        reset_signal: Signal,
153        reset_event: Option<Event>,
154        clock_event: Event,
155        duration: ClockCount<Expression>,
156        assert_value: u8,
157        deassert_value: u8,
158    },
159    Assert {
160        expr: Expression,
161        site_id: u32,
162        continue_on_fail: bool,
163        message: Option<AssertMessage<Argument>>,
164        location: Option<SourceLocation>,
165    },
166    Display {
167        message: Option<AssertMessage<Argument>>,
168        newline: bool,
169    },
170    If {
171        expr: Expression,
172        then_block: Vec<Self>,
173        else_block: Vec<Self>,
174    },
175    For {
176        loop_var: Option<(Signal, usize, bool)>,
177        start: LoopBound<Expression>,
178        end: LoopBound<Expression>,
179        inclusive: bool,
180        step: usize,
181        step_op: Option<TestbenchOperator>,
182        reverse: bool,
183        body: Vec<Self>,
184    },
185    Assign {
186        dst: Target,
187        expr: Expression,
188    },
189    RandomSeed {
190        handle: String,
191        value: Expression,
192    },
193    RandomGet {
194        handle: String,
195        width: u32,
196        signed: bool,
197        ret: Option<Target>,
198    },
199    RandomGetRange {
200        handle: String,
201        min: Expression,
202        max: Expression,
203        width: u32,
204        signed: bool,
205        ret: Option<Target>,
206    },
207    RandomGetSeed {
208        handle: String,
209        ret: Option<Target>,
210    },
211    ComponentMethod {
212        instance: String,
213        method: String,
214        args: Vec<Argument>,
215        ret: Option<Target>,
216        ret_width: Option<u32>,
217        ret_signed: bool,
218        ret_strict: bool,
219    },
220    Break,
221    Finish,
222}
223
224#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
225pub struct SemanticSignal<A> {
226    pub address: A,
227    pub width: usize,
228}
229
230#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
231pub struct TestbenchSelection<Expression> {
232    pub offset: Expression,
233    pub width: usize,
234}
235
236#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
237pub struct TestbenchTarget<Signal, Expression> {
238    pub signal: Signal,
239    pub selection: Option<TestbenchSelection<Expression>>,
240    pub width: usize,
241}
242
243#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
244pub struct SemanticArgument<A> {
245    pub expr: ExprBytecode<StateLocation<A>>,
246    pub width: usize,
247    pub signed: bool,
248    pub is_string: bool,
249}
250
251pub type SemanticStatement<A> = TestbenchStatement<
252    A,
253    SemanticSignal<A>,
254    ExprBytecode<StateLocation<A>>,
255    SemanticArgument<A>,
256    TestbenchTarget<SemanticSignal<A>, ExprBytecode<StateLocation<A>>>,
257>;
258pub type SemanticComponentBinding<A> =
259    ComponentBinding<A, SemanticSignal<A>, ExprBytecode<StateLocation<A>>>;
260
261#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
262pub struct TestbenchProgram<A> {
263    statements: Vec<SemanticStatement<A>>,
264    random_seed: Option<u64>,
265    components: Vec<TestbenchComponent>,
266    component_libraries: Vec<ComponentLibrary>,
267    component_file_base: Option<PathBuf>,
268    component_bindings: Vec<SemanticComponentBinding<A>>,
269}
270
271impl<A> Default for TestbenchProgram<A> {
272    fn default() -> Self {
273        Self {
274            statements: Vec::new(),
275            random_seed: None,
276            components: Vec::new(),
277            component_libraries: Vec::new(),
278            component_file_base: None,
279            component_bindings: Vec::new(),
280        }
281    }
282}
283
284impl<A> TestbenchProgram<A> {
285    pub fn new(statements: Vec<SemanticStatement<A>>) -> Self {
286        Self {
287            statements,
288            random_seed: None,
289            components: Vec::new(),
290            component_libraries: Vec::new(),
291            component_file_base: None,
292            component_bindings: Vec::new(),
293        }
294    }
295
296    pub fn with_random_seed(mut self, random_seed: u64) -> Self {
297        self.random_seed = Some(random_seed);
298        self
299    }
300
301    pub fn with_random_seed_option(mut self, random_seed: Option<u64>) -> Self {
302        self.random_seed = random_seed;
303        self
304    }
305
306    pub fn statements(&self) -> &[SemanticStatement<A>] {
307        &self.statements
308    }
309
310    pub fn into_statements(self) -> Vec<SemanticStatement<A>> {
311        self.statements
312    }
313
314    pub fn random_seed(&self) -> u64 {
315        self.random_seed.unwrap_or_default()
316    }
317
318    pub fn configured_random_seed(&self) -> Option<u64> {
319        self.random_seed
320    }
321
322    pub fn with_components(mut self, components: Vec<TestbenchComponent>) -> Self {
323        self.components = components;
324        self
325    }
326
327    pub fn with_component_runtime(
328        mut self,
329        libraries: Vec<ComponentLibrary>,
330        file_base: Option<PathBuf>,
331        bindings: Vec<SemanticComponentBinding<A>>,
332    ) -> Self {
333        self.component_libraries = libraries;
334        self.component_file_base = file_base;
335        self.component_bindings = bindings;
336        self
337    }
338
339    pub fn components(&self) -> &[TestbenchComponent] {
340        &self.components
341    }
342
343    pub fn component_libraries(&self) -> &[ComponentLibrary] {
344        &self.component_libraries
345    }
346
347    pub fn component_file_base(&self) -> Option<&std::path::Path> {
348        self.component_file_base.as_deref()
349    }
350
351    pub fn component_bindings(&self) -> &[SemanticComponentBinding<A>] {
352        &self.component_bindings
353    }
354
355    pub fn is_empty(&self) -> bool {
356        self.statements.is_empty()
357    }
358}
359
360#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
361pub enum ExprOpcode<L = usize> {
362    ConstU64(u64),
363    ConstWide(BigUint),
364    LoadU64 {
365        location: L,
366        byte_size: usize,
367        mask: u64,
368    },
369    LoadWide {
370        location: L,
371        byte_size: usize,
372        width: usize,
373    },
374    BinOp(TestbenchOperator),
375    TypedBinOp {
376        op: TestbenchOperator,
377        lhs_width: usize,
378        rhs_width: usize,
379        result_width: usize,
380        lhs_signed: bool,
381        rhs_signed: bool,
382    },
383    TypedUnary {
384        op: TestbenchOperator,
385        operand_width: usize,
386        result_width: usize,
387    },
388    Resize {
389        source_width: usize,
390        target_width: usize,
391        signed: bool,
392    },
393    ConcatPart {
394        part_width: usize,
395        result_width: usize,
396    },
397    Ternary {
398        then_len: usize,
399        else_len: usize,
400    },
401    LoadIndexed {
402        location: L,
403        stride_bits: usize,
404        base_bit_offset: usize,
405        element_width: usize,
406    },
407    LoadBitSelect {
408        location: L,
409        base_byte_size: usize,
410        select_width: usize,
411    },
412    StoreU64 {
413        location: L,
414        byte_size: usize,
415    },
416}
417
418#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
419pub struct ExprBytecode<L = usize> {
420    ops: Vec<ExprOpcode<L>>,
421}
422
423impl<L> ExprBytecode<L> {
424    pub fn new(ops: Vec<ExprOpcode<L>>) -> Self {
425        Self { ops }
426    }
427
428    pub fn ops(&self) -> &[ExprOpcode<L>] {
429        &self.ops
430    }
431
432    pub fn is_empty(&self) -> bool {
433        self.ops.is_empty()
434    }
435}
436
437#[derive(Clone, Debug, PartialEq, Eq)]
438pub struct BindError<A> {
439    pub address: A,
440}
441
442pub struct ExecutableArgument {
443    pub expr: CompiledExpr,
444    pub width: usize,
445    pub signed: bool,
446    pub is_string: bool,
447}
448
449pub type ExecutableAssertMessage = AssertMessage<ExecutableArgument>;
450pub type ExecutableClockCount = ClockCount<CompiledExpr>;
451pub type ExecutableLoopBound = LoopBound<CompiledExpr>;
452pub type ExecutableStatement<Event, Signal> = TestbenchStatement<
453    Event,
454    Signal,
455    CompiledExpr,
456    ExecutableArgument,
457    TestbenchTarget<Signal, CompiledExpr>,
458>;
459pub type ExecutableComponentBinding<Event, Signal> = ComponentBinding<Event, Signal, CompiledExpr>;
460
461pub struct ExecutableTestbench<Event, Signal> {
462    statements: Vec<ExecutableStatement<Event, Signal>>,
463    random_seed: Option<u64>,
464    components: Vec<TestbenchComponent>,
465    component_libraries: Vec<ComponentLibrary>,
466    component_file_base: Option<PathBuf>,
467    component_bindings: Vec<ExecutableComponentBinding<Event, Signal>>,
468}
469
470impl<Event, Signal> ExecutableTestbench<Event, Signal> {
471    pub fn new(statements: Vec<ExecutableStatement<Event, Signal>>, random_seed: u64) -> Self {
472        Self::new_with_random_seed(statements, Some(random_seed))
473    }
474
475    pub fn new_with_random_seed(
476        statements: Vec<ExecutableStatement<Event, Signal>>,
477        random_seed: Option<u64>,
478    ) -> Self {
479        Self {
480            statements,
481            random_seed,
482            components: Vec::new(),
483            component_libraries: Vec::new(),
484            component_file_base: None,
485            component_bindings: Vec::new(),
486        }
487    }
488
489    pub fn with_component_runtime(
490        mut self,
491        components: Vec<TestbenchComponent>,
492        libraries: Vec<ComponentLibrary>,
493        file_base: Option<PathBuf>,
494        bindings: Vec<ExecutableComponentBinding<Event, Signal>>,
495    ) -> Self {
496        self.components = components;
497        self.component_libraries = libraries;
498        self.component_file_base = file_base;
499        self.component_bindings = bindings;
500        self
501    }
502
503    pub fn statements(&self) -> &[ExecutableStatement<Event, Signal>] {
504        &self.statements
505    }
506
507    pub fn into_statements(self) -> Vec<ExecutableStatement<Event, Signal>> {
508        self.statements
509    }
510
511    pub fn random_seed(&self) -> u64 {
512        self.random_seed.unwrap_or_default()
513    }
514
515    pub fn configured_random_seed(&self) -> Option<u64> {
516        self.random_seed
517    }
518
519    pub fn components(&self) -> &[TestbenchComponent] {
520        &self.components
521    }
522
523    pub fn component_libraries(&self) -> &[ComponentLibrary] {
524        &self.component_libraries
525    }
526
527    pub fn component_file_base(&self) -> Option<&std::path::Path> {
528        self.component_file_base.as_deref()
529    }
530
531    pub fn component_bindings(&self) -> &[ExecutableComponentBinding<Event, Signal>] {
532        &self.component_bindings
533    }
534}
535
536impl<A> ExprBytecode<StateLocation<A>> {
537    pub fn bind_with(
538        self,
539        mut resolve: impl FnMut(&A) -> Option<usize>,
540    ) -> Result<ExprBytecode, BindError<A>> {
541        let mut ops = Vec::with_capacity(self.ops.len());
542        for op in self.ops {
543            let bound = match op {
544                ExprOpcode::ConstU64(value) => ExprOpcode::ConstU64(value),
545                ExprOpcode::ConstWide(value) => ExprOpcode::ConstWide(value),
546                ExprOpcode::LoadU64 {
547                    location,
548                    byte_size,
549                    mask,
550                } => ExprOpcode::LoadU64 {
551                    location: bind_location(location, &mut resolve)?,
552                    byte_size,
553                    mask,
554                },
555                ExprOpcode::LoadWide {
556                    location,
557                    byte_size,
558                    width,
559                } => ExprOpcode::LoadWide {
560                    location: bind_location(location, &mut resolve)?,
561                    byte_size,
562                    width,
563                },
564                ExprOpcode::BinOp(op) => ExprOpcode::BinOp(op),
565                ExprOpcode::TypedBinOp {
566                    op,
567                    lhs_width,
568                    rhs_width,
569                    result_width,
570                    lhs_signed,
571                    rhs_signed,
572                } => ExprOpcode::TypedBinOp {
573                    op,
574                    lhs_width,
575                    rhs_width,
576                    result_width,
577                    lhs_signed,
578                    rhs_signed,
579                },
580                ExprOpcode::TypedUnary {
581                    op,
582                    operand_width,
583                    result_width,
584                } => ExprOpcode::TypedUnary {
585                    op,
586                    operand_width,
587                    result_width,
588                },
589                ExprOpcode::Resize {
590                    source_width,
591                    target_width,
592                    signed,
593                } => ExprOpcode::Resize {
594                    source_width,
595                    target_width,
596                    signed,
597                },
598                ExprOpcode::ConcatPart {
599                    part_width,
600                    result_width,
601                } => ExprOpcode::ConcatPart {
602                    part_width,
603                    result_width,
604                },
605                ExprOpcode::Ternary { then_len, else_len } => {
606                    ExprOpcode::Ternary { then_len, else_len }
607                }
608                ExprOpcode::LoadIndexed {
609                    location,
610                    stride_bits,
611                    base_bit_offset,
612                    element_width,
613                } => ExprOpcode::LoadIndexed {
614                    location: bind_location(location, &mut resolve)?,
615                    stride_bits,
616                    base_bit_offset,
617                    element_width,
618                },
619                ExprOpcode::LoadBitSelect {
620                    location,
621                    base_byte_size,
622                    select_width,
623                } => ExprOpcode::LoadBitSelect {
624                    location: bind_location(location, &mut resolve)?,
625                    base_byte_size,
626                    select_width,
627                },
628                ExprOpcode::StoreU64 {
629                    location,
630                    byte_size,
631                } => ExprOpcode::StoreU64 {
632                    location: bind_location(location, &mut resolve)?,
633                    byte_size,
634                },
635            };
636            ops.push(bound);
637        }
638        Ok(ExprBytecode::new(ops))
639    }
640}
641
642fn bind_location<A>(
643    location: StateLocation<A>,
644    resolve: &mut impl FnMut(&A) -> Option<usize>,
645) -> Result<usize, BindError<A>> {
646    let Some(base) = resolve(&location.address) else {
647        return Err(BindError {
648            address: location.address,
649        });
650    };
651    Ok(base + location.byte_offset)
652}
653
654#[cfg(test)]
655mod tests {
656    use super::*;
657
658    #[test]
659    fn bytecode_uses_source_independent_operators() {
660        let code: ExprBytecode = ExprBytecode::new(vec![
661            ExprOpcode::ConstU64(1),
662            ExprOpcode::ConstU64(2),
663            ExprOpcode::TypedBinOp {
664                op: TestbenchOperator::Add,
665                lhs_width: 8,
666                rhs_width: 8,
667                result_width: 8,
668                lhs_signed: false,
669                rhs_signed: false,
670            },
671        ]);
672        assert_eq!(code.ops().len(), 3);
673        assert!(!code.is_empty());
674    }
675
676    #[test]
677    fn semantic_state_locations_bind_after_layout() {
678        let code = ExprBytecode::new(vec![ExprOpcode::LoadU64 {
679            location: StateLocation {
680                address: 7u32,
681                byte_offset: 3,
682            },
683            byte_size: 2,
684            mask: 0xffff,
685        }]);
686
687        let bound = code
688            .bind_with(|address| (*address == 7).then_some(100))
689            .unwrap();
690        assert_eq!(
691            bound.ops(),
692            &[ExprOpcode::LoadU64 {
693                location: 103,
694                byte_size: 2,
695                mask: 0xffff,
696            }]
697        );
698    }
699
700    #[test]
701    fn binding_reports_an_unmapped_semantic_address() {
702        let code = ExprBytecode::new(vec![ExprOpcode::StoreU64 {
703            location: StateLocation {
704                address: 9u32,
705                byte_offset: 0,
706            },
707            byte_size: 1,
708        }]);
709
710        assert_eq!(code.bind_with(|_| None), Err(BindError { address: 9 }));
711    }
712
713    #[test]
714    fn control_program_is_independent_of_runtime_handles() {
715        type SemanticStatement = TestbenchStatement<u32, u32, ExprBytecode<StateLocation<u32>>, ()>;
716
717        let statement = SemanticStatement::If {
718            expr: ExprBytecode::new(vec![ExprOpcode::LoadU64 {
719                location: StateLocation {
720                    address: 1,
721                    byte_offset: 0,
722                },
723                byte_size: 1,
724                mask: 1,
725            }]),
726            then_block: vec![SemanticStatement::ClockNext {
727                clock_event: 2,
728                count: ClockCount::Static(1),
729            }],
730            else_block: vec![SemanticStatement::Finish],
731        };
732
733        assert!(matches!(statement, SemanticStatement::If { .. }));
734    }
735}