1use std::{
2 collections::{BTreeSet, VecDeque},
3 rc::Rc,
4};
5
6use miden_assembly::SourceManager;
7use miden_core::{
8 mast::{MastNode, MastNodeId},
9 operations::AssemblyOp,
10};
11use miden_processor::{
12 ContextId, Continuation, ExecutionError, FastProcessor, Felt, ResumeContext, StackOutputs,
13 operation::Operation, trace::RowIndex,
14};
15
16use super::{DebuggerHost, ExecutionTrace, TraceMonitor};
17use crate::{
18 Breakpoint, BreakpointType, OperationMatcher,
19 debug::{
20 CallFrame, CallStack, ControlFlowOp, DebugVarTracker, StepInfo,
21 snapshot_transient_debug_values,
22 },
23};
24
25fn poll_immediately<T>(fut: impl std::future::Future<Output = T>) -> T {
32 let waker = std::task::Waker::noop();
33 let mut cx = std::task::Context::from_waker(waker);
34 let mut fut = std::pin::pin!(fut);
35 match fut.as_mut().poll(&mut cx) {
36 std::task::Poll::Ready(val) => val,
37 std::task::Poll::Pending => panic!("future was expected to complete immediately"),
38 }
39}
40
41pub struct DebugExecutor {
48 pub processor: FastProcessor,
50 pub host: DebuggerHost<dyn miden_assembly::SourceManager>,
52 pub resume_ctx: Option<ResumeContext>,
54
55 pub current_stack: Vec<Felt>,
58 pub current_op: Option<Operation>,
60 pub current_asmop: Option<AssemblyOp>,
62
63 pub stack_outputs: StackOutputs,
65 pub contexts: BTreeSet<ContextId>,
67 pub root_context: ContextId,
69 pub current_context: ContextId,
71 pub callstack: CallStack,
73 pub current_proc: Option<Rc<str>>,
75 pub debug_vars: DebugVarTracker,
77 pub last_debug_var_count: usize,
79 pub recent: VecDeque<Operation>,
81 pub cycle: usize,
83 pub stopped: bool,
85}
86
87impl super::query::DebugQuery for DebugExecutor {
88 #[inline]
89 fn state(&self) -> miden_processor::ProcessorState<'_> {
90 self.processor.state()
91 }
92
93 fn current_context(&self) -> ContextId {
94 self.current_context
95 }
96
97 fn current_clock(&self) -> RowIndex {
98 self.processor.state().clock()
99 }
100}
101
102impl DebugExecutor {
103 pub fn stack(&self) -> &[Felt] {
107 self.processor.stack()
108 }
109}
110
111pub(crate) fn extract_current_op(
114 ctx: &ResumeContext,
115) -> (Option<Operation>, Option<MastNodeId>, Option<usize>, Option<ControlFlowOp>) {
116 let forest = ctx.current_forest();
117 for cont in ctx.continuation_stack().iter_continuations_for_next_clock() {
118 match cont {
119 Continuation::ResumeBasicBlock {
120 node_id,
121 batch_index,
122 op_idx_in_batch,
123 } => {
124 let node = &forest[*node_id];
125 if let MastNode::Block(block) = node {
126 let mut global_idx = 0;
128 for batch in &block.op_batches()[..*batch_index] {
129 global_idx += batch.ops().len();
130 }
131 global_idx += op_idx_in_batch;
132 let op = block.op_batches()[*batch_index].ops().get(*op_idx_in_batch).copied();
133 return (op, Some(*node_id), Some(global_idx), None);
134 }
135 }
136 Continuation::Respan {
137 node_id,
138 batch_index,
139 } => {
140 let node = &forest[*node_id];
141 if let MastNode::Block(block) = node {
142 let mut global_idx = 0;
143 for batch in &block.op_batches()[..*batch_index] {
144 global_idx += batch.ops().len();
145 }
146 return (None, Some(*node_id), Some(global_idx), Some(ControlFlowOp::Respan));
147 }
148 }
149 Continuation::StartNode(node_id) => {
150 let control = match &forest[*node_id] {
151 MastNode::Block(_) => Some(ControlFlowOp::Span),
152 MastNode::Join(_) => Some(ControlFlowOp::Join),
153 MastNode::Split(_) => Some(ControlFlowOp::Split),
154 _ => None,
155 };
156 return (None, Some(*node_id), None, control);
157 }
158 Continuation::FinishBasicBlock(_)
159 | Continuation::FinishJoin(_)
160 | Continuation::FinishSplit(_)
161 | Continuation::FinishLoop { .. }
162 | Continuation::FinishCall(_)
163 | Continuation::FinishDyn(_)
164 | Continuation::FinishExternal(_) => {
165 return (None, None, None, Some(ControlFlowOp::End));
166 }
167 other if other.increments_clk() => {
168 return (None, None, None, None);
169 }
170 _ => continue,
171 }
172 }
173 (None, None, None, None)
174}
175
176impl DebugExecutor {
177 pub fn procedure_has_debug_vars(&self, procedure: &str) -> bool {
180 let Some(resume_ctx) = self.resume_ctx.as_ref() else {
181 return false;
182 };
183
184 let forest = resume_ctx.current_forest();
185 for (node_idx, node) in forest.nodes().iter().enumerate() {
186 let MastNode::Block(block) = node else {
187 continue;
188 };
189 let node_id = MastNodeId::new_unchecked(node_idx as u32);
190 for op_idx in 0..block.num_operations() as usize {
191 if forest.debug_vars_for_operation(node_id, op_idx).is_empty() {
192 continue;
193 }
194 if forest
195 .get_assembly_op(node_id, Some(op_idx))
196 .is_some_and(|op| op.context_name() == procedure)
197 {
198 return true;
199 }
200 }
201 }
202
203 false
204 }
205
206 pub fn register_trace_monitor_for(&mut self, monitor: TraceMonitor, event: super::TraceEvent) {
207 self.host.register_trace_handler(event, move |state, event| {
208 monitor.handle_event(state.clock(), event)
209 });
210 }
211
212 pub fn step(&mut self) -> Result<Option<CallFrame>, ExecutionError> {
219 if self.stopped {
220 self.last_debug_var_count = 0;
221 return Ok(None);
222 }
223
224 let resume_ctx = match self.resume_ctx.take() {
225 Some(ctx) => ctx,
226 None => {
227 self.stopped = true;
228 self.last_debug_var_count = 0;
229 return Ok(None);
230 }
231 };
232
233 let (op, node_id, op_idx, control) = extract_current_op(&resume_ctx);
235 let asmop = node_id
236 .and_then(|nid| resume_ctx.current_forest().get_assembly_op(nid, op_idx).cloned());
237
238 let mut debug_var_infos: Vec<_> = if let (Some(nid), Some(idx)) = (node_id, op_idx) {
240 let forest = resume_ctx.current_forest();
241 forest
242 .debug_vars_for_operation(nid, idx)
243 .iter()
244 .filter_map(|vid| forest.debug_var(*vid).cloned())
245 .collect()
246 } else {
247 vec![]
248 };
249 let pre_step_stack = self.processor.state().get_stack_state();
250 snapshot_transient_debug_values(&mut debug_var_infos, &pre_step_stack);
251
252 match poll_immediately(self.processor.step(&mut self.host, resume_ctx)) {
254 Ok(Some(new_ctx)) => {
255 self.resume_ctx = Some(new_ctx);
256 self.cycle += 1;
257
258 let state = self.processor.state();
260 let ctx = state.ctx();
261 self.current_stack = state.get_stack_state();
262
263 if self.current_context != ctx {
264 self.contexts.insert(ctx);
265 self.current_context = ctx;
266 }
267
268 self.current_op = op;
270 self.current_asmop = asmop.clone();
271 if let Some(asmop) = asmop.as_ref() {
272 self.current_proc = Some(Rc::from(asmop.context_name()));
273 }
274
275 if let Some(op) = op {
276 if self.recent.len() == 5 {
277 self.recent.pop_front();
278 }
279 self.recent.push_back(op);
280 }
281
282 let step_info = StepInfo {
284 op,
285 control,
286 asmop: self.current_asmop.as_ref(),
287 clk: RowIndex::from(self.cycle as u32),
288 ctx: self.current_context,
289 };
290 let exited = self.callstack.next(&step_info);
291
292 let debug_var_count = debug_var_infos.len();
294 self.debug_vars
295 .record_events(RowIndex::from(self.cycle as u32), debug_var_infos);
296 self.debug_vars.update_to_cycle(RowIndex::from(self.cycle as u32));
297 self.last_debug_var_count = debug_var_count;
298
299 Ok(exited)
300 }
301 Ok(None) => {
302 self.stopped = true;
304 self.last_debug_var_count = 0;
305 let state = self.processor.state();
306 self.current_stack = state.get_stack_state();
307
308 let len = self.current_stack.len().min(16);
310 self.stack_outputs =
311 StackOutputs::new(&self.current_stack[..len]).expect("invalid stack outputs");
312 Ok(None)
313 }
314 Err(err) => {
315 self.stopped = true;
316 self.last_debug_var_count = 0;
317 Err(err)
318 }
319 }
320 }
321
322 pub fn step_until(
327 &mut self,
328 breakpoint: BreakpointType,
329 trace_monitor: Option<TraceMonitor>,
330 source_manager: &dyn SourceManager,
331 ) -> Result<(), ExecutionError> {
332 let start_cycle = self.cycle;
333 let start_clock = self.processor.state().clock();
334 let breakpoint = Breakpoint {
335 id: 0,
336 creation_cycle: start_cycle,
337 ty: breakpoint,
338 };
339 let start_asmop = self.current_asmop.clone();
340 while !self.stopped {
341 match self.step()? {
342 Some(exited)
343 if exited.should_break_on_exit() && breakpoint.ty == BreakpointType::Finish =>
344 {
345 return Ok(());
346 }
347 _ => (),
348 }
349
350 if let BreakpointType::Trace(event_id) = breakpoint.ty
352 && let Some(trace_monitor) = trace_monitor.as_ref()
353 && trace_monitor.has_event_occurred_since(start_clock, |event| event == event_id)
354 {
355 return Ok(());
356 }
357
358 let (op, is_op_boundary, proc, loc) = {
359 let op = self.current_op;
360 let is_boundary = self.current_asmop.as_ref().map(|_info| true).unwrap_or(false);
361 let (proc, loc) = match self.callstack.current_frame() {
362 Some(frame) => {
363 let loc = frame
364 .recent()
365 .back()
366 .and_then(|detail| detail.resolve(source_manager))
367 .cloned();
368 (frame.procedure(""), loc)
369 }
370 None => (None, None),
371 };
372 (op, is_boundary, proc, loc)
373 };
374
375 if let Some(op) = op
376 && breakpoint.should_break_for(&op)
377 {
378 return Ok(());
379 }
380
381 if is_op_boundary
382 && let Some(asmop) = self.current_asmop.as_ref()
383 && matches!(&breakpoint.ty, BreakpointType::Opcode(OperationMatcher::Asm(expected)) if expected == asmop.op())
384 {
385 return Ok(());
386 }
387
388 let current_cycle = self.cycle;
390 let cycles_stepped = current_cycle - start_cycle;
391 if let Some(n) = breakpoint.cycles_to_skip(current_cycle)
392 && cycles_stepped >= n
393 {
394 return Ok(());
395 }
396
397 if cycles_stepped > 0
398 && is_op_boundary
399 && matches!(&breakpoint.ty, BreakpointType::Next)
400 && self.current_asmop != start_asmop
401 {
402 return Ok(());
403 }
404
405 if let Some(loc) = loc.as_ref()
406 && breakpoint.should_break_at(loc)
407 {
408 return Ok(());
409 }
410
411 if let Some(proc) = proc.as_deref()
412 && breakpoint.should_break_in(proc)
413 {
414 return Ok(());
415 }
416 }
417
418 Ok(())
419 }
420
421 pub fn into_execution_trace(self) -> ExecutionTrace {
423 ExecutionTrace {
424 processor: self.processor,
425 outputs: self.stack_outputs,
426 }
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use std::sync::Arc;
433
434 use miden_assembly::DefaultSourceManager;
435
436 use super::*;
437 use crate::exec::Executor;
438
439 #[test]
440 fn callstack_tracks_nested_frame_trace_events() {
441 let source_manager = Arc::new(DefaultSourceManager::default());
442 let program = miden_assembly::Assembler::new(source_manager.clone())
443 .assemble_program(
444 r#"
445proc inner
446 nop
447end
448
449proc outer
450 trace.240
451 nop
452 exec.inner
453 trace.252
454 nop
455end
456
457begin
458 trace.240
459 nop
460 exec.outer
461 trace.252
462 nop
463end
464"#,
465 )
466 .unwrap();
467
468 let mut executor = Executor::new(Vec::<Felt>::new()).into_debug(&program, source_manager);
469 let mut max_depth = 0;
470 let mut saw_inner = false;
471 let mut snapshots = Vec::new();
472
473 for _ in 0..64 {
474 executor.step().unwrap();
475 let frames = executor.callstack.frames();
476 max_depth = max_depth.max(frames.len());
477 snapshots.push(
478 frames
479 .iter()
480 .map(|frame| {
481 frame
482 .procedure("")
483 .map(|name| name.to_string())
484 .unwrap_or_else(|| "<unknown>".to_string())
485 })
486 .collect::<Vec<_>>(),
487 );
488 saw_inner |= frames.len() >= 3
489 && frames
490 .last()
491 .and_then(|frame| frame.procedure(""))
492 .is_some_and(|name| name.contains("inner"));
493
494 if saw_inner || executor.stopped {
495 break;
496 }
497 }
498
499 assert!(
500 max_depth >= 3,
501 "expected nested main -> outer -> inner frames, max depth was {max_depth}"
502 );
503 assert!(
504 saw_inner,
505 "expected innermost frame to resolve to inner; snapshots: {snapshots:?}"
506 );
507 }
508}