Skip to main content

miden_debug_engine/exec/
diagnostic.rs

1use std::{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    fn report_diagnostics(&self, err: &ExecutionError) {
42        eprintln!("\n=== Transaction Execution Failed ===");
43        eprintln!("Error: {err}");
44        eprintln!("Last known cycle: {}", self.last_cycle);
45        eprintln!("Call depth at failure: {}", self.call_depth);
46
47        if !self.last_stack_state.is_empty() {
48            let stack_display: Vec<_> =
49                self.last_stack_state.iter().take(16).map(|f| f.as_canonical_u64()).collect();
50            eprintln!("Last known stack state (top 16): {stack_display:?}");
51        }
52
53        eprintln!("====================================\n");
54    }
55
56    fn capture_state(&mut self, process: &ProcessorState<'_>) {
57        self.last_stack_state = process.get_stack_state();
58        self.last_cycle = process.clock();
59    }
60}
61
62impl<H: Host> BaseHost for DiagnosticHostWrapper<'_, H> {
63    fn get_label_and_source_file(
64        &self,
65        location: &miden_debug_types::Location,
66    ) -> (miden_debug_types::SourceSpan, Option<Arc<miden_debug_types::SourceFile>>) {
67        self.inner.get_label_and_source_file(location)
68    }
69
70    fn resolve_event(
71        &self,
72        event_id: miden_core::events::EventId,
73    ) -> Option<&miden_core::events::EventName> {
74        self.inner.resolve_event(event_id)
75    }
76}
77
78impl<H: Host> Host for DiagnosticHostWrapper<'_, H> {
79    fn get_mast_forest(
80        &self,
81        node_digest: &Word,
82    ) -> impl FutureMaybeSend<Option<LoadedMastForest>> {
83        self.inner.get_mast_forest(node_digest)
84    }
85
86    fn on_event(
87        &mut self,
88        process: &ProcessorState<'_>,
89    ) -> impl FutureMaybeSend<Result<Vec<AdviceMutation>, EventError>> {
90        self.capture_state(process);
91        let event_id = EventId::from_felt(process.get_stack_item(0));
92        match crate::Event::from(event_id) {
93            crate::Event::FrameStart => self.call_depth += 1,
94            crate::Event::FrameEnd => self.call_depth = self.call_depth.saturating_sub(1),
95            _ => (),
96        }
97        self.inner.on_event(process)
98    }
99}
100
101// DIAGNOSTIC EXECUTOR
102// ================================================================================================
103
104/// A [`ProgramExecutor`] that wraps [`FastProcessor`] with diagnostic capabilities.
105///
106/// When execution fails, it captures and reports rich diagnostic information including:
107/// - The clock cycle at failure
108/// - The call depth (from trace events)
109/// - The last known operand stack state
110///
111/// This executor is intended for use with [`TransactionExecutor`] to provide better error
112/// diagnostics when transactions fail during testing or development.
113///
114/// # Usage
115///
116/// ```ignore
117/// use miden_tx::TransactionExecutor;
118/// use miden_debug::DiagnosticExecutor;
119///
120/// let executor = TransactionExecutor::new(&store)
121///     .with_program_executor::<DiagnosticExecutor>()
122///     .execute_transaction(account_id, block_num, notes, tx_args)
123///     .await;
124/// ```
125pub struct DiagnosticExecutor {
126    stack_inputs: StackInputs,
127    advice_inputs: AdviceInputs,
128    options: ExecutionOptions,
129}
130
131impl DiagnosticExecutor {
132    pub fn new(
133        stack_inputs: StackInputs,
134        advice_inputs: AdviceInputs,
135        options: ExecutionOptions,
136    ) -> Self {
137        DiagnosticExecutor {
138            stack_inputs,
139            advice_inputs,
140            options,
141        }
142    }
143
144    pub fn execute_async<H: Host + Send>(
145        self,
146        program: &Program,
147        host: &mut H,
148    ) -> impl FutureMaybeSend<Result<ExecutionOutput, ExecutionError>> {
149        async move {
150            // Enable debugging and tracing for richer diagnostics.
151            let processor = FastProcessor::new_with_options(
152                self.stack_inputs,
153                self.advice_inputs,
154                self.options,
155            )
156            .expect("advice inputs should fit advice map limits");
157
158            let mut wrapper = DiagnosticHostWrapper::new(host);
159
160            match processor.execute(program, &mut wrapper).await {
161                Ok(output) => Ok(output),
162                Err(err) => {
163                    wrapper.report_diagnostics(&err);
164                    Err(err)
165                }
166            }
167        }
168    }
169}