Skip to main content

miden_processor/fast/
mod.rs

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