openvm_static_verifier/tracegen/
opcode_impl.rs

1#![allow(rustdoc::private_intra_doc_links)]
2//! Standalone tape-replay implementations of [`Halo2Opcode`]s.
3//!
4//! [`run_op`] drives a [`ReplayTape`]: [`CalculateOffsetsTape`] (build-time)
5//! records values + offsets and yields [`OpcodeMeta`]; [`WitnessTape`]
6//! (runtime) streams values into caller buffers. Runtime replay is stateless —
7//! `constant_skip_inds` precomputed at build time lists which `load_constant`
8//! calls materialize a cell, so nodes replay in parallel.
9
10use std::{cmp::Ordering, collections::HashMap};
11
12use halo2_base::{
13    halo2_proofs::{
14        arithmetic::Field as _,
15        halo2curves::{bn256::Fr, ff::PrimeField as _},
16    },
17    utils::{
18        bigint_to_fe, biguint_to_fe, bit_length, decompose_fe_to_u64_limbs, fe_to_bigint,
19        fe_to_biguint, modulus,
20    },
21};
22use num_bigint::{BigInt, BigUint};
23use num_integer::Integer;
24use openvm_stark_sdk::{
25    openvm_stark_backend::p3_field::{
26        extension::BinomiallyExtendable, BasedVectorSpace, Field, PrimeCharacteristicRing,
27        PrimeField32, PrimeField64,
28    },
29    p3_baby_bear::BabyBear,
30};
31
32use crate::{
33    field::baby_bear::{
34        BabyBearExt4, BABYBEAR_MAX_BITS, BABY_BEAR_MODULUS_U64, RESERVED_HIGH_BITS,
35    },
36    hash::{poseidon2::Poseidon2Params, POSEIDON2_COMPRESS_PARAMS, POSEIDON2_PARAMS},
37    tracegen::ir_builder::Halo2Opcode,
38    transcript::NUM_SAMPLES_PER_WORD,
39};
40
41/// Reduce when a bound would exceed this, mirroring `BabyBearChip`.
42const REDUCE_THRESHOLD: u16 = (Fr::CAPACITY as usize - RESERVED_HIGH_BITS) as u16;
43
44/// Sentinel offset for cells that exist outside the op's tape window (external
45/// operands and constants that hit the constant cache and assigned no advice cell).
46pub(crate) const UNMATERIALIZED: usize = usize::MAX;
47
48fn pow_of_two_fr(n: usize) -> Fr {
49    biguint_to_fe(&(BigUint::from(1u32) << n))
50}
51
52fn to_baby_bear(v: &Fr) -> BabyBear {
53    let mut b_int = fe_to_bigint(v) % BabyBear::ORDER_U32;
54    if b_int < BigInt::from(0) {
55        b_int += BabyBear::ORDER_U32;
56    }
57    BabyBear::from_u32(b_int.try_into().unwrap())
58}
59
60fn fr_from_bb(v: BabyBear) -> Fr {
61    Fr::from(v.as_canonical_u64())
62}
63
64/// A tape cell: at minimum it knows the advice value it carries.
65pub(crate) trait TapeCell: Copy {
66    fn value(&self) -> Fr;
67}
68
69impl TapeCell for Fr {
70    #[inline]
71    fn value(&self) -> Fr {
72        *self
73    }
74}
75
76/// A value together with the relative context-tape offset it was written at
77/// ([`UNMATERIALIZED`] for cells outside the op's window).
78#[derive(Copy, Clone, Debug, PartialEq, Eq)]
79pub(crate) struct OffsetCell {
80    pub value: Fr,
81    pub offset: usize,
82}
83
84impl TapeCell for OffsetCell {
85    #[inline]
86    fn value(&self) -> Fr {
87        self.value
88    }
89}
90
91/// Replay target for opcode execution. Required methods define how cells land
92/// on the tapes; provided methods implement the exact halo2 cell layouts.
93pub(crate) trait ReplayTape {
94    type TapeCell: TapeCell;
95
96    fn push(&mut self, value: Fr) -> Self::TapeCell;
97    fn lookup(&mut self, value: Fr);
98    fn lookup_bits(&self) -> usize;
99    /// A cell whose value flows in from outside the op's tape window.
100    fn external(&self, value: Fr) -> Self::TapeCell;
101    /// Constant load: writes a cell iff not already cached — decided by the
102    /// impl's cache ([`CalculateOffsetsTape`]) or skip indices ([`WitnessTape`]).
103    fn load_constant(&mut self, value: Fr) -> Self::TapeCell;
104    /// Records a logical output of the op, in output-index order.
105    fn output(&mut self, _cell: Self::TapeCell) {}
106
107    // --- gate ops (exact `GateInstructions` layouts); pure Fr in, cell out ---
108
109    /// `| a | b | 1 | a + b |`, out at +3.
110    fn gate_add(&mut self, a: Fr, b: Fr) -> Self::TapeCell {
111        self.push(a);
112        self.push(b);
113        self.push(Fr::ONE);
114        self.push(a + b)
115    }
116
117    /// `| a - b | b | 1 | a |`, out at +0.
118    fn gate_sub(&mut self, a: Fr, b: Fr) -> Self::TapeCell {
119        let out = self.push(a - b);
120        self.push(b);
121        self.push(Fr::ONE);
122        self.push(a);
123        out
124    }
125
126    /// `| a - b * c | b | c | a |`, out at +0.
127    fn gate_sub_mul(&mut self, a: Fr, b: Fr, c: Fr) -> Self::TapeCell {
128        let out = self.push(a - b * c);
129        self.push(b);
130        self.push(c);
131        self.push(a);
132        out
133    }
134
135    /// `| a | -a | 1 | 0 |`, out at +1.
136    fn gate_neg(&mut self, a: Fr) -> Self::TapeCell {
137        self.push(a);
138        let out = self.push(-a);
139        self.push(Fr::ONE);
140        self.push(Fr::ZERO);
141        out
142    }
143
144    /// `| 0 | a | b | a * b |`, out at +3.
145    fn gate_mul(&mut self, a: Fr, b: Fr) -> Self::TapeCell {
146        self.push(Fr::ZERO);
147        self.push(a);
148        self.push(b);
149        self.push(a * b)
150    }
151
152    /// `| c | a | b | a * b + c |`, out at +3.
153    fn gate_mul_add(&mut self, a: Fr, b: Fr, c: Fr) -> Self::TapeCell {
154        self.push(c);
155        self.push(a);
156        self.push(b);
157        self.push(a * b + c)
158    }
159
160    /// `| 0 | x | x | x |`.
161    fn gate_assert_bit(&mut self, x: Fr) {
162        self.push(Fr::ZERO);
163        self.push(x);
164        self.push(x);
165        self.push(x);
166    }
167
168    fn gate_not(&mut self, a: Fr) -> Self::TapeCell {
169        self.gate_sub(Fr::ONE, a)
170    }
171
172    /// `| a - b | 1 | b | a | b | sel | a - b | out |`, out at +7.
173    fn gate_select(&mut self, a: Fr, b: Fr, sel: Fr) -> Self::TapeCell {
174        let diff = a - b;
175        self.push(diff);
176        self.push(Fr::ONE);
177        self.push(b);
178        self.push(a);
179        self.push(b);
180        self.push(sel);
181        self.push(diff);
182        self.push(sel * diff + b)
183    }
184
185    /// `| is_zero | a | inv | 1 | 0 | a | is_zero | 0 |`, out at +6.
186    fn gate_is_zero(&mut self, a: Fr) -> Self::TapeCell {
187        let is_zero = if a == Fr::ZERO { Fr::ONE } else { Fr::ZERO };
188        let inv = a.invert().unwrap_or(Fr::ONE);
189        self.push(is_zero);
190        self.push(a);
191        self.push(inv);
192        self.push(Fr::ONE);
193        self.push(Fr::ZERO);
194        self.push(a);
195        let out = self.push(is_zero);
196        self.push(Fr::ZERO);
197        out
198    }
199
200    fn gate_is_equal(&mut self, a: Fr, b: Fr) -> Self::TapeCell {
201        self.gate_sub(a, b);
202        self.gate_is_zero(a - b)
203    }
204
205    /// `| v_0 |` then `| v_i | 1 | run |` per further element; out is the last cell.
206    fn gate_sum(&mut self, values: impl IntoIterator<Item = Fr>) -> Self::TapeCell {
207        let mut iter = values.into_iter();
208        let mut sum = iter.next().unwrap();
209        let mut last = self.push(sum);
210        for v in iter {
211            self.push(v);
212            self.push(Fr::ONE);
213            sum += v;
214            last = self.push(sum);
215        }
216        last
217    }
218
219    /// `inner_product_simple`: with `starts_with_one` the cells are `| a_0 |` then
220    /// `| a_i | b_i | run |` per further pair; otherwise `| 0 |` then
221    /// `| a_i | b_i | run |` per pair. Out is the last cell assigned.
222    fn gate_inner_product(
223        &mut self,
224        pairs: impl IntoIterator<Item = (Fr, Fr)>,
225        starts_with_one: bool,
226    ) -> Self::TapeCell {
227        let mut iter = pairs.into_iter();
228        let mut sum;
229        let mut last;
230        if starts_with_one {
231            let (a0, _) = iter.next().unwrap();
232            sum = a0;
233            last = self.push(a0);
234        } else {
235            sum = Fr::ZERO;
236            last = self.push(Fr::ZERO);
237        }
238        for (av, bv) in iter {
239            self.push(av);
240            self.push(bv);
241            sum += av * bv;
242            last = self.push(sum);
243        }
244        last
245    }
246
247    /// `num_to_bits`: little-endian bit decomposition + per-bit `assert_bit`. Each
248    /// bit cell is emitted as an op output, in bit order.
249    fn gate_num_to_bits(&mut self, a: Fr, range_bits: usize) {
250        let bits = decompose_fe_to_u64_limbs(&a, range_bits, 1);
251        // Inner product against powers of two; `pow_of_two[0] == 1`, so it starts
252        // with one and bit 0 is the first cell.
253        let mut sum = Fr::from(bits[0]);
254        let first = self.push(sum);
255        self.output(first);
256        for (i, &b) in bits.iter().enumerate().skip(1) {
257            let bv = Fr::from(b);
258            let cell = self.push(bv);
259            self.output(cell);
260            self.push(pow_of_two_fr(i));
261            sum += bv * pow_of_two_fr(i);
262            self.push(sum);
263        }
264        // constrain_equal: no advice cells
265        for &b in &bits {
266            self.gate_assert_bit(Fr::from(b));
267        }
268    }
269
270    // --- range ops (exact `RangeInstructions` layouts) ---
271
272    /// `_range_check`; returns the last (highest) limb value.
273    fn range_check(&mut self, a: Fr, range_bits: usize) -> Fr {
274        if range_bits == 0 {
275            // assert_is_const assigns no advice cells
276            return a;
277        }
278        let lookup_bits = self.lookup_bits();
279        let num_limbs = range_bits.div_ceil(lookup_bits);
280        let rem_bits = range_bits % lookup_bits;
281        let last_limb = if num_limbs == 1 {
282            self.lookup(a);
283            a
284        } else {
285            let limbs = decompose_fe_to_u64_limbs(&a, num_limbs, lookup_bits);
286            // `limb_bases[0] == 1`, so the inner product starts with one.
287            self.gate_inner_product(
288                limbs
289                    .iter()
290                    .enumerate()
291                    .map(|(i, &l)| (Fr::from(l), pow_of_two_fr(i * lookup_bits))),
292                true,
293            );
294            // constrain_equal: no advice cells; then all limbs are sent to lookup in
295            // natural order.
296            for &l in &limbs {
297                self.lookup(Fr::from(l));
298            }
299            Fr::from(limbs[num_limbs - 1])
300        };
301        match rem_bits.cmp(&1) {
302            Ordering::Equal => {
303                self.gate_assert_bit(last_limb);
304            }
305            Ordering::Greater => {
306                let shift = pow_of_two_fr(lookup_bits - rem_bits);
307                self.gate_mul(last_limb, shift);
308                self.lookup(last_limb * shift);
309            }
310            Ordering::Less => {}
311        }
312        last_limb
313    }
314
315    /// `| a + 2^n - b | b | 1 | a + 2^n | -2^n | 1 | a |` + range check of the diff.
316    fn check_less_than(&mut self, a: Fr, b: Fr, num_bits: usize) {
317        let pow = pow_of_two_fr(num_bits);
318        let shift_a = pow + a;
319        let diff = shift_a - b;
320        self.push(diff);
321        self.push(b);
322        self.push(Fr::ONE);
323        self.push(shift_a);
324        self.push(-pow);
325        self.push(Fr::ONE);
326        self.push(a);
327        self.range_check(diff, num_bits);
328    }
329
330    fn check_less_than_safe(&mut self, a: Fr, b: u64) {
331        let range_bits = bit_length(b).div_ceil(self.lookup_bits()) * self.lookup_bits();
332        self.range_check(a, range_bits);
333        self.check_less_than(a, Fr::from(b), range_bits);
334    }
335
336    fn check_big_less_than_safe(&mut self, a: Fr, b: &BigUint) {
337        let range_bits = (b.bits() as usize).div_ceil(self.lookup_bits()) * self.lookup_bits();
338        self.range_check(a, range_bits);
339        self.check_less_than(a, biguint_to_fe(b), range_bits);
340    }
341
342    /// Same 7-cell shape with `2^padded`, then `is_zero` of the top limb; returns
343    /// the comparison bit cell.
344    fn is_less_than(&mut self, a: Fr, b: Fr, num_bits: usize) -> Self::TapeCell {
345        let lookup_bits = self.lookup_bits();
346        let padded_bits = num_bits.div_ceil(lookup_bits) * lookup_bits;
347        let pow = pow_of_two_fr(padded_bits);
348        let shift_a = pow + a;
349        let shifted = shift_a - b;
350        self.push(shifted);
351        self.push(b);
352        self.push(Fr::ONE);
353        self.push(shift_a);
354        self.push(-pow);
355        self.push(Fr::ONE);
356        self.push(a);
357        let last_limb = self.range_check(shifted, padded_bits + lookup_bits);
358        self.gate_is_zero(last_limb)
359    }
360
361    fn is_big_less_than_safe(&mut self, a: Fr, b: &BigUint) -> Self::TapeCell {
362        let range_bits = (b.bits() as usize).div_ceil(self.lookup_bits()) * self.lookup_bits();
363        self.range_check(a, range_bits);
364        self.is_less_than(a, biguint_to_fe(b), range_bits)
365    }
366
367    /// `| rem | b | div | a |` + quotient and remainder bound checks; returns the
368    /// remainder cell.
369    fn div_mod(&mut self, a: Fr, b: &BigUint, a_num_bits: usize) -> Self::TapeCell {
370        let a_val = fe_to_biguint(&a);
371        let (div_val, rem_val) = a_val.div_mod_floor(b);
372        let rem = self.push(biguint_to_fe(&rem_val));
373        self.push(biguint_to_fe(b));
374        let div = self.push(biguint_to_fe(&div_val));
375        self.push(a);
376        self.check_big_less_than_safe(
377            div.value(),
378            &((BigUint::from(1u32) << a_num_bits) / b + 1u32),
379        );
380        self.check_big_less_than_safe(rem.value(), b);
381        rem
382    }
383
384    // --- BabyBear engine (exact `BabyBearChip` behavior) ---
385
386    fn bb_external(&self, value: Fr, max_bits: u16) -> BbWire<Self::TapeCell> {
387        BbWire {
388            cell: self.external(value),
389            max_bits,
390        }
391    }
392
393    /// `signed_div_mod`: `| rem | p | div | a |` + shifted-quotient range check +
394    /// `check_big_less_than_safe(rem, p)`; returns the remainder cell.
395    fn bb_signed_div_mod(&mut self, a: Fr, a_num_bits: u16) -> Self::TapeCell {
396        let b = BigUint::from(BABY_BEAR_MODULUS_U64);
397        let a_val = fe_to_bigint(&a);
398        let (div_val, rem_val) = a_val.div_mod_floor(&b.clone().into());
399        let rem = self.push(bigint_to_fe(&rem_val));
400        self.push(biguint_to_fe(&b));
401        let div = self.push(bigint_to_fe(&div_val));
402        self.push(a);
403        let bound = ((BigUint::from(1u32) << a_num_bits) - 1u32).div_ceil(&b);
404        let shifted = self.gate_add(div.value(), biguint_to_fe(&bound));
405        self.range_check(shifted.value(), (bound * 2u32 + 1u32).bits() as usize);
406        self.check_big_less_than_safe(rem.value(), &b);
407        rem
408    }
409
410    fn bb_reduce(&mut self, a: BbWire<Self::TapeCell>) -> BbWire<Self::TapeCell> {
411        let rem = self.bb_signed_div_mod(a.cell.value(), a.max_bits);
412        BbWire {
413            cell: rem,
414            max_bits: BABYBEAR_MAX_BITS as u16,
415        }
416    }
417
418    /// `assert_zero`: `| 0 | p | div | a |` + shifted-quotient range check (plain
419    /// division bound, no remainder check).
420    fn bb_assert_zero(&mut self, a: BbWire<Self::TapeCell>) {
421        let b = BigUint::from(BABY_BEAR_MODULUS_U64);
422        let a_val = fe_to_bigint(&a.cell.value());
423        let (div_val, _) = a_val.div_mod_floor(&b.clone().into());
424        self.push(Fr::ZERO);
425        self.push(biguint_to_fe(&b));
426        let div = self.push(bigint_to_fe(&div_val));
427        self.push(a.cell.value());
428        let bound = (BigUint::from(1u32) << a.max_bits) / &b;
429        let shifted = self.gate_add(div.value(), biguint_to_fe(&bound));
430        self.range_check(shifted.value(), (bound * 2u32 + 1u32).bits() as usize);
431    }
432
433    fn bb_load_witness(&mut self, value: BabyBear) -> BbWire<Self::TapeCell> {
434        let cell = self.push(fr_from_bb(value));
435        self.range_check(cell.value(), BABYBEAR_MAX_BITS);
436        BbWire {
437            cell,
438            max_bits: BABYBEAR_MAX_BITS as u16,
439        }
440    }
441
442    /// `load_constant`: assigns one cell on the first load of a constant; repeats
443    /// hit the constant cache and contribute no cells.
444    fn bb_load_constant(&mut self, value: BabyBear) -> BbWire<Self::TapeCell> {
445        let key = value.as_canonical_u64();
446        let cell = self.load_constant(Fr::from(key));
447        BbWire {
448            cell,
449            max_bits: bit_length(key) as u16,
450        }
451    }
452
453    /// Full `BabyBearChip::mul` with swap + interleaved reduces.
454    fn bb_mul(
455        &mut self,
456        mut a: BbWire<Self::TapeCell>,
457        mut b: BbWire<Self::TapeCell>,
458    ) -> BbWire<Self::TapeCell> {
459        if a.max_bits < b.max_bits {
460            std::mem::swap(&mut a, &mut b);
461        }
462        if a.max_bits + b.max_bits > REDUCE_THRESHOLD {
463            a = self.bb_reduce(a);
464            if a.max_bits + b.max_bits > REDUCE_THRESHOLD {
465                b = self.bb_reduce(b);
466            }
467        }
468        let cell = self.gate_mul(a.cell.value(), b.cell.value());
469        BbWire {
470            cell,
471            max_bits: a.max_bits + b.max_bits,
472        }
473    }
474
475    /// Full `BabyBearChip::sub` with interleaved reduces.
476    fn bb_sub(
477        &mut self,
478        mut a: BbWire<Self::TapeCell>,
479        mut b: BbWire<Self::TapeCell>,
480    ) -> BbWire<Self::TapeCell> {
481        if a.max_bits + 1 > REDUCE_THRESHOLD {
482            a = self.bb_reduce(a);
483        }
484        if b.max_bits + 1 > REDUCE_THRESHOLD {
485            b = self.bb_reduce(b);
486        }
487        let cell = self.gate_sub(a.cell.value(), b.cell.value());
488        BbWire {
489            cell,
490            max_bits: a.max_bits.max(b.max_bits) + 1,
491        }
492    }
493
494    /// Full `BabyBearChip::mul_add` with swap + interleaved reduces.
495    fn bb_mul_add(
496        &mut self,
497        mut a: BbWire<Self::TapeCell>,
498        mut b: BbWire<Self::TapeCell>,
499        mut c: BbWire<Self::TapeCell>,
500    ) -> BbWire<Self::TapeCell> {
501        if a.max_bits < b.max_bits {
502            std::mem::swap(&mut a, &mut b);
503        }
504        if a.max_bits + b.max_bits + 1 > REDUCE_THRESHOLD {
505            a = self.bb_reduce(a);
506            if a.max_bits + b.max_bits + 1 > REDUCE_THRESHOLD {
507                b = self.bb_reduce(b);
508            }
509        }
510        if c.max_bits + 1 > REDUCE_THRESHOLD {
511            c = self.bb_reduce(c);
512        }
513        let cell = self.gate_mul_add(a.cell.value(), b.cell.value(), c.cell.value());
514        BbWire {
515            cell,
516            max_bits: c.max_bits.max(a.max_bits + b.max_bits) + 1,
517        }
518    }
519
520    fn bb_assert_equal(&mut self, a: BbWire<Self::TapeCell>, b: BbWire<Self::TapeCell>) {
521        let diff = self.bb_sub(a, b);
522        self.bb_assert_zero(diff);
523    }
524
525    /// `BabyBearChip::special_inner_product`: reduce decisions mutate the operand
526    /// arrays persistently, mirroring the chip.
527    fn bb_special_inner_product(
528        &mut self,
529        a: &mut [BbWire<Self::TapeCell>; 4],
530        b: &mut [BbWire<Self::TapeCell>; 4],
531        s: usize,
532    ) -> BbWire<Self::TapeCell> {
533        let lb = s.saturating_sub(3);
534        let ub = 4.min(s + 1);
535        let len = if s < 3 { s + 1 } else { 7 - s };
536        let mut max_bits = 0;
537        for i in 0..(ub - lb) {
538            let ai = lb + i;
539            let bi = s - lb - i;
540            let limit = REDUCE_THRESHOLD - len as u16 + i as u16;
541            if a[ai].max_bits + b[bi].max_bits > limit {
542                if a[ai].max_bits >= b[bi].max_bits {
543                    a[ai] = self.bb_reduce(a[ai]);
544                    if a[ai].max_bits + b[bi].max_bits > limit {
545                        b[bi] = self.bb_reduce(b[bi]);
546                    }
547                } else {
548                    b[bi] = self.bb_reduce(b[bi]);
549                    if a[ai].max_bits + b[bi].max_bits > limit {
550                        a[ai] = self.bb_reduce(a[ai]);
551                    }
552                }
553            }
554            max_bits = if i == 0 {
555                a[ai].max_bits + b[bi].max_bits
556            } else {
557                max_bits.max(a[ai].max_bits + b[bi].max_bits) + 1
558            };
559        }
560        // All operands are Existing cells, so the inner product does NOT start with
561        // one: `| 0 |` then `| a_i | b_i | run |` per pair.
562        let out = self.gate_inner_product(
563            (0..(ub - lb)).map(|i| (a[lb + i].cell.value(), b[s - lb - i].cell.value())),
564            false,
565        );
566        BbWire {
567            cell: out,
568            max_bits,
569        }
570    }
571
572    /// `BabyBearExt4Chip::mul`. Mutates the operand arrays (reduce decisions persist
573    /// across the seven `special_inner_product` calls). Returns the four product
574    /// coefficients and the W constant cell (unmaterialized when it hit the cache).
575    fn ext_mul(
576        &mut self,
577        a: &mut [BbWire<Self::TapeCell>; 4],
578        b: &mut [BbWire<Self::TapeCell>; 4],
579    ) -> ([BbWire<Self::TapeCell>; 4], Self::TapeCell) {
580        let mut coeffs: [BbWire<Self::TapeCell>; 7] =
581            core::array::from_fn(|_| self.bb_external(Fr::ZERO, 0));
582        for (s, coeff) in coeffs.iter_mut().enumerate() {
583            *coeff = self.bb_special_inner_product(a, b, s);
584        }
585        let w = self.bb_load_constant(<BabyBear as BinomiallyExtendable<4>>::W);
586        for i in 4..7 {
587            coeffs[i - 4] = self.bb_mul_add(coeffs[i], w, coeffs[i - 4]);
588        }
589        ([coeffs[0], coeffs[1], coeffs[2], coeffs[3]], w.cell)
590    }
591}
592
593/// Mirror of `BabyBearWire` over tape cells.
594#[derive(Copy, Clone, Debug)]
595pub(crate) struct BbWire<C> {
596    cell: C,
597    max_bits: u16,
598}
599
600fn ext_value<C: TapeCell>(wires: &[BbWire<C>; 4]) -> BabyBearExt4 {
601    BabyBearExt4::from_basis_coefficients_fn(|i| to_baby_bear(&wires[i].cell.value()))
602}
603
604/// Records tapes + per-output offsets to derive [`OpcodeMeta`]. `warm`
605/// constants (already materialized by an earlier node) seed the cache as
606/// [`UNMATERIALIZED`] cells so loading them writes nothing. Build-time only.
607pub(crate) struct CalculateOffsetsTape {
608    pub advice: Vec<Fr>,
609    pub lookups: Vec<Fr>,
610    pub outputs: Vec<OffsetCell>,
611    /// Indices of the `load_constant` calls that missed the cache (i.e. wrote a
612    /// cell), in call order.
613    pub skip_inds: Vec<u32>,
614    lookup_bits: usize,
615    const_cache: HashMap<Fr, OffsetCell>,
616    const_calls: u32,
617}
618
619impl CalculateOffsetsTape {
620    pub(crate) fn new<'a>(lookup_bits: usize, warm: impl Iterator<Item = &'a Fr>) -> Self {
621        let const_cache = warm
622            .map(|&value| {
623                (
624                    value,
625                    OffsetCell {
626                        value,
627                        offset: UNMATERIALIZED,
628                    },
629                )
630            })
631            .collect();
632        CalculateOffsetsTape {
633            advice: Vec::new(),
634            lookups: Vec::new(),
635            outputs: Vec::new(),
636            skip_inds: Vec::new(),
637            lookup_bits,
638            const_cache,
639            const_calls: 0,
640        }
641    }
642}
643
644impl ReplayTape for CalculateOffsetsTape {
645    type TapeCell = OffsetCell;
646
647    fn push(&mut self, value: Fr) -> OffsetCell {
648        let offset = self.advice.len();
649        self.advice.push(value);
650        OffsetCell { value, offset }
651    }
652
653    fn lookup(&mut self, value: Fr) {
654        self.lookups.push(value);
655    }
656
657    fn lookup_bits(&self) -> usize {
658        self.lookup_bits
659    }
660
661    fn external(&self, value: Fr) -> OffsetCell {
662        OffsetCell {
663            value,
664            offset: UNMATERIALIZED,
665        }
666    }
667
668    fn load_constant(&mut self, value: Fr) -> OffsetCell {
669        let idx = self.const_calls;
670        self.const_calls += 1;
671        if let Some(&cell) = self.const_cache.get(&value) {
672            return cell;
673        }
674        self.skip_inds.push(idx);
675        let cell = self.push(value);
676        self.const_cache.insert(value, cell);
677        cell
678    }
679
680    fn output(&mut self, cell: OffsetCell) {
681        self.outputs.push(cell);
682    }
683}
684
685/// Streams witness values into caller-provided buffers via raw pointer bumps.
686/// Stateless: `write_const_inds` (the node's
687/// [`OpcodeMeta::constant_skip_inds`]) lists which `load_constant` calls write.
688///
689/// Safety: buffers passed to [`WitnessTape::new`] must be at least
690/// [`OpcodeMeta::ctx_len`] / [`OpcodeMeta::lookups_len`] long.
691pub(crate) struct WitnessTape {
692    advice: *mut Fr,
693    lookups: *mut Fr,
694    write_const_inds: *const u32,
695    write_const_end: *const u32,
696    num_const_idx: u32,
697    lookup_bits: usize,
698}
699
700impl WitnessTape {
701    pub(crate) fn new(
702        ctx: &mut [Fr],
703        lookups: &mut [Fr],
704        lookup_bits: usize,
705        write_const_inds: &[u32],
706    ) -> Self {
707        let range = write_const_inds.as_ptr_range();
708        WitnessTape {
709            advice: ctx.as_mut_ptr(),
710            lookups: lookups.as_mut_ptr(),
711            write_const_inds: range.start,
712            write_const_end: range.end,
713            num_const_idx: 0,
714            lookup_bits,
715        }
716    }
717}
718
719#[allow(unsafe_code)]
720impl ReplayTape for WitnessTape {
721    type TapeCell = Fr;
722
723    #[inline]
724    fn push(&mut self, value: Fr) -> Fr {
725        unsafe {
726            *self.advice = value;
727            self.advice = self.advice.add(1);
728        }
729        value
730    }
731
732    #[inline]
733    fn lookup(&mut self, value: Fr) {
734        unsafe {
735            *self.lookups = value;
736            self.lookups = self.lookups.add(1);
737        }
738    }
739
740    #[inline]
741    fn lookup_bits(&self) -> usize {
742        self.lookup_bits
743    }
744
745    #[inline]
746    fn external(&self, value: Fr) -> Fr {
747        value
748    }
749
750    #[inline]
751    fn load_constant(&mut self, value: Fr) -> Fr {
752        unsafe {
753            if self.write_const_inds != self.write_const_end
754                && *self.write_const_inds == self.num_const_idx
755            {
756                self.write_const_inds = self.write_const_inds.add(1);
757                self.push(value);
758            }
759        }
760        self.num_const_idx += 1;
761        value
762    }
763}
764
765// --- per-opcode run logic ---
766
767fn bb_div_run<T: ReplayTape>(t: &mut T, a_val: Fr, b_val: Fr, a_bits: u16, b_bits: u16) {
768    let mut a = t.bb_external(a_val, a_bits);
769    let b = t.bb_external(b_val, b_bits);
770    let b_bb = to_baby_bear(&b_val);
771    let b_inv_val = b_bb.try_inverse().unwrap();
772    let b_inv = t.bb_load_witness(b_inv_val);
773    let one = t.bb_load_constant(BabyBear::ONE);
774    // `b` is passed to `mul` by value, so reduces inside do not affect the outer `b`.
775    let inv_prod = t.bb_mul(b, b_inv);
776    t.bb_assert_equal(inv_prod, one);
777    let mut c = t.bb_load_witness(to_baby_bear(&a_val) * b_inv_val);
778    if a.max_bits + 1 > REDUCE_THRESHOLD {
779        a = t.bb_reduce(a);
780    }
781    let mut b = b;
782    if b.max_bits + c.max_bits + 1 > REDUCE_THRESHOLD {
783        b = t.bb_reduce(b);
784    }
785    if b.max_bits + c.max_bits + 1 > REDUCE_THRESHOLD {
786        c = t.bb_reduce(c);
787    }
788    let diff = t.gate_sub_mul(a.cell.value(), b.cell.value(), c.cell.value());
789    let max_bits = a.max_bits.max(b.max_bits + c.max_bits) + 1;
790    t.bb_assert_zero(BbWire {
791        cell: diff,
792        max_bits,
793    });
794    t.output(c.cell);
795    t.output(one.cell);
796}
797
798fn ext_mul_run<T: ReplayTape>(t: &mut T, args: &[Fr], bits: &[u16]) {
799    let mut a: [BbWire<T::TapeCell>; 4] = core::array::from_fn(|i| t.bb_external(args[i], bits[i]));
800    let mut b: [BbWire<T::TapeCell>; 4] =
801        core::array::from_fn(|i| t.bb_external(args[4 + i], bits[4 + i]));
802    let (coeffs, w) = t.ext_mul(&mut a, &mut b);
803    for coeff in &coeffs {
804        t.output(coeff.cell);
805    }
806    t.output(w);
807}
808
809fn ext_div_run<T: ReplayTape>(t: &mut T, args: &[Fr], bits: &[u16]) {
810    let a: [BbWire<T::TapeCell>; 4] = core::array::from_fn(|i| t.bb_external(args[i], bits[i]));
811    let b: [BbWire<T::TapeCell>; 4] =
812        core::array::from_fn(|i| t.bb_external(args[4 + i], bits[4 + i]));
813    let b_ext = ext_value(&b);
814    let b_inv_val = b_ext.try_inverse().unwrap();
815    let b_inv_coeffs = b_inv_val.as_basis_coefficients_slice();
816    let b_inv: [BbWire<T::TapeCell>; 4] =
817        core::array::from_fn(|i| t.bb_load_witness(b_inv_coeffs[i]));
818    // ext load_constant(ONE) = coeffs [1, 0, 0, 0]: ONE and the first ZERO may
819    // materialize; the remaining zeros always hit the constant cache.
820    let one_c = t.bb_load_constant(BabyBear::ONE);
821    let zero_c = t.bb_load_constant(BabyBear::ZERO);
822    let _ = t.bb_load_constant(BabyBear::ZERO);
823    let _ = t.bb_load_constant(BabyBear::ZERO);
824    let one_ext = [one_c, zero_c, zero_c, zero_c];
825    // Temporary copies so `ext_mul`'s reduce decisions don't persist to the
826    // outer arrays; W materializes here (or hits a pre-seeded cache).
827    let (inv_prod, w) = t.ext_mul(&mut { b }, &mut { b_inv });
828    for i in 0..4 {
829        t.bb_assert_equal(inv_prod[i], one_ext[i]);
830    }
831    let a_ext = ext_value(&a);
832    let c_val = a_ext * b_inv_val;
833    let c_coeffs = c_val.as_basis_coefficients_slice();
834    let c: [BbWire<T::TapeCell>; 4] = core::array::from_fn(|i| t.bb_load_witness(c_coeffs[i]));
835    // Second mul uses the outer `b` at its original bit bounds; W is now cached.
836    let (prod, _) = t.ext_mul(&mut { b }, &mut { c });
837    for i in 0..4 {
838        t.bb_assert_equal(a[i], prod[i]);
839    }
840    for coeff in &c {
841        t.output(coeff.cell);
842    }
843    t.output(one_c.cell);
844    t.output(zero_c.cell);
845    t.output(w);
846}
847
848fn poseidon_x_power5<T: ReplayTape>(t: &mut T, x: T::TapeCell) -> T::TapeCell {
849    let x2 = t.gate_mul(x.value(), x.value());
850    let x4 = t.gate_mul(x2.value(), x2.value());
851    t.gate_mul(x.value(), x4.value())
852}
853
854fn poseidon_sbox<T: ReplayTape, const N: usize>(t: &mut T, state: &mut [T::TapeCell; N]) {
855    for x in state.iter_mut() {
856        *x = poseidon_x_power5(t, *x);
857    }
858}
859
860fn poseidon_add_rc<T: ReplayTape, const N: usize>(
861    t: &mut T,
862    state: &mut [T::TapeCell; N],
863    rc: &[Fr; N],
864) {
865    for (x, rc) in state.iter_mut().zip(rc.iter()) {
866        *x = t.gate_add(x.value(), *rc);
867    }
868}
869
870fn poseidon_matmul_external<T: ReplayTape, const N: usize>(
871    t: &mut T,
872    state: &mut [T::TapeCell; N],
873) {
874    let sum = t.gate_sum(state.map(|c| c.value()));
875    for (i, x) in state.iter_mut().enumerate() {
876        let new_x = x.value() + sum.value();
877        if i % 2 == 0 {
878            // `| new_x | x | -1 | sum |`, out at +0
879            let out = t.push(new_x);
880            t.push(x.value());
881            t.push(-Fr::ONE);
882            t.push(sum.value());
883            *x = out;
884        } else {
885            // `| x | 1 | new_x |`, out at +2
886            t.push(x.value());
887            t.push(Fr::ONE);
888            *x = t.push(new_x);
889        }
890    }
891}
892
893fn poseidon_matmul_internal<T: ReplayTape, const N: usize>(
894    t: &mut T,
895    state: &mut [T::TapeCell; N],
896    diag: &[Fr; N],
897) {
898    let sum = t.gate_sum(state.map(|c| c.value()));
899    for i in 0..N {
900        let new_s = state[i].value() * diag[i] + sum.value();
901        if i % 2 == 0 {
902            // `| new_s | s_i | -diag_i | sum |`, out at +0
903            let out = t.push(new_s);
904            t.push(state[i].value());
905            t.push(-diag[i]);
906            t.push(sum.value());
907            state[i] = out;
908        } else {
909            // `| s_i | diag_i | new_s |`, out at +2
910            t.push(state[i].value());
911            t.push(diag[i]);
912            state[i] = t.push(new_s);
913        }
914    }
915}
916
917fn poseidon_permute_run<T: ReplayTape, const N: usize>(
918    t: &mut T,
919    args: &[Fr],
920    params: &Poseidon2Params<Fr, N>,
921) {
922    let mut state: [T::TapeCell; N] = core::array::from_fn(|i| t.external(args[i]));
923    let rounds_f_beginning = params.rounds_f / 2;
924    poseidon_matmul_external(t, &mut state);
925    for r in 0..rounds_f_beginning {
926        poseidon_add_rc(t, &mut state, &params.external_rc[r]);
927        poseidon_sbox(t, &mut state);
928        poseidon_matmul_external(t, &mut state);
929    }
930    for r in 0..params.rounds_p {
931        state[0] = t.gate_add(state[0].value(), params.internal_rc[r]);
932        state[0] = poseidon_x_power5(t, state[0]);
933        poseidon_matmul_internal(t, &mut state, &params.mat_internal_diag_m_1);
934    }
935    for r in rounds_f_beginning..params.rounds_f {
936        poseidon_add_rc(t, &mut state, &params.external_rc[r]);
937        poseidon_sbox(t, &mut state);
938        poseidon_matmul_external(t, &mut state);
939    }
940    for cell in state {
941        t.output(cell);
942    }
943}
944
945/// Mirrors `decompose_bn254_to_base_baby_bear_digits`: 6 hint cells (5 digits + top
946/// quotient) followed by per-digit canonicity checks, recomposition, and boundary
947/// checks. Outputs `0..NUM_SAMPLES_PER_WORD` are the digit cells.
948fn decompose_run<T: ReplayTape>(t: &mut T, packed: Fr) {
949    let p = BigUint::from(BABY_BEAR_MODULUS_U64);
950    let one = BigUint::from(1u64);
951    let q = modulus::<Fr>();
952    let q_minus_one = &q - &one;
953    let pow_k = p.pow(NUM_SAMPLES_PER_WORD as u32);
954    let top_quotient_max = &q_minus_one / &pow_k;
955    let lower_max_plus_one = &q_minus_one - &top_quotient_max * &pow_k + &one;
956
957    // Hint witnesses: digits then top quotient.
958    let mut value = fe_to_biguint(&packed);
959    let digit_values: [Fr; NUM_SAMPLES_PER_WORD] = core::array::from_fn(|_| {
960        let digit = &value % &p;
961        value /= &p;
962        biguint_to_fe(&digit)
963    });
964    let top_quotient_value: Fr = biguint_to_fe(&value);
965    for &digit in &digit_values {
966        let cell = t.push(digit);
967        t.output(cell);
968    }
969    let top_quotient = t.push(top_quotient_value);
970
971    for &digit in &digit_values {
972        t.check_less_than_safe(digit, BABY_BEAR_MODULUS_U64);
973    }
974    t.is_big_less_than_safe(top_quotient.value(), &(&top_quotient_max + &one));
975    // assert_is_const: no advice cells
976
977    // lower = sum(digit_i * p^i); `p^0 == 1`, so the inner product starts with one.
978    let mut power = one.clone();
979    let lower = t.gate_inner_product(
980        digit_values.iter().map(|&digit| {
981            let coeff = biguint_to_fe(&power);
982            power *= &p;
983            (digit, coeff)
984        }),
985        true,
986    );
987
988    // packed == top_quotient * p^k + lower
989    t.gate_mul_add(top_quotient.value(), biguint_to_fe(&pow_k), lower.value());
990    // constrain_equal: no advice cells
991
992    let at_top_boundary = t.gate_is_equal(top_quotient.value(), biguint_to_fe(&top_quotient_max));
993    let lower_range_bits = (pow_k.bits() as usize).div_ceil(t.lookup_bits()) * t.lookup_bits();
994    t.range_check(lower.value(), lower_range_bits);
995    let lower_is_valid = t.is_less_than(
996        lower.value(),
997        biguint_to_fe(&lower_max_plus_one),
998        lower_range_bits,
999    );
1000    let lower_is_invalid = t.gate_not(lower_is_valid.value());
1001    t.gate_mul(at_top_boundary.value(), lower_is_invalid.value());
1002    // assert_is_const: no advice cells
1003}
1004
1005/// Executes one opcode against a replay tape. `args` are the operand values in
1006/// order; `bits` are the operand bit bounds (used by the BabyBear ops).
1007pub(crate) fn run_op<T: ReplayTape>(t: &mut T, opcode: &Halo2Opcode, args: &[Fr], bits: &[u16]) {
1008    match *opcode {
1009        Halo2Opcode::Const => {
1010            let out = t.push(args[0]);
1011            t.output(out);
1012        }
1013        Halo2Opcode::Select => {
1014            let out = t.gate_select(args[0], args[1], args[2]);
1015            t.output(out);
1016        }
1017        Halo2Opcode::Num2Bits(n) => t.gate_num_to_bits(args[0], n as usize),
1018        Halo2Opcode::BBReduce => {
1019            let a = t.bb_external(args[0], bits[0]);
1020            let out = t.bb_reduce(a);
1021            t.output(out.cell);
1022        }
1023        // The IR builder pre-reduces operands, so these are the pure gates (and
1024        // `BBMul`/`BBMulAdd` operands arrive post-swap).
1025        Halo2Opcode::BBAdd => {
1026            let out = t.gate_add(args[0], args[1]);
1027            t.output(out);
1028        }
1029        Halo2Opcode::BBNeg => {
1030            let out = t.gate_neg(args[0]);
1031            t.output(out);
1032        }
1033        Halo2Opcode::BBSub => {
1034            let out = t.gate_sub(args[0], args[1]);
1035            t.output(out);
1036        }
1037        Halo2Opcode::BBMul => {
1038            let out = t.gate_mul(args[0], args[1]);
1039            t.output(out);
1040        }
1041        Halo2Opcode::BBMulAdd => {
1042            let out = t.gate_mul_add(args[0], args[1], args[2]);
1043            t.output(out);
1044        }
1045        Halo2Opcode::BBDiv => bb_div_run(t, args[0], args[1], bits[0], bits[1]),
1046        Halo2Opcode::BBAssertZero => {
1047            let a = t.bb_external(args[0], bits[0]);
1048            t.bb_assert_zero(a);
1049        }
1050        Halo2Opcode::ExtMul => ext_mul_run(t, args, bits),
1051        Halo2Opcode::ExtDiv => ext_div_run(t, args, bits),
1052        Halo2Opcode::PoseidonPermute2T2 => {
1053            poseidon_permute_run(t, args, &POSEIDON2_COMPRESS_PARAMS)
1054        }
1055        Halo2Opcode::PoseidonPermute2T3 => poseidon_permute_run(t, args, &POSEIDON2_PARAMS),
1056        Halo2Opcode::LoadWitness => {
1057            let out = t.push(args.first().copied().unwrap_or(Fr::ONE));
1058            t.output(out);
1059        }
1060        Halo2Opcode::CheckLessThanSafe => {
1061            t.check_less_than_safe(args[0], BABY_BEAR_MODULUS_U64);
1062        }
1063        Halo2Opcode::InnerProduct(_) => {
1064            // Operands are interleaved `[v_0, c_0, v_1, c_1, ...]`; the gate starts
1065            // with one exactly when the first coefficient is the constant ONE.
1066            let starts_with_one = args[1] == Fr::ONE;
1067            let out = t.gate_inner_product(
1068                args.chunks_exact(2).map(|pair| (pair[0], pair[1])),
1069                starts_with_one,
1070            );
1071            t.output(out);
1072        }
1073        Halo2Opcode::DecomposeBn254ToBabyBear => decompose_run(t, args[0]),
1074        Halo2Opcode::RangeDiv(n) => {
1075            let rem = t.div_mod(
1076                args[0],
1077                &(BigUint::from(1u32) << (n as usize)),
1078                BABYBEAR_MAX_BITS,
1079            );
1080            t.output(rem);
1081        }
1082    }
1083}
1084
1085/// Shape of one opcode's tape footprint for a given constant-cache state.
1086pub(crate) struct OpcodeMeta {
1087    /// Relative offset of each logical output ([`UNMATERIALIZED`] on cache hit).
1088    pub output_offsets: Vec<usize>,
1089    /// Context-tape (advice) slots appended by the op.
1090    pub ctx_len: usize,
1091    /// Range-tape (lookup) slots appended by the op.
1092    pub lookups_len: usize,
1093    /// `load_constant` call indices that write a cell (cache misses), in call
1094    /// order; feeds [`WitnessTape::new`] at replay time.
1095    pub constant_skip_inds: Vec<u32>,
1096}
1097
1098/// Derives [`OpcodeMeta`] by replaying the op on a [`CalculateOffsetsTape`]
1099/// seeded with `warm` (constants already materialized by earlier nodes). The
1100/// tape shape is a pure function of operand bit bounds, constant argument
1101/// values, `lookup_bits`, and the warm set.
1102pub(crate) fn derive_opcode_metadata<'a>(
1103    opcode: &Halo2Opcode,
1104    args: &[Fr],
1105    bits: &[u16],
1106    lookup_bits: usize,
1107    warm: impl Iterator<Item = &'a Fr>,
1108) -> OpcodeMeta {
1109    let mut tape = CalculateOffsetsTape::new(lookup_bits, warm);
1110    run_op(&mut tape, opcode, args, bits);
1111    OpcodeMeta {
1112        output_offsets: tape.outputs.iter().map(|c| c.offset).collect(),
1113        ctx_len: tape.advice.len(),
1114        lookups_len: tape.lookups.len(),
1115        constant_skip_inds: tape.skip_inds,
1116    }
1117}
1118
1119/// Runtime replay: writes advice into `ctx` and lookups into `lookups`.
1120/// Buffers must be [`OpcodeMeta::ctx_len`]/[`OpcodeMeta::lookups_len`] long;
1121/// `write_const_inds` must be the node's [`OpcodeMeta::constant_skip_inds`].
1122pub(crate) fn interpret_op(
1123    opcode: &Halo2Opcode,
1124    args: &[Fr],
1125    bits: &[u16],
1126    ctx: &mut [Fr],
1127    lookups: &mut [Fr],
1128    lookup_bits: usize,
1129    write_const_inds: &[u32],
1130) {
1131    let mut tape = WitnessTape::new(ctx, lookups, lookup_bits, write_const_inds);
1132    run_op(&mut tape, opcode, args, bits);
1133}
1134
1135#[cfg(test)]
1136mod tests {
1137    use std::{collections::HashSet, sync::Arc};
1138
1139    use halo2_base::{
1140        gates::{
1141            circuit::{builder::BaseCircuitBuilder, CircuitBuilderStage},
1142            GateInstructions, RangeChip, RangeInstructions,
1143        },
1144        halo2_proofs::arithmetic::Field as _,
1145        AssignedValue, Context, QuantumCell,
1146    };
1147
1148    use super::*;
1149    use crate::{
1150        field::baby_bear::{BabyBearChip, BabyBearExt4Chip, BabyBearExt4Wire, BabyBearWire},
1151        hash::poseidon2::Poseidon2State,
1152        transcript::decompose_bn254_to_base_baby_bear_digits,
1153    };
1154
1155    const LOOKUP_BITS: usize = 11;
1156
1157    /// Flattens the range tape: every value sent to `add_cell_to_lookup`, in order.
1158    /// All tests use a single context, so the lookup manager holds a single tag.
1159    fn lookup_tape(range: &RangeChip<Fr>) -> Vec<Fr> {
1160        let map = range.lookup_manager()[0].cells_to_lookup.lock().unwrap();
1161        assert!(map.len() <= 1, "expected a single context tag");
1162        map.values()
1163            .flat_map(|cells| cells.iter().map(|c| c[0].value.evaluate()))
1164            .collect()
1165    }
1166
1167    /// `2^bits - 1 - salt`, an "adversarially large" value for a given bit bound.
1168    fn big_val(bits: usize, salt: u64) -> Fr {
1169        biguint_to_fe(&((BigUint::from(1u32) << bits) - 1u32 - salt))
1170    }
1171
1172    /// A raw (unconstrained) wire with an externally asserted bit bound, standing in
1173    /// for the output of an earlier node.
1174    fn raw_wire(ctx: &mut Context<Fr>, value: Fr, max_bits: usize) -> BabyBearWire {
1175        BabyBearWire {
1176            value: ctx.load_witness(value),
1177            max_bits,
1178        }
1179    }
1180
1181    fn bb_ext(vals: [u32; 4]) -> BabyBearExt4 {
1182        BabyBearExt4::from_basis_coefficients_fn(|i| BabyBear::from_u32(vals[i]))
1183    }
1184
1185    /// Runs the real halo2 op and the tape replay and compares them bit for bit.
1186    ///
1187    /// `run_real` loads its own inputs (and pre-warms constant caches for every
1188    /// value in `warm`), records the tape start positions, runs the chip op, and
1189    /// returns `(ctx_start, range_start, outputs)` with the outputs in logical
1190    /// output-index order. The replay runs on a [`CalculateOffsetsTape`] seeded
1191    /// with `warm`; [`derive_opcode_metadata`] and [`interpret_op`] (driven by the
1192    /// derived constant-skip indices) are also checked against the real tapes.
1193    fn check_opcode(
1194        lookup_bits: usize,
1195        opcode: Halo2Opcode,
1196        args: &[Fr],
1197        bits: &[u16],
1198        warm: &[Fr],
1199        run_real: impl FnOnce(
1200            &mut Context<Fr>,
1201            &Arc<RangeChip<Fr>>,
1202        ) -> (usize, usize, Vec<AssignedValue<Fr>>),
1203    ) {
1204        let mut builder = BaseCircuitBuilder::from_stage(CircuitBuilderStage::Mock)
1205            .use_k(lookup_bits + 1)
1206            .use_lookup_bits(lookup_bits);
1207        let range = Arc::new(builder.range_chip());
1208        let ctx = builder.main(0);
1209
1210        let (ctx_start, range_start, outputs) = run_real(ctx, &range);
1211        let warm: HashSet<Fr> = warm.iter().copied().collect();
1212        let name = opcode.name();
1213        let real_ctx: Vec<Fr> = ctx.advice[ctx_start..]
1214            .iter()
1215            .map(|a| a.evaluate())
1216            .collect();
1217        let real_range: Vec<Fr> = lookup_tape(&range)[range_start..].to_vec();
1218
1219        let mut tape = CalculateOffsetsTape::new(lookup_bits, warm.iter());
1220        run_op(&mut tape, &opcode, args, bits);
1221        assert_eq!(tape.advice, real_ctx, "{name}: context tape");
1222        assert_eq!(tape.lookups, real_range, "{name}: range tape");
1223
1224        assert_eq!(
1225            outputs.len(),
1226            tape.outputs.len(),
1227            "{name}: number of outputs"
1228        );
1229        for (i, val) in outputs.iter().enumerate() {
1230            let out = tape.outputs[i];
1231            assert_eq!(val.value.evaluate(), out.value, "{name}: output {i} value");
1232            let real_offset = val.cell.unwrap().offset;
1233            if out.offset == UNMATERIALIZED {
1234                assert!(
1235                    real_offset < ctx_start,
1236                    "{name}: output {i} should be a pre-warmed cell"
1237                );
1238            } else {
1239                assert_eq!(
1240                    real_offset,
1241                    ctx_start + out.offset,
1242                    "{name}: output {i} offset"
1243                );
1244            }
1245        }
1246
1247        let meta = derive_opcode_metadata(&opcode, args, bits, lookup_bits, warm.iter());
1248        assert_eq!(meta.ctx_len, real_ctx.len(), "{name}: meta ctx_len");
1249        assert_eq!(
1250            meta.lookups_len,
1251            real_range.len(),
1252            "{name}: meta lookups_len"
1253        );
1254        let offsets: Vec<usize> = tape.outputs.iter().map(|c| c.offset).collect();
1255        assert_eq!(meta.output_offsets, offsets, "{name}: meta output_offsets");
1256        assert_eq!(
1257            meta.constant_skip_inds, tape.skip_inds,
1258            "{name}: meta constant_skip_inds"
1259        );
1260
1261        let mut ctx_buf = vec![Fr::ZERO; meta.ctx_len];
1262        let mut range_buf = vec![Fr::ZERO; meta.lookups_len];
1263        interpret_op(
1264            &opcode,
1265            args,
1266            bits,
1267            &mut ctx_buf,
1268            &mut range_buf,
1269            lookup_bits,
1270            &meta.constant_skip_inds,
1271        );
1272        assert_eq!(ctx_buf, real_ctx, "{name}: interpret_op context tape");
1273        assert_eq!(range_buf, real_range, "{name}: interpret_op range tape");
1274    }
1275
1276    #[test]
1277    fn const_matches_backend() {
1278        for v in [Fr::from(42u64), Fr::ZERO] {
1279            check_opcode(LOOKUP_BITS, Halo2Opcode::Const, &[v], &[], &[], |ctx, _| {
1280                let start = ctx.advice.len();
1281                let out = if v == Fr::ZERO {
1282                    ctx.load_zero()
1283                } else {
1284                    ctx.load_constant(v)
1285                };
1286                (start, 0, vec![out])
1287            });
1288        }
1289    }
1290
1291    #[test]
1292    fn select_matches_backend() {
1293        for sel in [Fr::ZERO, Fr::ONE] {
1294            let (a, b) = (Fr::from(1234u64), Fr::from(5678u64));
1295            check_opcode(
1296                LOOKUP_BITS,
1297                Halo2Opcode::Select,
1298                &[a, b, sel],
1299                &[],
1300                &[],
1301                |ctx, range| {
1302                    let av = ctx.load_witness(a);
1303                    let bv = ctx.load_witness(b);
1304                    let sv = ctx.load_witness(sel);
1305                    let start = ctx.advice.len();
1306                    let out = range.gate().select(ctx, av, bv, sv);
1307                    (start, 0, vec![out])
1308                },
1309            );
1310        }
1311    }
1312
1313    #[test]
1314    fn num2bits_matches_backend() {
1315        for (n, v) in [(1u16, Fr::ONE), (16, Fr::from(0xABCDu64))] {
1316            check_opcode(
1317                LOOKUP_BITS,
1318                Halo2Opcode::Num2Bits(n),
1319                &[v],
1320                &[],
1321                &[],
1322                |ctx, range| {
1323                    let a = ctx.load_witness(v);
1324                    let start = ctx.advice.len();
1325                    let outputs = range.gate().num_to_bits(ctx, a, n as usize);
1326                    (start, 0, outputs)
1327                },
1328            );
1329        }
1330    }
1331
1332    fn run_bb_reduce(lookup_bits: usize, bits: u16, v: Fr) {
1333        check_opcode(
1334            lookup_bits,
1335            Halo2Opcode::BBReduce,
1336            &[v],
1337            &[bits],
1338            &[],
1339            |ctx, range| {
1340                let chip = BabyBearChip::new(range.clone());
1341                let wire = raw_wire(ctx, v, bits as usize);
1342                let start = ctx.advice.len();
1343                let range_start = lookup_tape(range).len();
1344                let out = chip.reduce(ctx, wire);
1345                (start, range_start, vec![out.value])
1346            },
1347        );
1348    }
1349
1350    #[test]
1351    fn bb_reduce_matches_backend() {
1352        run_bb_reduce(LOOKUP_BITS, 32, big_val(32, 7));
1353        run_bb_reduce(LOOKUP_BITS, 60, big_val(60, 99));
1354        run_bb_reduce(LOOKUP_BITS, 200, big_val(200, 5));
1355        // negative value: |v| = 123456789 < 2^30
1356        run_bb_reduce(LOOKUP_BITS, 30, -Fr::from(123456789u64));
1357        // different lookup_bits changes limb decompositions
1358        run_bb_reduce(17, 60, big_val(60, 99));
1359    }
1360
1361    #[test]
1362    fn bb_add_matches_backend() {
1363        let (va, vb) = (big_val(100, 3), big_val(60, 11));
1364        check_opcode(
1365            LOOKUP_BITS,
1366            Halo2Opcode::BBAdd,
1367            &[va, vb],
1368            &[],
1369            &[],
1370            |ctx, range| {
1371                let chip = BabyBearChip::new(range.clone());
1372                let a = raw_wire(ctx, va, 100);
1373                let b = raw_wire(ctx, vb, 60);
1374                let start = ctx.advice.len();
1375                let out = chip.add(ctx, a, b);
1376                (start, 0, vec![out.value])
1377            },
1378        );
1379    }
1380
1381    #[test]
1382    fn bb_neg_matches_backend() {
1383        let va = big_val(100, 3);
1384        check_opcode(
1385            LOOKUP_BITS,
1386            Halo2Opcode::BBNeg,
1387            &[va],
1388            &[],
1389            &[],
1390            |ctx, range| {
1391                let chip = BabyBearChip::new(range.clone());
1392                let a = raw_wire(ctx, va, 100);
1393                let start = ctx.advice.len();
1394                let out = chip.neg(ctx, a);
1395                (start, 0, vec![out.value])
1396            },
1397        );
1398    }
1399
1400    #[test]
1401    fn bb_sub_matches_backend() {
1402        let (va, vb) = (big_val(100, 3), big_val(60, 11));
1403        check_opcode(
1404            LOOKUP_BITS,
1405            Halo2Opcode::BBSub,
1406            &[va, vb],
1407            &[],
1408            &[],
1409            |ctx, range| {
1410                let chip = BabyBearChip::new(range.clone());
1411                let a = raw_wire(ctx, va, 100);
1412                let b = raw_wire(ctx, vb, 60);
1413                let start = ctx.advice.len();
1414                let out = chip.sub(ctx, a, b);
1415                (start, 0, vec![out.value])
1416            },
1417        );
1418    }
1419
1420    #[test]
1421    fn bb_mul_matches_backend() {
1422        // a_bits >= b_bits: the IR builder emits operands post-swap.
1423        let (va, vb) = (big_val(100, 3), big_val(60, 11));
1424        check_opcode(
1425            LOOKUP_BITS,
1426            Halo2Opcode::BBMul,
1427            &[va, vb],
1428            &[],
1429            &[],
1430            |ctx, range| {
1431                let chip = BabyBearChip::new(range.clone());
1432                let a = raw_wire(ctx, va, 100);
1433                let b = raw_wire(ctx, vb, 60);
1434                let start = ctx.advice.len();
1435                let out = chip.mul(ctx, a, b);
1436                (start, 0, vec![out.value])
1437            },
1438        );
1439    }
1440
1441    #[test]
1442    fn bb_mul_add_matches_backend() {
1443        let (va, vb, vc) = (big_val(100, 3), big_val(60, 11), big_val(90, 27));
1444        check_opcode(
1445            LOOKUP_BITS,
1446            Halo2Opcode::BBMulAdd,
1447            &[va, vb, vc],
1448            &[],
1449            &[],
1450            |ctx, range| {
1451                let chip = BabyBearChip::new(range.clone());
1452                let a = raw_wire(ctx, va, 100);
1453                let b = raw_wire(ctx, vb, 60);
1454                let c = raw_wire(ctx, vc, 90);
1455                let start = ctx.advice.len();
1456                let out = chip.mul_add(ctx, a, b, c);
1457                (start, 0, vec![out.value])
1458            },
1459        );
1460    }
1461
1462    fn run_bb_div(a_bits: u16, b_bits: u16, va: Fr, vb: Fr, prewarm_one: bool) {
1463        let warm: &[Fr] = if prewarm_one { &[Fr::ONE] } else { &[] };
1464        check_opcode(
1465            LOOKUP_BITS,
1466            Halo2Opcode::BBDiv,
1467            &[va, vb],
1468            &[a_bits, b_bits],
1469            warm,
1470            |ctx, range| {
1471                let chip = BabyBearChip::new(range.clone());
1472                if prewarm_one {
1473                    chip.load_constant(ctx, BabyBear::ONE);
1474                }
1475                let a = raw_wire(ctx, va, a_bits as usize);
1476                let b = raw_wire(ctx, vb, b_bits as usize);
1477                let start = ctx.advice.len();
1478                let range_start = lookup_tape(range).len();
1479                let out = chip.div(ctx, a, b);
1480                // Cache hit: recovers the ONE cell (assigned inside `div` when it
1481                // was not pre-warmed).
1482                let one = chip.load_constant(ctx, BabyBear::ONE);
1483                (start, range_start, vec![out.value, one.value])
1484            },
1485        );
1486    }
1487
1488    #[test]
1489    fn bb_div_matches_backend() {
1490        run_bb_div(31, 31, Fr::from(123456u64), Fr::from(654321u64), false);
1491        run_bb_div(31, 31, Fr::from(123456u64), Fr::from(654321u64), true);
1492        // large operands exercise the internal reduces
1493        run_bb_div(240, 230, big_val(240, 17), big_val(230, 23), false);
1494    }
1495
1496    #[test]
1497    fn bb_assert_zero_matches_backend() {
1498        let p = Fr::from(BABY_BEAR_MODULUS_U64);
1499        for (v, bits) in [(p * Fr::from(3u64), 34u16), (-(p * Fr::from(5u64)), 35)] {
1500            check_opcode(
1501                LOOKUP_BITS,
1502                Halo2Opcode::BBAssertZero,
1503                &[v],
1504                &[bits],
1505                &[],
1506                |ctx, range| {
1507                    let chip = BabyBearChip::new(range.clone());
1508                    let a = raw_wire(ctx, v, bits as usize);
1509                    let start = ctx.advice.len();
1510                    let range_start = lookup_tape(range).len();
1511                    chip.assert_zero(ctx, a);
1512                    (start, range_start, vec![])
1513                },
1514            );
1515        }
1516    }
1517
1518    const W_BB: BabyBear = <BabyBear as BinomiallyExtendable<4>>::W;
1519
1520    fn run_ext_mul(a_bits: [u16; 4], a_vals: [Fr; 4], prewarm_w: bool) {
1521        let b_vals = [5u64, 6, 7, 8].map(Fr::from);
1522        let warm = if prewarm_w {
1523            vec![fr_from_bb(W_BB)]
1524        } else {
1525            vec![]
1526        };
1527        let bits: [u16; 8] = core::array::from_fn(|i| if i < 4 { a_bits[i] } else { 31 });
1528        let args: Vec<Fr> = a_vals.iter().chain(b_vals.iter()).copied().collect();
1529        check_opcode(
1530            LOOKUP_BITS,
1531            Halo2Opcode::ExtMul,
1532            &args,
1533            &bits,
1534            &warm,
1535            |ctx, range| {
1536                let chip = BabyBearExt4Chip::new(BabyBearChip::new(range.clone()));
1537                if prewarm_w {
1538                    chip.base.load_constant(ctx, W_BB);
1539                }
1540                let a = BabyBearExt4Wire(core::array::from_fn(|i| {
1541                    raw_wire(ctx, a_vals[i], a_bits[i] as usize)
1542                }));
1543                let b = BabyBearExt4Wire(core::array::from_fn(|i| raw_wire(ctx, b_vals[i], 31)));
1544                let start = ctx.advice.len();
1545                let range_start = lookup_tape(range).len();
1546                let out = chip.mul(ctx, a, b);
1547                let mut outputs: Vec<AssignedValue<Fr>> = out.0.iter().map(|w| w.value).collect();
1548                // Cache hit: recovers the W cell.
1549                outputs.push(chip.base.load_constant(ctx, W_BB).value);
1550                (start, range_start, outputs)
1551            },
1552        );
1553    }
1554
1555    #[test]
1556    fn ext_mul_matches_backend() {
1557        let small = [1u64, 2, 3, 4].map(Fr::from);
1558        run_ext_mul([31; 4], small, false);
1559        run_ext_mul([31; 4], small, true);
1560        // a wide first coefficient exercises the special_inner_product reduces
1561        run_ext_mul(
1562            [230, 31, 31, 31],
1563            [
1564                big_val(230, 9),
1565                Fr::from(2u64),
1566                Fr::from(3u64),
1567                Fr::from(4u64),
1568            ],
1569            false,
1570        );
1571    }
1572
1573    fn run_ext_div(prewarm: bool) {
1574        let a_vals = [1u32, 2, 3, 4];
1575        let b_vals = [5u32, 6, 7, 8];
1576        let a_ext = bb_ext(a_vals);
1577        let b_ext = bb_ext(b_vals);
1578        let warm = if prewarm {
1579            vec![Fr::ONE, Fr::ZERO, fr_from_bb(W_BB)]
1580        } else {
1581            vec![]
1582        };
1583        let args: Vec<Fr> = a_vals
1584            .iter()
1585            .chain(b_vals.iter())
1586            .map(|&v| Fr::from(v as u64))
1587            .collect();
1588        check_opcode(
1589            LOOKUP_BITS,
1590            Halo2Opcode::ExtDiv,
1591            &args,
1592            &[31; 8],
1593            &warm,
1594            |ctx, range| {
1595                let chip = BabyBearExt4Chip::new(BabyBearChip::new(range.clone()));
1596                if prewarm {
1597                    chip.base.load_constant(ctx, BabyBear::ONE);
1598                    chip.base.load_constant(ctx, BabyBear::ZERO);
1599                    chip.base.load_constant(ctx, W_BB);
1600                }
1601                let a = chip.load_witness(ctx, a_ext);
1602                let b = chip.load_witness(ctx, b_ext);
1603                let start = ctx.advice.len();
1604                let range_start = lookup_tape(range).len();
1605                let out = chip.div(ctx, a, b);
1606                let mut outputs: Vec<AssignedValue<Fr>> = out.0.iter().map(|w| w.value).collect();
1607                // Cache hits recover the constant cells.
1608                outputs.push(chip.base.load_constant(ctx, BabyBear::ONE).value);
1609                outputs.push(chip.base.load_constant(ctx, BabyBear::ZERO).value);
1610                outputs.push(chip.base.load_constant(ctx, W_BB).value);
1611                (start, range_start, outputs)
1612            },
1613        );
1614    }
1615
1616    #[test]
1617    fn ext_div_matches_backend() {
1618        run_ext_div(false);
1619        run_ext_div(true);
1620    }
1621
1622    #[test]
1623    fn poseidon_t3_matches_backend() {
1624        let vals = [11u64, 22, 33].map(Fr::from);
1625        check_opcode(
1626            LOOKUP_BITS,
1627            Halo2Opcode::PoseidonPermute2T3,
1628            &vals,
1629            &[],
1630            &[],
1631            |ctx, range| {
1632                let s: [AssignedValue<Fr>; 3] = core::array::from_fn(|i| ctx.load_witness(vals[i]));
1633                let start = ctx.advice.len();
1634                let mut state = Poseidon2State::new(s);
1635                state.permutation(ctx, range.gate(), &POSEIDON2_PARAMS);
1636                (start, 0, state.s.to_vec())
1637            },
1638        );
1639    }
1640
1641    #[test]
1642    fn poseidon_t2_matches_backend() {
1643        let vals = [44u64, 55].map(Fr::from);
1644        check_opcode(
1645            LOOKUP_BITS,
1646            Halo2Opcode::PoseidonPermute2T2,
1647            &vals,
1648            &[],
1649            &[],
1650            |ctx, range| {
1651                let s: [AssignedValue<Fr>; 2] = core::array::from_fn(|i| ctx.load_witness(vals[i]));
1652                let start = ctx.advice.len();
1653                let mut state = Poseidon2State::new(s);
1654                state.permutation(ctx, range.gate(), &POSEIDON2_COMPRESS_PARAMS);
1655                (start, 0, state.s.to_vec())
1656            },
1657        );
1658    }
1659
1660    #[test]
1661    fn load_witness_matches_backend() {
1662        let v = Fr::from(777u64);
1663        check_opcode(
1664            LOOKUP_BITS,
1665            Halo2Opcode::LoadWitness,
1666            &[v],
1667            &[],
1668            &[],
1669            |ctx, _| {
1670                let start = ctx.advice.len();
1671                let out = ctx.load_witness(v);
1672                (start, 0, vec![out])
1673            },
1674        );
1675    }
1676
1677    #[test]
1678    fn load_check_less_than_safe_matches_backend() {
1679        let v = BabyBear::from_u32(1234567);
1680        check_opcode(
1681            LOOKUP_BITS,
1682            Halo2Opcode::CheckLessThanSafe,
1683            &[fr_from_bb(v)],
1684            &[],
1685            &[],
1686            |ctx, range| {
1687                let chip = BabyBearChip::new(range.clone());
1688                let start = ctx.advice.len() + 1; // first witness not assigned
1689                let range_start = lookup_tape(range).len();
1690                let _ = chip.load_reduced_witness(ctx, v);
1691                (start, range_start, vec![])
1692            },
1693        );
1694    }
1695
1696    fn run_inner_product(n: usize, starts_with_one: bool) {
1697        let vals: Vec<Fr> = (0..n).map(|i| Fr::from(100 + i as u64)).collect();
1698        let coeffs: Vec<Fr> = (0..n)
1699            .map(|i| {
1700                if i == 0 && starts_with_one {
1701                    Fr::ONE
1702                } else {
1703                    Fr::from(7 + 3 * i as u64)
1704                }
1705            })
1706            .collect();
1707        let args: Vec<Fr> = vals
1708            .iter()
1709            .zip(&coeffs)
1710            .flat_map(|(&v, &c)| [v, c])
1711            .collect();
1712        check_opcode(
1713            LOOKUP_BITS,
1714            Halo2Opcode::InnerProduct(n as u16),
1715            &args,
1716            &[],
1717            &[],
1718            |ctx, range| {
1719                let loaded: Vec<AssignedValue<Fr>> =
1720                    vals.iter().map(|&v| ctx.load_witness(v)).collect();
1721                let start = ctx.advice.len();
1722                let out = range.gate().inner_product(
1723                    ctx,
1724                    loaded.iter().map(|v| QuantumCell::Existing(*v)),
1725                    coeffs.iter().map(|&c| QuantumCell::Constant(c)),
1726                );
1727                (start, 0, vec![out])
1728            },
1729        );
1730    }
1731
1732    #[test]
1733    fn inner_product_matches_backend() {
1734        run_inner_product(1, true);
1735        run_inner_product(1, false);
1736        run_inner_product(3, true);
1737        run_inner_product(3, false);
1738    }
1739
1740    fn run_decompose(v: Fr) {
1741        check_opcode(
1742            LOOKUP_BITS,
1743            Halo2Opcode::DecomposeBn254ToBabyBear,
1744            &[v],
1745            &[],
1746            &[],
1747            |ctx, range| {
1748                let chip = BabyBearChip::new(range.clone());
1749                let packed = ctx.load_witness(v);
1750                let start = ctx.advice.len();
1751                let range_start = lookup_tape(range).len();
1752                let wires = decompose_bn254_to_base_baby_bear_digits(ctx, &chip, packed);
1753                let outputs = wires.iter().map(|w| w.value).collect();
1754                (start, range_start, outputs)
1755            },
1756        );
1757    }
1758
1759    #[test]
1760    fn decompose_matches_backend() {
1761        run_decompose(big_val(250, 12345));
1762        // near the top boundary of the Bn254 field
1763        run_decompose(-Fr::from(2u64));
1764    }
1765
1766    #[test]
1767    fn range_div_matches_backend() {
1768        let v = Fr::from(1234567u64);
1769        let bits = 16u16;
1770        check_opcode(
1771            LOOKUP_BITS,
1772            Halo2Opcode::RangeDiv(bits),
1773            &[v],
1774            &[],
1775            &[],
1776            |ctx, range| {
1777                let a = ctx.load_witness(v);
1778                let start = ctx.advice.len();
1779                let range_start = lookup_tape(range).len();
1780                let (_, rem) = range.div_mod(
1781                    ctx,
1782                    a,
1783                    BigUint::from(1u64) << (bits as usize),
1784                    BABYBEAR_MAX_BITS,
1785                );
1786                (start, range_start, vec![rem])
1787            },
1788        );
1789    }
1790}