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 pub fn next(&mut self, state: &VmState) -> Option<CallFrame> {
71 if let Some(op) = state.op {
72 let procedure =
80 state.asmop.as_ref().map(|op| self.cache_procedure_name(op.context_name()));
81 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 let ignore = matches!(
95 op,
96 Operation::Join
97 | Operation::Split
98 | Operation::Span
99 | Operation::Respan
100 | Operation::End
101 );
102
103 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 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 let procedure =
165 procedure.or_else(|| self.frames.last().and_then(|f| f.procedure.clone()));
166
167 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 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 matches!(op, Operation::FmpUpdate) {
185 current_frame.context.pop_back();
186 }
187
188 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 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 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 if let Some(event) = event {
228 match event {
229 TraceEvent::FrameStart => {
230 if let Some(current_frame) = self.frames.last_mut() {
232 current_frame.push_exec(procedure.cloned());
233 }
234 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 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 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 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}