Skip to main content

miden_debug_engine/debug/stacktrace/
recovered.rs

1use miden_processor::{DebugCallFrame, DebugCallFrameResolver, ResumeContext};
2
3use super::*;
4
5/// Session-owned source-frame recovery, independent of the event stack used for execution control.
6#[derive(Default)]
7pub struct RecoveredCallStack {
8    resolver: DebugCallFrameResolver,
9    frames: Vec<RecoveredFrame>,
10    current_frame_recovered: bool,
11    next_invocation: u64,
12}
13
14/// Session-local identity of a recovered invocation, independent of stack position.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub struct InvocationId(pub(crate) u64);
17
18struct RecoveredFrame {
19    invocation: InvocationId,
20    identity: DebugCallFrame,
21    location: Option<Location>,
22    inline_frames: Vec<RecoveredInlineFrame>,
23}
24
25struct RecoveredInlineFrame {
26    display: InlineCallFrame,
27    info: Arc<PackageDebugInfo>,
28    function_idx: DebugFunctionIdx,
29}
30
31/// An invocation to finish, captured before execution resumes.
32///
33/// Source identity distinguishes recursive invocations. Event depth is used only when no
34/// invocation can be recovered at the point where the target is captured.
35#[derive(Clone)]
36pub struct ReturnTarget {
37    invocation: Option<DebugCallFrame>,
38    event_depth: usize,
39}
40
41impl ReturnTarget {
42    pub fn is_recovered(&self) -> bool {
43        self.invocation.is_some()
44    }
45
46    pub fn has_returned(&self, stack: &RecoveredCallStack, event_depth: usize) -> bool {
47        match self.invocation.as_ref() {
48            Some(invocation) => {
49                !stack.frames.iter().any(|frame| frame.identity.is_same_frame(invocation))
50            }
51            None => event_depth < self.event_depth,
52        }
53    }
54}
55
56impl RecoveredCallStack {
57    pub fn current_invocation(&self) -> Option<InvocationId> {
58        self.frames
59            .last()
60            .filter(|_| self.current_frame_recovered)
61            .map(|frame| frame.invocation)
62    }
63
64    pub fn invocation_ids(&self) -> impl Iterator<Item = InvocationId> + '_ {
65        self.frames.iter().map(|frame| frame.invocation)
66    }
67
68    pub fn return_target(&self, event_depth: usize) -> ReturnTarget {
69        ReturnTarget {
70            invocation: self
71                .frames
72                .last()
73                .filter(|_| self.current_frame_recovered)
74                .map(|frame| frame.identity.clone()),
75            event_depth,
76        }
77    }
78
79    /// Whether the innermost recovered frame belongs to the active package. A stripped callee
80    /// may leave recovered ancestors visible without providing a frame for the current invocation.
81    pub fn has_current_frame(&self) -> bool {
82        self.current_frame_recovered
83    }
84
85    pub fn is_empty(&self) -> bool {
86        self.frames.is_empty()
87    }
88
89    /// Observe the operation about to execute. Suspended callers retain their last source location.
90    pub fn update(
91        &mut self,
92        context: &ResumeContext,
93        current: Option<(DebugSourceNodeId, u32)>,
94        asmop: Option<&AssemblyOp>,
95    ) {
96        let identities = self.resolver.resolve(context);
97        let common = self
98            .frames
99            .iter()
100            .zip(&identities)
101            .take_while(|(old, new)| old.identity.is_same_frame(new))
102            .count();
103        self.frames.truncate(common);
104        for (index, identity) in identities.into_iter().enumerate() {
105            if let Some(frame) = self.frames.get_mut(index) {
106                frame.identity = identity;
107                continue;
108            }
109            let invocation = InvocationId(self.next_invocation);
110            self.next_invocation =
111                self.next_invocation.checked_add(1).expect("invocation identity exhausted");
112            self.frames.push(RecoveredFrame {
113                invocation,
114                identity,
115                location: None,
116                inline_frames: Vec::new(),
117            });
118        }
119
120        let debug_info = context.debug_info();
121        self.current_frame_recovered = self
122            .frames
123            .last()
124            .zip(debug_info.as_deref())
125            .is_some_and(|(frame, info)| core::ptr::eq(frame.identity.debug_info(), info));
126        let mut rows = Vec::new();
127        if let Some((info, (source, operation))) = debug_info.as_ref().zip(current) {
128            rows.extend(info.inline_calls_for_operation(source, operation).map(|row| (info, row)));
129        }
130        for inherited in context.inherited_inline_call_contexts() {
131            rows.extend(inherited.inline_calls().map(|row| (inherited.debug_info(), row)));
132        }
133        rows.reverse();
134
135        for index in 0..self.frames.len() {
136            let start = self.frames[index].identity.inherited_inline_calls().min(rows.len());
137            let next = self.frames.get(index + 1);
138            let end = next
139                .map_or(rows.len(), |next| next.identity.inherited_inline_calls().min(rows.len()));
140            // An exec boundary is represented both by an inline row and a recovered invocation.
141            // Compare package/function identities: names cannot distinguish aliases or recursion.
142            // Across packages, the imported declaration and loaded definition share a MAST root,
143            // but their function indices belong to different tables.
144            let boundary = next.and_then(|next| {
145                let (info, row) = *rows.get(end.checked_sub(1)?)?;
146                let same_function = if core::ptr::eq(info.as_ref(), next.identity.debug_info()) {
147                    row.callee_idx == next.identity.function_idx()
148                } else {
149                    info.get_function(row.callee_idx)?.mast_root
150                        == next.identity.function().mast_root
151                };
152                (end > start && same_function).then_some((info, row))
153            });
154            let top = index + 1 == self.frames.len();
155            let frame = &mut self.frames[index];
156            if top || end > start {
157                let own_end = end - usize::from(boundary.is_some());
158                frame.inline_frames.clear();
159                for (info, row) in rows[start..own_end.max(start)].iter().rev() {
160                    let Some(function) = info.get_function(row.callee_idx) else {
161                        continue;
162                    };
163                    let Some(name) = info.get_string(function.name_idx) else {
164                        continue;
165                    };
166                    let Some(call_site) = info.get_location(row.loc_idx) else {
167                        continue;
168                    };
169                    frame.inline_frames.push(RecoveredInlineFrame {
170                        display: InlineCallFrame { name, call_site },
171                        info: Arc::clone(info),
172                        function_idx: row.callee_idx,
173                    });
174                }
175            }
176            if let Some((info, row)) = boundary {
177                frame.location = info.get_location(row.loc_idx);
178            } else if top && let Some(location) = asmop.and_then(AssemblyOp::location) {
179                frame.location = Some(location.clone());
180            }
181        }
182    }
183
184    pub fn logical_frames(&self, strip_prefix: &str) -> Vec<LogicalStackFrame> {
185        let mut logical = Vec::new();
186        for frame in &self.frames {
187            let name = &frame.identity.debug_info()[frame.identity.function().name_idx];
188            let name = name.trim_start_matches("::");
189            let name = match name.split_once("::") {
190                Some((module, rest)) if module == strip_prefix => rest,
191                _ => name,
192            };
193            let location = frame
194                .inline_frames
195                .last()
196                .map(|inline| &inline.display.call_site)
197                .or(frame.location.as_ref())
198                .cloned()
199                .map(LogicalFrameLocation::Assembly);
200            logical.push(LogicalStackFrame {
201                name: Arc::from(demangle(name)),
202                kind: LogicalFrameKind::Physical,
203                location,
204                physical_index: None,
205                origin: Some(frame.identity.origin()),
206                invocation: Some(frame.invocation),
207                source: Some(LogicalFrameSource::Physical(frame.identity.clone())),
208            });
209            for index in (0..frame.inline_frames.len()).rev() {
210                let inline = &frame.inline_frames[index];
211                let location = if index == 0 {
212                    frame.location.clone()
213                } else {
214                    Some(frame.inline_frames[index - 1].display.call_site.clone())
215                };
216                logical.push(LogicalStackFrame {
217                    name: Arc::from(inline.display.display_name()),
218                    kind: LogicalFrameKind::Inline,
219                    location: location.map(LogicalFrameLocation::Assembly),
220                    physical_index: None,
221                    origin: None,
222                    invocation: None,
223                    source: Some(LogicalFrameSource::Inline {
224                        owner: frame.invocation,
225                        info: Arc::clone(&inline.info),
226                        function_idx: inline.function_idx,
227                        call_site: inline.display.call_site.clone(),
228                    }),
229                });
230            }
231        }
232        logical
233    }
234}
235
236#[cfg(test)]
237mod tests;