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