Skip to main content

miden_debug_engine/debug/
stacktrace.rs

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::{
16    DebugFunctionIdx, DebugFunctionInfo, DebugSourceInlineCall, DebugSourceNodeId, PackageDebugInfo,
17};
18use miden_processor::{ContextId, SourceInlineCallContext, operation::Operation, trace::RowIndex};
19use miden_utils_sync::RwLock;
20
21use crate::Event;
22
23mod recovered;
24pub use recovered::{InvocationId, RecoveredCallStack, ReturnTarget};
25
26#[derive(Copy, Clone, Debug, Eq, PartialEq)]
27pub enum ControlFlowOp {
28    Span,
29    Respan,
30    Join,
31    Split,
32    End,
33}
34
35pub struct StepInfo<'a> {
36    pub op: Option<Operation>,
37    pub control: Option<ControlFlowOp>,
38    pub asmop: Option<&'a AssemblyOp>,
39    pub clk: RowIndex,
40    pub ctx: ContextId,
41    pub inline_frames: &'a [InlineCallFrame],
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct InlineCallFrame {
46    name: Arc<str>,
47    call_site: Location,
48}
49
50impl InlineCallFrame {
51    pub fn name(&self) -> &str {
52        &self.name
53    }
54
55    pub fn call_site(&self) -> &Location {
56        &self.call_site
57    }
58
59    pub fn display_name(&self) -> String {
60        demangle(&self.name)
61    }
62}
63
64#[derive(Debug, Copy, Clone, PartialEq, Eq)]
65pub enum LogicalFrameKind {
66    Physical,
67    Inline,
68}
69
70#[derive(Debug, Clone)]
71enum LogicalFrameLocation {
72    Assembly(Location),
73    Resolved(ResolvedLocation),
74}
75
76#[derive(Debug, Clone)]
77pub struct LogicalStackFrame {
78    name: Arc<str>,
79    kind: LogicalFrameKind,
80    location: Option<LogicalFrameLocation>,
81    physical_index: Option<usize>,
82    origin: Option<miden_processor::DebugFrameOrigin>,
83    invocation: Option<InvocationId>,
84    source: Option<LogicalFrameSource>,
85}
86
87#[derive(Debug, Clone)]
88enum LogicalFrameSource {
89    Physical(miden_processor::DebugCallFrame),
90    Inline {
91        owner: InvocationId,
92        info: Arc<PackageDebugInfo>,
93        function_idx: DebugFunctionIdx,
94        call_site: Location,
95    },
96}
97
98impl LogicalStackFrame {
99    /// Compare source invocations independently of presentation names and current locations.
100    pub(crate) fn is_same_source_frame(&self, other: &Self) -> bool {
101        match (&self.source, &other.source) {
102            (
103                Some(LogicalFrameSource::Physical(left)),
104                Some(LogicalFrameSource::Physical(right)),
105            ) => left.is_same_frame(right),
106            (
107                Some(LogicalFrameSource::Inline {
108                    owner: left_owner,
109                    info: left_info,
110                    function_idx: left_function,
111                    call_site: left_site,
112                }),
113                Some(LogicalFrameSource::Inline {
114                    owner: right_owner,
115                    info: right_info,
116                    function_idx: right_function,
117                    call_site: right_site,
118                }),
119            ) => {
120                left_owner == right_owner
121                    && Arc::ptr_eq(left_info, right_info)
122                    && left_function == right_function
123                    && left_site == right_site
124            }
125            _ => false,
126        }
127    }
128
129    pub(crate) fn debug_info(&self) -> Option<&PackageDebugInfo> {
130        match self.source.as_ref()? {
131            LogicalFrameSource::Physical(frame) => Some(frame.debug_info()),
132            LogicalFrameSource::Inline { info, .. } => Some(info),
133        }
134    }
135
136    pub(crate) fn function(&self) -> Option<&DebugFunctionInfo> {
137        match self.source.as_ref()? {
138            LogicalFrameSource::Physical(frame) => Some(frame.function()),
139            LogicalFrameSource::Inline {
140                info, function_idx, ..
141            } => info.get_function(*function_idx),
142        }
143    }
144
145    /// Physical source invocations have local ownership; inline and event-only rows do not.
146    pub fn invocation_id(&self) -> Option<InvocationId> {
147        self.invocation
148    }
149
150    pub fn name(&self) -> &str {
151        &self.name
152    }
153
154    pub fn kind(&self) -> LogicalFrameKind {
155        self.kind
156    }
157
158    pub fn physical_index(&self) -> Option<usize> {
159        self.physical_index
160    }
161
162    pub fn origin(&self) -> Option<miden_processor::DebugFrameOrigin> {
163        self.origin
164    }
165
166    pub fn assembly_location(&self) -> Option<&Location> {
167        match self.location.as_ref()? {
168            LogicalFrameLocation::Assembly(location) => Some(location),
169            LogicalFrameLocation::Resolved(_) => None,
170        }
171    }
172
173    pub fn display_name(&self) -> String {
174        if self
175            .origin
176            .is_some_and(|origin| origin != miden_processor::DebugFrameOrigin::Source)
177        {
178            return format!("[inferred] {}", self.name);
179        }
180        match self.kind {
181            LogicalFrameKind::Physical => self.name.to_string(),
182            LogicalFrameKind::Inline => format!("[inlined] {}", self.name),
183        }
184    }
185
186    pub fn resolved(&self, source_manager: &dyn SourceManager) -> Option<ResolvedLocation> {
187        match self.location.as_ref()? {
188            LogicalFrameLocation::Assembly(location) => {
189                resolve_assembly_location(source_manager, location)
190            }
191            LogicalFrameLocation::Resolved(resolved) => Some(resolved.clone()),
192        }
193    }
194}
195
196/// Resolves the inline frames active for an operation.
197///
198/// Rows owned by the current package come first. Contexts inherited across dynamic/external
199/// package boundaries follow in the VM-provided innermost-to-outermost order.
200pub fn inline_frames_for_operation<'a>(
201    current: Option<(&PackageDebugInfo, DebugSourceNodeId, u32)>,
202    inherited: impl IntoIterator<Item = &'a SourceInlineCallContext>,
203) -> Vec<InlineCallFrame> {
204    let mut frames = Vec::new();
205    if let Some((debug_info, source_node, op_idx)) = current {
206        append_inline_frames(
207            &mut frames,
208            debug_info,
209            debug_info.inline_calls_for_operation(source_node, op_idx),
210        );
211    }
212    for context in inherited {
213        append_inline_frames(&mut frames, context.debug_info(), context.inline_calls());
214    }
215    frames
216}
217
218fn append_inline_frames<'a>(
219    frames: &mut Vec<InlineCallFrame>,
220    debug_info: &PackageDebugInfo,
221    rows: impl IntoIterator<Item = &'a DebugSourceInlineCall>,
222) {
223    frames.extend(rows.into_iter().filter_map(|row| {
224        let function = debug_info.get_function(row.callee_idx)?;
225        let name = debug_info.get_string(function.name_idx)?;
226        let call_site = debug_info.get_location(row.loc_idx)?;
227        Some(InlineCallFrame { name, call_site })
228    }));
229}
230
231#[derive(Debug, Clone)]
232struct SpanContext {
233    frame_index: usize,
234    location: Option<Location>,
235}
236
237pub struct CallStack {
238    events: Arc<RwLock<BTreeMap<RowIndex, Event>>>,
239    contexts: BTreeSet<Arc<str>>,
240    frames: Vec<CallFrame>,
241    block_stack: Vec<Option<SpanContext>>,
242    recovered: RecoveredCallStack,
243    frame_transition: Option<FrameTransition>,
244}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq)]
247pub enum FrameTransition {
248    Entered,
249    Exited,
250}
251
252impl CallStack {
253    pub fn new(events: Arc<RwLock<BTreeMap<RowIndex, Event>>>) -> Self {
254        Self {
255            events,
256            contexts: BTreeSet::default(),
257            frames: vec![],
258            block_stack: vec![],
259            recovered: RecoveredCallStack::default(),
260            frame_transition: None,
261        }
262    }
263
264    /// Build a [CallStack] from pre-built frames — used in DAP client mode.
265    #[cfg(feature = "dap")]
266    pub fn from_remote_frames(frames: Vec<CallFrame>) -> Self {
267        Self {
268            events: Arc::new(Default::default()),
269            contexts: BTreeSet::default(),
270            frames,
271            block_stack: vec![],
272            recovered: RecoveredCallStack::default(),
273            frame_transition: None,
274        }
275    }
276
277    pub fn stacktrace<'a>(
278        &'a self,
279        recent: &'a VecDeque<Operation>,
280        source_manager: &'a dyn SourceManager,
281    ) -> StackTrace<'a> {
282        StackTrace::new(self, recent, source_manager)
283    }
284
285    pub fn current_frame(&self) -> Option<&CallFrame> {
286        self.frames.last()
287    }
288
289    pub fn current_frame_mut(&mut self) -> Option<&mut CallFrame> {
290        self.frames.last_mut()
291    }
292
293    pub fn frames(&self) -> &[CallFrame] {
294        self.frames.as_slice()
295    }
296
297    /// Capture the current invocation for return stepping, with event fallback when needed.
298    pub fn return_target(&self) -> ReturnTarget {
299        self.recovered.return_target(self.frames.len())
300    }
301
302    pub fn has_returned(&self, target: &ReturnTarget) -> bool {
303        target.has_returned(&self.recovered, self.frames.len())
304    }
305
306    pub fn current_invocation(&self) -> Option<InvocationId> {
307        self.recovered.current_invocation()
308    }
309
310    pub fn invocation_ids(&self) -> impl Iterator<Item = InvocationId> + '_ {
311        self.recovered.invocation_ids()
312    }
313
314    pub fn has_recovered_current_frame(&self) -> bool {
315        self.recovered.has_current_frame()
316    }
317
318    pub fn observe_source_context(
319        &mut self,
320        context: &miden_processor::ResumeContext,
321        current: Option<(DebugSourceNodeId, u32)>,
322        asmop: Option<&AssemblyOp>,
323    ) {
324        self.recovered.update(context, current, asmop);
325    }
326
327    pub fn logical_frames(&self, strip_prefix: &str) -> Vec<LogicalStackFrame> {
328        if self.recovered.has_current_frame() {
329            return self.recovered.logical_frames(strip_prefix);
330        }
331        let mut logical = Vec::new();
332        for (physical_index, frame) in self.frames.iter().enumerate() {
333            let current_location = frame.last_logical_location();
334            let location = frame
335                .inline_frames
336                .last()
337                .map(|inline| LogicalFrameLocation::Assembly(inline.call_site.clone()))
338                .or_else(|| current_location.clone());
339            logical.push(LogicalStackFrame {
340                name: frame.procedure(strip_prefix).unwrap_or_else(|| Arc::from("<unknown>")),
341                kind: LogicalFrameKind::Physical,
342                location,
343                physical_index: Some(physical_index),
344                origin: None,
345                invocation: None,
346                source: None,
347            });
348
349            for inline_index in (0..frame.inline_frames.len()).rev() {
350                let inline = &frame.inline_frames[inline_index];
351                let location = if inline_index == 0 {
352                    current_location.clone()
353                } else {
354                    Some(LogicalFrameLocation::Assembly(
355                        frame.inline_frames[inline_index - 1].call_site.clone(),
356                    ))
357                };
358                logical.push(LogicalStackFrame {
359                    name: Arc::from(inline.display_name().into_boxed_str()),
360                    kind: LogicalFrameKind::Inline,
361                    location,
362                    physical_index: Some(physical_index),
363                    origin: None,
364                    invocation: None,
365                    source: None,
366                });
367            }
368        }
369        logical
370    }
371
372    /// Takes the frame event handled by the most recent [`Self::next`], clearing it so a later
373    /// cycle does not read it as its own.
374    pub fn take_frame_transition(&mut self) -> Option<FrameTransition> {
375        self.frame_transition.take()
376    }
377
378    /// Updates the call stack from `info`
379    ///
380    /// Returns the call frame exited this cycle, if any
381    pub fn next(&mut self, info: &StepInfo<'_>) -> Option<CallFrame> {
382        let procedure = info.asmop.map(|op| self.cache_procedure_name(op.context_name()));
383
384        let event = {
385            let mut events = self.events.write();
386            match events.first_key_value() {
387                Some((clk, _)) if *clk <= info.clk => events.pop_first().map(|(_, event)| event),
388                _ => None,
389            }
390        };
391        log::trace!("handling {:?}/{:?} at cycle {}: {:?}", info.control, info.op, info.clk, event);
392        let is_frame_start = event.as_ref().is_some_and(|event| event.is_frame_start());
393        let is_frame_end = event.as_ref().is_some_and(|event| event.is_frame_end());
394        self.frame_transition = match (is_frame_start, is_frame_end) {
395            (true, _) => Some(FrameTransition::Entered),
396            (_, true) => Some(FrameTransition::Exited),
397            _ => None,
398        };
399        let popped_frame = self.handle_event(event, procedure.clone(), info.op, info.asmop);
400
401        match info.control {
402            Some(ControlFlowOp::Span) => {
403                if let Some(asmop) = info.asmop {
404                    log::debug!("{asmop:#?}");
405                    self.block_stack.push(Some(SpanContext {
406                        frame_index: self.frames.len().saturating_sub(1),
407                        location: asmop.location().cloned(),
408                    }));
409                } else {
410                    self.block_stack.push(None);
411                }
412            }
413            Some(ControlFlowOp::Join | ControlFlowOp::Split) => {
414                self.block_stack.push(None);
415            }
416            Some(ControlFlowOp::End) => {
417                self.block_stack.pop();
418            }
419            Some(ControlFlowOp::Respan) | None => {}
420        }
421
422        if !is_frame_end {
423            if self.frames.is_empty() {
424                self.frames.push(CallFrame::new(procedure.clone()));
425            }
426            self.frames.last_mut().unwrap().inline_frames = info.inline_frames.to_vec();
427            self.update_current_procedure(procedure.clone());
428        }
429
430        if is_frame_start || is_frame_end {
431            return popped_frame;
432        }
433
434        let Some(op) = info.op else {
435            return popped_frame;
436        };
437
438        // Attempt to supply procedure context from the current span context, if needed +
439        // available
440        let (procedure, asmop) = match procedure {
441            proc @ Some(_) => (proc, info.asmop.map(Cow::Borrowed)),
442            None => match self.block_stack.last() {
443                Some(Some(span_ctx)) => {
444                    let proc =
445                        self.frames.get(span_ctx.frame_index).and_then(|f| f.procedure.clone());
446                    let asmop_cow = info.asmop.map(Cow::Borrowed).or_else(|| {
447                        let context_name = proc.as_deref().unwrap_or("<unknown>").to_string();
448                        let raw_asmop = AssemblyOp::new(
449                            span_ctx.location.clone(),
450                            context_name,
451                            1,
452                            op.to_string(),
453                        );
454                        Some(Cow::Owned(raw_asmop))
455                    });
456                    (proc, asmop_cow)
457                }
458                _ => (None, info.asmop.map(Cow::Borrowed)),
459            },
460        };
461
462        // Use the current frame's procedure context, if no other more precise context is
463        // available
464        let procedure = procedure.or_else(|| self.frames.last().and_then(|f| f.procedure.clone()));
465
466        // `exec` changes procedure context without creating a physical frame. Keep the physical
467        // frame synchronized with the best context available for the current operation.
468        self.update_current_procedure(procedure);
469        let current_frame = self.frames.last_mut().unwrap();
470
471        // Push op into call frame if this is any op other than `nop` or frame setup
472        if !matches!(op, Operation::Noop) {
473            let cycle_idx = info.asmop.map(|a| a.num_cycles()).unwrap_or(1);
474            current_frame.push(op, cycle_idx, asmop.as_deref());
475        }
476
477        popped_frame
478    }
479
480    fn update_current_procedure(&mut self, procedure: Option<Arc<str>>) {
481        let context_initialized = self
482            .frames
483            .last_mut()
484            .is_some_and(|frame| frame.update_procedure(procedure.clone()));
485        let num_frames = self.frames.len();
486        if context_initialized && num_frames > 1 {
487            let caller_frame = &mut self.frames[num_frames - 2];
488            if let Some(OpDetail::Exec { callee }) = caller_frame.context.back_mut()
489                && callee.is_none()
490            {
491                *callee = procedure;
492            }
493        }
494    }
495
496    // Get or cache procedure name/context as `Arc<str>`
497    fn cache_procedure_name(&mut self, context_name: &str) -> Arc<str> {
498        match self.contexts.get(context_name) {
499            Some(name) => Arc::clone(name),
500            None => {
501                let name = Arc::from(context_name.to_string().into_boxed_str());
502                self.contexts.insert(Arc::clone(&name));
503                name
504            }
505        }
506    }
507
508    fn handle_event(
509        &mut self,
510        event: Option<Event>,
511        procedure: Option<Arc<str>>,
512        op: Option<Operation>,
513        asmop: Option<&AssemblyOp>,
514    ) -> Option<CallFrame> {
515        // Do we need to handle any frame events?
516        match event? {
517            Event::FrameStart => {
518                // Record the fact that we exec'd a new procedure in the op context
519                if let Some(current_frame) = self.frames.last_mut() {
520                    current_frame.push_exec(procedure.clone());
521                }
522                // The event is emitted at the start of the callee.
523                let mut frame = CallFrame::new(procedure);
524                if let Some(op) = op {
525                    frame.push(op, 0, asmop);
526                }
527                self.frames.push(frame);
528            }
529            Event::Unknown(code) => log::debug!("unknown trace event: {code}"),
530            Event::FrameEnd => {
531                return self.frames.pop();
532            }
533            _ => (),
534        }
535        None
536    }
537}
538
539pub struct CallFrame {
540    procedure: Option<Arc<str>>,
541    context: VecDeque<OpDetail>,
542    display_name: OnceCell<Arc<str>>,
543    finishing: bool,
544    inline_frames: Vec<InlineCallFrame>,
545}
546impl CallFrame {
547    pub fn new(procedure: Option<Arc<str>>) -> Self {
548        Self {
549            procedure,
550            context: Default::default(),
551            display_name: Default::default(),
552            finishing: false,
553            inline_frames: Vec::new(),
554        }
555    }
556
557    /// Build a frame from remote (DAP) data — used in DAP client mode.
558    ///
559    /// The frame stores the procedure name and an optional [ResolvedLocation]
560    /// as a pre-resolved `OpDetail::Full` entry so that `last_resolved()` and
561    /// `recent()` work correctly for pane rendering.
562    #[cfg(feature = "dap")]
563    pub fn from_remote(procedure: Option<Arc<str>>, resolved: Option<ResolvedLocation>) -> Self {
564        let mut context = VecDeque::new();
565        if let Some(loc) = resolved {
566            let cell = OnceCell::new();
567            cell.set(Some(loc)).ok();
568            context.push_back(OpDetail::Full {
569                op: miden_processor::operation::Operation::Noop,
570                location: None,
571                resolved: cell,
572            });
573        }
574        Self {
575            procedure,
576            context,
577            display_name: Default::default(),
578            finishing: false,
579            inline_frames: Vec::new(),
580        }
581    }
582
583    pub fn procedure(&self, strip_prefix: &str) -> Option<Arc<str>> {
584        self.procedure.as_ref()?;
585        let name = self.display_name.get_or_init(|| {
586            let name = self.procedure.as_deref().unwrap();
587            let name = match name.split_once("::") {
588                Some((module, rest)) if module == strip_prefix => demangle(rest),
589                _ => demangle(name),
590            };
591            Arc::<str>::from(name.into_boxed_str())
592        });
593        Some(Arc::clone(name))
594    }
595
596    /// Update this physical frame's procedure, returning true only when the context was first
597    /// initialized. Later changes arise from `exec` and must invalidate the cached display name,
598    /// but must not rewrite the caller's recorded callee.
599    fn update_procedure(&mut self, procedure: Option<Arc<str>>) -> bool {
600        let Some(procedure) = procedure else {
601            return false;
602        };
603        if self.procedure.as_ref() == Some(&procedure) {
604            return false;
605        }
606
607        let initialized = self.procedure.is_none();
608        self.procedure = Some(procedure);
609        self.display_name.take();
610        initialized
611    }
612
613    pub fn push_exec(&mut self, callee: Option<Arc<str>>) {
614        if self.context.len() == 5 {
615            self.context.pop_front();
616        }
617
618        self.context.push_back(OpDetail::Exec { callee });
619    }
620
621    pub fn push(&mut self, opcode: Operation, cycle_idx: u8, op: Option<&AssemblyOp>) {
622        if cycle_idx > 1 {
623            // Should we ignore this op?
624            let skip = self.context.back().map(|detail| matches!(detail, OpDetail::Full { op, .. } | OpDetail::Basic { op } if op == &opcode)).unwrap_or(false);
625            if skip {
626                return;
627            }
628        }
629
630        if self.context.len() == 5 {
631            self.context.pop_front();
632        }
633
634        match op {
635            Some(op) => {
636                let location = op.location().cloned();
637                self.context.push_back(OpDetail::Full {
638                    op: opcode,
639                    location,
640                    resolved: Default::default(),
641                });
642            }
643            None => {
644                // If this instruction does not have a location, inherit the location
645                // of the previous op in the frame, if one is present
646                if let Some(loc) = self.context.back().map(|op| op.location().cloned()) {
647                    self.context.push_back(OpDetail::Full {
648                        op: opcode,
649                        location: loc,
650                        resolved: Default::default(),
651                    });
652                } else {
653                    self.context.push_back(OpDetail::Basic { op: opcode });
654                }
655            }
656        }
657    }
658
659    pub fn last_location(&self) -> Option<&Location> {
660        self.context.iter().rev().find_map(OpDetail::location)
661    }
662
663    fn last_logical_location(&self) -> Option<LogicalFrameLocation> {
664        self.context.iter().rev().find_map(|detail| {
665            detail
666                .location()
667                .cloned()
668                .map(LogicalFrameLocation::Assembly)
669                .or_else(|| detail.cached_resolved().cloned().map(LogicalFrameLocation::Resolved))
670        })
671    }
672
673    pub fn last_resolved(&self, source_manager: &dyn SourceManager) -> Option<&ResolvedLocation> {
674        // Search through context in reverse order to find the most recent op with a resolvable
675        // location.
676        for op in self.context.iter().rev() {
677            if let Some(resolved) = op.resolve(source_manager) {
678                return Some(resolved);
679            }
680        }
681        None
682    }
683
684    pub fn recent(&self) -> &VecDeque<OpDetail> {
685        &self.context
686    }
687
688    #[inline(always)]
689    pub fn should_break_on_exit(&self) -> bool {
690        self.finishing
691    }
692
693    #[inline(always)]
694    pub fn break_on_exit(&mut self) {
695        self.finishing = true;
696    }
697}
698
699#[derive(Debug, Clone)]
700pub enum OpDetail {
701    Full {
702        op: Operation,
703        location: Option<Location>,
704        resolved: OnceCell<Option<ResolvedLocation>>,
705    },
706    Exec {
707        callee: Option<Arc<str>>,
708    },
709    Basic {
710        op: Operation,
711    },
712}
713impl OpDetail {
714    pub fn callee(&self, strip_prefix: &str) -> Option<Box<str>> {
715        match self {
716            Self::Exec { callee: None } => Some(Box::from("<unknown>")),
717            Self::Exec {
718                callee: Some(callee),
719            } => {
720                let name = match callee.split_once("::") {
721                    Some((module, rest)) if module == strip_prefix => demangle(rest),
722                    _ => demangle(callee),
723                };
724                Some(name.into_boxed_str())
725            }
726            _ => None,
727        }
728    }
729
730    pub fn display(&self) -> String {
731        match self {
732            Self::Full { op, .. } | Self::Basic { op } => format!("{op}"),
733            Self::Exec {
734                callee: Some(callee),
735            } => format!("exec.{callee}"),
736            Self::Exec { callee: None } => "exec.<unavailable>".to_string(),
737        }
738    }
739
740    pub fn opcode(&self) -> Operation {
741        match self {
742            Self::Full { op, .. } | Self::Basic { op } => *op,
743            Self::Exec { .. } => panic!("no opcode associated with execs"),
744        }
745    }
746
747    pub fn location(&self) -> Option<&Location> {
748        match self {
749            Self::Full { location, .. } => location.as_ref(),
750            Self::Basic { .. } | Self::Exec { .. } => None,
751        }
752    }
753
754    pub fn resolve(&self, source_manager: &dyn SourceManager) -> Option<&ResolvedLocation> {
755        match self {
756            Self::Full {
757                location, resolved, ..
758            } => {
759                if let Some(cached) = resolved.get() {
760                    return cached.as_ref();
761                }
762                let loc = location.as_ref()?;
763                resolved
764                    .get_or_init(|| {
765                        let source_file = resolve_source_file_for_location(source_manager, loc)?;
766                        let span = SourceSpan::new(source_file.id(), loc.start..loc.end);
767                        let file_line_col = source_file.location(span);
768                        Some(ResolvedLocation {
769                            source_file,
770                            line: file_line_col.line.to_u32(),
771                            col: file_line_col.column.to_u32(),
772                            span,
773                        })
774                    })
775                    .as_ref()
776            }
777            _ => None,
778        }
779    }
780
781    fn cached_resolved(&self) -> Option<&ResolvedLocation> {
782        match self {
783            Self::Full { resolved, .. } => resolved.get().and_then(Option::as_ref),
784            Self::Exec { .. } | Self::Basic { .. } => None,
785        }
786    }
787}
788
789/// Resolve a source file for `location`.
790///
791/// Compiled packages may contain remapped paths such as `src/lib.rs`, while sources loaded by the
792/// VM host may be keyed by an absolute path, or may not be loaded yet at all. Prefer the source
793/// manager's existing URI table, then fall back to loading the file from disk.
794#[cfg(feature = "std")]
795pub fn resolve_source_file_for_location(
796    source_manager: &dyn SourceManager,
797    location: &Location,
798) -> Option<Arc<SourceFile>> {
799    use miden_assembly_syntax::debuginfo::SourceManagerExt;
800    source_manager.get_by_uri(location.uri()).or_else(|| {
801        resolve_source_path(location.uri()).and_then(|path| source_manager.load_file(&path).ok())
802    })
803}
804
805#[cfg(not(feature = "std"))]
806pub fn resolve_source_file_for_location(
807    source_manager: &dyn SourceManager,
808    location: &Location,
809) -> Option<Arc<SourceFile>> {
810    source_manager.get_by_uri(location.uri())
811}
812
813/// Resolve a source URI to an existing local filesystem path.
814///
815/// Non-file URI schemes are left to the source manager. Relative paths are resolved against the
816/// debugger process' current directory, which DAP clients set to the launch `cwd`.
817#[cfg(feature = "std")]
818pub fn resolve_source_path(uri: &Uri) -> Option<PathBuf> {
819    let path = match uri.scheme() {
820        None | Some("file") => uri.to_path()?,
821        Some(_) => return None,
822    };
823
824    fn existing_path(path: &Path) -> Option<PathBuf> {
825        path.exists()
826            .then(|| path.canonicalize().unwrap_or_else(|_| path.to_path_buf()))
827    }
828
829    existing_path(&path).or_else(|| {
830        if path.is_relative() {
831            std::env::current_dir().ok().and_then(|cwd| existing_path(&cwd.join(path)))
832        } else {
833            None
834        }
835    })
836}
837
838/// Resolve a source location directly from the filesystem, returning the resolved path and line.
839#[cfg(feature = "std")]
840pub fn resolve_location_from_filesystem(location: &Location) -> Option<(PathBuf, u32)> {
841    let path = resolve_source_path(location.uri())?;
842    let bytes = std::fs::read(&path).ok()?;
843    let start = location.start.to_usize().min(bytes.len());
844    let line = bytes[..start].iter().filter(|byte| **byte == b'\n').count() as u32 + 1;
845    Some((path, line))
846}
847
848/// Returns true for source paths emitted by compiler/runtime internals rather than user code.
849pub fn is_internal_source_uri(uri: &Uri) -> bool {
850    let path = uri.as_str().replace('\\', "/");
851    path.contains("/codegen/masm/intrinsics/") || path.contains("/rustlib/src/rust/library/")
852}
853
854#[derive(Debug, Clone)]
855pub struct ResolvedLocation {
856    pub source_file: Arc<SourceFile>,
857    // TODO(fabrio): Use LineNumber and ColumnNumber instead of raw `u32`.
858    pub line: u32,
859    pub col: u32,
860    pub span: SourceSpan,
861}
862impl fmt::Display for ResolvedLocation {
863    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
864        write!(f, "{}:{}:{}", self.source_file.uri().as_str(), self.line, self.col)
865    }
866}
867
868pub struct CurrentFrame {
869    pub procedure: Option<Arc<str>>,
870    pub location: Option<ResolvedLocation>,
871}
872
873pub struct StackTrace<'a> {
874    callstack: &'a CallStack,
875    recent: &'a VecDeque<Operation>,
876    source_manager: &'a dyn SourceManager,
877    current_frame: Option<CurrentFrame>,
878}
879
880impl<'a> StackTrace<'a> {
881    pub fn new(
882        callstack: &'a CallStack,
883        recent: &'a VecDeque<Operation>,
884        source_manager: &'a dyn SourceManager,
885    ) -> Self {
886        let current_frame = callstack.logical_frames("").last().map(|frame| {
887            let location = frame.resolved(source_manager);
888            let procedure = Some(Arc::from(frame.display_name().into_boxed_str()));
889            CurrentFrame {
890                procedure,
891                location,
892            }
893        });
894        Self {
895            callstack,
896            recent,
897            source_manager,
898            current_frame,
899        }
900    }
901
902    pub fn current_frame(&self) -> Option<&CurrentFrame> {
903        self.current_frame.as_ref()
904    }
905}
906
907impl fmt::Display for StackTrace<'_> {
908    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
909        use core::fmt::Write;
910
911        let frames = self.callstack.logical_frames("");
912        let num_frames = frames.len();
913
914        writeln!(f, "\nStack Trace:")?;
915
916        for (i, frame) in frames.iter().enumerate() {
917            let is_top = i + 1 == num_frames;
918            let name = frame.display_name();
919            if is_top {
920                write!(f, " `-> {name}")?;
921            } else {
922                write!(f, " |-> {name}")?;
923            }
924            if let Some(resolved) = frame.resolved(self.source_manager) {
925                write!(f, " in {resolved}")?;
926            } else {
927                write!(f, " in <unavailable>")?;
928            }
929            if is_top {
930                let context = self.callstack.current_frame().map(CallFrame::recent);
931                // A failure can have recovered source frames before any physical frame exists.
932                let context_size = context.map_or(0, VecDeque::len);
933                writeln!(f, ":\n\nLast {context_size} Instructions (of current frame):")?;
934                for (i, op) in context.into_iter().flatten().enumerate() {
935                    let is_last = i + 1 == context_size;
936                    if let Some(callee) = op.callee("") {
937                        write!(f, " |   exec.{callee}")?;
938                    } else {
939                        write!(f, " |   {}", op.opcode())?;
940                    }
941                    if is_last {
942                        writeln!(f, "\n `-> <error occurred here>")?;
943                    } else {
944                        f.write_char('\n')?;
945                    }
946                }
947
948                let context_size = self.recent.len();
949                writeln!(f, "\n\nLast {context_size} Instructions (any frame):")?;
950                for (i, op) in self.recent.iter().enumerate() {
951                    let is_last = i + 1 == context_size;
952                    if is_last {
953                        writeln!(f, " |   {}", op)?;
954                        writeln!(f, " `-> <error occurred here>")?;
955                    } else {
956                        writeln!(f, " |   {}", op)?;
957                    }
958                }
959            } else {
960                f.write_char('\n')?;
961            }
962        }
963
964        Ok(())
965    }
966}
967
968fn resolve_assembly_location(
969    source_manager: &dyn SourceManager,
970    location: &Location,
971) -> Option<ResolvedLocation> {
972    let source_file = resolve_source_file_for_location(source_manager, location)?;
973    let span = SourceSpan::new(source_file.id(), location.start..location.end);
974    let file_line_col = source_file.location(span);
975    Some(ResolvedLocation {
976        source_file,
977        line: file_line_col.line.to_u32(),
978        col: file_line_col.column.to_u32(),
979        span,
980    })
981}
982
983#[cfg(feature = "std")]
984pub(super) fn demangle(name: &str) -> String {
985    let mut input = name.as_bytes();
986    let mut demangled = Vec::with_capacity(input.len() * 2);
987    rustc_demangle::demangle_stream(&mut input, &mut demangled, /* include_hash= */ false)
988        .expect("failed to write demangled identifier");
989    String::from_utf8(demangled).expect("demangled identifier contains invalid utf-8")
990}
991
992#[cfg(not(feature = "std"))]
993pub(super) fn demangle(name: &str) -> String {
994    // Assembly contexts qualify the linkage symbol with a package/module path.
995    match name.rsplit_once("::") {
996        Some((prefix, symbol)) => format!("{prefix}::{:#}", rustc_demangle::demangle(symbol)),
997        None => format!("{:#}", rustc_demangle::demangle(name)),
998    }
999}
1000
1001#[cfg(test)]
1002mod tests;