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    /// Remap filesystem paths retained for runtime libraries and diagnostics.
356    /// Literal strings supplied by the design are left untouched.
357    /// A failed mapping can leave some paths remapped; discard that program.
358    pub fn try_map_paths<E>(
359        &mut self,
360        map: &mut impl FnMut(&std::path::Path) -> Result<PathBuf, E>,
361    ) -> Result<(), E> {
362        fn location<E>(
363            source: &mut Option<SourceLocation>,
364            map: &mut impl FnMut(&std::path::Path) -> Result<PathBuf, E>,
365        ) -> Result<(), E> {
366            if let Some(source) = source
367                && !source.file.is_empty()
368            {
369                source.file = map(std::path::Path::new(&source.file))?
370                    .to_string_lossy()
371                    .into_owned();
372            }
373            Ok(())
374        }
375        fn statements<A, E>(
376            block: &mut [SemanticStatement<A>],
377            map: &mut impl FnMut(&std::path::Path) -> Result<PathBuf, E>,
378        ) -> Result<(), E> {
379            for statement in block {
380                match statement {
381                    TestbenchStatement::Assert {
382                        location: source, ..
383                    } => location(source, map)?,
384                    TestbenchStatement::If {
385                        then_block,
386                        else_block,
387                        ..
388                    } => {
389                        statements(then_block, map)?;
390                        statements(else_block, map)?;
391                    }
392                    TestbenchStatement::For { body, .. } => statements(body, map)?,
393                    _ => {}
394                }
395            }
396            Ok(())
397        }
398        for library in &mut self.component_libraries {
399            library.path = map(&library.path)?;
400        }
401        if let Some(base) = &mut self.component_file_base {
402            *base = map(base)?;
403        }
404        for component in &mut self.components {
405            location(&mut component.source, map)?;
406        }
407        statements(&mut self.statements, map)
408    }
409
410    pub fn is_empty(&self) -> bool {
411        self.statements.is_empty()
412    }
413}
414
415#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
416pub enum ExprOpcode<L = usize> {
417    ConstU64(u64),
418    ConstWide(BigUint),
419    LoadU64 {
420        location: L,
421        byte_size: usize,
422        mask: u64,
423    },
424    LoadWide {
425        location: L,
426        byte_size: usize,
427        width: usize,
428    },
429    BinOp(TestbenchOperator),
430    TypedBinOp {
431        op: TestbenchOperator,
432        lhs_width: usize,
433        rhs_width: usize,
434        result_width: usize,
435        lhs_signed: bool,
436        rhs_signed: bool,
437    },
438    TypedUnary {
439        op: TestbenchOperator,
440        operand_width: usize,
441        result_width: usize,
442    },
443    Resize {
444        source_width: usize,
445        target_width: usize,
446        signed: bool,
447    },
448    ConcatPart {
449        part_width: usize,
450        result_width: usize,
451    },
452    Ternary {
453        then_len: usize,
454        else_len: usize,
455    },
456    LoadIndexed {
457        location: L,
458        stride_bits: usize,
459        base_bit_offset: usize,
460        element_width: usize,
461    },
462    LoadBitSelect {
463        location: L,
464        base_byte_size: usize,
465        select_width: usize,
466    },
467    StoreU64 {
468        location: L,
469        byte_size: usize,
470    },
471}
472
473#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
474pub struct ExprBytecode<L = usize> {
475    ops: Vec<ExprOpcode<L>>,
476}
477
478impl<L> ExprBytecode<L> {
479    pub fn new(ops: Vec<ExprOpcode<L>>) -> Self {
480        Self { ops }
481    }
482
483    pub fn ops(&self) -> &[ExprOpcode<L>] {
484        &self.ops
485    }
486
487    pub fn is_empty(&self) -> bool {
488        self.ops.is_empty()
489    }
490}
491
492#[derive(Clone, Debug, PartialEq, Eq)]
493pub struct BindError<A> {
494    pub address: A,
495}
496
497pub struct ExecutableArgument {
498    pub expr: CompiledExpr,
499    pub width: usize,
500    pub signed: bool,
501    pub is_string: bool,
502}
503
504pub type ExecutableAssertMessage = AssertMessage<ExecutableArgument>;
505pub type ExecutableClockCount = ClockCount<CompiledExpr>;
506pub type ExecutableLoopBound = LoopBound<CompiledExpr>;
507pub type ExecutableStatement<Event, Signal> = TestbenchStatement<
508    Event,
509    Signal,
510    CompiledExpr,
511    ExecutableArgument,
512    TestbenchTarget<Signal, CompiledExpr>,
513>;
514pub type ExecutableComponentBinding<Event, Signal> = ComponentBinding<Event, Signal, CompiledExpr>;
515
516pub struct ExecutableTestbench<Event, Signal> {
517    statements: Vec<ExecutableStatement<Event, Signal>>,
518    random_seed: Option<u64>,
519    components: Vec<TestbenchComponent>,
520    component_libraries: Vec<ComponentLibrary>,
521    component_file_base: Option<PathBuf>,
522    component_bindings: Vec<ExecutableComponentBinding<Event, Signal>>,
523}
524
525impl<Event, Signal> ExecutableTestbench<Event, Signal> {
526    pub fn new(statements: Vec<ExecutableStatement<Event, Signal>>, random_seed: u64) -> Self {
527        Self::new_with_random_seed(statements, Some(random_seed))
528    }
529
530    pub fn new_with_random_seed(
531        statements: Vec<ExecutableStatement<Event, Signal>>,
532        random_seed: Option<u64>,
533    ) -> Self {
534        Self {
535            statements,
536            random_seed,
537            components: Vec::new(),
538            component_libraries: Vec::new(),
539            component_file_base: None,
540            component_bindings: Vec::new(),
541        }
542    }
543
544    pub fn with_component_runtime(
545        mut self,
546        components: Vec<TestbenchComponent>,
547        libraries: Vec<ComponentLibrary>,
548        file_base: Option<PathBuf>,
549        bindings: Vec<ExecutableComponentBinding<Event, Signal>>,
550    ) -> Self {
551        self.components = components;
552        self.component_libraries = libraries;
553        self.component_file_base = file_base;
554        self.component_bindings = bindings;
555        self
556    }
557
558    pub fn statements(&self) -> &[ExecutableStatement<Event, Signal>] {
559        &self.statements
560    }
561
562    pub fn into_statements(self) -> Vec<ExecutableStatement<Event, Signal>> {
563        self.statements
564    }
565
566    pub fn random_seed(&self) -> u64 {
567        self.random_seed.unwrap_or_default()
568    }
569
570    pub fn configured_random_seed(&self) -> Option<u64> {
571        self.random_seed
572    }
573
574    pub fn components(&self) -> &[TestbenchComponent] {
575        &self.components
576    }
577
578    pub fn component_libraries(&self) -> &[ComponentLibrary] {
579        &self.component_libraries
580    }
581
582    pub fn component_file_base(&self) -> Option<&std::path::Path> {
583        self.component_file_base.as_deref()
584    }
585
586    pub fn component_bindings(&self) -> &[ExecutableComponentBinding<Event, Signal>] {
587        &self.component_bindings
588    }
589}
590
591impl<A> ExprBytecode<StateLocation<A>> {
592    pub fn bind_with(
593        self,
594        mut resolve: impl FnMut(&A) -> Option<usize>,
595    ) -> Result<ExprBytecode, BindError<A>> {
596        let mut ops = Vec::with_capacity(self.ops.len());
597        for op in self.ops {
598            let bound = match op {
599                ExprOpcode::ConstU64(value) => ExprOpcode::ConstU64(value),
600                ExprOpcode::ConstWide(value) => ExprOpcode::ConstWide(value),
601                ExprOpcode::LoadU64 {
602                    location,
603                    byte_size,
604                    mask,
605                } => ExprOpcode::LoadU64 {
606                    location: bind_location(location, &mut resolve)?,
607                    byte_size,
608                    mask,
609                },
610                ExprOpcode::LoadWide {
611                    location,
612                    byte_size,
613                    width,
614                } => ExprOpcode::LoadWide {
615                    location: bind_location(location, &mut resolve)?,
616                    byte_size,
617                    width,
618                },
619                ExprOpcode::BinOp(op) => ExprOpcode::BinOp(op),
620                ExprOpcode::TypedBinOp {
621                    op,
622                    lhs_width,
623                    rhs_width,
624                    result_width,
625                    lhs_signed,
626                    rhs_signed,
627                } => ExprOpcode::TypedBinOp {
628                    op,
629                    lhs_width,
630                    rhs_width,
631                    result_width,
632                    lhs_signed,
633                    rhs_signed,
634                },
635                ExprOpcode::TypedUnary {
636                    op,
637                    operand_width,
638                    result_width,
639                } => ExprOpcode::TypedUnary {
640                    op,
641                    operand_width,
642                    result_width,
643                },
644                ExprOpcode::Resize {
645                    source_width,
646                    target_width,
647                    signed,
648                } => ExprOpcode::Resize {
649                    source_width,
650                    target_width,
651                    signed,
652                },
653                ExprOpcode::ConcatPart {
654                    part_width,
655                    result_width,
656                } => ExprOpcode::ConcatPart {
657                    part_width,
658                    result_width,
659                },
660                ExprOpcode::Ternary { then_len, else_len } => {
661                    ExprOpcode::Ternary { then_len, else_len }
662                }
663                ExprOpcode::LoadIndexed {
664                    location,
665                    stride_bits,
666                    base_bit_offset,
667                    element_width,
668                } => ExprOpcode::LoadIndexed {
669                    location: bind_location(location, &mut resolve)?,
670                    stride_bits,
671                    base_bit_offset,
672                    element_width,
673                },
674                ExprOpcode::LoadBitSelect {
675                    location,
676                    base_byte_size,
677                    select_width,
678                } => ExprOpcode::LoadBitSelect {
679                    location: bind_location(location, &mut resolve)?,
680                    base_byte_size,
681                    select_width,
682                },
683                ExprOpcode::StoreU64 {
684                    location,
685                    byte_size,
686                } => ExprOpcode::StoreU64 {
687                    location: bind_location(location, &mut resolve)?,
688                    byte_size,
689                },
690            };
691            ops.push(bound);
692        }
693        Ok(ExprBytecode::new(ops))
694    }
695}
696
697fn bind_location<A>(
698    location: StateLocation<A>,
699    resolve: &mut impl FnMut(&A) -> Option<usize>,
700) -> Result<usize, BindError<A>> {
701    let Some(base) = resolve(&location.address) else {
702        return Err(BindError {
703            address: location.address,
704        });
705    };
706    Ok(base + location.byte_offset)
707}
708
709#[cfg(test)]
710mod tests {
711    use super::*;
712
713    #[test]
714    fn bytecode_uses_source_independent_operators() {
715        let code: ExprBytecode = ExprBytecode::new(vec![
716            ExprOpcode::ConstU64(1),
717            ExprOpcode::ConstU64(2),
718            ExprOpcode::TypedBinOp {
719                op: TestbenchOperator::Add,
720                lhs_width: 8,
721                rhs_width: 8,
722                result_width: 8,
723                lhs_signed: false,
724                rhs_signed: false,
725            },
726        ]);
727        assert_eq!(code.ops().len(), 3);
728        assert!(!code.is_empty());
729    }
730
731    #[test]
732    fn semantic_state_locations_bind_after_layout() {
733        let code = ExprBytecode::new(vec![ExprOpcode::LoadU64 {
734            location: StateLocation {
735                address: 7u32,
736                byte_offset: 3,
737            },
738            byte_size: 2,
739            mask: 0xffff,
740        }]);
741
742        let bound = code
743            .bind_with(|address| (*address == 7).then_some(100))
744            .unwrap();
745        assert_eq!(
746            bound.ops(),
747            &[ExprOpcode::LoadU64 {
748                location: 103,
749                byte_size: 2,
750                mask: 0xffff,
751            }]
752        );
753    }
754
755    #[test]
756    fn binding_reports_an_unmapped_semantic_address() {
757        let code = ExprBytecode::new(vec![ExprOpcode::StoreU64 {
758            location: StateLocation {
759                address: 9u32,
760                byte_offset: 0,
761            },
762            byte_size: 1,
763        }]);
764
765        assert_eq!(code.bind_with(|_| None), Err(BindError { address: 9 }));
766    }
767
768    #[test]
769    fn control_program_is_independent_of_runtime_handles() {
770        type SemanticStatement = TestbenchStatement<u32, u32, ExprBytecode<StateLocation<u32>>, ()>;
771
772        let statement = SemanticStatement::If {
773            expr: ExprBytecode::new(vec![ExprOpcode::LoadU64 {
774                location: StateLocation {
775                    address: 1,
776                    byte_offset: 0,
777                },
778                byte_size: 1,
779                mask: 1,
780            }]),
781            then_block: vec![SemanticStatement::ClockNext {
782                clock_event: 2,
783                count: ClockCount::Static(1),
784            }],
785            else_block: vec![SemanticStatement::Finish],
786        };
787
788        assert!(matches!(statement, SemanticStatement::If { .. }));
789    }
790}