Skip to main content

miden_debug_engine/exec/
state.rs

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