Skip to main content

miden_debug_engine/exec/
state.rs

1use alloc::{
2    collections::{BTreeSet, VecDeque},
3    sync::Arc,
4    vec::Vec,
5};
6
7use miden_assembly_syntax::{ast::DebugVarLocation, debuginfo::SourceManager};
8use miden_core::{
9    mast::{MastNode, MastNodeId},
10    operations::AssemblyOp,
11};
12use miden_mast_package::debug_info::{DebugSourceNodeId, PackageDebugInfo};
13use miden_processor::{
14    ContextId, Continuation, ExecutionError, FastProcessor, Felt, ResumeContext, StackOutputs,
15    operation::Operation, trace::RowIndex,
16};
17
18use super::{DebuggerHost, ExecutionTrace};
19use crate::{
20    Breakpoint, BreakpointType, OperationMatcher,
21    debug::{
22        CallFrame, CallStack, ControlFlowOp, DebugVarTracker, StepInfo, inline_frames_for_operation,
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_syntax::debuginfo::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
214/// Wait for entry declarations in the current straight-line source occurrence, but never run
215/// through a branch or a callee just because a later location exists somewhere in the function.
216/// Only declarations observed this cycle count; kills and stale caller variables do not.
217pub(crate) fn should_wait_for_entry_variables(
218    resume_ctx: &ResumeContext,
219    procedure: &str,
220    variables: &DebugVarTracker,
221    cycle: usize,
222) -> bool {
223    if variables
224        .current_variables()
225        .any(|variable| variable.clk == RowIndex::from(cycle as u32))
226    {
227        return false;
228    }
229    let Some(debug_info) = resume_ctx.debug_info() else {
230        return false;
231    };
232    let next = extract_current_op(resume_ctx);
233    let Some(source_node) = next.source_node_id.map(|node| &debug_info[node]) else {
234        return false;
235    };
236    let Some(op_idx) = next.op_idx else {
237        return false;
238    };
239    source_node.debug_vars.iter().any(|variable| {
240        variable.op_idx as usize >= op_idx
241            && !matches!(variable.value_location, DebugVarLocation::Unavailable)
242            && source_node.asm_op_for_operation(variable.op_idx).is_some_and(|operation| {
243                debug_info[operation.context_name_idx].as_ref() == procedure
244            })
245    })
246}
247
248impl DebugExecutor {
249    /// Returns whether a function breakpoint should wait for pending entry-block declarations.
250    pub fn should_wait_for_entry_variables(&self, procedure: &str) -> bool {
251        self.resume_ctx.as_ref().is_some_and(|resume_ctx| {
252            should_wait_for_entry_variables(resume_ctx, procedure, &self.debug_vars, self.cycle)
253        })
254    }
255
256    /// Advance the program state by one cycle.
257    ///
258    /// If the program has already reached its termination state, it returns the same result
259    /// as the previous time it was called.
260    ///
261    /// Returns the call frame exited this cycle, if any
262    pub fn step(&mut self) -> Result<Option<CallFrame>, ExecutionError> {
263        if self.stopped {
264            self.last_debug_var_count = 0;
265            return Ok(None);
266        }
267
268        let resume_ctx = match self.resume_ctx.take() {
269            Some(ctx) => ctx,
270            None => {
271                self.stopped = true;
272                self.last_debug_var_count = 0;
273                return Ok(None);
274            }
275        };
276
277        let debug_info: Option<Arc<PackageDebugInfo>> = resume_ctx.debug_info();
278
279        // Before step: peek continuation to determine what will execute
280        let CurrentCycleInfo {
281            op,
282            node_id,
283            source_node_id,
284            op_idx,
285            control_flow_kind,
286        } = extract_current_op(&resume_ctx);
287        let debug_node_id = source_node_id.or_else(|| {
288            node_id.zip(debug_info.as_deref()).and_then(|(exec_node, di)| {
289                di.unique_source_root_for_exec_node(exec_node).ok().flatten()
290            })
291        });
292        let source_node = debug_node_id.zip(debug_info.as_deref()).map(|(dnid, di)| &di[dnid]);
293        let asmop = source_node.and_then(|source_node| match op_idx {
294            Some(op_idx) => source_node.asm_op_for_operation(op_idx as u32),
295            None => source_node.asm_op_for_operation(0),
296        });
297        let inline_frames = inline_frames_for_operation(
298            debug_info.as_deref().zip(debug_node_id).map(|(debug_info, debug_node_id)| {
299                (debug_info, debug_node_id, op_idx.unwrap_or_default() as u32)
300            }),
301            resume_ctx.inherited_inline_call_contexts(),
302        );
303
304        // Look up debug vars from MAST forest for the current operation
305        let debug_var_infos: Vec<_> = source_node
306            .zip(op_idx)
307            .zip(debug_info.as_deref())
308            .map(|((source_node, op_idx), di)| {
309                source_node.debug_infos_for_operation(op_idx as u32, di).collect()
310            })
311            .unwrap_or_default();
312        let pre_step_stack = self.processor.state().get_stack_state();
313
314        // Execute one step
315        let step_result = if let Some(debug_info) = debug_info.as_deref() {
316            self.processor
317                .step_with_package_debug_info_sync(&mut self.host, resume_ctx, debug_info)
318        } else {
319            self.processor.step_sync(&mut self.host, resume_ctx)
320        };
321        match step_result {
322            Ok(Some(new_ctx)) => {
323                self.resume_ctx = Some(new_ctx);
324                self.cycle += 1;
325
326                // Query processor state
327                let state = self.processor.state();
328                let ctx = state.ctx();
329                self.current_stack = state.get_stack_state();
330
331                if self.current_context != ctx {
332                    self.contexts.insert(ctx);
333                    self.current_context = ctx;
334                }
335
336                // Track operation
337                self.current_op = op;
338                self.current_asmop = asmop.zip(debug_info.as_deref()).map(|(asmop, di)| {
339                    AssemblyOp::new(
340                        asmop.location_idx.into_option().and_then(|loc| di.get_location(loc)),
341                        di[asmop.context_name_idx].clone(),
342                        asmop.num_cycles,
343                        di[asmop.op_name_idx].clone(),
344                    )
345                });
346                if let Some(asmop) = self.current_asmop.as_ref() {
347                    self.current_proc = Some(asmop.context_name().clone());
348                }
349
350                if let Some(op) = op {
351                    if self.recent.len() == 5 {
352                        self.recent.pop_front();
353                    }
354                    self.recent.push_back(op);
355                    self.profiler.on_operation_execution_cycle(op, self.current_proc.as_deref());
356                }
357
358                // Update call stack
359                let step_info = StepInfo {
360                    op,
361                    control: control_flow_kind,
362                    asmop: self.current_asmop.as_ref(),
363                    clk: RowIndex::from(self.cycle as u32),
364                    ctx: self.current_context,
365                    inline_frames: &inline_frames,
366                };
367                let exited = self.callstack.next(&step_info);
368
369                // Record and process debug variable events
370                let debug_var_count = debug_var_infos.len();
371                self.debug_vars.record_events_with_stack(
372                    RowIndex::from(self.cycle as u32),
373                    debug_var_infos,
374                    &pre_step_stack,
375                );
376                self.debug_vars.update_to_cycle(RowIndex::from(self.cycle as u32));
377                self.last_debug_var_count = debug_var_count;
378
379                Ok(exited)
380            }
381            Ok(None) => {
382                // Program completed
383                self.stopped = true;
384                self.last_debug_var_count = 0;
385                let state = self.processor.state();
386                self.current_stack = state.get_stack_state();
387
388                // Capture the final stack as StackOutputs (truncate to 16 elements)
389                let len = self.current_stack.len().min(16);
390                self.stack_outputs =
391                    StackOutputs::new(&self.current_stack[..len]).expect("invalid stack outputs");
392
393                // Write profiling reports in case its enabled
394                #[cfg(feature = "std")]
395                {
396                    self.profiler.write_reports();
397                }
398                Ok(None)
399            }
400            Err(err) => {
401                self.stopped = true;
402                self.last_debug_var_count = 0;
403                Err(err)
404            }
405        }
406    }
407
408    /// Advance the program state until `breakpoint` is hit.
409    ///
410    /// If the program has already reached its termination state, it returns the same result
411    /// as the previous time it was called.
412    pub fn step_until(
413        &mut self,
414        breakpoint: BreakpointType,
415        source_manager: &dyn SourceManager,
416    ) -> Result<(), ExecutionError> {
417        let start_cycle = self.cycle;
418        let breakpoint = Breakpoint {
419            id: 0,
420            creation_cycle: start_cycle,
421            ty: breakpoint,
422        };
423        let start_asmop = self.current_asmop.clone();
424        while !self.stopped {
425            match self.step()? {
426                Some(exited)
427                    if exited.should_break_on_exit() && breakpoint.ty == BreakpointType::Finish =>
428                {
429                    return Ok(());
430                }
431                _ => (),
432            }
433
434            let (op, is_op_boundary, proc, loc) = {
435                let op = self.current_op;
436                let is_boundary = self.current_asmop.as_ref().map(|_info| true).unwrap_or(false);
437                let (proc, loc) = match self.callstack.current_frame() {
438                    Some(frame) => {
439                        let loc = frame
440                            .recent()
441                            .back()
442                            .and_then(|detail| detail.resolve(source_manager))
443                            .cloned();
444                        (frame.procedure(""), loc)
445                    }
446                    None => (None, None),
447                };
448                (op, is_boundary, proc, loc)
449            };
450
451            if let Some(op) = op
452                && breakpoint.should_break_for(&op, &self.processor.state())
453            {
454                return Ok(());
455            }
456
457            if is_op_boundary
458                && let Some(asmop) = self.current_asmop.as_ref()
459                && matches!(&breakpoint.ty, BreakpointType::Opcode(OperationMatcher::Asm(expected)) if expected.as_str() == asmop.op().as_ref())
460            {
461                return Ok(());
462            }
463
464            // Check if `breakpoint` was triggered at this cycle
465            let current_cycle = self.cycle;
466            let cycles_stepped = current_cycle - start_cycle;
467            if let Some(n) = breakpoint.cycles_to_skip(current_cycle)
468                && cycles_stepped > 0
469                && n == 0
470            {
471                return Ok(());
472            }
473
474            if cycles_stepped > 0
475                && is_op_boundary
476                && matches!(&breakpoint.ty, BreakpointType::Next)
477                && self.current_asmop != start_asmop
478            {
479                return Ok(());
480            }
481
482            if let Some(loc) = loc.as_ref()
483                && breakpoint.should_break_at(loc)
484            {
485                return Ok(());
486            }
487
488            if let Some(proc) = proc.as_deref()
489                && breakpoint.should_break_in(proc)
490            {
491                return Ok(());
492            }
493        }
494
495        Ok(())
496    }
497
498    /// Consume the [DebugExecutor], converting it into an [ExecutionTrace] at the current cycle.
499    pub fn into_execution_trace(self) -> ExecutionTrace {
500        ExecutionTrace {
501            processor: self.processor,
502            outputs: self.stack_outputs,
503        }
504    }
505}
506
507#[cfg(test)]
508mod tests {
509    use alloc::string::ToString;
510    use std::sync::Arc;
511
512    use miden_assembly::DefaultSourceManager;
513    use miden_mast_package::Package;
514
515    use super::*;
516    use crate::exec::Executor;
517
518    #[test]
519    fn callstack_tracks_nested_frame_events() {
520        use crate::event::{FRAME_END_EVENT, FRAME_START_EVENT};
521        let source_manager = Arc::new(DefaultSourceManager::default());
522        let program = miden_assembly::Assembler::new(source_manager.clone())
523            .assemble_program(
524                "program",
525                format!(
526                    r#"
527proc inner
528    emit.event("{FRAME_START_EVENT}")
529    nop
530    emit.event("{FRAME_END_EVENT}")
531end
532
533proc outer
534    emit.event("{FRAME_START_EVENT}")
535    exec.inner
536    emit.event("{FRAME_END_EVENT}")
537end
538
539begin
540    emit.event("{FRAME_START_EVENT}")
541    exec.outer
542    emit.event("{FRAME_END_EVENT}")
543end
544"#
545                ),
546            )
547            .map(Arc::<Package>::from)
548            .unwrap();
549
550        let mut executor = Executor::new(Vec::<Felt>::new()).into_debug(program, source_manager);
551        let mut max_depth = 0;
552        let mut saw_inner = false;
553        let mut snapshots = Vec::new();
554
555        for _ in 0..64 {
556            executor.step().unwrap();
557            let frames = executor.callstack.frames();
558            max_depth = max_depth.max(frames.len());
559            snapshots.push(
560                frames
561                    .iter()
562                    .map(|frame| {
563                        frame
564                            .procedure("")
565                            .map(|name| name.to_string())
566                            .unwrap_or_else(|| "<unknown>".to_string())
567                    })
568                    .collect::<Vec<_>>(),
569            );
570            saw_inner |= frames.len() >= 3
571                && frames
572                    .last()
573                    .and_then(|frame| frame.procedure(""))
574                    .is_some_and(|name| name.contains("inner"));
575
576            if saw_inner || executor.stopped {
577                break;
578            }
579        }
580
581        assert!(
582            max_depth >= 3,
583            "expected nested main -> outer -> inner frames, max depth was {max_depth}"
584        );
585        assert!(
586            saw_inner,
587            "expected innermost frame to resolve to inner; snapshots: {snapshots:?}"
588        );
589    }
590}