Skip to main content

miden_debug_engine/debug/
calltrace.rs

1use alloc::{string::String, vec::Vec};
2
3use super::{InvocationId, LogicalStackFrame};
4
5/// The hierarchy of calls made during an execution.
6#[derive(Debug, Default)]
7pub struct CallTrace {
8    pub roots: Vec<CallFrameRecord>,
9}
10
11/// A single call, with the calls it made nested underneath.
12#[derive(Debug)]
13pub struct CallFrameRecord {
14    pub callee: Option<String>,
15    pub enter_clk: usize,
16    pub exit_clk: Option<usize>,
17    pub args: Vec<TracedArg>,
18    /// Returned values, top of the stack first, or `None` when they could not be worked out.
19    pub results: Option<Vec<u64>>,
20    pub children: Vec<CallFrameRecord>,
21}
22
23/// A single argument of a traced call.
24#[derive(Debug)]
25pub struct TracedArg {
26    pub index: u32,
27    pub name: String,
28    /// How many stack elements the argument takes, or `None` if its type is not known.
29    pub felt_count: Option<usize>,
30    /// The argument's felts, or `None` if its width is not known or they could not be read.
31    pub values: Option<Vec<u64>>,
32}
33
34impl CallFrameRecord {
35    /// Number of cycles spent in this frame, including its children.
36    pub fn cycles(&self) -> Option<usize> {
37        self.exit_clk.map(|exit| exit.saturating_sub(self.enter_clk))
38    }
39}
40
41/// Builds a [`CallTrace`] from frame entries and exits reported in execution order.
42#[derive(Default)]
43pub struct CallTraceRecorder {
44    /// Frames that started and did not end yet. The last one is the current frame.
45    open: Vec<OpenFrame>,
46    /// Frames that ended when no parent frame was open.
47    roots: Vec<CallFrameRecord>,
48}
49
50struct OpenFrame {
51    record: CallFrameRecord,
52    /// Source context while selecting the callee; cleared once selected or unrecoverable.
53    caller: Option<Vec<LogicalStackFrame>>,
54    /// Felts the callee returns; `None` means unknown.
55    output_felt_count: Option<usize>,
56    argument_owner: Option<InvocationId>,
57    /// Set once this frame makes a call, after which arguments are no longer recorded.
58    args_sealed: bool,
59}
60
61impl CallTraceRecorder {
62    /// Open a frame entered at `clk`.
63    pub fn enter(&mut self, clk: usize, caller: Vec<LogicalStackFrame>) {
64        if let Some(parent) = self.open.last_mut() {
65            parent.args_sealed = true;
66        }
67        self.open.push(OpenFrame {
68            record: CallFrameRecord {
69                callee: None,
70                enter_clk: clk,
71                exit_clk: None,
72                args: Vec::new(),
73                results: None,
74                children: Vec::new(),
75            },
76            caller: (!caller.is_empty()).then_some(caller),
77            output_felt_count: None,
78            argument_owner: None,
79            args_sealed: false,
80        });
81    }
82
83    /// Whether source context is still needed to select the innermost callee.
84    pub fn needs_callee(&self) -> bool {
85        self.open.last().is_some_and(|frame| frame.caller.is_some())
86    }
87
88    /// Stop selecting a callee after entering code whose source invocation is unknown.
89    /// Later recovered descendants cannot identify the missing invocation.
90    pub fn stop_callee_search(&mut self) {
91        if let Some(frame) = self.open.last_mut() {
92            frame.caller = None;
93        }
94    }
95
96    /// The first new logical callee beneath the caller observed at the start marker.
97    /// Source identities distinguish recursion and aliases even when their names or code match.
98    pub fn pending_callee<'a>(
99        &self,
100        frames: &'a [LogicalStackFrame],
101    ) -> Option<&'a LogicalStackFrame> {
102        let caller = self.open.last()?.caller.as_ref()?;
103        if !caller
104            .iter()
105            .zip(frames)
106            .all(|(caller, current)| caller.is_same_source_frame(current))
107        {
108            return None;
109        }
110        frames.get(caller.len())
111    }
112
113    /// Freeze the selected callee's name, result width, and argument ownership together.
114    pub fn observe_callee(
115        &mut self,
116        name: &str,
117        output_felt_count: Option<usize>,
118        argument_owner: Option<InvocationId>,
119    ) {
120        if let Some(frame) = self.open.last_mut()
121            && frame.record.callee.is_none()
122        {
123            frame.caller = None;
124            frame.record.callee = Some(String::from(name));
125            frame.output_felt_count = output_felt_count;
126            frame.argument_owner = argument_owner;
127        }
128    }
129
130    pub fn argument_owner(&self) -> Option<InvocationId> {
131        self.open.last()?.argument_owner
132    }
133
134    /// The cycle the innermost open frame was entered at, while it still takes arguments.
135    ///
136    /// `None` once that frame has made a call of its own, which seals its arguments.
137    pub fn accepting_args(&self) -> Option<usize> {
138        self.open
139            .last()
140            .filter(|frame| !frame.args_sealed)
141            .map(|frame| frame.record.enter_clk)
142    }
143
144    /// Whether the innermost open frame already holds the argument at `index`.
145    ///
146    /// Lets the caller skip re-reading the argument from VM memory on every cycle, since
147    /// [`Self::observe_arg`] keeps the first value observed.
148    pub fn has_arg(&self, index: u32) -> bool {
149        self.open
150            .last()
151            .is_some_and(|frame| frame.record.args.iter().any(|arg| arg.index == index))
152    }
153
154    /// Records an argument of the innermost frame. Only the first value is kept, because the body
155    /// can overwrite a parameter.
156    ///
157    /// ```text
158    /// fn function_1(mut op: Felt) { op = 99; .. }
159    ///
160    /// op = 3    value at the call   -> kept
161    /// op = 99   written by the body -> skipped
162    /// ```
163    pub fn observe_arg(
164        &mut self,
165        index: u32,
166        name: &str,
167        felt_count: Option<usize>,
168        values: Option<Vec<u64>>,
169    ) {
170        let Some(frame) = self.open.last_mut() else {
171            return;
172        };
173        if frame.args_sealed || frame.record.args.iter().any(|arg| arg.index == index) {
174            return;
175        }
176        frame.record.args.push(TracedArg {
177            index,
178            name: String::from(name),
179            felt_count,
180            values,
181        });
182    }
183
184    /// Closes the innermost frame at `clk`; its results are the top
185    /// [`Self::observe_callee`] felts of `stack`.
186    ///
187    /// ```text
188    /// -> Felt          width 1     -> results = [top felt]
189    /// returns nothing  width 0     -> results = []
190    /// no signature     width None  -> results = None (unknown)
191    /// ```
192    pub fn exit(&mut self, clk: usize, stack: &[u64]) {
193        let Some(frame) = self.open.pop() else {
194            return;
195        };
196        let mut record = frame.record;
197        record.exit_clk = Some(clk);
198
199        // If the stack is shorter than the width, the width is wrong, so the result is unknown.
200        record.results = frame
201            .output_felt_count
202            .and_then(|count| (stack.len() >= count).then(|| stack[..count].to_vec()));
203
204        self.attach(record);
205    }
206
207    /// Return the trace, leaving frames that are still open without an exit cycle.
208    pub fn finish(mut self) -> CallTrace {
209        while let Some(frame) = self.open.pop() {
210            self.attach(frame.record);
211        }
212        CallTrace { roots: self.roots }
213    }
214
215    /// Put a closed frame in its place in the tree, with its arguments in signature order.
216    fn attach(&mut self, mut frame: CallFrameRecord) {
217        frame.args.sort_by_key(|arg| arg.index);
218        match self.open.last_mut() {
219            Some(parent) => parent.record.children.push(frame),
220            None => self.roots.push(frame),
221        }
222    }
223}
224
225#[cfg(test)]
226mod tests;