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,
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
82impl<H: Host> Host for DiagnosticHostWrapper<'_, H> {
83    fn get_mast_forest(
84        &self,
85        node_digest: &Word,
86    ) -> impl FutureMaybeSend<Option<LoadedMastForest>> {
87        self.inner.get_mast_forest(node_digest)
88    }
89
90    fn on_event(
91        &mut self,
92        process: &ProcessorState<'_>,
93    ) -> impl FutureMaybeSend<Result<Vec<AdviceMutation>, EventError>> {
94        self.capture_state(process);
95        let event_id = EventId::from_felt(process.get_stack_item(0));
96        match crate::Event::from(event_id) {
97            crate::Event::FrameStart => self.call_depth += 1,
98            crate::Event::FrameEnd => self.call_depth = self.call_depth.saturating_sub(1),
99            _ => (),
100        }
101        self.inner.on_event(process)
102    }
103}
104
105// DIAGNOSTIC EXECUTOR
106// ================================================================================================
107
108/// A [`ProgramExecutor`] that wraps [`FastProcessor`] with diagnostic capabilities.
109///
110/// When execution fails, it captures and reports rich diagnostic information including:
111/// - The clock cycle at failure
112/// - The call depth (from trace events)
113/// - The last known operand stack state
114///
115/// This executor is intended for use with [`TransactionExecutor`] to provide better error
116/// diagnostics when transactions fail during testing or development.
117///
118/// # Usage
119///
120/// ```ignore
121/// use miden_tx::TransactionExecutor;
122/// use miden_debug::DiagnosticExecutor;
123///
124/// let executor = TransactionExecutor::new(&store)
125///     .with_program_executor::<DiagnosticExecutor>()
126///     .execute_transaction(account_id, block_num, notes, tx_args)
127///     .await;
128/// ```
129pub struct DiagnosticExecutor {
130    stack_inputs: StackInputs,
131    advice_inputs: AdviceInputs,
132    options: ExecutionOptions,
133}
134
135impl DiagnosticExecutor {
136    pub fn new(
137        stack_inputs: StackInputs,
138        advice_inputs: AdviceInputs,
139        options: ExecutionOptions,
140    ) -> Self {
141        DiagnosticExecutor {
142            stack_inputs,
143            advice_inputs,
144            options,
145        }
146    }
147
148    pub fn execute_async<H: Host + Send>(
149        self,
150        program: &Program,
151        host: &mut H,
152    ) -> impl FutureMaybeSend<Result<ExecutionOutput, ExecutionError>> {
153        async move {
154            // Enable debugging and tracing for richer diagnostics.
155            let processor = FastProcessor::new_with_options(
156                self.stack_inputs,
157                self.advice_inputs,
158                self.options,
159            )
160            .expect("advice inputs should fit advice map limits");
161
162            let mut wrapper = DiagnosticHostWrapper::new(host);
163
164            match processor.execute(program, &mut wrapper).await {
165                Ok(output) => Ok(output),
166                Err(err) => {
167                    wrapper.report_diagnostics(&err);
168                    Err(err)
169                }
170            }
171        }
172    }
173}