Skip to main content

miden_processor/trace/parallel/
mod.rs

1use alloc::{boxed::Box, sync::Arc, vec::Vec};
2use core::borrow::{Borrow, BorrowMut};
3
4use itertools::Itertools;
5use miden_air::{
6    CoreCols, Felt, StackCols, SystemCols,
7    trace::{
8        DECODER_TRACE_WIDTH, MIN_TRACE_LEN, MainTrace, RANGE_CHECK_TRACE_WIDTH, RowIndex,
9        STACK_TRACE_WIDTH, SYS_TRACE_WIDTH, chiplets::bitwise::OP_CYCLE_LEN, decoder::NUM_OP_BITS,
10    },
11};
12use miden_core::{
13    ONE, Word, ZERO,
14    field::{PrimeCharacteristicRing, batch_inversion_allow_zeros},
15    mast::{MastForestId, OpBatch, SparseMastForest},
16    operations::opcodes,
17    program::{KernelDescriptor, MIN_STACK_DEPTH},
18    utils::Idx,
19};
20use rayon::prelude::*;
21use tracing::{info_span, instrument};
22
23use super::{
24    chiplets::Chiplets,
25    execution_tracer::TraceGenerationContext,
26    trace_state::{
27        AceReplay, BitwiseOp, BitwiseReplay, CoreTraceFragmentContext, CoreTraceState,
28        ExecutionReplay, HasherRequestReplay, KernelReplay, MemoryWritesReplay, RangeCheckerReplay,
29        ResolvedBasicBlockGroups, ResolvedHasherOp,
30    },
31};
32use crate::{
33    ContextId, ExecutionError,
34    continuation_stack::{Continuation, ContinuationStack},
35    errors::MapExecErrNoCtx,
36    trace::{
37        ChipletsLengths, ExecutionTrace, TraceBuildInputs, TraceLenSummary,
38        chiplets::{Ace, Bitwise, Hasher, KernelRom, Memory},
39        parallel::{processor::ReplayProcessor, tracer::CoreTraceGenerationTracer},
40        range::RangeChecker,
41        utils::RowMajorTraceWriter,
42    },
43};
44
45/// Per-row payload written by the core tracer (system + decoder + stack).
46pub const CORE_TRACE_WIDTH: usize = SYS_TRACE_WIDTH + DECODER_TRACE_WIDTH + STACK_TRACE_WIDTH;
47
48/// Physical row width of the core buffer: the [`CORE_TRACE_WIDTH`] payload plus the two
49/// trailing range-checker columns, which together form the per-AIR Core matrix
50/// (`NUM_CORE_COLS`) consumed directly by proving. The range columns are filled in-place
51/// after padding (see `write_range_into_core`).
52pub const CORE_STORAGE_WIDTH: usize = CORE_TRACE_WIDTH + RANGE_CHECK_TRACE_WIDTH;
53
54/// `build_trace()` uses this as a hard cap on trace rows.
55///
56/// The code checks `core_trace_contexts.len() * fragment_size` before allocation. It checks the
57/// same cap again while replaying chiplet activity. This keeps memory use bounded.
58pub(crate) const MAX_TRACE_LEN: usize = 1 << 29;
59
60pub(crate) mod core_trace_fragment;
61
62mod processor;
63mod tracer;
64
65#[cfg(test)]
66mod tests;
67
68// BUILD TRACE
69// ================================================================================================
70
71/// Builds the main trace from the provided trace states in parallel.
72///
73/// # Example
74/// ```
75/// use miden_assembly::Assembler;
76/// use miden_processor::{DefaultHost, FastProcessor, StackInputs};
77///
78/// let program = Assembler::default()
79///     .assemble_program("prg", "begin push.1 drop end")
80///     .unwrap()
81///     .unwrap_program();
82/// let mut host = DefaultHost::default();
83///
84/// let trace_inputs = FastProcessor::new(StackInputs::default())
85///     .execute_trace_inputs_sync(&program, &mut host)
86///     .unwrap();
87/// let trace = miden_processor::trace::build_trace(trace_inputs).unwrap();
88///
89/// assert_eq!(*trace.program_hash(), program.hash());
90/// ```
91#[instrument(name = "build_trace", skip_all)]
92pub fn build_trace(inputs: TraceBuildInputs) -> Result<ExecutionTrace, ExecutionError> {
93    build_trace_with_max_len(inputs, MAX_TRACE_LEN)
94}
95
96/// Same as [`build_trace`], but with a custom hard cap.
97///
98/// When the trace would go over `max_trace_len`, this returns
99/// [`ExecutionError::TraceLenExceeded`].
100pub fn build_trace_with_max_len(
101    inputs: TraceBuildInputs,
102    max_trace_len: usize,
103) -> Result<ExecutionTrace, ExecutionError> {
104    build_trace_inner(inputs, None, max_trace_len)
105}
106
107/// Same as [`build_trace`], but with a hasher chiplet that was already built — used by the
108/// streaming path, where the hasher builder runs concurrently with program execution
109/// (`FastProcessor::execute_and_build_trace_sync`, std-only).
110#[cfg(feature = "std")]
111pub(crate) fn build_trace_with_prebuilt_hasher(
112    inputs: TraceBuildInputs,
113    prebuilt_hasher: Hasher,
114) -> Result<ExecutionTrace, ExecutionError> {
115    build_trace_inner(inputs, Some(prebuilt_hasher), MAX_TRACE_LEN)
116}
117
118fn build_trace_inner(
119    inputs: TraceBuildInputs,
120    prebuilt_hasher: Option<Hasher>,
121    max_trace_len: usize,
122) -> Result<ExecutionTrace, ExecutionError> {
123    let TraceBuildInputs {
124        trace_output,
125        trace_generation_context,
126        program_info,
127    } = inputs;
128
129    let TraceGenerationContext {
130        core_trace_contexts,
131        mast_forest_store,
132        range_checker_replay,
133        memory_writes,
134        bitwise_replay: bitwise,
135        kernel_replay,
136        hasher_for_chiplet,
137        ace_replay,
138        fragment_size,
139        max_stack_depth,
140    } = trace_generation_context;
141
142    // Before any trace generation, check that the number of core trace rows doesn't exceed the
143    // maximum trace length. This is a necessary check to avoid OOM panics during trace generation,
144    // which can occur if the execution produces an extremely large number of steps.
145    //
146    // Note that we add 1 to the total core trace rows to account for the additional HALT opcode row
147    // that is pushed at the end of the last fragment.
148    let total_core_trace_rows = core_trace_contexts
149        .len()
150        .checked_mul(fragment_size)
151        .and_then(|n| n.checked_add(1))
152        .ok_or(ExecutionError::TraceLenExceeded(max_trace_len))?;
153    if total_core_trace_rows > max_trace_len {
154        return Err(ExecutionError::TraceLenExceeded(max_trace_len));
155    }
156
157    if core_trace_contexts.is_empty() {
158        return Err(ExecutionError::Internal(
159            "no trace fragments provided in the trace generation context",
160        ));
161    }
162
163    let chiplets = info_span!("initialize_chiplets").in_scope(|| {
164        initialize_chiplets(
165            program_info.kernel().clone(),
166            &core_trace_contexts,
167            memory_writes,
168            bitwise,
169            kernel_replay,
170            hasher_for_chiplet,
171            prebuilt_hasher,
172            ace_replay,
173            &mast_forest_store,
174            max_trace_len,
175        )
176    })?;
177
178    let range_checker = info_span!("initialize_range_checker")
179        .in_scope(|| initialize_range_checker(range_checker_replay, &chiplets));
180
181    let mut core_trace_data = info_span!("generate_core_trace").in_scope(|| {
182        generate_core_trace_row_major(
183            core_trace_contexts,
184            program_info.kernel().clone(),
185            fragment_size,
186            &mast_forest_store,
187            max_stack_depth,
188        )
189    })?;
190
191    let core_trace_len = core_trace_data.len() / CORE_STORAGE_WIDTH;
192
193    // Get the number of rows for the range checker
194    let range_table_len = range_checker.get_number_range_checker_rows();
195
196    let core_height = pad_to_trace_length(core_trace_len.max(range_table_len));
197    let chiplets_height = pad_to_trace_length(chiplets.trace_len());
198    let poseidon2_permutation_trace_len = chiplets.poseidon2_permutation_trace_len();
199    let poseidon2_permutation_height = pad_to_trace_length(poseidon2_permutation_trace_len);
200    let padded_trace_len = core_height.max(chiplets_height).max(poseidon2_permutation_height);
201
202    // Cap check against the padded height: pad-up can push over MAX_TRACE_LEN even
203    // when the unpadded check above passed.
204    if padded_trace_len > max_trace_len {
205        return Err(ExecutionError::TraceLenExceeded(max_trace_len));
206    }
207
208    let trace_len_summary = TraceLenSummary::new_with_padded(
209        core_trace_len,
210        range_table_len,
211        ChipletsLengths::new(&chiplets),
212        poseidon2_permutation_trace_len,
213        padded_trace_len,
214    );
215
216    // Each segment is built at its own per-AIR height (no cross-padding to the unified max).
217    let ((chiplets_trace, poseidon2_permutation_trace), ()) = info_span!("chiplet_traces_core_pad")
218        .in_scope(|| {
219            rayon::join(
220                || chiplets.into_traces(chiplets_height, poseidon2_permutation_height),
221                || pad_core_row_major(&mut core_trace_data, core_height),
222            )
223        });
224
225    // The range checker occupies the two trailing columns of the core buffer.
226    info_span!("write_range_checker_columns").in_scope(|| {
227        range_checker.write_range_into_core(
228            &mut core_trace_data,
229            CORE_STORAGE_WIDTH,
230            CORE_TRACE_WIDTH,
231            CORE_TRACE_WIDTH + 1,
232            range_table_len,
233            core_height,
234        )
235    });
236
237    // Create the MainTrace
238    let main_trace = {
239        let last_program_row = RowIndex::from((core_trace_len as u32).saturating_sub(1));
240        MainTrace::from_parts(
241            core_trace_data,
242            chiplets_trace.trace,
243            poseidon2_permutation_trace.trace,
244            last_program_row,
245        )
246    };
247
248    Ok(ExecutionTrace::new_from_parts(
249        program_info,
250        trace_output,
251        main_trace,
252        trace_len_summary,
253    ))
254}
255
256// HELPERS
257// ================================================================================================
258
259/// Pad a logical row count to a valid trace length: next power of two, clamped to `MIN_TRACE_LEN`.
260fn pad_to_trace_length(logical_len: usize) -> usize {
261    logical_len.next_power_of_two().max(MIN_TRACE_LEN)
262}
263
264/// Generates row-major core trace in parallel from the provided trace fragment contexts.
265fn generate_core_trace_row_major(
266    core_trace_contexts: Vec<CoreTraceFragmentContext>,
267    kernel: KernelDescriptor,
268    fragment_size: usize,
269    mast_forest_store: &[Arc<SparseMastForest>],
270    max_stack_depth: usize,
271) -> Result<Vec<Felt>, ExecutionError> {
272    let num_fragments = core_trace_contexts.len();
273    let total_allocated_rows = num_fragments * fragment_size;
274
275    let mut core_trace_data = Felt::zero_vec(total_allocated_rows * CORE_STORAGE_WIDTH);
276
277    // Save the first stack top for initialization
278    let first_stack_top = if let Some(first_context) = core_trace_contexts.first() {
279        first_context.state.stack.stack_top.to_vec()
280    } else {
281        vec![ZERO; MIN_STACK_DEPTH]
282    };
283
284    let writers: Vec<RowMajorTraceWriter<'_, Felt>> = core_trace_data
285        .chunks_exact_mut(fragment_size * CORE_STORAGE_WIDTH)
286        .map(|chunk| {
287            RowMajorTraceWriter::with_stride(chunk, CORE_STORAGE_WIDTH, CORE_STORAGE_WIDTH)
288        })
289        .collect();
290
291    // Build the core trace fragments in parallel
292    let fragment_results: Result<Vec<_>, ExecutionError> = core_trace_contexts
293        .into_par_iter()
294        .zip(writers.into_par_iter())
295        .map(|(trace_state, writer)| {
296            let (mut processor, mut tracer, mut continuation_stack, mut current_forest) =
297                split_trace_fragment_context(
298                    trace_state,
299                    writer,
300                    fragment_size,
301                    mast_forest_store,
302                    max_stack_depth,
303                )?;
304
305            processor.execute(
306                &mut continuation_stack,
307                &mut current_forest,
308                &kernel,
309                &mut tracer,
310            )?;
311
312            tracer.into_final_state()
313        })
314        .collect();
315    let fragment_results = fragment_results?;
316
317    let mut stack_rows = Vec::new();
318    let mut system_rows = Vec::new();
319    let mut total_core_trace_rows = 0;
320
321    for final_state in fragment_results {
322        stack_rows.push(final_state.last_stack_cols);
323        system_rows.push(final_state.last_system_cols);
324        total_core_trace_rows += final_state.num_rows_written;
325    }
326
327    // Fix up stack and system rows
328    fixup_stack_and_system_rows(
329        &mut core_trace_data,
330        fragment_size,
331        &stack_rows,
332        &system_rows,
333        &first_stack_top,
334    );
335
336    // Run batch inversion on stack's H0 helper column, processing each fragment in parallel.
337    // This must be done after fixup_stack_and_system_rows since that function overwrites the first
338    // row of each fragment with non-inverted values.
339    {
340        let w = CORE_STORAGE_WIDTH;
341        core_trace_data[..total_core_trace_rows * w]
342            .par_chunks_mut(fragment_size * w)
343            .for_each(|fragment_chunk| {
344                let num_rows = fragment_chunk.len() / w;
345                let mut h0_vals: Vec<Felt> = (0..num_rows)
346                    .map(|r| {
347                        let row: &CoreCols<Felt> = fragment_chunk[r * w..(r + 1) * w].borrow();
348                        row.stack.h0
349                    })
350                    .collect();
351                batch_inversion_allow_zeros(&mut h0_vals);
352                for (r, &val) in h0_vals.iter().enumerate() {
353                    let row: &mut CoreCols<Felt> = fragment_chunk[r * w..(r + 1) * w].borrow_mut();
354                    row.stack.h0 = val;
355                }
356            });
357    }
358
359    // Truncate the core trace columns to the actual number of rows written.
360    core_trace_data.truncate(total_core_trace_rows * CORE_STORAGE_WIDTH);
361
362    push_halt_opcode_row(
363        &mut core_trace_data,
364        total_core_trace_rows,
365        system_rows.last().ok_or(ExecutionError::Internal(
366            "no trace fragments provided in the trace generation context",
367        ))?,
368        stack_rows.last().ok_or(ExecutionError::Internal(
369            "no trace fragments provided in the trace generation context",
370        ))?,
371    );
372
373    Ok(core_trace_data)
374}
375
376/// Initializing the first row of each fragment with the appropriate stack and system state.
377///
378/// This needs to be done as a separate pass after all fragments have been generated, because the
379/// system and stack rows write the state at clk `i` to the row at index `i+1`. Hence, the state of
380/// the last row of any given fragment cannot be written in parallel, since any given fragment
381/// filler doesn't have access to the next fragment's first row.
382fn fixup_stack_and_system_rows(
383    core_trace_data: &mut [Felt],
384    fragment_size: usize,
385    stack_rows: &[StackCols<Felt>],
386    system_rows: &[SystemCols<Felt>],
387    first_stack_top: &[Felt],
388) {
389    const MIN_STACK_DEPTH_FELT: Felt = Felt::new_unchecked(MIN_STACK_DEPTH as u64);
390    let w = CORE_STORAGE_WIDTH;
391
392    {
393        let row: &mut CoreCols<Felt> = core_trace_data[..w].borrow_mut();
394
395        // Stack order in the trace is reversed vs `first_stack_top`.
396        for (stack_col_idx, &value) in first_stack_top.iter().rev().enumerate() {
397            row.stack.top[stack_col_idx] = value;
398        }
399
400        row.stack.b0 = MIN_STACK_DEPTH_FELT;
401        row.stack.b1 = ZERO;
402        row.stack.h0 = ZERO;
403    }
404
405    let total_rows = core_trace_data.len() / w;
406    let num_fragments = total_rows / fragment_size;
407
408    for frag_idx in 1..num_fragments {
409        let row_idx = frag_idx * fragment_size;
410        let row_start = row_idx * w;
411        let row: &mut CoreCols<Felt> = core_trace_data[row_start..row_start + w].borrow_mut();
412        row.system = system_rows[frag_idx - 1].clone();
413        row.stack = stack_rows[frag_idx - 1].clone();
414    }
415}
416
417/// Appends a HALT row (`num_rows_before` is the row count before append).
418///
419/// This ensures that the trace ends with at least one HALT operation, which is necessary to satisfy
420/// the constraints.
421fn push_halt_opcode_row(
422    core_trace_data: &mut Vec<Felt>,
423    num_rows_before: usize,
424    last_system_state: &SystemCols<Felt>,
425    last_stack_state: &StackCols<Felt>,
426) {
427    let w = CORE_STORAGE_WIDTH;
428    let mut row_data = [ZERO; CORE_STORAGE_WIDTH];
429
430    // Read the previous row's hasher state first half before we take a mutable borrow on
431    // `row_data` (propagates the program hash into the HALT padding).
432    let prev_hasher_state_first_half: [Felt; 4] = if num_rows_before > 0 {
433        let last_row_start = (num_rows_before - 1) * w;
434        let prev: &CoreCols<Felt> = core_trace_data[last_row_start..last_row_start + w].borrow();
435        let hs = &prev.decoder.hasher_state;
436        [hs[0], hs[1], hs[2], hs[3]]
437    } else {
438        [ZERO; 4]
439    };
440
441    {
442        let row: &mut CoreCols<Felt> = row_data.as_mut_slice().borrow_mut();
443
444        row.system = last_system_state.clone();
445        row.stack = last_stack_state.clone();
446
447        // Pad op_bits columns with HALT opcode bits
448        let halt_opcode = opcodes::HALT;
449        for bit_idx in 0..NUM_OP_BITS {
450            row.decoder.op_bits[bit_idx] = Felt::from_u8((halt_opcode >> bit_idx) & 1);
451        }
452
453        // Pad hasher state columns (8 columns)
454        // - First 4 columns: copy the last value (to propagate program hash)
455        // - Remaining 4 columns: fill with ZEROs
456        row.decoder.hasher_state[..4].copy_from_slice(&prev_hasher_state_first_half);
457
458        // Pad op_bit_extra columns (2 columns)
459        // - First column: do nothing (pre-filled with ZEROs, HALT doesn't use this)
460        // - Second column: fill with ONEs (product of two most significant HALT bits, both are 1)
461        row.decoder.extra[1] = ONE;
462    }
463
464    core_trace_data.extend_from_slice(&row_data);
465}
466
467/// Initializes the ranger checker from the recorded range checks during execution and returns it.
468///
469/// Note that the maximum number of rows that the range checker can produce is 2^16, which is less
470/// than the maximum trace length (2^29). Hence, we can safely generate the entire range checker
471/// trace and then pad it to the final trace length, without worrying about hitting memory limits.
472fn initialize_range_checker(
473    range_checker_replay: RangeCheckerReplay,
474    chiplets: &Chiplets,
475) -> RangeChecker {
476    let mut range_checker = RangeChecker::new();
477
478    // Add all u32 range checks recorded during execution
479    for values in range_checker_replay {
480        range_checker.add_range_checks(&values);
481    }
482
483    // Add all memory-related range checks
484    chiplets.append_range_checks(&mut range_checker);
485
486    range_checker
487}
488
489/// Replays recorded operations to populate chiplet traces. Results were already used during
490/// execution; this pass only needs the trace-recording side effects.
491///
492/// The five chiplets are populated from disjoint replays, so they build in parallel. Their
493/// non-hasher lengths are known from the replay metadata; checking those up front and giving the
494/// hasher only the remaining rows preserves the hard cap before any builder materializes its
495/// trace on the buffered path. A prebuilt (streamed) hasher was already built during execution
496/// under the full `max_trace_len` budget and is instead validated against the remaining rows
497/// after the fact.
498fn initialize_chiplets(
499    kernel: KernelDescriptor,
500    core_trace_contexts: &[CoreTraceFragmentContext],
501    memory_writes: MemoryWritesReplay,
502    bitwise: BitwiseReplay,
503    kernel_replay: KernelReplay,
504    hasher_for_chiplet: HasherRequestReplay,
505    prebuilt_hasher: Option<Hasher>,
506    ace_replay: AceReplay,
507    mast_forest_store: &[Arc<SparseMastForest>],
508    max_trace_len: usize,
509) -> Result<Chiplets, ExecutionError> {
510    let non_hasher_trace_len = non_hasher_trace_len(
511        &kernel,
512        core_trace_contexts,
513        &memory_writes,
514        &bitwise,
515        &ace_replay,
516        max_trace_len,
517    )?;
518    let max_hasher_trace_len = max_trace_len
519        .checked_sub(non_hasher_trace_len)
520        .ok_or(ExecutionError::TraceLenExceeded(max_trace_len))?;
521
522    if prebuilt_hasher
523        .as_ref()
524        .is_some_and(|hasher| hasher.trace_len() > max_hasher_trace_len)
525    {
526        return Err(ExecutionError::TraceLenExceeded(max_trace_len));
527    }
528
529    let (hasher, (bitwise, (memory, (ace, kernel_rom)))) = rayon::join(
530        || match prebuilt_hasher {
531            Some(hasher) => Ok(hasher),
532            None => build_hasher_chiplet(
533                hasher_for_chiplet.into_resolved_ops(mast_forest_store),
534                max_hasher_trace_len,
535            )
536            .map_err(|err| match err {
537                // The builder reports its internal remainder budget; surface the
538                // configured cap instead, like every other rejection site.
539                ExecutionError::TraceLenExceeded(_) => {
540                    ExecutionError::TraceLenExceeded(max_trace_len)
541                },
542                other => other,
543            }),
544        },
545        || {
546            rayon::join(
547                || build_bitwise_chiplet(bitwise, max_trace_len),
548                || {
549                    rayon::join(
550                        || build_memory_chiplet(memory_writes, core_trace_contexts, max_trace_len),
551                        || {
552                            rayon::join(
553                                || build_ace_chiplet(ace_replay, max_trace_len),
554                                || build_kernel_rom_chiplet(kernel, kernel_replay, max_trace_len),
555                            )
556                        },
557                    )
558                },
559            )
560        },
561    );
562
563    let chiplets = Chiplets {
564        hasher: hasher?,
565        bitwise: bitwise?,
566        memory: memory?,
567        ace: ace?,
568        kernel_rom: kernel_rom?,
569    };
570    debug_assert_eq!(
571        non_hasher_trace_len,
572        chiplets.trace_len() - chiplets.hasher.trace_len(),
573        "chiplet preflight length differs from the materialized trace",
574    );
575    // Release-only insurance: in debug builds a preflight undercount trips the
576    // assert above before this check can fire.
577    if chiplets.trace_len() > max_trace_len {
578        return Err(ExecutionError::TraceLenExceeded(max_trace_len));
579    }
580    Ok(chiplets)
581}
582
583fn non_hasher_trace_len(
584    kernel: &KernelDescriptor,
585    core_trace_contexts: &[CoreTraceFragmentContext],
586    memory_writes: &MemoryWritesReplay,
587    bitwise: &BitwiseReplay,
588    ace: &AceReplay,
589    max_trace_len: usize,
590) -> Result<usize, ExecutionError> {
591    let overflow = || ExecutionError::TraceLenExceeded(max_trace_len);
592    let bitwise_len = bitwise.num_operations().checked_mul(OP_CYCLE_LEN).ok_or_else(overflow)?;
593    let memory_reads_len = core_trace_contexts.iter().try_fold(0usize, |len, context| {
594        len.checked_add(context.replay.memory_reads.num_accesses()?)
595    });
596    let memory_len = memory_writes
597        .num_accesses()
598        .and_then(|writes| memory_reads_len.and_then(|reads| writes.checked_add(reads)))
599        .ok_or_else(overflow)?;
600    let ace_len = ace.trace_len().ok_or_else(overflow)?;
601
602    [1, kernel.proc_hashes().len(), bitwise_len, memory_len, ace_len]
603        .into_iter()
604        .try_fold(0usize, usize::checked_add)
605        .filter(|&total| total <= max_trace_len)
606        .ok_or_else(overflow)
607}
608
609/// Builds the hasher chiplet by replaying resolved requests in order.
610///
611/// The iterator abstracts over the two delivery modes: the buffered replay drained against the
612/// finalized forest store, or a live channel fed by a concurrently executing processor (see
613/// `FastProcessor::execute_and_build_trace_sync`).
614pub(crate) fn build_hasher_chiplet<'a>(
615    ops: impl IntoIterator<Item = Result<ResolvedHasherOp<'a>, ExecutionError>>,
616    max_trace_len: usize,
617) -> Result<Hasher, ExecutionError> {
618    let mut hasher = Hasher::default();
619    for hasher_op in ops {
620        match hasher_op? {
621            ResolvedHasherOp::Permute(input_state) => {
622                let _ = hasher.permute(input_state);
623            },
624            ResolvedHasherOp::HashControlBlock((h1, h2, domain, expected_hash)) => {
625                let _ = hasher.hash_control_block(h1, h2, domain, expected_hash);
626            },
627            ResolvedHasherOp::HashBasicBlock((batch_groups, expected_hash)) => match batch_groups {
628                ResolvedBasicBlockGroups::Borrowed(op_batches) => {
629                    let _ = hasher
630                        .hash_basic_block(op_batches.iter().map(OpBatch::groups), expected_hash);
631                },
632                ResolvedBasicBlockGroups::Owned(batch_groups) => {
633                    let _ = hasher.hash_basic_block(batch_groups.iter(), expected_hash);
634                },
635            },
636            ResolvedHasherOp::BuildMerkleRoot((value, path, index)) => {
637                let _ = hasher.build_merkle_root(value, &path, index);
638            },
639            ResolvedHasherOp::UpdateMerkleRoot((old_value, new_value, path, index)) => {
640                hasher.update_merkle_root(old_value, new_value, &path, index);
641            },
642        }
643        if hasher.trace_len() > max_trace_len {
644            return Err(ExecutionError::TraceLenExceeded(max_trace_len));
645        }
646    }
647    Ok(hasher)
648}
649
650/// Builds the bitwise chiplet by replaying recorded `u32and`/`u32xor` requests in order.
651fn build_bitwise_chiplet(
652    bitwise_replay: BitwiseReplay,
653    max_trace_len: usize,
654) -> Result<Bitwise, ExecutionError> {
655    let mut bitwise = Bitwise::default();
656    for (bitwise_op, a, b) in bitwise_replay {
657        match bitwise_op {
658            BitwiseOp::U32And => {
659                bitwise.u32and(a, b).map_exec_err_no_ctx()?;
660            },
661            BitwiseOp::U32Xor => {
662                bitwise.u32xor(a, b).map_exec_err_no_ctx()?;
663            },
664        }
665        if bitwise.trace_len() > max_trace_len {
666            return Err(ExecutionError::TraceLenExceeded(max_trace_len));
667        }
668    }
669    Ok(bitwise)
670}
671
672/// Builds the memory chiplet by replaying recorded accesses merged in clock-cycle order.
673fn build_memory_chiplet(
674    memory_writes: MemoryWritesReplay,
675    core_trace_contexts: &[CoreTraceFragmentContext],
676    max_trace_len: usize,
677) -> Result<Memory, ExecutionError> {
678    enum MemoryAccess {
679        ReadElement(Felt, ContextId, RowIndex),
680        WriteElement(Felt, Felt, ContextId, RowIndex),
681        ReadWord(Felt, ContextId, RowIndex),
682        WriteWord(Felt, Word, ContextId, RowIndex),
683    }
684
685    impl MemoryAccess {
686        fn clk(&self) -> RowIndex {
687            match self {
688                MemoryAccess::ReadElement(_, _, clk) => *clk,
689                MemoryAccess::WriteElement(_, _, _, clk) => *clk,
690                MemoryAccess::ReadWord(_, _, clk) => *clk,
691                MemoryAccess::WriteWord(_, _, _, clk) => *clk,
692            }
693        }
694    }
695
696    let mut memory = Memory::default();
697
698    // Note: care is taken to order all the accesses by clock cycle, since the memory chiplet
699    // currently assumes that all memory accesses are issued in the same order as they appear in
700    // the trace.
701    let elements_written: Box<dyn Iterator<Item = MemoryAccess>> =
702        Box::new(memory_writes.iter_elements_written().map(|(element, addr, ctx, clk)| {
703            MemoryAccess::WriteElement(*addr, *element, *ctx, *clk)
704        }));
705    let words_written: Box<dyn Iterator<Item = MemoryAccess>> = Box::new(
706        memory_writes
707            .iter_words_written()
708            .map(|(word, addr, ctx, clk)| MemoryAccess::WriteWord(*addr, *word, *ctx, *clk)),
709    );
710    let elements_read: Box<dyn Iterator<Item = MemoryAccess>> =
711        Box::new(core_trace_contexts.iter().flat_map(|ctx| {
712            ctx.replay
713                .memory_reads
714                .iter_read_elements()
715                .map(|(_, addr, ctx, clk)| MemoryAccess::ReadElement(addr, ctx, clk))
716        }));
717    let words_read: Box<dyn Iterator<Item = MemoryAccess>> =
718        Box::new(core_trace_contexts.iter().flat_map(|ctx| {
719            ctx.replay
720                .memory_reads
721                .iter_read_words()
722                .map(|(_, addr, ctx, clk)| MemoryAccess::ReadWord(addr, ctx, clk))
723        }));
724
725    [elements_written, words_written, elements_read, words_read]
726        .into_iter()
727        .kmerge_by(|a, b| a.clk() < b.clk())
728        .try_for_each(|mem_access| {
729            match mem_access {
730                MemoryAccess::ReadElement(addr, ctx, clk) => memory
731                    .read(ctx, addr, clk)
732                    .map(|_| ())
733                    .map_err(ExecutionError::MemoryErrorNoCtx)?,
734                MemoryAccess::WriteElement(addr, element, ctx, clk) => memory
735                    .write(ctx, addr, clk, element)
736                    .map_err(ExecutionError::MemoryErrorNoCtx)?,
737                MemoryAccess::ReadWord(addr, ctx, clk) => memory
738                    .read_word(ctx, addr, clk)
739                    .map(|_| ())
740                    .map_err(ExecutionError::MemoryErrorNoCtx)?,
741                MemoryAccess::WriteWord(addr, word, ctx, clk) => memory
742                    .write_word(ctx, addr, clk, word)
743                    .map_err(ExecutionError::MemoryErrorNoCtx)?,
744            }
745            if memory.trace_len() > max_trace_len {
746                return Err(ExecutionError::TraceLenExceeded(max_trace_len));
747            }
748            Ok(())
749        })?;
750
751    Ok(memory)
752}
753
754/// Builds the ACE chiplet by replaying recorded circuit evaluations in order.
755fn build_ace_chiplet(ace_replay: AceReplay, max_trace_len: usize) -> Result<Ace, ExecutionError> {
756    let mut ace = Ace::default();
757    for (clk, circuit_eval) in ace_replay.into_iter() {
758        ace.add_circuit_evaluation(clk, circuit_eval);
759        if ace.trace_len() > max_trace_len {
760            return Err(ExecutionError::TraceLenExceeded(max_trace_len));
761        }
762    }
763    Ok(ace)
764}
765
766/// Builds the kernel ROM chiplet by replaying recorded kernel procedure accesses in order.
767fn build_kernel_rom_chiplet(
768    kernel: KernelDescriptor,
769    kernel_replay: KernelReplay,
770    max_trace_len: usize,
771) -> Result<KernelRom, ExecutionError> {
772    let mut kernel_rom = KernelRom::new(kernel);
773    for proc_hash in kernel_replay.into_iter() {
774        kernel_rom.access_proc(proc_hash).map_exec_err_no_ctx()?;
775        if kernel_rom.trace_len() > max_trace_len {
776            return Err(ExecutionError::TraceLenExceeded(max_trace_len));
777        }
778    }
779    Ok(kernel_rom)
780}
781
782/// Pads the core trace to `core_height` rows (HALT template, CLK incremented per row).
783fn pad_core_row_major(core_trace_data: &mut Vec<Felt>, core_height: usize) {
784    let w = CORE_STORAGE_WIDTH;
785    let total_program_rows = core_trace_data.len() / w;
786    assert!(total_program_rows <= core_height);
787    assert!(total_program_rows > 0);
788
789    let num_padding_rows = core_height - total_program_rows;
790    if num_padding_rows == 0 {
791        return;
792    }
793    let last_row_start = (total_program_rows - 1) * w;
794
795    // Safety: per our documented safety guarantees, we know that `total_program_rows > 0`,
796    // and row `total_program_rows - 1` is initialized.
797    let (last_hasher_first_half, last_stack): ([Felt; 4], StackCols<Felt>) = {
798        let last: &CoreCols<Felt> = core_trace_data[last_row_start..last_row_start + w].borrow();
799        let hs = &last.decoder.hasher_state;
800        let last_hasher: [Felt; 4] = [hs[0], hs[1], hs[2], hs[3]];
801        (last_hasher, last.stack.clone())
802    };
803
804    let mut template_data = [ZERO; CORE_STORAGE_WIDTH];
805    {
806        let template: &mut CoreCols<Felt> = template_data.as_mut_slice().borrow_mut();
807
808        // Decoder columns
809        // ------------------------
810
811        // Pad op_bits columns with HALT opcode bits
812        let halt_opcode = opcodes::HALT;
813        for i in 0..NUM_OP_BITS {
814            template.decoder.op_bits[i] = Felt::from_u8((halt_opcode >> i) & 1);
815        }
816        // Pad hasher state columns (8 columns)
817        // - First 4 columns: copy the last value (to propagate program hash)
818        // - Remaining 4 columns: fill with ZEROs
819        template.decoder.hasher_state[..4].copy_from_slice(&last_hasher_first_half);
820
821        // Pad op_bit_extra columns (2 columns)
822        // - First column: do nothing (filled with ZEROs, HALT doesn't use this)
823        // - Second column: fill with ONEs (product of two most significant HALT bits, both are 1)
824        template.decoder.extra[1] = ONE;
825
826        // Stack columns
827        // ------------------------
828
829        // Pad stack columns with the last value in each column (analogous to Stack::into_trace())
830        template.stack = last_stack;
831    }
832
833    // System columns
834    // ------------------------
835
836    // Pad CLK trace - fill with index values
837
838    let pad_start = total_program_rows * w;
839    core_trace_data.resize(pad_start + num_padding_rows * w, ZERO);
840    core_trace_data[pad_start..]
841        .par_chunks_mut(w)
842        .enumerate()
843        .for_each(|(idx, row_buf)| {
844            row_buf.copy_from_slice(&template_data);
845            let row: &mut CoreCols<Felt> = row_buf.borrow_mut();
846            row.system.clk = Felt::from_u32((total_program_rows + idx) as u32);
847        });
848}
849
850type SplitFragmentContext<'a> = (
851    ReplayProcessor,
852    CoreTraceGenerationTracer<'a>,
853    ContinuationStack<Arc<SparseMastForest>>,
854    Arc<SparseMastForest>,
855);
856
857/// Uses the provided `CoreTraceFragmentContext` to build and return a `ReplayProcessor` and
858/// `CoreTraceGenerationTracer` that can be used to execute the fragment.
859///
860/// `mast_forest_store` provides the [`SparseMastForest`]s that the indices stored in the fragment
861/// (the initial forest index and the `EnterForest` continuations) refer to.
862///
863/// # Errors
864///
865/// Returns [`ExecutionError::Internal`] if any [`MastForestId`] referenced by the fragment
866/// (either `initial_mast_forest_id` or an `EnterForest` continuation) is out of range of
867/// `mast_forest_store`. Because [`CoreTraceFragmentContext`] is attacker-controllable when fed in
868/// from outside, we validate these indices rather than indexing-and-panicking.
869fn split_trace_fragment_context<'a>(
870    fragment_context: CoreTraceFragmentContext,
871    writer: RowMajorTraceWriter<'a, Felt>,
872    fragment_size: usize,
873    mast_forest_store: &[Arc<SparseMastForest>],
874    max_stack_depth: usize,
875) -> Result<SplitFragmentContext<'a>, ExecutionError> {
876    let CoreTraceFragmentContext {
877        state: CoreTraceState { system, decoder, stack },
878        replay:
879            ExecutionReplay {
880                block_stack: block_stack_replay,
881                execution_context: execution_context_replay,
882                stack_overflow: stack_overflow_replay,
883                memory_reads: memory_reads_replay,
884                advice: advice_replay,
885                hasher: hasher_response_replay,
886                block_address: block_address_replay,
887                mast_forest_resolution: mast_forest_resolution_replay,
888            },
889        continuation,
890        initial_mast_forest_id,
891    } = fragment_context;
892
893    let translated_continuation =
894        translate_snapshot_continuation_stack(continuation, mast_forest_store)?;
895
896    let initial_mast_forest =
897        lookup_mast_forest(mast_forest_store, initial_mast_forest_id)?.clone();
898
899    let processor = ReplayProcessor::new(
900        system,
901        stack,
902        stack_overflow_replay,
903        execution_context_replay,
904        advice_replay,
905        memory_reads_replay,
906        hasher_response_replay,
907        mast_forest_resolution_replay,
908        mast_forest_store.to_vec(),
909        max_stack_depth,
910        fragment_size.into(),
911    );
912    let tracer =
913        CoreTraceGenerationTracer::new(writer, decoder, block_address_replay, block_stack_replay);
914
915    Ok((processor, tracer, translated_continuation, initial_mast_forest))
916}
917
918/// Translates a snapshotted `ContinuationStack<MastForestId>` into one carrying actual
919/// [`Arc<SparseMastForest>`] handles, ready to drive `execute_impl`.
920///
921/// Returns [`ExecutionError::Internal`] if any `EnterForest` continuation carries a
922/// [`MastForestId`] that is out of range of `mast_forest_store`.
923fn translate_snapshot_continuation_stack(
924    snapshot: ContinuationStack<MastForestId>,
925    mast_forest_store: &[Arc<SparseMastForest>],
926) -> Result<ContinuationStack<Arc<SparseMastForest>>, ExecutionError> {
927    let mut out: ContinuationStack<Arc<SparseMastForest>> = ContinuationStack::default();
928    for cont in snapshot.into_inner() {
929        let translated = match cont {
930            Continuation::EnterForest { forest: id, package_debug_info } => {
931                Continuation::EnterForest {
932                    forest: lookup_mast_forest(mast_forest_store, id)?.clone(),
933                    package_debug_info,
934                }
935            },
936            Continuation::StartNode(id) => Continuation::StartNode(id),
937            Continuation::FinishJoin(id) => Continuation::FinishJoin(id),
938            Continuation::FinishSplit(id) => Continuation::FinishSplit(id),
939            Continuation::FinishLoop(node_id) => Continuation::FinishLoop(node_id),
940            Continuation::FinishCall(id) => Continuation::FinishCall(id),
941            Continuation::FinishDyn(id) => Continuation::FinishDyn(id),
942            Continuation::ResumeBasicBlock { node_id, batch_index, op_idx_in_batch } => {
943                Continuation::ResumeBasicBlock { node_id, batch_index, op_idx_in_batch }
944            },
945            Continuation::Respan { node_id, batch_index } => {
946                Continuation::Respan { node_id, batch_index }
947            },
948            Continuation::FinishBasicBlock(id) => Continuation::FinishBasicBlock(id),
949        };
950        out.push_continuation(translated);
951    }
952    Ok(out)
953}
954
955/// Looks up `id` in `mast_forest_store`, returning [`ExecutionError::Internal`] if it is out of
956/// range.
957pub(super) fn lookup_mast_forest(
958    mast_forest_store: &[Arc<SparseMastForest>],
959    id: MastForestId,
960) -> Result<&Arc<SparseMastForest>, ExecutionError> {
961    mast_forest_store
962        .get(id.to_usize())
963        .ok_or(ExecutionError::Internal("MastForestId out of range of mast_forest_store"))
964}