1use std::{
2 borrow::Cow,
3 cell::OnceCell,
4 collections::{BTreeMap, BTreeSet, VecDeque},
5 fmt,
6 path::{Path, PathBuf},
7 sync::{Arc, Mutex},
8};
9
10use miden_core::operations::AssemblyOp;
11use miden_debug_types::{Location, SourceFile, SourceManager, SourceManagerExt, SourceSpan, Uri};
12use miden_processor::{ContextId, operation::Operation, trace::RowIndex};
13
14use crate::Event;
15
16#[derive(Copy, Clone, Debug, Eq, PartialEq)]
17pub enum ControlFlowOp {
18 Span,
19 Respan,
20 Join,
21 Split,
22 End,
23}
24
25pub struct StepInfo<'a> {
26 pub op: Option<Operation>,
27 pub control: Option<ControlFlowOp>,
28 pub asmop: Option<&'a AssemblyOp>,
29 pub clk: RowIndex,
30 pub ctx: ContextId,
31}
32
33#[derive(Debug, Clone)]
34struct SpanContext {
35 frame_index: usize,
36 location: Option<Location>,
37}
38
39pub struct CallStack {
40 events: Arc<Mutex<BTreeMap<RowIndex, Event>>>,
41 contexts: BTreeSet<Arc<str>>,
42 frames: Vec<CallFrame>,
43 block_stack: Vec<Option<SpanContext>>,
44}
45impl CallStack {
46 pub fn new(events: Arc<Mutex<BTreeMap<RowIndex, Event>>>) -> Self {
47 Self {
48 events,
49 contexts: BTreeSet::default(),
50 frames: vec![],
51 block_stack: vec![],
52 }
53 }
54
55 #[cfg(feature = "dap")]
57 pub fn from_remote_frames(frames: Vec<CallFrame>) -> Self {
58 Self {
59 events: Arc::new(Default::default()),
60 contexts: BTreeSet::default(),
61 frames,
62 block_stack: vec![],
63 }
64 }
65
66 pub fn stacktrace<'a>(
67 &'a self,
68 recent: &'a VecDeque<Operation>,
69 source_manager: &'a dyn SourceManager,
70 ) -> StackTrace<'a> {
71 StackTrace::new(self, recent, source_manager)
72 }
73
74 pub fn current_frame(&self) -> Option<&CallFrame> {
75 self.frames.last()
76 }
77
78 pub fn current_frame_mut(&mut self) -> Option<&mut CallFrame> {
79 self.frames.last_mut()
80 }
81
82 pub fn frames(&self) -> &[CallFrame] {
83 self.frames.as_slice()
84 }
85
86 pub fn next(&mut self, info: &StepInfo<'_>) -> Option<CallFrame> {
90 let procedure = info.asmop.map(|op| self.cache_procedure_name(op.context_name()));
91
92 let event = {
93 let mut events = self.events.lock().unwrap();
94 match events.first_key_value() {
95 Some((clk, _)) if *clk <= info.clk => events.pop_first().map(|(_, event)| event),
96 _ => None,
97 }
98 };
99 log::trace!("handling {:?}/{:?} at cycle {}: {:?}", info.control, info.op, info.clk, event);
100 let is_frame_start = event.as_ref().is_some_and(|event| event.is_frame_start());
101 let popped_frame = self.handle_event(event, procedure.clone(), info.op, info.asmop);
102 let is_frame_end = popped_frame.is_some();
103
104 match info.control {
105 Some(ControlFlowOp::Span) => {
106 if let Some(asmop) = info.asmop {
107 log::debug!("{asmop:#?}");
108 self.block_stack.push(Some(SpanContext {
109 frame_index: self.frames.len().saturating_sub(1),
110 location: asmop.location().cloned(),
111 }));
112 } else {
113 self.block_stack.push(None);
114 }
115 }
116 Some(ControlFlowOp::Join | ControlFlowOp::Split) => {
117 self.block_stack.push(None);
118 }
119 Some(ControlFlowOp::End) => {
120 self.block_stack.pop();
121 }
122 Some(ControlFlowOp::Respan) | None => {}
123 }
124
125 let Some(op) = info.op else {
126 return popped_frame;
127 };
128
129 if is_frame_start || is_frame_end {
130 return popped_frame;
131 }
132
133 let (procedure, asmop) = match procedure {
136 proc @ Some(_) => (proc, info.asmop.map(Cow::Borrowed)),
137 None => match self.block_stack.last() {
138 Some(Some(span_ctx)) => {
139 let proc =
140 self.frames.get(span_ctx.frame_index).and_then(|f| f.procedure.clone());
141 let asmop_cow = info.asmop.map(Cow::Borrowed).or_else(|| {
142 let context_name = proc.as_deref().unwrap_or("<unknown>").to_string();
143 let raw_asmop = AssemblyOp::new(
144 span_ctx.location.clone(),
145 context_name,
146 1,
147 op.to_string(),
148 );
149 Some(Cow::Owned(raw_asmop))
150 });
151 (proc, asmop_cow)
152 }
153 _ => (None, info.asmop.map(Cow::Borrowed)),
154 },
155 };
156
157 let procedure = procedure.or_else(|| self.frames.last().and_then(|f| f.procedure.clone()));
160
161 if self.frames.is_empty() {
163 self.frames.push(CallFrame::new(procedure.clone()));
164 }
165
166 let current_frame = self.frames.last_mut().unwrap();
167
168 let procedure_context_updated = current_frame.procedure.is_none() && procedure.is_some();
171 if procedure_context_updated {
172 current_frame.procedure.clone_from(&procedure);
173 }
174
175 if !matches!(op, Operation::Noop) {
177 let cycle_idx = info.asmop.map(|a| a.num_cycles()).unwrap_or(1);
178 current_frame.push(op, cycle_idx, asmop.as_deref());
179 }
180
181 let num_frames = self.frames.len();
183 if procedure_context_updated && num_frames > 1 {
184 let caller_frame = &mut self.frames[num_frames - 2];
185 if let Some(OpDetail::Exec { callee }) = caller_frame.context.back_mut()
186 && callee.is_none()
187 {
188 *callee = procedure;
189 }
190 }
191
192 popped_frame
193 }
194
195 fn cache_procedure_name(&mut self, context_name: &str) -> Arc<str> {
197 match self.contexts.get(context_name) {
198 Some(name) => Arc::clone(name),
199 None => {
200 let name = Arc::from(context_name.to_string().into_boxed_str());
201 self.contexts.insert(Arc::clone(&name));
202 name
203 }
204 }
205 }
206
207 fn handle_event(
208 &mut self,
209 event: Option<Event>,
210 procedure: Option<Arc<str>>,
211 op: Option<Operation>,
212 asmop: Option<&AssemblyOp>,
213 ) -> Option<CallFrame> {
214 match event? {
216 Event::FrameStart => {
217 if let Some(current_frame) = self.frames.last_mut() {
219 current_frame.push_exec(procedure.clone());
220 }
221 let mut frame = CallFrame::new(procedure);
223 if let Some(op) = op {
224 frame.push(op, 0, asmop);
225 }
226 self.frames.push(frame);
227 }
228 Event::Unknown(code) => log::debug!("unknown trace event: {code}"),
229 Event::FrameEnd => {
230 return self.frames.pop();
231 }
232 _ => (),
233 }
234 None
235 }
236}
237
238pub struct CallFrame {
239 procedure: Option<Arc<str>>,
240 context: VecDeque<OpDetail>,
241 display_name: std::cell::OnceCell<Arc<str>>,
242 finishing: bool,
243}
244impl CallFrame {
245 pub fn new(procedure: Option<Arc<str>>) -> Self {
246 Self {
247 procedure,
248 context: Default::default(),
249 display_name: Default::default(),
250 finishing: false,
251 }
252 }
253
254 #[cfg(feature = "dap")]
260 pub fn from_remote(procedure: Option<Arc<str>>, resolved: Option<ResolvedLocation>) -> Self {
261 let mut context = VecDeque::new();
262 if let Some(loc) = resolved {
263 let cell = OnceCell::new();
264 cell.set(Some(loc)).ok();
265 context.push_back(OpDetail::Full {
266 op: miden_processor::operation::Operation::Noop,
267 location: None,
268 resolved: cell,
269 });
270 }
271 Self {
272 procedure,
273 context,
274 display_name: Default::default(),
275 finishing: false,
276 }
277 }
278
279 pub fn procedure(&self, strip_prefix: &str) -> Option<Arc<str>> {
280 self.procedure.as_ref()?;
281 let name = self.display_name.get_or_init(|| {
282 let name = self.procedure.as_deref().unwrap();
283 let name = match name.split_once("::") {
284 Some((module, rest)) if module == strip_prefix => demangle(rest),
285 _ => demangle(name),
286 };
287 Arc::<str>::from(name.into_boxed_str())
288 });
289 Some(Arc::clone(name))
290 }
291
292 pub fn push_exec(&mut self, callee: Option<Arc<str>>) {
293 if self.context.len() == 5 {
294 self.context.pop_front();
295 }
296
297 self.context.push_back(OpDetail::Exec { callee });
298 }
299
300 pub fn push(&mut self, opcode: Operation, cycle_idx: u8, op: Option<&AssemblyOp>) {
301 if cycle_idx > 1 {
302 let skip = self.context.back().map(|detail| matches!(detail, OpDetail::Full { op, .. } | OpDetail::Basic { op } if op == &opcode)).unwrap_or(false);
304 if skip {
305 return;
306 }
307 }
308
309 if self.context.len() == 5 {
310 self.context.pop_front();
311 }
312
313 match op {
314 Some(op) => {
315 let location = op.location().cloned();
316 self.context.push_back(OpDetail::Full {
317 op: opcode,
318 location,
319 resolved: Default::default(),
320 });
321 }
322 None => {
323 if let Some(loc) = self.context.back().map(|op| op.location().cloned()) {
326 self.context.push_back(OpDetail::Full {
327 op: opcode,
328 location: loc,
329 resolved: Default::default(),
330 });
331 } else {
332 self.context.push_back(OpDetail::Basic { op: opcode });
333 }
334 }
335 }
336 }
337
338 pub fn last_location(&self) -> Option<&Location> {
339 match self.context.back() {
340 Some(OpDetail::Full { location, .. }) => location.as_ref(),
341 Some(OpDetail::Basic { .. }) => None,
342 Some(OpDetail::Exec { .. }) => {
343 let op = self.context.iter().rev().nth(1)?;
344 op.location()
345 }
346 None => None,
347 }
348 }
349
350 pub fn last_resolved(&self, source_manager: &dyn SourceManager) -> Option<&ResolvedLocation> {
351 for op in self.context.iter().rev() {
354 if let Some(resolved) = op.resolve(source_manager) {
355 return Some(resolved);
356 }
357 }
358 None
359 }
360
361 pub fn recent(&self) -> &VecDeque<OpDetail> {
362 &self.context
363 }
364
365 #[inline(always)]
366 pub fn should_break_on_exit(&self) -> bool {
367 self.finishing
368 }
369
370 #[inline(always)]
371 pub fn break_on_exit(&mut self) {
372 self.finishing = true;
373 }
374}
375
376#[derive(Debug, Clone)]
377pub enum OpDetail {
378 Full {
379 op: Operation,
380 location: Option<Location>,
381 resolved: OnceCell<Option<ResolvedLocation>>,
382 },
383 Exec {
384 callee: Option<Arc<str>>,
385 },
386 Basic {
387 op: Operation,
388 },
389}
390impl OpDetail {
391 pub fn callee(&self, strip_prefix: &str) -> Option<Box<str>> {
392 match self {
393 Self::Exec { callee: None } => Some(Box::from("<unknown>")),
394 Self::Exec {
395 callee: Some(callee),
396 } => {
397 let name = match callee.split_once("::") {
398 Some((module, rest)) if module == strip_prefix => demangle(rest),
399 _ => demangle(callee),
400 };
401 Some(name.into_boxed_str())
402 }
403 _ => None,
404 }
405 }
406
407 pub fn display(&self) -> String {
408 match self {
409 Self::Full { op, .. } | Self::Basic { op } => format!("{op}"),
410 Self::Exec {
411 callee: Some(callee),
412 } => format!("exec.{callee}"),
413 Self::Exec { callee: None } => "exec.<unavailable>".to_string(),
414 }
415 }
416
417 pub fn opcode(&self) -> Operation {
418 match self {
419 Self::Full { op, .. } | Self::Basic { op } => *op,
420 Self::Exec { .. } => panic!("no opcode associated with execs"),
421 }
422 }
423
424 pub fn location(&self) -> Option<&Location> {
425 match self {
426 Self::Full { location, .. } => location.as_ref(),
427 Self::Basic { .. } | Self::Exec { .. } => None,
428 }
429 }
430
431 pub fn resolve(&self, source_manager: &dyn SourceManager) -> Option<&ResolvedLocation> {
432 match self {
433 Self::Full {
434 location: Some(loc),
435 resolved,
436 ..
437 } => resolved
438 .get_or_init(|| {
439 let source_file = resolve_source_file_for_location(source_manager, loc)?;
440 let span = SourceSpan::new(source_file.id(), loc.start..loc.end);
441 let file_line_col = source_file.location(span);
442 Some(ResolvedLocation {
443 source_file,
444 line: file_line_col.line.to_u32(),
445 col: file_line_col.column.to_u32(),
446 span,
447 })
448 })
449 .as_ref(),
450 _ => None,
451 }
452 }
453}
454
455pub fn resolve_source_file_for_location(
461 source_manager: &dyn SourceManager,
462 location: &Location,
463) -> Option<Arc<SourceFile>> {
464 source_manager.get_by_uri(location.uri()).or_else(|| {
465 resolve_source_path(location.uri()).and_then(|path| source_manager.load_file(&path).ok())
466 })
467}
468
469pub fn resolve_source_path(uri: &Uri) -> Option<PathBuf> {
474 let path = match uri.scheme() {
475 None | Some("file") => Path::new(uri.path()),
476 Some(_) => return None,
477 };
478
479 existing_path(path).or_else(|| {
480 if path.is_relative() {
481 std::env::current_dir().ok().and_then(|cwd| existing_path(&cwd.join(path)))
482 } else {
483 None
484 }
485 })
486}
487
488pub fn resolve_location_from_filesystem(location: &Location) -> Option<(PathBuf, u32)> {
490 let path = resolve_source_path(location.uri())?;
491 let bytes = std::fs::read(&path).ok()?;
492 let start = location.start.to_usize().min(bytes.len());
493 let line = bytes[..start].iter().filter(|byte| **byte == b'\n').count() as u32 + 1;
494 Some((path, line))
495}
496
497pub fn is_internal_source_uri(uri: &Uri) -> bool {
499 let path = uri.path().replace('\\', "/");
500 path.contains("/codegen/masm/intrinsics/") || path.contains("/rustlib/src/rust/library/")
501}
502
503fn existing_path(path: &Path) -> Option<PathBuf> {
504 path.exists()
505 .then(|| path.canonicalize().unwrap_or_else(|_| path.to_path_buf()))
506}
507
508#[derive(Debug, Clone)]
509pub struct ResolvedLocation {
510 pub source_file: Arc<SourceFile>,
511 pub line: u32,
513 pub col: u32,
514 pub span: SourceSpan,
515}
516impl fmt::Display for ResolvedLocation {
517 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
518 write!(f, "{}:{}:{}", self.source_file.uri().as_str(), self.line, self.col)
519 }
520}
521
522pub struct CurrentFrame {
523 pub procedure: Option<Arc<str>>,
524 pub location: Option<ResolvedLocation>,
525}
526
527pub struct StackTrace<'a> {
528 callstack: &'a CallStack,
529 recent: &'a VecDeque<Operation>,
530 source_manager: &'a dyn SourceManager,
531 current_frame: Option<CurrentFrame>,
532}
533
534impl<'a> StackTrace<'a> {
535 pub fn new(
536 callstack: &'a CallStack,
537 recent: &'a VecDeque<Operation>,
538 source_manager: &'a dyn SourceManager,
539 ) -> Self {
540 let current_frame = callstack.current_frame().map(|frame| {
541 let location = frame.last_resolved(source_manager).cloned();
542 let procedure = frame.procedure("");
543 CurrentFrame {
544 procedure,
545 location,
546 }
547 });
548 Self {
549 callstack,
550 recent,
551 source_manager,
552 current_frame,
553 }
554 }
555
556 pub fn current_frame(&self) -> Option<&CurrentFrame> {
557 self.current_frame.as_ref()
558 }
559}
560
561impl fmt::Display for StackTrace<'_> {
562 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
563 use std::fmt::Write;
564
565 let num_frames = self.callstack.frames.len();
566
567 writeln!(f, "\nStack Trace:")?;
568
569 for (i, frame) in self.callstack.frames.iter().enumerate() {
570 let is_top = i + 1 == num_frames;
571 let name = frame.procedure("");
572 let name = name.as_deref().unwrap_or("<unknown>");
573 if is_top {
574 write!(f, " `-> {name}")?;
575 } else {
576 write!(f, " |-> {name}")?;
577 }
578 if let Some(resolved) = frame.last_resolved(self.source_manager) {
579 write!(f, " in {resolved}")?;
580 } else {
581 write!(f, " in <unavailable>")?;
582 }
583 if is_top {
584 let context_size = frame.context.len();
586 writeln!(f, ":\n\nLast {context_size} Instructions (of current frame):")?;
587 for (i, op) in frame.context.iter().enumerate() {
588 let is_last = i + 1 == context_size;
589 if let Some(callee) = op.callee("") {
590 write!(f, " | exec.{callee}")?;
591 } else {
592 write!(f, " | {}", op.opcode())?;
593 }
594 if is_last {
595 writeln!(f, "\n `-> <error occurred here>")?;
596 } else {
597 f.write_char('\n')?;
598 }
599 }
600
601 let context_size = self.recent.len();
602 writeln!(f, "\n\nLast {context_size} Instructions (any frame):")?;
603 for (i, op) in self.recent.iter().enumerate() {
604 let is_last = i + 1 == context_size;
605 if is_last {
606 writeln!(f, " | {}", op)?;
607 writeln!(f, " `-> <error occurred here>")?;
608 } else {
609 writeln!(f, " | {}", op)?;
610 }
611 }
612 } else {
613 f.write_char('\n')?;
614 }
615 }
616
617 Ok(())
618 }
619}
620
621fn demangle(name: &str) -> String {
622 let mut input = name.as_bytes();
623 let mut demangled = Vec::with_capacity(input.len() * 2);
624 rustc_demangle::demangle_stream(&mut input, &mut demangled, false)
625 .expect("failed to write demangled identifier");
626 String::from_utf8(demangled).expect("demangled identifier contains invalid utf-8")
627}
628
629#[cfg(test)]
630mod tests {
631 use std::{cell::OnceCell, fs, path::PathBuf};
632
633 use miden_assembly::DefaultSourceManager;
634 use miden_debug_types::{ByteIndex, Location, Uri};
635
636 use super::*;
637
638 #[test]
639 fn resolves_relative_source_locations_from_filesystem() {
640 let path = test_source_path("relative");
641 fs::create_dir_all(path.parent().unwrap()).unwrap();
642 fs::write(&path, "fn main() {\n let x = 1;\n}\n").unwrap();
643
644 let start = "fn main() {\n ".len() as u32;
645 let location = Location::new(
646 Uri::from(path.display().to_string()),
647 ByteIndex::new(start),
648 ByteIndex::new(start + 5),
649 );
650 let detail = OpDetail::Full {
651 op: Operation::Noop,
652 location: Some(location),
653 resolved: OnceCell::new(),
654 };
655 let source_manager = DefaultSourceManager::default();
656
657 let resolved = detail.resolve(&source_manager).expect("source should resolve");
658 assert_eq!(resolved.line, 2);
659 assert!(resolved.source_file.uri().as_str().ends_with("src/lib.rs"));
660
661 fs::remove_dir_all(path.parent().unwrap().parent().unwrap()).ok();
662 }
663
664 fn test_source_path(test_name: &str) -> PathBuf {
665 PathBuf::from("target")
666 .join("debugger-source-tests")
667 .join(format!("{}-{}", test_name, std::process::id()))
668 .join("src")
669 .join("lib.rs")
670 }
671}