1use std::{
2 collections::{BTreeSet, VecDeque},
3 rc::Rc,
4 sync::Arc,
5};
6
7use miden_assembly::SourceManager;
8use miden_core::{
9 mast::{MastNode, MastNodeId},
10 operations::AssemblyOp,
11};
12use miden_mast_package::debug_info::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,
23 snapshot_transient_debug_values,
24 },
25 profiling::Profiler,
26};
27
28fn poll_immediately<T>(fut: impl std::future::Future<Output = T>) -> T {
35 let waker = std::task::Waker::noop();
36 let mut cx = std::task::Context::from_waker(waker);
37 let mut fut = std::pin::pin!(fut);
38 match fut.as_mut().poll(&mut cx) {
39 std::task::Poll::Ready(val) => val,
40 std::task::Poll::Pending => panic!("future was expected to complete immediately"),
41 }
42}
43
44pub struct DebugExecutor {
51 pub processor: FastProcessor,
53 pub host: DebuggerHost<dyn miden_assembly::SourceManager>,
55 pub resume_ctx: Option<ResumeContext>,
57
58 pub current_stack: Vec<Felt>,
61 pub current_op: Option<Operation>,
63 pub current_asmop: Option<AssemblyOp>,
65
66 pub stack_outputs: StackOutputs,
68 pub contexts: BTreeSet<ContextId>,
70 pub root_context: ContextId,
72 pub current_context: ContextId,
74 pub callstack: CallStack,
76 pub current_proc: Option<Rc<str>>,
78 pub debug_vars: DebugVarTracker,
80 pub last_debug_var_count: usize,
82 pub recent: VecDeque<Operation>,
84 pub cycle: usize,
86 pub stopped: bool,
88 pub profiler: Profiler,
90}
91
92impl super::query::DebugQuery for DebugExecutor {
93 #[inline]
94 fn state(&self) -> miden_processor::ProcessorState<'_> {
95 self.processor.state()
96 }
97
98 fn current_context(&self) -> ContextId {
99 self.current_context
100 }
101
102 fn current_clock(&self) -> RowIndex {
103 self.processor.state().clock()
104 }
105}
106
107impl DebugExecutor {
108 pub fn stack(&self) -> &[Felt] {
112 self.processor.stack()
113 }
114}
115
116pub(crate) fn extract_current_op(
119 ctx: &ResumeContext,
120) -> (Option<Operation>, Option<MastNodeId>, Option<usize>, Option<ControlFlowOp>) {
121 let forest = ctx.current_forest();
122 for cont in ctx.continuation_stack().iter_continuations_for_next_clock() {
123 match cont {
124 Continuation::ResumeBasicBlock {
125 node_id,
126 batch_index,
127 op_idx_in_batch,
128 } => {
129 let node = &forest[*node_id];
130 if let MastNode::Block(block) = node {
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 (op, Some(*node_id), Some(global_idx), None);
139 }
140 }
141 Continuation::Respan {
142 node_id,
143 batch_index,
144 } => {
145 let node = &forest[*node_id];
146 if let MastNode::Block(block) = node {
147 let mut global_idx = 0;
148 for batch in &block.op_batches()[..*batch_index] {
149 global_idx += batch.ops().len();
150 }
151 return (None, Some(*node_id), Some(global_idx), Some(ControlFlowOp::Respan));
152 }
153 }
154 Continuation::StartNode(node_id) => {
155 let control = match &forest[*node_id] {
156 MastNode::Block(_) => Some(ControlFlowOp::Span),
157 MastNode::Join(_) => Some(ControlFlowOp::Join),
158 MastNode::Split(_) => Some(ControlFlowOp::Split),
159 _ => None,
160 };
161 return (None, Some(*node_id), None, control);
162 }
163 Continuation::FinishBasicBlock(_)
164 | Continuation::FinishJoin(_)
165 | Continuation::FinishSplit(_)
166 | Continuation::FinishLoop { .. }
167 | Continuation::FinishCall(_)
168 | Continuation::FinishDyn(_) => {
169 return (None, None, None, Some(ControlFlowOp::End));
170 }
171 other if other.increments_clk() => {
172 return (None, None, None, None);
173 }
174 _ => continue,
175 }
176 }
177 (None, None, None, None)
178}
179
180impl DebugExecutor {
181 #[allow(unused)]
184 pub fn procedure_has_debug_vars(&self, procedure: &str) -> bool {
185 let Some(resume_ctx) = self.resume_ctx.as_ref() else {
186 return false;
187 };
188 let di: PackageDebugInfo = todo!();
189 let Some(source_map) = di.source_map() else {
196 return false;
197 };
198 for asm_op in source_map.asm_ops() {
199 if asm_op.context_name != procedure {
200 continue;
201 }
202 let mut debug_vars = di.debug_vars_for_operation(asm_op.source_node, asm_op.op_idx);
203 if debug_vars.next().is_some() {
204 return true;
205 }
206 }
207
208 false
209 }
210
211 pub fn step(&mut self) -> Result<Option<CallFrame>, ExecutionError> {
218 if self.stopped {
219 self.last_debug_var_count = 0;
220 return Ok(None);
221 }
222
223 let resume_ctx = match self.resume_ctx.take() {
224 Some(ctx) => ctx,
225 None => {
226 self.stopped = true;
227 self.last_debug_var_count = 0;
228 return Ok(None);
229 }
230 };
231
232 let debug_info: Option<Arc<PackageDebugInfo>> = resume_ctx.debug_info();
233
234 let (op, node_id, op_idx, control) = extract_current_op(&resume_ctx);
236 let debug_node_id = match node_id {
237 Some(nid) => match debug_info.as_deref() {
238 Some(di) => di.unique_source_root_for_exec_node(nid).ok().flatten(),
239 None => None,
240 },
241 None => None,
242 };
243 let asmop = debug_node_id.and_then(|dnid| match op_idx {
244 Some(op_idx) => {
245 debug_info.as_deref().unwrap().asm_op_for_operation(dnid, op_idx as u32)
246 }
247 None => debug_info.as_deref().unwrap().first_asm_op_for_source_node(dnid),
248 });
249
250 let mut debug_var_infos: Vec<_> = if let (Some(di), Some(dnid), Some(op_idx)) =
252 (debug_info.as_deref(), debug_node_id, op_idx)
253 {
254 di.debug_vars_for_operation(dnid, op_idx as u32)
255 .map(|dsv| dsv.var.clone())
256 .collect()
257 } else {
258 vec![]
259 };
260 let pre_step_stack = self.processor.state().get_stack_state();
261 snapshot_transient_debug_values(&mut debug_var_infos, &pre_step_stack);
262
263 match poll_immediately(self.processor.step(&mut self.host, resume_ctx)) {
265 Ok(Some(new_ctx)) => {
266 self.resume_ctx = Some(new_ctx);
267 self.cycle += 1;
268
269 let state = self.processor.state();
271 let ctx = state.ctx();
272 self.current_stack = state.get_stack_state();
273
274 if self.current_context != ctx {
275 self.contexts.insert(ctx);
276 self.current_context = ctx;
277 }
278
279 self.current_op = op;
281 self.current_asmop = asmop.map(|asmop| {
282 AssemblyOp::new(
283 asmop.location.clone(),
284 asmop.context_name.clone(),
285 asmop.num_cycles,
286 asmop.op.clone(),
287 )
288 });
289 if let Some(asmop) = asmop.as_ref() {
290 self.current_proc = Some(Rc::from(asmop.context_name.clone()));
291 }
292
293 if let Some(op) = op {
294 if self.recent.len() == 5 {
295 self.recent.pop_front();
296 }
297 self.recent.push_back(op);
298 self.profiler.on_operation_execution_cycle(op);
299 }
300
301 let step_info = StepInfo {
303 op,
304 control,
305 asmop: self.current_asmop.as_ref(),
306 clk: RowIndex::from(self.cycle as u32),
307 ctx: self.current_context,
308 };
309 let exited = self.callstack.next(&step_info);
310
311 let debug_var_count = debug_var_infos.len();
313 self.debug_vars
314 .record_events(RowIndex::from(self.cycle as u32), debug_var_infos);
315 self.debug_vars.update_to_cycle(RowIndex::from(self.cycle as u32));
316 self.last_debug_var_count = debug_var_count;
317
318 Ok(exited)
319 }
320 Ok(None) => {
321 self.stopped = true;
323 self.last_debug_var_count = 0;
324 let state = self.processor.state();
325 self.current_stack = state.get_stack_state();
326
327 let len = self.current_stack.len().min(16);
329 self.stack_outputs =
330 StackOutputs::new(&self.current_stack[..len]).expect("invalid stack outputs");
331
332 self.profiler.write_reports();
334 Ok(None)
335 }
336 Err(err) => {
337 self.stopped = true;
338 self.last_debug_var_count = 0;
339 Err(err)
340 }
341 }
342 }
343
344 pub fn step_until(
349 &mut self,
350 breakpoint: BreakpointType,
351 source_manager: &dyn SourceManager,
352 ) -> Result<(), ExecutionError> {
353 let start_cycle = self.cycle;
354 let breakpoint = Breakpoint {
355 id: 0,
356 creation_cycle: start_cycle,
357 ty: breakpoint,
358 };
359 let start_asmop = self.current_asmop.clone();
360 while !self.stopped {
361 match self.step()? {
362 Some(exited)
363 if exited.should_break_on_exit() && breakpoint.ty == BreakpointType::Finish =>
364 {
365 return Ok(());
366 }
367 _ => (),
368 }
369
370 let (op, is_op_boundary, proc, loc) = {
371 let op = self.current_op;
372 let is_boundary = self.current_asmop.as_ref().map(|_info| true).unwrap_or(false);
373 let (proc, loc) = match self.callstack.current_frame() {
374 Some(frame) => {
375 let loc = frame
376 .recent()
377 .back()
378 .and_then(|detail| detail.resolve(source_manager))
379 .cloned();
380 (frame.procedure(""), loc)
381 }
382 None => (None, None),
383 };
384 (op, is_boundary, proc, loc)
385 };
386
387 if let Some(op) = op
388 && breakpoint.should_break_for(&op, &self.processor.state())
389 {
390 return Ok(());
391 }
392
393 if is_op_boundary
394 && let Some(asmop) = self.current_asmop.as_ref()
395 && matches!(&breakpoint.ty, BreakpointType::Opcode(OperationMatcher::Asm(expected)) if expected == asmop.op())
396 {
397 return Ok(());
398 }
399
400 let current_cycle = self.cycle;
402 let cycles_stepped = current_cycle - start_cycle;
403 if let Some(n) = breakpoint.cycles_to_skip(current_cycle)
404 && cycles_stepped > 0
405 && n == 0
406 {
407 return Ok(());
408 }
409
410 if cycles_stepped > 0
411 && is_op_boundary
412 && matches!(&breakpoint.ty, BreakpointType::Next)
413 && self.current_asmop != start_asmop
414 {
415 return Ok(());
416 }
417
418 if let Some(loc) = loc.as_ref()
419 && breakpoint.should_break_at(loc)
420 {
421 return Ok(());
422 }
423
424 if let Some(proc) = proc.as_deref()
425 && breakpoint.should_break_in(proc)
426 {
427 return Ok(());
428 }
429 }
430
431 Ok(())
432 }
433
434 pub fn into_execution_trace(self) -> ExecutionTrace {
436 ExecutionTrace {
437 processor: self.processor,
438 outputs: self.stack_outputs,
439 }
440 }
441}
442
443#[cfg(test)]
444mod tests {
445 use std::sync::Arc;
446
447 use miden_assembly::DefaultSourceManager;
448 use miden_mast_package::Package;
449
450 use super::*;
451 use crate::exec::Executor;
452
453 #[test]
454 fn callstack_tracks_nested_frame_events() {
455 use crate::event::{FRAME_END_EVENT, FRAME_START_EVENT};
456 let source_manager = Arc::new(DefaultSourceManager::default());
457 let program = miden_assembly::Assembler::new(source_manager.clone())
458 .assemble_program(
459 "program",
460 format!(
461 r#"
462proc inner
463 emit.event("{FRAME_START_EVENT}")
464 nop
465 emit.event("{FRAME_END_EVENT}")
466end
467
468proc outer
469 emit.event("{FRAME_START_EVENT}")
470 exec.inner
471 emit.event("{FRAME_END_EVENT}")
472end
473
474begin
475 emit.event("{FRAME_START_EVENT}")
476 exec.outer
477 emit.event("{FRAME_END_EVENT}")
478end
479"#
480 ),
481 )
482 .map(Arc::<Package>::from)
483 .unwrap();
484
485 let mut executor = Executor::new(Vec::<Felt>::new()).into_debug(program, source_manager);
486 let mut max_depth = 0;
487 let mut saw_inner = false;
488 let mut snapshots = Vec::new();
489
490 for _ in 0..64 {
491 executor.step().unwrap();
492 let frames = executor.callstack.frames();
493 max_depth = max_depth.max(frames.len());
494 snapshots.push(
495 frames
496 .iter()
497 .map(|frame| {
498 frame
499 .procedure("")
500 .map(|name| name.to_string())
501 .unwrap_or_else(|| "<unknown>".to_string())
502 })
503 .collect::<Vec<_>>(),
504 );
505 saw_inner |= frames.len() >= 3
506 && frames
507 .last()
508 .and_then(|frame| frame.procedure(""))
509 .is_some_and(|name| name.contains("inner"));
510
511 if saw_inner || executor.stopped {
512 break;
513 }
514 }
515
516 assert!(
517 max_depth >= 3,
518 "expected nested main -> outer -> inner frames, max depth was {max_depth}"
519 );
520 assert!(
521 saw_inner,
522 "expected innermost frame to resolve to inner; snapshots: {snapshots:?}"
523 );
524 }
525}