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