1use std::{
2 collections::{BTreeSet, VecDeque},
3 rc::Rc,
4};
5
6use miden_assembly::SourceManager;
7use miden_core::{
8 mast::{MastForest, 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 forest_procedure_has_debug_vars(resume_ctx.current_forest(), procedure)
185 }
186
187 pub fn register_trace_monitor_for(&mut self, monitor: TraceMonitor, event: super::TraceEvent) {
188 self.host.register_trace_handler(event, move |state, event| {
189 monitor.handle_event(state.clock(), event)
190 });
191 }
192
193 pub fn step(&mut self) -> Result<Option<CallFrame>, ExecutionError> {
200 if self.stopped {
201 self.last_debug_var_count = 0;
202 return Ok(None);
203 }
204
205 let resume_ctx = match self.resume_ctx.take() {
206 Some(ctx) => ctx,
207 None => {
208 self.stopped = true;
209 self.last_debug_var_count = 0;
210 return Ok(None);
211 }
212 };
213
214 let (op, node_id, op_idx, control) = extract_current_op(&resume_ctx);
216 let asmop = node_id
217 .and_then(|nid| resume_ctx.current_forest().get_assembly_op(nid, op_idx).cloned());
218
219 let mut debug_var_infos: Vec<_> = if let (Some(nid), Some(idx)) = (node_id, op_idx) {
221 let forest = resume_ctx.current_forest();
222 forest
223 .debug_vars_for_operation(nid, idx)
224 .iter()
225 .filter_map(|vid| forest.debug_var(*vid).cloned())
226 .collect()
227 } else {
228 vec![]
229 };
230 let pre_step_stack = self.processor.state().get_stack_state();
231 snapshot_transient_debug_values(&mut debug_var_infos, &pre_step_stack);
232
233 match poll_immediately(self.processor.step(&mut self.host, resume_ctx)) {
235 Ok(Some(new_ctx)) => {
236 self.resume_ctx = Some(new_ctx);
237 self.cycle += 1;
238
239 let state = self.processor.state();
241 let ctx = state.ctx();
242 self.current_stack = state.get_stack_state();
243
244 if self.current_context != ctx {
245 self.contexts.insert(ctx);
246 self.current_context = ctx;
247 }
248
249 self.current_op = op;
251 self.current_asmop = asmop.clone();
252 if let Some(asmop) = asmop.as_ref() {
253 self.current_proc = Some(Rc::from(asmop.context_name()));
254 }
255
256 if let Some(op) = op {
257 if self.recent.len() == 5 {
258 self.recent.pop_front();
259 }
260 self.recent.push_back(op);
261 }
262
263 let step_info = StepInfo {
265 op,
266 control,
267 asmop: self.current_asmop.as_ref(),
268 clk: RowIndex::from(self.cycle as u32),
269 ctx: self.current_context,
270 };
271 let exited = self.callstack.next(&step_info);
272
273 let debug_var_count = debug_var_infos.len();
275 self.debug_vars
276 .record_events(RowIndex::from(self.cycle as u32), debug_var_infos);
277 self.debug_vars.update_to_cycle(RowIndex::from(self.cycle as u32));
278 self.last_debug_var_count = debug_var_count;
279
280 Ok(exited)
281 }
282 Ok(None) => {
283 self.stopped = true;
285 self.last_debug_var_count = 0;
286 let state = self.processor.state();
287 self.current_stack = state.get_stack_state();
288
289 let len = self.current_stack.len().min(16);
291 self.stack_outputs =
292 StackOutputs::new(&self.current_stack[..len]).expect("invalid stack outputs");
293 Ok(None)
294 }
295 Err(err) => {
296 self.stopped = true;
297 self.last_debug_var_count = 0;
298 Err(err)
299 }
300 }
301 }
302
303 pub fn step_until(
308 &mut self,
309 breakpoint: BreakpointType,
310 trace_monitor: Option<TraceMonitor>,
311 source_manager: &dyn SourceManager,
312 ) -> Result<(), ExecutionError> {
313 let start_cycle = self.cycle;
314 let start_clock = self.processor.state().clock();
315 let breakpoint = Breakpoint {
316 id: 0,
317 creation_cycle: start_cycle,
318 ty: breakpoint,
319 };
320 let start_asmop = self.current_asmop.clone();
321 while !self.stopped {
322 match self.step()? {
323 Some(exited)
324 if exited.should_break_on_exit() && breakpoint.ty == BreakpointType::Finish =>
325 {
326 return Ok(());
327 }
328 _ => (),
329 }
330
331 if let BreakpointType::Trace(event_id) = breakpoint.ty
333 && let Some(trace_monitor) = trace_monitor.as_ref()
334 && trace_monitor.has_event_occurred_since(start_clock, |event| event == event_id)
335 {
336 return Ok(());
337 }
338
339 let (op, is_op_boundary, proc, loc) = {
340 let op = self.current_op;
341 let is_boundary = self.current_asmop.as_ref().map(|_info| true).unwrap_or(false);
342 let (proc, loc) = match self.callstack.current_frame() {
343 Some(frame) => {
344 let loc = frame
345 .recent()
346 .back()
347 .and_then(|detail| detail.resolve(source_manager))
348 .cloned();
349 (frame.procedure(""), loc)
350 }
351 None => (None, None),
352 };
353 (op, is_boundary, proc, loc)
354 };
355
356 if let Some(op) = op
357 && breakpoint.should_break_for(&op)
358 {
359 return Ok(());
360 }
361
362 if is_op_boundary
363 && let Some(asmop) = self.current_asmop.as_ref()
364 && matches!(&breakpoint.ty, BreakpointType::Opcode(OperationMatcher::Asm(expected)) if expected == asmop.op())
365 {
366 return Ok(());
367 }
368
369 let current_cycle = self.cycle;
371 let cycles_stepped = current_cycle - start_cycle;
372 if let Some(n) = breakpoint.cycles_to_skip(current_cycle)
373 && cycles_stepped >= n
374 {
375 return Ok(());
376 }
377
378 if cycles_stepped > 0
379 && is_op_boundary
380 && matches!(&breakpoint.ty, BreakpointType::Next)
381 && self.current_asmop != start_asmop
382 {
383 return Ok(());
384 }
385
386 if let Some(loc) = loc.as_ref()
387 && breakpoint.should_break_at(loc)
388 {
389 return Ok(());
390 }
391
392 if let Some(proc) = proc.as_deref()
393 && breakpoint.should_break_in(proc)
394 {
395 return Ok(());
396 }
397 }
398
399 Ok(())
400 }
401
402 pub fn into_execution_trace(self) -> ExecutionTrace {
404 ExecutionTrace {
405 processor: self.processor,
406 outputs: self.stack_outputs,
407 }
408 }
409}
410
411pub(crate) fn forest_procedure_has_debug_vars(forest: &MastForest, procedure: &str) -> bool {
412 forest_operation_matches(forest, |node_id, op_idx, asmop| {
413 asmop.context_name() == procedure
414 && !forest.debug_vars_for_operation(node_id, op_idx).is_empty()
415 })
416}
417
418fn forest_operation_matches(
419 forest: &MastForest,
420 mut matches: impl FnMut(MastNodeId, usize, &AssemblyOp) -> bool,
421) -> bool {
422 for (node_idx, node) in forest.nodes().iter().enumerate() {
423 let MastNode::Block(block) = node else {
424 continue;
425 };
426 let node_id = MastNodeId::new_unchecked(node_idx as u32);
427 for op_idx in 0..block.num_operations() as usize {
428 if forest
429 .get_assembly_op(node_id, Some(op_idx))
430 .is_some_and(|asmop| matches(node_id, op_idx, asmop))
431 {
432 return true;
433 }
434 }
435 }
436
437 false
438}
439
440#[cfg(test)]
441mod tests {
442 use std::sync::Arc;
443
444 use miden_assembly::DefaultSourceManager;
445
446 use super::*;
447 use crate::exec::Executor;
448
449 #[test]
450 fn callstack_tracks_nested_frame_trace_events() {
451 let source_manager = Arc::new(DefaultSourceManager::default());
452 let program = miden_assembly::Assembler::new(source_manager.clone())
453 .assemble_program(
454 r#"
455proc inner
456 nop
457end
458
459proc outer
460 trace.240
461 nop
462 exec.inner
463 trace.252
464 nop
465end
466
467begin
468 trace.240
469 nop
470 exec.outer
471 trace.252
472 nop
473end
474"#,
475 )
476 .unwrap();
477
478 let mut executor = Executor::new(Vec::<Felt>::new()).into_debug(&program, source_manager);
479 let mut max_depth = 0;
480 let mut saw_inner = false;
481 let mut snapshots = Vec::new();
482
483 for _ in 0..64 {
484 executor.step().unwrap();
485 let frames = executor.callstack.frames();
486 max_depth = max_depth.max(frames.len());
487 snapshots.push(
488 frames
489 .iter()
490 .map(|frame| {
491 frame
492 .procedure("")
493 .map(|name| name.to_string())
494 .unwrap_or_else(|| "<unknown>".to_string())
495 })
496 .collect::<Vec<_>>(),
497 );
498 saw_inner |= frames.len() >= 3
499 && frames
500 .last()
501 .and_then(|frame| frame.procedure(""))
502 .is_some_and(|name| name.contains("inner"));
503
504 if saw_inner || executor.stopped {
505 break;
506 }
507 }
508
509 assert!(
510 max_depth >= 3,
511 "expected nested main -> outer -> inner frames, max depth was {max_depth}"
512 );
513 assert!(
514 saw_inner,
515 "expected innermost frame to resolve to inner; snapshots: {snapshots:?}"
516 );
517 }
518}