openvm_rv32im_circuit/shift/
core.rs

1use std::{
2    array,
3    borrow::{Borrow, BorrowMut},
4};
5
6use openvm_circuit::{
7    arch::*,
8    system::memory::{online::TracingMemory, MemoryAuxColsFactory},
9};
10use openvm_circuit_primitives::{
11    bitwise_op_lookup::{BitwiseOperationLookupBus, SharedBitwiseOperationLookupChip},
12    utils::not,
13    var_range::{SharedVariableRangeCheckerChip, VariableRangeCheckerBus},
14    AlignedBytesBorrow, ColumnsAir, StructReflection, StructReflectionHelper,
15};
16use openvm_circuit_primitives_derive::AlignedBorrow;
17use openvm_instructions::{instruction::Instruction, program::DEFAULT_PC_STEP, LocalOpcode};
18use openvm_rv32im_transpiler::ShiftOpcode;
19use openvm_stark_backend::{
20    interaction::InteractionBuilder,
21    p3_air::{AirBuilder, BaseAir},
22    p3_field::{Field, PrimeCharacteristicRing, PrimeField32},
23    BaseAirWithPublicValues,
24};
25use strum::IntoEnumIterator;
26
27#[repr(C)]
28#[derive(AlignedBorrow, StructReflection, Clone, Copy, Debug)]
29pub struct ShiftCoreCols<T, const NUM_LIMBS: usize, const LIMB_BITS: usize> {
30    pub a: [T; NUM_LIMBS],
31    pub b: [T; NUM_LIMBS],
32    pub c: [T; NUM_LIMBS],
33
34    pub opcode_sll_flag: T,
35    pub opcode_srl_flag: T,
36    pub opcode_sra_flag: T,
37
38    // bit_multiplier = 2^bit_shift
39    pub bit_multiplier_left: T,
40    pub bit_multiplier_right: T,
41
42    // Sign of x for SRA
43    pub b_sign: T,
44
45    // Boolean columns that are 1 exactly at the index of the bit/limb shift amount
46    pub bit_shift_marker: [T; LIMB_BITS],
47    pub limb_shift_marker: [T; NUM_LIMBS],
48
49    // Part of each x[i] that gets bit shifted to the next limb
50    pub bit_shift_carry: [T; NUM_LIMBS],
51}
52
53/// RV32 shift AIR.
54/// Note: when the shift amount from operand is greater than the number of bits, only shift
55/// `shift_amount % num_bits` bits. This matches the RV32 specs for SLL/SRL/SRA.
56#[derive(Copy, Clone, Debug, derive_new::new, ColumnsAir)]
57#[columns_via(ShiftCoreCols<u8, NUM_LIMBS, LIMB_BITS>)]
58pub struct ShiftCoreAir<const NUM_LIMBS: usize, const LIMB_BITS: usize> {
59    pub bitwise_lookup_bus: BitwiseOperationLookupBus,
60    pub range_bus: VariableRangeCheckerBus,
61    pub offset: usize,
62}
63
64impl<F: Field, const NUM_LIMBS: usize, const LIMB_BITS: usize> BaseAir<F>
65    for ShiftCoreAir<NUM_LIMBS, LIMB_BITS>
66{
67    fn width(&self) -> usize {
68        ShiftCoreCols::<F, NUM_LIMBS, LIMB_BITS>::width()
69    }
70}
71impl<F: Field, const NUM_LIMBS: usize, const LIMB_BITS: usize> BaseAirWithPublicValues<F>
72    for ShiftCoreAir<NUM_LIMBS, LIMB_BITS>
73{
74}
75
76impl<AB, I, const NUM_LIMBS: usize, const LIMB_BITS: usize> VmCoreAir<AB, I>
77    for ShiftCoreAir<NUM_LIMBS, LIMB_BITS>
78where
79    AB: InteractionBuilder,
80    I: VmAdapterInterface<AB::Expr>,
81    I::Reads: From<[[AB::Expr; NUM_LIMBS]; 2]>,
82    I::Writes: From<[[AB::Expr; NUM_LIMBS]; 1]>,
83    I::ProcessedInstruction: From<MinimalInstruction<AB::Expr>>,
84{
85    fn eval(
86        &self,
87        builder: &mut AB,
88        local_core: &[AB::Var],
89        _from_pc: AB::Var,
90    ) -> AdapterAirContext<AB::Expr, I> {
91        let cols: &ShiftCoreCols<_, NUM_LIMBS, LIMB_BITS> = local_core.borrow();
92        let flags = [
93            cols.opcode_sll_flag,
94            cols.opcode_srl_flag,
95            cols.opcode_sra_flag,
96        ];
97
98        let is_valid = flags.iter().fold(AB::Expr::ZERO, |acc, &flag| {
99            builder.assert_bool(flag);
100            acc + flag.into()
101        });
102        builder.assert_bool(is_valid.clone());
103
104        let a = &cols.a;
105        let b = &cols.b;
106        let c = &cols.c;
107        let right_shift = cols.opcode_srl_flag + cols.opcode_sra_flag;
108
109        // Constrain that bit_shift, bit_multiplier are correct, i.e. that bit_multiplier =
110        // 1 << bit_shift. Because the sum of all bit_shift_marker[i] is constrained to be
111        // 1, bit_shift is guaranteed to be in range.
112        let mut bit_marker_sum = AB::Expr::ZERO;
113        let mut bit_shift = AB::Expr::ZERO;
114
115        for i in 0..LIMB_BITS {
116            builder.assert_bool(cols.bit_shift_marker[i]);
117            bit_marker_sum += cols.bit_shift_marker[i].into();
118            bit_shift += AB::Expr::from_usize(i) * cols.bit_shift_marker[i];
119
120            let mut when_bit_shift = builder.when(cols.bit_shift_marker[i]);
121            when_bit_shift.assert_eq(
122                cols.bit_multiplier_left,
123                AB::Expr::from_usize(1 << i) * cols.opcode_sll_flag,
124            );
125            when_bit_shift.assert_eq(
126                cols.bit_multiplier_right,
127                AB::Expr::from_usize(1 << i) * right_shift.clone(),
128            );
129        }
130        builder.when(is_valid.clone()).assert_one(bit_marker_sum);
131
132        // Check that a[i] = b[i] <</>> c[i] both on the bit and limb shift level if c <
133        // NUM_LIMBS * LIMB_BITS.
134        let mut limb_marker_sum = AB::Expr::ZERO;
135        let mut limb_shift = AB::Expr::ZERO;
136        for i in 0..NUM_LIMBS {
137            builder.assert_bool(cols.limb_shift_marker[i]);
138            limb_marker_sum += cols.limb_shift_marker[i].into();
139            limb_shift += AB::Expr::from_usize(i) * cols.limb_shift_marker[i];
140
141            let mut when_limb_shift = builder.when(cols.limb_shift_marker[i]);
142
143            for j in 0..NUM_LIMBS {
144                // SLL constraints
145                if j < i {
146                    when_limb_shift.assert_zero(a[j] * cols.opcode_sll_flag);
147                } else {
148                    let expected_a_left = if j - i == 0 {
149                        AB::Expr::ZERO
150                    } else {
151                        cols.bit_shift_carry[j - i - 1].into() * cols.opcode_sll_flag
152                    } + b[j - i] * cols.bit_multiplier_left
153                        - AB::Expr::from_usize(1 << LIMB_BITS)
154                            * cols.bit_shift_carry[j - i]
155                            * cols.opcode_sll_flag;
156                    when_limb_shift.assert_eq(a[j] * cols.opcode_sll_flag, expected_a_left);
157                }
158
159                // SRL and SRA constraints. Combining with above would require an additional column.
160                if j + i > NUM_LIMBS - 1 {
161                    when_limb_shift.assert_eq(
162                        a[j] * right_shift.clone(),
163                        cols.b_sign * AB::F::from_usize((1 << LIMB_BITS) - 1),
164                    );
165                } else {
166                    let expected_a_right = if j + i == NUM_LIMBS - 1 {
167                        cols.b_sign * (cols.bit_multiplier_right - AB::F::ONE)
168                    } else {
169                        cols.bit_shift_carry[j + i + 1].into() * right_shift.clone()
170                    } * AB::F::from_usize(1 << LIMB_BITS)
171                        + right_shift.clone() * (b[j + i] - cols.bit_shift_carry[j + i]);
172                    when_limb_shift.assert_eq(a[j] * cols.bit_multiplier_right, expected_a_right);
173                }
174            }
175        }
176        builder.when(is_valid.clone()).assert_one(limb_marker_sum);
177
178        // Check that bit_shift and limb_shift are correct.
179        let num_bits = AB::F::from_usize(NUM_LIMBS * LIMB_BITS);
180        self.range_bus
181            .range_check(
182                (c[0] - limb_shift * AB::F::from_usize(LIMB_BITS) - bit_shift.clone())
183                    * num_bits.inverse(),
184                LIMB_BITS - ((NUM_LIMBS * LIMB_BITS) as u32).ilog2() as usize,
185            )
186            .eval(builder, is_valid.clone());
187
188        // Check b_sign & b[NUM_LIMBS - 1] == b_sign using XOR
189        builder.assert_bool(cols.b_sign);
190        builder
191            .when(not(cols.opcode_sra_flag))
192            .assert_zero(cols.b_sign);
193
194        let mask = AB::F::from_u32(1 << (LIMB_BITS - 1));
195        let b_sign_shifted = cols.b_sign * mask;
196        self.bitwise_lookup_bus
197            .send_xor(
198                b[NUM_LIMBS - 1],
199                mask,
200                b[NUM_LIMBS - 1] + mask - (AB::Expr::from_u32(2) * b_sign_shifted),
201            )
202            .eval(builder, cols.opcode_sra_flag);
203
204        for i in 0..(NUM_LIMBS / 2) {
205            self.bitwise_lookup_bus
206                .send_range(a[i * 2], a[i * 2 + 1])
207                .eval(builder, is_valid.clone());
208        }
209
210        for carry in cols.bit_shift_carry {
211            self.range_bus
212                .send(carry, bit_shift.clone())
213                .eval(builder, is_valid.clone());
214        }
215
216        let expected_opcode = VmCoreAir::<AB, I>::expr_to_global_expr(
217            self,
218            flags
219                .iter()
220                .zip(ShiftOpcode::iter())
221                .fold(AB::Expr::ZERO, |acc, (flag, opcode)| {
222                    acc + (*flag).into() * AB::Expr::from_u8(opcode as u8)
223                }),
224        );
225
226        AdapterAirContext {
227            to_pc: None,
228            reads: [cols.b.map(Into::into), cols.c.map(Into::into)].into(),
229            writes: [cols.a.map(Into::into)].into(),
230            instruction: MinimalInstruction {
231                is_valid,
232                opcode: expected_opcode,
233            }
234            .into(),
235        }
236    }
237
238    fn start_offset(&self) -> usize {
239        self.offset
240    }
241}
242
243#[repr(C)]
244#[derive(AlignedBytesBorrow, Debug)]
245pub struct ShiftCoreRecord<const NUM_LIMBS: usize, const LIMB_BITS: usize> {
246    pub b: [u8; NUM_LIMBS],
247    pub c: [u8; NUM_LIMBS],
248    pub local_opcode: u8,
249}
250
251#[derive(Clone, Copy)]
252pub struct ShiftExecutor<A, const NUM_LIMBS: usize, const LIMB_BITS: usize> {
253    adapter: A,
254    pub offset: usize,
255}
256
257#[derive(Clone)]
258pub struct ShiftFiller<A, const NUM_LIMBS: usize, const LIMB_BITS: usize> {
259    adapter: A,
260    pub offset: usize,
261    pub bitwise_lookup_chip: SharedBitwiseOperationLookupChip<LIMB_BITS>,
262    pub range_checker_chip: SharedVariableRangeCheckerChip,
263}
264
265impl<A, const NUM_LIMBS: usize, const LIMB_BITS: usize> ShiftExecutor<A, NUM_LIMBS, LIMB_BITS> {
266    pub fn new(adapter: A, offset: usize) -> Self {
267        assert_eq!(NUM_LIMBS % 2, 0, "Number of limbs must be divisible by 2");
268        Self { adapter, offset }
269    }
270}
271
272impl<A, const NUM_LIMBS: usize, const LIMB_BITS: usize> ShiftFiller<A, NUM_LIMBS, LIMB_BITS> {
273    pub fn new(
274        adapter: A,
275        bitwise_lookup_chip: SharedBitwiseOperationLookupChip<LIMB_BITS>,
276        range_checker_chip: SharedVariableRangeCheckerChip,
277        offset: usize,
278    ) -> Self {
279        assert_eq!(NUM_LIMBS % 2, 0, "Number of limbs must be divisible by 2");
280        Self {
281            adapter,
282            offset,
283            bitwise_lookup_chip,
284            range_checker_chip,
285        }
286    }
287}
288
289impl<F, A, RA, const NUM_LIMBS: usize, const LIMB_BITS: usize> PreflightExecutor<F, RA>
290    for ShiftExecutor<A, NUM_LIMBS, LIMB_BITS>
291where
292    F: PrimeField32,
293    A: 'static
294        + AdapterTraceExecutor<
295            F,
296            ReadData: Into<[[u8; NUM_LIMBS]; 2]>,
297            WriteData: From<[[u8; NUM_LIMBS]; 1]>,
298        >,
299    for<'buf> RA: RecordArena<
300        'buf,
301        EmptyAdapterCoreLayout<F, A>,
302        (
303            A::RecordMut<'buf>,
304            &'buf mut ShiftCoreRecord<NUM_LIMBS, LIMB_BITS>,
305        ),
306    >,
307{
308    fn get_opcode_name(&self, opcode: usize) -> String {
309        format!("{:?}", ShiftOpcode::from_usize(opcode - self.offset))
310    }
311
312    fn execute(
313        &self,
314        state: VmStateMut<F, TracingMemory, RA>,
315        instruction: &Instruction<F>,
316    ) -> Result<(), ExecutionError> {
317        let Instruction { opcode, .. } = instruction;
318
319        let local_opcode = ShiftOpcode::from_usize(opcode.local_opcode_idx(self.offset));
320
321        let (mut adapter_record, core_record) = state.ctx.alloc(EmptyAdapterCoreLayout::new());
322
323        A::start(*state.pc, state.memory, &mut adapter_record);
324
325        let [rs1, rs2] = self
326            .adapter
327            .read(state.memory, instruction, &mut adapter_record)
328            .into();
329
330        let (output, _, _) = run_shift::<NUM_LIMBS, LIMB_BITS>(local_opcode, &rs1, &rs2);
331
332        core_record.b = rs1;
333        core_record.c = rs2;
334        core_record.local_opcode = local_opcode as u8;
335
336        self.adapter.write(
337            state.memory,
338            instruction,
339            [output].into(),
340            &mut adapter_record,
341        );
342        *state.pc = state.pc.wrapping_add(DEFAULT_PC_STEP);
343
344        Ok(())
345    }
346}
347
348impl<F, A, const NUM_LIMBS: usize, const LIMB_BITS: usize> TraceFiller<F>
349    for ShiftFiller<A, NUM_LIMBS, LIMB_BITS>
350where
351    F: PrimeField32,
352    A: 'static + AdapterTraceFiller<F>,
353{
354    fn fill_trace_row(&self, mem_helper: &MemoryAuxColsFactory<F>, row_slice: &mut [F]) {
355        // SAFETY: row_slice is guaranteed by the caller to have at least A::WIDTH +
356        // ShiftCoreCols::width() elements
357        let (adapter_row, mut core_row) = unsafe { row_slice.split_at_mut_unchecked(A::WIDTH) };
358        self.adapter.fill_trace_row(mem_helper, adapter_row);
359        // SAFETY: core_row contains a valid ShiftCoreRecord written by the executor
360        // during trace generation
361        let record: &ShiftCoreRecord<NUM_LIMBS, LIMB_BITS> =
362            unsafe { get_record_from_slice(&mut core_row, ()) };
363
364        let core_row: &mut ShiftCoreCols<F, NUM_LIMBS, LIMB_BITS> = core_row.borrow_mut();
365
366        let opcode = ShiftOpcode::from_usize(record.local_opcode as usize);
367        let (a, limb_shift, bit_shift) =
368            run_shift::<NUM_LIMBS, LIMB_BITS>(opcode, &record.b, &record.c);
369
370        for pair in a.chunks_exact(2) {
371            self.bitwise_lookup_chip
372                .request_range(pair[0] as u32, pair[1] as u32);
373        }
374
375        let num_bits_log = (NUM_LIMBS * LIMB_BITS).ilog2();
376        self.range_checker_chip.add_count(
377            ((record.c[0] as usize - bit_shift - limb_shift * LIMB_BITS) >> num_bits_log) as u32,
378            LIMB_BITS - num_bits_log as usize,
379        );
380
381        core_row.bit_shift_carry = if bit_shift == 0 {
382            for _ in 0..NUM_LIMBS {
383                self.range_checker_chip.add_count(0, 0);
384            }
385            [F::ZERO; NUM_LIMBS]
386        } else {
387            array::from_fn(|i| {
388                let carry = match opcode {
389                    ShiftOpcode::SLL => record.b[i] >> (LIMB_BITS - bit_shift),
390                    _ => record.b[i] % (1 << bit_shift),
391                };
392                self.range_checker_chip.add_count(carry as u32, bit_shift);
393                F::from_u8(carry)
394            })
395        };
396
397        core_row.limb_shift_marker = [F::ZERO; NUM_LIMBS];
398        core_row.limb_shift_marker[limb_shift] = F::ONE;
399        core_row.bit_shift_marker = [F::ZERO; LIMB_BITS];
400        core_row.bit_shift_marker[bit_shift] = F::ONE;
401
402        core_row.b_sign = F::ZERO;
403        if opcode == ShiftOpcode::SRA {
404            core_row.b_sign = F::from_u8(record.b[NUM_LIMBS - 1] >> (LIMB_BITS - 1));
405            self.bitwise_lookup_chip
406                .request_xor(record.b[NUM_LIMBS - 1] as u32, 1 << (LIMB_BITS - 1));
407        }
408
409        core_row.bit_multiplier_right = match opcode {
410            ShiftOpcode::SLL => F::ZERO,
411            _ => F::from_usize(1 << bit_shift),
412        };
413        core_row.bit_multiplier_left = match opcode {
414            ShiftOpcode::SLL => F::from_usize(1 << bit_shift),
415            _ => F::ZERO,
416        };
417
418        core_row.opcode_sra_flag = F::from_bool(opcode == ShiftOpcode::SRA);
419        core_row.opcode_srl_flag = F::from_bool(opcode == ShiftOpcode::SRL);
420        core_row.opcode_sll_flag = F::from_bool(opcode == ShiftOpcode::SLL);
421
422        core_row.c = record.c.map(F::from_u8);
423        core_row.b = record.b.map(F::from_u8);
424        core_row.a = a.map(F::from_u8);
425    }
426}
427
428// Returns (result, limb_shift, bit_shift)
429#[inline(always)]
430pub(super) fn run_shift<const NUM_LIMBS: usize, const LIMB_BITS: usize>(
431    opcode: ShiftOpcode,
432    x: &[u8; NUM_LIMBS],
433    y: &[u8; NUM_LIMBS],
434) -> ([u8; NUM_LIMBS], usize, usize) {
435    match opcode {
436        ShiftOpcode::SLL => run_shift_left::<NUM_LIMBS, LIMB_BITS>(x, y),
437        ShiftOpcode::SRL => run_shift_right::<NUM_LIMBS, LIMB_BITS>(x, y, true),
438        ShiftOpcode::SRA => run_shift_right::<NUM_LIMBS, LIMB_BITS>(x, y, false),
439    }
440}
441
442#[inline(always)]
443fn run_shift_left<const NUM_LIMBS: usize, const LIMB_BITS: usize>(
444    x: &[u8; NUM_LIMBS],
445    y: &[u8; NUM_LIMBS],
446) -> ([u8; NUM_LIMBS], usize, usize) {
447    let mut result = [0u8; NUM_LIMBS];
448
449    let (limb_shift, bit_shift) = get_shift::<NUM_LIMBS, LIMB_BITS>(y);
450
451    for i in limb_shift..NUM_LIMBS {
452        result[i] = if i > limb_shift {
453            (((x[i - limb_shift] as u16) << bit_shift)
454                | ((x[i - limb_shift - 1] as u16) >> (LIMB_BITS - bit_shift)))
455                % (1u16 << LIMB_BITS)
456        } else {
457            ((x[i - limb_shift] as u16) << bit_shift) % (1u16 << LIMB_BITS)
458        } as u8;
459    }
460    (result, limb_shift, bit_shift)
461}
462
463#[inline(always)]
464fn run_shift_right<const NUM_LIMBS: usize, const LIMB_BITS: usize>(
465    x: &[u8; NUM_LIMBS],
466    y: &[u8; NUM_LIMBS],
467    logical: bool,
468) -> ([u8; NUM_LIMBS], usize, usize) {
469    let fill = if logical {
470        0
471    } else {
472        (((1u16 << LIMB_BITS) - 1) as u8) * (x[NUM_LIMBS - 1] >> (LIMB_BITS - 1))
473    };
474    let mut result = [fill; NUM_LIMBS];
475
476    let (limb_shift, bit_shift) = get_shift::<NUM_LIMBS, LIMB_BITS>(y);
477
478    for i in 0..(NUM_LIMBS - limb_shift) {
479        let res = if i + limb_shift + 1 < NUM_LIMBS {
480            (((x[i + limb_shift] >> bit_shift) as u16)
481                | ((x[i + limb_shift + 1] as u16) << (LIMB_BITS - bit_shift)))
482                % (1u16 << LIMB_BITS)
483        } else {
484            (((x[i + limb_shift] >> bit_shift) as u16) | ((fill as u16) << (LIMB_BITS - bit_shift)))
485                % (1u16 << LIMB_BITS)
486        };
487        result[i] = res as u8;
488    }
489    (result, limb_shift, bit_shift)
490}
491
492#[inline(always)]
493fn get_shift<const NUM_LIMBS: usize, const LIMB_BITS: usize>(y: &[u8]) -> (usize, usize) {
494    debug_assert!(NUM_LIMBS * LIMB_BITS <= (1 << LIMB_BITS));
495    // We assume `NUM_LIMBS * LIMB_BITS <= 2^LIMB_BITS` so the shift is defined
496    // entirely in y[0].
497    let shift = (y[0] as usize) % (NUM_LIMBS * LIMB_BITS);
498    (shift / LIMB_BITS, shift % LIMB_BITS)
499}