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::operations::AssemblyOp;
9use miden_processor::{
10    ContextId, ExecutionError, FastProcessor, Felt, ResumeContext, StackOutputs,
11    operation::Operation, trace::RowIndex,
12};
13
14pub(crate) use super::source::{CurrentCycleInfo, extract_current_op};
15use super::{DebuggerHost, ExecutionTrace, source::SourceContext};
16use crate::{
17    Breakpoint, BreakpointType, OperationMatcher,
18    debug::{CallFrame, CallStack, DebugVarTracker, StepInfo},
19    profiling::Profiler,
20};
21
22/// A special version of [crate::Executor] which provides finer-grained control over execution,
23/// and captures a ton of information about the program being executed, so as to make it possible
24/// to introspect everything about the program and the state of the VM at a given cycle.
25///
26/// This is used by the debugger to execute programs, and provide all of the functionality made
27/// available by the TUI.
28pub struct DebugExecutor {
29    /// The underlying [FastProcessor] being driven
30    pub processor: FastProcessor,
31    /// The host providing debugging callbacks
32    pub host: DebuggerHost<dyn miden_assembly_syntax::debuginfo::SourceManager>,
33    /// The resume context for the next step (None if program has finished)
34    pub resume_ctx: Option<ResumeContext>,
35
36    // State from last step (replaces VmState fields)
37    /// The current operand stack state
38    pub current_stack: Vec<Felt>,
39    /// The operation that was just executed
40    pub current_op: Option<Operation>,
41    /// The assembly-level operation info for the current op
42    pub current_asmop: Option<AssemblyOp>,
43
44    /// The final outcome of the program being executed
45    pub stack_outputs: StackOutputs,
46    /// The set of contexts allocated during execution so far
47    pub contexts: BTreeSet<ContextId>,
48    /// The root context
49    pub root_context: ContextId,
50    /// The current context at `cycle`
51    pub current_context: ContextId,
52    /// The current call stack
53    pub callstack: CallStack,
54    /// The most recent live procedure name observed from assembly operation metadata.
55    pub current_proc: Option<Arc<str>>,
56    /// Debug variable tracker for source-level variable inspection
57    pub debug_vars: DebugVarTracker,
58    /// Number of debug variable location records observed during the most recent step.
59    pub last_debug_var_count: usize,
60    /// A sliding window of the last 5 operations successfully executed by the VM
61    pub recent: VecDeque<Operation>,
62    /// The current clock cycle
63    pub cycle: usize,
64    /// Whether or not execution has terminated
65    pub stopped: bool,
66    /// The profiler used by this executor
67    pub profiler: Profiler,
68}
69
70impl super::query::DebugQuery for DebugExecutor {
71    #[inline]
72    fn state(&self) -> miden_processor::ProcessorState<'_> {
73        self.processor.state()
74    }
75
76    fn current_context(&self) -> ContextId {
77        self.current_context
78    }
79
80    fn current_clock(&self) -> RowIndex {
81        self.processor.state().clock()
82    }
83}
84
85impl DebugExecutor {
86    /// Get the current operand stack as a slice - the top of the stack is the last item in the slice
87    ///
88    /// This returns the entire stack, not just the top 16 elements
89    pub fn stack(&self) -> &[Felt] {
90        self.processor.stack()
91    }
92}
93
94/// Wait for entry declarations in the current straight-line source occurrence, but never run
95/// through a branch or a callee just because a later location exists somewhere in the function.
96/// Only declarations observed this cycle count; kills and stale caller variables do not.
97pub(crate) fn should_wait_for_entry_variables(
98    resume_ctx: &ResumeContext,
99    procedure: &str,
100    variables: &DebugVarTracker,
101    cycle: usize,
102) -> bool {
103    if variables
104        .current_variables()
105        .any(|variable| variable.clk == RowIndex::from(cycle as u32))
106    {
107        return false;
108    }
109    let Some(debug_info) = resume_ctx.debug_info() else {
110        return false;
111    };
112    let next = extract_current_op(resume_ctx);
113    let Some(source_node) = next.source_node_id.map(|node| &debug_info[node]) else {
114        return false;
115    };
116    let Some(op_idx) = next.op_idx else {
117        return false;
118    };
119    source_node.debug_vars.iter().any(|variable| {
120        variable.op_idx as usize >= op_idx
121            && !matches!(variable.value_location, DebugVarLocation::Unavailable)
122            && source_node.asm_op_for_operation(variable.op_idx).is_some_and(|operation| {
123                debug_info[operation.context_name_idx].as_ref() == procedure
124            })
125    })
126}
127
128impl DebugExecutor {
129    /// Returns whether a function breakpoint should wait for pending entry-block declarations.
130    pub fn should_wait_for_entry_variables(&self, procedure: &str) -> bool {
131        self.resume_ctx.as_ref().is_some_and(|resume_ctx| {
132            should_wait_for_entry_variables(resume_ctx, procedure, &self.debug_vars, self.cycle)
133        })
134    }
135
136    /// Advance the program state by one cycle.
137    ///
138    /// If the program has already reached its termination state, it returns the same result
139    /// as the previous time it was called.
140    ///
141    /// Returns the call frame exited this cycle, if any
142    pub fn step(&mut self) -> Result<Option<CallFrame>, ExecutionError> {
143        self.step_with_observer(|_, _| {})
144    }
145
146    /// Observe the current source context, recovered frames, and declarations before a cycle.
147    /// The observer also runs when that cycle fails, but never after execution has stopped.
148    pub(crate) fn step_with_observer(
149        &mut self,
150        observe: impl FnOnce(&Self, &SourceContext),
151    ) -> Result<Option<CallFrame>, ExecutionError> {
152        if self.stopped {
153            self.last_debug_var_count = 0;
154            return Ok(None);
155        }
156
157        let resume_ctx = match self.resume_ctx.take() {
158            Some(ctx) => ctx,
159            None => {
160                self.stopped = true;
161                self.last_debug_var_count = 0;
162                return Ok(None);
163            }
164        };
165
166        let source = SourceContext::new(&resume_ctx);
167        let CurrentCycleInfo {
168            op,
169            control_flow_kind,
170            ..
171        } = source.cycle;
172        let debug_var_infos = source.debug_vars();
173        let source_asmop = source.asmop();
174        self.callstack.observe_source_context(
175            &resume_ctx,
176            source.source_operation(),
177            source_asmop.as_ref(),
178        );
179
180        let debug_var_count = debug_var_infos.len();
181        self.debug_vars.observe_execution(
182            self.callstack.invocation_ids(),
183            self.callstack.current_invocation(),
184            &self.processor,
185            RowIndex::from((self.cycle + 1) as u32),
186            debug_var_infos,
187        );
188        observe(self, &source);
189
190        // Execute one step
191        let step_result = if let Some(debug_info) = source.debug_info() {
192            self.processor
193                .step_with_package_debug_info_sync(&mut self.host, resume_ctx, debug_info)
194        } else {
195            self.processor.step_sync(&mut self.host, resume_ctx)
196        };
197        match step_result {
198            Ok(Some(new_ctx)) => {
199                self.resume_ctx = Some(new_ctx);
200                self.cycle += 1;
201
202                // Query processor state
203                let state = self.processor.state();
204                let ctx = state.ctx();
205                self.current_stack = state.get_stack_state();
206
207                if self.current_context != ctx {
208                    self.contexts.insert(ctx);
209                    self.current_context = ctx;
210                }
211
212                // Track operation
213                self.current_op = op;
214                self.current_asmop = source_asmop;
215                if let Some(asmop) = self.current_asmop.as_ref() {
216                    self.current_proc = Some(asmop.context_name().clone());
217                }
218
219                if let Some(op) = op {
220                    if self.recent.len() == 5 {
221                        self.recent.pop_front();
222                    }
223                    self.recent.push_back(op);
224                    self.profiler.on_operation_execution_cycle(op, self.current_proc.as_deref());
225                }
226
227                // Update call stack
228                let step_info = StepInfo {
229                    op,
230                    control: control_flow_kind,
231                    asmop: self.current_asmop.as_ref(),
232                    clk: RowIndex::from(self.cycle as u32),
233                    ctx: self.current_context,
234                    inline_frames: source.inline_frames(),
235                };
236                let exited = self.callstack.next(&step_info);
237
238                self.last_debug_var_count = debug_var_count;
239
240                Ok(exited)
241            }
242            Ok(None) => {
243                // Program completed: no invocation remains available for inspection.
244                self.debug_vars.reset();
245                self.stopped = true;
246                self.last_debug_var_count = 0;
247                let state = self.processor.state();
248                self.current_stack = state.get_stack_state();
249
250                // Capture the final stack as StackOutputs (truncate to 16 elements)
251                let len = self.current_stack.len().min(16);
252                self.stack_outputs =
253                    StackOutputs::new(&self.current_stack[..len]).expect("invalid stack outputs");
254
255                // Write profiling reports in case its enabled
256                #[cfg(feature = "std")]
257                {
258                    self.profiler.write_reports();
259                }
260                Ok(None)
261            }
262            Err(err) => {
263                self.stopped = true;
264                self.last_debug_var_count = 0;
265                Err(err)
266            }
267        }
268    }
269
270    /// Whether a captured invocation has returned and execution has reached a caller operation.
271    /// Event-only targets may stop without source information.
272    pub fn has_returned(&self, target: &crate::debug::ReturnTarget) -> bool {
273        self.callstack.has_returned(target)
274            && (!target.is_recovered()
275                || !self.callstack.has_recovered_current_frame()
276                || (self.current_op.is_some() && self.current_asmop.is_some()))
277    }
278
279    /// Advance the program state until `breakpoint` is hit.
280    ///
281    /// If the program has already reached its termination state, it returns the same result
282    /// as the previous time it was called.
283    pub fn step_until(
284        &mut self,
285        breakpoint: BreakpointType,
286        source_manager: &dyn SourceManager,
287    ) -> Result<(), ExecutionError> {
288        let start_cycle = self.cycle;
289        let breakpoint = Breakpoint {
290            id: 0,
291            creation_cycle: start_cycle,
292            ty: breakpoint,
293        };
294        let start_asmop = self.current_asmop.clone();
295        let return_target =
296            (breakpoint.ty == BreakpointType::Finish).then(|| self.callstack.return_target());
297        while !self.stopped {
298            self.step()?;
299            if return_target.as_ref().is_some_and(|target| self.has_returned(target)) {
300                return Ok(());
301            }
302
303            let (op, is_op_boundary, proc, loc) = {
304                let op = self.current_op;
305                let is_boundary = self.current_asmop.as_ref().map(|_info| true).unwrap_or(false);
306                let (proc, loc) = match self.callstack.current_frame() {
307                    Some(frame) => {
308                        let loc = frame
309                            .recent()
310                            .back()
311                            .and_then(|detail| detail.resolve(source_manager))
312                            .cloned();
313                        (frame.procedure(""), loc)
314                    }
315                    None => (None, None),
316                };
317                (op, is_boundary, proc, loc)
318            };
319
320            if let Some(op) = op
321                && breakpoint.should_break_for(&op, &self.processor.state())
322            {
323                return Ok(());
324            }
325
326            if is_op_boundary
327                && let Some(asmop) = self.current_asmop.as_ref()
328                && matches!(&breakpoint.ty, BreakpointType::Opcode(OperationMatcher::Asm(expected)) if expected.as_str() == asmop.op().as_ref())
329            {
330                return Ok(());
331            }
332
333            // Check if `breakpoint` was triggered at this cycle
334            let current_cycle = self.cycle;
335            let cycles_stepped = current_cycle - start_cycle;
336            if let Some(n) = breakpoint.cycles_to_skip(current_cycle)
337                && cycles_stepped > 0
338                && n == 0
339            {
340                return Ok(());
341            }
342
343            if cycles_stepped > 0
344                && is_op_boundary
345                && matches!(&breakpoint.ty, BreakpointType::Next)
346                && self.current_asmop != start_asmop
347            {
348                return Ok(());
349            }
350
351            if let Some(loc) = loc.as_ref()
352                && breakpoint.should_break_at(loc)
353            {
354                return Ok(());
355            }
356
357            if let Some(proc) = proc.as_deref()
358                && breakpoint.should_break_in(proc)
359            {
360                return Ok(());
361            }
362        }
363
364        Ok(())
365    }
366
367    /// Consume the [DebugExecutor], converting it into an [ExecutionTrace] at the current cycle.
368    pub fn into_execution_trace(self) -> ExecutionTrace {
369        ExecutionTrace {
370            processor: self.processor,
371            outputs: self.stack_outputs,
372        }
373    }
374}
375
376#[cfg(test)]
377mod tests;