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