Skip to main content

miden_processor/
execution_options.rs

1use miden_air::trace::MIN_TRACE_LEN;
2use miden_core::program::MIN_STACK_DEPTH;
3
4// EXECUTION OPTIONS
5// ================================================================================================
6
7/// A set of parameters specifying execution parameters of the VM.
8///
9/// - `max_cycles` specifies the maximum number of cycles a program is allowed to execute.
10/// - `expected_cycles` specifies the number of cycles a program is expected to execute.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct ExecutionOptions {
13    max_cycles: u32,
14    expected_cycles: u32,
15    core_trace_fragment_size: usize,
16    /// Maximum combined logical size, in bytes, of the advice stack, map, and Merkle store.
17    max_advice_size_bytes: usize,
18    /// Whether the synchronous prover may overlap hasher-chiplet trace building with program
19    /// execution (std-only; the sequential path is used on no_std regardless).
20    overlapped_trace_build: bool,
21    /// Maximum number of input bytes allowed for a single hash precompile invocation.
22    max_hash_len_bytes: usize,
23    /// Maximum number of continuations allowed on the continuation stack at any point during
24    /// execution.
25    max_num_continuations: usize,
26    /// Maximum number of field elements allowed on the operand stack across the active execution
27    /// context and all suspended contexts.
28    ///
29    /// A `call`, `dyncall`, or `syscall` context switch hides the caller's operand-stack overflow
30    /// (everything below the top 16 elements) until the callee returns. This limit bounds the
31    /// aggregate of the active context's depth plus all such suspended overflow, so nesting
32    /// context switches cannot accumulate hidden operand-stack memory beyond the configured
33    /// budget.
34    max_stack_depth: usize,
35    /// Maximum number of field elements allowed in the processor's memory at any point during
36    /// execution, rounded up to the nearest multiple of 4.
37    max_memory_elements: usize,
38}
39
40impl Default for ExecutionOptions {
41    fn default() -> Self {
42        ExecutionOptions {
43            max_cycles: Self::MAX_CYCLES,
44            expected_cycles: MIN_TRACE_LEN as u32,
45            core_trace_fragment_size: Self::DEFAULT_CORE_TRACE_FRAGMENT_SIZE,
46            max_advice_size_bytes: Self::DEFAULT_MAX_ADVICE_SIZE_BYTES,
47            max_hash_len_bytes: Self::DEFAULT_MAX_HASH_LEN_BYTES,
48            max_num_continuations: Self::DEFAULT_MAX_NUM_CONTINUATIONS,
49            max_stack_depth: Self::DEFAULT_MAX_STACK_DEPTH,
50            max_memory_elements: Self::DEFAULT_MAX_MEMORY_ELEMENTS,
51            overlapped_trace_build: true,
52        }
53    }
54}
55
56impl ExecutionOptions {
57    // CONSTANTS
58    // --------------------------------------------------------------------------------------------
59
60    /// The maximum number of VM cycles a program is allowed to take.
61    pub const MAX_CYCLES: u32 = 1 << 29;
62
63    /// Default fragment size for core trace generation.
64    pub const DEFAULT_CORE_TRACE_FRAGMENT_SIZE: usize = 4096; // 2^12
65
66    /// Default maximum combined logical size of the advice provider. Set to 16 MiB.
67    pub const DEFAULT_MAX_ADVICE_SIZE_BYTES: usize = 16 * 1024 * 1024;
68
69    /// Default maximum number of input bytes for a single hash precompile invocation.
70    /// Set to 2^20 (1 MB).
71    pub const DEFAULT_MAX_HASH_LEN_BYTES: usize = 1 << 20;
72
73    /// Default maximum number of continuations allowed on the continuation stack.
74    /// Set to 2^16 (65536).
75    pub const DEFAULT_MAX_NUM_CONTINUATIONS: usize = 1 << 16;
76
77    /// Default maximum number of field elements allowed on the operand stack.
78    ///
79    /// This preserves the effective stack depth ceiling imposed by the previous fixed
80    /// `FastProcessor` stack buffer.
81    pub const DEFAULT_MAX_STACK_DEPTH: usize = 6615;
82
83    /// Default maximum number of field elements allowed in the processor's memory.
84    ///
85    /// Memory is element-addressable, so this bounds the total number of elements live across all
86    /// contexts. Internally memory is stored at word granularity (4 elements per word), so the
87    /// effective limit is rounded up to a whole number of words. Set to 2^28, which lets programs
88    /// use a large amount of memory while still providing a finite host-memory backstop against
89    /// unbounded growth from writes to arbitrarily many unique addresses.
90    pub const DEFAULT_MAX_MEMORY_ELEMENTS: usize = 1 << 28;
91
92    // CONSTRUCTOR
93    // --------------------------------------------------------------------------------------------
94
95    /// Creates a new instance of [ExecutionOptions] from the specified parameters.
96    ///
97    /// If the `max_cycles` is `None` the maximum number of cycles will be set to 2^29.
98    ///
99    /// # Errors
100    /// Returns an error if:
101    /// - `max_cycles` is outside the valid range
102    /// - after rounding up to the next power of two, `expected_cycles` exceeds `max_cycles`
103    /// - `core_trace_fragment_size` is zero
104    pub fn new(
105        max_cycles: Option<u32>,
106        expected_cycles: u32,
107        core_trace_fragment_size: usize,
108    ) -> Result<Self, ExecutionOptionsError> {
109        // Validate max cycles.
110        let max_cycles = if let Some(max_cycles) = max_cycles {
111            if max_cycles > Self::MAX_CYCLES {
112                return Err(ExecutionOptionsError::MaxCycleNumTooBig {
113                    max_cycles,
114                    max_cycles_limit: Self::MAX_CYCLES,
115                });
116            }
117            if max_cycles < MIN_TRACE_LEN as u32 {
118                return Err(ExecutionOptionsError::MaxCycleNumTooSmall {
119                    max_cycles,
120                    min_cycles_limit: MIN_TRACE_LEN,
121                });
122            }
123            max_cycles
124        } else {
125            Self::MAX_CYCLES
126        };
127        // Round up the expected number of cycles to the next power of two. If it is smaller than
128        // MIN_TRACE_LEN -- pad expected number to it.
129        let expected_cycles = expected_cycles.next_power_of_two().max(MIN_TRACE_LEN as u32);
130        // Validate expected cycles (after rounding) against max_cycles.
131        if max_cycles < expected_cycles {
132            return Err(ExecutionOptionsError::ExpectedCyclesTooBig {
133                max_cycles,
134                expected_cycles,
135            });
136        }
137
138        // Validate core trace fragment size.
139        if core_trace_fragment_size == 0 {
140            return Err(ExecutionOptionsError::CoreTraceFragmentSizeTooSmall);
141        }
142
143        Ok(ExecutionOptions {
144            max_cycles,
145            expected_cycles,
146            core_trace_fragment_size,
147            max_advice_size_bytes: Self::DEFAULT_MAX_ADVICE_SIZE_BYTES,
148            max_hash_len_bytes: Self::DEFAULT_MAX_HASH_LEN_BYTES,
149            max_num_continuations: Self::DEFAULT_MAX_NUM_CONTINUATIONS,
150            max_stack_depth: Self::DEFAULT_MAX_STACK_DEPTH,
151            max_memory_elements: Self::DEFAULT_MAX_MEMORY_ELEMENTS,
152            overlapped_trace_build: true,
153        })
154    }
155
156    /// Sets the fragment size for core trace generation.
157    ///
158    /// Returns an error if the size is zero.
159    pub fn with_core_trace_fragment_size(
160        mut self,
161        size: usize,
162    ) -> Result<Self, ExecutionOptionsError> {
163        if size == 0 {
164            return Err(ExecutionOptionsError::CoreTraceFragmentSizeTooSmall);
165        }
166        self.core_trace_fragment_size = size;
167        Ok(self)
168    }
169
170    // PUBLIC ACCESSORS
171    // --------------------------------------------------------------------------------------------
172
173    /// Returns maximum number of cycles a program is allowed to execute for.
174    #[inline(always)]
175    pub fn max_cycles(&self) -> u32 {
176        self.max_cycles
177    }
178
179    /// Returns the number of cycles a program is expected to take.
180    ///
181    /// This will serve as a hint to the VM for how much memory to allocate for a program's
182    /// execution trace and may result in performance improvements when the number of expected
183    /// cycles is equal to the number of actual cycles.
184    pub fn expected_cycles(&self) -> u32 {
185        self.expected_cycles
186    }
187
188    /// Returns the fragment size for core trace generation.
189    pub fn core_trace_fragment_size(&self) -> usize {
190        self.core_trace_fragment_size
191    }
192
193    /// Returns the maximum combined logical size, in bytes, of the advice provider.
194    #[inline]
195    pub fn max_advice_size_bytes(&self) -> usize {
196        self.max_advice_size_bytes
197    }
198
199    /// Returns the maximum number of input bytes allowed for a single hash precompile invocation.
200    #[inline]
201    pub fn max_hash_len_bytes(&self) -> usize {
202        self.max_hash_len_bytes
203    }
204
205    /// Sets whether the synchronous prover may overlap hasher-chiplet trace building with program
206    /// execution (defaults to `true`; ignored on no_std, which always uses the sequential path).
207    /// When enabled, overlap is opportunistic. A caller with no separate Rayon worker uses compact
208    /// buffered replay.
209    pub fn with_overlapped_trace_build(mut self, overlapped: bool) -> Self {
210        self.overlapped_trace_build = overlapped;
211        self
212    }
213
214    /// Returns whether the synchronous prover may overlap trace building with execution.
215    pub fn overlapped_trace_build(&self) -> bool {
216        self.overlapped_trace_build
217    }
218
219    /// Sets the maximum combined logical size, in bytes, of the advice provider.
220    pub fn with_max_advice_size_bytes(mut self, size: usize) -> Self {
221        self.max_advice_size_bytes = size;
222        self
223    }
224
225    /// Sets the maximum number of input bytes allowed for a single hash precompile invocation.
226    pub fn with_max_hash_len_bytes(mut self, size: usize) -> Self {
227        self.max_hash_len_bytes = size;
228        self
229    }
230
231    /// Returns the maximum number of continuations allowed on the continuation stack.
232    #[inline]
233    pub fn max_num_continuations(&self) -> usize {
234        self.max_num_continuations
235    }
236
237    /// Returns the maximum number of field elements allowed on the operand stack across the active
238    /// execution context and all suspended contexts.
239    #[inline]
240    pub fn max_stack_depth(&self) -> usize {
241        self.max_stack_depth
242    }
243
244    /// Returns the configured maximum number of field elements allowed in the processor's memory.
245    ///
246    /// This is the raw value as set via [`Self::with_max_memory_elements`]; the effective cap is
247    /// rounded up to a whole number of words (a multiple of 4) when memory is initialized.
248    #[inline]
249    pub fn max_memory_elements(&self) -> usize {
250        self.max_memory_elements
251    }
252
253    /// Sets the maximum number of continuations allowed on the continuation stack.
254    pub fn with_max_num_continuations(mut self, max_num_continuations: usize) -> Self {
255        self.max_num_continuations = max_num_continuations;
256        self
257    }
258
259    /// Sets the maximum number of field elements allowed on the operand stack across the active
260    /// execution context and all suspended contexts.
261    pub fn with_max_stack_depth(
262        mut self,
263        max_stack_depth: usize,
264    ) -> Result<Self, ExecutionOptionsError> {
265        if max_stack_depth < MIN_STACK_DEPTH {
266            return Err(ExecutionOptionsError::MaxStackDepthTooSmall {
267                max_stack_depth,
268                min_stack_depth: MIN_STACK_DEPTH,
269            });
270        }
271        self.max_stack_depth = max_stack_depth;
272        Ok(self)
273    }
274
275    /// Sets the maximum number of field elements allowed in the processor's memory.
276    pub fn with_max_memory_elements(mut self, max_memory_elements: usize) -> Self {
277        self.max_memory_elements = max_memory_elements;
278        self
279    }
280}
281
282// EXECUTION OPTIONS ERROR
283// ================================================================================================
284
285#[derive(Debug, thiserror::Error)]
286pub enum ExecutionOptionsError {
287    #[error(
288        "expected number of cycles {expected_cycles} must be smaller than the maximum number of cycles {max_cycles}"
289    )]
290    ExpectedCyclesTooBig { max_cycles: u32, expected_cycles: u32 },
291    #[error("maximum number of cycles {max_cycles} must be greater than {min_cycles_limit}")]
292    MaxCycleNumTooSmall { max_cycles: u32, min_cycles_limit: usize },
293    #[error("maximum number of cycles {max_cycles} must be less than {max_cycles_limit}")]
294    MaxCycleNumTooBig { max_cycles: u32, max_cycles_limit: u32 },
295    #[error("core trace fragment size must be greater than 0")]
296    CoreTraceFragmentSizeTooSmall,
297    #[error("maximum stack depth {max_stack_depth} must be at least {min_stack_depth}")]
298    MaxStackDepthTooSmall {
299        max_stack_depth: usize,
300        min_stack_depth: usize,
301    },
302}
303
304// TESTS
305// ================================================================================================
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310
311    #[test]
312    fn valid_fragment_size() {
313        // Valid power of two values should succeed
314        let opts = ExecutionOptions::new(None, 64, 1024);
315        assert!(opts.is_ok());
316        assert_eq!(opts.unwrap().core_trace_fragment_size(), 1024);
317
318        let opts = ExecutionOptions::new(None, 64, 4096);
319        assert!(opts.is_ok());
320
321        let opts = ExecutionOptions::new(None, 64, 1);
322        assert!(opts.is_ok());
323    }
324
325    #[test]
326    fn zero_fragment_size_fails() {
327        let opts = ExecutionOptions::new(None, 64, 0);
328        assert!(matches!(opts, Err(ExecutionOptionsError::CoreTraceFragmentSizeTooSmall)));
329    }
330
331    #[test]
332    fn with_core_trace_fragment_size_validates() {
333        // Valid size should succeed
334        let result = ExecutionOptions::default().with_core_trace_fragment_size(2048);
335        assert!(result.is_ok());
336        assert_eq!(result.unwrap().core_trace_fragment_size(), 2048);
337
338        // Zero should fail
339        let result = ExecutionOptions::default().with_core_trace_fragment_size(0);
340        assert!(matches!(result, Err(ExecutionOptionsError::CoreTraceFragmentSizeTooSmall)));
341    }
342
343    #[test]
344    fn expected_cycles_validated_after_rounding() {
345        // expected_cycles=65 rounds to 128; max_cycles=100 -> must fail (128 > 100).
346        let opts = ExecutionOptions::new(Some(100), 65, 1024);
347        assert!(matches!(
348            opts,
349            Err(ExecutionOptionsError::ExpectedCyclesTooBig {
350                max_cycles: 100,
351                expected_cycles: 128
352            })
353        ));
354
355        // expected_cycles=64 rounds to 64; max_cycles=100 -> ok.
356        let opts = ExecutionOptions::new(Some(100), 64, 1024);
357        assert!(opts.is_ok());
358        assert_eq!(opts.unwrap().expected_cycles(), 64);
359    }
360
361    #[test]
362    fn max_stack_depth_validates_minimum_depth() {
363        let result = ExecutionOptions::default().with_max_stack_depth(MIN_STACK_DEPTH - 1);
364        assert!(matches!(
365            result,
366            Err(ExecutionOptionsError::MaxStackDepthTooSmall {
367                max_stack_depth,
368                min_stack_depth: MIN_STACK_DEPTH,
369            }) if max_stack_depth == MIN_STACK_DEPTH - 1
370        ));
371
372        let result = ExecutionOptions::default().with_max_stack_depth(MIN_STACK_DEPTH);
373        assert!(result.is_ok());
374        assert_eq!(result.unwrap().max_stack_depth(), MIN_STACK_DEPTH);
375    }
376}