1use alloc::{
2 collections::{BTreeSet, VecDeque},
3 sync::Arc,
4 vec::Vec,
5};
6
7use miden_assembly_syntax::{ast::DebugVarLocation, debuginfo::SourceManager};
8use miden_core::{
9 mast::{MastNode, MastNodeId},
10 operations::AssemblyOp,
11};
12use miden_mast_package::debug_info::{DebugSourceNodeId, PackageDebugInfo};
13use miden_processor::{
14 ContextId, Continuation, ExecutionError, FastProcessor, Felt, ResumeContext, StackOutputs,
15 operation::Operation, trace::RowIndex,
16};
17
18use super::{DebuggerHost, ExecutionTrace};
19use crate::{
20 Breakpoint, BreakpointType, OperationMatcher,
21 debug::{
22 CallFrame, CallStack, ControlFlowOp, DebugVarTracker, StepInfo, inline_frames_for_operation,
23 },
24 profiling::Profiler,
25};
26
27pub struct DebugExecutor {
34 pub processor: FastProcessor,
36 pub host: DebuggerHost<dyn miden_assembly_syntax::debuginfo::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
214pub(crate) fn should_wait_for_entry_variables(
218 resume_ctx: &ResumeContext,
219 procedure: &str,
220 variables: &DebugVarTracker,
221 cycle: usize,
222) -> bool {
223 if variables
224 .current_variables()
225 .any(|variable| variable.clk == RowIndex::from(cycle as u32))
226 {
227 return false;
228 }
229 let Some(debug_info) = resume_ctx.debug_info() else {
230 return false;
231 };
232 let next = extract_current_op(resume_ctx);
233 let Some(source_node) = next.source_node_id.map(|node| &debug_info[node]) else {
234 return false;
235 };
236 let Some(op_idx) = next.op_idx else {
237 return false;
238 };
239 source_node.debug_vars.iter().any(|variable| {
240 variable.op_idx as usize >= op_idx
241 && !matches!(variable.value_location, DebugVarLocation::Unavailable)
242 && source_node.asm_op_for_operation(variable.op_idx).is_some_and(|operation| {
243 debug_info[operation.context_name_idx].as_ref() == procedure
244 })
245 })
246}
247
248impl DebugExecutor {
249 pub fn should_wait_for_entry_variables(&self, procedure: &str) -> bool {
251 self.resume_ctx.as_ref().is_some_and(|resume_ctx| {
252 should_wait_for_entry_variables(resume_ctx, procedure, &self.debug_vars, self.cycle)
253 })
254 }
255
256 pub fn step(&mut self) -> Result<Option<CallFrame>, ExecutionError> {
263 if self.stopped {
264 self.last_debug_var_count = 0;
265 return Ok(None);
266 }
267
268 let resume_ctx = match self.resume_ctx.take() {
269 Some(ctx) => ctx,
270 None => {
271 self.stopped = true;
272 self.last_debug_var_count = 0;
273 return Ok(None);
274 }
275 };
276
277 let debug_info: Option<Arc<PackageDebugInfo>> = resume_ctx.debug_info();
278
279 let CurrentCycleInfo {
281 op,
282 node_id,
283 source_node_id,
284 op_idx,
285 control_flow_kind,
286 } = extract_current_op(&resume_ctx);
287 let debug_node_id = source_node_id.or_else(|| {
288 node_id.zip(debug_info.as_deref()).and_then(|(exec_node, di)| {
289 di.unique_source_root_for_exec_node(exec_node).ok().flatten()
290 })
291 });
292 let source_node = debug_node_id.zip(debug_info.as_deref()).map(|(dnid, di)| &di[dnid]);
293 let asmop = source_node.and_then(|source_node| match op_idx {
294 Some(op_idx) => source_node.asm_op_for_operation(op_idx as u32),
295 None => source_node.asm_op_for_operation(0),
296 });
297 let inline_frames = inline_frames_for_operation(
298 debug_info.as_deref().zip(debug_node_id).map(|(debug_info, debug_node_id)| {
299 (debug_info, debug_node_id, op_idx.unwrap_or_default() as u32)
300 }),
301 resume_ctx.inherited_inline_call_contexts(),
302 );
303
304 let debug_var_infos: Vec<_> = source_node
306 .zip(op_idx)
307 .zip(debug_info.as_deref())
308 .map(|((source_node, op_idx), di)| {
309 source_node.debug_infos_for_operation(op_idx as u32, di).collect()
310 })
311 .unwrap_or_default();
312 let pre_step_stack = self.processor.state().get_stack_state();
313
314 let step_result = if let Some(debug_info) = debug_info.as_deref() {
316 self.processor
317 .step_with_package_debug_info_sync(&mut self.host, resume_ctx, debug_info)
318 } else {
319 self.processor.step_sync(&mut self.host, resume_ctx)
320 };
321 match step_result {
322 Ok(Some(new_ctx)) => {
323 self.resume_ctx = Some(new_ctx);
324 self.cycle += 1;
325
326 let state = self.processor.state();
328 let ctx = state.ctx();
329 self.current_stack = state.get_stack_state();
330
331 if self.current_context != ctx {
332 self.contexts.insert(ctx);
333 self.current_context = ctx;
334 }
335
336 self.current_op = op;
338 self.current_asmop = asmop.zip(debug_info.as_deref()).map(|(asmop, di)| {
339 AssemblyOp::new(
340 asmop.location_idx.into_option().and_then(|loc| di.get_location(loc)),
341 di[asmop.context_name_idx].clone(),
342 asmop.num_cycles,
343 di[asmop.op_name_idx].clone(),
344 )
345 });
346 if let Some(asmop) = self.current_asmop.as_ref() {
347 self.current_proc = Some(asmop.context_name().clone());
348 }
349
350 if let Some(op) = op {
351 if self.recent.len() == 5 {
352 self.recent.pop_front();
353 }
354 self.recent.push_back(op);
355 self.profiler.on_operation_execution_cycle(op, self.current_proc.as_deref());
356 }
357
358 let step_info = StepInfo {
360 op,
361 control: control_flow_kind,
362 asmop: self.current_asmop.as_ref(),
363 clk: RowIndex::from(self.cycle as u32),
364 ctx: self.current_context,
365 inline_frames: &inline_frames,
366 };
367 let exited = self.callstack.next(&step_info);
368
369 let debug_var_count = debug_var_infos.len();
371 self.debug_vars.record_events_with_stack(
372 RowIndex::from(self.cycle as u32),
373 debug_var_infos,
374 &pre_step_stack,
375 );
376 self.debug_vars.update_to_cycle(RowIndex::from(self.cycle as u32));
377 self.last_debug_var_count = debug_var_count;
378
379 Ok(exited)
380 }
381 Ok(None) => {
382 self.stopped = true;
384 self.last_debug_var_count = 0;
385 let state = self.processor.state();
386 self.current_stack = state.get_stack_state();
387
388 let len = self.current_stack.len().min(16);
390 self.stack_outputs =
391 StackOutputs::new(&self.current_stack[..len]).expect("invalid stack outputs");
392
393 #[cfg(feature = "std")]
395 {
396 self.profiler.write_reports();
397 }
398 Ok(None)
399 }
400 Err(err) => {
401 self.stopped = true;
402 self.last_debug_var_count = 0;
403 Err(err)
404 }
405 }
406 }
407
408 pub fn step_until(
413 &mut self,
414 breakpoint: BreakpointType,
415 source_manager: &dyn SourceManager,
416 ) -> Result<(), ExecutionError> {
417 let start_cycle = self.cycle;
418 let breakpoint = Breakpoint {
419 id: 0,
420 creation_cycle: start_cycle,
421 ty: breakpoint,
422 };
423 let start_asmop = self.current_asmop.clone();
424 while !self.stopped {
425 match self.step()? {
426 Some(exited)
427 if exited.should_break_on_exit() && breakpoint.ty == BreakpointType::Finish =>
428 {
429 return Ok(());
430 }
431 _ => (),
432 }
433
434 let (op, is_op_boundary, proc, loc) = {
435 let op = self.current_op;
436 let is_boundary = self.current_asmop.as_ref().map(|_info| true).unwrap_or(false);
437 let (proc, loc) = match self.callstack.current_frame() {
438 Some(frame) => {
439 let loc = frame
440 .recent()
441 .back()
442 .and_then(|detail| detail.resolve(source_manager))
443 .cloned();
444 (frame.procedure(""), loc)
445 }
446 None => (None, None),
447 };
448 (op, is_boundary, proc, loc)
449 };
450
451 if let Some(op) = op
452 && breakpoint.should_break_for(&op, &self.processor.state())
453 {
454 return Ok(());
455 }
456
457 if is_op_boundary
458 && let Some(asmop) = self.current_asmop.as_ref()
459 && matches!(&breakpoint.ty, BreakpointType::Opcode(OperationMatcher::Asm(expected)) if expected.as_str() == asmop.op().as_ref())
460 {
461 return Ok(());
462 }
463
464 let current_cycle = self.cycle;
466 let cycles_stepped = current_cycle - start_cycle;
467 if let Some(n) = breakpoint.cycles_to_skip(current_cycle)
468 && cycles_stepped > 0
469 && n == 0
470 {
471 return Ok(());
472 }
473
474 if cycles_stepped > 0
475 && is_op_boundary
476 && matches!(&breakpoint.ty, BreakpointType::Next)
477 && self.current_asmop != start_asmop
478 {
479 return Ok(());
480 }
481
482 if let Some(loc) = loc.as_ref()
483 && breakpoint.should_break_at(loc)
484 {
485 return Ok(());
486 }
487
488 if let Some(proc) = proc.as_deref()
489 && breakpoint.should_break_in(proc)
490 {
491 return Ok(());
492 }
493 }
494
495 Ok(())
496 }
497
498 pub fn into_execution_trace(self) -> ExecutionTrace {
500 ExecutionTrace {
501 processor: self.processor,
502 outputs: self.stack_outputs,
503 }
504 }
505}
506
507#[cfg(test)]
508mod tests {
509 use alloc::string::ToString;
510 use std::sync::Arc;
511
512 use miden_assembly::DefaultSourceManager;
513 use miden_mast_package::Package;
514
515 use super::*;
516 use crate::exec::Executor;
517
518 #[test]
519 fn callstack_tracks_nested_frame_events() {
520 use crate::event::{FRAME_END_EVENT, FRAME_START_EVENT};
521 let source_manager = Arc::new(DefaultSourceManager::default());
522 let program = miden_assembly::Assembler::new(source_manager.clone())
523 .assemble_program(
524 "program",
525 format!(
526 r#"
527proc inner
528 emit.event("{FRAME_START_EVENT}")
529 nop
530 emit.event("{FRAME_END_EVENT}")
531end
532
533proc outer
534 emit.event("{FRAME_START_EVENT}")
535 exec.inner
536 emit.event("{FRAME_END_EVENT}")
537end
538
539begin
540 emit.event("{FRAME_START_EVENT}")
541 exec.outer
542 emit.event("{FRAME_END_EVENT}")
543end
544"#
545 ),
546 )
547 .map(Arc::<Package>::from)
548 .unwrap();
549
550 let mut executor = Executor::new(Vec::<Felt>::new()).into_debug(program, source_manager);
551 let mut max_depth = 0;
552 let mut saw_inner = false;
553 let mut snapshots = Vec::new();
554
555 for _ in 0..64 {
556 executor.step().unwrap();
557 let frames = executor.callstack.frames();
558 max_depth = max_depth.max(frames.len());
559 snapshots.push(
560 frames
561 .iter()
562 .map(|frame| {
563 frame
564 .procedure("")
565 .map(|name| name.to_string())
566 .unwrap_or_else(|| "<unknown>".to_string())
567 })
568 .collect::<Vec<_>>(),
569 );
570 saw_inner |= frames.len() >= 3
571 && frames
572 .last()
573 .and_then(|frame| frame.procedure(""))
574 .is_some_and(|name| name.contains("inner"));
575
576 if saw_inner || executor.stopped {
577 break;
578 }
579 }
580
581 assert!(
582 max_depth >= 3,
583 "expected nested main -> outer -> inner frames, max depth was {max_depth}"
584 );
585 assert!(
586 saw_inner,
587 "expected innermost frame to resolve to inner; snapshots: {snapshots:?}"
588 );
589 }
590}