Skip to main content

miden_debug_engine/exec/
diagnostic.rs

1use std::{sync::Arc, vec::Vec};
2
3use miden_core::{Word, operations::DebugOptions, program::Program};
4use miden_processor::{
5    BaseHost, ExecutionError, ExecutionOptions, ExecutionOutput, FastProcessor, Felt,
6    FutureMaybeSend, Host, ProcessorState, StackInputs, TraceError,
7    advice::{AdviceInputs, AdviceMutation},
8    event::EventError,
9    mast::MastForest,
10    trace::RowIndex,
11};
12
13use super::TraceEvent;
14
15// DIAGNOSTIC HOST WRAPPER
16// ================================================================================================
17
18/// A host wrapper that intercepts trace events to track call frames and processor state,
19/// while delegating all other operations to the inner host.
20///
21/// This enables capturing diagnostic information during transaction execution (or any program
22/// execution) without modifying the inner host.
23struct DiagnosticHostWrapper<'a, H: Host> {
24    inner: &'a mut H,
25    /// Call depth tracked from FrameStart/FrameEnd trace events.
26    call_depth: usize,
27    /// Stack state captured at the last trace or event callback.
28    last_stack_state: Vec<Felt>,
29    /// Clock cycle at the last trace or event callback.
30    last_cycle: RowIndex,
31}
32
33impl<'a, H: Host> DiagnosticHostWrapper<'a, H> {
34    fn new(inner: &'a mut H) -> Self {
35        Self {
36            inner,
37            call_depth: 0,
38            last_stack_state: Vec::new(),
39            last_cycle: RowIndex::from(0u32),
40        }
41    }
42
43    /// Report diagnostic information when an execution error occurs.
44    fn report_diagnostics(&self, err: &ExecutionError) {
45        eprintln!("\n=== Transaction Execution Failed ===");
46        eprintln!("Error: {err}");
47        eprintln!("Last known cycle: {}", self.last_cycle);
48        eprintln!("Call depth at failure: {}", self.call_depth);
49
50        if !self.last_stack_state.is_empty() {
51            let stack_display: Vec<_> =
52                self.last_stack_state.iter().take(16).map(|f| f.as_canonical_u64()).collect();
53            eprintln!("Last known stack state (top 16): {stack_display:?}");
54        }
55
56        eprintln!("====================================\n");
57    }
58
59    fn capture_state(&mut self, process: &ProcessorState<'_>) {
60        self.last_stack_state = process.get_stack_state();
61        self.last_cycle = process.clock();
62    }
63}
64
65impl<H: Host> BaseHost for DiagnosticHostWrapper<'_, H> {
66    fn get_label_and_source_file(
67        &self,
68        location: &miden_debug_types::Location,
69    ) -> (miden_debug_types::SourceSpan, Option<Arc<miden_debug_types::SourceFile>>) {
70        self.inner.get_label_and_source_file(location)
71    }
72
73    fn on_debug(
74        &mut self,
75        process: &ProcessorState<'_>,
76        options: &DebugOptions,
77    ) -> Result<(), miden_processor::DebugError> {
78        self.inner.on_debug(process, options)
79    }
80
81    fn on_trace(&mut self, process: &ProcessorState<'_>, trace_id: u32) -> Result<(), TraceError> {
82        self.capture_state(process);
83
84        let event = TraceEvent::from(trace_id);
85        match event {
86            TraceEvent::FrameStart => self.call_depth += 1,
87            TraceEvent::FrameEnd => self.call_depth = self.call_depth.saturating_sub(1),
88            _ => {}
89        }
90
91        self.inner.on_trace(process, trace_id)
92    }
93
94    fn resolve_event(
95        &self,
96        event_id: miden_core::events::EventId,
97    ) -> Option<&miden_core::events::EventName> {
98        self.inner.resolve_event(event_id)
99    }
100}
101
102impl<H: Host> Host for DiagnosticHostWrapper<'_, H> {
103    fn get_mast_forest(&self, node_digest: &Word) -> impl FutureMaybeSend<Option<Arc<MastForest>>> {
104        self.inner.get_mast_forest(node_digest)
105    }
106
107    fn on_event(
108        &mut self,
109        process: &ProcessorState<'_>,
110    ) -> impl FutureMaybeSend<Result<Vec<AdviceMutation>, EventError>> {
111        self.capture_state(process);
112        self.inner.on_event(process)
113    }
114}
115
116// DIAGNOSTIC EXECUTOR
117// ================================================================================================
118
119/// A [`ProgramExecutor`] that wraps [`FastProcessor`] with diagnostic capabilities.
120///
121/// When execution fails, it captures and reports rich diagnostic information including:
122/// - The clock cycle at failure
123/// - The call depth (from trace events)
124/// - The last known operand stack state
125///
126/// This executor is intended for use with [`TransactionExecutor`] to provide better error
127/// diagnostics when transactions fail during testing or development.
128///
129/// # Usage
130///
131/// ```ignore
132/// use miden_tx::TransactionExecutor;
133/// use miden_debug::DiagnosticExecutor;
134///
135/// let executor = TransactionExecutor::new(&store)
136///     .with_program_executor::<DiagnosticExecutor>()
137///     .execute_transaction(account_id, block_num, notes, tx_args)
138///     .await;
139/// ```
140pub struct DiagnosticExecutor {
141    stack_inputs: StackInputs,
142    advice_inputs: AdviceInputs,
143    options: ExecutionOptions,
144}
145
146impl DiagnosticExecutor {
147    pub fn new(
148        stack_inputs: StackInputs,
149        advice_inputs: AdviceInputs,
150        options: ExecutionOptions,
151    ) -> Self {
152        DiagnosticExecutor {
153            stack_inputs,
154            advice_inputs,
155            options,
156        }
157    }
158
159    pub fn execute_async<H: Host + Send>(
160        self,
161        program: &Program,
162        host: &mut H,
163    ) -> impl FutureMaybeSend<Result<ExecutionOutput, ExecutionError>> {
164        async move {
165            // Enable debugging and tracing for richer diagnostics.
166            let options = self.options.with_debugging(true).with_tracing(true);
167            let processor =
168                FastProcessor::new_with_options(self.stack_inputs, self.advice_inputs, options)
169                    .expect("advice inputs should fit advice map limits");
170
171            let mut wrapper = DiagnosticHostWrapper::new(host);
172
173            match processor.execute(program, &mut wrapper).await {
174                Ok(output) => Ok(output),
175                Err(err) => {
176                    wrapper.report_diagnostics(&err);
177                    Err(err)
178                }
179            }
180        }
181    }
182}