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