Skip to main content

miden_processor/trace/
execution_tracer.rs

1use alloc::{collections::BTreeMap, sync::Arc, vec::Vec};
2
3use miden_air::trace::chiplets::hasher::{
4    CONTROLLER_ROWS_PER_PERM_FELT, CONTROLLER_ROWS_PER_PERMUTATION, STATE_WIDTH,
5};
6use miden_core::{FMP_ADDR, FMP_INIT_VALUE, operations::Operation};
7
8use super::{
9    block_stack::{BlockInfo, BlockStack, ExecutionContextInfo},
10    stack::OverflowTable,
11    trace_state::{
12        AceReplay, AdviceReplay, BitwiseReplay, BlockAddressReplay, BlockStackReplay,
13        CoreTraceFragmentContext, CoreTraceState, DecoderState, ExecutionContextReplay,
14        ExecutionContextSystemInfo, ExecutionReplay, HasherRequestReplay, HasherResponseReplay,
15        KernelReplay, MastForestResolutionReplay, MemoryReadsReplay, MemoryWritesReplay,
16        RangeCheckerReplay, StackOverflowReplay, StackState, SystemState,
17    },
18    utils::split_u32_into_u16,
19};
20use crate::{
21    ContextId, EMPTY_WORD, ExecutionError, FastProcessor, Felt, MIN_STACK_DEPTH, ONE, RowIndex,
22    Word, ZERO,
23    continuation_stack::{Continuation, ContinuationStack},
24    crypto::merkle::MerklePath,
25    mast::{
26        BasicBlockNode, JoinNode, LoopNode, MastForest, MastForestId, MastNode, MastNodeExt,
27        MastNodeId, SparseMastForest, SparseMastForestBuilder, SplitNode, VisitKind,
28    },
29    processor::{Processor, StackInterface, SystemInterface},
30    trace::chiplets::{CircuitEvaluation, PTR_OFFSET_ELEM, PTR_OFFSET_WORD},
31    tracer::{OperationHelperRegisters, Tracer},
32    utils::Idx,
33};
34
35// STATE SNAPSHOT
36// ================================================================================================
37
38/// Execution state snapshot, used to record the state at the start of a trace fragment.
39#[derive(Debug)]
40struct StateSnapshot {
41    state: CoreTraceState,
42    /// Continuation stack with forest references already translated to [`MastForestId`]s into the
43    /// `mast_forest_store` of the [`TraceGenerationContext`] being built.
44    continuation_stack: ContinuationStack<MastForestId>,
45    /// The active forest at the start of this fragment, in `mast_forest_store`.
46    initial_mast_forest_id: MastForestId,
47}
48
49// TRACE GENERATION CONTEXT
50// ================================================================================================
51
52#[derive(Debug)]
53pub struct TraceGenerationContext {
54    /// The list of trace fragment contexts built during execution.
55    pub core_trace_contexts: Vec<CoreTraceFragmentContext>,
56
57    /// Sparse MAST forests, one per source [`MastForest`] visited during execution.
58    ///
59    /// Each entry contains only the [`MastNode`]s that were actually visited, while preserving the
60    /// original [`MastNodeId`]s of the source forest. References from `CoreTraceFragmentContext`,
61    /// `MastForestResolutionReplay`, and `HasherOp::HashBasicBlock` are encoded as
62    /// [`MastForestId`]s into this vector.
63    pub mast_forest_store: Vec<Arc<SparseMastForest>>,
64
65    // Replays that contain additional data needed to generate the range checker and chiplets
66    // columns.
67    pub range_checker_replay: RangeCheckerReplay,
68    pub memory_writes: MemoryWritesReplay,
69    pub bitwise_replay: BitwiseReplay,
70    pub hasher_for_chiplet: HasherRequestReplay,
71    pub kernel_replay: KernelReplay,
72    pub ace_replay: AceReplay,
73
74    /// The number of rows per core trace fragment, except for the last fragment which may be
75    /// shorter.
76    pub fragment_size: usize,
77
78    /// The maximum number of field elements allowed on the operand stack in an active execution
79    /// context.
80    pub max_stack_depth: usize,
81}
82
83/// Builder for recording the context to generate trace fragments during execution.
84///
85/// Specifically, this records the information necessary to be able to generate the trace in
86/// fragments of configurable length. This requires storing state at the very beginning of the
87/// fragment before any operations are executed, as well as recording the various values read during
88/// execution in the corresponding "replays" (e.g. values read from memory are recorded in
89/// `MemoryReadsReplay`, values read from the advice provider are recorded in `AdviceReplay``, etc).
90///
91/// Then, to generate a trace fragment, we initialize the state of the processor using the stored
92/// snapshot from the beginning of the fragment, and replay the recorded values as they are
93/// encountered during execution (e.g. when encountering a memory read operation, we will replay the
94/// value rather than querying the memory chiplet).
95#[derive(Debug)]
96pub struct ExecutionTracer {
97    // State stored at the start of a core trace fragment.
98    //
99    // This field is only set to `None` at initialization, and is populated when starting a new
100    // trace fragment with `Self::start_new_fragment_context()`. Hence, on the first call to
101    // `Self::start_new_fragment_context()`, we don't extract a new `TraceFragmentContext`, but in
102    // every other call, we do.
103    state_snapshot: Option<StateSnapshot>,
104
105    // Replay data aggregated throughout the execution of a core trace fragment
106    overflow_table: OverflowTable,
107    overflow_replay: StackOverflowReplay,
108
109    block_stack: BlockStack,
110    block_stack_replay: BlockStackReplay,
111    execution_context_replay: ExecutionContextReplay,
112
113    hasher_chiplet_shim: HasherChipletShim,
114    memory_reads: MemoryReadsReplay,
115    advice: AdviceReplay,
116    external: MastForestResolutionReplay,
117
118    // Replays that contain additional data needed to generate the range checker and chiplets
119    // columns.
120    range_checker: RangeCheckerReplay,
121    memory_writes: MemoryWritesReplay,
122    bitwise: BitwiseReplay,
123    kernel: KernelReplay,
124    hasher_for_chiplet: HasherRequestReplay,
125    ace: AceReplay,
126
127    // Output
128    fragment_contexts: Vec<CoreTraceFragmentContext>,
129
130    /// Per-source-forest sparse builders, indexed by `mast_forest_indices`.
131    ///
132    /// Each builder accumulates the [`MastNodeId`]s of nodes visited inside its source forest
133    /// during execution, and is finalized into an [`Arc<SparseMastForest>`] in
134    /// [`Self::into_trace_generation_context`].
135    mast_forest_builders: Vec<SparseMastForestBuilder>,
136
137    /// Maps a source forest's `Arc::as_ptr` identity to its [`MastForestId`] in
138    /// `mast_forest_builders` (and, by construction, the eventual id in
139    /// `TraceGenerationContext::mast_forest_store`).
140    ///
141    /// Pointer identity is sound here because the trace-generation flow never crosses a process
142    /// boundary: the tracer builds the store and the replay processor consumes it in the same
143    /// run.
144    mast_forest_ids: BTreeMap<*const MastForest, MastForestId>,
145
146    /// The number of rows per core trace fragment.
147    fragment_size: usize,
148
149    /// The maximum number of field elements allowed on the operand stack in an active execution
150    /// context.
151    max_stack_depth: usize,
152
153    /// Flag set in `start_clock_cycle` when a Call/Syscall/Dyncall END is encountered, consumed
154    /// in `finalize_clock_cycle` to call `overflow_table.restore_context()`. This is deferred to
155    /// `finalize_clock_cycle` because `finalize_clock_cycle` is only called when the operation
156    /// succeeds (i.e., the stack depth check passes).
157    pending_restore_context: bool,
158
159    /// Flag set in `start_clock_cycle` when an `EvalCircuit` operation is encountered, consumed
160    /// in `finalize_clock_cycle` to record the memory reads performed by the operation.
161    is_eval_circuit_op: bool,
162}
163
164impl ExecutionTracer {
165    /// Creates a tracer whose hasher-chiplet requests stream to `hasher_sender` as they are
166    /// recorded, instead of buffering for post-execution replay. Everything else records as in
167    /// [`Self::new`].
168    #[cfg(feature = "std")]
169    pub(crate) fn new_with_streamed_hasher(
170        fragment_size: usize,
171        max_stack_depth: usize,
172        hasher_sender: std::sync::mpsc::Sender<crate::trace::ResolvedHasherOp<'static>>,
173    ) -> Self {
174        let mut tracer = Self::new(fragment_size, max_stack_depth);
175        tracer.hasher_for_chiplet = HasherRequestReplay::streamed(hasher_sender);
176        tracer
177    }
178
179    /// Creates a new `ExecutionTracer` with the given fragment size.
180    #[inline(always)]
181    pub fn new(fragment_size: usize, max_stack_depth: usize) -> Self {
182        Self {
183            state_snapshot: None,
184            overflow_table: OverflowTable::default(),
185            overflow_replay: StackOverflowReplay::default(),
186            block_stack: BlockStack::default(),
187            block_stack_replay: BlockStackReplay::default(),
188            execution_context_replay: ExecutionContextReplay::default(),
189            hasher_chiplet_shim: HasherChipletShim::default(),
190            memory_reads: MemoryReadsReplay::default(),
191            range_checker: RangeCheckerReplay::default(),
192            memory_writes: MemoryWritesReplay::default(),
193            advice: AdviceReplay::default(),
194            bitwise: BitwiseReplay::default(),
195            kernel: KernelReplay::default(),
196            hasher_for_chiplet: HasherRequestReplay::default(),
197            ace: AceReplay::default(),
198            external: MastForestResolutionReplay::default(),
199            fragment_contexts: Vec::new(),
200            mast_forest_builders: Vec::new(),
201            mast_forest_ids: BTreeMap::new(),
202            fragment_size,
203            max_stack_depth,
204            pending_restore_context: false,
205            is_eval_circuit_op: false,
206        }
207    }
208
209    /// Returns the [`MastForestId`] of `forest` in [`Self::mast_forest_builders`], creating a new
210    /// builder for it on first encounter. Forests are identified by `Arc::as_ptr`.
211    #[inline]
212    fn forest_id(&mut self, forest: &Arc<MastForest>) -> MastForestId {
213        let key = Arc::as_ptr(forest);
214        if let Some(&id) = self.mast_forest_ids.get(&key) {
215            return id;
216        }
217
218        let id = MastForestId::from(self.mast_forest_builders.len() as u32);
219        self.mast_forest_builders.push(SparseMastForestBuilder::new(forest.clone()));
220        self.mast_forest_ids.insert(key, id);
221        id
222    }
223
224    /// Records that the node with id `node_id` was visited inside `forest`. Also records the
225    /// node's immediate children (if any) as digest-only references.
226    ///
227    /// The parent is recorded as a [`VisitKind::FullVisit`] so its full [`MastNode`] is available
228    /// at replay time. Children are recorded as [`VisitKind::DigestOnly`] because trace
229    /// generation only needs their digest when building the parent's trace row (e.g. a Split
230    /// node needs the digest of *both* branches even if only one is taken; a Join needs both
231    /// children's digests; a Call needs the callee's digest). If a child is later actually
232    /// entered, a subsequent `record_visit` call promotes it to a full visit.
233    ///
234    /// Keeping un-entered children as digest-only means accidental entry into a pruned node
235    /// surfaces as a clean `get_node_by_id` miss rather than a partially-populated node.
236    #[inline]
237    fn record_visit(&mut self, forest: &Arc<MastForest>, node_id: MastNodeId) {
238        let id = self.forest_id(forest);
239        self.mast_forest_builders[id.to_usize()].record_visit(node_id, VisitKind::FullVisit);
240
241        if let Some(node) = forest.get_node_by_id(node_id) {
242            node.for_each_child(|child_id| {
243                self.mast_forest_builders[id.to_usize()]
244                    .record_visit(child_id, VisitKind::DigestOnly);
245            });
246        }
247    }
248
249    /// Translates a live continuation stack carrying `Arc<MastForest>` references into one carrying
250    /// [`MastForestId`]s into `mast_forest_builders`.
251    fn translate_continuation_stack(
252        &mut self,
253        live: ContinuationStack<Arc<MastForest>>,
254    ) -> ContinuationStack<MastForestId> {
255        let mut translated: ContinuationStack<MastForestId> = ContinuationStack::default();
256        for cont in live.into_inner() {
257            let translated_cont = match cont {
258                Continuation::EnterForest { forest, package_debug_info } => {
259                    Continuation::EnterForest {
260                        forest: self.forest_id(&forest),
261                        package_debug_info,
262                    }
263                },
264                Continuation::StartNode(id) => Continuation::StartNode(id),
265                Continuation::FinishJoin(id) => Continuation::FinishJoin(id),
266                Continuation::FinishSplit(id) => Continuation::FinishSplit(id),
267                Continuation::FinishLoop(node_id) => Continuation::FinishLoop(node_id),
268                Continuation::FinishCall(id) => Continuation::FinishCall(id),
269                Continuation::FinishDyn(id) => Continuation::FinishDyn(id),
270                Continuation::ResumeBasicBlock { node_id, batch_index, op_idx_in_batch } => {
271                    Continuation::ResumeBasicBlock { node_id, batch_index, op_idx_in_batch }
272                },
273                Continuation::Respan { node_id, batch_index } => {
274                    Continuation::Respan { node_id, batch_index }
275                },
276                Continuation::FinishBasicBlock(id) => Continuation::FinishBasicBlock(id),
277            };
278            translated.push_continuation(translated_cont);
279        }
280        translated
281    }
282
283    /// Convert the `ExecutionTracer` into a [TraceGenerationContext] using the data accumulated
284    /// during execution.
285    #[inline(always)]
286    pub fn into_trace_generation_context(mut self) -> TraceGenerationContext {
287        // If there is an ongoing trace state being built, finish it
288        self.finish_current_fragment_context();
289
290        // Finalize each per-source-forest builder into a `SparseMastForest`. Indices stored on
291        // fragments and replays line up with the position in this vector by construction (the
292        // builders were appended in the same order the indices were assigned).
293        let mast_forest_store = self
294            .mast_forest_builders
295            .into_iter()
296            .map(|builder| Arc::new(builder.finalize()))
297            .collect();
298
299        TraceGenerationContext {
300            core_trace_contexts: self.fragment_contexts,
301            mast_forest_store,
302            range_checker_replay: self.range_checker,
303            memory_writes: self.memory_writes,
304            bitwise_replay: self.bitwise,
305            kernel_replay: self.kernel,
306            hasher_for_chiplet: self.hasher_for_chiplet,
307            ace_replay: self.ace,
308            fragment_size: self.fragment_size,
309            max_stack_depth: self.max_stack_depth,
310        }
311    }
312
313    // HELPERS
314    // -------------------------------------------------------------------------------------------
315
316    /// Captures the internal state into a new [TraceFragmentContext] (stored internally), resets
317    /// the internal replay state of the builder, and records a new state snapshot, marking the
318    /// beginning of the next trace state.
319    ///
320    /// This must be called at the beginning of a new trace fragment, before executing the first
321    /// operation. Internal replay fields are expected to be accessed during execution of this new
322    /// fragment to record data to be replayed by the trace fragment generators.
323    #[inline(always)]
324    fn start_new_fragment_context(
325        &mut self,
326        system_state: SystemState,
327        stack_top: [Felt; MIN_STACK_DEPTH],
328        mut continuation_stack: ContinuationStack<Arc<MastForest>>,
329        continuation: Continuation<Arc<MastForest>>,
330        current_forest: Arc<MastForest>,
331    ) {
332        // If there is an ongoing snapshot, finish it
333        self.finish_current_fragment_context();
334
335        // Start a new snapshot
336        let decoder_state = {
337            if self.block_stack.is_empty() {
338                DecoderState { current_addr: ZERO, parent_addr: ZERO }
339            } else {
340                let block_info = self.block_stack.peek();
341
342                DecoderState {
343                    current_addr: block_info.addr,
344                    parent_addr: block_info.parent_addr,
345                }
346            }
347        };
348        let stack = {
349            let stack_depth = MIN_STACK_DEPTH + self.overflow_table.num_elements_in_current_ctx();
350            let last_overflow_addr = self.overflow_table.last_update_clk_in_current_ctx();
351            StackState::new(stack_top, stack_depth, last_overflow_addr)
352        };
353
354        // Push new continuation corresponding to the current execution state
355        continuation_stack.push_continuation(continuation);
356
357        // Translate the live `Arc<MastForest>`-bearing continuation stack into one indexed by
358        // `MastForestId` into `mast_forest_builders`, registering any newly-encountered forests
359        // along the way.
360        let initial_mast_forest_id = self.forest_id(&current_forest);
361        let translated_stack = self.translate_continuation_stack(continuation_stack);
362
363        self.state_snapshot = Some(StateSnapshot {
364            state: CoreTraceState {
365                system: system_state,
366                decoder: decoder_state,
367                stack,
368            },
369            continuation_stack: translated_stack,
370            initial_mast_forest_id,
371        });
372    }
373
374    #[inline(always)]
375    fn record_control_node_start<P: Processor>(
376        &mut self,
377        node: &MastNode,
378        processor: &P,
379        current_forest: &MastForest,
380    ) {
381        let ctx_info = match node {
382            MastNode::Join(node) => {
383                let child1_hash = current_forest
384                    .get_node_by_id(node.first())
385                    .expect("join node's first child expected to be in the forest")
386                    .digest();
387                let child2_hash = current_forest
388                    .get_node_by_id(node.second())
389                    .expect("join node's second child expected to be in the forest")
390                    .digest();
391                self.hasher_for_chiplet.record_hash_control_block(
392                    child1_hash,
393                    child2_hash,
394                    JoinNode::DOMAIN,
395                    node.digest(),
396                );
397
398                None
399            },
400            MastNode::Split(node) => {
401                let child1_hash = current_forest
402                    .get_node_by_id(node.on_true())
403                    .expect("split node's true child expected to be in the forest")
404                    .digest();
405                let child2_hash = current_forest
406                    .get_node_by_id(node.on_false())
407                    .expect("split node's false child expected to be in the forest")
408                    .digest();
409                self.hasher_for_chiplet.record_hash_control_block(
410                    child1_hash,
411                    child2_hash,
412                    SplitNode::DOMAIN,
413                    node.digest(),
414                );
415
416                None
417            },
418            MastNode::Loop(node) => {
419                let body_hash = current_forest
420                    .get_node_by_id(node.body())
421                    .expect("loop node's body expected to be in the forest")
422                    .digest();
423
424                self.hasher_for_chiplet.record_hash_control_block(
425                    body_hash,
426                    EMPTY_WORD,
427                    LoopNode::DOMAIN,
428                    node.digest(),
429                );
430
431                None
432            },
433            MastNode::Call(node) => {
434                let callee_hash = current_forest
435                    .get_node_by_id(node.callee())
436                    .expect("call node's callee expected to be in the forest")
437                    .digest();
438
439                self.hasher_for_chiplet.record_hash_control_block(
440                    callee_hash,
441                    EMPTY_WORD,
442                    node.domain(),
443                    node.digest(),
444                );
445
446                let overflow_addr = self.overflow_table.last_update_clk_in_current_ctx();
447                Some(ExecutionContextInfo::new(
448                    processor.system().ctx(),
449                    processor.system().caller_hash(),
450                    processor.stack().depth(),
451                    overflow_addr,
452                ))
453            },
454            MastNode::Dyn(dyn_node) => {
455                self.hasher_for_chiplet.record_hash_control_block(
456                    EMPTY_WORD,
457                    EMPTY_WORD,
458                    dyn_node.domain(),
459                    dyn_node.digest(),
460                );
461
462                if dyn_node.is_dyncall() {
463                    // DYNCALL drops the top stack element (the memory address holding the
464                    // callee hash) and records the stack state *after* the drop as the new
465                    // context.
466                    //
467                    // `record_control_node_start()` is called *before* `decrement_stack_size()`,
468                    // so we must compute the post-drop overflow address without actually
469                    // performing the pop.  We use `clk_after_pop_in_current_ctx()` which
470                    // returns the clock of the second-to-last overflow entry (i.e. what
471                    // `last_update_clk_in_current_ctx()` would return after the pop), or ZERO
472                    // when the overflow stack has ≤1 entry and would become empty.
473                    //
474                    // When the stack is already at MIN_STACK_DEPTH the drop does not reduce
475                    // the depth and the overflow address is ZERO — mirroring the same guard
476                    // already present in the parallel-tracer path.  See #2813 / PR #2904.
477                    let (stack_depth_after_drop, overflow_addr) =
478                        if processor.stack().depth() > MIN_STACK_DEPTH as u32 {
479                            (
480                                processor.stack().depth() - 1,
481                                self.overflow_table.clk_after_pop_in_current_ctx(),
482                            )
483                        } else {
484                            (processor.stack().depth(), ZERO)
485                        };
486                    Some(ExecutionContextInfo::new(
487                        processor.system().ctx(),
488                        processor.system().caller_hash(),
489                        stack_depth_after_drop,
490                        overflow_addr,
491                    ))
492                } else {
493                    None
494                }
495            },
496            MastNode::Block(_) => panic!(
497                "`ExecutionTracer::record_basic_block_start()` must be called instead for basic blocks"
498            ),
499            MastNode::External(_) => panic!(
500                "External nodes are guaranteed to be resolved before record_control_node_start is called"
501            ),
502        };
503
504        let block_addr = self.hasher_chiplet_shim.record_hash_control_block();
505        let parent_addr = self.block_stack.push(block_addr, ctx_info);
506        self.block_stack_replay.record_node_start_parent_addr(parent_addr);
507    }
508
509    /// Records the block address for an END operation based on the block being popped.
510    #[inline(always)]
511    fn record_node_end(&mut self, block_info: &BlockInfo) {
512        let (prev_addr, prev_parent_addr) = if self.block_stack.is_empty() {
513            (ZERO, ZERO)
514        } else {
515            let prev_block = self.block_stack.peek();
516            (prev_block.addr, prev_block.parent_addr)
517        };
518        self.block_stack_replay
519            .record_node_end(block_info.addr, prev_addr, prev_parent_addr);
520    }
521
522    /// Records the execution context system info for CALL/SYSCALL/DYNCALL operations.
523    #[inline(always)]
524    fn record_execution_context(&mut self, ctx_info: ExecutionContextSystemInfo) {
525        self.execution_context_replay.record_execution_context(ctx_info);
526    }
527
528    /// Records the current core trace state, if any.
529    ///
530    /// Specifically, extracts the stored [SnapshotStart] as well as all the replay data recorded
531    /// from the various components (e.g. memory, advice, etc) since the last call to this method.
532    /// Resets the internal state to default values to prepare for the next trace fragment.
533    ///
534    /// Note that the very first time that this is called (at clock cycle 0), the snapshot will not
535    /// contain any replay data, and so no core trace state will be recorded.
536    #[inline(always)]
537    fn finish_current_fragment_context(&mut self) {
538        if let Some(snapshot) = self.state_snapshot.take() {
539            // Extract the replays
540            let (hasher_replay, block_addr_replay) = self.hasher_chiplet_shim.extract_replay();
541            let memory_reads_replay = core::mem::take(&mut self.memory_reads);
542            let advice_replay = core::mem::take(&mut self.advice);
543            let external_replay = core::mem::take(&mut self.external);
544            let stack_overflow_replay = core::mem::take(&mut self.overflow_replay);
545            let block_stack_replay = core::mem::take(&mut self.block_stack_replay);
546            let execution_context_replay = core::mem::take(&mut self.execution_context_replay);
547
548            let trace_state = CoreTraceFragmentContext {
549                state: snapshot.state,
550                replay: ExecutionReplay {
551                    hasher: hasher_replay,
552                    block_address: block_addr_replay,
553                    memory_reads: memory_reads_replay,
554                    advice: advice_replay,
555                    mast_forest_resolution: external_replay,
556                    stack_overflow: stack_overflow_replay,
557                    block_stack: block_stack_replay,
558                    execution_context: execution_context_replay,
559                },
560                continuation: snapshot.continuation_stack,
561                initial_mast_forest_id: snapshot.initial_mast_forest_id,
562            };
563
564            self.fragment_contexts.push(trace_state);
565        }
566    }
567
568    /// Pushes the value at stack position 15 onto the overflow table. This must be called in
569    /// `Tracer::start_clock_cycle()` *before* the processor increments the stack size, where stack
570    /// position 15 at the start of the clock cycle corresponds to the element that overflows.
571    #[inline(always)]
572    fn increment_stack_size(&mut self, processor: &FastProcessor) {
573        let new_overflow_value = processor.stack_get(15);
574        self.overflow_table.push(new_overflow_value, processor.system().clock());
575    }
576
577    /// Pops a value from the overflow table and records it for replay.
578    #[inline(always)]
579    fn decrement_stack_size(&mut self) {
580        if let Some(popped_value) = self.overflow_table.pop() {
581            let new_overflow_addr = self.overflow_table.last_update_clk_in_current_ctx();
582            self.overflow_replay.record_pop_overflow(popped_value, new_overflow_addr);
583        }
584    }
585}
586
587impl Tracer for ExecutionTracer {
588    type Processor = FastProcessor;
589    type Forest = Arc<MastForest>;
590
591    /// When sufficiently many clock cycles have elapsed, starts a new trace state. Also updates the
592    /// internal block stack.
593    #[inline(always)]
594    fn start_clock_cycle(
595        &mut self,
596        processor: &FastProcessor,
597        continuation: Continuation<Arc<MastForest>>,
598        continuation_stack: &ContinuationStack<Arc<MastForest>>,
599        current_forest: &Arc<MastForest>,
600    ) {
601        // check if we need to start a new trace state
602        if processor.system().clock().as_usize().is_multiple_of(self.fragment_size) {
603            self.start_new_fragment_context(
604                SystemState::from_processor(processor),
605                processor
606                    .stack_top()
607                    .try_into()
608                    .expect("stack_top expected to be MIN_STACK_DEPTH elements"),
609                continuation_stack.clone(),
610                continuation.clone(),
611                current_forest.clone(),
612            );
613        }
614
615        // Record that the node being executed at this cycle was visited inside `current_forest`.
616        // This builds up the per-source-forest sparse subset used at trace generation time. Note
617        // that an `ExecutionTracer`'s state is invalidated on error, so it is safe to record the
618        // visit here even if the operation later fails (per Tracer invariants).
619        if let Some(visited_node_id) = node_id_for_visit(&continuation) {
620            self.record_visit(current_forest, visited_node_id);
621        }
622
623        match continuation {
624            Continuation::ResumeBasicBlock { node_id, batch_index, op_idx_in_batch } => {
625                // Update overflow table based on whether the operation increments or decrements
626                // the stack size.
627                let basic_block = current_forest[node_id].unwrap_basic_block();
628                let op = &basic_block.op_batches()[batch_index].ops()[op_idx_in_batch];
629
630                if op.increments_stack_size() {
631                    self.increment_stack_size(processor);
632                } else if op.decrements_stack_size() {
633                    self.decrement_stack_size();
634                }
635
636                if matches!(op, Operation::EvalCircuit) {
637                    self.is_eval_circuit_op = true;
638                }
639            },
640            Continuation::StartNode(mast_node_id) => match &current_forest[mast_node_id] {
641                MastNode::Join(_) | MastNode::Loop(_) => {
642                    self.record_control_node_start(
643                        &current_forest[mast_node_id],
644                        processor,
645                        current_forest,
646                    );
647                },
648                MastNode::Split(_) => {
649                    self.record_control_node_start(
650                        &current_forest[mast_node_id],
651                        processor,
652                        current_forest,
653                    );
654                    self.decrement_stack_size();
655                },
656                MastNode::Call(_) => {
657                    self.record_control_node_start(
658                        &current_forest[mast_node_id],
659                        processor,
660                        current_forest,
661                    );
662                    self.overflow_table.start_context();
663                },
664                MastNode::Dyn(dyn_node) => {
665                    self.record_control_node_start(
666                        &current_forest[mast_node_id],
667                        processor,
668                        current_forest,
669                    );
670                    // DYN and DYNCALL both drop the memory address from the stack.
671                    self.decrement_stack_size();
672
673                    if dyn_node.is_dyncall() {
674                        // Note: the overflow pop (stack size decrement above) must happen before
675                        // starting the new context so that it operates on the old context's
676                        // overflow table, per the semantics of dyncall.
677                        self.overflow_table.start_context();
678                    }
679                },
680                MastNode::Block(basic_block_node) => {
681                    let forest_id = self.forest_id(current_forest);
682                    self.hasher_for_chiplet.record_hash_basic_block(
683                        forest_id,
684                        mast_node_id,
685                        basic_block_node,
686                    );
687                    let block_addr =
688                        self.hasher_chiplet_shim.record_hash_basic_block(basic_block_node);
689                    let parent_addr = self.block_stack.push(block_addr, None);
690                    self.block_stack_replay.record_node_start_parent_addr(parent_addr);
691                },
692                MastNode::External(_) => unreachable!(
693                    "start_clock_cycle is guaranteed not to be called on external nodes"
694                ),
695            },
696            Continuation::Respan { node_id: _, batch_index: _ } => {
697                self.block_stack.peek_mut().addr += CONTROLLER_ROWS_PER_PERM_FELT;
698            },
699            Continuation::FinishLoop(_) if processor.stack_get(0) == ONE => {
700                // This is a REPEAT operation, which drops the condition (top element) off the stack
701                self.decrement_stack_size();
702            },
703            Continuation::FinishJoin(_)
704            | Continuation::FinishSplit(_)
705            | Continuation::FinishCall(_)
706            | Continuation::FinishDyn(_)
707            | Continuation::FinishLoop(_) // not a REPEAT, which is handled separately above
708            | Continuation::FinishBasicBlock(_) => {
709                // The END of a loop drops the condition from the stack (the body always pushes it).
710                if matches!(
711                    &continuation,
712                    Continuation::FinishLoop(_)
713                ) {
714                    self.decrement_stack_size();
715                }
716
717                // This is an END operation; pop the block stack and record the node end
718                let block_info = self.block_stack.pop();
719                self.record_node_end(&block_info);
720
721                if let Some(ctx_info) = block_info.ctx_info {
722                    self.record_execution_context(ExecutionContextSystemInfo {
723                        parent_ctx: ctx_info.parent_ctx,
724                        parent_fn_hash: ctx_info.parent_fn_hash,
725                    });
726
727                    self.pending_restore_context = true;
728                }
729            },
730            Continuation::EnterForest { .. } => {
731                panic!("EnterForest continuations are guaranteed not to be passed here")
732            },
733        }
734    }
735
736    #[inline(always)]
737    fn record_mast_forest_resolution(&mut self, node_id: MastNodeId, forest: &Arc<MastForest>) {
738        let forest_id = self.forest_id(forest);
739        self.external.record_resolution(node_id, forest_id);
740    }
741
742    #[inline(always)]
743    fn record_external_node_entered(
744        &mut self,
745        external_node_id: MastNodeId,
746        forest: &Arc<MastForest>,
747    ) {
748        // External nodes don't go through `start_clock_cycle`, so we record their visit here so
749        // the per-source-forest sparse builder includes them. The replay path needs the external
750        // node present in the sparse forest because `execute_external_node` indexes the forest by
751        // id to fetch the procedure digest.
752        self.record_visit(forest, external_node_id);
753    }
754
755    #[inline(always)]
756    fn record_hasher_permute(
757        &mut self,
758        input_state: [Felt; STATE_WIDTH],
759        output_state: [Felt; STATE_WIDTH],
760    ) {
761        self.hasher_for_chiplet.record_permute_input(input_state);
762        self.hasher_chiplet_shim.record_permute_output(output_state);
763    }
764
765    #[inline(always)]
766    fn record_hasher_build_merkle_root(
767        &mut self,
768        node: Word,
769        path: Option<&MerklePath>,
770        index: Felt,
771        output_root: Word,
772    ) {
773        let path = path.expect("execution tracer expects a valid Merkle path");
774        self.hasher_chiplet_shim.record_build_merkle_root(path, output_root);
775        self.hasher_for_chiplet.record_build_merkle_root(node, path.clone(), index);
776    }
777
778    #[inline(always)]
779    fn record_hasher_update_merkle_root(
780        &mut self,
781        old_value: Word,
782        new_value: Word,
783        path: Option<&MerklePath>,
784        index: Felt,
785        old_root: Word,
786        new_root: Word,
787    ) {
788        let path = path.expect("execution tracer expects a valid Merkle path");
789        self.hasher_chiplet_shim.record_update_merkle_root(path, old_root, new_root);
790        self.hasher_for_chiplet.record_update_merkle_root(
791            old_value,
792            new_value,
793            path.clone(),
794            index,
795        );
796    }
797
798    #[inline(always)]
799    fn record_memory_read_element(
800        &mut self,
801        element: Felt,
802        addr: Felt,
803        ctx: ContextId,
804        clk: RowIndex,
805    ) {
806        self.memory_reads.record_read_element(element, addr, ctx, clk);
807    }
808
809    #[inline(always)]
810    fn record_memory_read_word(&mut self, word: Word, addr: Felt, ctx: ContextId, clk: RowIndex) {
811        self.memory_reads.record_read_word(word, addr, ctx, clk);
812    }
813
814    #[inline(always)]
815    fn record_memory_write_element(
816        &mut self,
817        element: Felt,
818        addr: Felt,
819        ctx: ContextId,
820        clk: RowIndex,
821    ) {
822        self.memory_writes.record_write_element(element, addr, ctx, clk);
823    }
824
825    #[inline(always)]
826    fn record_memory_write_word(&mut self, word: Word, addr: Felt, ctx: ContextId, clk: RowIndex) {
827        self.memory_writes.record_write_word(word, addr, ctx, clk);
828    }
829
830    #[inline(always)]
831    fn record_memory_read_element_pair(
832        &mut self,
833        element_0: Felt,
834        addr_0: Felt,
835        element_1: Felt,
836        addr_1: Felt,
837        ctx: ContextId,
838        clk: RowIndex,
839    ) {
840        self.memory_reads.record_read_element(element_0, addr_0, ctx, clk);
841        self.memory_reads.record_read_element(element_1, addr_1, ctx, clk);
842    }
843
844    #[inline(always)]
845    fn record_memory_read_dword(
846        &mut self,
847        words: [Word; 2],
848        addr: Felt,
849        ctx: ContextId,
850        clk: RowIndex,
851    ) {
852        self.memory_reads.record_read_word(words[0], addr, ctx, clk);
853        self.memory_reads.record_read_word(words[1], addr + PTR_OFFSET_WORD, ctx, clk);
854    }
855
856    #[inline(always)]
857    fn record_dyncall_memory(
858        &mut self,
859        callee_hash: Word,
860        read_addr: Felt,
861        read_ctx: ContextId,
862        fmp_ctx: ContextId,
863        clk: RowIndex,
864    ) {
865        self.memory_reads.record_read_word(callee_hash, read_addr, read_ctx, clk);
866        self.memory_writes.record_write_element(FMP_INIT_VALUE, FMP_ADDR, fmp_ctx, clk);
867    }
868
869    #[inline(always)]
870    fn record_crypto_stream(
871        &mut self,
872        plaintext: [Word; 2],
873        src_addr: Felt,
874        ciphertext: [Word; 2],
875        dst_addr: Felt,
876        ctx: ContextId,
877        clk: RowIndex,
878    ) {
879        self.memory_reads.record_read_word(plaintext[0], src_addr, ctx, clk);
880        self.memory_reads
881            .record_read_word(plaintext[1], src_addr + PTR_OFFSET_WORD, ctx, clk);
882        self.memory_writes.record_write_word(ciphertext[0], dst_addr, ctx, clk);
883        self.memory_writes
884            .record_write_word(ciphertext[1], dst_addr + PTR_OFFSET_WORD, ctx, clk);
885    }
886
887    #[inline(always)]
888    fn record_pipe(&mut self, words: [Word; 2], addr: Felt, ctx: ContextId, clk: RowIndex) {
889        self.advice.record_pop_stack_dword(words);
890        self.memory_writes.record_write_word(words[0], addr, ctx, clk);
891        self.memory_writes.record_write_word(words[1], addr + PTR_OFFSET_WORD, ctx, clk);
892    }
893
894    #[inline(always)]
895    fn record_advice_pop_stack(&mut self, value: Felt) {
896        self.advice.record_pop_stack(value);
897    }
898
899    #[inline(always)]
900    fn record_advice_pop_stack_word(&mut self, word: Word) {
901        self.advice.record_pop_stack_word(word);
902    }
903
904    #[inline(always)]
905    fn record_u32and(&mut self, a: Felt, b: Felt) {
906        self.bitwise.record_u32and(a, b);
907    }
908
909    #[inline(always)]
910    fn record_u32xor(&mut self, a: Felt, b: Felt) {
911        self.bitwise.record_u32xor(a, b);
912    }
913
914    #[inline(always)]
915    fn record_u32_range_checks(&mut self, u32_lo: Felt, u32_hi: Felt) {
916        let (t1, t0) = split_u32_into_u16(u32_lo.as_canonical_u64());
917        let (t3, t2) = split_u32_into_u16(u32_hi.as_canonical_u64());
918
919        self.range_checker.record_range_check_u32([t0, t1, t2, t3]);
920    }
921
922    #[inline(always)]
923    fn record_kernel_proc_access(&mut self, proc_hash: Word) {
924        self.kernel.record_kernel_proc_access(proc_hash);
925    }
926
927    #[inline(always)]
928    fn record_circuit_evaluation(&mut self, circuit_evaluation: CircuitEvaluation) {
929        self.ace.record_circuit_evaluation(circuit_evaluation);
930    }
931
932    #[inline(always)]
933    fn finalize_clock_cycle(
934        &mut self,
935        processor: &FastProcessor,
936        _op_helper_registers: OperationHelperRegisters,
937        _current_forest: &Arc<MastForest>,
938    ) -> Result<(), ExecutionError> {
939        // Restore the overflow table context for Call/Syscall/Dyncall END. This is deferred
940        // from start_clock_cycle because finalize_clock_cycle is only called when the operation
941        // succeeds (i.e., the stack depth check in processor.restore_context() passes).
942        if self.pending_restore_context {
943            // Restore context for call/syscall/dyncall: pop the current context's
944            // (empty) overflow stack and restore the previous context's overflow state.
945            self.overflow_table.restore_context().map_err(|_| {
946                ExecutionError::Internal(
947                    "overflow table restore_context failed during trace finalization",
948                )
949            })?;
950            self.overflow_replay.record_restore_context_overflow_addr(
951                MIN_STACK_DEPTH + self.overflow_table.num_elements_in_current_ctx(),
952                self.overflow_table.last_update_clk_in_current_ctx(),
953            );
954
955            self.pending_restore_context = false;
956        }
957
958        // Record all memory reads performed during EvalCircuit operations. We run this in
959        // `finalize_clock_cycle` to ensure that the memory reads are only recorded if the operation
960        // succeeds (and hence the values read from the stack can be assumed to be valid).
961        if self.is_eval_circuit_op {
962            let ptr = processor.stack_get(0);
963            let num_read = processor.stack_get(1).as_canonical_u64();
964            let num_eval = processor.stack_get(2).as_canonical_u64();
965            let ctx = processor.ctx();
966            let clk = processor.clock();
967
968            let num_read_rows = num_read / 2;
969
970            let mut addr = ptr;
971            for _ in 0..num_read_rows {
972                let word = processor
973                    .memory()
974                    .read_word(ctx, addr, clk)
975                    .expect("EvalCircuit memory read should not fail after successful execution");
976                self.memory_reads.record_read_word(word, addr, ctx, clk);
977                addr += PTR_OFFSET_WORD;
978            }
979            for _ in 0..num_eval {
980                let element = processor
981                    .memory()
982                    .read_element(ctx, addr)
983                    .expect("EvalCircuit memory read should not fail after successful execution");
984                self.memory_reads.record_read_element(element, addr, ctx, clk);
985                addr += PTR_OFFSET_ELEM;
986            }
987
988            self.is_eval_circuit_op = false;
989        }
990
991        Ok(())
992    }
993}
994
995/// Extracts the [`MastNodeId`] that is being executed at the start of a clock cycle from the given
996/// continuation, so that the node can be marked as visited in its source forest's sparse builder.
997///
998/// Continuations not passed to [`Tracer::start_clock_cycle`] (per the trait contract) are matched
999/// pessimistically here, returning `None`.
1000#[inline]
1001fn node_id_for_visit<F>(continuation: &Continuation<F>) -> Option<MastNodeId> {
1002    match *continuation {
1003        Continuation::StartNode(id)
1004        | Continuation::FinishJoin(id)
1005        | Continuation::FinishSplit(id)
1006        | Continuation::FinishCall(id)
1007        | Continuation::FinishDyn(id)
1008        | Continuation::FinishBasicBlock(id) => Some(id),
1009        Continuation::FinishLoop(id) => Some(id),
1010        Continuation::ResumeBasicBlock { node_id, .. } | Continuation::Respan { node_id, .. } => {
1011            Some(node_id)
1012        },
1013        Continuation::EnterForest { .. } => None,
1014    }
1015}
1016
1017// HASHER CHIPLET SHIM
1018// ================================================================================================
1019
1020/// The number of controller rows per permutation request (input + output = 2), as u32.
1021const NUM_HASHER_ROWS_PER_PERMUTATION: u32 = CONTROLLER_ROWS_PER_PERMUTATION as u32;
1022
1023/// Implements a shim for the hasher chiplet, where the responses of the hasher chiplet are emulated
1024/// and recorded for later replay.
1025///
1026/// This is used to simulate hasher operations in parallel trace generation without needing to
1027/// actually generate the hasher trace. All hasher operations are recorded during fast execution and
1028/// then replayed during core trace generation.
1029#[derive(Debug)]
1030pub struct HasherChipletShim {
1031    /// The address of the next MAST node encountered during execution. This field is used to keep
1032    /// track of the number of rows in the hasher chiplet, from which the address of the next MAST
1033    /// node is derived.
1034    addr: u32,
1035    /// Replay for the hasher chiplet responses, recording only the hasher chiplet responses.
1036    hasher_replay: HasherResponseReplay,
1037    block_addr_replay: BlockAddressReplay,
1038}
1039
1040impl HasherChipletShim {
1041    /// Creates a new [HasherChipletShim].
1042    pub fn new() -> Self {
1043        Self {
1044            addr: 1,
1045            hasher_replay: HasherResponseReplay::default(),
1046            block_addr_replay: BlockAddressReplay::default(),
1047        }
1048    }
1049
1050    /// Records the address returned from a call to `Hasher::hash_control_block()`.
1051    pub fn record_hash_control_block(&mut self) -> Felt {
1052        let block_addr = Felt::from_u32(self.addr);
1053
1054        self.block_addr_replay.record_block_address(block_addr);
1055        self.addr += NUM_HASHER_ROWS_PER_PERMUTATION;
1056
1057        block_addr
1058    }
1059
1060    /// Records the address returned from a call to `Hasher::hash_basic_block()`.
1061    pub fn record_hash_basic_block(&mut self, basic_block_node: &BasicBlockNode) -> Felt {
1062        let block_addr = Felt::from_u32(self.addr);
1063
1064        self.block_addr_replay.record_block_address(block_addr);
1065        self.addr += NUM_HASHER_ROWS_PER_PERMUTATION * basic_block_node.num_op_batches() as u32;
1066
1067        block_addr
1068    }
1069    /// Records the result of a call to `Hasher::permute()`.
1070    pub fn record_permute_output(&mut self, hashed_state: [Felt; 12]) {
1071        self.hasher_replay.record_permute(Felt::from_u32(self.addr), hashed_state);
1072        self.addr += NUM_HASHER_ROWS_PER_PERMUTATION;
1073    }
1074
1075    /// Records the result of a call to `Hasher::build_merkle_root()`.
1076    pub fn record_build_merkle_root(&mut self, path: &MerklePath, computed_root: Word) {
1077        self.hasher_replay
1078            .record_build_merkle_root(Felt::from_u32(self.addr), computed_root);
1079        self.addr += NUM_HASHER_ROWS_PER_PERMUTATION * path.depth() as u32;
1080    }
1081
1082    /// Records the result of a call to `Hasher::update_merkle_root()`.
1083    pub fn record_update_merkle_root(&mut self, path: &MerklePath, old_root: Word, new_root: Word) {
1084        self.hasher_replay
1085            .record_update_merkle_root(Felt::from_u32(self.addr), old_root, new_root);
1086
1087        // The Merkle path is verified twice: once for the old root and once for the new root.
1088        self.addr += 2 * NUM_HASHER_ROWS_PER_PERMUTATION * path.depth() as u32;
1089    }
1090
1091    pub fn extract_replay(&mut self) -> (HasherResponseReplay, BlockAddressReplay) {
1092        (
1093            core::mem::take(&mut self.hasher_replay),
1094            core::mem::take(&mut self.block_addr_replay),
1095        )
1096    }
1097}
1098
1099impl Default for HasherChipletShim {
1100    fn default() -> Self {
1101        Self::new()
1102    }
1103}