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