Skip to main content

miden_debug_engine/exec/
state.rs

1use std::{
2    collections::{BTreeSet, VecDeque},
3    rc::Rc,
4};
5
6use miden_assembly::SourceManager;
7use miden_core::{
8    mast::{MastForest, MastNode, MastNodeId},
9    operations::AssemblyOp,
10};
11use miden_processor::{
12    ContextId, Continuation, ExecutionError, FastProcessor, Felt, ResumeContext, StackOutputs,
13    operation::Operation, trace::RowIndex,
14};
15
16use super::{DebuggerHost, ExecutionTrace, TraceMonitor};
17use crate::{
18    Breakpoint, BreakpointType, OperationMatcher,
19    debug::{
20        CallFrame, CallStack, ControlFlowOp, DebugVarTracker, StepInfo,
21        snapshot_transient_debug_values,
22    },
23};
24
25/// Resolve a future that is expected to complete immediately (synchronous host methods).
26///
27/// We use a noop waker because our Host methods all return `std::future::ready(...)`.
28/// This avoids calling `step_sync()` which would create its own tokio runtime and
29/// panic inside the TUI's existing tokio current-thread runtime.
30/// TODO: Revisit this (djole).
31fn poll_immediately<T>(fut: impl std::future::Future<Output = T>) -> T {
32    let waker = std::task::Waker::noop();
33    let mut cx = std::task::Context::from_waker(waker);
34    let mut fut = std::pin::pin!(fut);
35    match fut.as_mut().poll(&mut cx) {
36        std::task::Poll::Ready(val) => val,
37        std::task::Poll::Pending => panic!("future was expected to complete immediately"),
38    }
39}
40
41/// A special version of [crate::Executor] which provides finer-grained control over execution,
42/// and captures a ton of information about the program being executed, so as to make it possible
43/// to introspect everything about the program and the state of the VM at a given cycle.
44///
45/// This is used by the debugger to execute programs, and provide all of the functionality made
46/// available by the TUI.
47pub struct DebugExecutor {
48    /// The underlying [FastProcessor] being driven
49    pub processor: FastProcessor,
50    /// The host providing debugging callbacks
51    pub host: DebuggerHost<dyn miden_assembly::SourceManager>,
52    /// The resume context for the next step (None if program has finished)
53    pub resume_ctx: Option<ResumeContext>,
54
55    // State from last step (replaces VmState fields)
56    /// The current operand stack state
57    pub current_stack: Vec<Felt>,
58    /// The operation that was just executed
59    pub current_op: Option<Operation>,
60    /// The assembly-level operation info for the current op
61    pub current_asmop: Option<AssemblyOp>,
62
63    /// The final outcome of the program being executed
64    pub stack_outputs: StackOutputs,
65    /// The set of contexts allocated during execution so far
66    pub contexts: BTreeSet<ContextId>,
67    /// The root context
68    pub root_context: ContextId,
69    /// The current context at `cycle`
70    pub current_context: ContextId,
71    /// The current call stack
72    pub callstack: CallStack,
73    /// The most recent live procedure name observed from assembly operation metadata.
74    pub current_proc: Option<Rc<str>>,
75    /// Debug variable tracker for source-level variable inspection
76    pub debug_vars: DebugVarTracker,
77    /// Number of debug variable location records observed during the most recent step.
78    pub last_debug_var_count: usize,
79    /// A sliding window of the last 5 operations successfully executed by the VM
80    pub recent: VecDeque<Operation>,
81    /// The current clock cycle
82    pub cycle: usize,
83    /// Whether or not execution has terminated
84    pub stopped: bool,
85}
86
87impl super::query::DebugQuery for DebugExecutor {
88    #[inline]
89    fn state(&self) -> miden_processor::ProcessorState<'_> {
90        self.processor.state()
91    }
92
93    fn current_context(&self) -> ContextId {
94        self.current_context
95    }
96
97    fn current_clock(&self) -> RowIndex {
98        self.processor.state().clock()
99    }
100}
101
102impl DebugExecutor {
103    /// Get the current operand stack as a slice - the top of the stack is the last item in the slice
104    ///
105    /// This returns the entire stack, not just the top 16 elements
106    pub fn stack(&self) -> &[Felt] {
107        self.processor.stack()
108    }
109}
110
111/// Extract the current operation and assembly info from the continuation stack
112/// before a step is executed. This lets us know what operation will run next.
113pub(crate) fn extract_current_op(
114    ctx: &ResumeContext,
115) -> (Option<Operation>, Option<MastNodeId>, Option<usize>, Option<ControlFlowOp>) {
116    let forest = ctx.current_forest();
117    for cont in ctx.continuation_stack().iter_continuations_for_next_clock() {
118        match cont {
119            Continuation::ResumeBasicBlock {
120                node_id,
121                batch_index,
122                op_idx_in_batch,
123            } => {
124                let node = &forest[*node_id];
125                if let MastNode::Block(block) = node {
126                    // Compute global op index within the basic block
127                    let mut global_idx = 0;
128                    for batch in &block.op_batches()[..*batch_index] {
129                        global_idx += batch.ops().len();
130                    }
131                    global_idx += op_idx_in_batch;
132                    let op = block.op_batches()[*batch_index].ops().get(*op_idx_in_batch).copied();
133                    return (op, Some(*node_id), Some(global_idx), None);
134                }
135            }
136            Continuation::Respan {
137                node_id,
138                batch_index,
139            } => {
140                let node = &forest[*node_id];
141                if let MastNode::Block(block) = node {
142                    let mut global_idx = 0;
143                    for batch in &block.op_batches()[..*batch_index] {
144                        global_idx += batch.ops().len();
145                    }
146                    return (None, Some(*node_id), Some(global_idx), Some(ControlFlowOp::Respan));
147                }
148            }
149            Continuation::StartNode(node_id) => {
150                let control = match &forest[*node_id] {
151                    MastNode::Block(_) => Some(ControlFlowOp::Span),
152                    MastNode::Join(_) => Some(ControlFlowOp::Join),
153                    MastNode::Split(_) => Some(ControlFlowOp::Split),
154                    _ => None,
155                };
156                return (None, Some(*node_id), None, control);
157            }
158            Continuation::FinishBasicBlock(_)
159            | Continuation::FinishJoin(_)
160            | Continuation::FinishSplit(_)
161            | Continuation::FinishLoop { .. }
162            | Continuation::FinishCall(_)
163            | Continuation::FinishDyn(_)
164            | Continuation::FinishExternal(_) => {
165                return (None, None, None, Some(ControlFlowOp::End));
166            }
167            other if other.increments_clk() => {
168                return (None, None, None, None);
169            }
170            _ => continue,
171        }
172    }
173    (None, None, None, None)
174}
175
176impl DebugExecutor {
177    /// Returns true if the current program forest has debug-variable locations associated with
178    /// `procedure`.
179    pub fn procedure_has_debug_vars(&self, procedure: &str) -> bool {
180        let Some(resume_ctx) = self.resume_ctx.as_ref() else {
181            return false;
182        };
183
184        forest_procedure_has_debug_vars(resume_ctx.current_forest(), procedure)
185    }
186
187    pub fn register_trace_monitor_for(&mut self, monitor: TraceMonitor, event: super::TraceEvent) {
188        self.host.register_trace_handler(event, move |state, event| {
189            monitor.handle_event(state.clock(), event)
190        });
191    }
192
193    /// Advance the program state by one cycle.
194    ///
195    /// If the program has already reached its termination state, it returns the same result
196    /// as the previous time it was called.
197    ///
198    /// Returns the call frame exited this cycle, if any
199    pub fn step(&mut self) -> Result<Option<CallFrame>, ExecutionError> {
200        if self.stopped {
201            self.last_debug_var_count = 0;
202            return Ok(None);
203        }
204
205        let resume_ctx = match self.resume_ctx.take() {
206            Some(ctx) => ctx,
207            None => {
208                self.stopped = true;
209                self.last_debug_var_count = 0;
210                return Ok(None);
211            }
212        };
213
214        // Before step: peek continuation to determine what will execute
215        let (op, node_id, op_idx, control) = extract_current_op(&resume_ctx);
216        let asmop = node_id
217            .and_then(|nid| resume_ctx.current_forest().get_assembly_op(nid, op_idx).cloned());
218
219        // Look up debug vars from MAST forest for the current operation
220        let mut debug_var_infos: Vec<_> = if let (Some(nid), Some(idx)) = (node_id, op_idx) {
221            let forest = resume_ctx.current_forest();
222            forest
223                .debug_vars_for_operation(nid, idx)
224                .iter()
225                .filter_map(|vid| forest.debug_var(*vid).cloned())
226                .collect()
227        } else {
228            vec![]
229        };
230        let pre_step_stack = self.processor.state().get_stack_state();
231        snapshot_transient_debug_values(&mut debug_var_infos, &pre_step_stack);
232
233        // Execute one step
234        match poll_immediately(self.processor.step(&mut self.host, resume_ctx)) {
235            Ok(Some(new_ctx)) => {
236                self.resume_ctx = Some(new_ctx);
237                self.cycle += 1;
238
239                // Query processor state
240                let state = self.processor.state();
241                let ctx = state.ctx();
242                self.current_stack = state.get_stack_state();
243
244                if self.current_context != ctx {
245                    self.contexts.insert(ctx);
246                    self.current_context = ctx;
247                }
248
249                // Track operation
250                self.current_op = op;
251                self.current_asmop = asmop.clone();
252                if let Some(asmop) = asmop.as_ref() {
253                    self.current_proc = Some(Rc::from(asmop.context_name()));
254                }
255
256                if let Some(op) = op {
257                    if self.recent.len() == 5 {
258                        self.recent.pop_front();
259                    }
260                    self.recent.push_back(op);
261                }
262
263                // Update call stack
264                let step_info = StepInfo {
265                    op,
266                    control,
267                    asmop: self.current_asmop.as_ref(),
268                    clk: RowIndex::from(self.cycle as u32),
269                    ctx: self.current_context,
270                };
271                let exited = self.callstack.next(&step_info);
272
273                // Record and process debug variable events
274                let debug_var_count = debug_var_infos.len();
275                self.debug_vars
276                    .record_events(RowIndex::from(self.cycle as u32), debug_var_infos);
277                self.debug_vars.update_to_cycle(RowIndex::from(self.cycle as u32));
278                self.last_debug_var_count = debug_var_count;
279
280                Ok(exited)
281            }
282            Ok(None) => {
283                // Program completed
284                self.stopped = true;
285                self.last_debug_var_count = 0;
286                let state = self.processor.state();
287                self.current_stack = state.get_stack_state();
288
289                // Capture the final stack as StackOutputs (truncate to 16 elements)
290                let len = self.current_stack.len().min(16);
291                self.stack_outputs =
292                    StackOutputs::new(&self.current_stack[..len]).expect("invalid stack outputs");
293                Ok(None)
294            }
295            Err(err) => {
296                self.stopped = true;
297                self.last_debug_var_count = 0;
298                Err(err)
299            }
300        }
301    }
302
303    /// Advance the program state until `breakpoint` is hit.
304    ///
305    /// If the program has already reached its termination state, it returns the same result
306    /// as the previous time it was called.
307    pub fn step_until(
308        &mut self,
309        breakpoint: BreakpointType,
310        trace_monitor: Option<TraceMonitor>,
311        source_manager: &dyn SourceManager,
312    ) -> Result<(), ExecutionError> {
313        let start_cycle = self.cycle;
314        let start_clock = self.processor.state().clock();
315        let breakpoint = Breakpoint {
316            id: 0,
317            creation_cycle: start_cycle,
318            ty: breakpoint,
319        };
320        let start_asmop = self.current_asmop.clone();
321        while !self.stopped {
322            match self.step()? {
323                Some(exited)
324                    if exited.should_break_on_exit() && breakpoint.ty == BreakpointType::Finish =>
325                {
326                    return Ok(());
327                }
328                _ => (),
329            }
330
331            // Break on trace events, if monitored
332            if let BreakpointType::Trace(event_id) = breakpoint.ty
333                && let Some(trace_monitor) = trace_monitor.as_ref()
334                && trace_monitor.has_event_occurred_since(start_clock, |event| event == event_id)
335            {
336                return Ok(());
337            }
338
339            let (op, is_op_boundary, proc, loc) = {
340                let op = self.current_op;
341                let is_boundary = self.current_asmop.as_ref().map(|_info| true).unwrap_or(false);
342                let (proc, loc) = match self.callstack.current_frame() {
343                    Some(frame) => {
344                        let loc = frame
345                            .recent()
346                            .back()
347                            .and_then(|detail| detail.resolve(source_manager))
348                            .cloned();
349                        (frame.procedure(""), loc)
350                    }
351                    None => (None, None),
352                };
353                (op, is_boundary, proc, loc)
354            };
355
356            if let Some(op) = op
357                && breakpoint.should_break_for(&op)
358            {
359                return Ok(());
360            }
361
362            if is_op_boundary
363                && let Some(asmop) = self.current_asmop.as_ref()
364                && matches!(&breakpoint.ty, BreakpointType::Opcode(OperationMatcher::Asm(expected)) if expected == asmop.op())
365            {
366                return Ok(());
367            }
368
369            // Check if `breakpoint` was triggered at this cycle
370            let current_cycle = self.cycle;
371            let cycles_stepped = current_cycle - start_cycle;
372            if let Some(n) = breakpoint.cycles_to_skip(current_cycle)
373                && cycles_stepped >= n
374            {
375                return Ok(());
376            }
377
378            if cycles_stepped > 0
379                && is_op_boundary
380                && matches!(&breakpoint.ty, BreakpointType::Next)
381                && self.current_asmop != start_asmop
382            {
383                return Ok(());
384            }
385
386            if let Some(loc) = loc.as_ref()
387                && breakpoint.should_break_at(loc)
388            {
389                return Ok(());
390            }
391
392            if let Some(proc) = proc.as_deref()
393                && breakpoint.should_break_in(proc)
394            {
395                return Ok(());
396            }
397        }
398
399        Ok(())
400    }
401
402    /// Consume the [DebugExecutor], converting it into an [ExecutionTrace] at the current cycle.
403    pub fn into_execution_trace(self) -> ExecutionTrace {
404        ExecutionTrace {
405            processor: self.processor,
406            outputs: self.stack_outputs,
407        }
408    }
409}
410
411pub(crate) fn forest_procedure_has_debug_vars(forest: &MastForest, procedure: &str) -> bool {
412    forest_operation_matches(forest, |node_id, op_idx, asmop| {
413        asmop.context_name() == procedure
414            && !forest.debug_vars_for_operation(node_id, op_idx).is_empty()
415    })
416}
417
418fn forest_operation_matches(
419    forest: &MastForest,
420    mut matches: impl FnMut(MastNodeId, usize, &AssemblyOp) -> bool,
421) -> bool {
422    for (node_idx, node) in forest.nodes().iter().enumerate() {
423        let MastNode::Block(block) = node else {
424            continue;
425        };
426        let node_id = MastNodeId::new_unchecked(node_idx as u32);
427        for op_idx in 0..block.num_operations() as usize {
428            if forest
429                .get_assembly_op(node_id, Some(op_idx))
430                .is_some_and(|asmop| matches(node_id, op_idx, asmop))
431            {
432                return true;
433            }
434        }
435    }
436
437    false
438}
439
440#[cfg(test)]
441mod tests {
442    use std::sync::Arc;
443
444    use miden_assembly::DefaultSourceManager;
445
446    use super::*;
447    use crate::exec::Executor;
448
449    #[test]
450    fn callstack_tracks_nested_frame_trace_events() {
451        let source_manager = Arc::new(DefaultSourceManager::default());
452        let program = miden_assembly::Assembler::new(source_manager.clone())
453            .assemble_program(
454                r#"
455proc inner
456    nop
457end
458
459proc outer
460    trace.240
461    nop
462    exec.inner
463    trace.252
464    nop
465end
466
467begin
468    trace.240
469    nop
470    exec.outer
471    trace.252
472    nop
473end
474"#,
475            )
476            .unwrap();
477
478        let mut executor = Executor::new(Vec::<Felt>::new()).into_debug(&program, source_manager);
479        let mut max_depth = 0;
480        let mut saw_inner = false;
481        let mut snapshots = Vec::new();
482
483        for _ in 0..64 {
484            executor.step().unwrap();
485            let frames = executor.callstack.frames();
486            max_depth = max_depth.max(frames.len());
487            snapshots.push(
488                frames
489                    .iter()
490                    .map(|frame| {
491                        frame
492                            .procedure("")
493                            .map(|name| name.to_string())
494                            .unwrap_or_else(|| "<unknown>".to_string())
495                    })
496                    .collect::<Vec<_>>(),
497            );
498            saw_inner |= frames.len() >= 3
499                && frames
500                    .last()
501                    .and_then(|frame| frame.procedure(""))
502                    .is_some_and(|name| name.contains("inner"));
503
504            if saw_inner || executor.stopped {
505                break;
506            }
507        }
508
509        assert!(
510            max_depth >= 3,
511            "expected nested main -> outer -> inner frames, max depth was {max_depth}"
512        );
513        assert!(
514            saw_inner,
515            "expected innermost frame to resolve to inner; snapshots: {snapshots:?}"
516        );
517    }
518}