Skip to main content

miden_debug_engine/debug/
stacktrace.rs

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
113/// Resolves the inline frames active for an operation.
114///
115/// Rows owned by the current package come first. Contexts inherited across dynamic/external
116/// package boundaries follow in the VM-provided innermost-to-outermost order.
117pub 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    /// Build a [CallStack] from pre-built frames — used in DAP client mode.
171    #[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    /// Updates the call stack from `info`
238    ///
239    /// Returns the call frame exited this cycle, if any
240    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        // Attempt to supply procedure context from the current span context, if needed +
293        // available
294        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        // Use the current frame's procedure context, if no other more precise context is
317        // available
318        let procedure = procedure.or_else(|| self.frames.last().and_then(|f| f.procedure.clone()));
319
320        // `exec` changes procedure context without creating a physical frame. Keep the physical
321        // frame synchronized with the best context available for the current operation.
322        self.update_current_procedure(procedure);
323        let current_frame = self.frames.last_mut().unwrap();
324
325        // Push op into call frame if this is any op other than `nop` or frame setup
326        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    // Get or cache procedure name/context as `Arc<str>`
351    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        // Do we need to handle any frame events?
370        match event? {
371            Event::FrameStart => {
372                // Record the fact that we exec'd a new procedure in the op context
373                if let Some(current_frame) = self.frames.last_mut() {
374                    current_frame.push_exec(procedure.clone());
375                }
376                // The event is emitted at the start of the callee.
377                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    /// Build a frame from remote (DAP) data — used in DAP client mode.
412    ///
413    /// The frame stores the procedure name and an optional [ResolvedLocation]
414    /// as a pre-resolved `OpDetail::Full` entry so that `last_resolved()` and
415    /// `recent()` work correctly for pane rendering.
416    #[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    /// Update this physical frame's procedure, returning true only when the context was first
451    /// initialized. Later changes arise from `exec` and must invalidate the cached display name,
452    /// but must not rewrite the caller's recorded callee.
453    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            // Should we ignore this op?
478            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 this instruction does not have a location, inherit the location
499                // of the previous op in the frame, if one is present
500                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        // Search through context in reverse order to find the most recent op with a resolvable
529        // location.
530        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
643/// Resolve a source file for `location`.
644///
645/// Compiled packages may contain remapped paths such as `src/lib.rs`, while sources loaded by the
646/// VM host may be keyed by an absolute path, or may not be loaded yet at all. Prefer the source
647/// manager's existing URI table, then fall back to loading the file from disk.
648pub 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
657/// Resolve a source URI to an existing local filesystem path.
658///
659/// Non-file URI schemes are left to the source manager. Relative paths are resolved against the
660/// debugger process' current directory, which DAP clients set to the launch `cwd`.
661pub 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
676/// Resolve a source location directly from the filesystem, returning the resolved path and line.
677pub 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
685/// Returns true for source paths emitted by compiler/runtime internals rather than user code.
686pub 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    // TODO(fabrio): Use LineNumber and ColumnNumber instead of raw `u32`.
700    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                // Print op context
774                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, /* include_hash= */ 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}