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