1use alloc::{
2 collections::{BTreeSet, VecDeque},
3 sync::Arc,
4 vec::Vec,
5};
6
7use miden_assembly_syntax::{ast::DebugVarLocation, debuginfo::SourceManager};
8use miden_core::operations::AssemblyOp;
9use miden_processor::{
10 ContextId, ExecutionError, FastProcessor, Felt, ResumeContext, StackOutputs,
11 operation::Operation, trace::RowIndex,
12};
13
14pub(crate) use super::source::{CurrentCycleInfo, extract_current_op};
15use super::{DebuggerHost, ExecutionTrace, source::SourceContext};
16use crate::{
17 Breakpoint, BreakpointType, OperationMatcher,
18 debug::{CallFrame, CallStack, DebugVarTracker, StepInfo},
19 profiling::Profiler,
20};
21
22pub struct DebugExecutor {
29 pub processor: FastProcessor,
31 pub host: DebuggerHost<dyn miden_assembly_syntax::debuginfo::SourceManager>,
33 pub resume_ctx: Option<ResumeContext>,
35
36 pub current_stack: Vec<Felt>,
39 pub current_op: Option<Operation>,
41 pub current_asmop: Option<AssemblyOp>,
43
44 pub stack_outputs: StackOutputs,
46 pub contexts: BTreeSet<ContextId>,
48 pub root_context: ContextId,
50 pub current_context: ContextId,
52 pub callstack: CallStack,
54 pub current_proc: Option<Arc<str>>,
56 pub debug_vars: DebugVarTracker,
58 pub last_debug_var_count: usize,
60 pub recent: VecDeque<Operation>,
62 pub cycle: usize,
64 pub stopped: bool,
66 pub profiler: Profiler,
68}
69
70impl super::query::DebugQuery for DebugExecutor {
71 #[inline]
72 fn state(&self) -> miden_processor::ProcessorState<'_> {
73 self.processor.state()
74 }
75
76 fn current_context(&self) -> ContextId {
77 self.current_context
78 }
79
80 fn current_clock(&self) -> RowIndex {
81 self.processor.state().clock()
82 }
83}
84
85impl DebugExecutor {
86 pub fn stack(&self) -> &[Felt] {
90 self.processor.stack()
91 }
92}
93
94pub(crate) fn should_wait_for_entry_variables(
98 resume_ctx: &ResumeContext,
99 procedure: &str,
100 variables: &DebugVarTracker,
101 cycle: usize,
102) -> bool {
103 if variables
104 .current_variables()
105 .any(|variable| variable.clk == RowIndex::from(cycle as u32))
106 {
107 return false;
108 }
109 let Some(debug_info) = resume_ctx.debug_info() else {
110 return false;
111 };
112 let next = extract_current_op(resume_ctx);
113 let Some(source_node) = next.source_node_id.map(|node| &debug_info[node]) else {
114 return false;
115 };
116 let Some(op_idx) = next.op_idx else {
117 return false;
118 };
119 source_node.debug_vars.iter().any(|variable| {
120 variable.op_idx as usize >= op_idx
121 && !matches!(variable.value_location, DebugVarLocation::Unavailable)
122 && source_node.asm_op_for_operation(variable.op_idx).is_some_and(|operation| {
123 debug_info[operation.context_name_idx].as_ref() == procedure
124 })
125 })
126}
127
128impl DebugExecutor {
129 pub fn should_wait_for_entry_variables(&self, procedure: &str) -> bool {
131 self.resume_ctx.as_ref().is_some_and(|resume_ctx| {
132 should_wait_for_entry_variables(resume_ctx, procedure, &self.debug_vars, self.cycle)
133 })
134 }
135
136 pub fn step(&mut self) -> Result<Option<CallFrame>, ExecutionError> {
143 self.step_with_observer(|_, _| {})
144 }
145
146 pub(crate) fn step_with_observer(
149 &mut self,
150 observe: impl FnOnce(&Self, &SourceContext),
151 ) -> Result<Option<CallFrame>, ExecutionError> {
152 if self.stopped {
153 self.last_debug_var_count = 0;
154 return Ok(None);
155 }
156
157 let resume_ctx = match self.resume_ctx.take() {
158 Some(ctx) => ctx,
159 None => {
160 self.stopped = true;
161 self.last_debug_var_count = 0;
162 return Ok(None);
163 }
164 };
165
166 let source = SourceContext::new(&resume_ctx);
167 let CurrentCycleInfo {
168 op,
169 control_flow_kind,
170 ..
171 } = source.cycle;
172 let debug_var_infos = source.debug_vars();
173 let source_asmop = source.asmop();
174 self.callstack.observe_source_context(
175 &resume_ctx,
176 source.source_operation(),
177 source_asmop.as_ref(),
178 );
179
180 let debug_var_count = debug_var_infos.len();
181 self.debug_vars.observe_execution(
182 self.callstack.invocation_ids(),
183 self.callstack.current_invocation(),
184 &self.processor,
185 RowIndex::from((self.cycle + 1) as u32),
186 debug_var_infos,
187 );
188 observe(self, &source);
189
190 let step_result = if let Some(debug_info) = source.debug_info() {
192 self.processor
193 .step_with_package_debug_info_sync(&mut self.host, resume_ctx, debug_info)
194 } else {
195 self.processor.step_sync(&mut self.host, resume_ctx)
196 };
197 match step_result {
198 Ok(Some(new_ctx)) => {
199 self.resume_ctx = Some(new_ctx);
200 self.cycle += 1;
201
202 let state = self.processor.state();
204 let ctx = state.ctx();
205 self.current_stack = state.get_stack_state();
206
207 if self.current_context != ctx {
208 self.contexts.insert(ctx);
209 self.current_context = ctx;
210 }
211
212 self.current_op = op;
214 self.current_asmop = source_asmop;
215 if let Some(asmop) = self.current_asmop.as_ref() {
216 self.current_proc = Some(asmop.context_name().clone());
217 }
218
219 if let Some(op) = op {
220 if self.recent.len() == 5 {
221 self.recent.pop_front();
222 }
223 self.recent.push_back(op);
224 self.profiler.on_operation_execution_cycle(op, self.current_proc.as_deref());
225 }
226
227 let step_info = StepInfo {
229 op,
230 control: control_flow_kind,
231 asmop: self.current_asmop.as_ref(),
232 clk: RowIndex::from(self.cycle as u32),
233 ctx: self.current_context,
234 inline_frames: source.inline_frames(),
235 };
236 let exited = self.callstack.next(&step_info);
237
238 self.last_debug_var_count = debug_var_count;
239
240 Ok(exited)
241 }
242 Ok(None) => {
243 self.debug_vars.reset();
245 self.stopped = true;
246 self.last_debug_var_count = 0;
247 let state = self.processor.state();
248 self.current_stack = state.get_stack_state();
249
250 let len = self.current_stack.len().min(16);
252 self.stack_outputs =
253 StackOutputs::new(&self.current_stack[..len]).expect("invalid stack outputs");
254
255 #[cfg(feature = "std")]
257 {
258 self.profiler.write_reports();
259 }
260 Ok(None)
261 }
262 Err(err) => {
263 self.stopped = true;
264 self.last_debug_var_count = 0;
265 Err(err)
266 }
267 }
268 }
269
270 pub fn has_returned(&self, target: &crate::debug::ReturnTarget) -> bool {
273 self.callstack.has_returned(target)
274 && (!target.is_recovered()
275 || !self.callstack.has_recovered_current_frame()
276 || (self.current_op.is_some() && self.current_asmop.is_some()))
277 }
278
279 pub fn step_until(
284 &mut self,
285 breakpoint: BreakpointType,
286 source_manager: &dyn SourceManager,
287 ) -> Result<(), ExecutionError> {
288 let start_cycle = self.cycle;
289 let breakpoint = Breakpoint {
290 id: 0,
291 creation_cycle: start_cycle,
292 ty: breakpoint,
293 };
294 let start_asmop = self.current_asmop.clone();
295 let return_target =
296 (breakpoint.ty == BreakpointType::Finish).then(|| self.callstack.return_target());
297 while !self.stopped {
298 self.step()?;
299 if return_target.as_ref().is_some_and(|target| self.has_returned(target)) {
300 return Ok(());
301 }
302
303 let (op, is_op_boundary, proc, loc) = {
304 let op = self.current_op;
305 let is_boundary = self.current_asmop.as_ref().map(|_info| true).unwrap_or(false);
306 let (proc, loc) = match self.callstack.current_frame() {
307 Some(frame) => {
308 let loc = frame
309 .recent()
310 .back()
311 .and_then(|detail| detail.resolve(source_manager))
312 .cloned();
313 (frame.procedure(""), loc)
314 }
315 None => (None, None),
316 };
317 (op, is_boundary, proc, loc)
318 };
319
320 if let Some(op) = op
321 && breakpoint.should_break_for(&op, &self.processor.state())
322 {
323 return Ok(());
324 }
325
326 if is_op_boundary
327 && let Some(asmop) = self.current_asmop.as_ref()
328 && matches!(&breakpoint.ty, BreakpointType::Opcode(OperationMatcher::Asm(expected)) if expected.as_str() == asmop.op().as_ref())
329 {
330 return Ok(());
331 }
332
333 let current_cycle = self.cycle;
335 let cycles_stepped = current_cycle - start_cycle;
336 if let Some(n) = breakpoint.cycles_to_skip(current_cycle)
337 && cycles_stepped > 0
338 && n == 0
339 {
340 return Ok(());
341 }
342
343 if cycles_stepped > 0
344 && is_op_boundary
345 && matches!(&breakpoint.ty, BreakpointType::Next)
346 && self.current_asmop != start_asmop
347 {
348 return Ok(());
349 }
350
351 if let Some(loc) = loc.as_ref()
352 && breakpoint.should_break_at(loc)
353 {
354 return Ok(());
355 }
356
357 if let Some(proc) = proc.as_deref()
358 && breakpoint.should_break_in(proc)
359 {
360 return Ok(());
361 }
362 }
363
364 Ok(())
365 }
366
367 pub fn into_execution_trace(self) -> ExecutionTrace {
369 ExecutionTrace {
370 processor: self.processor,
371 outputs: self.stack_outputs,
372 }
373 }
374}
375
376#[cfg(test)]
377mod tests;