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::{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        let forest = resume_ctx.current_forest();
185        for (node_idx, node) in forest.nodes().iter().enumerate() {
186            let MastNode::Block(block) = node else {
187                continue;
188            };
189            let node_id = MastNodeId::new_unchecked(node_idx as u32);
190            for op_idx in 0..block.num_operations() as usize {
191                if forest.debug_vars_for_operation(node_id, op_idx).is_empty() {
192                    continue;
193                }
194                if forest
195                    .get_assembly_op(node_id, Some(op_idx))
196                    .is_some_and(|op| op.context_name() == procedure)
197                {
198                    return true;
199                }
200            }
201        }
202
203        false
204    }
205
206    pub fn register_trace_monitor_for(&mut self, monitor: TraceMonitor, event: super::TraceEvent) {
207        self.host.register_trace_handler(event, move |state, event| {
208            monitor.handle_event(state.clock(), event)
209        });
210    }
211
212    /// Advance the program state by one cycle.
213    ///
214    /// If the program has already reached its termination state, it returns the same result
215    /// as the previous time it was called.
216    ///
217    /// Returns the call frame exited this cycle, if any
218    pub fn step(&mut self) -> Result<Option<CallFrame>, ExecutionError> {
219        if self.stopped {
220            self.last_debug_var_count = 0;
221            return Ok(None);
222        }
223
224        let resume_ctx = match self.resume_ctx.take() {
225            Some(ctx) => ctx,
226            None => {
227                self.stopped = true;
228                self.last_debug_var_count = 0;
229                return Ok(None);
230            }
231        };
232
233        // Before step: peek continuation to determine what will execute
234        let (op, node_id, op_idx, control) = extract_current_op(&resume_ctx);
235        let asmop = node_id
236            .and_then(|nid| resume_ctx.current_forest().get_assembly_op(nid, op_idx).cloned());
237
238        // Look up debug vars from MAST forest for the current operation
239        let mut debug_var_infos: Vec<_> = if let (Some(nid), Some(idx)) = (node_id, op_idx) {
240            let forest = resume_ctx.current_forest();
241            forest
242                .debug_vars_for_operation(nid, idx)
243                .iter()
244                .filter_map(|vid| forest.debug_var(*vid).cloned())
245                .collect()
246        } else {
247            vec![]
248        };
249        let pre_step_stack = self.processor.state().get_stack_state();
250        snapshot_transient_debug_values(&mut debug_var_infos, &pre_step_stack);
251
252        // Execute one step
253        match poll_immediately(self.processor.step(&mut self.host, resume_ctx)) {
254            Ok(Some(new_ctx)) => {
255                self.resume_ctx = Some(new_ctx);
256                self.cycle += 1;
257
258                // Query processor state
259                let state = self.processor.state();
260                let ctx = state.ctx();
261                self.current_stack = state.get_stack_state();
262
263                if self.current_context != ctx {
264                    self.contexts.insert(ctx);
265                    self.current_context = ctx;
266                }
267
268                // Track operation
269                self.current_op = op;
270                self.current_asmop = asmop.clone();
271                if let Some(asmop) = asmop.as_ref() {
272                    self.current_proc = Some(Rc::from(asmop.context_name()));
273                }
274
275                if let Some(op) = op {
276                    if self.recent.len() == 5 {
277                        self.recent.pop_front();
278                    }
279                    self.recent.push_back(op);
280                }
281
282                // Update call stack
283                let step_info = StepInfo {
284                    op,
285                    control,
286                    asmop: self.current_asmop.as_ref(),
287                    clk: RowIndex::from(self.cycle as u32),
288                    ctx: self.current_context,
289                };
290                let exited = self.callstack.next(&step_info);
291
292                // Record and process debug variable events
293                let debug_var_count = debug_var_infos.len();
294                self.debug_vars
295                    .record_events(RowIndex::from(self.cycle as u32), debug_var_infos);
296                self.debug_vars.update_to_cycle(RowIndex::from(self.cycle as u32));
297                self.last_debug_var_count = debug_var_count;
298
299                Ok(exited)
300            }
301            Ok(None) => {
302                // Program completed
303                self.stopped = true;
304                self.last_debug_var_count = 0;
305                let state = self.processor.state();
306                self.current_stack = state.get_stack_state();
307
308                // Capture the final stack as StackOutputs (truncate to 16 elements)
309                let len = self.current_stack.len().min(16);
310                self.stack_outputs =
311                    StackOutputs::new(&self.current_stack[..len]).expect("invalid stack outputs");
312                Ok(None)
313            }
314            Err(err) => {
315                self.stopped = true;
316                self.last_debug_var_count = 0;
317                Err(err)
318            }
319        }
320    }
321
322    /// Advance the program state until `breakpoint` is hit.
323    ///
324    /// If the program has already reached its termination state, it returns the same result
325    /// as the previous time it was called.
326    pub fn step_until(
327        &mut self,
328        breakpoint: BreakpointType,
329        trace_monitor: Option<TraceMonitor>,
330        source_manager: &dyn SourceManager,
331    ) -> Result<(), ExecutionError> {
332        let start_cycle = self.cycle;
333        let start_clock = self.processor.state().clock();
334        let breakpoint = Breakpoint {
335            id: 0,
336            creation_cycle: start_cycle,
337            ty: breakpoint,
338        };
339        let start_asmop = self.current_asmop.clone();
340        while !self.stopped {
341            match self.step()? {
342                Some(exited)
343                    if exited.should_break_on_exit() && breakpoint.ty == BreakpointType::Finish =>
344                {
345                    return Ok(());
346                }
347                _ => (),
348            }
349
350            // Break on trace events, if monitored
351            if let BreakpointType::Trace(event_id) = breakpoint.ty
352                && let Some(trace_monitor) = trace_monitor.as_ref()
353                && trace_monitor.has_event_occurred_since(start_clock, |event| event == event_id)
354            {
355                return Ok(());
356            }
357
358            let (op, is_op_boundary, proc, loc) = {
359                let op = self.current_op;
360                let is_boundary = self.current_asmop.as_ref().map(|_info| true).unwrap_or(false);
361                let (proc, loc) = match self.callstack.current_frame() {
362                    Some(frame) => {
363                        let loc = frame
364                            .recent()
365                            .back()
366                            .and_then(|detail| detail.resolve(source_manager))
367                            .cloned();
368                        (frame.procedure(""), loc)
369                    }
370                    None => (None, None),
371                };
372                (op, is_boundary, proc, loc)
373            };
374
375            if let Some(op) = op
376                && breakpoint.should_break_for(&op)
377            {
378                return Ok(());
379            }
380
381            if is_op_boundary
382                && let Some(asmop) = self.current_asmop.as_ref()
383                && matches!(&breakpoint.ty, BreakpointType::Opcode(OperationMatcher::Asm(expected)) if expected == asmop.op())
384            {
385                return Ok(());
386            }
387
388            // Check if `breakpoint` was triggered at this cycle
389            let current_cycle = self.cycle;
390            let cycles_stepped = current_cycle - start_cycle;
391            if let Some(n) = breakpoint.cycles_to_skip(current_cycle)
392                && cycles_stepped >= n
393            {
394                return Ok(());
395            }
396
397            if cycles_stepped > 0
398                && is_op_boundary
399                && matches!(&breakpoint.ty, BreakpointType::Next)
400                && self.current_asmop != start_asmop
401            {
402                return Ok(());
403            }
404
405            if let Some(loc) = loc.as_ref()
406                && breakpoint.should_break_at(loc)
407            {
408                return Ok(());
409            }
410
411            if let Some(proc) = proc.as_deref()
412                && breakpoint.should_break_in(proc)
413            {
414                return Ok(());
415            }
416        }
417
418        Ok(())
419    }
420
421    /// Consume the [DebugExecutor], converting it into an [ExecutionTrace] at the current cycle.
422    pub fn into_execution_trace(self) -> ExecutionTrace {
423        ExecutionTrace {
424            processor: self.processor,
425            outputs: self.stack_outputs,
426        }
427    }
428}
429
430#[cfg(test)]
431mod tests {
432    use std::sync::Arc;
433
434    use miden_assembly::DefaultSourceManager;
435
436    use super::*;
437    use crate::exec::Executor;
438
439    #[test]
440    fn callstack_tracks_nested_frame_trace_events() {
441        let source_manager = Arc::new(DefaultSourceManager::default());
442        let program = miden_assembly::Assembler::new(source_manager.clone())
443            .assemble_program(
444                r#"
445proc inner
446    nop
447end
448
449proc outer
450    trace.240
451    nop
452    exec.inner
453    trace.252
454    nop
455end
456
457begin
458    trace.240
459    nop
460    exec.outer
461    trace.252
462    nop
463end
464"#,
465            )
466            .unwrap();
467
468        let mut executor = Executor::new(Vec::<Felt>::new()).into_debug(&program, source_manager);
469        let mut max_depth = 0;
470        let mut saw_inner = false;
471        let mut snapshots = Vec::new();
472
473        for _ in 0..64 {
474            executor.step().unwrap();
475            let frames = executor.callstack.frames();
476            max_depth = max_depth.max(frames.len());
477            snapshots.push(
478                frames
479                    .iter()
480                    .map(|frame| {
481                        frame
482                            .procedure("")
483                            .map(|name| name.to_string())
484                            .unwrap_or_else(|| "<unknown>".to_string())
485                    })
486                    .collect::<Vec<_>>(),
487            );
488            saw_inner |= frames.len() >= 3
489                && frames
490                    .last()
491                    .and_then(|frame| frame.procedure(""))
492                    .is_some_and(|name| name.contains("inner"));
493
494            if saw_inner || executor.stopped {
495                break;
496            }
497        }
498
499        assert!(
500            max_depth >= 3,
501            "expected nested main -> outer -> inner frames, max depth was {max_depth}"
502        );
503        assert!(
504            saw_inner,
505            "expected innermost frame to resolve to inner; snapshots: {snapshots:?}"
506        );
507    }
508}