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