Skip to main content

miden_debug/debug/
stacktrace.rs

1use std::{
2    borrow::Cow,
3    cell::{OnceCell, RefCell},
4    collections::{BTreeMap, BTreeSet, VecDeque},
5    fmt,
6    path::Path,
7    rc::Rc,
8    sync::Arc,
9};
10
11use miden_core::operations::AssemblyOp;
12use miden_debug_types::{Location, SourceFile, SourceManager, SourceManagerExt, SourceSpan};
13use miden_processor::{ContextId, operation::Operation, trace::RowIndex};
14
15use crate::exec::TraceEvent;
16
17pub struct StepInfo<'a> {
18    pub op: Option<Operation>,
19    pub asmop: Option<&'a AssemblyOp>,
20    pub clk: RowIndex,
21    pub ctx: ContextId,
22}
23
24#[derive(Debug, Clone)]
25struct SpanContext {
26    frame_index: usize,
27    location: Option<Location>,
28}
29
30pub struct CallStack {
31    trace_events: Rc<RefCell<BTreeMap<RowIndex, TraceEvent>>>,
32    contexts: BTreeSet<Rc<str>>,
33    frames: Vec<CallFrame>,
34    block_stack: Vec<Option<SpanContext>>,
35}
36impl CallStack {
37    pub fn new(trace_events: Rc<RefCell<BTreeMap<RowIndex, TraceEvent>>>) -> Self {
38        Self {
39            trace_events,
40            contexts: BTreeSet::default(),
41            frames: vec![],
42            block_stack: vec![],
43        }
44    }
45
46    pub fn stacktrace<'a>(
47        &'a self,
48        recent: &'a VecDeque<Operation>,
49        source_manager: &'a dyn SourceManager,
50    ) -> StackTrace<'a> {
51        StackTrace::new(self, recent, source_manager)
52    }
53
54    pub fn current_frame(&self) -> Option<&CallFrame> {
55        self.frames.last()
56    }
57
58    pub fn current_frame_mut(&mut self) -> Option<&mut CallFrame> {
59        self.frames.last_mut()
60    }
61
62    pub fn frames(&self) -> &[CallFrame] {
63        self.frames.as_slice()
64    }
65
66    /// Updates the call stack from `info`
67    ///
68    /// Returns the call frame exited this cycle, if any
69    pub fn next(&mut self, info: &StepInfo<'_>) -> Option<CallFrame> {
70        if let Some(op) = info.op {
71            // Get the current procedure name context, if available
72            let procedure = info.asmop.map(|op| self.cache_procedure_name(op.context_name()));
73
74            // Handle trace events for this cycle
75            let event = self.trace_events.borrow().get(&info.clk).copied();
76            log::trace!("handling {op} at cycle {}: {:?}", info.clk, &event);
77            let popped_frame = self.handle_trace_event(event, procedure.as_ref());
78            let is_frame_end = popped_frame.is_some();
79
80            let ignore = false;
81
82            if ignore || is_frame_end {
83                return popped_frame;
84            }
85
86            // Attempt to supply procedure context from the current span context, if needed +
87            // available
88            let (procedure, asmop) = match procedure {
89                proc @ Some(_) => (proc, info.asmop.map(Cow::Borrowed)),
90                None => match self.block_stack.last() {
91                    Some(Some(span_ctx)) => {
92                        let proc =
93                            self.frames.get(span_ctx.frame_index).and_then(|f| f.procedure.clone());
94                        let asmop_cow = info.asmop.map(Cow::Borrowed).or_else(|| {
95                            let context_name = proc.as_deref().unwrap_or("<unknown>").to_string();
96                            let raw_asmop = AssemblyOp::new(
97                                span_ctx.location.clone(),
98                                context_name,
99                                1,
100                                op.to_string(),
101                            );
102                            Some(Cow::Owned(raw_asmop))
103                        });
104                        (proc, asmop_cow)
105                    }
106                    _ => (None, info.asmop.map(Cow::Borrowed)),
107                },
108            };
109
110            // Use the current frame's procedure context, if no other more precise context is
111            // available
112            let procedure =
113                procedure.or_else(|| self.frames.last().and_then(|f| f.procedure.clone()));
114
115            // Do we have a frame? If not, create one
116            if self.frames.is_empty() {
117                self.frames.push(CallFrame::new(procedure.clone()));
118            }
119
120            let current_frame = self.frames.last_mut().unwrap();
121
122            // Does the current frame have a procedure context/location? Use the one from this op if
123            // so
124            let procedure_context_updated =
125                current_frame.procedure.is_none() && procedure.is_some();
126            if procedure_context_updated {
127                current_frame.procedure.clone_from(&procedure);
128            }
129
130            // Push op into call frame if this is any op other than `nop` or frame setup
131            if !matches!(op, Operation::Noop) {
132                let cycle_idx = info.asmop.map(|a| a.num_cycles()).unwrap_or(1);
133                current_frame.push(op, cycle_idx, asmop.as_deref());
134            }
135
136            // Check if we should also update the caller frame's exec detail
137            let num_frames = self.frames.len();
138            if procedure_context_updated && num_frames > 1 {
139                let caller_frame = &mut self.frames[num_frames - 2];
140                if let Some(OpDetail::Exec { callee }) = caller_frame.context.back_mut()
141                    && callee.is_none()
142                {
143                    *callee = procedure;
144                }
145            }
146        }
147
148        None
149    }
150
151    // Get or cache procedure name/context as `Rc<str>`
152    fn cache_procedure_name(&mut self, context_name: &str) -> Rc<str> {
153        match self.contexts.get(context_name) {
154            Some(name) => Rc::clone(name),
155            None => {
156                let name = Rc::from(context_name.to_string().into_boxed_str());
157                self.contexts.insert(Rc::clone(&name));
158                name
159            }
160        }
161    }
162
163    fn handle_trace_event(
164        &mut self,
165        event: Option<TraceEvent>,
166        procedure: Option<&Rc<str>>,
167    ) -> Option<CallFrame> {
168        // Do we need to handle any frame events?
169        if let Some(event) = event {
170            match event {
171                TraceEvent::FrameStart => {
172                    // Record the fact that we exec'd a new procedure in the op context
173                    if let Some(current_frame) = self.frames.last_mut() {
174                        current_frame.push_exec(procedure.cloned());
175                    }
176                    // Push a new frame
177                    self.frames.push(CallFrame::new(procedure.cloned()));
178                }
179                TraceEvent::Unknown(code) => log::debug!("unknown trace event: {code}"),
180                TraceEvent::FrameEnd => {
181                    return self.frames.pop();
182                }
183                _ => (),
184            }
185        }
186        None
187    }
188}
189
190pub struct CallFrame {
191    procedure: Option<Rc<str>>,
192    context: VecDeque<OpDetail>,
193    display_name: std::cell::OnceCell<Rc<str>>,
194    finishing: bool,
195}
196impl CallFrame {
197    pub fn new(procedure: Option<Rc<str>>) -> Self {
198        Self {
199            procedure,
200            context: Default::default(),
201            display_name: Default::default(),
202            finishing: false,
203        }
204    }
205
206    pub fn procedure(&self, strip_prefix: &str) -> Option<Rc<str>> {
207        self.procedure.as_ref()?;
208        let name = self.display_name.get_or_init(|| {
209            let name = self.procedure.as_deref().unwrap();
210            let name = match name.split_once("::") {
211                Some((module, rest)) if module == strip_prefix => demangle(rest),
212                _ => demangle(name),
213            };
214            Rc::from(name.into_boxed_str())
215        });
216        Some(Rc::clone(name))
217    }
218
219    pub fn push_exec(&mut self, callee: Option<Rc<str>>) {
220        if self.context.len() == 5 {
221            self.context.pop_front();
222        }
223
224        self.context.push_back(OpDetail::Exec { callee });
225    }
226
227    pub fn push(&mut self, opcode: Operation, cycle_idx: u8, op: Option<&AssemblyOp>) {
228        if cycle_idx > 1 {
229            // Should we ignore this op?
230            let skip = self.context.back().map(|detail| matches!(detail, OpDetail::Full { op, .. } | OpDetail::Basic { op } if op == &opcode)).unwrap_or(false);
231            if skip {
232                return;
233            }
234        }
235
236        if self.context.len() == 5 {
237            self.context.pop_front();
238        }
239
240        match op {
241            Some(op) => {
242                let location = op.location().cloned();
243                self.context.push_back(OpDetail::Full {
244                    op: opcode,
245                    location,
246                    resolved: Default::default(),
247                });
248            }
249            None => {
250                // If this instruction does not have a location, inherit the location
251                // of the previous op in the frame, if one is present
252                if let Some(loc) = self.context.back().map(|op| op.location().cloned()) {
253                    self.context.push_back(OpDetail::Full {
254                        op: opcode,
255                        location: loc,
256                        resolved: Default::default(),
257                    });
258                } else {
259                    self.context.push_back(OpDetail::Basic { op: opcode });
260                }
261            }
262        }
263    }
264
265    pub fn last_location(&self) -> Option<&Location> {
266        match self.context.back() {
267            Some(OpDetail::Full { location, .. }) => {
268                let loc = location.as_ref();
269                if loc.is_none() {
270                    dbg!(&self.context);
271                }
272                loc
273            }
274            Some(OpDetail::Basic { .. }) => None,
275            Some(OpDetail::Exec { .. }) => {
276                let op = self.context.iter().rev().nth(1)?;
277                op.location()
278            }
279            None => None,
280        }
281    }
282
283    pub fn last_resolved(&self, source_manager: &dyn SourceManager) -> Option<&ResolvedLocation> {
284        // Search through context in reverse order to find the most recent op with a resolvable
285        // location.
286        for op in self.context.iter().rev() {
287            if let Some(resolved) = op.resolve(source_manager) {
288                return Some(resolved);
289            }
290        }
291        None
292    }
293
294    pub fn recent(&self) -> &VecDeque<OpDetail> {
295        &self.context
296    }
297
298    #[inline(always)]
299    pub fn should_break_on_exit(&self) -> bool {
300        self.finishing
301    }
302
303    #[inline(always)]
304    pub fn break_on_exit(&mut self) {
305        self.finishing = true;
306    }
307}
308
309#[derive(Debug, Clone)]
310pub enum OpDetail {
311    Full {
312        op: Operation,
313        location: Option<Location>,
314        resolved: OnceCell<Option<ResolvedLocation>>,
315    },
316    Exec {
317        callee: Option<Rc<str>>,
318    },
319    Basic {
320        op: Operation,
321    },
322}
323impl OpDetail {
324    pub fn callee(&self, strip_prefix: &str) -> Option<Box<str>> {
325        match self {
326            Self::Exec { callee: None } => Some(Box::from("<unknown>")),
327            Self::Exec {
328                callee: Some(callee),
329            } => {
330                let name = match callee.split_once("::") {
331                    Some((module, rest)) if module == strip_prefix => demangle(rest),
332                    _ => demangle(callee),
333                };
334                Some(name.into_boxed_str())
335            }
336            _ => None,
337        }
338    }
339
340    pub fn display(&self) -> String {
341        match self {
342            Self::Full { op, .. } | Self::Basic { op } => format!("{op}"),
343            Self::Exec {
344                callee: Some(callee),
345            } => format!("exec.{callee}"),
346            Self::Exec { callee: None } => "exec.<unavailable>".to_string(),
347        }
348    }
349
350    pub fn opcode(&self) -> Operation {
351        match self {
352            Self::Full { op, .. } | Self::Basic { op } => *op,
353            Self::Exec { .. } => panic!("no opcode associated with execs"),
354        }
355    }
356
357    pub fn location(&self) -> Option<&Location> {
358        match self {
359            Self::Full { location, .. } => location.as_ref(),
360            Self::Basic { .. } | Self::Exec { .. } => None,
361        }
362    }
363
364    pub fn resolve(&self, source_manager: &dyn SourceManager) -> Option<&ResolvedLocation> {
365        match self {
366            Self::Full {
367                location: Some(loc),
368                resolved,
369                ..
370            } => resolved
371                .get_or_init(|| {
372                    let path = Path::new(loc.uri().as_str());
373                    let source_file = if path.exists() {
374                        source_manager.load_file(path).ok()?
375                    } else {
376                        source_manager.get_by_uri(loc.uri())?
377                    };
378                    let span = SourceSpan::new(source_file.id(), loc.start..loc.end);
379                    let file_line_col = source_file.location(span);
380                    Some(ResolvedLocation {
381                        source_file,
382                        line: file_line_col.line.to_u32(),
383                        col: file_line_col.column.to_u32(),
384                        span,
385                    })
386                })
387                .as_ref(),
388            _ => None,
389        }
390    }
391}
392
393#[derive(Debug, Clone)]
394pub struct ResolvedLocation {
395    pub source_file: Arc<SourceFile>,
396    // TODO(fabrio): Use LineNumber and ColumnNumber instead of raw `u32`.
397    pub line: u32,
398    pub col: u32,
399    pub span: SourceSpan,
400}
401impl fmt::Display for ResolvedLocation {
402    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
403        write!(f, "{}:{}:{}", self.source_file.uri().as_str(), self.line, self.col)
404    }
405}
406
407pub struct CurrentFrame {
408    pub procedure: Option<Rc<str>>,
409    pub location: Option<ResolvedLocation>,
410}
411
412pub struct StackTrace<'a> {
413    callstack: &'a CallStack,
414    recent: &'a VecDeque<Operation>,
415    source_manager: &'a dyn SourceManager,
416    current_frame: Option<CurrentFrame>,
417}
418
419impl<'a> StackTrace<'a> {
420    pub fn new(
421        callstack: &'a CallStack,
422        recent: &'a VecDeque<Operation>,
423        source_manager: &'a dyn SourceManager,
424    ) -> Self {
425        let current_frame = callstack.current_frame().map(|frame| {
426            let location = frame.last_resolved(source_manager).cloned();
427            let procedure = frame.procedure("");
428            CurrentFrame {
429                procedure,
430                location,
431            }
432        });
433        Self {
434            callstack,
435            recent,
436            source_manager,
437            current_frame,
438        }
439    }
440
441    pub fn current_frame(&self) -> Option<&CurrentFrame> {
442        self.current_frame.as_ref()
443    }
444}
445
446impl fmt::Display for StackTrace<'_> {
447    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
448        use std::fmt::Write;
449
450        let num_frames = self.callstack.frames.len();
451
452        writeln!(f, "\nStack Trace:")?;
453
454        for (i, frame) in self.callstack.frames.iter().enumerate() {
455            let is_top = i + 1 == num_frames;
456            let name = frame.procedure("");
457            let name = name.as_deref().unwrap_or("<unknown>");
458            if is_top {
459                write!(f, " `-> {name}")?;
460            } else {
461                write!(f, " |-> {name}")?;
462            }
463            if let Some(resolved) = frame.last_resolved(self.source_manager) {
464                write!(f, " in {resolved}")?;
465            } else {
466                write!(f, " in <unavailable>")?;
467            }
468            if is_top {
469                // Print op context
470                let context_size = frame.context.len();
471                writeln!(f, ":\n\nLast {context_size} Instructions (of current frame):")?;
472                for (i, op) in frame.context.iter().enumerate() {
473                    let is_last = i + 1 == context_size;
474                    if let Some(callee) = op.callee("") {
475                        write!(f, " |   exec.{callee}")?;
476                    } else {
477                        write!(f, " |   {}", &op.opcode())?;
478                    }
479                    if is_last {
480                        writeln!(f, "\n `-> <error occured here>")?;
481                    } else {
482                        f.write_char('\n')?;
483                    }
484                }
485
486                let context_size = self.recent.len();
487                writeln!(f, "\n\nLast {context_size} Instructions (any frame):")?;
488                for (i, op) in self.recent.iter().enumerate() {
489                    let is_last = i + 1 == context_size;
490                    if is_last {
491                        writeln!(f, " |   {}", &op)?;
492                        writeln!(f, " `-> <error occured here>")?;
493                    } else {
494                        writeln!(f, " |   {}", &op)?;
495                    }
496                }
497            } else {
498                f.write_char('\n')?;
499            }
500        }
501
502        Ok(())
503    }
504}
505
506fn demangle(name: &str) -> String {
507    let mut input = name.as_bytes();
508    let mut demangled = Vec::with_capacity(input.len() * 2);
509    rustc_demangle::demangle_stream(&mut input, &mut demangled, /* include_hash= */ false)
510        .expect("failed to write demangled identifier");
511    String::from_utf8(demangled).expect("demangled identifier contains invalid utf-8")
512}