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