Skip to main content

miden_debug_engine/exec/
state.rs

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