Skip to main content

miden_debug_engine/exec/
executor.rs

1use alloc::{
2    collections::{BTreeMap, VecDeque},
3    rc::Rc,
4    string::String,
5    sync::Arc,
6    vec::Vec,
7};
8use core::{
9    cell::{Cell, RefCell},
10    fmt,
11    ops::Deref,
12};
13
14use log::Level;
15use miden_assembly_syntax::{ast::DebugVarInfo, debuginfo::SourceFile, diagnostics::Report};
16use miden_core::program::StackInputs;
17use miden_debug_types::{ByteIndex, SourceManager};
18use miden_mast_package::Package;
19use miden_package_registry::PackageCache;
20use miden_processor::{
21    ContextId, ExecutionError, ExecutionOptions, FastProcessor, Felt, LoadedMastForest,
22    ProcessorState,
23    advice::{AdviceInputs, AdviceMutation},
24    event::{EventError, EventHandler, EventName},
25    trace::RowIndex,
26};
27use miden_utils_sync::RwLock;
28
29use super::{
30    DebugExecutor, DebuggerHost, Event, ExecutionConfig, ExecutionTrace,
31    event::{FRAME_END_EVENT, FRAME_START_EVENT, PRINTLN_EVENT},
32    query::read_memory_bytes,
33};
34use crate::{
35    HybridPackageRegistry,
36    debug::{CallStack, DebugVarTracker, NativePtr},
37    felt::FromMidenRepr,
38    profiling::{Profiler, ProfilerConfig},
39};
40
41/// Maximum number of bytes for a single `println` output.
42///
43/// A limit is required as `u32::MAX` exceeds the size that strings can take in Miden VM. The limit
44/// is generous and still permits use cases like formatting a large amount of data in storage.
45///
46/// Exceeding the limit likely indicates a bug in the corresponding trace event handling.
47const MAX_PRINTLN_BYTES: usize = 512 * 1024;
48
49/// The [Executor] is responsible for executing a program with the Miden VM.
50///
51/// It is used by either converting it into a [DebugExecutor], and using that to
52/// manage execution step-by-step, such as is done by the debugger; or by running
53/// the program to completion and obtaining an [ExecutionTrace], which can be used
54/// to introspect the final program state.
55pub struct Executor {
56    stack: StackInputs,
57    advice: AdviceInputs,
58    options: ExecutionOptions,
59    event_handlers: Vec<(EventName, Arc<dyn EventHandler>)>,
60    registry: HybridPackageRegistry,
61    record_event_mutations: bool,
62    profiler_config: ProfilerConfig,
63}
64
65impl Executor {
66    /// Construct an executor with the given arguments on the operand stack
67    pub fn new(args: Vec<Felt>) -> Self {
68        let config = ExecutionConfig {
69            inputs: StackInputs::new(&args).expect("invalid stack inputs"),
70            ..Default::default()
71        };
72
73        Self::from_config(config)
74    }
75
76    /// Construct an executor from the given configuration
77    ///
78    /// NOTE: The execution options for tracing/debugging will be set to true for you
79    pub fn from_config(config: ExecutionConfig) -> Self {
80        let ExecutionConfig {
81            inputs,
82            advice_inputs,
83            options,
84        } = config;
85
86        Self {
87            stack: inputs,
88            advice: advice_inputs,
89            options,
90            event_handlers: Default::default(),
91            registry: HybridPackageRegistry::empty(),
92            record_event_mutations: false,
93            profiler_config: Default::default(),
94        }
95    }
96
97    #[inline]
98    pub fn with_registry(mut self, registry: HybridPackageRegistry) -> Self {
99        self.registry = registry;
100        self
101    }
102
103    /// Set the contents of memory for the shadow stack frame of the entrypoint
104    pub fn with_advice_inputs(&mut self, advice: AdviceInputs) -> &mut Self {
105        self.advice.extend(advice);
106        self
107    }
108
109    /// Add a [Package] to the execution context
110    pub fn with_package(&mut self, package: Arc<Package>) -> Result<&mut Self, Report> {
111        self.registry.cache_package(package)?;
112        Ok(self)
113    }
114
115    /// Record the advice mutations produced by each event handler invocation during execution.
116    ///
117    /// Recording is a private detail of the debug host created by [Executor::into_debug]: once
118    /// the program completes, take the log via [DebuggerHost::take_recorded_event_mutations] on
119    /// the [DebugExecutor]'s host, and feed it back into [Executor::into_debug_with_replay] to
120    /// debug the same execution later without the original event handlers (e.g. transaction
121    /// debugging with event replay).
122    ///
123    /// Mutations are only recorded for live event handling; nothing is recorded while an event
124    /// replay queue is being consumed.
125    pub fn with_event_advice_mutations_recording(&mut self) -> &mut Self {
126        self.record_event_mutations = true;
127        self
128    }
129
130    /// Register a VM event handler to be available during execution.
131    pub fn register_event_handler(
132        &mut self,
133        event: EventName,
134        handler: Arc<dyn EventHandler>,
135    ) -> Result<&mut Self, ExecutionError> {
136        self.event_handlers.push((event, handler));
137        Ok(self)
138    }
139
140    /// Set the profiler configuration for this executor.
141    pub fn with_profiler_config(&mut self, profiler_config: ProfilerConfig) -> &mut Self {
142        self.profiler_config = profiler_config;
143        self
144    }
145
146    /// Convert this [Executor] into a [DebugExecutor], which captures much more information
147    /// about the program being executed, and must be stepped manually.
148    pub fn into_debug(
149        mut self,
150        package: Arc<Package>,
151        source_manager: Arc<dyn SourceManager>,
152    ) -> DebugExecutor {
153        assert!(package.is_program());
154
155        log::debug!("creating debug executor");
156
157        let mut host = DebuggerHost::new(source_manager.clone());
158        for lib in self.registry.all() {
159            host.load_package(lib);
160        }
161        for (event, handler) in core::mem::take(&mut self.event_handlers) {
162            host.register_event_handler(event, handler)
163                .expect("failed to register debug executor event handler");
164        }
165        if self.record_event_mutations {
166            host = host.with_event_advice_mutations_recording();
167        }
168
169        let events: Arc<RwLock<BTreeMap<RowIndex, Event>>> = Arc::new(Default::default());
170        register_builtin_event_handlers(&mut host, Arc::clone(&events));
171
172        // Set up debug variable tracking
173        let debug_var_events: Rc<RefCell<BTreeMap<RowIndex, Vec<DebugVarInfo>>>> =
174            Rc::new(Default::default());
175
176        let mut processor = FastProcessor::new_with_options(self.stack, self.advice, self.options)
177            .expect("advice inputs should fit advice map limits");
178
179        let root_context = ContextId::root();
180        let resume_ctx = processor
181            .get_initial_resume_context_for_package(package)
182            .expect("failed to get initial resume context");
183
184        let callstack = CallStack::new(events);
185        let debug_vars = DebugVarTracker::new(debug_var_events);
186        DebugExecutor {
187            processor,
188            host,
189            resume_ctx: Some(resume_ctx),
190            current_stack: vec![],
191            current_op: None,
192            current_asmop: None,
193            stack_outputs: Default::default(),
194            contexts: Default::default(),
195            root_context,
196            current_context: root_context,
197            callstack,
198            current_proc: None,
199            debug_vars,
200            last_debug_var_count: 0,
201            recent: VecDeque::with_capacity(5),
202            cycle: 0,
203            stopped: false,
204            profiler: Profiler::from_config(self.profiler_config),
205        }
206    }
207
208    /// Convert this [Executor] into a [DebugExecutor] with event replay support.
209    ///
210    /// Like [`into_debug`](Self::into_debug), but additionally:
211    /// - Loads `extra_forests` into the host's MAST forest store
212    /// - Sets the event replay queue so that `on_event()` returns pre-recorded mutations
213    ///
214    /// This is used for transaction debugging where events were recorded during a prior
215    /// execution with the real transaction host.
216    pub fn into_debug_with_replay(
217        self,
218        package: Arc<Package>,
219        source_manager: Arc<dyn SourceManager>,
220        extra_mast_forests: Vec<LoadedMastForest>,
221        event_replay: VecDeque<Vec<AdviceMutation>>,
222    ) -> DebugExecutor {
223        assert!(package.is_program());
224
225        log::debug!("creating debug executor with event replay");
226
227        let mut host = DebuggerHost::new(source_manager.clone());
228        for lib in self.registry.all() {
229            host.load_package(lib);
230        }
231        for forest in extra_mast_forests {
232            host.load_mast_forest(forest);
233        }
234        host.set_event_replay(event_replay);
235
236        let debug_var_events: Rc<RefCell<BTreeMap<RowIndex, Vec<DebugVarInfo>>>> =
237            Rc::new(Default::default());
238
239        let events: Arc<RwLock<BTreeMap<RowIndex, Event>>> = Arc::new(Default::default());
240        register_builtin_event_handlers(&mut host, Arc::clone(&events));
241
242        let mut processor = FastProcessor::new_with_options(self.stack, self.advice, self.options)
243            .expect("advice inputs should fit advice map limits");
244
245        let root_context = ContextId::root();
246        let resume_ctx = processor
247            .get_initial_resume_context_for_package(package)
248            .expect("failed to get initial resume context");
249
250        let callstack = CallStack::new(events);
251        let debug_vars = DebugVarTracker::new(debug_var_events);
252        DebugExecutor {
253            processor,
254            host,
255            resume_ctx: Some(resume_ctx),
256            current_stack: vec![],
257            current_op: None,
258            current_asmop: None,
259            stack_outputs: Default::default(),
260            contexts: Default::default(),
261            root_context,
262            current_context: root_context,
263            callstack,
264            current_proc: None,
265            debug_vars,
266            last_debug_var_count: 0,
267            recent: VecDeque::with_capacity(5),
268            cycle: 0,
269            stopped: false,
270            profiler: Profiler::from_config(self.profiler_config),
271        }
272    }
273
274    /// Execute the given program until termination, producing a trace
275    pub fn capture_trace(
276        self,
277        package: Arc<Package>,
278        source_manager: Arc<dyn SourceManager>,
279    ) -> ExecutionTrace {
280        let mut executor = self.into_debug(package, source_manager);
281        loop {
282            if executor.stopped {
283                break;
284            }
285            match executor.step() {
286                Ok(_) => continue,
287                Err(err) => {
288                    log::warn!(
289                        target: "executor",
290                        "capture_trace stopped early at cycle {}: {err}",
291                        executor.cycle,
292                    );
293                    break;
294                }
295            }
296        }
297        executor.into_execution_trace()
298    }
299
300    /// Execute the given program, producing a trace
301    #[track_caller]
302    pub fn execute(
303        self,
304        package: Arc<Package>,
305        source_manager: Arc<dyn SourceManager>,
306    ) -> ExecutionTrace {
307        let mut executor = self.into_debug(package, source_manager.clone());
308        loop {
309            if executor.stopped {
310                break;
311            }
312            match executor.step() {
313                Ok(_) => {
314                    if log::log_enabled!(target: "executor", log::Level::Trace)
315                        && let (Some(op), Some(asmop)) =
316                            (executor.current_op, executor.current_asmop.as_ref())
317                    {
318                        log::trace!(target: "executor", "stack: {:?}", executor.current_stack);
319                        let source_loc = asmop
320                            .location()
321                            .and_then(|loc| location_to_source_file(loc, &source_manager));
322                        if let Some((source_file, line_start)) = source_loc {
323                            let line_number = source_file.content().line_index(line_start).number();
324                            log::trace!(target: "executor", "in {} (located at {}:{})", asmop.context_name(), source_file.deref().uri().as_str(), line_number);
325                        } else {
326                            log::trace!(target: "executor", "in {} (no source location available)", asmop.context_name());
327                        }
328                        log::trace!(target: "executor", "  executed `{op:?}` of `{}` ({} cycles)", asmop.op(), asmop.num_cycles());
329                        log::trace!(target: "executor", "  stack state: {:#?}", executor.current_stack);
330                    }
331                }
332                Err(err) => {
333                    render_execution_error(err, &executor, &source_manager);
334                }
335            }
336        }
337
338        executor.into_execution_trace()
339    }
340
341    /// Execute a program, parsing the operand stack outputs as a value of type `T`
342    pub fn execute_into<T>(self, package: Arc<Package>, source_manager: Arc<dyn SourceManager>) -> T
343    where
344        T: FromMidenRepr + PartialEq,
345    {
346        let out = self.execute(package, source_manager);
347        out.parse_result().expect("invalid result")
348    }
349}
350
351#[cfg(feature = "std")]
352fn location_to_source_file(
353    loc: &miden_debug_types::Location,
354    source_manager: &dyn SourceManager,
355) -> Option<(Arc<SourceFile>, ByteIndex)> {
356    use miden_assembly_syntax::debuginfo::SourceManagerExt;
357    let path = loc.uri().to_path()?;
358    let file = source_manager.load_file(&path).ok()?;
359    Some((file, loc.start))
360}
361
362#[cfg(not(feature = "std"))]
363fn location_to_source_file(
364    loc: &miden_debug_types::Location,
365    source_manager: &dyn SourceManager,
366) -> Option<(Arc<SourceFile>, ByteIndex)> {
367    let file = source_manager.get_by_uri(loc.uri())?;
368    Some((file, loc.start))
369}
370
371#[derive(Debug, thiserror::Error)]
372enum PrintLnError {
373    #[error("address should fit in u32")]
374    InvalidAddress,
375    #[error("string length should fit in usize")]
376    InvalidLength,
377    #[error("string length {requested} exceeds maximum {max}")]
378    LengthExceeded { requested: usize, max: usize },
379    #[error("memory is not initialized")]
380    MemoryNotInitialized,
381    #[error("failed to read memory: {0}")]
382    MemoryRead(#[from] super::trace::MemoryReadError),
383    #[error("invalid UTF-8")]
384    InvalidUtf8,
385}
386
387fn register_builtin_event_handlers(
388    host: &mut DebuggerHost<dyn SourceManager>,
389    events: Arc<RwLock<BTreeMap<RowIndex, Event>>>,
390) {
391    let println_handler = |process: &ProcessorState| -> Result<Vec<AdviceMutation>, EventError> {
392        match decode_println(process) {
393            Ok(content) => {
394                log::log!(target: "stdout", Level::Info, "{content}");
395            }
396            Err(err) => {
397                log::warn!(
398                    target: "executor",
399                    "emit.{PRINTLN_EVENT} failed at cycle {}: {err}",
400                    process.clock(),
401                );
402            }
403        }
404
405        Ok(vec![])
406    };
407
408    // Keep builtin event handlers in sync with `Event::has_builtin_handler`
409
410    host.register_event_handler(PRINTLN_EVENT, Arc::new(println_handler))
411        .expect("failed to register println event handler");
412
413    let frame_start_events = Arc::clone(&events);
414    let frame_start_handler =
415        move |process: &ProcessorState| -> Result<Vec<AdviceMutation>, EventError> {
416            frame_start_events.write().insert(process.clock(), Event::FrameStart);
417            Ok(vec![])
418        };
419    host.register_event_handler(FRAME_START_EVENT, Arc::new(frame_start_handler))
420        .expect("failed to register frame start event handler");
421
422    let frame_end_events = Arc::clone(&events);
423    let frame_end_handler =
424        move |process: &ProcessorState| -> Result<Vec<AdviceMutation>, EventError> {
425            frame_end_events.write().insert(process.clock(), Event::FrameEnd);
426            Ok(vec![])
427        };
428    host.register_event_handler(FRAME_END_EVENT, Arc::from(frame_end_handler))
429        .expect("failed to register frame end event handler");
430
431    /*
432    let assertion_events = Rc::clone(&events);
433    host.register_assert_failed_tracer(move |process, event| {
434        assertion_events.borrow_mut().insert(process.clock(), event);
435    });
436     */
437}
438
439/// Decode a [`Event::PrintLn`] event into a UTF-8 string.
440///
441/// Expects `[event_id, address, length]` on the operand stack. Reads `length` bytes from `address`
442/// in the current context's memory and returns them as a string.
443fn decode_println(process: &ProcessorState<'_>) -> Result<String, PrintLnError> {
444    let addr = u32::try_from(process.get_stack_item(1).as_canonical_u64())
445        .map_err(|_| PrintLnError::InvalidAddress)?;
446    let len = usize::try_from(process.get_stack_item(2).as_canonical_u64())
447        .map_err(|_| PrintLnError::InvalidLength)?;
448    if len > MAX_PRINTLN_BYTES {
449        return Err(PrintLnError::LengthExceeded {
450            requested: len,
451            max: MAX_PRINTLN_BYTES,
452        });
453    }
454    let ptr = NativePtr::from_ptr(addr);
455    let ctx = process.ctx();
456
457    let bytes = read_memory_bytes(ptr, len, |addr| {
458        process.get_mem_value(ctx, addr).ok_or(PrintLnError::MemoryNotInitialized)
459    })?;
460
461    String::from_utf8(bytes).map_err(|_| PrintLnError::InvalidUtf8)
462}
463
464#[cfg(feature = "std")]
465#[track_caller]
466fn render_execution_error(
467    err: ExecutionError,
468    execution_state: &DebugExecutor,
469    source_manager: &dyn SourceManager,
470) -> ! {
471    use miden_assembly_syntax::diagnostics::{
472        LabeledSpan, miette::miette, reporting::PrintDiagnostic,
473    };
474
475    let stacktrace = execution_state.callstack.stacktrace(&execution_state.recent, source_manager);
476
477    eprintln!("{stacktrace}");
478
479    if !execution_state.current_stack.is_empty() {
480        let stack = execution_state.current_stack.iter().map(|elem| elem.as_canonical_u64());
481        let stack = DisplayValues::new(stack);
482        eprintln!(
483            "\nLast Known State (at most recent instruction which succeeded):
484 | Operand Stack: [{stack}]
485 "
486        );
487
488        let mut labels = vec![];
489        if let Some(span) = stacktrace
490            .current_frame()
491            .and_then(|frame| frame.location.as_ref())
492            .map(|loc| loc.span)
493        {
494            labels.push(LabeledSpan::new_with_span(
495                None,
496                span.start().to_usize()..span.end().to_usize(),
497            ));
498        }
499        let report = miette!(
500            labels = labels,
501            "program execution failed at step {step} (cycle {cycle}): {err}",
502            step = execution_state.cycle,
503            cycle = execution_state.cycle,
504        );
505        let report = match stacktrace
506            .current_frame()
507            .and_then(|frame| frame.location.as_ref())
508            .map(|loc| loc.source_file.clone())
509        {
510            Some(source) => report.with_source_code(source),
511            None => report,
512        };
513
514        panic!("{}", PrintDiagnostic::new(report));
515    } else {
516        panic!("program execution failed at step {step}: {err}", step = execution_state.cycle);
517    }
518}
519
520#[cfg(not(feature = "std"))]
521#[track_caller]
522fn render_execution_error(
523    err: ExecutionError,
524    execution_state: &DebugExecutor,
525    source_manager: &dyn SourceManager,
526) -> ! {
527    use core::fmt::Write;
528
529    use miden_assembly_syntax::diagnostics::{
530        LabeledSpan, miette::miette, reporting::PrintDiagnostic,
531    };
532
533    let stacktrace = execution_state.callstack.stacktrace(&execution_state.recent, source_manager);
534
535    let mut buf = String::with_capacity(1024);
536    writeln!(&mut buf, "{stacktrace}").unwrap();
537
538    if !execution_state.current_stack.is_empty() {
539        let stack = execution_state.current_stack.iter().map(|elem| elem.as_canonical_u64());
540        let stack = DisplayValues::new(stack);
541        writeln!(
542            &mut buf,
543            "\nLast Known State (at most recent instruction which succeeded):
544 | Operand Stack: [{stack}]
545 "
546        )
547        .unwrap();
548
549        let mut labels = vec![];
550        if let Some(span) = stacktrace
551            .current_frame()
552            .and_then(|frame| frame.location.as_ref())
553            .map(|loc| loc.span)
554        {
555            labels.push(LabeledSpan::new_with_span(
556                None,
557                span.start().to_usize()..span.end().to_usize(),
558            ));
559        }
560        let report = miette!(
561            labels = labels,
562            "program execution failed at step {step} (cycle {cycle}): {err}",
563            step = execution_state.cycle,
564            cycle = execution_state.cycle,
565        );
566        let report = match stacktrace
567            .current_frame()
568            .and_then(|frame| frame.location.as_ref())
569            .map(|loc| loc.source_file.clone())
570        {
571            Some(source) => report.with_source_code(source),
572            None => report,
573        };
574
575        panic!("{buf}\n\n{}", PrintDiagnostic::new(report));
576    } else {
577        panic!(
578            "{buf}\n\nprogram execution failed at step {step}: {err}",
579            step = execution_state.cycle
580        );
581    }
582}
583/// Render an iterator of `T`, comma-separated
584struct DisplayValues<T>(Cell<Option<T>>);
585
586impl<T> DisplayValues<T> {
587    pub fn new(inner: T) -> Self {
588        Self(Cell::new(Some(inner)))
589    }
590}
591
592impl<T, I> fmt::Display for DisplayValues<I>
593where
594    T: fmt::Display,
595    I: Iterator<Item = T>,
596{
597    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
598        let iter = self.0.take().unwrap();
599        for (i, item) in iter.enumerate() {
600            if i == 0 {
601                write!(f, "{item}")?;
602            } else {
603                write!(f, ", {item}")?;
604            }
605        }
606        Ok(())
607    }
608}
609
610#[cfg(test)]
611mod tests {
612    use alloc::string::ToString;
613
614    use super::*;
615
616    /// One entry per `on_event` invocation, in execution order, and the recorded log replays to
617    /// an identical result without the original event handlers.
618    #[test]
619    fn records_event_mutations_and_replays_them() {
620        use std::sync::atomic::{AtomicU64, Ordering};
621
622        use miden_assembly::DefaultSourceManager;
623        use miden_core::events::EventId;
624        use miden_processor::{ProcessorState, advice::AdviceMutation, event::EventError};
625
626        struct CountingHandler {
627            calls: AtomicU64,
628        }
629
630        impl EventHandler for CountingHandler {
631            fn on_event(
632                &self,
633                _process: &ProcessorState<'_>,
634            ) -> Result<Vec<AdviceMutation>, EventError> {
635                let call = self.calls.fetch_add(1, Ordering::SeqCst);
636                Ok(vec![AdviceMutation::extend_advice_stack(
637                    [Felt::from(100u32 + call as u32)].into_iter().collect(),
638                )])
639            }
640        }
641
642        let source_manager: Arc<DefaultSourceManager> = Arc::new(DefaultSourceManager::default());
643        let event_name = "miden-debug::test::record-replay";
644        let event_id = EventId::from_name(event_name).as_u64();
645        // Each emit invokes the handler, which pushes one value onto the advice stack;
646        // adv_push moves it to the operand stack, and the sum of both values is the result.
647        let source = format!(
648            "begin push.{event_id} emit drop adv_push push.{event_id} emit drop adv_push add swap \
649             drop end"
650        );
651        let program = miden_assembly::Assembler::new(source_manager.clone())
652            .assemble_program("program", source)
653            .map(Arc::from)
654            .expect("failed to assemble test program");
655
656        let mut executor = Executor::new(Vec::new());
657        executor
658            .register_event_handler(
659                EventName::from_string(event_name.to_string()),
660                Arc::new(CountingHandler {
661                    calls: AtomicU64::new(0),
662                }),
663            )
664            .expect("failed to register event handler");
665        executor.with_event_advice_mutations_recording();
666
667        // Run to completion through the debug executor: recording is an internal detail of its
668        // host, and the log is taken from the host once execution finishes.
669        let mut debug_executor = executor.into_debug(Arc::clone(&program), source_manager.clone());
670        while !debug_executor.stopped {
671            debug_executor.step().expect("recording step failed");
672        }
673        let recorded = debug_executor.host.take_recorded_event_mutations();
674        let recorded_result: u32 =
675            debug_executor.into_execution_trace().parse_result().expect("invalid result");
676        assert_eq!(recorded_result, 201);
677
678        assert_eq!(recorded.len(), 2, "expected one recorded entry per emit");
679        for (index, batch) in recorded.iter().enumerate() {
680            match batch.as_slice() {
681                [AdviceMutation::ExtendStack { stack }] => {
682                    assert_eq!(
683                        stack.iter().copied().collect::<Vec<_>>(),
684                        [Felt::from(100u32 + index as u32)],
685                    );
686                }
687                _ => panic!("unexpected mutations recorded for event {index}"),
688            }
689        }
690
691        // Replay the recorded mutations without any event handlers registered: execution must
692        // reach the same result, proving the log is sufficient for event replay.
693        let replay_executor = Executor::new(Vec::new());
694        let mut debug_executor = replay_executor.into_debug_with_replay(
695            program,
696            source_manager,
697            Vec::new(),
698            recorded.into(),
699        );
700        while !debug_executor.stopped {
701            debug_executor.step().expect("replay step failed");
702        }
703        let replayed_result: u32 = debug_executor
704            .into_execution_trace()
705            .parse_result()
706            .expect("invalid replay result");
707        assert_eq!(replayed_result, recorded_result);
708    }
709
710    /// Replayed builtin events still reach their handlers so debugger state remains available.
711    #[test]
712    fn replay_invokes_builtin_event_handlers() {
713        use miden_assembly::DefaultSourceManager;
714
715        let source_manager: Arc<DefaultSourceManager> = Arc::new(DefaultSourceManager::default());
716        let program = miden_assembly::Assembler::new(source_manager.clone())
717            .assemble_program(
718                "program",
719                format!(
720                    r#"
721begin
722    emit.event("{FRAME_START_EVENT}")
723    emit.event("{FRAME_START_EVENT}")
724end
725"#
726                ),
727            )
728            .map(Arc::<Package>::from)
729            .expect("failed to assemble test program");
730
731        let event_replay = VecDeque::from([Vec::new(), Vec::new()]);
732        let mut debug_executor = Executor::new(Vec::new()).into_debug_with_replay(
733            program,
734            source_manager,
735            Vec::new(),
736            event_replay,
737        );
738        while !debug_executor.stopped {
739            debug_executor.step().expect("replay step failed");
740        }
741
742        assert!(
743            debug_executor.callstack.frames().len() >= 2,
744            "expected replayed frame-start events to update the debugger call stack"
745        );
746    }
747
748    /// A recorded execution serialized into a [ReplaySnapshot](crate::exec::ReplaySnapshot) and
749    /// read back from bytes replays to the same result — the offline record→replay path, end to
750    /// end, exactly what `miden-debug --replay <snapshot>` drives.
751    #[test]
752    fn replays_from_a_serialized_snapshot() {
753        use std::sync::atomic::{AtomicU64, Ordering};
754
755        use miden_assembly::DefaultSourceManager;
756        use miden_core::events::EventId;
757        use miden_processor::{ProcessorState, advice::AdviceMutation, event::EventError};
758
759        use crate::exec::ReplaySnapshot;
760
761        struct CountingHandler {
762            calls: AtomicU64,
763        }
764
765        impl EventHandler for CountingHandler {
766            fn on_event(
767                &self,
768                _process: &ProcessorState<'_>,
769            ) -> Result<Vec<AdviceMutation>, EventError> {
770                let call = self.calls.fetch_add(1, Ordering::SeqCst);
771                Ok(vec![AdviceMutation::extend_advice_stack(
772                    [Felt::from(100u32 + call as u32)].into_iter().collect(),
773                )])
774            }
775        }
776
777        let source_manager: Arc<DefaultSourceManager> = Arc::new(DefaultSourceManager::default());
778        let event_name = "miden-debug::test::snapshot-replay";
779        let event_id = EventId::from_name(event_name).as_u64();
780        let source = format!(
781            "begin push.{event_id} emit drop adv_push push.{event_id} emit drop adv_push add add \
782             end"
783        );
784        let program = miden_assembly::Assembler::new(source_manager.clone())
785            .assemble_program("program", source)
786            .map(Arc::<Package>::from)
787            .expect("failed to assemble test program");
788        let stack_inputs = StackInputs::new(&[Felt::from(7u32)]).unwrap();
789        let advice_inputs = AdviceInputs::default();
790        let options = ExecutionOptions::default();
791
792        // Record the event mutations by running to completion with a live handler.
793        let mut executor = Executor::from_config(ExecutionConfig {
794            inputs: stack_inputs,
795            advice_inputs: advice_inputs.clone(),
796            options,
797        });
798        executor
799            .register_event_handler(
800                EventName::from_string(event_name.to_string()),
801                Arc::new(CountingHandler {
802                    calls: AtomicU64::new(0),
803                }),
804            )
805            .expect("failed to register event handler");
806        executor.with_event_advice_mutations_recording();
807        let mut debug_executor = executor.into_debug(program.clone(), source_manager.clone());
808        while !debug_executor.stopped {
809            debug_executor.step().expect("recording step failed");
810        }
811        let event_log = debug_executor.host.take_recorded_event_mutations();
812        let recorded_result: u32 =
813            debug_executor.into_execution_trace().parse_result().expect("invalid result");
814
815        // Persist the recording as a snapshot and read it back from its serialized bytes.
816        let snapshot = ReplaySnapshot {
817            package: program.clone(),
818            stack_inputs,
819            advice_inputs,
820            options,
821            mast_forests: vec![LoadedMastForest::with_package_debug_info(
822                program.mast_forest().clone(),
823                program.debug_info(),
824            )],
825            event_log,
826        };
827        let restored = ReplaySnapshot::read_from_bytes(&snapshot.to_bytes())
828            .expect("snapshot failed to deserialize");
829
830        // Replay from the deserialized snapshot, with no event handlers registered.
831        let replay_executor = Executor::from_config(ExecutionConfig {
832            inputs: restored.stack_inputs,
833            advice_inputs: restored.advice_inputs,
834            options: restored.options,
835        });
836        let mut debug_executor = replay_executor.into_debug_with_replay(
837            restored.package.clone(),
838            source_manager,
839            restored.mast_forests.clone(),
840            restored.event_log.into(),
841        );
842        while !debug_executor.stopped {
843            debug_executor.step().expect("replay step failed");
844        }
845        let replayed_result: u32 = debug_executor
846            .into_execution_trace()
847            .parse_result()
848            .expect("invalid replay result");
849        assert_eq!(replayed_result, recorded_result);
850        assert_eq!(replayed_result, 208);
851    }
852}