Skip to main content

miden_debug_engine/exec/
state.rs

1use std::collections::{BTreeSet, VecDeque};
2
3use miden_core::{
4    mast::{MastNode, MastNodeId},
5    operations::AssemblyOp,
6};
7use miden_processor::{
8    ContextId, Continuation, ExecutionError, FastProcessor, Felt, ResumeContext, StackOutputs,
9    operation::Operation, trace::RowIndex,
10};
11
12use super::{DebuggerHost, ExecutionTrace};
13use crate::debug::{CallFrame, CallStack, ControlFlowOp, StepInfo};
14
15/// Resolve a future that is expected to complete immediately (synchronous host methods).
16///
17/// We use a noop waker because our Host methods all return `std::future::ready(...)`.
18/// This avoids calling `step_sync()` which would create its own tokio runtime and
19/// panic inside the TUI's existing tokio current-thread runtime.
20/// TODO: Revisit this (djole).
21fn poll_immediately<T>(fut: impl std::future::Future<Output = T>) -> T {
22    let waker = std::task::Waker::noop();
23    let mut cx = std::task::Context::from_waker(waker);
24    let mut fut = std::pin::pin!(fut);
25    match fut.as_mut().poll(&mut cx) {
26        std::task::Poll::Ready(val) => val,
27        std::task::Poll::Pending => panic!("future was expected to complete immediately"),
28    }
29}
30
31/// A special version of [crate::Executor] which provides finer-grained control over execution,
32/// and captures a ton of information about the program being executed, so as to make it possible
33/// to introspect everything about the program and the state of the VM at a given cycle.
34///
35/// This is used by the debugger to execute programs, and provide all of the functionality made
36/// available by the TUI.
37pub struct DebugExecutor {
38    /// The underlying [FastProcessor] being driven
39    pub processor: FastProcessor,
40    /// The host providing debugging callbacks
41    pub host: DebuggerHost<dyn miden_assembly::SourceManager>,
42    /// The resume context for the next step (None if program has finished)
43    pub resume_ctx: Option<ResumeContext>,
44
45    // State from last step (replaces VmState fields)
46    /// The current operand stack state
47    pub current_stack: Vec<Felt>,
48    /// The operation that was just executed
49    pub current_op: Option<Operation>,
50    /// The assembly-level operation info for the current op
51    pub current_asmop: Option<AssemblyOp>,
52
53    /// The final outcome of the program being executed
54    pub stack_outputs: StackOutputs,
55    /// The set of contexts allocated during execution so far
56    pub contexts: BTreeSet<ContextId>,
57    /// The root context
58    pub root_context: ContextId,
59    /// The current context at `cycle`
60    pub current_context: ContextId,
61    /// The current call stack
62    pub callstack: CallStack,
63    /// A sliding window of the last 5 operations successfully executed by the VM
64    pub recent: VecDeque<Operation>,
65    /// The current clock cycle
66    pub cycle: usize,
67    /// Whether or not execution has terminated
68    pub stopped: bool,
69}
70
71/// Extract the current operation and assembly info from the continuation stack
72/// before a step is executed. This lets us know what operation will run next.
73pub(crate) fn extract_current_op(
74    ctx: &ResumeContext,
75) -> (Option<Operation>, Option<MastNodeId>, Option<usize>, Option<ControlFlowOp>) {
76    let forest = ctx.current_forest();
77    for cont in ctx.continuation_stack().iter_continuations_for_next_clock() {
78        match cont {
79            Continuation::ResumeBasicBlock {
80                node_id,
81                batch_index,
82                op_idx_in_batch,
83            } => {
84                let node = &forest[*node_id];
85                if let MastNode::Block(block) = node {
86                    // Compute global op index within the basic block
87                    let mut global_idx = 0;
88                    for batch in &block.op_batches()[..*batch_index] {
89                        global_idx += batch.ops().len();
90                    }
91                    global_idx += op_idx_in_batch;
92                    let op = block.op_batches()[*batch_index].ops().get(*op_idx_in_batch).copied();
93                    return (op, Some(*node_id), Some(global_idx), None);
94                }
95            }
96            Continuation::Respan {
97                node_id,
98                batch_index,
99            } => {
100                let node = &forest[*node_id];
101                if let MastNode::Block(block) = node {
102                    let mut global_idx = 0;
103                    for batch in &block.op_batches()[..*batch_index] {
104                        global_idx += batch.ops().len();
105                    }
106                    return (None, Some(*node_id), Some(global_idx), Some(ControlFlowOp::Respan));
107                }
108            }
109            Continuation::StartNode(node_id) => {
110                let control = match &forest[*node_id] {
111                    MastNode::Block(_) => Some(ControlFlowOp::Span),
112                    MastNode::Join(_) => Some(ControlFlowOp::Join),
113                    MastNode::Split(_) => Some(ControlFlowOp::Split),
114                    _ => None,
115                };
116                return (None, Some(*node_id), None, control);
117            }
118            Continuation::FinishBasicBlock(_)
119            | Continuation::FinishJoin(_)
120            | Continuation::FinishSplit(_)
121            | Continuation::FinishLoop { .. }
122            | Continuation::FinishCall(_)
123            | Continuation::FinishDyn(_)
124            | Continuation::FinishExternal(_) => {
125                return (None, None, None, Some(ControlFlowOp::End));
126            }
127            other if other.increments_clk() => {
128                return (None, None, None, None);
129            }
130            _ => continue,
131        }
132    }
133    (None, None, None, None)
134}
135
136impl DebugExecutor {
137    /// Advance the program state by one cycle.
138    ///
139    /// If the program has already reached its termination state, it returns the same result
140    /// as the previous time it was called.
141    ///
142    /// Returns the call frame exited this cycle, if any
143    pub fn step(&mut self) -> Result<Option<CallFrame>, ExecutionError> {
144        if self.stopped {
145            return Ok(None);
146        }
147
148        let resume_ctx = match self.resume_ctx.take() {
149            Some(ctx) => ctx,
150            None => {
151                self.stopped = true;
152                return Ok(None);
153            }
154        };
155
156        // Before step: peek continuation to determine what will execute
157        let (op, node_id, op_idx, control) = extract_current_op(&resume_ctx);
158        let asmop = node_id
159            .and_then(|nid| resume_ctx.current_forest().get_assembly_op(nid, op_idx).cloned());
160
161        // Execute one step
162        match poll_immediately(self.processor.step(&mut self.host, resume_ctx)) {
163            Ok(Some(new_ctx)) => {
164                self.resume_ctx = Some(new_ctx);
165                self.cycle += 1;
166
167                // Query processor state
168                let state = self.processor.state();
169                let ctx = state.ctx();
170                self.current_stack = state.get_stack_state();
171
172                if self.current_context != ctx {
173                    self.contexts.insert(ctx);
174                    self.current_context = ctx;
175                }
176
177                // Track operation
178                self.current_op = op;
179                self.current_asmop = asmop.clone();
180
181                if let Some(op) = op {
182                    if self.recent.len() == 5 {
183                        self.recent.pop_front();
184                    }
185                    self.recent.push_back(op);
186                }
187
188                // Update call stack
189                let step_info = StepInfo {
190                    op,
191                    control,
192                    asmop: self.current_asmop.as_ref(),
193                    clk: RowIndex::from(self.cycle as u32),
194                    ctx: self.current_context,
195                };
196                let exited = self.callstack.next(&step_info);
197
198                Ok(exited)
199            }
200            Ok(None) => {
201                // Program completed
202                self.stopped = true;
203                let state = self.processor.state();
204                self.current_stack = state.get_stack_state();
205
206                // Capture the final stack as StackOutputs (truncate to 16 elements)
207                let len = self.current_stack.len().min(16);
208                self.stack_outputs =
209                    StackOutputs::new(&self.current_stack[..len]).expect("invalid stack outputs");
210                Ok(None)
211            }
212            Err(err) => {
213                self.stopped = true;
214                Err(err)
215            }
216        }
217    }
218
219    /// Consume the [DebugExecutor], converting it into an [ExecutionTrace] at the current cycle.
220    pub fn into_execution_trace(self) -> ExecutionTrace {
221        ExecutionTrace {
222            root_context: self.root_context,
223            last_cycle: RowIndex::from(self.cycle as u32),
224            processor: self.processor,
225            outputs: self.stack_outputs,
226        }
227    }
228}