openvm_algebra_circuit/
preflight.rs

1//! Fast PreflightExecutor implementations that use native field arithmetic
2//! instead of BigUint-based computation.
3//!
4//! During preflight execution, we only need to compute outputs to write to memory.
5//! The trace filler will re-execute with BigUint arithmetic for constraint generation.
6//! This module optimizes the preflight path by using native field arithmetic for known
7//! field types (K256, P256, BN254, BLS12-381).
8
9use openvm_algebra_transpiler::{Fp2Opcode, Rv32ModularArithmeticOpcode};
10use openvm_circuit::{
11    arch::{ExecutionError, PreflightExecutor, RecordArena, VmStateMut},
12    system::memory::online::TracingMemory,
13};
14use openvm_instructions::{instruction::Instruction, program::DEFAULT_PC_STEP};
15use openvm_mod_circuit_builder::{
16    run_field_expression_precomputed, FieldExpressionCoreRecordMut, FieldExpressionRecordLayout,
17};
18use openvm_rv32_adapters::Rv32VecHeapAdapterExecutor;
19use openvm_stark_backend::p3_field::PrimeField32;
20use strum::EnumCount;
21
22use crate::{
23    fields::{field_operation, fp2_operation, FieldType, Operation},
24    FieldExprVecHeapExecutor,
25};
26
27/// Generates a match statement dispatching (FieldType, Operation) pairs to a const-generic
28/// function call. Mirrors the `generate_field_dispatch!` macro in `execution.rs`.
29macro_rules! dispatch_field_op {
30    // Exhaustive variant (all field type × operation combinations listed)
31    ($fn:ident, $field_type:expr, $op:expr, $read_data:expr,
32     [$(($curve:ident, $operation:ident)),* $(,)?]) => {
33        match ($field_type, $op) {
34            $(
35                (FieldType::$curve, Operation::$operation) => $fn::<
36                    { FieldType::$curve as u8 },
37                    BLOCKS,
38                    BLOCK_SIZE,
39                    { Operation::$operation as u8 },
40                >($read_data),
41            )*
42            #[allow(unreachable_patterns)]
43            _ => unreachable!(),
44        }
45    };
46}
47
48/// Compute output using fast native field arithmetic for known field types.
49/// Returns None if the field type is not supported (falls back to slow path).
50#[inline]
51fn compute_output_fast<const BLOCKS: usize, const BLOCK_SIZE: usize, const IS_FP2: bool>(
52    field_type: Option<FieldType>,
53    operation: Option<Operation>,
54    read_data: [[[u8; BLOCK_SIZE]; BLOCKS]; 2],
55) -> Option<[[u8; BLOCK_SIZE]; BLOCKS]> {
56    let field_type = field_type?;
57    let op = operation?;
58
59    if IS_FP2 {
60        Some(compute_fp2_fast::<BLOCKS, BLOCK_SIZE>(
61            field_type, op, read_data,
62        ))
63    } else {
64        Some(compute_field_fast::<BLOCKS, BLOCK_SIZE>(
65            field_type, op, read_data,
66        ))
67    }
68}
69
70/// Compute field operation using native arithmetic.
71#[inline]
72fn compute_field_fast<const BLOCKS: usize, const BLOCK_SIZE: usize>(
73    field_type: FieldType,
74    op: Operation,
75    read_data: [[[u8; BLOCK_SIZE]; BLOCKS]; 2],
76) -> [[u8; BLOCK_SIZE]; BLOCKS] {
77    dispatch_field_op!(
78        field_operation,
79        field_type,
80        op,
81        read_data,
82        [
83            (K256Coordinate, Add),
84            (K256Coordinate, Sub),
85            (K256Coordinate, Mul),
86            (K256Coordinate, Div),
87            (K256Scalar, Add),
88            (K256Scalar, Sub),
89            (K256Scalar, Mul),
90            (K256Scalar, Div),
91            (P256Coordinate, Add),
92            (P256Coordinate, Sub),
93            (P256Coordinate, Mul),
94            (P256Coordinate, Div),
95            (P256Scalar, Add),
96            (P256Scalar, Sub),
97            (P256Scalar, Mul),
98            (P256Scalar, Div),
99            (BN254Coordinate, Add),
100            (BN254Coordinate, Sub),
101            (BN254Coordinate, Mul),
102            (BN254Coordinate, Div),
103            (BN254Scalar, Add),
104            (BN254Scalar, Sub),
105            (BN254Scalar, Mul),
106            (BN254Scalar, Div),
107            (BLS12_381Coordinate, Add),
108            (BLS12_381Coordinate, Sub),
109            (BLS12_381Coordinate, Mul),
110            (BLS12_381Coordinate, Div),
111            (BLS12_381Scalar, Add),
112            (BLS12_381Scalar, Sub),
113            (BLS12_381Scalar, Mul),
114            (BLS12_381Scalar, Div),
115        ]
116    )
117}
118
119/// Compute Fp2 operation using native arithmetic.
120#[inline]
121fn compute_fp2_fast<const BLOCKS: usize, const BLOCK_SIZE: usize>(
122    field_type: FieldType,
123    op: Operation,
124    read_data: [[[u8; BLOCK_SIZE]; BLOCKS]; 2],
125) -> [[u8; BLOCK_SIZE]; BLOCKS] {
126    dispatch_field_op!(
127        fp2_operation,
128        field_type,
129        op,
130        read_data,
131        [
132            (BN254Coordinate, Add),
133            (BN254Coordinate, Sub),
134            (BN254Coordinate, Mul),
135            (BN254Coordinate, Div),
136            (BLS12_381Coordinate, Add),
137            (BLS12_381Coordinate, Sub),
138            (BLS12_381Coordinate, Mul),
139            (BLS12_381Coordinate, Div),
140        ]
141    )
142}
143
144/// Convert local opcode to Operation enum for modular arithmetic.
145#[inline]
146fn local_opcode_to_modular_operation(local_opcode: usize) -> Option<Operation> {
147    let base_opcode = local_opcode % Rv32ModularArithmeticOpcode::COUNT;
148    match base_opcode {
149        x if x == Rv32ModularArithmeticOpcode::ADD as usize => Some(Operation::Add),
150        x if x == Rv32ModularArithmeticOpcode::SUB as usize => Some(Operation::Sub),
151        x if x == Rv32ModularArithmeticOpcode::MUL as usize => Some(Operation::Mul),
152        x if x == Rv32ModularArithmeticOpcode::DIV as usize => Some(Operation::Div),
153        _ => None,
154    }
155}
156
157/// Convert local opcode to Operation enum for Fp2 arithmetic.
158#[inline]
159fn local_opcode_to_fp2_operation(local_opcode: usize) -> Option<Operation> {
160    let base_opcode = local_opcode % Fp2Opcode::COUNT;
161    match base_opcode {
162        x if x == Fp2Opcode::ADD as usize => Some(Operation::Add),
163        x if x == Fp2Opcode::SUB as usize => Some(Operation::Sub),
164        x if x == Fp2Opcode::MUL as usize => Some(Operation::Mul),
165        x if x == Fp2Opcode::DIV as usize => Some(Operation::Div),
166        _ => None,
167    }
168}
169
170impl<F, RA, const BLOCKS: usize, const BLOCK_SIZE: usize, const IS_FP2: bool>
171    PreflightExecutor<F, RA> for FieldExprVecHeapExecutor<BLOCKS, BLOCK_SIZE, IS_FP2>
172where
173    F: PrimeField32,
174    for<'buf> RA: RecordArena<
175        'buf,
176        FieldExpressionRecordLayout<
177            F,
178            Rv32VecHeapAdapterExecutor<2, BLOCKS, BLOCKS, BLOCK_SIZE, BLOCK_SIZE>,
179        >,
180        (
181            <Rv32VecHeapAdapterExecutor<2, BLOCKS, BLOCKS, BLOCK_SIZE, BLOCK_SIZE> as openvm_circuit::arch::AdapterTraceExecutor<F>>::RecordMut<'buf>,
182            FieldExpressionCoreRecordMut<'buf>,
183        ),
184    >,
185{
186    fn execute(
187        &self,
188        state: VmStateMut<F, TracingMemory, RA>,
189        instruction: &Instruction<F>,
190    ) -> Result<(), ExecutionError> {
191        use openvm_circuit::arch::AdapterTraceExecutor;
192
193        let (mut adapter_record, mut core_record) =
194            state.ctx.alloc(self.inner.get_record_layout());
195
196        <Rv32VecHeapAdapterExecutor<2, BLOCKS, BLOCKS, BLOCK_SIZE, BLOCK_SIZE> as AdapterTraceExecutor<F>>::start(
197            *state.pc,
198            state.memory,
199            &mut adapter_record,
200        );
201
202        let read_data: [[[u8; BLOCK_SIZE]; BLOCKS]; 2] = self
203            .inner
204            .adapter()
205            .read(state.memory, instruction, &mut adapter_record);
206
207        let local_opcode = instruction.opcode.local_opcode_idx(self.inner.offset);
208
209        core_record
210            .fill_from_execution_data(local_opcode as u8, read_data.as_flattened().as_flattened());
211
212        // Try fast path: use native field arithmetic for known field types and non-setup operations
213        let operation = if IS_FP2 {
214            local_opcode_to_fp2_operation(local_opcode)
215        } else {
216            local_opcode_to_modular_operation(local_opcode)
217        };
218
219        let output: [[u8; BLOCK_SIZE]; BLOCKS] =
220            if let Some(output) = compute_output_fast::<BLOCKS, BLOCK_SIZE, IS_FP2>(
221                self.cached_field_type,
222                operation,
223                read_data,
224            ) {
225                output
226            } else {
227                // Fall back to slow path for unsupported field types or SETUP operations
228                let flag_idx = self
229                    .inner
230                    .local_opcode_idx
231                    .iter()
232                    .position(|&idx| idx == local_opcode)
233                    .and_then(|pos| {
234                        if pos < self.inner.opcode_flag_idx.len() {
235                            Some(self.inner.opcode_flag_idx[pos])
236                        } else {
237                            None
238                        }
239                    })
240                    .unwrap_or_else(|| self.inner.expr.num_flags());
241
242                run_field_expression_precomputed::<true>(
243                    &self.inner.expr,
244                    flag_idx,
245                    read_data.as_flattened().as_flattened(),
246                )
247                .into()
248            };
249
250        self.inner.adapter().write(
251            state.memory,
252            instruction,
253            output,
254            &mut adapter_record,
255        );
256
257        *state.pc = state.pc.wrapping_add(DEFAULT_PC_STEP);
258        Ok(())
259    }
260
261    fn get_opcode_name(&self, _opcode: usize) -> String {
262        self.inner.name.clone()
263    }
264}