Skip to main content

miden_processor/fast/
mod.rs

1#[cfg(test)]
2use alloc::rc::Rc;
3use alloc::{boxed::Box, sync::Arc, vec, vec::Vec};
4#[cfg(test)]
5use core::cell::Cell;
6use core::{cmp::min, ops::ControlFlow};
7
8use miden_air::{Felt, trace::RowIndex};
9use miden_core::{
10    EMPTY_WORD, WORD_SIZE, Word, ZERO,
11    mast::{MastForest, MastNodeExt, MastNodeId},
12    operations::Decorator,
13    precompile::PrecompileTranscript,
14    program::{MIN_STACK_DEPTH, Program, StackInputs, StackOutputs},
15    utils::range,
16};
17
18use crate::{
19    AdviceInputs, AdviceProvider, BaseHost, ContextId, ExecutionError, ExecutionOptions,
20    ProcessorState,
21    advice::AdviceError,
22    continuation_stack::{Continuation, ContinuationStack},
23    errors::MapExecErrNoCtx,
24    tracer::{OperationHelperRegisters, Tracer},
25};
26
27mod basic_block;
28mod call_and_dyn;
29mod execution_api;
30mod external;
31mod memory;
32mod operation;
33mod step;
34
35pub use basic_block::SystemEventError;
36pub use memory::Memory;
37pub use step::{BreakReason, ResumeContext};
38
39#[cfg(test)]
40mod tests;
41
42// CONSTANTS
43// ================================================================================================
44
45/// The initial size of the stack buffer.
46///
47/// Note: This value is much larger than it needs to be for the majority of programs. However, some
48/// existing programs need it, so we're forced to push it up (though this should be double-checked).
49/// At this high a value, we're starting to see some performance degradation on benchmarks. For
50/// example, the blake3 benchmark went from 285 MHz to 250 MHz (~10% degradation). Perhaps a better
51/// solution would be to make this value much smaller (~1000), and then fallback to a `Vec` if the
52/// stack overflows.
53const INITIAL_STACK_BUFFER_SIZE: usize = 6850;
54
55/// The initial position of the top of the stack in the stack buffer.
56///
57/// We place this value close to 0 because if a program hits the limit, it's much more likely to hit
58/// the upper bound than the lower bound, since hitting the lower bound only occurs when you drop
59/// 0's that were generated automatically to keep the stack depth at 16. In practice, if this
60/// occurs, it is most likely a bug.
61const INITIAL_STACK_TOP_IDX: usize = 250;
62
63/// Default maximum operand stack depth preserving the previous fixed-buffer ceiling.
64const DEFAULT_MAX_STACK_DEPTH: usize =
65    INITIAL_STACK_BUFFER_SIZE - INITIAL_STACK_TOP_IDX - 1 + MIN_STACK_DEPTH;
66
67const _: [(); 1] =
68    [(); (ExecutionOptions::DEFAULT_MAX_STACK_DEPTH == DEFAULT_MAX_STACK_DEPTH) as usize];
69
70/// The stack buffer index where the logical operand stack starts after reset/recenter.
71const STACK_BUFFER_BASE_IDX: usize = INITIAL_STACK_TOP_IDX - MIN_STACK_DEPTH;
72
73// FAST PROCESSOR
74// ================================================================================================
75
76/// A fast processor which doesn't generate any trace.
77///
78/// This processor is designed to be as fast as possible. Hence, it only keeps track of the current
79/// state of the processor (i.e. the stack, current clock cycle, current memory context, and free
80/// memory pointer).
81///
82/// # Stack Management
83/// A few key points about how the stack was designed for maximum performance:
84///
85/// - The stack starts with a fixed buffer size defined by `INITIAL_STACK_BUFFER_SIZE`.
86///     - This was observed to increase performance by at least 2x compared to using a `Vec` with
87///       `push()` & `pop()`.
88///     - We track the stack top and bottom using indices `stack_top_idx` and `stack_bot_idx`,
89///       respectively.
90/// - Since we are using a fixed-size buffer, we need to ensure that stack buffer accesses are not
91///   out of bounds. Naively, we could check for this on every access. However, every operation
92///   alters the stack depth by a predetermined amount, allowing us to precisely determine the
93///   minimum number of operations required to reach a stack buffer boundary, whether at the top or
94///   bottom.
95///     - For example, if the stack top is 10 elements away from the top boundary, and the stack
96///       bottom is 15 elements away from the bottom boundary, then we can safely execute 10
97///       operations that modify the stack depth with no bounds check.
98/// - When switching contexts (e.g., during a call or syscall), all elements past the first 16 are
99///   stored in an `ExecutionContextInfo` struct, and the stack is truncated to 16 elements. This
100///   will be restored when returning from the call or syscall.
101///
102/// # Clock Cycle Management
103/// - The clock cycle (`clk`) is managed in the same way as in `Process`. That is, it is incremented
104///   by 1 for every row that `Process` adds to the main trace.
105///     - It is important to do so because the clock cycle is used to determine the context ID for
106///       new execution contexts when using `call` or `dyncall`.
107#[derive(Debug)]
108pub struct FastProcessor {
109    /// The stack is stored in reverse order, so that the last element is at the top of the stack.
110    stack: Box<[Felt]>,
111    /// The index of the top of the stack.
112    stack_top_idx: usize,
113    /// The index of the bottom of the stack.
114    stack_bot_idx: usize,
115
116    /// The current clock cycle.
117    clk: RowIndex,
118
119    /// The current context ID.
120    ctx: ContextId,
121
122    /// The hash of the function that called into the current context, or `[ZERO, ZERO, ZERO,
123    /// ZERO]` if we are in the first context (i.e. when `call_stack` is empty).
124    caller_hash: Word,
125
126    /// The advice provider to be used during execution.
127    advice: AdviceProvider,
128
129    /// A map from (context_id, word_address) to the word stored starting at that memory location.
130    memory: Memory,
131
132    /// The call stack is used when starting a new execution context (from a `call`, `syscall` or
133    /// `dyncall`) to keep track of the information needed to return to the previous context upon
134    /// return. It is a stack since calls can be nested.
135    call_stack: Vec<ExecutionContextInfo>,
136
137    /// Options for execution, including but not limited to whether debug or tracing is enabled,
138    /// the size of core trace fragments during execution, etc.
139    options: ExecutionOptions,
140
141    /// Transcript used to record commitments via `log_precompile` instruction (implemented via
142    /// Poseidon2 sponge).
143    pc_transcript: PrecompileTranscript,
144
145    /// Tracks decorator retrieval calls for testing.
146    #[cfg(test)]
147    pub decorator_retrieval_count: Rc<Cell<usize>>,
148}
149
150impl FastProcessor {
151    /// Packages the processor state after successful execution into a public result type.
152    #[inline(always)]
153    fn into_execution_output(self, stack: StackOutputs) -> ExecutionOutput {
154        ExecutionOutput {
155            stack,
156            advice: self.advice,
157            memory: self.memory,
158            final_precompile_transcript: self.pc_transcript,
159        }
160    }
161
162    /// Converts the terminal result of a full execution run into [`ExecutionOutput`].
163    #[inline(always)]
164    fn execution_result_from_flow(
165        flow: ControlFlow<BreakReason, StackOutputs>,
166        processor: Self,
167    ) -> Result<ExecutionOutput, ExecutionError> {
168        match flow {
169            ControlFlow::Continue(stack_outputs) => {
170                Ok(processor.into_execution_output(stack_outputs))
171            },
172            ControlFlow::Break(break_reason) => match break_reason {
173                BreakReason::Err(err) => Err(err),
174                BreakReason::Stopped(_) => {
175                    unreachable!("Execution never stops prematurely with NeverStopper")
176                },
177            },
178        }
179    }
180
181    /// Converts a testing-only execution result into stack outputs.
182    #[cfg(any(test, feature = "testing"))]
183    #[inline(always)]
184    fn stack_result_from_flow(
185        flow: ControlFlow<BreakReason, StackOutputs>,
186    ) -> Result<StackOutputs, ExecutionError> {
187        match flow {
188            ControlFlow::Continue(stack_outputs) => Ok(stack_outputs),
189            ControlFlow::Break(break_reason) => match break_reason {
190                BreakReason::Err(err) => Err(err),
191                BreakReason::Stopped(_) => {
192                    unreachable!("Execution never stops prematurely with NeverStopper")
193                },
194            },
195        }
196    }
197
198    // CONSTRUCTORS
199    // ----------------------------------------------------------------------------------------------
200
201    /// Creates a new `FastProcessor` instance with the given stack inputs.
202    ///
203    /// By default, advice inputs are empty and execution options use their defaults
204    /// (debugging and tracing disabled).
205    ///
206    /// # Example
207    /// ```ignore
208    /// use miden_processor::FastProcessor;
209    ///
210    /// let processor = FastProcessor::new(stack_inputs)
211    ///     .with_advice(advice_inputs)
212    ///     .expect("advice inputs should fit advice map limits")
213    ///     .with_debugging(true)
214    ///     .with_tracing(true);
215    /// ```
216    ///
217    /// When using non-default advice map limits, prefer [`Self::new_with_options`] so the advice
218    /// inputs are validated against the intended execution options.
219    pub fn new(stack_inputs: StackInputs) -> Self {
220        Self::new_with_options(stack_inputs, AdviceInputs::default(), ExecutionOptions::default())
221            .expect("empty advice inputs should fit default advice map limits")
222    }
223
224    /// Sets the advice inputs for the processor.
225    ///
226    /// Advice inputs are loaded into the live advice provider immediately and are validated against
227    /// the processor's current [`ExecutionOptions`]. If the advice map needs non-default limits,
228    /// construct the processor with [`Self::new_with_options`] or call [`Self::with_options`]
229    /// before calling this method.
230    pub fn with_advice(mut self, advice_inputs: AdviceInputs) -> Result<Self, AdviceError> {
231        self.advice = AdviceProvider::new(advice_inputs, &self.options)?;
232        Ok(self)
233    }
234
235    /// Sets the execution options for the processor.
236    ///
237    /// This will override any previously set debugging or tracing settings.
238    ///
239    /// Existing advice inputs are revalidated against the new options before they are applied. To
240    /// load advice inputs that require non-default advice map limits, call this before
241    /// [`Self::with_advice`] or use [`Self::new_with_options`].
242    pub fn with_options(mut self, options: ExecutionOptions) -> Result<Self, AdviceError> {
243        self.advice.set_options(&options)?;
244        self.options = options;
245        Ok(self)
246    }
247
248    /// Enables or disables debugging mode.
249    ///
250    /// When debugging is enabled, debug decorators will be executed during program execution.
251    pub fn with_debugging(mut self, enabled: bool) -> Self {
252        self.options = self.options.with_debugging(enabled);
253        self
254    }
255
256    /// Enables or disables tracing mode.
257    ///
258    /// When tracing is enabled, trace decorators will be executed during program execution.
259    pub fn with_tracing(mut self, enabled: bool) -> Self {
260        self.options = self.options.with_tracing(enabled);
261        self
262    }
263
264    /// Constructor for creating a `FastProcessor` with all options specified at once.
265    ///
266    /// For a more fluent API, consider using `FastProcessor::new()` with builder methods.
267    pub fn new_with_options(
268        stack_inputs: StackInputs,
269        advice_inputs: AdviceInputs,
270        options: ExecutionOptions,
271    ) -> Result<Self, AdviceError> {
272        let stack_top_idx = INITIAL_STACK_TOP_IDX;
273        let stack = {
274            // Note: we use `Vec::into_boxed_slice()` here, since `Box::new([T; N])` first allocates
275            // the array on the stack, and then moves it to the heap. This might cause a
276            // stack overflow on some systems.
277            let mut stack = vec![ZERO; INITIAL_STACK_BUFFER_SIZE].into_boxed_slice();
278
279            // Copy inputs in reverse order so first element ends up at top of stack
280            for (i, &input) in stack_inputs.iter().enumerate() {
281                stack[stack_top_idx - 1 - i] = input;
282            }
283            stack
284        };
285
286        Ok(Self {
287            advice: AdviceProvider::new(advice_inputs, &options)?,
288            stack,
289            stack_top_idx,
290            stack_bot_idx: stack_top_idx - MIN_STACK_DEPTH,
291            clk: 0_u32.into(),
292            ctx: 0_u32.into(),
293            caller_hash: EMPTY_WORD,
294            memory: Memory::new(),
295            call_stack: Vec::new(),
296            options,
297            pc_transcript: PrecompileTranscript::new(),
298            #[cfg(test)]
299            decorator_retrieval_count: Rc::new(Cell::new(0)),
300        })
301    }
302
303    /// Returns the resume context to be used with the first call to `step_sync()`.
304    pub fn get_initial_resume_context(
305        &mut self,
306        program: &Program,
307    ) -> Result<ResumeContext, ExecutionError> {
308        self.advice
309            .extend_map(program.mast_forest().advice_map())
310            .map_exec_err_no_ctx()?;
311
312        Ok(ResumeContext {
313            current_forest: program.mast_forest().clone(),
314            continuation_stack: ContinuationStack::new(program),
315            kernel: program.kernel().clone(),
316        })
317    }
318
319    // ACCESSORS
320    // -------------------------------------------------------------------------------------------
321
322    /// Returns whether the processor is executing in debug mode.
323    #[inline(always)]
324    pub fn in_debug_mode(&self) -> bool {
325        self.options.enable_debugging()
326    }
327
328    /// Returns true if decorators should be executed.
329    ///
330    /// This corresponds to either being in debug mode (for debug decorators) or having tracing
331    /// enabled (for trace decorators).
332    #[inline(always)]
333    fn should_execute_decorators(&self) -> bool {
334        self.in_debug_mode() || self.options.enable_tracing()
335    }
336
337    #[cfg(test)]
338    #[inline(always)]
339    fn record_decorator_retrieval(&self) {
340        self.decorator_retrieval_count.set(self.decorator_retrieval_count.get() + 1);
341    }
342
343    /// Returns the size of the stack.
344    #[inline(always)]
345    fn stack_size(&self) -> usize {
346        self.stack_top_idx - self.stack_bot_idx
347    }
348
349    /// Returns the stack, such that the top of the stack is at the last index of the returned
350    /// slice.
351    pub fn stack(&self) -> &[Felt] {
352        &self.stack[self.stack_bot_idx..self.stack_top_idx]
353    }
354
355    /// Returns the top 16 elements of the stack.
356    pub fn stack_top(&self) -> &[Felt] {
357        &self.stack[self.stack_top_idx - MIN_STACK_DEPTH..self.stack_top_idx]
358    }
359
360    /// Returns a mutable reference to the top 16 elements of the stack.
361    pub fn stack_top_mut(&mut self) -> &mut [Felt] {
362        &mut self.stack[self.stack_top_idx - MIN_STACK_DEPTH..self.stack_top_idx]
363    }
364
365    /// Returns the element on the stack at index `idx`.
366    ///
367    /// This method is only meant to be used to access the stack top by operation handlers, and
368    /// system event handlers.
369    ///
370    /// # Preconditions
371    /// - `idx` must be less than or equal to 15.
372    #[inline(always)]
373    pub fn stack_get(&self, idx: usize) -> Felt {
374        self.stack[self.stack_top_idx - idx - 1]
375    }
376
377    /// Same as [`Self::stack_get()`], but returns [`ZERO`] if `idx` falls below index 0 in the
378    /// stack buffer.
379    ///
380    /// Use this instead of `stack_get()` when `idx` may exceed 15.
381    #[inline(always)]
382    pub fn stack_get_safe(&self, idx: usize) -> Felt {
383        if idx < self.stack_top_idx {
384            self.stack[self.stack_top_idx - idx - 1]
385        } else {
386            ZERO
387        }
388    }
389
390    /// Mutable variant of `stack_get()`.
391    ///
392    /// This method is only meant to be used to access the stack top by operation handlers, and
393    /// system event handlers.
394    ///
395    /// # Preconditions
396    /// - `idx` must be less than or equal to 15.
397    #[inline(always)]
398    pub fn stack_get_mut(&mut self, idx: usize) -> &mut Felt {
399        &mut self.stack[self.stack_top_idx - idx - 1]
400    }
401
402    /// Returns the word on the stack starting at index `start_idx` in "stack order".
403    ///
404    /// For `start_idx=0` the top element of the stack will be at position 0 in the word.
405    ///
406    /// For example, if the stack looks like this:
407    ///
408    /// top                                                       bottom
409    /// v                                                           v
410    /// a | b | c | d | e | f | g | h | i | j | k | l | m | n | o | p
411    ///
412    /// Then
413    /// - `stack_get_word(0)` returns `[a, b, c, d]`,
414    /// - `stack_get_word(1)` returns `[b, c, d, e]`,
415    /// - etc.
416    ///
417    /// This method is only meant to be used to access the stack top by operation handlers, and
418    /// system event handlers.
419    ///
420    /// # Preconditions
421    /// - `start_idx` must be less than or equal to 12.
422    #[inline(always)]
423    pub fn stack_get_word(&self, start_idx: usize) -> Word {
424        // Ensure we have enough elements to form a complete word
425        debug_assert!(
426            start_idx + WORD_SIZE <= self.stack_depth() as usize,
427            "Not enough elements on stack to read word starting at index {start_idx}"
428        );
429
430        let word_start_idx = self.stack_top_idx - start_idx - WORD_SIZE;
431        let mut result: [Felt; WORD_SIZE] =
432            self.stack[range(word_start_idx, WORD_SIZE)].try_into().unwrap();
433        // Reverse so top of stack (idx 0) goes to word[0]
434        result.reverse();
435        result.into()
436    }
437
438    /// Same as [`Self::stack_get_word()`], but returns [`ZERO`] for any element that falls below
439    /// index 0 in the stack buffer.
440    ///
441    /// Use this instead of `stack_get_word()` when `start_idx + WORD_SIZE` may exceed
442    /// `stack_top_idx`.
443    #[inline(always)]
444    pub fn stack_get_word_safe(&self, start_idx: usize) -> Word {
445        let buf_end = self.stack_top_idx.saturating_sub(start_idx);
446        let buf_start = self.stack_top_idx.saturating_sub(start_idx.saturating_add(WORD_SIZE));
447        let num_elements_to_read_from_buf = buf_end - buf_start;
448
449        let mut result = [ZERO; WORD_SIZE];
450        if num_elements_to_read_from_buf == WORD_SIZE {
451            result.copy_from_slice(&self.stack[range(buf_start, WORD_SIZE)]);
452        } else if num_elements_to_read_from_buf > 0 {
453            let offset = WORD_SIZE - num_elements_to_read_from_buf;
454            result[offset..]
455                .copy_from_slice(&self.stack[range(buf_start, num_elements_to_read_from_buf)]);
456        }
457        result.reverse();
458
459        result.into()
460    }
461
462    /// Returns the number of elements on the stack in the current context.
463    #[inline(always)]
464    pub fn stack_depth(&self) -> u32 {
465        (self.stack_top_idx - self.stack_bot_idx) as u32
466    }
467
468    /// Returns a reference to the processor's memory.
469    pub fn memory(&self) -> &Memory {
470        &self.memory
471    }
472
473    /// Consumes the processor and returns the advice provider, memory, and precompile
474    /// transcript.
475    pub fn into_parts(self) -> (AdviceProvider, Memory, PrecompileTranscript) {
476        (self.advice, self.memory, self.pc_transcript)
477    }
478
479    /// Returns a reference to the execution options.
480    pub fn execution_options(&self) -> &ExecutionOptions {
481        &self.options
482    }
483
484    /// Returns a narrowed interface for reading and updating the processor state.
485    #[inline(always)]
486    pub fn state(&self) -> ProcessorState<'_> {
487        ProcessorState { processor: self }
488    }
489
490    // MUTATORS
491    // -------------------------------------------------------------------------------------------
492
493    /// Writes an element to the stack at the given index.
494    #[inline(always)]
495    pub fn stack_write(&mut self, idx: usize, element: Felt) {
496        self.stack[self.stack_top_idx - idx - 1] = element
497    }
498
499    /// Writes a word to the stack starting at the given index.
500    ///
501    /// `word[0]` goes to stack position start_idx (top), `word[1]` to start_idx+1, etc.
502    #[inline(always)]
503    pub fn stack_write_word(&mut self, start_idx: usize, word: &Word) {
504        debug_assert!(start_idx <= MIN_STACK_DEPTH - WORD_SIZE);
505
506        let word_start_idx = self.stack_top_idx - start_idx - 4;
507        let mut source: [Felt; WORD_SIZE] = (*word).into();
508        // Reverse so word[0] ends up at the top of stack (highest internal index)
509        source.reverse();
510        self.stack[range(word_start_idx, WORD_SIZE)].copy_from_slice(&source)
511    }
512
513    /// Swaps the elements at the given indices on the stack.
514    #[inline(always)]
515    pub fn stack_swap(&mut self, idx1: usize, idx2: usize) {
516        let a = self.stack_get(idx1);
517        let b = self.stack_get(idx2);
518        self.stack_write(idx1, b);
519        self.stack_write(idx2, a);
520    }
521
522    // DECORATOR EXECUTORS
523    // --------------------------------------------------------------------------------------------
524
525    /// Executes the decorators that should be executed before entering a node.
526    fn execute_before_enter_decorators(
527        &self,
528        node_id: MastNodeId,
529        current_forest: &MastForest,
530        host: &mut impl BaseHost,
531    ) -> ControlFlow<BreakReason> {
532        if !self.should_execute_decorators() {
533            return ControlFlow::Continue(());
534        }
535
536        #[cfg(test)]
537        self.record_decorator_retrieval();
538
539        let node = current_forest
540            .get_node_by_id(node_id)
541            .expect("internal error: node id {node_id} not found in current forest");
542
543        for &decorator_id in node.before_enter(current_forest) {
544            self.execute_decorator(&current_forest[decorator_id], host)?;
545        }
546
547        ControlFlow::Continue(())
548    }
549
550    /// Executes the decorators that should be executed after exiting a node.
551    fn execute_after_exit_decorators(
552        &self,
553        node_id: MastNodeId,
554        current_forest: &MastForest,
555        host: &mut impl BaseHost,
556    ) -> ControlFlow<BreakReason> {
557        if !self.should_execute_decorators() {
558            return ControlFlow::Continue(());
559        }
560
561        #[cfg(test)]
562        self.record_decorator_retrieval();
563
564        let node = current_forest
565            .get_node_by_id(node_id)
566            .expect("internal error: node id {node_id} not found in current forest");
567
568        for &decorator_id in node.after_exit(current_forest) {
569            self.execute_decorator(&current_forest[decorator_id], host)?;
570        }
571
572        ControlFlow::Continue(())
573    }
574
575    /// Executes the specified decorator
576    fn execute_decorator(
577        &self,
578        decorator: &Decorator,
579        host: &mut impl BaseHost,
580    ) -> ControlFlow<BreakReason> {
581        match decorator {
582            Decorator::Debug(options) => {
583                if self.in_debug_mode() {
584                    let processor_state = self.state();
585                    if let Err(err) = host.on_debug(&processor_state, options) {
586                        return ControlFlow::Break(BreakReason::Err(
587                            crate::errors::HostError::DebugHandlerError { err }.into(),
588                        ));
589                    }
590                }
591            },
592            Decorator::Trace(id) => {
593                if self.options.enable_tracing() {
594                    let processor_state = self.state();
595                    if let Err(err) = host.on_trace(&processor_state, *id) {
596                        return ControlFlow::Break(BreakReason::Err(
597                            crate::errors::HostError::TraceHandlerError { trace_id: *id, err }
598                                .into(),
599                        ));
600                    }
601                }
602            },
603        };
604        ControlFlow::Continue(())
605    }
606
607    /// Increments the stack top pointer by 1.
608    ///
609    /// The bottom of the stack is never affected by this operation.
610    #[inline(always)]
611    fn increment_stack_size(&mut self) {
612        self.stack_top_idx += 1;
613    }
614
615    /// Ensures the internal stack storage can accommodate one additional logical stack element.
616    ///
617    /// The operand stack depth limit is the semantic resource bound; the buffer is only an
618    /// implementation detail. We therefore check the logical depth before allocating so a program
619    /// cannot force memory growth beyond `ExecutionOptions::max_stack_depth()`. When storage does
620    /// need to grow, it grows geometrically and remains heap-allocated as a boxed slice. A
621    /// `SmallVec` would put a useful inline buffer inside `FastProcessor`, and preallocating the
622    /// full limit would penalize ordinary programs. This policy is performance-sensitive and should
623    /// be benchmarked against the fixed-buffer baseline.
624    #[inline(always)]
625    fn ensure_stack_capacity_for_push(&mut self) -> Result<(), ExecutionError> {
626        let depth = self.stack_size() + 1;
627        let max = self.options.max_stack_depth();
628        if depth > max {
629            return Err(ExecutionError::StackDepthLimitExceeded { depth, max });
630        }
631
632        if self.stack_top_idx >= self.stack.len() - 1 {
633            self.grow_stack_buffer(self.stack_top_idx + 2);
634        }
635
636        Ok(())
637    }
638
639    fn ensure_stack_capacity_for_top_idx(&mut self, top_idx: usize) {
640        if top_idx >= self.stack.len() {
641            self.grow_stack_buffer(top_idx + 1);
642        }
643    }
644
645    fn grow_stack_buffer(&mut self, requested_min_len: usize) {
646        // The maximum allocation is tied to the logical operand stack depth, not to the current
647        // buffer position. Using `stack_bot_idx` here would make the allocation ceiling drift when
648        // the live stack has moved away from the initial base.
649        let max_len = STACK_BUFFER_BASE_IDX
650            .saturating_add(self.options.max_stack_depth())
651            .saturating_add(1);
652        let live_len = self.stack_size();
653
654        // Growth also recenters the live stack at the normal base. This keeps future push/drop
655        // behavior close to the fixed-buffer layout and avoids carrying unused prefix cells into
656        // the new allocation. The extra slot is for the next checked push that triggered growth.
657        let recentered_min_len = STACK_BUFFER_BASE_IDX.saturating_add(live_len).saturating_add(2);
658        debug_assert!(recentered_min_len <= max_len);
659
660        // Allocation growth is based on the stack's post-recentered live range, not the previous
661        // buffer length. The `requested_min_len` may be beyond the allocation cap when a shallow
662        // context is still positioned near the end of the old buffer; recentering the live stack is
663        // what makes that valid. The VM-visible requirements are that the live stack is restored at
664        // `STACK_BUFFER_BASE_IDX`, the post-recentered push slot is available, and allocation stays
665        // capped by the configured stack depth. The allocation size can differ from the previous
666        // doubling policy: normal push growth may allocate a couple of extra cells because of the
667        // spare push slot, while restoring a deep caller from a shallow callee may allocate only
668        // the requested restored range instead of doubling the old buffer. That smaller
669        // restore allocation is intentional, but it means future pushes can grow again
670        // sooner and should stay covered by benchmarks.
671        let new_len = recentered_min_len.saturating_mul(2).max(requested_min_len).min(max_len);
672        debug_assert!(new_len <= max_len);
673
674        let mut new_stack = vec![ZERO; new_len].into_boxed_slice();
675        let new_stack_bot_idx = STACK_BUFFER_BASE_IDX;
676        let new_stack_top_idx = new_stack_bot_idx + live_len;
677
678        // Only the active stack range carries VM state. Prefix/suffix cells are scratch storage and
679        // stay zeroed, which keeps growth proportional to the live depth instead of the old buffer
680        // length.
681        new_stack[new_stack_bot_idx..new_stack_top_idx]
682            .copy_from_slice(&self.stack[self.stack_bot_idx..self.stack_top_idx]);
683
684        self.stack = new_stack;
685        self.stack_bot_idx = new_stack_bot_idx;
686        self.stack_top_idx = new_stack_top_idx;
687    }
688
689    /// Decrements the stack top pointer by 1.
690    ///
691    /// The bottom of the stack is only decremented in cases where the stack depth would become less
692    /// than 16.
693    #[inline(always)]
694    fn decrement_stack_size(&mut self) {
695        if self.stack_top_idx == MIN_STACK_DEPTH {
696            // We no longer have any room in the stack buffer to decrement the stack size (which
697            // would cause the `stack_bot_idx` to go below 0). We therefore reset the stack to its
698            // original position.
699            self.reset_stack_in_buffer(INITIAL_STACK_TOP_IDX);
700        }
701
702        self.stack_top_idx -= 1;
703        self.stack_bot_idx = min(self.stack_bot_idx, self.stack_top_idx - MIN_STACK_DEPTH);
704    }
705
706    /// Resets the stack in the buffer to a new position, preserving the top 16 elements of the
707    /// stack.
708    ///
709    /// # Preconditions
710    /// - The stack is expected to have exactly 16 elements.
711    #[inline(always)]
712    fn reset_stack_in_buffer(&mut self, new_stack_top_idx: usize) {
713        debug_assert_eq!(self.stack_depth(), MIN_STACK_DEPTH as u32);
714
715        let new_stack_bot_idx = new_stack_top_idx - MIN_STACK_DEPTH;
716
717        // Copy stack to its new position
718        self.stack
719            .copy_within(self.stack_bot_idx..self.stack_top_idx, new_stack_bot_idx);
720
721        // Zero out stack below the new new_stack_bot_idx, since this is where overflow values
722        // come from, and are guaranteed to be ZERO. We don't need to zero out above
723        // `stack_top_idx`, since values there are never read before being written.
724        self.stack[0..new_stack_bot_idx].fill(ZERO);
725
726        // Update indices.
727        self.stack_bot_idx = new_stack_bot_idx;
728        self.stack_top_idx = new_stack_top_idx;
729    }
730}
731
732// EXECUTION OUTPUT
733// ===============================================================================================
734
735/// The output of a program execution, containing the state of the stack, advice provider,
736/// memory, and final precompile transcript at the end of execution.
737#[derive(Debug)]
738pub struct ExecutionOutput {
739    pub stack: StackOutputs,
740    pub advice: AdviceProvider,
741    pub memory: Memory,
742    pub final_precompile_transcript: PrecompileTranscript,
743}
744
745// EXECUTION CONTEXT INFO
746// ===============================================================================================
747
748/// Information about the execution context.
749///
750/// This struct is used to keep track of the information needed to return to the previous context
751/// upon return from a `call`, `syscall` or `dyncall`.
752#[derive(Debug)]
753struct ExecutionContextInfo {
754    /// This stores all the elements on the stack at the call site, excluding the top 16 elements.
755    /// This corresponds to the overflow table in [crate::Process].
756    overflow_stack: Vec<Felt>,
757    ctx: ContextId,
758    fn_hash: Word,
759}
760
761// NOOP TRACER
762// ================================================================================================
763
764/// A [Tracer] that does nothing.
765pub struct NoopTracer;
766
767impl Tracer for NoopTracer {
768    type Processor = FastProcessor;
769
770    #[inline(always)]
771    fn start_clock_cycle(
772        &mut self,
773        _processor: &FastProcessor,
774        _continuation: Continuation,
775        _continuation_stack: &ContinuationStack,
776        _current_forest: &Arc<MastForest>,
777    ) {
778        // do nothing
779    }
780
781    #[inline(always)]
782    fn finalize_clock_cycle(
783        &mut self,
784        _processor: &FastProcessor,
785        _op_helper_registers: OperationHelperRegisters,
786        _current_forest: &Arc<MastForest>,
787    ) {
788        // do nothing
789    }
790}