1use alloc::{
2 borrow::Cow,
3 boxed::Box,
4 collections::{BTreeMap, BTreeSet, VecDeque},
5 string::{String, ToString},
6 sync::Arc,
7 vec::Vec,
8};
9use core::{cell::OnceCell, fmt};
10#[cfg(feature = "std")]
11use std::path::{Path, PathBuf};
12
13use miden_core::operations::AssemblyOp;
14use miden_debug_types::{Location, SourceFile, SourceManager, SourceSpan, Uri};
15use miden_mast_package::debug_info::{DebugSourceInlineCall, DebugSourceNodeId, PackageDebugInfo};
16use miden_processor::{ContextId, SourceInlineCallContext, operation::Operation, trace::RowIndex};
17use miden_utils_sync::RwLock;
18
19use crate::Event;
20
21#[derive(Copy, Clone, Debug, Eq, PartialEq)]
22pub enum ControlFlowOp {
23 Span,
24 Respan,
25 Join,
26 Split,
27 End,
28}
29
30pub struct StepInfo<'a> {
31 pub op: Option<Operation>,
32 pub control: Option<ControlFlowOp>,
33 pub asmop: Option<&'a AssemblyOp>,
34 pub clk: RowIndex,
35 pub ctx: ContextId,
36 pub inline_frames: &'a [InlineCallFrame],
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct InlineCallFrame {
41 name: Arc<str>,
42 call_site: Location,
43}
44
45impl InlineCallFrame {
46 #[cfg(all(test, feature = "dap"))]
47 pub(crate) fn new_for_test(name: impl Into<Arc<str>>, call_site: Location) -> Self {
48 Self {
49 name: name.into(),
50 call_site,
51 }
52 }
53
54 pub fn name(&self) -> &str {
55 &self.name
56 }
57
58 pub fn call_site(&self) -> &Location {
59 &self.call_site
60 }
61
62 pub fn display_name(&self) -> String {
63 demangle(&self.name)
64 }
65}
66
67#[derive(Debug, Copy, Clone, PartialEq, Eq)]
68pub enum LogicalFrameKind {
69 Physical,
70 Inline,
71}
72
73#[derive(Debug, Clone)]
74enum LogicalFrameLocation {
75 Assembly(Location),
76 Resolved(ResolvedLocation),
77}
78
79#[derive(Debug, Clone)]
80pub struct LogicalStackFrame {
81 name: Arc<str>,
82 kind: LogicalFrameKind,
83 location: Option<LogicalFrameLocation>,
84 physical_index: usize,
85}
86
87impl LogicalStackFrame {
88 pub fn name(&self) -> &str {
89 &self.name
90 }
91
92 pub fn kind(&self) -> LogicalFrameKind {
93 self.kind
94 }
95
96 pub fn physical_index(&self) -> usize {
97 self.physical_index
98 }
99
100 pub fn display_name(&self) -> String {
101 match self.kind {
102 LogicalFrameKind::Physical => self.name.to_string(),
103 LogicalFrameKind::Inline => format!("[inlined] {}", self.name),
104 }
105 }
106
107 pub fn resolved(&self, source_manager: &dyn SourceManager) -> Option<ResolvedLocation> {
108 match self.location.as_ref()? {
109 LogicalFrameLocation::Assembly(location) => {
110 resolve_assembly_location(source_manager, location)
111 }
112 LogicalFrameLocation::Resolved(resolved) => Some(resolved.clone()),
113 }
114 }
115}
116
117pub fn inline_frames_for_operation<'a>(
122 current: Option<(&PackageDebugInfo, DebugSourceNodeId, u32)>,
123 inherited: impl IntoIterator<Item = &'a SourceInlineCallContext>,
124) -> Vec<InlineCallFrame> {
125 let mut frames = Vec::new();
126 if let Some((debug_info, source_node, op_idx)) = current {
127 append_inline_frames(
128 &mut frames,
129 debug_info,
130 debug_info.inline_calls_for_operation(source_node, op_idx),
131 );
132 }
133 for context in inherited {
134 append_inline_frames(&mut frames, context.debug_info(), context.inline_calls());
135 }
136 frames
137}
138
139fn append_inline_frames<'a>(
140 frames: &mut Vec<InlineCallFrame>,
141 debug_info: &PackageDebugInfo,
142 rows: impl IntoIterator<Item = &'a DebugSourceInlineCall>,
143) {
144 frames.extend(rows.into_iter().filter_map(|row| {
145 let function = debug_info.get_function(row.callee_idx)?;
146 let name = debug_info.get_string(function.name_idx)?;
147 let call_site = debug_info.get_location(row.loc_idx)?;
148 Some(InlineCallFrame { name, call_site })
149 }));
150}
151
152#[derive(Debug, Clone)]
153struct SpanContext {
154 frame_index: usize,
155 location: Option<Location>,
156}
157
158pub struct CallStack {
159 events: Arc<RwLock<BTreeMap<RowIndex, Event>>>,
160 contexts: BTreeSet<Arc<str>>,
161 frames: Vec<CallFrame>,
162 block_stack: Vec<Option<SpanContext>>,
163}
164impl CallStack {
165 pub fn new(events: Arc<RwLock<BTreeMap<RowIndex, Event>>>) -> Self {
166 Self {
167 events,
168 contexts: BTreeSet::default(),
169 frames: vec![],
170 block_stack: vec![],
171 }
172 }
173
174 #[cfg(feature = "dap")]
176 pub fn from_remote_frames(frames: Vec<CallFrame>) -> Self {
177 Self {
178 events: Arc::new(Default::default()),
179 contexts: BTreeSet::default(),
180 frames,
181 block_stack: vec![],
182 }
183 }
184
185 pub fn stacktrace<'a>(
186 &'a self,
187 recent: &'a VecDeque<Operation>,
188 source_manager: &'a dyn SourceManager,
189 ) -> StackTrace<'a> {
190 StackTrace::new(self, recent, source_manager)
191 }
192
193 pub fn current_frame(&self) -> Option<&CallFrame> {
194 self.frames.last()
195 }
196
197 pub fn current_frame_mut(&mut self) -> Option<&mut CallFrame> {
198 self.frames.last_mut()
199 }
200
201 pub fn frames(&self) -> &[CallFrame] {
202 self.frames.as_slice()
203 }
204
205 pub fn logical_frames(&self, strip_prefix: &str) -> Vec<LogicalStackFrame> {
206 let mut logical = Vec::new();
207 for (physical_index, frame) in self.frames.iter().enumerate() {
208 let current_location = frame.last_logical_location();
209 let location = frame
210 .inline_frames
211 .last()
212 .map(|inline| LogicalFrameLocation::Assembly(inline.call_site.clone()))
213 .or_else(|| current_location.clone());
214 logical.push(LogicalStackFrame {
215 name: frame.procedure(strip_prefix).unwrap_or_else(|| Arc::from("<unknown>")),
216 kind: LogicalFrameKind::Physical,
217 location,
218 physical_index,
219 });
220
221 for inline_index in (0..frame.inline_frames.len()).rev() {
222 let inline = &frame.inline_frames[inline_index];
223 let location = if inline_index == 0 {
224 current_location.clone()
225 } else {
226 Some(LogicalFrameLocation::Assembly(
227 frame.inline_frames[inline_index - 1].call_site.clone(),
228 ))
229 };
230 logical.push(LogicalStackFrame {
231 name: Arc::from(inline.display_name().into_boxed_str()),
232 kind: LogicalFrameKind::Inline,
233 location,
234 physical_index,
235 });
236 }
237 }
238 logical
239 }
240
241 pub fn next(&mut self, info: &StepInfo<'_>) -> Option<CallFrame> {
245 let procedure = info.asmop.map(|op| self.cache_procedure_name(op.context_name()));
246
247 let event = {
248 let mut events = self.events.write();
249 match events.first_key_value() {
250 Some((clk, _)) if *clk <= info.clk => events.pop_first().map(|(_, event)| event),
251 _ => None,
252 }
253 };
254 log::trace!("handling {:?}/{:?} at cycle {}: {:?}", info.control, info.op, info.clk, event);
255 let is_frame_start = event.as_ref().is_some_and(|event| event.is_frame_start());
256 let is_frame_end = event.as_ref().is_some_and(|event| event.is_frame_end());
257 let popped_frame = self.handle_event(event, procedure.clone(), info.op, info.asmop);
258
259 match info.control {
260 Some(ControlFlowOp::Span) => {
261 if let Some(asmop) = info.asmop {
262 log::debug!("{asmop:#?}");
263 self.block_stack.push(Some(SpanContext {
264 frame_index: self.frames.len().saturating_sub(1),
265 location: asmop.location().cloned(),
266 }));
267 } else {
268 self.block_stack.push(None);
269 }
270 }
271 Some(ControlFlowOp::Join | ControlFlowOp::Split) => {
272 self.block_stack.push(None);
273 }
274 Some(ControlFlowOp::End) => {
275 self.block_stack.pop();
276 }
277 Some(ControlFlowOp::Respan) | None => {}
278 }
279
280 if !is_frame_end {
281 if self.frames.is_empty() {
282 self.frames.push(CallFrame::new(procedure.clone()));
283 }
284 self.frames.last_mut().unwrap().inline_frames = info.inline_frames.to_vec();
285 self.update_current_procedure(procedure.clone());
286 }
287
288 if is_frame_start || is_frame_end {
289 return popped_frame;
290 }
291
292 let Some(op) = info.op else {
293 return popped_frame;
294 };
295
296 let (procedure, asmop) = match procedure {
299 proc @ Some(_) => (proc, info.asmop.map(Cow::Borrowed)),
300 None => match self.block_stack.last() {
301 Some(Some(span_ctx)) => {
302 let proc =
303 self.frames.get(span_ctx.frame_index).and_then(|f| f.procedure.clone());
304 let asmop_cow = info.asmop.map(Cow::Borrowed).or_else(|| {
305 let context_name = proc.as_deref().unwrap_or("<unknown>").to_string();
306 let raw_asmop = AssemblyOp::new(
307 span_ctx.location.clone(),
308 context_name,
309 1,
310 op.to_string(),
311 );
312 Some(Cow::Owned(raw_asmop))
313 });
314 (proc, asmop_cow)
315 }
316 _ => (None, info.asmop.map(Cow::Borrowed)),
317 },
318 };
319
320 let procedure = procedure.or_else(|| self.frames.last().and_then(|f| f.procedure.clone()));
323
324 self.update_current_procedure(procedure);
327 let current_frame = self.frames.last_mut().unwrap();
328
329 if !matches!(op, Operation::Noop) {
331 let cycle_idx = info.asmop.map(|a| a.num_cycles()).unwrap_or(1);
332 current_frame.push(op, cycle_idx, asmop.as_deref());
333 }
334
335 popped_frame
336 }
337
338 fn update_current_procedure(&mut self, procedure: Option<Arc<str>>) {
339 let context_initialized = self
340 .frames
341 .last_mut()
342 .is_some_and(|frame| frame.update_procedure(procedure.clone()));
343 let num_frames = self.frames.len();
344 if context_initialized && num_frames > 1 {
345 let caller_frame = &mut self.frames[num_frames - 2];
346 if let Some(OpDetail::Exec { callee }) = caller_frame.context.back_mut()
347 && callee.is_none()
348 {
349 *callee = procedure;
350 }
351 }
352 }
353
354 fn cache_procedure_name(&mut self, context_name: &str) -> Arc<str> {
356 match self.contexts.get(context_name) {
357 Some(name) => Arc::clone(name),
358 None => {
359 let name = Arc::from(context_name.to_string().into_boxed_str());
360 self.contexts.insert(Arc::clone(&name));
361 name
362 }
363 }
364 }
365
366 fn handle_event(
367 &mut self,
368 event: Option<Event>,
369 procedure: Option<Arc<str>>,
370 op: Option<Operation>,
371 asmop: Option<&AssemblyOp>,
372 ) -> Option<CallFrame> {
373 match event? {
375 Event::FrameStart => {
376 if let Some(current_frame) = self.frames.last_mut() {
378 current_frame.push_exec(procedure.clone());
379 }
380 let mut frame = CallFrame::new(procedure);
382 if let Some(op) = op {
383 frame.push(op, 0, asmop);
384 }
385 self.frames.push(frame);
386 }
387 Event::Unknown(code) => log::debug!("unknown trace event: {code}"),
388 Event::FrameEnd => {
389 return self.frames.pop();
390 }
391 _ => (),
392 }
393 None
394 }
395}
396
397pub struct CallFrame {
398 procedure: Option<Arc<str>>,
399 context: VecDeque<OpDetail>,
400 display_name: OnceCell<Arc<str>>,
401 finishing: bool,
402 inline_frames: Vec<InlineCallFrame>,
403}
404impl CallFrame {
405 pub fn new(procedure: Option<Arc<str>>) -> Self {
406 Self {
407 procedure,
408 context: Default::default(),
409 display_name: Default::default(),
410 finishing: false,
411 inline_frames: Vec::new(),
412 }
413 }
414
415 #[cfg(feature = "dap")]
421 pub fn from_remote(procedure: Option<Arc<str>>, resolved: Option<ResolvedLocation>) -> Self {
422 let mut context = VecDeque::new();
423 if let Some(loc) = resolved {
424 let cell = OnceCell::new();
425 cell.set(Some(loc)).ok();
426 context.push_back(OpDetail::Full {
427 op: miden_processor::operation::Operation::Noop,
428 location: None,
429 resolved: cell,
430 });
431 }
432 Self {
433 procedure,
434 context,
435 display_name: Default::default(),
436 finishing: false,
437 inline_frames: Vec::new(),
438 }
439 }
440
441 pub fn procedure(&self, strip_prefix: &str) -> Option<Arc<str>> {
442 self.procedure.as_ref()?;
443 let name = self.display_name.get_or_init(|| {
444 let name = self.procedure.as_deref().unwrap();
445 let name = match name.split_once("::") {
446 Some((module, rest)) if module == strip_prefix => demangle(rest),
447 _ => demangle(name),
448 };
449 Arc::<str>::from(name.into_boxed_str())
450 });
451 Some(Arc::clone(name))
452 }
453
454 fn update_procedure(&mut self, procedure: Option<Arc<str>>) -> bool {
458 let Some(procedure) = procedure else {
459 return false;
460 };
461 if self.procedure.as_ref() == Some(&procedure) {
462 return false;
463 }
464
465 let initialized = self.procedure.is_none();
466 self.procedure = Some(procedure);
467 self.display_name.take();
468 initialized
469 }
470
471 pub fn push_exec(&mut self, callee: Option<Arc<str>>) {
472 if self.context.len() == 5 {
473 self.context.pop_front();
474 }
475
476 self.context.push_back(OpDetail::Exec { callee });
477 }
478
479 pub fn push(&mut self, opcode: Operation, cycle_idx: u8, op: Option<&AssemblyOp>) {
480 if cycle_idx > 1 {
481 let skip = self.context.back().map(|detail| matches!(detail, OpDetail::Full { op, .. } | OpDetail::Basic { op } if op == &opcode)).unwrap_or(false);
483 if skip {
484 return;
485 }
486 }
487
488 if self.context.len() == 5 {
489 self.context.pop_front();
490 }
491
492 match op {
493 Some(op) => {
494 let location = op.location().cloned();
495 self.context.push_back(OpDetail::Full {
496 op: opcode,
497 location,
498 resolved: Default::default(),
499 });
500 }
501 None => {
502 if let Some(loc) = self.context.back().map(|op| op.location().cloned()) {
505 self.context.push_back(OpDetail::Full {
506 op: opcode,
507 location: loc,
508 resolved: Default::default(),
509 });
510 } else {
511 self.context.push_back(OpDetail::Basic { op: opcode });
512 }
513 }
514 }
515 }
516
517 pub fn last_location(&self) -> Option<&Location> {
518 self.context.iter().rev().find_map(OpDetail::location)
519 }
520
521 fn last_logical_location(&self) -> Option<LogicalFrameLocation> {
522 self.context.iter().rev().find_map(|detail| {
523 detail
524 .location()
525 .cloned()
526 .map(LogicalFrameLocation::Assembly)
527 .or_else(|| detail.cached_resolved().cloned().map(LogicalFrameLocation::Resolved))
528 })
529 }
530
531 pub fn last_resolved(&self, source_manager: &dyn SourceManager) -> Option<&ResolvedLocation> {
532 for op in self.context.iter().rev() {
535 if let Some(resolved) = op.resolve(source_manager) {
536 return Some(resolved);
537 }
538 }
539 None
540 }
541
542 pub fn recent(&self) -> &VecDeque<OpDetail> {
543 &self.context
544 }
545
546 #[inline(always)]
547 pub fn should_break_on_exit(&self) -> bool {
548 self.finishing
549 }
550
551 #[inline(always)]
552 pub fn break_on_exit(&mut self) {
553 self.finishing = true;
554 }
555}
556
557#[derive(Debug, Clone)]
558pub enum OpDetail {
559 Full {
560 op: Operation,
561 location: Option<Location>,
562 resolved: OnceCell<Option<ResolvedLocation>>,
563 },
564 Exec {
565 callee: Option<Arc<str>>,
566 },
567 Basic {
568 op: Operation,
569 },
570}
571impl OpDetail {
572 pub fn callee(&self, strip_prefix: &str) -> Option<Box<str>> {
573 match self {
574 Self::Exec { callee: None } => Some(Box::from("<unknown>")),
575 Self::Exec {
576 callee: Some(callee),
577 } => {
578 let name = match callee.split_once("::") {
579 Some((module, rest)) if module == strip_prefix => demangle(rest),
580 _ => demangle(callee),
581 };
582 Some(name.into_boxed_str())
583 }
584 _ => None,
585 }
586 }
587
588 pub fn display(&self) -> String {
589 match self {
590 Self::Full { op, .. } | Self::Basic { op } => format!("{op}"),
591 Self::Exec {
592 callee: Some(callee),
593 } => format!("exec.{callee}"),
594 Self::Exec { callee: None } => "exec.<unavailable>".to_string(),
595 }
596 }
597
598 pub fn opcode(&self) -> Operation {
599 match self {
600 Self::Full { op, .. } | Self::Basic { op } => *op,
601 Self::Exec { .. } => panic!("no opcode associated with execs"),
602 }
603 }
604
605 pub fn location(&self) -> Option<&Location> {
606 match self {
607 Self::Full { location, .. } => location.as_ref(),
608 Self::Basic { .. } | Self::Exec { .. } => None,
609 }
610 }
611
612 pub fn resolve(&self, source_manager: &dyn SourceManager) -> Option<&ResolvedLocation> {
613 match self {
614 Self::Full {
615 location, resolved, ..
616 } => {
617 if let Some(cached) = resolved.get() {
618 return cached.as_ref();
619 }
620 let loc = location.as_ref()?;
621 resolved
622 .get_or_init(|| {
623 let source_file = resolve_source_file_for_location(source_manager, loc)?;
624 let span = SourceSpan::new(source_file.id(), loc.start..loc.end);
625 let file_line_col = source_file.location(span);
626 Some(ResolvedLocation {
627 source_file,
628 line: file_line_col.line.to_u32(),
629 col: file_line_col.column.to_u32(),
630 span,
631 })
632 })
633 .as_ref()
634 }
635 _ => None,
636 }
637 }
638
639 fn cached_resolved(&self) -> Option<&ResolvedLocation> {
640 match self {
641 Self::Full { resolved, .. } => resolved.get().and_then(Option::as_ref),
642 Self::Exec { .. } | Self::Basic { .. } => None,
643 }
644 }
645}
646
647#[cfg(feature = "std")]
653pub fn resolve_source_file_for_location(
654 source_manager: &dyn SourceManager,
655 location: &Location,
656) -> Option<Arc<SourceFile>> {
657 use miden_assembly_syntax::debuginfo::SourceManagerExt;
658 source_manager.get_by_uri(location.uri()).or_else(|| {
659 resolve_source_path(location.uri()).and_then(|path| source_manager.load_file(&path).ok())
660 })
661}
662
663#[cfg(not(feature = "std"))]
664pub fn resolve_source_file_for_location(
665 source_manager: &dyn SourceManager,
666 location: &Location,
667) -> Option<Arc<SourceFile>> {
668 source_manager.get_by_uri(location.uri())
669}
670
671#[cfg(feature = "std")]
676pub fn resolve_source_path(uri: &Uri) -> Option<PathBuf> {
677 let path = match uri.scheme() {
678 None | Some("file") => uri.to_path()?,
679 Some(_) => return None,
680 };
681
682 fn existing_path(path: &Path) -> Option<PathBuf> {
683 path.exists()
684 .then(|| path.canonicalize().unwrap_or_else(|_| path.to_path_buf()))
685 }
686
687 existing_path(&path).or_else(|| {
688 if path.is_relative() {
689 std::env::current_dir().ok().and_then(|cwd| existing_path(&cwd.join(path)))
690 } else {
691 None
692 }
693 })
694}
695
696#[cfg(feature = "std")]
698pub fn resolve_location_from_filesystem(location: &Location) -> Option<(PathBuf, u32)> {
699 let path = resolve_source_path(location.uri())?;
700 let bytes = std::fs::read(&path).ok()?;
701 let start = location.start.to_usize().min(bytes.len());
702 let line = bytes[..start].iter().filter(|byte| **byte == b'\n').count() as u32 + 1;
703 Some((path, line))
704}
705
706pub fn is_internal_source_uri(uri: &Uri) -> bool {
708 let path = uri.as_str().replace('\\', "/");
709 path.contains("/codegen/masm/intrinsics/") || path.contains("/rustlib/src/rust/library/")
710}
711
712#[derive(Debug, Clone)]
713pub struct ResolvedLocation {
714 pub source_file: Arc<SourceFile>,
715 pub line: u32,
717 pub col: u32,
718 pub span: SourceSpan,
719}
720impl fmt::Display for ResolvedLocation {
721 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
722 write!(f, "{}:{}:{}", self.source_file.uri().as_str(), self.line, self.col)
723 }
724}
725
726pub struct CurrentFrame {
727 pub procedure: Option<Arc<str>>,
728 pub location: Option<ResolvedLocation>,
729}
730
731pub struct StackTrace<'a> {
732 callstack: &'a CallStack,
733 recent: &'a VecDeque<Operation>,
734 source_manager: &'a dyn SourceManager,
735 current_frame: Option<CurrentFrame>,
736}
737
738impl<'a> StackTrace<'a> {
739 pub fn new(
740 callstack: &'a CallStack,
741 recent: &'a VecDeque<Operation>,
742 source_manager: &'a dyn SourceManager,
743 ) -> Self {
744 let current_frame = callstack.logical_frames("").last().map(|frame| {
745 let location = frame.resolved(source_manager);
746 let procedure = Some(Arc::from(frame.display_name().into_boxed_str()));
747 CurrentFrame {
748 procedure,
749 location,
750 }
751 });
752 Self {
753 callstack,
754 recent,
755 source_manager,
756 current_frame,
757 }
758 }
759
760 pub fn current_frame(&self) -> Option<&CurrentFrame> {
761 self.current_frame.as_ref()
762 }
763}
764
765impl fmt::Display for StackTrace<'_> {
766 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
767 use core::fmt::Write;
768
769 let frames = self.callstack.logical_frames("");
770 let num_frames = frames.len();
771
772 writeln!(f, "\nStack Trace:")?;
773
774 for (i, frame) in frames.iter().enumerate() {
775 let is_top = i + 1 == num_frames;
776 let name = frame.display_name();
777 if is_top {
778 write!(f, " `-> {name}")?;
779 } else {
780 write!(f, " |-> {name}")?;
781 }
782 if let Some(resolved) = frame.resolved(self.source_manager) {
783 write!(f, " in {resolved}")?;
784 } else {
785 write!(f, " in <unavailable>")?;
786 }
787 if is_top {
788 let physical_frame = &self.callstack.frames[frame.physical_index()];
789 let context_size = physical_frame.context.len();
791 writeln!(f, ":\n\nLast {context_size} Instructions (of current frame):")?;
792 for (i, op) in physical_frame.context.iter().enumerate() {
793 let is_last = i + 1 == context_size;
794 if let Some(callee) = op.callee("") {
795 write!(f, " | exec.{callee}")?;
796 } else {
797 write!(f, " | {}", op.opcode())?;
798 }
799 if is_last {
800 writeln!(f, "\n `-> <error occurred here>")?;
801 } else {
802 f.write_char('\n')?;
803 }
804 }
805
806 let context_size = self.recent.len();
807 writeln!(f, "\n\nLast {context_size} Instructions (any frame):")?;
808 for (i, op) in self.recent.iter().enumerate() {
809 let is_last = i + 1 == context_size;
810 if is_last {
811 writeln!(f, " | {}", op)?;
812 writeln!(f, " `-> <error occurred here>")?;
813 } else {
814 writeln!(f, " | {}", op)?;
815 }
816 }
817 } else {
818 f.write_char('\n')?;
819 }
820 }
821
822 Ok(())
823 }
824}
825
826fn resolve_assembly_location(
827 source_manager: &dyn SourceManager,
828 location: &Location,
829) -> Option<ResolvedLocation> {
830 let source_file = resolve_source_file_for_location(source_manager, location)?;
831 let span = SourceSpan::new(source_file.id(), location.start..location.end);
832 let file_line_col = source_file.location(span);
833 Some(ResolvedLocation {
834 source_file,
835 line: file_line_col.line.to_u32(),
836 col: file_line_col.column.to_u32(),
837 span,
838 })
839}
840
841#[cfg(feature = "std")]
842fn demangle(name: &str) -> String {
843 let mut input = name.as_bytes();
844 let mut demangled = Vec::with_capacity(input.len() * 2);
845 rustc_demangle::demangle_stream(&mut input, &mut demangled, false)
846 .expect("failed to write demangled identifier");
847 String::from_utf8(demangled).expect("demangled identifier contains invalid utf-8")
848}
849
850#[cfg(not(feature = "std"))]
851fn demangle(name: &str) -> String {
852 rustc_demangle::demangle(name).to_string()
853}
854
855#[cfg(test)]
856mod tests {
857 use std::{cell::OnceCell, fs, path::PathBuf};
858
859 use miden_assembly_syntax::debuginfo::{DefaultSourceManager, SourceManagerExt};
860 use miden_debug_types::{ByteIndex, Location, Uri};
861
862 use super::*;
863
864 #[test]
865 fn resolves_relative_source_locations_from_filesystem() {
866 let path = test_source_path("relative");
867 fs::create_dir_all(path.parent().unwrap()).unwrap();
868 fs::write(&path, "fn main() {\n let x = 1;\n}\n").unwrap();
869
870 let start = "fn main() {\n ".len() as u32;
871 let location = Location::new(
872 Uri::from(path.display().to_string()),
873 ByteIndex::new(start),
874 ByteIndex::new(start + 5),
875 );
876 let detail = OpDetail::Full {
877 op: Operation::Noop,
878 location: Some(location),
879 resolved: OnceCell::new(),
880 };
881 let source_manager = DefaultSourceManager::default();
882
883 let resolved = detail.resolve(&source_manager).expect("source should resolve");
884 assert_eq!(resolved.line, 2);
885 assert!(resolved.source_file.uri().as_str().ends_with("src/lib.rs"));
886
887 fs::remove_dir_all(path.parent().unwrap().parent().unwrap()).ok();
888 }
889
890 #[test]
891 fn logical_frames_place_innermost_inline_frame_on_top() {
892 let path = test_source_path("inline-frames");
893 fs::create_dir_all(path.parent().unwrap()).unwrap();
894 let source = "physical call\nouter call\ninner body\n";
895 fs::write(&path, source).unwrap();
896 let uri = Uri::from(path.display().to_string());
897
898 let mut frame = CallFrame::new(Some(Arc::from("crate::physical")));
899 let outer_start = "physical call\n".len() as u32;
900 frame.inline_frames = vec![
901 InlineCallFrame {
902 name: Arc::from("crate::inner"),
903 call_site: Location::new(
904 uri.clone(),
905 ByteIndex::new(outer_start),
906 ByteIndex::new(outer_start + "outer call".len() as u32),
907 ),
908 },
909 InlineCallFrame {
910 name: Arc::from("crate::outer"),
911 call_site: Location::new(
912 uri.clone(),
913 ByteIndex::new(0),
914 ByteIndex::new("physical call".len() as u32),
915 ),
916 },
917 ];
918 let inner_start = "physical call\nouter call\n".len() as u32;
919 let asmop = AssemblyOp::new(
920 Some(Location::new(
921 uri,
922 ByteIndex::new(inner_start),
923 ByteIndex::new(inner_start + "inner body".len() as u32),
924 )),
925 "crate::physical".to_string(),
926 1,
927 "add".to_string(),
928 );
929 frame.push(Operation::Add, 1, Some(&asmop));
930
931 let mut callstack = CallStack::new(Arc::new(RwLock::new(BTreeMap::new())));
932 callstack.frames.push(frame);
933 let source_manager = DefaultSourceManager::default();
934 let logical = callstack.logical_frames("");
935
936 assert_eq!(logical.len(), 3);
937 assert_eq!(logical[0].name(), "crate::physical");
938 assert_eq!(logical[0].kind(), LogicalFrameKind::Physical);
939 assert_eq!(logical[0].resolved(&source_manager).unwrap().line, 1);
940 assert_eq!(logical[1].name(), "crate::outer");
941 assert_eq!(logical[1].resolved(&source_manager).unwrap().line, 2);
942 assert_eq!(logical[2].name(), "crate::inner");
943 assert_eq!(logical[2].kind(), LogicalFrameKind::Inline);
944 assert_eq!(logical[2].resolved(&source_manager).unwrap().line, 3);
945
946 fs::remove_dir_all(path.parent().unwrap().parent().unwrap()).ok();
947 }
948
949 #[test]
950 fn control_cycles_replace_and_clear_inline_frames() {
951 let inline = InlineCallFrame {
952 name: Arc::from("crate::inline"),
953 call_site: Location::new(Uri::new("test.masm"), ByteIndex::new(0), ByteIndex::new(1)),
954 };
955 let mut callstack = CallStack::new(Arc::new(RwLock::new(BTreeMap::new())));
956
957 callstack.next(&StepInfo {
958 op: None,
959 control: Some(ControlFlowOp::Split),
960 asmop: None,
961 clk: RowIndex::from(0u32),
962 ctx: ContextId::root(),
963 inline_frames: std::slice::from_ref(&inline),
964 });
965
966 let logical = callstack.logical_frames("");
967 assert_eq!(logical.len(), 2);
968 assert_eq!(logical[0].name(), "<unknown>");
969 assert_eq!(logical[1].name(), "crate::inline");
970
971 callstack.next(&StepInfo {
972 op: None,
973 control: Some(ControlFlowOp::Respan),
974 asmop: None,
975 clk: RowIndex::from(1u32),
976 ctx: ContextId::root(),
977 inline_frames: &[],
978 });
979
980 let logical = callstack.logical_frames("");
981 assert_eq!(logical.len(), 1);
982 assert_eq!(logical[0].name(), "<unknown>");
983 }
984
985 #[test]
986 fn logical_physical_frame_tracks_exec_procedure_changes() {
987 let mut callstack = CallStack::new(Arc::new(RwLock::new(BTreeMap::new())));
988 let main = AssemblyOp::new(None, "program::main".to_string(), 1, "add".to_string());
989 callstack.next(&StepInfo {
990 op: Some(Operation::Add),
991 control: None,
992 asmop: Some(&main),
993 clk: RowIndex::from(0u32),
994 ctx: ContextId::root(),
995 inline_frames: &[],
996 });
997
998 let logical = callstack.logical_frames("");
999 assert_eq!(logical[0].name(), "program::main");
1000 assert_eq!(logical[0].display_name(), "program::main");
1001
1002 let inline = InlineCallFrame {
1003 name: Arc::from("source::inline"),
1004 call_site: Location::new(Uri::new("test.masm"), ByteIndex::new(0), ByteIndex::new(1)),
1005 };
1006 let exec = AssemblyOp::new(None, "program::double".to_string(), 1, "mul".to_string());
1007 callstack.next(&StepInfo {
1008 op: Some(Operation::Mul),
1009 control: None,
1010 asmop: Some(&exec),
1011 clk: RowIndex::from(1u32),
1012 ctx: ContextId::root(),
1013 inline_frames: std::slice::from_ref(&inline),
1014 });
1015
1016 let logical = callstack.logical_frames("");
1017 assert_eq!(logical.len(), 2);
1018 assert_eq!(logical[0].kind(), LogicalFrameKind::Physical);
1019 assert_eq!(logical[0].name(), "program::double");
1020 assert_eq!(logical[0].display_name(), "program::double");
1021 assert_eq!(logical[1].kind(), LogicalFrameKind::Inline);
1022 }
1023
1024 #[test]
1025 fn control_cycle_tracks_exec_procedure_change_before_first_operation() {
1026 let mut callstack = CallStack::new(Arc::new(RwLock::new(BTreeMap::new())));
1027 let main = AssemblyOp::new(None, "program::main".to_string(), 1, "add".to_string());
1028 callstack.next(&StepInfo {
1029 op: Some(Operation::Add),
1030 control: None,
1031 asmop: Some(&main),
1032 clk: RowIndex::from(0u32),
1033 ctx: ContextId::root(),
1034 inline_frames: &[],
1035 });
1036
1037 let exec = AssemblyOp::new(None, "program::double".to_string(), 1, "if.true".to_string());
1038 callstack.next(&StepInfo {
1039 op: None,
1040 control: Some(ControlFlowOp::Split),
1041 asmop: Some(&exec),
1042 clk: RowIndex::from(1u32),
1043 ctx: ContextId::root(),
1044 inline_frames: &[],
1045 });
1046
1047 let logical = callstack.logical_frames("");
1048 assert_eq!(logical[0].name(), "program::double");
1049 }
1050
1051 #[cfg(feature = "dap")]
1052 #[test]
1053 fn remote_logical_frames_preserve_pre_resolved_locations() {
1054 let path = test_source_path("remote-logical-frame");
1055 fs::create_dir_all(path.parent().unwrap()).unwrap();
1056 fs::write(&path, "first line\nsecond line\n").unwrap();
1057
1058 let source_manager = DefaultSourceManager::default();
1059 let source_file = source_manager.load_file(&path).expect("source should load");
1060 let span = SourceSpan::new(source_file.id(), ByteIndex::new(11)..ByteIndex::new(17));
1061 let remote = ResolvedLocation {
1062 source_file,
1063 line: 77,
1064 col: 13,
1065 span,
1066 };
1067 let callstack = CallStack::from_remote_frames(vec![CallFrame::from_remote(
1068 Some(Arc::from("remote::procedure")),
1069 Some(remote.clone()),
1070 )]);
1071
1072 let recent = callstack
1073 .current_frame()
1074 .unwrap()
1075 .last_resolved(&source_manager)
1076 .expect("remote frame should retain its cached location");
1077 assert_eq!(recent.line, remote.line);
1078 assert_eq!(recent.col, remote.col);
1079 assert_eq!(recent.span, remote.span);
1080
1081 let logical = callstack.logical_frames("");
1082 let resolved = logical[0]
1083 .resolved(&source_manager)
1084 .expect("logical frame should retain its cached location");
1085 assert_eq!(resolved.source_file.uri(), remote.source_file.uri());
1086 assert_eq!(resolved.line, remote.line);
1087 assert_eq!(resolved.col, remote.col);
1088 assert_eq!(resolved.span, remote.span);
1089
1090 fs::remove_dir_all(path.parent().unwrap().parent().unwrap()).ok();
1091 }
1092
1093 fn test_source_path(test_name: &str) -> PathBuf {
1094 PathBuf::from("target")
1095 .join("debugger-source-tests")
1096 .join(format!("{}-{}", test_name, std::process::id()))
1097 .join("src")
1098 .join("lib.rs")
1099 }
1100}