Skip to main content

celox_testbench/
vm.rs

1use num_bigint::{BigInt, BigUint, Sign};
2use num_traits::ToPrimitive as _;
3
4use crate::{ExprBytecode, ExprOpcode as TbOpcode, TestbenchOperator as Op};
5
6// ── Bytecode VM ────────────────────────────────────────────────────────
7
8/// A compiled expression: flat bytecode evaluated on a stack VM.
9#[derive(Clone, Debug)]
10pub struct CompiledExpr {
11    bytecode: ExprBytecode,
12}
13
14/// Stack value: either a native u64 or a heap-allocated BigUint.
15#[derive(Clone, Debug)]
16pub enum TestbenchValue {
17    U64(u64),
18    Wide(BigUint),
19}
20
21impl TestbenchValue {
22    #[inline]
23    pub fn to_u64(&self) -> u64 {
24        match self {
25            TestbenchValue::U64(v) => *v,
26            TestbenchValue::Wide(v) => {
27                let digits = v.to_u64_digits();
28                digits.first().copied().unwrap_or(0)
29            }
30        }
31    }
32
33    #[inline]
34    pub fn is_zero(&self) -> bool {
35        match self {
36            TestbenchValue::U64(v) => *v == 0,
37            TestbenchValue::Wide(v) => *v == BigUint::ZERO,
38        }
39    }
40
41    #[inline]
42    pub fn to_biguint(&self) -> BigUint {
43        match self {
44            TestbenchValue::U64(v) => BigUint::from(*v),
45            TestbenchValue::Wide(v) => v.clone(),
46        }
47    }
48}
49
50impl CompiledExpr {
51    pub fn new(bytecode: ExprBytecode) -> Self {
52        Self { bytecode }
53    }
54
55    /// Evaluate against raw simulator memory, returning the result as u64.
56    /// For wide results, returns the low 64 bits.
57    pub fn eval_u64(&self, memory: *mut u8) -> u64 {
58        self.eval(memory).to_u64()
59    }
60
61    /// Evaluate and return the full `TestbenchValue` (preserves wide results).
62    pub fn eval_value(&self, memory: *mut u8) -> TestbenchValue {
63        self.eval(memory)
64    }
65
66    pub fn eval_bool(&self, memory: *mut u8) -> bool {
67        !self.eval(memory).is_zero()
68    }
69
70    /// Returns the value when this expression does not read simulator memory.
71    pub fn constant_u64(&self) -> Option<u64> {
72        if self.bytecode.ops().iter().any(|op| {
73            matches!(
74                op,
75                TbOpcode::LoadU64 { .. }
76                    | TbOpcode::LoadWide { .. }
77                    | TbOpcode::LoadIndexed { .. }
78                    | TbOpcode::LoadBitSelect { .. }
79                    | TbOpcode::StoreU64 { .. }
80            )
81        }) {
82            return None;
83        }
84        Some(self.eval_u64(std::ptr::null_mut()))
85    }
86
87    /// Core evaluation loop.  Uses `TestbenchValue` to handle both u64 and wide
88    /// signals on a single stack.  The common case (all ≤64-bit operands)
89    /// stays in the `TestbenchValue::U64` variant and never allocates.
90    fn eval(&self, memory: *mut u8) -> TestbenchValue {
91        let mut stack: Vec<TestbenchValue> = Vec::with_capacity(16);
92        let mut pc: usize = 0;
93        let ops = self.bytecode.ops();
94
95        while pc < ops.len() {
96            self.exec_at(ops, &mut pc, &mut stack, memory);
97        }
98        stack.pop().unwrap_or_else(|| {
99            debug_assert!(false, "testbench bytecode: stack empty after evaluation");
100            TestbenchValue::U64(0)
101        })
102    }
103
104    /// Execute the opcode at `pc` and advance `pc` past it.
105    /// Handles all opcodes including `Ternary` (with recursive sub-block
106    /// evaluation), so there is no separate `step()` function.
107    fn exec_at(
108        &self,
109        ops: &[TbOpcode],
110        pc: &mut usize,
111        stack: &mut Vec<TestbenchValue>,
112        memory: *mut u8,
113    ) {
114        match &ops[*pc] {
115            TbOpcode::ConstU64(v) => {
116                stack.push(TestbenchValue::U64(*v));
117                *pc += 1;
118            }
119            TbOpcode::ConstWide(v) => {
120                stack.push(TestbenchValue::Wide(v.clone()));
121                *pc += 1;
122            }
123            TbOpcode::LoadU64 {
124                location,
125                byte_size,
126                mask,
127            } => {
128                // SAFETY: caller guarantees `memory` is valid simulator memory
129                let val = unsafe { read_le_u64(memory.add(*location), *byte_size) } & mask;
130                stack.push(TestbenchValue::U64(val));
131                *pc += 1;
132            }
133            TbOpcode::LoadWide {
134                location,
135                byte_size,
136                width,
137            } => {
138                let val = unsafe { read_le_wide(memory.add(*location), *byte_size, *width) };
139                stack.push(TestbenchValue::Wide(val));
140                *pc += 1;
141            }
142            TbOpcode::BinOp(op) => {
143                let r = stack.pop().unwrap_or_else(|| {
144                    debug_assert!(false, "testbench bytecode: BinOp rhs underflow");
145                    TestbenchValue::U64(0)
146                });
147                let l = stack.pop().unwrap_or_else(|| {
148                    debug_assert!(false, "testbench bytecode: BinOp lhs underflow");
149                    TestbenchValue::U64(0)
150                });
151                stack.push(eval_binop(l, *op, r));
152                *pc += 1;
153            }
154            TbOpcode::TypedBinOp {
155                op,
156                lhs_width,
157                rhs_width,
158                result_width,
159                lhs_signed,
160                rhs_signed,
161            } => {
162                let r = stack.pop().unwrap_or_else(|| {
163                    debug_assert!(false, "testbench bytecode: TypedBinOp rhs underflow");
164                    TestbenchValue::U64(0)
165                });
166                let l = stack.pop().unwrap_or_else(|| {
167                    debug_assert!(false, "testbench bytecode: TypedBinOp lhs underflow");
168                    TestbenchValue::U64(0)
169                });
170                stack.push(eval_typed_binop(
171                    l,
172                    *op,
173                    r,
174                    *lhs_width,
175                    *rhs_width,
176                    *result_width,
177                    *lhs_signed,
178                    *rhs_signed,
179                ));
180                *pc += 1;
181            }
182            TbOpcode::TypedUnary {
183                op,
184                operand_width,
185                result_width,
186            } => {
187                if let Some(top) = stack.last_mut() {
188                    *top = eval_typed_unop(*op, top, *operand_width, *result_width);
189                } else {
190                    debug_assert!(false, "testbench bytecode: TypedUnary underflow");
191                }
192                *pc += 1;
193            }
194            TbOpcode::Resize {
195                source_width,
196                target_width,
197                signed,
198            } => {
199                if let Some(top) = stack.last_mut() {
200                    *top = resize_tb_value(top, *source_width, *target_width, *signed);
201                } else {
202                    debug_assert!(false, "testbench bytecode: Resize underflow");
203                }
204                *pc += 1;
205            }
206            TbOpcode::ConcatPart {
207                part_width,
208                result_width,
209            } => {
210                let part = stack.pop().unwrap_or_else(|| {
211                    debug_assert!(false, "testbench bytecode: ConcatPart value underflow");
212                    TestbenchValue::U64(0)
213                });
214                let accumulator = stack.pop().unwrap_or_else(|| {
215                    debug_assert!(
216                        false,
217                        "testbench bytecode: ConcatPart accumulator underflow"
218                    );
219                    TestbenchValue::U64(0)
220                });
221                if let (TestbenchValue::U64(accumulator), TestbenchValue::U64(part)) =
222                    (&accumulator, &part)
223                    && *result_width <= 64
224                {
225                    let shifted = if *part_width >= 64 {
226                        0
227                    } else {
228                        accumulator << part_width
229                    };
230                    stack.push(TestbenchValue::U64(
231                        shifted | (part & width_mask_u64(*part_width)),
232                    ));
233                } else {
234                    let value = (accumulator.to_biguint() << part_width)
235                        | normalized_bits(&part, *part_width);
236                    stack.push(tb_value_from_bits(value, *result_width));
237                }
238                *pc += 1;
239            }
240            TbOpcode::Ternary { then_len, else_len } => {
241                let cond = stack.pop().unwrap_or_else(|| {
242                    debug_assert!(false, "testbench bytecode: Ternary cond underflow");
243                    TestbenchValue::U64(0)
244                });
245                *pc += 1; // skip past Ternary opcode
246                if !cond.is_zero() {
247                    let then_end = *pc + then_len;
248                    while *pc < then_end {
249                        self.exec_at(ops, pc, stack, memory);
250                    }
251                    *pc += else_len; // skip else block
252                } else {
253                    *pc += then_len; // skip then block
254                    let else_end = *pc + else_len;
255                    while *pc < else_end {
256                        self.exec_at(ops, pc, stack, memory);
257                    }
258                }
259            }
260            TbOpcode::LoadIndexed {
261                location,
262                stride_bits,
263                base_bit_offset,
264                element_width,
265            } => {
266                let idx = stack.pop().unwrap_or_else(|| {
267                    debug_assert!(false, "testbench bytecode: LoadIndexed underflow");
268                    TestbenchValue::U64(0)
269                });
270                let i = idx.to_u64() as usize;
271                let bit_offset = base_bit_offset.saturating_add(i.saturating_mul(*stride_bits));
272                let val = unsafe { read_bits(memory.add(*location), bit_offset, *element_width) };
273                stack.push(val);
274                *pc += 1;
275            }
276            TbOpcode::LoadBitSelect {
277                location,
278                base_byte_size,
279                select_width,
280            } => {
281                let bit_idx = stack.pop().unwrap_or_else(|| {
282                    debug_assert!(false, "testbench bytecode: LoadBitSelect underflow");
283                    TestbenchValue::U64(0)
284                });
285                let shift = bit_idx.to_u64() as usize;
286                if *base_byte_size <= 8 && *select_width <= 64 {
287                    let full_val = unsafe { read_le_u64(memory.add(*location), *base_byte_size) };
288                    let mask = if *select_width == 64 {
289                        u64::MAX
290                    } else {
291                        (1u64 << select_width) - 1
292                    };
293                    stack.push(TestbenchValue::U64((full_val >> shift) & mask));
294                } else {
295                    let full_width = base_byte_size.saturating_mul(8);
296                    let full_val =
297                        unsafe { read_le_wide(memory.add(*location), *base_byte_size, full_width) };
298                    let val = (full_val >> shift) & width_mask(*select_width);
299                    stack.push(tb_value_from_bits(val, *select_width));
300                }
301                *pc += 1;
302            }
303            TbOpcode::StoreU64 {
304                location,
305                byte_size,
306            } => {
307                let val = stack.pop().unwrap_or_else(|| {
308                    debug_assert!(false, "testbench bytecode: StoreU64 underflow");
309                    TestbenchValue::U64(0)
310                });
311                let v = val.to_u64();
312                let bytes = v.to_le_bytes();
313                let n = (*byte_size).min(8);
314                unsafe {
315                    std::ptr::copy_nonoverlapping(bytes.as_ptr(), memory.add(*location), n);
316                }
317                *pc += 1;
318            }
319        }
320    }
321}
322
323/// # Safety
324/// `ptr` must be valid for `byte_size` bytes of read access.
325#[inline(always)]
326unsafe fn read_le_u64(ptr: *const u8, byte_size: usize) -> u64 {
327    let mut buf = [0u8; 8];
328    unsafe {
329        std::ptr::copy_nonoverlapping(ptr, buf.as_mut_ptr(), byte_size.min(8));
330    }
331    u64::from_le_bytes(buf)
332}
333
334/// # Safety
335/// `ptr` must be valid for `byte_size` bytes of read access.
336unsafe fn read_le_wide(ptr: *const u8, byte_size: usize, width: usize) -> BigUint {
337    let mut buf = vec![0u8; byte_size];
338    unsafe {
339        std::ptr::copy_nonoverlapping(ptr, buf.as_mut_ptr(), byte_size);
340    }
341    let mut val = BigUint::from_bytes_le(&buf);
342    let extra_bits = byte_size * 8 - width;
343    if extra_bits > 0 {
344        val &= (BigUint::from(1u32) << width) - BigUint::from(1u32);
345    }
346    val
347}
348
349/// # Safety
350/// `ptr` must be valid for the byte span containing `bit_offset..bit_offset + width`.
351unsafe fn read_bits(ptr: *const u8, bit_offset: usize, width: usize) -> TestbenchValue {
352    let byte_offset = bit_offset / 8;
353    let sub = bit_offset % 8;
354    let span_width = sub.saturating_add(width);
355    let byte_size = span_width.div_ceil(8);
356    if span_width <= 64 {
357        let value = unsafe { read_le_u64(ptr.add(byte_offset), byte_size) } >> sub;
358        TestbenchValue::U64(value & width_mask_u64(width))
359    } else {
360        let value = unsafe { read_le_wide(ptr.add(byte_offset), byte_size, span_width) } >> sub;
361        tb_value_from_bits(value & width_mask(width), width)
362    }
363}
364
365// ── Typed evaluation ───────────────────────────────────────────────────
366
367/// Binary operation on `TestbenchValue`.  When both operands are `U64` the fast
368/// path runs entirely in registers; otherwise we promote to `BigUint`.
369#[inline]
370fn eval_binop(l: TestbenchValue, op: Op, r: TestbenchValue) -> TestbenchValue {
371    match (&l, &r) {
372        (TestbenchValue::U64(lv), TestbenchValue::U64(rv)) => {
373            TestbenchValue::U64(eval_binop_u64(*lv, op, *rv))
374        }
375        _ => {
376            let lv = l.to_biguint();
377            let rv = r.to_biguint();
378            // Comparison / logic ops always return u64
379            match op {
380                Op::Eq
381                | Op::Ne
382                | Op::Less
383                | Op::LessEq
384                | Op::Greater
385                | Op::GreaterEq
386                | Op::LogicAnd
387                | Op::LogicOr => TestbenchValue::U64(eval_binop_wide_cmp(&lv, op, &rv)),
388                _ => TestbenchValue::Wide(eval_binop_wide(lv, op, rv)),
389            }
390        }
391    }
392}
393
394fn width_mask(width: usize) -> BigUint {
395    if width == 0 {
396        BigUint::ZERO
397    } else {
398        (BigUint::from(1u8) << width) - BigUint::from(1u8)
399    }
400}
401
402#[inline]
403fn width_mask_u64(width: usize) -> u64 {
404    match width {
405        0 => 0,
406        1..=63 => (1u64 << width) - 1,
407        _ => u64::MAX,
408    }
409}
410
411#[inline]
412fn signed_i128(value: u64, width: usize) -> i128 {
413    let value = value & width_mask_u64(width);
414    if width == 0 || width >= 64 {
415        (value as i64) as i128
416    } else if value & (1u64 << (width - 1)) == 0 {
417        value as i128
418    } else {
419        value as i128 - (1i128 << width)
420    }
421}
422
423fn normalized_bits(value: &TestbenchValue, width: usize) -> BigUint {
424    value.to_biguint() & width_mask(width)
425}
426
427fn tb_value_from_bits(value: BigUint, width: usize) -> TestbenchValue {
428    let value = value & width_mask(width);
429    if width <= 64 {
430        TestbenchValue::U64(value.to_u64().unwrap_or(0))
431    } else {
432        TestbenchValue::Wide(value)
433    }
434}
435
436fn signed_bigint(value: &TestbenchValue, width: usize) -> BigInt {
437    let raw = normalized_bits(value, width);
438    if width == 0 || !raw.bit((width - 1) as u64) {
439        BigInt::from(raw)
440    } else {
441        BigInt::from(raw) - (BigInt::from(1u8) << width)
442    }
443}
444
445fn signed_bits(value: BigInt, width: usize) -> BigUint {
446    if width == 0 {
447        return BigUint::ZERO;
448    }
449    let modulus = BigUint::from(1u8) << width;
450    match value.sign() {
451        Sign::Minus => {
452            let magnitude = (-value).to_biguint().unwrap_or_default() % &modulus;
453            if magnitude == BigUint::ZERO {
454                BigUint::ZERO
455            } else {
456                modulus - magnitude
457            }
458        }
459        _ => value.to_biguint().unwrap_or_default() % modulus,
460    }
461}
462
463fn resize_tb_value(
464    value: &TestbenchValue,
465    source_width: usize,
466    target_width: usize,
467    signed: bool,
468) -> TestbenchValue {
469    if target_width == 0 {
470        return TestbenchValue::U64(0);
471    }
472    if source_width == 0 {
473        let fill = if value.to_u64() & 1 == 0 {
474            BigUint::ZERO
475        } else {
476            width_mask(target_width)
477        };
478        return tb_value_from_bits(fill, target_width);
479    }
480
481    if let TestbenchValue::U64(value) = value
482        && source_width <= 64
483        && target_width <= 64
484    {
485        let mut value = value & width_mask_u64(source_width);
486        if target_width > source_width && signed && value & (1u64 << (source_width - 1)) != 0 {
487            value |= width_mask_u64(target_width) ^ width_mask_u64(source_width);
488        }
489        return TestbenchValue::U64(value & width_mask_u64(target_width));
490    }
491
492    let mut value = normalized_bits(value, source_width);
493    if target_width > source_width && signed && value.bit((source_width - 1) as u64) {
494        value |= width_mask(target_width) ^ width_mask(source_width);
495    }
496    tb_value_from_bits(value, target_width)
497}
498
499fn eval_typed_binop_u64(
500    l: u64,
501    op: Op,
502    r: u64,
503    lhs_width: usize,
504    rhs_width: usize,
505    result_width: usize,
506    lhs_signed: bool,
507    rhs_signed: bool,
508) -> u64 {
509    let l = l & width_mask_u64(lhs_width);
510    let r = r & width_mask_u64(rhs_width);
511    let result_mask = width_mask_u64(result_width);
512    let signed = lhs_signed && rhs_signed;
513    let bool_value = |value: bool| u64::from(value);
514
515    match op {
516        Op::Eq | Op::EqWildcard => bool_value(l == r),
517        Op::Ne | Op::NeWildcard => bool_value(l != r),
518        Op::Less if signed => bool_value(signed_i128(l, lhs_width) < signed_i128(r, rhs_width)),
519        Op::Less => bool_value(l < r),
520        Op::LessEq if signed => bool_value(signed_i128(l, lhs_width) <= signed_i128(r, rhs_width)),
521        Op::LessEq => bool_value(l <= r),
522        Op::Greater if signed => bool_value(signed_i128(l, lhs_width) > signed_i128(r, rhs_width)),
523        Op::Greater => bool_value(l > r),
524        Op::GreaterEq if signed => {
525            bool_value(signed_i128(l, lhs_width) >= signed_i128(r, rhs_width))
526        }
527        Op::GreaterEq => bool_value(l >= r),
528        Op::LogicAnd => bool_value(l != 0 && r != 0),
529        Op::LogicOr => bool_value(l != 0 || r != 0),
530        Op::Add => l.wrapping_add(r) & result_mask,
531        Op::Sub => l.wrapping_sub(r) & result_mask,
532        Op::Mul => l.wrapping_mul(r) & result_mask,
533        Op::Div if signed => {
534            let divisor = signed_i128(r, rhs_width);
535            if divisor == 0 {
536                0
537            } else {
538                (signed_i128(l, lhs_width) / divisor) as u64 & result_mask
539            }
540        }
541        Op::Div => l.checked_div(r).unwrap_or(0) & result_mask,
542        Op::Rem if signed => {
543            let divisor = signed_i128(r, rhs_width);
544            if divisor == 0 {
545                0
546            } else {
547                (signed_i128(l, lhs_width) % divisor) as u64 & result_mask
548            }
549        }
550        Op::Rem => l.checked_rem(r).unwrap_or(0) & result_mask,
551        Op::Pow => {
552            let mut exponent = r;
553            let mut base = l & result_mask;
554            let mut value = 1u64 & result_mask;
555            while exponent != 0 {
556                if exponent & 1 != 0 {
557                    value = ((value as u128 * base as u128) as u64) & result_mask;
558                }
559                exponent >>= 1;
560                if exponent != 0 {
561                    base = ((base as u128 * base as u128) as u64) & result_mask;
562                }
563            }
564            value
565        }
566        Op::BitAnd => (l & r) & result_mask,
567        Op::BitOr => (l | r) & result_mask,
568        Op::BitXor => (l ^ r) & result_mask,
569        Op::BitXnor => (!(l ^ r)) & result_mask,
570        Op::BitNand => (!(l & r)) & result_mask,
571        Op::BitNor => (!(l | r)) & result_mask,
572        Op::LogicShiftL | Op::ArithShiftL => {
573            if r >= result_width as u64 {
574                0
575            } else {
576                l.wrapping_shl(r as u32) & result_mask
577            }
578        }
579        Op::LogicShiftR => {
580            if r >= result_width as u64 {
581                0
582            } else {
583                (l >> r) & result_mask
584            }
585        }
586        Op::ArithShiftR if lhs_signed => {
587            let value = signed_i128(l, lhs_width);
588            if r >= result_width as u64 {
589                if value < 0 { result_mask } else { 0 }
590            } else {
591                ((value >> r) as u64) & result_mask
592            }
593        }
594        Op::ArithShiftR => {
595            if r >= result_width as u64 {
596                0
597            } else {
598                (l >> r) & result_mask
599            }
600        }
601        _ => unreachable!("operator is not a source-language binary op: {op:?}"),
602    }
603}
604
605fn eval_typed_binop(
606    l: TestbenchValue,
607    op: Op,
608    r: TestbenchValue,
609    lhs_width: usize,
610    rhs_width: usize,
611    result_width: usize,
612    lhs_signed: bool,
613    rhs_signed: bool,
614) -> TestbenchValue {
615    if let (TestbenchValue::U64(l), TestbenchValue::U64(r)) = (&l, &r)
616        && lhs_width <= 64
617        && rhs_width <= 64
618        && result_width <= 64
619    {
620        return TestbenchValue::U64(eval_typed_binop_u64(
621            *l,
622            op,
623            *r,
624            lhs_width,
625            rhs_width,
626            result_width,
627            lhs_signed,
628            rhs_signed,
629        ));
630    }
631    let lb = normalized_bits(&l, lhs_width);
632    let rb = normalized_bits(&r, rhs_width);
633    let signed = lhs_signed && rhs_signed;
634
635    let comparison = |value: bool| TestbenchValue::U64(u64::from(value));
636    match op {
637        Op::Eq | Op::EqWildcard => comparison(lb == rb),
638        Op::Ne | Op::NeWildcard => comparison(lb != rb),
639        Op::Less if signed => {
640            comparison(signed_bigint(&l, lhs_width) < signed_bigint(&r, rhs_width))
641        }
642        Op::Less => comparison(lb < rb),
643        Op::LessEq if signed => {
644            comparison(signed_bigint(&l, lhs_width) <= signed_bigint(&r, rhs_width))
645        }
646        Op::LessEq => comparison(lb <= rb),
647        Op::Greater if signed => {
648            comparison(signed_bigint(&l, lhs_width) > signed_bigint(&r, rhs_width))
649        }
650        Op::Greater => comparison(lb > rb),
651        Op::GreaterEq if signed => {
652            comparison(signed_bigint(&l, lhs_width) >= signed_bigint(&r, rhs_width))
653        }
654        Op::GreaterEq => comparison(lb >= rb),
655        Op::LogicAnd => comparison(lb != BigUint::ZERO && rb != BigUint::ZERO),
656        Op::LogicOr => comparison(lb != BigUint::ZERO || rb != BigUint::ZERO),
657        Op::Add => tb_value_from_bits(lb + rb, result_width),
658        Op::Sub => tb_value_from_bits(
659            signed_bits(BigInt::from(lb) - BigInt::from(rb), result_width),
660            result_width,
661        ),
662        Op::Mul => tb_value_from_bits(lb * rb, result_width),
663        Op::Div if signed => {
664            let divisor = signed_bigint(&r, rhs_width);
665            if divisor == BigInt::from(0u8) {
666                TestbenchValue::U64(0)
667            } else {
668                let quotient = signed_bigint(&l, lhs_width) / divisor;
669                tb_value_from_bits(signed_bits(quotient, result_width), result_width)
670            }
671        }
672        Op::Div => {
673            if rb == BigUint::ZERO {
674                TestbenchValue::U64(0)
675            } else {
676                tb_value_from_bits(lb / rb, result_width)
677            }
678        }
679        Op::Rem if signed => {
680            let divisor = signed_bigint(&r, rhs_width);
681            if divisor == BigInt::from(0u8) {
682                TestbenchValue::U64(0)
683            } else {
684                let remainder = signed_bigint(&l, lhs_width) % divisor;
685                tb_value_from_bits(signed_bits(remainder, result_width), result_width)
686            }
687        }
688        Op::Rem => {
689            if rb == BigUint::ZERO {
690                TestbenchValue::U64(0)
691            } else {
692                tb_value_from_bits(lb % rb, result_width)
693            }
694        }
695        Op::Pow => {
696            if result_width == 0 {
697                TestbenchValue::U64(0)
698            } else {
699                let modulus = BigUint::from(1u8) << result_width;
700                tb_value_from_bits(lb.modpow(&rb, &modulus), result_width)
701            }
702        }
703        Op::BitAnd => tb_value_from_bits(lb & rb, result_width),
704        Op::BitOr => tb_value_from_bits(lb | rb, result_width),
705        Op::BitXor => tb_value_from_bits(lb ^ rb, result_width),
706        Op::BitXnor => tb_value_from_bits((lb ^ rb) ^ width_mask(result_width), result_width),
707        Op::LogicShiftL | Op::ArithShiftL => {
708            let shift = rb.to_usize().unwrap_or(usize::MAX);
709            if shift >= result_width {
710                TestbenchValue::U64(0)
711            } else {
712                tb_value_from_bits(lb << shift, result_width)
713            }
714        }
715        Op::LogicShiftR => {
716            let shift = rb.to_usize().unwrap_or(usize::MAX);
717            if shift >= result_width {
718                TestbenchValue::U64(0)
719            } else {
720                tb_value_from_bits(lb >> shift, result_width)
721            }
722        }
723        Op::ArithShiftR if lhs_signed => {
724            let shift = rb.to_usize().unwrap_or(usize::MAX);
725            let value = signed_bigint(&l, lhs_width);
726            let shifted = if shift >= result_width {
727                if value.sign() == Sign::Minus {
728                    BigInt::from(-1)
729                } else {
730                    BigInt::from(0)
731                }
732            } else {
733                value >> shift
734            };
735            tb_value_from_bits(signed_bits(shifted, result_width), result_width)
736        }
737        Op::ArithShiftR => {
738            let shift = rb.to_usize().unwrap_or(usize::MAX);
739            if shift >= result_width {
740                TestbenchValue::U64(0)
741            } else {
742                tb_value_from_bits(lb >> shift, result_width)
743            }
744        }
745        Op::BitNand => tb_value_from_bits((lb & rb) ^ width_mask(result_width), result_width),
746        Op::BitNor => tb_value_from_bits((lb | rb) ^ width_mask(result_width), result_width),
747        _ => unreachable!("operator is not a source-language binary op: {op:?}"),
748    }
749}
750
751fn eval_typed_unop(
752    op: Op,
753    value: &TestbenchValue,
754    operand_width: usize,
755    result_width: usize,
756) -> TestbenchValue {
757    if let TestbenchValue::U64(value) = value
758        && operand_width <= 64
759        && result_width <= 64
760    {
761        let bits = value & width_mask_u64(operand_width);
762        let value = match op {
763            Op::LogicNot => u64::from(bits == 0),
764            Op::BitAnd => u64::from(bits == width_mask_u64(operand_width)),
765            Op::BitNand => u64::from(bits != width_mask_u64(operand_width)),
766            Op::BitOr => u64::from(bits != 0),
767            Op::BitNor => u64::from(bits == 0),
768            Op::BitXor => u64::from(!bits.count_ones().is_multiple_of(2)),
769            Op::BitXnor => u64::from(bits.count_ones().is_multiple_of(2)),
770            Op::Add => bits & width_mask_u64(result_width),
771            Op::Sub => bits.wrapping_neg() & width_mask_u64(result_width),
772            Op::BitNot => !bits & width_mask_u64(result_width),
773            _ => unreachable!("operator is not a source-language unary op: {op:?}"),
774        };
775        return TestbenchValue::U64(value);
776    }
777
778    let bits = normalized_bits(value, operand_width);
779    let reduced = match op {
780        Op::LogicNot => Some(bits == BigUint::ZERO),
781        Op::BitAnd => Some(bits == width_mask(operand_width)),
782        Op::BitNand => Some(bits != width_mask(operand_width)),
783        Op::BitOr => Some(bits != BigUint::ZERO),
784        Op::BitNor => Some(bits == BigUint::ZERO),
785        Op::BitXor | Op::BitXnor => {
786            let odd = bits.iter_u64_digits().map(u64::count_ones).sum::<u32>() % 2 != 0;
787            Some(if matches!(op, Op::BitXor) { odd } else { !odd })
788        }
789        _ => None,
790    };
791    if let Some(value) = reduced {
792        return TestbenchValue::U64(u64::from(value));
793    }
794
795    match op {
796        Op::Add => tb_value_from_bits(bits, result_width),
797        Op::Sub => tb_value_from_bits(signed_bits(-BigInt::from(bits), result_width), result_width),
798        Op::BitNot => tb_value_from_bits(bits ^ width_mask(operand_width), result_width),
799        _ => unreachable!("operator is not a source-language unary op: {op:?}"),
800    }
801}
802
803#[inline]
804fn eval_binop_u64(l: u64, op: Op, r: u64) -> u64 {
805    match op {
806        Op::Add => l.wrapping_add(r),
807        Op::Sub => l.wrapping_sub(r),
808        Op::Mul => l.wrapping_mul(r),
809        Op::Div => l.checked_div(r).unwrap_or(0),
810        Op::Rem => l.checked_rem(r).unwrap_or(0),
811        Op::BitAnd => l & r,
812        Op::BitOr => l | r,
813        Op::BitXor => l ^ r,
814        Op::LogicShiftL => {
815            if r >= 64 {
816                0
817            } else {
818                l << r
819            }
820        }
821        Op::LogicShiftR => {
822            if r >= 64 {
823                0
824            } else {
825                l >> r
826            }
827        }
828        Op::ArithShiftL => {
829            if r >= 64 {
830                0
831            } else {
832                l << r
833            }
834        }
835        Op::ArithShiftR => {
836            if r >= 64 {
837                ((l as i64) >> 63) as u64
838            } else {
839                ((l as i64) >> r) as u64
840            }
841        }
842        Op::Eq => (l == r) as u64,
843        Op::Ne => (l != r) as u64,
844        Op::Less => (l < r) as u64,
845        Op::LessEq => (l <= r) as u64,
846        Op::Greater => (l > r) as u64,
847        Op::GreaterEq => (l >= r) as u64,
848        Op::LogicAnd => ((l != 0) && (r != 0)) as u64,
849        Op::LogicOr => ((l != 0) || (r != 0)) as u64,
850        _ => unreachable!("operator is not testbench bytecode plumbing: {op:?}"),
851    }
852}
853
854fn eval_binop_wide(l: BigUint, op: Op, r: BigUint) -> BigUint {
855    match op {
856        Op::Add => l + r,
857        Op::Sub => {
858            if l >= r {
859                l - r
860            } else {
861                BigUint::ZERO
862            }
863        }
864        Op::Mul => l * r,
865        Op::Div => {
866            if r == BigUint::ZERO {
867                BigUint::ZERO
868            } else {
869                l / r
870            }
871        }
872        Op::Rem => {
873            if r == BigUint::ZERO {
874                BigUint::ZERO
875            } else {
876                l % r
877            }
878        }
879        Op::BitAnd => l & r,
880        Op::BitOr => l | r,
881        Op::BitXor => l ^ r,
882        Op::LogicShiftL => {
883            let s: u64 = (&r).try_into().unwrap_or(256);
884            l << s
885        }
886        Op::LogicShiftR => {
887            let s: u64 = (&r).try_into().unwrap_or(256);
888            l >> s
889        }
890        _ => unreachable!("operator is not wide testbench bytecode plumbing: {op:?}"),
891    }
892}
893
894fn eval_binop_wide_cmp(l: &BigUint, op: Op, r: &BigUint) -> u64 {
895    match op {
896        Op::Eq => (l == r) as u64,
897        Op::Ne => (l != r) as u64,
898        Op::Less => (l < r) as u64,
899        Op::LessEq => (l <= r) as u64,
900        Op::Greater => (l > r) as u64,
901        Op::GreaterEq => (l >= r) as u64,
902        Op::LogicAnd => ((*l != BigUint::ZERO) && (*r != BigUint::ZERO)) as u64,
903        Op::LogicOr => ((*l != BigUint::ZERO) || (*r != BigUint::ZERO)) as u64,
904        _ => unreachable!("operator is not testbench comparison plumbing: {op:?}"),
905    }
906}