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        /// Relative clock period for concurrent scheduling, clamped to at least two.
151        period: u64,
152    },
153    ResetAssert {
154        reset_signal: Signal,
155        reset_event: Option<Event>,
156        clock_event: Event,
157        duration: ClockCount<Expression>,
158        period: u64,
159        assert_value: u8,
160        deassert_value: u8,
161    },
162    Assert {
163        expr: Expression,
164        site_id: u32,
165        continue_on_fail: bool,
166        message: Option<AssertMessage<Argument>>,
167        location: Option<SourceLocation>,
168    },
169    Display {
170        message: Option<AssertMessage<Argument>>,
171        newline: bool,
172    },
173    If {
174        expr: Expression,
175        then_block: Vec<Self>,
176        else_block: Vec<Self>,
177    },
178    For {
179        loop_var: Option<(Signal, usize, bool)>,
180        start: LoopBound<Expression>,
181        end: LoopBound<Expression>,
182        inclusive: bool,
183        step: usize,
184        step_op: Option<TestbenchOperator>,
185        reverse: bool,
186        body: Vec<Self>,
187    },
188    Assign {
189        dst: Target,
190        expr: Expression,
191    },
192    RandomSeed {
193        handle: String,
194        value: Expression,
195    },
196    RandomGet {
197        handle: String,
198        width: u32,
199        signed: bool,
200        ret: Option<Target>,
201    },
202    RandomGetRange {
203        handle: String,
204        min: Expression,
205        max: Expression,
206        width: u32,
207        signed: bool,
208        ret: Option<Target>,
209    },
210    RandomGetSeed {
211        handle: String,
212        ret: Option<Target>,
213    },
214    ComponentMethod {
215        instance: String,
216        method: String,
217        args: Vec<Argument>,
218        ret: Option<Target>,
219        ret_width: Option<u32>,
220        ret_signed: bool,
221        ret_strict: bool,
222    },
223    Break,
224    Finish,
225    /// Apply preloaded memory data at this point in the testbench program.
226    WriteMemory {
227        signal: Signal,
228        writes: Vec<celox_design::InitialStateWriteRun>,
229    },
230}
231
232#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
233pub struct SemanticSignal<A> {
234    pub address: A,
235    pub width: usize,
236}
237
238#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
239pub struct TestbenchSelection<Expression> {
240    pub offset: Expression,
241    pub width: usize,
242}
243
244#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
245pub struct TestbenchTarget<Signal, Expression> {
246    pub signal: Signal,
247    pub selection: Option<TestbenchSelection<Expression>>,
248    pub width: usize,
249}
250
251#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
252pub struct SemanticArgument<A> {
253    pub expr: ExprBytecode<StateLocation<A>>,
254    pub width: usize,
255    pub signed: bool,
256    pub is_string: bool,
257}
258
259pub type SemanticStatement<A> = TestbenchStatement<
260    A,
261    SemanticSignal<A>,
262    ExprBytecode<StateLocation<A>>,
263    SemanticArgument<A>,
264    TestbenchTarget<SemanticSignal<A>, ExprBytecode<StateLocation<A>>>,
265>;
266pub type SemanticComponentBinding<A> =
267    ComponentBinding<A, SemanticSignal<A>, ExprBytecode<StateLocation<A>>>;
268
269#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
270pub struct TestbenchProgram<A> {
271    statements: Vec<SemanticStatement<A>>,
272    process_lengths: Vec<usize>,
273    private_variables: Vec<A>,
274    random_seed: Option<u64>,
275    components: Vec<TestbenchComponent>,
276    component_libraries: Vec<ComponentLibrary>,
277    component_file_base: Option<PathBuf>,
278    component_bindings: Vec<SemanticComponentBinding<A>>,
279}
280
281impl<A> Default for TestbenchProgram<A> {
282    fn default() -> Self {
283        Self {
284            statements: Vec::new(),
285            process_lengths: Vec::new(),
286            private_variables: Vec::new(),
287            random_seed: None,
288            components: Vec::new(),
289            component_libraries: Vec::new(),
290            component_file_base: None,
291            component_bindings: Vec::new(),
292        }
293    }
294}
295
296impl<A> TestbenchProgram<A> {
297    pub fn new(statements: Vec<SemanticStatement<A>>) -> Self {
298        Self {
299            process_lengths: vec![statements.len()],
300            private_variables: Vec::new(),
301            statements,
302            random_seed: None,
303            components: Vec::new(),
304            component_libraries: Vec::new(),
305            component_file_base: None,
306            component_bindings: Vec::new(),
307        }
308    }
309
310    /// Build independently scheduled initial processes in declaration order.
311    pub fn from_processes(processes: Vec<Vec<SemanticStatement<A>>>) -> Self {
312        let lengths = processes.iter().map(Vec::len).collect();
313        let mut program = Self::new(processes.into_iter().flatten().collect());
314        program.process_lengths = lengths;
315        program
316    }
317
318    /// Temporaries whose values belong to each independently suspended process.
319    pub fn with_private_variables(mut self, variables: Vec<A>) -> Self {
320        self.private_variables = variables;
321        self
322    }
323
324    pub fn private_variables(&self) -> &[A] {
325        &self.private_variables
326    }
327
328    pub fn process_lengths(&self) -> &[usize] {
329        &self.process_lengths
330    }
331
332    pub fn with_random_seed(mut self, random_seed: u64) -> Self {
333        self.random_seed = Some(random_seed);
334        self
335    }
336
337    pub fn with_random_seed_option(mut self, random_seed: Option<u64>) -> Self {
338        self.random_seed = random_seed;
339        self
340    }
341
342    pub fn statements(&self) -> &[SemanticStatement<A>] {
343        &self.statements
344    }
345
346    pub fn into_statements(self) -> Vec<SemanticStatement<A>> {
347        self.statements
348    }
349
350    pub fn random_seed(&self) -> u64 {
351        self.random_seed.unwrap_or_default()
352    }
353
354    pub fn configured_random_seed(&self) -> Option<u64> {
355        self.random_seed
356    }
357
358    pub fn with_components(mut self, components: Vec<TestbenchComponent>) -> Self {
359        self.components = components;
360        self
361    }
362
363    pub fn with_component_runtime(
364        mut self,
365        libraries: Vec<ComponentLibrary>,
366        file_base: Option<PathBuf>,
367        bindings: Vec<SemanticComponentBinding<A>>,
368    ) -> Self {
369        self.component_libraries = libraries;
370        self.component_file_base = file_base;
371        self.component_bindings = bindings;
372        self
373    }
374
375    pub fn components(&self) -> &[TestbenchComponent] {
376        &self.components
377    }
378
379    pub fn component_libraries(&self) -> &[ComponentLibrary] {
380        &self.component_libraries
381    }
382
383    pub fn component_file_base(&self) -> Option<&std::path::Path> {
384        self.component_file_base.as_deref()
385    }
386
387    pub fn component_bindings(&self) -> &[SemanticComponentBinding<A>] {
388        &self.component_bindings
389    }
390
391    /// Remap filesystem paths retained for runtime libraries and diagnostics.
392    /// Literal strings supplied by the design are left untouched.
393    /// A failed mapping can leave some paths remapped; discard that program.
394    pub fn try_map_paths<E>(
395        &mut self,
396        map: &mut impl FnMut(&std::path::Path) -> Result<PathBuf, E>,
397    ) -> Result<(), E> {
398        fn location<E>(
399            source: &mut Option<SourceLocation>,
400            map: &mut impl FnMut(&std::path::Path) -> Result<PathBuf, E>,
401        ) -> Result<(), E> {
402            if let Some(source) = source
403                && !source.file.is_empty()
404            {
405                source.file = map(std::path::Path::new(&source.file))?
406                    .to_string_lossy()
407                    .into_owned();
408            }
409            Ok(())
410        }
411        fn statements<A, E>(
412            block: &mut [SemanticStatement<A>],
413            map: &mut impl FnMut(&std::path::Path) -> Result<PathBuf, E>,
414        ) -> Result<(), E> {
415            for statement in block {
416                match statement {
417                    TestbenchStatement::Assert {
418                        location: source, ..
419                    } => location(source, map)?,
420                    TestbenchStatement::If {
421                        then_block,
422                        else_block,
423                        ..
424                    } => {
425                        statements(then_block, map)?;
426                        statements(else_block, map)?;
427                    }
428                    TestbenchStatement::For { body, .. } => statements(body, map)?,
429                    _ => {}
430                }
431            }
432            Ok(())
433        }
434        for library in &mut self.component_libraries {
435            library.path = map(&library.path)?;
436        }
437        if let Some(base) = &mut self.component_file_base {
438            *base = map(base)?;
439        }
440        for component in &mut self.components {
441            location(&mut component.source, map)?;
442        }
443        statements(&mut self.statements, map)
444    }
445
446    pub fn is_empty(&self) -> bool {
447        self.statements.is_empty()
448    }
449}
450
451#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
452pub enum ExprOpcode<L = usize> {
453    ConstU64(u64),
454    ConstWide(BigUint),
455    LoadU64 {
456        location: L,
457        byte_size: usize,
458        mask: u64,
459    },
460    LoadWide {
461        location: L,
462        byte_size: usize,
463        width: usize,
464    },
465    BinOp(TestbenchOperator),
466    TypedBinOp {
467        op: TestbenchOperator,
468        lhs_width: usize,
469        rhs_width: usize,
470        result_width: usize,
471        lhs_signed: bool,
472        rhs_signed: bool,
473    },
474    TypedUnary {
475        op: TestbenchOperator,
476        operand_width: usize,
477        result_width: usize,
478    },
479    Resize {
480        source_width: usize,
481        target_width: usize,
482        signed: bool,
483    },
484    ConcatPart {
485        part_width: usize,
486        result_width: usize,
487    },
488    Ternary {
489        then_len: usize,
490        else_len: usize,
491    },
492    LoadIndexed {
493        location: L,
494        stride_bits: usize,
495        base_bit_offset: usize,
496        element_width: usize,
497    },
498    LoadBitSelect {
499        location: L,
500        base_byte_size: usize,
501        select_width: usize,
502    },
503    StoreU64 {
504        location: L,
505        byte_size: usize,
506    },
507}
508
509#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
510pub struct ExprBytecode<L = usize> {
511    ops: Vec<ExprOpcode<L>>,
512}
513
514impl<L> ExprBytecode<L> {
515    pub fn new(ops: Vec<ExprOpcode<L>>) -> Self {
516        Self { ops }
517    }
518
519    pub fn ops(&self) -> &[ExprOpcode<L>] {
520        &self.ops
521    }
522
523    pub fn is_empty(&self) -> bool {
524        self.ops.is_empty()
525    }
526}
527
528#[derive(Clone, Debug, PartialEq, Eq)]
529pub struct BindError<A> {
530    pub address: A,
531}
532
533pub struct ExecutableArgument {
534    pub expr: CompiledExpr,
535    pub width: usize,
536    pub signed: bool,
537    pub is_string: bool,
538}
539
540pub type ExecutableAssertMessage = AssertMessage<ExecutableArgument>;
541pub type ExecutableClockCount = ClockCount<CompiledExpr>;
542pub type ExecutableLoopBound = LoopBound<CompiledExpr>;
543pub type ExecutableStatement<Event, Signal> = TestbenchStatement<
544    Event,
545    Signal,
546    CompiledExpr,
547    ExecutableArgument,
548    TestbenchTarget<Signal, CompiledExpr>,
549>;
550pub type ExecutableComponentBinding<Event, Signal> = ComponentBinding<Event, Signal, CompiledExpr>;
551
552pub struct ExecutableTestbench<Event, Signal> {
553    statements: Vec<ExecutableStatement<Event, Signal>>,
554    process_lengths: Vec<usize>,
555    private_signals: Vec<Signal>,
556    random_seed: Option<u64>,
557    components: Vec<TestbenchComponent>,
558    component_libraries: Vec<ComponentLibrary>,
559    component_file_base: Option<PathBuf>,
560    component_bindings: Vec<ExecutableComponentBinding<Event, Signal>>,
561}
562
563impl<Event, Signal> ExecutableTestbench<Event, Signal> {
564    pub fn new(statements: Vec<ExecutableStatement<Event, Signal>>, random_seed: u64) -> Self {
565        Self::new_with_random_seed(statements, Some(random_seed))
566    }
567
568    pub fn new_with_random_seed(
569        statements: Vec<ExecutableStatement<Event, Signal>>,
570        random_seed: Option<u64>,
571    ) -> Self {
572        Self {
573            process_lengths: vec![statements.len()],
574            private_signals: Vec::new(),
575            statements,
576            random_seed,
577            components: Vec::new(),
578            component_libraries: Vec::new(),
579            component_file_base: None,
580            component_bindings: Vec::new(),
581        }
582    }
583
584    pub fn with_component_runtime(
585        mut self,
586        components: Vec<TestbenchComponent>,
587        libraries: Vec<ComponentLibrary>,
588        file_base: Option<PathBuf>,
589        bindings: Vec<ExecutableComponentBinding<Event, Signal>>,
590    ) -> Self {
591        self.components = components;
592        self.component_libraries = libraries;
593        self.component_file_base = file_base;
594        self.component_bindings = bindings;
595        self
596    }
597
598    /// Preserve process boundaries after binding a semantic program.
599    pub fn from_processes(
600        processes: Vec<Vec<ExecutableStatement<Event, Signal>>>,
601        random_seed: Option<u64>,
602    ) -> Self {
603        let lengths = processes.iter().map(Vec::len).collect();
604        let mut program =
605            Self::new_with_random_seed(processes.into_iter().flatten().collect(), random_seed);
606        program.process_lengths = lengths;
607        program
608    }
609
610    pub fn with_private_signals(mut self, signals: Vec<Signal>) -> Self {
611        self.private_signals = signals;
612        self
613    }
614
615    pub fn private_signals(&self) -> &[Signal] {
616        &self.private_signals
617    }
618
619    pub fn processes(&self) -> impl Iterator<Item = &[ExecutableStatement<Event, Signal>]> {
620        self.process_lengths.iter().scan(0, |offset, &length| {
621            let start = *offset;
622            *offset += length;
623            Some(&self.statements[start..*offset])
624        })
625    }
626
627    pub fn statements(&self) -> &[ExecutableStatement<Event, Signal>] {
628        &self.statements
629    }
630
631    pub fn into_statements(self) -> Vec<ExecutableStatement<Event, Signal>> {
632        self.statements
633    }
634
635    pub fn random_seed(&self) -> u64 {
636        self.random_seed.unwrap_or_default()
637    }
638
639    pub fn configured_random_seed(&self) -> Option<u64> {
640        self.random_seed
641    }
642
643    pub fn components(&self) -> &[TestbenchComponent] {
644        &self.components
645    }
646
647    pub fn component_libraries(&self) -> &[ComponentLibrary] {
648        &self.component_libraries
649    }
650
651    pub fn component_file_base(&self) -> Option<&std::path::Path> {
652        self.component_file_base.as_deref()
653    }
654
655    pub fn component_bindings(&self) -> &[ExecutableComponentBinding<Event, Signal>] {
656        &self.component_bindings
657    }
658}
659
660impl<A> ExprBytecode<StateLocation<A>> {
661    pub fn bind_with(
662        self,
663        mut resolve: impl FnMut(&A) -> Option<usize>,
664    ) -> Result<ExprBytecode, BindError<A>> {
665        let mut ops = Vec::with_capacity(self.ops.len());
666        for op in self.ops {
667            let bound = match op {
668                ExprOpcode::ConstU64(value) => ExprOpcode::ConstU64(value),
669                ExprOpcode::ConstWide(value) => ExprOpcode::ConstWide(value),
670                ExprOpcode::LoadU64 {
671                    location,
672                    byte_size,
673                    mask,
674                } => ExprOpcode::LoadU64 {
675                    location: bind_location(location, &mut resolve)?,
676                    byte_size,
677                    mask,
678                },
679                ExprOpcode::LoadWide {
680                    location,
681                    byte_size,
682                    width,
683                } => ExprOpcode::LoadWide {
684                    location: bind_location(location, &mut resolve)?,
685                    byte_size,
686                    width,
687                },
688                ExprOpcode::BinOp(op) => ExprOpcode::BinOp(op),
689                ExprOpcode::TypedBinOp {
690                    op,
691                    lhs_width,
692                    rhs_width,
693                    result_width,
694                    lhs_signed,
695                    rhs_signed,
696                } => ExprOpcode::TypedBinOp {
697                    op,
698                    lhs_width,
699                    rhs_width,
700                    result_width,
701                    lhs_signed,
702                    rhs_signed,
703                },
704                ExprOpcode::TypedUnary {
705                    op,
706                    operand_width,
707                    result_width,
708                } => ExprOpcode::TypedUnary {
709                    op,
710                    operand_width,
711                    result_width,
712                },
713                ExprOpcode::Resize {
714                    source_width,
715                    target_width,
716                    signed,
717                } => ExprOpcode::Resize {
718                    source_width,
719                    target_width,
720                    signed,
721                },
722                ExprOpcode::ConcatPart {
723                    part_width,
724                    result_width,
725                } => ExprOpcode::ConcatPart {
726                    part_width,
727                    result_width,
728                },
729                ExprOpcode::Ternary { then_len, else_len } => {
730                    ExprOpcode::Ternary { then_len, else_len }
731                }
732                ExprOpcode::LoadIndexed {
733                    location,
734                    stride_bits,
735                    base_bit_offset,
736                    element_width,
737                } => ExprOpcode::LoadIndexed {
738                    location: bind_location(location, &mut resolve)?,
739                    stride_bits,
740                    base_bit_offset,
741                    element_width,
742                },
743                ExprOpcode::LoadBitSelect {
744                    location,
745                    base_byte_size,
746                    select_width,
747                } => ExprOpcode::LoadBitSelect {
748                    location: bind_location(location, &mut resolve)?,
749                    base_byte_size,
750                    select_width,
751                },
752                ExprOpcode::StoreU64 {
753                    location,
754                    byte_size,
755                } => ExprOpcode::StoreU64 {
756                    location: bind_location(location, &mut resolve)?,
757                    byte_size,
758                },
759            };
760            ops.push(bound);
761        }
762        Ok(ExprBytecode::new(ops))
763    }
764}
765
766fn bind_location<A>(
767    location: StateLocation<A>,
768    resolve: &mut impl FnMut(&A) -> Option<usize>,
769) -> Result<usize, BindError<A>> {
770    let Some(base) = resolve(&location.address) else {
771        return Err(BindError {
772            address: location.address,
773        });
774    };
775    Ok(base + location.byte_offset)
776}
777
778#[cfg(test)]
779mod tests {
780    use super::*;
781
782    #[test]
783    fn bytecode_uses_source_independent_operators() {
784        let code: ExprBytecode = ExprBytecode::new(vec![
785            ExprOpcode::ConstU64(1),
786            ExprOpcode::ConstU64(2),
787            ExprOpcode::TypedBinOp {
788                op: TestbenchOperator::Add,
789                lhs_width: 8,
790                rhs_width: 8,
791                result_width: 8,
792                lhs_signed: false,
793                rhs_signed: false,
794            },
795        ]);
796        assert_eq!(code.ops().len(), 3);
797        assert!(!code.is_empty());
798    }
799
800    #[test]
801    fn semantic_state_locations_bind_after_layout() {
802        let code = ExprBytecode::new(vec![ExprOpcode::LoadU64 {
803            location: StateLocation {
804                address: 7u32,
805                byte_offset: 3,
806            },
807            byte_size: 2,
808            mask: 0xffff,
809        }]);
810
811        let bound = code
812            .bind_with(|address| (*address == 7).then_some(100))
813            .unwrap();
814        assert_eq!(
815            bound.ops(),
816            &[ExprOpcode::LoadU64 {
817                location: 103,
818                byte_size: 2,
819                mask: 0xffff,
820            }]
821        );
822    }
823
824    #[test]
825    fn binding_reports_an_unmapped_semantic_address() {
826        let code = ExprBytecode::new(vec![ExprOpcode::StoreU64 {
827            location: StateLocation {
828                address: 9u32,
829                byte_offset: 0,
830            },
831            byte_size: 1,
832        }]);
833
834        assert_eq!(code.bind_with(|_| None), Err(BindError { address: 9 }));
835    }
836
837    #[test]
838    fn control_program_is_independent_of_runtime_handles() {
839        type SemanticStatement = TestbenchStatement<u32, u32, ExprBytecode<StateLocation<u32>>, ()>;
840
841        let statement = SemanticStatement::If {
842            expr: ExprBytecode::new(vec![ExprOpcode::LoadU64 {
843                location: StateLocation {
844                    address: 1,
845                    byte_offset: 0,
846                },
847                byte_size: 1,
848                mask: 1,
849            }]),
850            then_block: vec![SemanticStatement::ClockNext {
851                clock_event: 2,
852                count: ClockCount::Static(1),
853                period: 2,
854            }],
855            else_block: vec![SemanticStatement::Finish],
856        };
857
858        assert!(matches!(statement, SemanticStatement::If { .. }));
859    }
860}