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