1use std::{
2 collections::{BTreeSet, VecDeque},
3 sync::Arc,
4};
5
6use miden_assembly::SourceManager;
7use miden_core::{
8 mast::{MastNode, MastNodeId},
9 operations::AssemblyOp,
10};
11use miden_mast_package::debug_info::{DebugSourceNodeId, PackageDebugInfo};
12use miden_processor::{
13 ContextId, Continuation, ExecutionError, FastProcessor, Felt, ResumeContext, StackOutputs,
14 operation::Operation, trace::RowIndex,
15};
16
17use super::{DebuggerHost, ExecutionTrace};
18use crate::{
19 Breakpoint, BreakpointType, OperationMatcher,
20 debug::{
21 CallFrame, CallStack, ControlFlowOp, DebugVarTracker, StepInfo, inline_frames_for_operation,
22 },
23 profiling::Profiler,
24};
25
26pub struct DebugExecutor {
33 pub processor: FastProcessor,
35 pub host: DebuggerHost<dyn miden_assembly::SourceManager>,
37 pub resume_ctx: Option<ResumeContext>,
39
40 pub current_stack: Vec<Felt>,
43 pub current_op: Option<Operation>,
45 pub current_asmop: Option<AssemblyOp>,
47
48 pub stack_outputs: StackOutputs,
50 pub contexts: BTreeSet<ContextId>,
52 pub root_context: ContextId,
54 pub current_context: ContextId,
56 pub callstack: CallStack,
58 pub current_proc: Option<Arc<str>>,
60 pub debug_vars: DebugVarTracker,
62 pub last_debug_var_count: usize,
64 pub recent: VecDeque<Operation>,
66 pub cycle: usize,
68 pub stopped: bool,
70 pub profiler: Profiler,
72}
73
74impl super::query::DebugQuery for DebugExecutor {
75 #[inline]
76 fn state(&self) -> miden_processor::ProcessorState<'_> {
77 self.processor.state()
78 }
79
80 fn current_context(&self) -> ContextId {
81 self.current_context
82 }
83
84 fn current_clock(&self) -> RowIndex {
85 self.processor.state().clock()
86 }
87}
88
89impl DebugExecutor {
90 pub fn stack(&self) -> &[Felt] {
94 self.processor.stack()
95 }
96}
97
98pub(crate) struct CurrentCycleInfo {
99 pub op: Option<Operation>,
100 pub node_id: Option<MastNodeId>,
101 pub source_node_id: Option<DebugSourceNodeId>,
102 pub op_idx: Option<usize>,
103 pub control_flow_kind: Option<ControlFlowOp>,
104}
105
106pub(crate) fn extract_current_op(ctx: &ResumeContext) -> CurrentCycleInfo {
109 let forest = ctx.current_forest();
110 let debug_info = ctx.debug_info();
111 let continuation_stack = ctx.continuation_stack();
112 for (cont, source_node_id) in
113 continuation_stack.iter_continuations_for_next_clock_with_source_node_ids()
114 {
115 let exec_node = cont.exec_node();
116 let source_node_id = source_node_id.or_else(|| {
117 exec_node.zip(debug_info.as_deref()).and_then(|(exec_node, di)| {
118 di.unique_source_root_for_exec_node(exec_node).ok().flatten()
119 })
120 });
121 match cont {
122 Continuation::ResumeBasicBlock {
123 node_id,
124 batch_index,
125 op_idx_in_batch,
126 } => {
127 let MastNode::Block(block) = &forest[*node_id] else {
128 unreachable!()
129 };
130 let mut global_idx = 0;
132 for batch in &block.op_batches()[..*batch_index] {
133 global_idx += batch.ops().len();
134 }
135 global_idx += op_idx_in_batch;
136 let op = block.op_batches()[*batch_index].ops().get(*op_idx_in_batch).copied();
137 return CurrentCycleInfo {
138 op,
139 node_id: Some(*node_id),
140 source_node_id,
141 op_idx: Some(global_idx),
142 control_flow_kind: None,
143 };
144 }
145 Continuation::Respan {
146 node_id,
147 batch_index,
148 } => {
149 let node = &forest[*node_id];
150 if let MastNode::Block(block) = node {
151 let mut global_idx = 0;
152 for batch in &block.op_batches()[..*batch_index] {
153 global_idx += batch.ops().len();
154 }
155 return CurrentCycleInfo {
156 op: None,
157 node_id: Some(*node_id),
158 source_node_id,
159 op_idx: Some(global_idx),
160 control_flow_kind: Some(ControlFlowOp::Respan),
161 };
162 }
163 }
164 Continuation::StartNode(node_id) => {
165 let control_flow_kind = match &forest[*node_id] {
166 MastNode::Block(_) => Some(ControlFlowOp::Span),
167 MastNode::Join(_) => Some(ControlFlowOp::Join),
168 MastNode::Split(_) => Some(ControlFlowOp::Split),
169 _ => None,
170 };
171 return CurrentCycleInfo {
172 op: None,
173 node_id: Some(*node_id),
174 source_node_id,
175 op_idx: None,
176 control_flow_kind,
177 };
178 }
179 Continuation::FinishBasicBlock(_)
180 | Continuation::FinishJoin(_)
181 | Continuation::FinishSplit(_)
182 | Continuation::FinishLoop { .. }
183 | Continuation::FinishCall(_)
184 | Continuation::FinishDyn(_) => {
185 return CurrentCycleInfo {
186 op: None,
187 node_id: None,
188 source_node_id,
189 op_idx: None,
190 control_flow_kind: Some(ControlFlowOp::End),
191 };
192 }
193 Continuation::EnterForest { .. } => {
194 return CurrentCycleInfo {
195 op: None,
196 node_id: None,
197 source_node_id,
198 op_idx: None,
199 control_flow_kind: None,
200 };
201 }
202 }
203 }
204 CurrentCycleInfo {
205 op: None,
206 node_id: None,
207 source_node_id: None,
208 op_idx: None,
209 control_flow_kind: None,
210 }
211}
212
213impl DebugExecutor {
214 #[allow(unused)]
217 pub fn procedure_has_debug_vars(&self, procedure: &str) -> bool {
218 let Some(resume_ctx) = self.resume_ctx.as_ref() else {
219 return false;
220 };
221 let Some(debug_info) = resume_ctx.debug_info() else {
222 return false;
223 };
224
225 for function_info in debug_info.functions() {
226 if debug_info[function_info.name_idx].as_ref() != procedure {
227 continue;
228 }
229 if let Some(source_node) = function_info.source_node.into_option() {
230 return !debug_info[source_node].debug_vars.is_empty();
231 } else if let Some(exec_node) =
232 resume_ctx.current_forest().find_procedure_root(function_info.mast_root)
233 && let Ok(Some(source_node)) =
234 debug_info.unique_source_root_for_exec_node(exec_node)
235 {
236 return !debug_info[source_node].debug_vars.is_empty();
237 } else {
238 return false;
239 }
240 }
241
242 false
243 }
244
245 pub fn step(&mut self) -> Result<Option<CallFrame>, ExecutionError> {
252 if self.stopped {
253 self.last_debug_var_count = 0;
254 return Ok(None);
255 }
256
257 let resume_ctx = match self.resume_ctx.take() {
258 Some(ctx) => ctx,
259 None => {
260 self.stopped = true;
261 self.last_debug_var_count = 0;
262 return Ok(None);
263 }
264 };
265
266 let debug_info: Option<Arc<PackageDebugInfo>> = resume_ctx.debug_info();
267
268 let CurrentCycleInfo {
270 op,
271 node_id,
272 source_node_id,
273 op_idx,
274 control_flow_kind,
275 } = extract_current_op(&resume_ctx);
276 let debug_node_id = source_node_id.or_else(|| {
277 node_id.zip(debug_info.as_deref()).and_then(|(exec_node, di)| {
278 di.unique_source_root_for_exec_node(exec_node).ok().flatten()
279 })
280 });
281 let source_node = debug_node_id.zip(debug_info.as_deref()).map(|(dnid, di)| &di[dnid]);
282 let asmop = source_node.and_then(|source_node| match op_idx {
283 Some(op_idx) => source_node.asm_op_for_operation(op_idx as u32),
284 None => source_node.asm_op_for_operation(0),
285 });
286 let inline_frames = inline_frames_for_operation(
287 debug_info.as_deref().zip(debug_node_id).map(|(debug_info, debug_node_id)| {
288 (debug_info, debug_node_id, op_idx.unwrap_or_default() as u32)
289 }),
290 resume_ctx.inherited_inline_call_contexts(),
291 );
292
293 let debug_var_infos: Vec<_> = source_node
295 .zip(op_idx)
296 .zip(debug_info.as_deref())
297 .map(|((source_node, op_idx), di)| {
298 source_node.debug_infos_for_operation(op_idx as u32, di).collect()
299 })
300 .unwrap_or_default();
301 let pre_step_stack = self.processor.state().get_stack_state();
302
303 let step_result = if let Some(debug_info) = debug_info.as_deref() {
305 self.processor
306 .step_with_package_debug_info_sync(&mut self.host, resume_ctx, debug_info)
307 } else {
308 self.processor.step_sync(&mut self.host, resume_ctx)
309 };
310 match step_result {
311 Ok(Some(new_ctx)) => {
312 self.resume_ctx = Some(new_ctx);
313 self.cycle += 1;
314
315 let state = self.processor.state();
317 let ctx = state.ctx();
318 self.current_stack = state.get_stack_state();
319
320 if self.current_context != ctx {
321 self.contexts.insert(ctx);
322 self.current_context = ctx;
323 }
324
325 self.current_op = op;
327 self.current_asmop = asmop.zip(debug_info.as_deref()).map(|(asmop, di)| {
328 AssemblyOp::new(
329 asmop.location_idx.into_option().and_then(|loc| di.get_location(loc)),
330 di[asmop.context_name_idx].clone(),
331 asmop.num_cycles,
332 di[asmop.op_name_idx].clone(),
333 )
334 });
335 if let Some(asmop) = self.current_asmop.as_ref() {
336 self.current_proc = Some(asmop.context_name().clone());
337 }
338
339 if let Some(op) = op {
340 if self.recent.len() == 5 {
341 self.recent.pop_front();
342 }
343 self.recent.push_back(op);
344 self.profiler.on_operation_execution_cycle(op, self.current_proc.as_deref());
345 }
346
347 let step_info = StepInfo {
349 op,
350 control: control_flow_kind,
351 asmop: self.current_asmop.as_ref(),
352 clk: RowIndex::from(self.cycle as u32),
353 ctx: self.current_context,
354 inline_frames: &inline_frames,
355 };
356 let exited = self.callstack.next(&step_info);
357
358 let debug_var_count = debug_var_infos.len();
360 self.debug_vars.record_events_with_stack(
361 RowIndex::from(self.cycle as u32),
362 debug_var_infos,
363 &pre_step_stack,
364 );
365 self.debug_vars.update_to_cycle(RowIndex::from(self.cycle as u32));
366 self.last_debug_var_count = debug_var_count;
367
368 Ok(exited)
369 }
370 Ok(None) => {
371 self.stopped = true;
373 self.last_debug_var_count = 0;
374 let state = self.processor.state();
375 self.current_stack = state.get_stack_state();
376
377 let len = self.current_stack.len().min(16);
379 self.stack_outputs =
380 StackOutputs::new(&self.current_stack[..len]).expect("invalid stack outputs");
381
382 self.profiler.write_reports();
384 Ok(None)
385 }
386 Err(err) => {
387 self.stopped = true;
388 self.last_debug_var_count = 0;
389 Err(err)
390 }
391 }
392 }
393
394 pub fn step_until(
399 &mut self,
400 breakpoint: BreakpointType,
401 source_manager: &dyn SourceManager,
402 ) -> Result<(), ExecutionError> {
403 let start_cycle = self.cycle;
404 let breakpoint = Breakpoint {
405 id: 0,
406 creation_cycle: start_cycle,
407 ty: breakpoint,
408 };
409 let start_asmop = self.current_asmop.clone();
410 while !self.stopped {
411 match self.step()? {
412 Some(exited)
413 if exited.should_break_on_exit() && breakpoint.ty == BreakpointType::Finish =>
414 {
415 return Ok(());
416 }
417 _ => (),
418 }
419
420 let (op, is_op_boundary, proc, loc) = {
421 let op = self.current_op;
422 let is_boundary = self.current_asmop.as_ref().map(|_info| true).unwrap_or(false);
423 let (proc, loc) = match self.callstack.current_frame() {
424 Some(frame) => {
425 let loc = frame
426 .recent()
427 .back()
428 .and_then(|detail| detail.resolve(source_manager))
429 .cloned();
430 (frame.procedure(""), loc)
431 }
432 None => (None, None),
433 };
434 (op, is_boundary, proc, loc)
435 };
436
437 if let Some(op) = op
438 && breakpoint.should_break_for(&op, &self.processor.state())
439 {
440 return Ok(());
441 }
442
443 if is_op_boundary
444 && let Some(asmop) = self.current_asmop.as_ref()
445 && matches!(&breakpoint.ty, BreakpointType::Opcode(OperationMatcher::Asm(expected)) if expected.as_str() == asmop.op().as_ref())
446 {
447 return Ok(());
448 }
449
450 let current_cycle = self.cycle;
452 let cycles_stepped = current_cycle - start_cycle;
453 if let Some(n) = breakpoint.cycles_to_skip(current_cycle)
454 && cycles_stepped > 0
455 && n == 0
456 {
457 return Ok(());
458 }
459
460 if cycles_stepped > 0
461 && is_op_boundary
462 && matches!(&breakpoint.ty, BreakpointType::Next)
463 && self.current_asmop != start_asmop
464 {
465 return Ok(());
466 }
467
468 if let Some(loc) = loc.as_ref()
469 && breakpoint.should_break_at(loc)
470 {
471 return Ok(());
472 }
473
474 if let Some(proc) = proc.as_deref()
475 && breakpoint.should_break_in(proc)
476 {
477 return Ok(());
478 }
479 }
480
481 Ok(())
482 }
483
484 pub fn into_execution_trace(self) -> ExecutionTrace {
486 ExecutionTrace {
487 processor: self.processor,
488 outputs: self.stack_outputs,
489 }
490 }
491}
492
493#[cfg(test)]
494mod tests {
495 use std::sync::Arc;
496
497 use miden_assembly::DefaultSourceManager;
498 use miden_mast_package::Package;
499
500 use super::*;
501 use crate::exec::Executor;
502
503 #[test]
504 fn callstack_tracks_nested_frame_events() {
505 use crate::event::{FRAME_END_EVENT, FRAME_START_EVENT};
506 let source_manager = Arc::new(DefaultSourceManager::default());
507 let program = miden_assembly::Assembler::new(source_manager.clone())
508 .assemble_program(
509 "program",
510 format!(
511 r#"
512proc inner
513 emit.event("{FRAME_START_EVENT}")
514 nop
515 emit.event("{FRAME_END_EVENT}")
516end
517
518proc outer
519 emit.event("{FRAME_START_EVENT}")
520 exec.inner
521 emit.event("{FRAME_END_EVENT}")
522end
523
524begin
525 emit.event("{FRAME_START_EVENT}")
526 exec.outer
527 emit.event("{FRAME_END_EVENT}")
528end
529"#
530 ),
531 )
532 .map(Arc::<Package>::from)
533 .unwrap();
534
535 let mut executor = Executor::new(Vec::<Felt>::new()).into_debug(program, source_manager);
536 let mut max_depth = 0;
537 let mut saw_inner = false;
538 let mut snapshots = Vec::new();
539
540 for _ in 0..64 {
541 executor.step().unwrap();
542 let frames = executor.callstack.frames();
543 max_depth = max_depth.max(frames.len());
544 snapshots.push(
545 frames
546 .iter()
547 .map(|frame| {
548 frame
549 .procedure("")
550 .map(|name| name.to_string())
551 .unwrap_or_else(|| "<unknown>".to_string())
552 })
553 .collect::<Vec<_>>(),
554 );
555 saw_inner |= frames.len() >= 3
556 && frames
557 .last()
558 .and_then(|frame| frame.procedure(""))
559 .is_some_and(|name| name.contains("inner"));
560
561 if saw_inner || executor.stopped {
562 break;
563 }
564 }
565
566 assert!(
567 max_depth >= 3,
568 "expected nested main -> outer -> inner frames, max depth was {max_depth}"
569 );
570 assert!(
571 saw_inner,
572 "expected innermost frame to resolve to inner; snapshots: {snapshots:?}"
573 );
574 }
575}