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