Skip to main content

miden_processor/fast/
mod.rs

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