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