Skip to main content

miden_debug_engine/exec/
diagnostic.rs

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