Skip to main content

miden_processor/
execution_options.rs

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