Skip to main content

miden_debug_engine/exec/
collect_trace.rs

1use alloc::{collections::VecDeque, sync::Arc, vec::Vec};
2
3use miden_core::FMP_ADDR;
4use miden_debug_types::DefaultSourceManager;
5use miden_processor::{ExecutionError, Felt, event::EventId};
6
7use super::{
8    DebugQuery, Event, ExecutionConfig, Executor, ReplaySnapshot, source::SourceContext,
9    state::DebugExecutor,
10};
11use crate::debug::{
12    CallTrace, CallTraceRecorder, DebugVarSnapshot, FrameTransition, resolve_typed_variable_values,
13    return_felt_count, value_felt_count,
14};
15
16/// The result of replaying a recorded execution with [`replay_call_trace`].
17pub struct CallTraceReplay {
18    pub trace: CallTrace,
19    pub cycles: usize,
20    pub error: Option<ExecutionError>,
21}
22
23/// Replay `recording` and collect its call trace.
24///
25/// Calls with insufficient source metadata retain their timing and children, but may have no
26/// callee name, arguments, or result values. Callee recovery conservatively stops at metadata gaps.
27pub fn replay_call_trace(recording: ReplaySnapshot) -> CallTraceReplay {
28    let ReplaySnapshot {
29        package,
30        stack_inputs,
31        advice_inputs,
32        options,
33        event_log,
34        mast_forests,
35    } = recording;
36
37    let executor = Executor::from_config(ExecutionConfig {
38        inputs: stack_inputs,
39        advice_inputs,
40        options,
41    });
42    let mut executor = executor.into_debug_with_replay(
43        package,
44        Arc::new(DefaultSourceManager::default()),
45        mast_forests,
46        VecDeque::from(event_log),
47    );
48
49    let (trace, error) = collect_call_trace(&mut executor);
50
51    CallTraceReplay {
52        trace,
53        cycles: executor.cycle,
54        error,
55    }
56}
57
58/// Run `executor` to completion and collect its call trace.
59///
60/// If execution fails, the trace collected up to the failure is returned alongside the error.
61fn collect_call_trace(executor: &mut DebugExecutor) -> (CallTrace, Option<ExecutionError>) {
62    let mut recorder = CallTraceRecorder::default();
63    let mut error = None;
64
65    while !executor.stopped {
66        if let Err(err) = executor.step_with_observer(|executor, source| {
67            observe_callee(&mut recorder, executor, source);
68            observe_arguments(&mut recorder, executor);
69        }) {
70            error = Some(err);
71            break;
72        }
73        // The last step runs no cycle, so the executor state is stale and must not be read.
74        if executor.stopped {
75            break;
76        }
77
78        let clk = executor.cycle;
79        // Frame events open and close frames, even when the call stack has nothing to pop.
80        let transition = executor.callstack.take_frame_transition();
81
82        // Only `exit` uses the stack.
83        let stack = match transition {
84            Some(FrameTransition::Exited) => stack_without_frame_marker(&executor.current_stack),
85            _ => Vec::new(),
86        };
87
88        match transition {
89            Some(FrameTransition::Exited) => recorder.exit(clk, &stack),
90            // On the entry cycle execution is still in the caller, so the callee is named later.
91            Some(FrameTransition::Entered) => {
92                recorder.enter(clk, executor.callstack.logical_frames(""));
93            }
94            None => {}
95        }
96    }
97
98    (recorder.finish(), error)
99}
100
101fn observe_callee(
102    recorder: &mut CallTraceRecorder,
103    executor: &DebugExecutor,
104    source: &SourceContext,
105) {
106    if !recorder.needs_callee() {
107        return;
108    }
109    if !executor.callstack.has_recovered_current_frame() || has_metadata_gap(source) {
110        recorder.stop_callee_search();
111        return;
112    }
113    let frames = executor.callstack.logical_frames("");
114    if let Some(callee) = recorder.pending_callee(&frames)
115        && let Some((info, function)) = callee.debug_info().zip(callee.function())
116    {
117        recorder.observe_callee(
118            &info[function.name_idx],
119            return_felt_count(info, function),
120            callee.invocation_id(),
121        );
122    }
123}
124
125/// A stripped static import can retain a source node and recovered ancestors without attribution
126/// for its own code. Do not let a later unmarked descendant supply its name or result signature.
127///
128/// This is deliberately a call-trace heuristic: an empty source occurrence may also be generated
129/// code, while inherited inline rows or merged operations can hide a gap. On a detected gap we
130/// prefer an unnamed frame, preserving its timing and independently marked children.
131///
132/// TODO: Remove this heuristic when the processor resolver exposes active-invocation recovery
133/// evidence that distinguishes the current invocation from retained ancestors.
134fn has_metadata_gap(source: &SourceContext) -> bool {
135    source
136        .source_node()
137        .is_none_or(|node| node.asm_ops.is_empty() && node.inline_calls.is_empty())
138}
139
140fn observe_arguments(recorder: &mut CallTraceRecorder, executor: &DebugExecutor) {
141    // Only inspect the selected physical invocation. Inline-local ownership is not
142    // available, so an exec-only wrapper must not borrow an unmarked leaf's arguments.
143    if let Some(enter_clk) = recorder.accepting_args()
144        && let Some(owner) = recorder.argument_owner()
145        && executor.debug_vars.current_invocation() == Some(owner)
146    {
147        for snapshot in executor.debug_vars.current_variables() {
148            let Some(index) = snapshot.info.arg_index() else {
149                continue;
150            };
151            // Up to and including `enter_clk`, variables belong to the caller.
152            if usize::from(snapshot.clk) <= enter_clk {
153                continue;
154            }
155            // Already recorded, so skip reading VM memory again.
156            if recorder.has_arg(index.get()) {
157                continue;
158            }
159            // An untyped argument has no width, so it is recorded by name only.
160            let ty = snapshot.info.ty();
161            let felt_count = ty.and_then(value_felt_count);
162            let values = felt_count
163                .zip(ty)
164                .and_then(|(count, ty)| resolve_arg_values(executor, snapshot, ty, count));
165            recorder.observe_arg(index.get(), snapshot.info.name(), felt_count, values);
166        }
167    }
168}
169
170/// Read `count` consecutive felts of a typed argument out of the VM state.
171fn resolve_arg_values(
172    executor: &DebugExecutor,
173    snapshot: &DebugVarSnapshot,
174    ty: &miden_assembly_syntax::ast::types::Type,
175    count: usize,
176) -> Option<Vec<u64>> {
177    let to_u64 = |values: Vec<Felt>| values.iter().map(Felt::as_canonical_u64).collect();
178
179    // `Stack` locations are resolved at the debug decorator and rewritten to a `Const`, which
180    // holds a single felt (see `snapshot_transient_debug_values`). Anything wider than one felt
181    // survives only in the capture.
182    if let Some(captured) = executor.debug_vars.captured_values(snapshot.info.name())
183        && captured.len() == count
184    {
185        return Some(to_u64(captured.to_vec()));
186    }
187
188    let read_memory = |addr: u32| executor.read_memory_element(addr);
189    let read_local = |offset: i16| {
190        let fmp = read_memory(FMP_ADDR.as_canonical_u64() as u32)?;
191        let fmp = u32::try_from(fmp.as_canonical_u64()).ok()?;
192        read_memory(fmp.checked_add_signed(i32::from(offset))?)
193    };
194
195    resolve_typed_variable_values(
196        snapshot.info.value_location(),
197        ty,
198        count,
199        &executor.current_stack,
200        read_memory,
201        read_local,
202    )
203    .map(to_u64)
204}
205
206fn stack_without_frame_marker(stack: &[Felt]) -> Vec<u64> {
207    // `emit.event(...)` pushes the event id, so drop the top element when it is a frame event id.
208    let is_marker = |value: &Felt| {
209        let id = EventId::from_felt(*value);
210        id == Event::FrameStart.as_event_id() || id == Event::FrameEnd.as_event_id()
211    };
212
213    let rest = match stack.split_first() {
214        Some((top, rest)) if is_marker(top) => rest,
215        _ => stack,
216    };
217
218    rest.iter().map(Felt::as_canonical_u64).collect()
219}
220
221#[cfg(test)]
222mod tests;