Skip to main content

miden_debug_engine/exec/
executor.rs

1use std::{
2    cell::{Cell, RefCell},
3    collections::{BTreeMap, VecDeque},
4    fmt,
5    ops::Deref,
6    rc::Rc,
7    sync::Arc,
8};
9
10use log::Level;
11use miden_assembly_syntax::{Library, diagnostics::Report};
12use miden_core::{
13    Word,
14    operations::DebugVarInfo,
15    program::{Program, StackInputs},
16};
17use miden_debug_types::{SourceManager, SourceManagerExt};
18use miden_mast_package::Dependency;
19use miden_processor::{
20    ContextId, ExecutionError, ExecutionOptions, FastProcessor, Felt, ProcessorState,
21    advice::{AdviceInputs, AdviceMutation},
22    event::{EventHandler, EventName},
23    mast::MastForest,
24    trace::RowIndex,
25};
26
27use super::{
28    DebugExecutor, DebuggerHost, ExecutionConfig, ExecutionTrace, TraceEvent,
29    query::read_memory_bytes, trace_event::TRACE_PRINT_LN,
30};
31use crate::{
32    debug::{CallStack, DebugVarTracker, NativePtr},
33    felt::FromMidenRepr,
34};
35
36/// Maximum number of bytes for a single `println` output.
37///
38/// A limit is required as `u32::MAX` exceeds the size that strings can take in Miden VM. The limit
39/// is generous and still permits use cases like formatting a large amount of data in storage.
40///
41/// Exceeding the limit likely indicates a bug in the corresponding trace event handling.
42const MAX_PRINTLN_BYTES: usize = 512 * 1024;
43
44/// The [Executor] is responsible for executing a program with the Miden VM.
45///
46/// It is used by either converting it into a [DebugExecutor], and using that to
47/// manage execution step-by-step, such as is done by the debugger; or by running
48/// the program to completion and obtaining an [ExecutionTrace], which can be used
49/// to introspect the final program state.
50pub struct Executor {
51    stack: StackInputs,
52    advice: AdviceInputs,
53    options: ExecutionOptions,
54    libraries: Vec<Arc<Library>>,
55    event_handlers: Vec<(EventName, Arc<dyn EventHandler>)>,
56    dependency_resolver: BTreeMap<Word, Arc<Library>>,
57}
58impl Executor {
59    /// Construct an executor with the given arguments on the operand stack
60    pub fn new(args: Vec<Felt>) -> Self {
61        let config = ExecutionConfig {
62            inputs: StackInputs::new(&args).expect("invalid stack inputs"),
63            ..Default::default()
64        };
65
66        Self::from_config(config)
67    }
68
69    /// Construct an executor from the given configuration
70    ///
71    /// NOTE: The execution options for tracing/debugging will be set to true for you
72    pub fn from_config(config: ExecutionConfig) -> Self {
73        let ExecutionConfig {
74            inputs,
75            advice_inputs,
76            options,
77        } = config;
78        let options = options.with_tracing(true).with_debugging(true);
79        let dependency_resolver = BTreeMap::new();
80
81        Self {
82            stack: inputs,
83            advice: advice_inputs,
84            options,
85            libraries: Default::default(),
86            event_handlers: Default::default(),
87            dependency_resolver,
88        }
89    }
90
91    /// Construct the executor with the given inputs and adds dependencies from the given package
92    pub fn for_package<I>(package: &miden_mast_package::Package, args: I) -> Result<Self, Report>
93    where
94        I: IntoIterator<Item = Felt>,
95    {
96        use miden_assembly_syntax::DisplayHex;
97        log::debug!(
98            "creating executor for package '{}' (digest={})",
99            package.name,
100            DisplayHex::new(&package.digest().as_bytes())
101        );
102        let mut exec = Self::new(args.into_iter().collect());
103        let dependencies = package.manifest.dependencies();
104        exec.with_dependencies(dependencies)?;
105        log::debug!("executor created");
106        Ok(exec)
107    }
108
109    /// Adds dependencies to the executor
110    pub fn with_dependencies<'a>(
111        &mut self,
112        dependencies: impl Iterator<Item = &'a Dependency>,
113    ) -> Result<&mut Self, Report> {
114        for dep in dependencies {
115            let digest = dep.digest;
116            match self.dependency_resolver.get(&digest) {
117                Some(lib) => {
118                    log::debug!("dependency {dep:?} resolved");
119                    self.with_library(lib.clone());
120                }
121                None => panic!("{dep:?} not found in resolver"),
122            }
123        }
124
125        log::debug!("executor created");
126
127        Ok(self)
128    }
129
130    /// Set the contents of memory for the shadow stack frame of the entrypoint
131    pub fn with_advice_inputs(&mut self, advice: AdviceInputs) -> &mut Self {
132        self.advice.extend(advice);
133        self
134    }
135
136    /// Add a [Library] to the execution context
137    pub fn with_library(&mut self, lib: Arc<Library>) -> &mut Self {
138        self.libraries.push(lib);
139        self
140    }
141
142    /// Register a VM event handler to be available during execution.
143    pub fn register_event_handler(
144        &mut self,
145        event: EventName,
146        handler: Arc<dyn EventHandler>,
147    ) -> Result<&mut Self, ExecutionError> {
148        self.event_handlers.push((event, handler));
149        Ok(self)
150    }
151
152    /// Convert this [Executor] into a [DebugExecutor], which captures much more information
153    /// about the program being executed, and must be stepped manually.
154    pub fn into_debug(
155        mut self,
156        program: &Program,
157        source_manager: Arc<dyn SourceManager>,
158    ) -> DebugExecutor {
159        log::debug!("creating debug executor");
160
161        let mut host = DebuggerHost::new(source_manager.clone());
162        for lib in core::mem::take(&mut self.libraries) {
163            host.load_mast_forest(lib.mast_forest().clone());
164        }
165        for (event, handler) in core::mem::take(&mut self.event_handlers) {
166            host.register_event_handler(event, handler)
167                .expect("failed to register debug executor event handler");
168        }
169
170        let trace_events: Rc<RefCell<BTreeMap<RowIndex, TraceEvent>>> = Rc::new(Default::default());
171        register_builtin_trace_handlers(&mut host, Rc::clone(&trace_events));
172
173        // Set up debug variable tracking
174        // Note: Currently no debug var events are emitted (requires new miden-core),
175        // but we set up the infrastructure for when they become available.
176        let debug_var_events: Rc<RefCell<BTreeMap<RowIndex, Vec<DebugVarInfo>>>> =
177            Rc::new(Default::default());
178
179        let mut processor = FastProcessor::new_with_options(self.stack, self.advice, self.options)
180            .expect("advice inputs should fit advice map limits")
181            .with_debugging(true)
182            .with_tracing(true);
183
184        let root_context = ContextId::root();
185        let resume_ctx = processor
186            .get_initial_resume_context(program)
187            .expect("failed to get initial resume context");
188
189        let callstack = CallStack::new(trace_events);
190        let debug_vars = DebugVarTracker::new(debug_var_events);
191        DebugExecutor {
192            processor,
193            host,
194            resume_ctx: Some(resume_ctx),
195            current_stack: vec![],
196            current_op: None,
197            current_asmop: None,
198            stack_outputs: Default::default(),
199            contexts: Default::default(),
200            root_context,
201            current_context: root_context,
202            callstack,
203            current_proc: None,
204            debug_vars,
205            last_debug_var_count: 0,
206            recent: VecDeque::with_capacity(5),
207            cycle: 0,
208            stopped: false,
209        }
210    }
211
212    /// Convert this [Executor] into a [DebugExecutor] with event replay support.
213    ///
214    /// Like [`into_debug`](Self::into_debug), but additionally:
215    /// - Loads `extra_forests` into the host's MAST forest store
216    /// - Sets the event replay queue so that `on_event()` returns pre-recorded mutations
217    ///
218    /// This is used for transaction debugging where events were recorded during a prior
219    /// execution with the real transaction host.
220    pub fn into_debug_with_replay(
221        mut self,
222        program: &Program,
223        source_manager: Arc<dyn SourceManager>,
224        extra_forests: Vec<Arc<MastForest>>,
225        event_replay: VecDeque<Vec<AdviceMutation>>,
226    ) -> DebugExecutor {
227        log::debug!("creating debug executor with event replay");
228
229        let mut host = DebuggerHost::new(source_manager.clone());
230        for lib in core::mem::take(&mut self.libraries) {
231            host.load_mast_forest(lib.mast_forest().clone());
232        }
233        for forest in extra_forests {
234            host.load_mast_forest(forest);
235        }
236        host.set_event_replay(event_replay);
237
238        let debug_var_events: Rc<RefCell<BTreeMap<RowIndex, Vec<DebugVarInfo>>>> =
239            Rc::new(Default::default());
240
241        let trace_events: Rc<RefCell<BTreeMap<RowIndex, TraceEvent>>> = Rc::new(Default::default());
242        register_builtin_trace_handlers(&mut host, Rc::clone(&trace_events));
243
244        let mut processor = FastProcessor::new_with_options(self.stack, self.advice, self.options)
245            .expect("advice inputs should fit advice map limits")
246            .with_debugging(true)
247            .with_tracing(true);
248
249        let root_context = ContextId::root();
250        let resume_ctx = processor
251            .get_initial_resume_context(program)
252            .expect("failed to get initial resume context");
253
254        let callstack = CallStack::new(trace_events);
255        let debug_vars = DebugVarTracker::new(debug_var_events);
256        DebugExecutor {
257            processor,
258            host,
259            resume_ctx: Some(resume_ctx),
260            current_stack: vec![],
261            current_op: None,
262            current_asmop: None,
263            stack_outputs: Default::default(),
264            contexts: Default::default(),
265            root_context,
266            current_context: root_context,
267            callstack,
268            current_proc: None,
269            debug_vars,
270            last_debug_var_count: 0,
271            recent: VecDeque::with_capacity(5),
272            cycle: 0,
273            stopped: false,
274        }
275    }
276
277    /// Execute the given program until termination, producing a trace
278    pub fn capture_trace(
279        self,
280        program: &Program,
281        source_manager: Arc<dyn SourceManager>,
282    ) -> ExecutionTrace {
283        let mut executor = self.into_debug(program, source_manager);
284        loop {
285            if executor.stopped {
286                break;
287            }
288            match executor.step() {
289                Ok(_) => continue,
290                Err(err) => {
291                    log::warn!(
292                        target: "executor",
293                        "capture_trace stopped early at cycle {}: {err}",
294                        executor.cycle,
295                    );
296                    break;
297                }
298            }
299        }
300        executor.into_execution_trace()
301    }
302
303    /// Execute the given program, producing a trace
304    #[track_caller]
305    pub fn execute(
306        self,
307        program: &Program,
308        source_manager: Arc<dyn SourceManager>,
309    ) -> ExecutionTrace {
310        let mut executor = self.into_debug(program, source_manager.clone());
311        loop {
312            if executor.stopped {
313                break;
314            }
315            match executor.step() {
316                Ok(_) => {
317                    if log::log_enabled!(target: "executor", log::Level::Trace)
318                        && let (Some(op), Some(asmop)) =
319                            (executor.current_op, executor.current_asmop.as_ref())
320                    {
321                        dbg!(&executor.current_stack);
322                        let source_loc = asmop.location().map(|loc| {
323                            let path = std::path::Path::new(loc.uri().path());
324                            let file = source_manager.load_file(path).unwrap();
325                            (file, loc.start)
326                        });
327                        if let Some((source_file, line_start)) = source_loc {
328                            let line_number = source_file.content().line_index(line_start).number();
329                            log::trace!(target: "executor", "in {} (located at {}:{})", asmop.context_name(), source_file.deref().uri().as_str(), &line_number);
330                        } else {
331                            log::trace!(target: "executor", "in {} (no source location available)", asmop.context_name());
332                        }
333                        log::trace!(target: "executor", "  executed `{op:?}` of `{}` ({} cycles)", asmop.op(), asmop.num_cycles());
334                        log::trace!(target: "executor", "  stack state: {:#?}", &executor.current_stack);
335                    }
336                }
337                Err(err) => {
338                    render_execution_error(err, &executor, &source_manager);
339                }
340            }
341        }
342
343        executor.into_execution_trace()
344    }
345
346    /// Execute a program, parsing the operand stack outputs as a value of type `T`
347    pub fn execute_into<T>(self, program: &Program, source_manager: Arc<dyn SourceManager>) -> T
348    where
349        T: FromMidenRepr + PartialEq,
350    {
351        let out = self.execute(program, source_manager);
352        out.parse_result().expect("invalid result")
353    }
354
355    pub fn dependency_resolver_mut(&mut self) -> &mut BTreeMap<Word, Arc<Library>> {
356        &mut self.dependency_resolver
357    }
358
359    /// Register a library with the dependency resolver so it can be found when resolving package dependencies
360    pub fn register_library_dependency(&mut self, lib: Arc<Library>) {
361        let digest = *lib.digest();
362        self.dependency_resolver.insert(digest, lib);
363    }
364}
365
366#[derive(Debug, thiserror::Error)]
367enum PrintLnError {
368    #[error("address should fit in u32")]
369    InvalidAddress,
370    #[error("string length should fit in usize")]
371    InvalidLength,
372    #[error("string length {requested} exceeds maximum {max}")]
373    LengthExceeded { requested: usize, max: usize },
374    #[error("memory is not initialized")]
375    MemoryNotInitialized,
376    #[error("failed to read memory: {0}")]
377    MemoryRead(#[from] super::trace::MemoryReadError),
378    #[error("invalid UTF-8")]
379    InvalidUtf8,
380}
381
382fn register_builtin_trace_handlers(
383    host: &mut DebuggerHost<dyn SourceManager>,
384    trace_events: Rc<RefCell<BTreeMap<RowIndex, TraceEvent>>>,
385) {
386    let frame_start_events = Rc::clone(&trace_events);
387    host.register_trace_handler(TraceEvent::FrameStart, move |process, event| {
388        frame_start_events.borrow_mut().insert(process.clock(), event);
389    });
390    let frame_end_events = Rc::clone(&trace_events);
391    host.register_trace_handler(TraceEvent::FrameEnd, move |process, event| {
392        frame_end_events.borrow_mut().insert(process.clock(), event);
393    });
394
395    host.register_trace_handler(TraceEvent::PrintLn, move |process, _event| {
396        match decode_println(process) {
397            Ok(content) => {
398                log::log!(target: "stdout", Level::Info, "{content}");
399            }
400            Err(err) => {
401                log::warn!(
402                    target: "executor",
403                    "trace.{TRACE_PRINT_LN} failed at cycle {}: {err}",
404                    process.clock(),
405                );
406            }
407        }
408    });
409
410    let assertion_events = Rc::clone(&trace_events);
411    host.register_assert_failed_tracer(move |process, event| {
412        assertion_events.borrow_mut().insert(process.clock(), event);
413    });
414}
415
416/// Decode a `TRACE_PRINT_LN` event into a UTF-8 string.
417///
418/// Expects `[address, length]` on the operand stack. Reads `length` bytes from
419/// `address` in the current context's memory and returns them as a string.
420fn decode_println(process: &ProcessorState<'_>) -> Result<String, PrintLnError> {
421    let addr = u32::try_from(process.get_stack_item(0).as_canonical_u64())
422        .map_err(|_| PrintLnError::InvalidAddress)?;
423    let len = usize::try_from(process.get_stack_item(1).as_canonical_u64())
424        .map_err(|_| PrintLnError::InvalidLength)?;
425    if len > MAX_PRINTLN_BYTES {
426        return Err(PrintLnError::LengthExceeded {
427            requested: len,
428            max: MAX_PRINTLN_BYTES,
429        });
430    }
431    let ptr = NativePtr::from_ptr(addr);
432    let ctx = process.ctx();
433
434    let bytes = read_memory_bytes(ptr, len, |addr| {
435        process.get_mem_value(ctx, addr).ok_or(PrintLnError::MemoryNotInitialized)
436    })?;
437
438    String::from_utf8(bytes).map_err(|_| PrintLnError::InvalidUtf8)
439}
440
441#[track_caller]
442fn render_execution_error(
443    err: ExecutionError,
444    execution_state: &DebugExecutor,
445    source_manager: &dyn SourceManager,
446) -> ! {
447    use miden_assembly_syntax::diagnostics::{
448        LabeledSpan, miette::miette, reporting::PrintDiagnostic,
449    };
450
451    let stacktrace = execution_state.callstack.stacktrace(&execution_state.recent, source_manager);
452
453    eprintln!("{stacktrace}");
454
455    if !execution_state.current_stack.is_empty() {
456        let stack = execution_state.current_stack.iter().map(|elem| elem.as_canonical_u64());
457        let stack = DisplayValues::new(stack);
458        eprintln!(
459            "\nLast Known State (at most recent instruction which succeeded):
460 | Operand Stack: [{stack}]
461 "
462        );
463
464        let mut labels = vec![];
465        if let Some(span) = stacktrace
466            .current_frame()
467            .and_then(|frame| frame.location.as_ref())
468            .map(|loc| loc.span)
469        {
470            labels.push(LabeledSpan::new_with_span(
471                None,
472                span.start().to_usize()..span.end().to_usize(),
473            ));
474        }
475        let report = miette!(
476            labels = labels,
477            "program execution failed at step {step} (cycle {cycle}): {err}",
478            step = execution_state.cycle,
479            cycle = execution_state.cycle,
480        );
481        let report = match stacktrace
482            .current_frame()
483            .and_then(|frame| frame.location.as_ref())
484            .map(|loc| loc.source_file.clone())
485        {
486            Some(source) => report.with_source_code(source),
487            None => report,
488        };
489
490        panic!("{}", PrintDiagnostic::new(report));
491    } else {
492        panic!("program execution failed at step {step}: {err}", step = execution_state.cycle);
493    }
494}
495
496/// Render an iterator of `T`, comma-separated
497struct DisplayValues<T>(Cell<Option<T>>);
498
499impl<T> DisplayValues<T> {
500    pub fn new(inner: T) -> Self {
501        Self(Cell::new(Some(inner)))
502    }
503}
504
505impl<T, I> fmt::Display for DisplayValues<I>
506where
507    T: fmt::Display,
508    I: Iterator<Item = T>,
509{
510    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
511        let iter = self.0.take().unwrap();
512        for (i, item) in iter.enumerate() {
513            if i == 0 {
514                write!(f, "{item}")?;
515            } else {
516                write!(f, ", {item}")?;
517            }
518        }
519        Ok(())
520    }
521}