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    trace::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(self.stack)
180            .with_advice(self.advice)
181            .with_options(self.options)
182            .with_debugging(true)
183            .with_tracing(true);
184
185        let root_context = ContextId::root();
186        let resume_ctx = processor
187            .get_initial_resume_context(program)
188            .expect("failed to get initial resume context");
189
190        let callstack = CallStack::new(trace_events);
191        let debug_vars = DebugVarTracker::new(debug_var_events);
192        DebugExecutor {
193            processor,
194            host,
195            resume_ctx: Some(resume_ctx),
196            current_stack: vec![],
197            current_op: None,
198            current_asmop: None,
199            stack_outputs: Default::default(),
200            contexts: Default::default(),
201            root_context,
202            current_context: root_context,
203            callstack,
204            current_proc: None,
205            debug_vars,
206            last_debug_var_count: 0,
207            recent: VecDeque::with_capacity(5),
208            cycle: 0,
209            stopped: false,
210        }
211    }
212
213    /// Convert this [Executor] into a [DebugExecutor] with event replay support.
214    ///
215    /// Like [`into_debug`](Self::into_debug), but additionally:
216    /// - Loads `extra_forests` into the host's MAST forest store
217    /// - Sets the event replay queue so that `on_event()` returns pre-recorded mutations
218    ///
219    /// This is used for transaction debugging where events were recorded during a prior
220    /// execution with the real transaction host.
221    pub fn into_debug_with_replay(
222        mut self,
223        program: &Program,
224        source_manager: Arc<dyn SourceManager>,
225        extra_forests: Vec<Arc<MastForest>>,
226        event_replay: VecDeque<Vec<AdviceMutation>>,
227    ) -> DebugExecutor {
228        log::debug!("creating debug executor with event replay");
229
230        let mut host = DebuggerHost::new(source_manager.clone());
231        for lib in core::mem::take(&mut self.libraries) {
232            host.load_mast_forest(lib.mast_forest().clone());
233        }
234        for forest in extra_forests {
235            host.load_mast_forest(forest);
236        }
237        host.set_event_replay(event_replay);
238
239        let debug_var_events: Rc<RefCell<BTreeMap<RowIndex, Vec<DebugVarInfo>>>> =
240            Rc::new(Default::default());
241
242        let trace_events: Rc<RefCell<BTreeMap<RowIndex, TraceEvent>>> = Rc::new(Default::default());
243        register_builtin_trace_handlers(&mut host, Rc::clone(&trace_events));
244
245        let mut processor = FastProcessor::new(self.stack)
246            .with_advice(self.advice)
247            .with_options(self.options)
248            .with_debugging(true)
249            .with_tracing(true);
250
251        let root_context = ContextId::root();
252        let resume_ctx = processor
253            .get_initial_resume_context(program)
254            .expect("failed to get initial resume context");
255
256        let callstack = CallStack::new(trace_events);
257        let debug_vars = DebugVarTracker::new(debug_var_events);
258        DebugExecutor {
259            processor,
260            host,
261            resume_ctx: Some(resume_ctx),
262            current_stack: vec![],
263            current_op: None,
264            current_asmop: None,
265            stack_outputs: Default::default(),
266            contexts: Default::default(),
267            root_context,
268            current_context: root_context,
269            callstack,
270            current_proc: None,
271            debug_vars,
272            last_debug_var_count: 0,
273            recent: VecDeque::with_capacity(5),
274            cycle: 0,
275            stopped: false,
276        }
277    }
278
279    /// Execute the given program until termination, producing a trace
280    pub fn capture_trace(
281        self,
282        program: &Program,
283        source_manager: Arc<dyn SourceManager>,
284    ) -> ExecutionTrace {
285        let mut executor = self.into_debug(program, source_manager);
286        loop {
287            if executor.stopped {
288                break;
289            }
290            match executor.step() {
291                Ok(_) => continue,
292                Err(err) => {
293                    log::warn!(
294                        target: "executor",
295                        "capture_trace stopped early at cycle {}: {err}",
296                        executor.cycle,
297                    );
298                    break;
299                }
300            }
301        }
302        executor.into_execution_trace()
303    }
304
305    /// Execute the given program, producing a trace
306    #[track_caller]
307    pub fn execute(
308        self,
309        program: &Program,
310        source_manager: Arc<dyn SourceManager>,
311    ) -> ExecutionTrace {
312        let mut executor = self.into_debug(program, source_manager.clone());
313        loop {
314            if executor.stopped {
315                break;
316            }
317            match executor.step() {
318                Ok(_) => {
319                    if log::log_enabled!(target: "executor", log::Level::Trace)
320                        && let (Some(op), Some(asmop)) =
321                            (executor.current_op, executor.current_asmop.as_ref())
322                    {
323                        dbg!(&executor.current_stack);
324                        let source_loc = asmop.location().map(|loc| {
325                            let path = std::path::Path::new(loc.uri().path());
326                            let file = source_manager.load_file(path).unwrap();
327                            (file, loc.start)
328                        });
329                        if let Some((source_file, line_start)) = source_loc {
330                            let line_number = source_file.content().line_index(line_start).number();
331                            log::trace!(target: "executor", "in {} (located at {}:{})", asmop.context_name(), source_file.deref().uri().as_str(), &line_number);
332                        } else {
333                            log::trace!(target: "executor", "in {} (no source location available)", asmop.context_name());
334                        }
335                        log::trace!(target: "executor", "  executed `{op:?}` of `{}` ({} cycles)", asmop.op(), asmop.num_cycles());
336                        log::trace!(target: "executor", "  stack state: {:#?}", &executor.current_stack);
337                    }
338                }
339                Err(err) => {
340                    render_execution_error(err, &executor, &source_manager);
341                }
342            }
343        }
344
345        executor.into_execution_trace()
346    }
347
348    /// Execute a program, parsing the operand stack outputs as a value of type `T`
349    pub fn execute_into<T>(self, program: &Program, source_manager: Arc<dyn SourceManager>) -> T
350    where
351        T: FromMidenRepr + PartialEq,
352    {
353        let out = self.execute(program, source_manager);
354        out.parse_result().expect("invalid result")
355    }
356
357    pub fn dependency_resolver_mut(&mut self) -> &mut BTreeMap<Word, Arc<Library>> {
358        &mut self.dependency_resolver
359    }
360
361    /// Register a library with the dependency resolver so it can be found when resolving package dependencies
362    pub fn register_library_dependency(&mut self, lib: Arc<Library>) {
363        let digest = *lib.digest();
364        self.dependency_resolver.insert(digest, lib);
365    }
366}
367
368#[derive(Debug, thiserror::Error)]
369enum PrintLnError {
370    #[error("address should fit in u32")]
371    InvalidAddress,
372    #[error("string length should fit in usize")]
373    InvalidLength,
374    #[error("string length {requested} exceeds maximum {max}")]
375    LengthExceeded { requested: usize, max: usize },
376    #[error("memory is not initialized")]
377    MemoryNotInitialized,
378    #[error("failed to read memory: {0}")]
379    MemoryRead(#[from] super::trace::MemoryReadError),
380    #[error("invalid UTF-8")]
381    InvalidUtf8,
382}
383
384fn register_builtin_trace_handlers(
385    host: &mut DebuggerHost<dyn SourceManager>,
386    trace_events: Rc<RefCell<BTreeMap<RowIndex, TraceEvent>>>,
387) {
388    let frame_start_events = Rc::clone(&trace_events);
389    host.register_trace_handler(TraceEvent::FrameStart, move |process, event| {
390        frame_start_events.borrow_mut().insert(process.clock(), event);
391    });
392    let frame_end_events = Rc::clone(&trace_events);
393    host.register_trace_handler(TraceEvent::FrameEnd, move |process, event| {
394        frame_end_events.borrow_mut().insert(process.clock(), event);
395    });
396
397    host.register_trace_handler(TraceEvent::PrintLn, move |process, _event| {
398        match decode_println(process) {
399            Ok(content) => {
400                log::log!(target: "stdout", Level::Info, "{content}");
401            }
402            Err(err) => {
403                log::warn!(
404                    target: "executor",
405                    "trace.{TRACE_PRINT_LN} failed at cycle {}: {err}",
406                    process.clock(),
407                );
408            }
409        }
410    });
411
412    let assertion_events = Rc::clone(&trace_events);
413    host.register_assert_failed_tracer(move |process, event| {
414        assertion_events.borrow_mut().insert(process.clock(), event);
415    });
416}
417
418/// Decode a `TRACE_PRINT_LN` event into a UTF-8 string.
419///
420/// Expects `[address, length]` on the operand stack. Reads `length` bytes from
421/// `address` in the current context's memory and returns them as a string.
422fn decode_println(process: &ProcessorState<'_>) -> Result<String, PrintLnError> {
423    let addr = u32::try_from(process.get_stack_item(0).as_canonical_u64())
424        .map_err(|_| PrintLnError::InvalidAddress)?;
425    let len = usize::try_from(process.get_stack_item(1).as_canonical_u64())
426        .map_err(|_| PrintLnError::InvalidLength)?;
427    if len > MAX_PRINTLN_BYTES {
428        return Err(PrintLnError::LengthExceeded {
429            requested: len,
430            max: MAX_PRINTLN_BYTES,
431        });
432    }
433    let ptr = NativePtr::from_ptr(addr);
434    let ctx = process.ctx();
435
436    let bytes = read_memory_bytes(ptr, len, |addr| {
437        process.get_mem_value(ctx, addr).ok_or(PrintLnError::MemoryNotInitialized)
438    })?;
439
440    String::from_utf8(bytes).map_err(|_| PrintLnError::InvalidUtf8)
441}
442
443#[track_caller]
444fn render_execution_error(
445    err: ExecutionError,
446    execution_state: &DebugExecutor,
447    source_manager: &dyn SourceManager,
448) -> ! {
449    use miden_assembly_syntax::diagnostics::{
450        LabeledSpan, miette::miette, reporting::PrintDiagnostic,
451    };
452
453    let stacktrace = execution_state.callstack.stacktrace(&execution_state.recent, source_manager);
454
455    eprintln!("{stacktrace}");
456
457    if !execution_state.current_stack.is_empty() {
458        let stack = execution_state.current_stack.iter().map(|elem| elem.as_canonical_u64());
459        let stack = DisplayValues::new(stack);
460        eprintln!(
461            "\nLast Known State (at most recent instruction which succeeded):
462 | Operand Stack: [{stack}]
463 "
464        );
465
466        let mut labels = vec![];
467        if let Some(span) = stacktrace
468            .current_frame()
469            .and_then(|frame| frame.location.as_ref())
470            .map(|loc| loc.span)
471        {
472            labels.push(LabeledSpan::new_with_span(
473                None,
474                span.start().to_usize()..span.end().to_usize(),
475            ));
476        }
477        let report = miette!(
478            labels = labels,
479            "program execution failed at step {step} (cycle {cycle}): {err}",
480            step = execution_state.cycle,
481            cycle = execution_state.cycle,
482        );
483        let report = match stacktrace
484            .current_frame()
485            .and_then(|frame| frame.location.as_ref())
486            .map(|loc| loc.source_file.clone())
487        {
488            Some(source) => report.with_source_code(source),
489            None => report,
490        };
491
492        panic!("{}", PrintDiagnostic::new(report));
493    } else {
494        panic!("program execution failed at step {step}: {err}", step = execution_state.cycle);
495    }
496}
497
498/// Render an iterator of `T`, comma-separated
499struct DisplayValues<T>(Cell<Option<T>>);
500
501impl<T> DisplayValues<T> {
502    pub fn new(inner: T) -> Self {
503        Self(Cell::new(Some(inner)))
504    }
505}
506
507impl<T, I> fmt::Display for DisplayValues<I>
508where
509    T: fmt::Display,
510    I: Iterator<Item = T>,
511{
512    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
513        let iter = self.0.take().unwrap();
514        for (i, item) in iter.enumerate() {
515            if i == 0 {
516                write!(f, "{item}")?;
517            } else {
518                write!(f, ", {item}")?;
519            }
520        }
521        Ok(())
522    }
523}