1use std::{
2 borrow::ToOwned,
3 boxed::Box,
4 collections::{BTreeSet, VecDeque},
5 path::{Path, PathBuf},
6 string::{String, ToString},
7 sync::Arc,
8 vec::Vec,
9};
10
11use miden_assembly::{DefaultSourceManager, SourceManager};
12use miden_assembly_syntax::diagnostics::Report;
13use miden_debug_engine::{DebugQuery, normalize_source_path};
14use miden_debug_types::{Location, SourceManagerExt, SourceSpan};
15use miden_mast_package::Package;
16use miden_processor::{
17 Felt, LoadedMastForest, StackInputs,
18 advice::{AdviceInputs, AdviceMutation},
19};
20
21use crate::{
22 config::DebuggerConfig,
23 debug::{
24 Breakpoint, BreakpointType, OperationMatcher, ReadMemoryExpr, ResolvedLocation,
25 TypedProcedure, format_value, resolve_typed_variable_values, resolve_variable_value,
26 },
27 exec::{DebugExecutor, ExecutionConfig, Executor},
28};
29
30#[derive(Debug, Copy, Clone, PartialEq, Eq)]
32pub enum DebugMode {
33 Program,
35 Transaction,
37 Remote,
39}
40
41fn clone_event_replay_queue(event_replay: &[Vec<AdviceMutation>]) -> VecDeque<Vec<AdviceMutation>> {
42 event_replay
43 .iter()
44 .map(|batch| crate::exec::clone_advice_mutations(batch))
45 .collect()
46}
47
48pub struct State {
49 pub source_manager: Arc<dyn SourceManager>,
50 pub config: Box<DebuggerConfig>,
51 pub input_mode: InputMode,
52 pub breakpoints: Vec<Breakpoint>,
53 pub breakpoints_hit: Vec<Breakpoint>,
54 pub next_breakpoint_id: u8,
55 pub stopped: bool,
56 pub debug_mode: DebugMode,
57 selected_stack_frame: usize,
58 session: SessionState,
59}
60
61#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
62pub enum InputMode {
63 #[default]
64 Normal,
65 #[allow(dead_code)]
66 Insert,
67 Command,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct DebugVariableSource {
73 pub path: String,
74 pub line: u32,
75 pub column: u32,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct DebugVariableValue {
81 pub name: String,
82 pub value: Option<Felt>,
83 pub display_value: Option<String>,
84 pub location: String,
85 pub source: Option<DebugVariableSource>,
86}
87
88struct LocalState {
89 executor: DebugExecutor,
90 execution_failed: Option<miden_processor::ExecutionError>,
91 typed_procedure: Option<TypedProcedure>,
92}
93
94#[cfg(feature = "dap")]
95struct RemoteState {
96 client: crate::exec::DapClient,
97 executor: DebugExecutor,
98 addr: String,
99 synced_bp_files: std::collections::BTreeSet<String>,
102}
103
104enum SessionState {
105 Local(Box<LocalState>),
106 #[cfg(feature = "dap")]
107 Remote(Box<RemoteState>),
108}
109
110#[cfg(feature = "dap")]
111struct RemoteSnapshot {
112 callstack: crate::debug::CallStack,
113 current_stack: Vec<Felt>,
114 cycle: usize,
115}
116
117#[cfg(feature = "dap")]
118impl RemoteState {
119 fn connect(addr: &str, source_manager: &Arc<dyn SourceManager>) -> Result<Self, Report> {
120 use std::{cell::RefCell, collections::BTreeSet, rc::Rc};
121
122 use miden_debug_engine::{debug::DebugVarTracker, profiling::Profiler};
123 use miden_processor::{ContextId, FastProcessor};
124
125 use crate::exec::DebuggerHost;
126
127 let mut client = crate::exec::DapClient::connect(addr).map_err(Report::msg)?;
128 let ui_state = client.handshake().map_err(Report::msg)?;
129 let snapshot = convert_ui_state(&ui_state, source_manager);
130
131 let debug_vars = DebugVarTracker::new(Rc::new(RefCell::new(Default::default())));
132 let executor = DebugExecutor {
133 processor: FastProcessor::new(StackInputs::default()),
134 host: DebuggerHost::new(source_manager.clone()),
135 resume_ctx: None,
136 current_stack: snapshot.current_stack,
137 current_op: None,
138 current_asmop: None,
139 stack_outputs: Default::default(),
140 contexts: BTreeSet::new(),
141 root_context: ContextId::root(),
142 current_context: ContextId::root(),
143 callstack: snapshot.callstack,
144 current_proc: None,
145 debug_vars,
146 last_debug_var_count: 0,
147 recent: VecDeque::new(),
148 cycle: snapshot.cycle,
149 stopped: false,
150 profiler: Profiler::default(),
151 };
152
153 Ok(Self {
154 client,
155 executor,
156 addr: addr.to_string(),
157 synced_bp_files: std::collections::BTreeSet::new(),
158 })
159 }
160
161 fn read_memory(&mut self, expr: &ReadMemoryExpr) -> Result<String, String> {
162 self.client.read_memory(expr)
163 }
164
165 fn sync_breakpoints(&mut self, breakpoints: &[Breakpoint]) {
166 use std::collections::BTreeMap;
167
168 let mut by_file: BTreeMap<String, Vec<i64>> = BTreeMap::new();
170 let mut func_names: Vec<String> = Vec::new();
172
173 for bp in breakpoints {
174 match &bp.ty {
175 BreakpointType::Line { pattern, line } => {
176 by_file.entry(pattern.glob().to_string()).or_default().push(*line as i64);
177 }
178 BreakpointType::Called(pattern) | BreakpointType::File(pattern) => {
179 func_names.push(pattern.glob().to_string());
180 }
181 _ => {}
182 }
183 }
184
185 let stale_files: Vec<String> = self
188 .synced_bp_files
189 .iter()
190 .filter(|f| !by_file.contains_key(f.as_str()))
191 .cloned()
192 .collect();
193 for file in &stale_files {
194 let _ = self.client.set_breakpoints(file, &[]);
195 }
196
197 for (file, lines) in &by_file {
199 let _ = self.client.set_breakpoints(file, lines);
200 }
201
202 let _ = self.client.set_function_breakpoints(&func_names);
204
205 self.synced_bp_files = by_file.into_keys().collect();
207 }
208
209 fn resume(&mut self, breakpoints: &[Breakpoint]) -> Result<crate::exec::DapStopReason, String> {
210 self.sync_breakpoints(breakpoints);
212
213 let has_step = breakpoints.iter().any(|bp| matches!(bp.ty, BreakpointType::Step));
214 let has_next = breakpoints
215 .iter()
216 .any(|bp| matches!(bp.ty, BreakpointType::Next | BreakpointType::NextLine));
217 let has_finish = breakpoints.iter().any(|bp| matches!(bp.ty, BreakpointType::Finish));
218
219 if has_step {
220 self.client.step_in()
221 } else if has_next {
222 self.client.step_over()
223 } else if has_finish {
224 self.client.step_out()
225 } else {
226 self.client.continue_()
227 }
228 }
229
230 fn refresh_executor(
231 &mut self,
232 source_manager: &Arc<dyn SourceManager>,
233 pushed: &crate::exec::DapUiState,
234 ) {
235 let snapshot = convert_ui_state(pushed, source_manager);
241 self.executor.current_stack = snapshot.current_stack;
242 self.executor.callstack = snapshot.callstack;
243 self.executor.cycle = snapshot.cycle;
244 }
245
246 fn reconnect(&mut self, source_manager: &Arc<dyn SourceManager>) -> Result<(), Report> {
247 let timeout = std::time::Duration::from_secs(30);
248 let mut new_client =
249 crate::exec::DapClient::connect_with_retry(&self.addr, timeout).map_err(Report::msg)?;
250 let ui_state = new_client.handshake().map_err(Report::msg)?;
251 let snapshot = convert_ui_state(&ui_state, source_manager);
252
253 self.client = new_client;
254 self.executor.current_stack = snapshot.current_stack;
255 self.executor.callstack = snapshot.callstack;
256 self.executor.cycle = snapshot.cycle;
257 Ok(())
258 }
259}
260
261impl State {
262 fn new_local(
263 source_manager: Arc<dyn SourceManager>,
264 config: Box<DebuggerConfig>,
265 debug_mode: DebugMode,
266 local: LocalState,
267 ) -> Self {
268 Self {
269 source_manager,
270 config,
271 input_mode: InputMode::Normal,
272 breakpoints: vec![],
273 breakpoints_hit: vec![],
274 next_breakpoint_id: 0,
275 stopped: true,
276 debug_mode,
277 selected_stack_frame: 0,
278 session: SessionState::Local(Box::new(local)),
279 }
280 }
281
282 pub fn new(config: Box<DebuggerConfig>) -> Result<Self, Report> {
283 let source_manager = Arc::new(DefaultSourceManager::default());
284 let local = create_local_state(&config, source_manager.clone())?;
285
286 Ok(Self::new_local(source_manager, config, DebugMode::Program, local))
287 }
288
289 pub fn new_for_transaction(
295 package: Arc<Package>,
296 stack_inputs: StackInputs,
297 advice_inputs: AdviceInputs,
298 options: miden_processor::ExecutionOptions,
299 source_manager: Arc<dyn SourceManager>,
300 mast_forests: Vec<LoadedMastForest>,
301 event_replay: Vec<Vec<AdviceMutation>>,
302 ) -> Result<Self, Report> {
303 let executor = Executor::from_config(ExecutionConfig {
305 inputs: stack_inputs,
306 advice_inputs,
307 options,
308 });
309 let debug_executor = executor.into_debug_with_replay(
310 package,
311 source_manager.clone(),
312 mast_forests,
313 clone_event_replay_queue(&event_replay),
314 );
315
316 Ok(Self::new_local(
317 source_manager,
318 Box::default(),
319 DebugMode::Transaction,
320 LocalState {
321 executor: debug_executor,
322 execution_failed: None,
323 typed_procedure: None,
324 },
325 ))
326 }
327
328 pub fn reload(&mut self) -> Result<(), Report> {
329 if self.debug_mode == DebugMode::Transaction {
330 return Err(Report::msg("reload is not supported in transaction debug mode"));
331 }
332 if self.debug_mode == DebugMode::Remote {
333 #[cfg(feature = "dap")]
334 {
335 let source_manager = self.source_manager.clone();
336 let SessionState::Remote(remote) = &mut self.session else {
337 return Err(Report::msg("no remote debug session"));
338 };
339 let result = remote.client.restart_phase2().map_err(Report::msg)?;
340 match result {
341 crate::exec::DapStopReason::Restarting => {
342 remote.reconnect(&source_manager)?;
343 }
344 crate::exec::DapStopReason::Stopped(snapshot) => {
345 remote.refresh_executor(&source_manager, &snapshot);
347 }
348 crate::exec::DapStopReason::Terminated => {
349 return Err(Report::msg("server terminated without restart signal"));
350 }
351 }
352 }
353 #[cfg(not(feature = "dap"))]
354 return Err(Report::msg("remote debug mode requires the `dap` feature"));
355 } else {
356 log::debug!("reloading program");
357 let local = create_local_state(&self.config, self.source_manager.clone())?;
358
359 self.session = SessionState::Local(Box::new(local));
360 let breakpoints = core::mem::take(&mut self.breakpoints);
361 self.breakpoints.reserve(breakpoints.len());
362 self.next_breakpoint_id = 0;
363 for bp in breakpoints {
364 if bp.is_internal() {
370 continue;
371 }
372 self.create_breakpoint(bp.ty);
373 }
374 }
375
376 self.finish_reload();
377 Ok(())
378 }
379
380 fn finish_reload(&mut self) {
381 self.executor_mut().stopped = false;
382 self.selected_stack_frame = 0;
383 self.breakpoints_hit.clear();
384 self.stopped = true;
385 }
386
387 pub fn run_until_stopped(&mut self) {
389 let start_cycle = self.executor().cycle;
390 let start_asmop = self.executor().current_asmop.clone();
391 let start_proc = self.current_procedure();
392 let start_line_loc = self.current_display_location();
393 let source_path_prefixes = self.source_path_prefixes();
394 let minimum_source_line =
395 start_proc.as_deref().zip(start_line_loc.as_ref()).and_then(|(proc, loc)| {
396 self.minimum_source_line_for_proc(proc, loc.source_file.uri().as_str())
397 });
398 let mut previous_proc = self.current_procedure();
399 let mut previous_source_loc = self.current_user_source_location();
400 let mut previous_internal_loc = self.current_internal_source_location();
401 let mut pending_called_breakpoints = Vec::new();
402 let mut breakpoints = core::mem::take(&mut self.breakpoints);
403 self.breakpoints_hit.clear();
404 self.stopped = false;
405
406 let stopped = loop {
407 if self.executor().stopped {
408 break true;
409 }
410
411 let mut consume_most_recent_finish = false;
412 match self.executor_mut().step() {
413 Ok(Some(exited)) if exited.should_break_on_exit() => {
414 consume_most_recent_finish = true;
415 }
416 Ok(_) => {}
417 Err(err) => {
418 self.set_execution_failed(err);
419 break true;
420 }
421 }
422
423 if breakpoints.is_empty() {
424 continue;
425 }
426
427 let is_op_boundary = self.executor().current_asmop.is_some();
428 let user_source_loc = self.current_user_source_location();
429 let internal_source_loc = self.current_internal_source_location();
430 let line_loc = self.current_display_location();
431 let proc = self.current_procedure();
432 let current_cycle = self.executor().cycle;
433 let cycles_stepped = current_cycle - start_cycle;
434 let has_internal_breakpoint = breakpoints.iter().any(|bp| bp.is_internal());
435 let current_op = self.executor().current_op;
436 let current_asmop_str = if breakpoints
437 .iter()
438 .any(|bp| matches!(&bp.ty, BreakpointType::Opcode(OperationMatcher::Asm(_))))
439 {
440 self.executor().current_asmop.as_ref().map(|asmop| asmop.op().to_string())
441 } else {
442 None
443 };
444
445 breakpoints.retain_mut(|bp| {
446 if let Some(n) = bp.cycles_to_skip(current_cycle) {
447 if cycles_stepped > 0 && n == 0 {
448 let retained = !bp.is_one_shot();
449 if retained {
450 self.breakpoints_hit.push(bp.clone());
451 } else {
452 self.breakpoints_hit.push(core::mem::take(bp));
453 }
454 return retained;
455 }
456 return true;
457 }
458
459 if cycles_stepped > 0
460 && is_op_boundary
461 && matches!(&bp.ty, BreakpointType::Next)
462 && self.executor().current_asmop != start_asmop
463 {
464 self.breakpoints_hit.push(core::mem::take(bp));
465 return false;
466 }
467
468 if cycles_stepped > 0
469 && is_op_boundary
470 && matches!(&bp.ty, BreakpointType::NextLine)
471 && Self::is_next_source_line(
472 start_proc.as_deref(),
473 start_line_loc.as_ref(),
474 proc.as_deref(),
475 line_loc.as_ref(),
476 &source_path_prefixes,
477 minimum_source_line,
478 )
479 {
480 self.breakpoints_hit.push(core::mem::take(bp));
481 return false;
482 }
483
484 if has_internal_breakpoint && !bp.is_internal() {
485 return true;
486 }
487
488 if cycles_stepped > 0
492 && (current_op
493 .is_some_and(|op| bp.should_break_for(&op, &self.executor().state()))
494 || (is_op_boundary
495 && matches!(
496 (&bp.ty, current_asmop_str.as_deref()),
497 (
498 BreakpointType::Opcode(OperationMatcher::Asm(expected)),
499 Some(current),
500 ) if expected == current
501 )))
502 {
503 self.breakpoints_hit.push(bp.clone());
504 return true;
505 }
506
507 if let Some(loc) = user_source_loc.as_ref()
511 && bp.should_break_at(loc)
512 && !previous_source_loc.as_ref().is_some_and(|prev| bp.should_break_at(prev))
513 {
514 let retained = !bp.is_one_shot();
515 if retained {
516 self.breakpoints_hit.push(bp.clone());
517 } else {
518 self.breakpoints_hit.push(core::mem::take(bp));
519 }
520 return retained;
521 }
522
523 if let Some(loc) = internal_source_loc.as_ref()
530 && bp.should_break_at(loc)
531 && !previous_internal_loc.as_ref().is_some_and(|prev| bp.should_break_at(prev))
532 {
533 let retained = !bp.is_one_shot();
534 if retained {
535 self.breakpoints_hit.push(bp.clone());
536 } else {
537 self.breakpoints_hit.push(core::mem::take(bp));
538 }
539 return retained;
540 }
541
542 if matches!(&bp.ty, BreakpointType::Called(_))
543 && let Some(proc) = proc.as_deref()
544 {
545 let matched = bp.should_break_in(proc);
546 if !matched {
547 pending_called_breakpoints.retain(|id| *id != bp.id);
548 return true;
549 }
550
551 let was_matched = previous_proc
552 .as_deref()
553 .is_some_and(|previous| bp.should_break_in(previous));
554 let matched_at_start =
555 start_proc.as_deref().is_some_and(|start| bp.should_break_in(start));
556 let pending = pending_called_breakpoints.contains(&bp.id);
557 let entered_matching_proc = !was_matched && !matched_at_start;
558
559 if entered_matching_proc
560 && self.should_defer_called_breakpoint(proc, line_loc.as_ref())
561 {
562 if !pending {
563 pending_called_breakpoints.push(bp.id);
564 }
565 return true;
566 }
567
568 if entered_matching_proc
569 || (pending
570 && !self.should_defer_called_breakpoint(proc, line_loc.as_ref()))
571 {
572 pending_called_breakpoints.retain(|id| *id != bp.id);
573 let retained = !bp.is_one_shot();
574 if retained {
575 self.breakpoints_hit.push(bp.clone());
576 } else {
577 self.breakpoints_hit.push(core::mem::take(bp));
578 }
579 return retained;
580 }
581 }
582
583 true
584 });
585
586 if consume_most_recent_finish
587 && let Some(id) = breakpoints.iter().rev().find_map(|bp| {
588 if matches!(bp.ty, BreakpointType::Finish) {
589 Some(bp.id)
590 } else {
591 None
592 }
593 })
594 {
595 breakpoints.retain(|bp| bp.id != id);
596 break true;
597 }
598
599 if !self.breakpoints_hit.is_empty() {
600 break true;
601 }
602
603 previous_proc = proc;
604 previous_source_loc = user_source_loc;
605 previous_internal_loc = internal_source_loc;
606 };
607
608 self.breakpoints = breakpoints;
609 self.stopped = stopped;
610 self.selected_stack_frame = 0;
611 }
612
613 pub fn create_breakpoint(&mut self, ty: BreakpointType) {
614 let id = self.next_breakpoint_id();
615 let creation_cycle = self.executor().cycle;
616 log::trace!("created breakpoint with id {id} at cycle {creation_cycle}");
617 if matches!(ty, BreakpointType::Finish)
618 && let Some(frame) = self.executor_mut().callstack.current_frame_mut()
619 {
620 frame.break_on_exit();
621 }
622 self.breakpoints.push(Breakpoint {
623 id,
624 creation_cycle,
625 ty,
626 });
627 }
628
629 fn next_breakpoint_id(&mut self) -> u8 {
630 let mut candidate = self.next_breakpoint_id;
631 let initial = candidate;
632 let mut next = candidate.wrapping_add(1);
633 loop {
634 assert_ne!(initial, next, "unable to allocate a breakpoint id: too many breakpoints");
635 if self
636 .breakpoints
637 .iter()
638 .chain(self.breakpoints_hit.iter())
639 .any(|bp| bp.id == candidate)
640 {
641 candidate = next;
642 next = candidate.wrapping_add(1);
643 continue;
644 }
645 self.next_breakpoint_id = next;
646 break candidate;
647 }
648 }
649
650 pub fn executor(&self) -> &DebugExecutor {
651 match &self.session {
652 SessionState::Local(local) => &local.executor,
653 #[cfg(feature = "dap")]
654 SessionState::Remote(remote) => &remote.executor,
655 }
656 }
657
658 pub fn executor_mut(&mut self) -> &mut DebugExecutor {
659 match &mut self.session {
660 SessionState::Local(local) => &mut local.executor,
661 #[cfg(feature = "dap")]
662 SessionState::Remote(remote) => &mut remote.executor,
663 }
664 }
665
666 pub fn current_procedure(&self) -> Option<Arc<str>> {
667 let live_proc = self
668 .executor()
669 .current_asmop
670 .as_ref()
671 .map(|op| op.context_name().clone())
672 .or_else(|| self.executor().current_proc.clone());
673 let frame_proc =
674 self.executor().callstack.current_frame().and_then(|frame| frame.procedure(""));
675 live_proc.or(frame_proc)
676 }
677
678 pub fn current_location(&self) -> Option<ResolvedLocation> {
679 self.executor()
680 .callstack
681 .current_frame()
682 .and_then(|frame| frame.recent().back())
683 .and_then(|detail| self.resolve_op_location(detail.location()?))
684 }
685
686 pub fn current_display_location(&self) -> Option<ResolvedLocation> {
687 let frame = self.executor().callstack.current_frame()?;
688 for detail in frame.recent().iter().rev() {
689 if let Some(location) = detail.location()
690 && let Some(resolved) = self.resolve_op_location(location)
691 {
692 return Some(resolved);
693 }
694 }
695 None
696 }
697
698 pub fn logical_stack_frames(&self) -> Vec<crate::debug::LogicalStackFrame> {
699 self.executor().callstack.logical_frames("")
700 }
701
702 pub fn selected_stack_frame(&self) -> usize {
703 self.selected_stack_frame
704 }
705
706 pub fn select_older_stack_frame(&mut self) {
707 let last = self.logical_stack_frames().len().saturating_sub(1);
708 self.selected_stack_frame = self.selected_stack_frame.saturating_add(1).min(last);
709 }
710
711 pub fn select_newer_stack_frame(&mut self) {
712 self.selected_stack_frame = self.selected_stack_frame.saturating_sub(1);
713 }
714
715 pub fn selected_display_location(&self) -> Option<ResolvedLocation> {
716 self.logical_stack_frames()
717 .iter()
718 .rev()
719 .nth(self.selected_stack_frame)
720 .and_then(|frame| frame.resolved(&*self.source_manager))
721 }
722
723 fn current_user_source_location(&self) -> Option<ResolvedLocation> {
731 for frame in self.executor().callstack.frames().iter().rev() {
732 for detail in frame.recent().iter().rev() {
733 if let Some(location) = detail.location()
734 && let Some(resolved) = self.resolve_op_location(location)
735 {
736 if crate::debug::is_internal_source_uri(resolved.source_file.uri()) {
737 break;
740 }
741 return Some(resolved);
742 }
743 }
744 }
745 None
746 }
747
748 fn current_internal_source_location(&self) -> Option<ResolvedLocation> {
756 self.current_location()
757 .filter(|loc| crate::debug::is_internal_source_uri(loc.source_file.uri()))
758 }
759
760 pub fn is_next_source_line(
761 start_proc: Option<&str>,
762 start_loc: Option<&ResolvedLocation>,
763 current_proc: Option<&str>,
764 current_loc: Option<&ResolvedLocation>,
765 source_path_prefixes: &[String],
766 minimum_source_line: Option<u32>,
767 ) -> bool {
768 let same_proc = match (start_proc, current_proc) {
769 (Some(start), Some(current)) => start == current,
770 (Some(_), None) => false,
771 _ => true,
772 };
773 if !same_proc {
774 return false;
775 }
776
777 if let (Some(minimum_source_line), Some(current)) = (minimum_source_line, current_loc)
778 && current.line < minimum_source_line
779 {
780 return false;
781 }
782
783 match (start_loc, current_loc) {
784 (Some(start), Some(current)) => {
785 source_paths_match(
786 start.source_file.uri().as_str(),
787 current.source_file.uri().as_str(),
788 source_path_prefixes,
789 ) && start.line != current.line
790 }
791 (None, Some(_)) => true,
792 _ => false,
793 }
794 }
795
796 pub(crate) fn minimum_source_line_for_proc(
797 &self,
798 procedure: &str,
799 source_path: &str,
800 ) -> Option<u32> {
801 let executor = self.executor();
802 let ctx = executor.resume_ctx.as_ref()?;
803 let mast_forest = ctx.current_forest();
804 let debug_info = ctx.debug_info()?;
805
806 let source_path_prefixes = self.source_path_prefixes();
807
808 let mut lines = BTreeSet::<u32>::default();
809 for function_info in debug_info.functions() {
810 if debug_info[function_info.name_idx].as_ref() != procedure {
811 continue;
812 }
813 let source_node_id = function_info.source_node.into_option().or_else(|| {
814 mast_forest.find_procedure_root(function_info.mast_root).and_then(|exec_node| {
815 debug_info.unique_source_root_for_exec_node(exec_node).ok().flatten()
816 })
817 })?;
818 for asm_op in debug_info[source_node_id].asm_ops.iter() {
819 let Some(location_idx) = asm_op.location_idx.into_option() else {
820 continue;
821 };
822 let location = debug_info.get_location(location_idx).unwrap();
823 let Some(resolved_location) = self.resolve_op_location(&location) else {
824 continue;
825 };
826 if resolved_location.line > 1
827 && source_paths_match(
828 resolved_location.source_file.uri().as_str(),
829 source_path,
830 &source_path_prefixes,
831 )
832 {
833 lines.insert(resolved_location.line);
834 }
835 }
836 }
837 lines.pop_first()
838 }
839
840 pub(crate) fn source_path_prefixes(&self) -> Vec<String> {
841 let mut prefixes = self
842 .config
843 .source_path_prefixes
844 .iter()
845 .map(|path| path.to_string_lossy().into_owned())
846 .collect::<Vec<_>>();
847 if let Ok(cwd) = std::env::current_dir() {
848 let cwd = cwd.to_string_lossy().into_owned();
849 if !prefixes
850 .iter()
851 .any(|prefix| normalize_source_path(prefix) == normalize_source_path(&cwd))
852 {
853 prefixes.push(cwd);
854 }
855 }
856 prefixes
857 }
858
859 fn resolve_op_location(&self, loc: &Location) -> Option<ResolvedLocation> {
860 let source_file = self.load_source_file_for_uri(loc.uri())?;
861 let span = SourceSpan::new(source_file.id(), loc.start..loc.end);
862 let file_line_col = source_file.location(span);
863 Some(ResolvedLocation {
864 source_file,
865 line: file_line_col.line.to_u32(),
866 col: file_line_col.column.to_u32(),
867 span,
868 })
869 }
870
871 fn load_source_file_for_uri(
872 &self,
873 uri: &miden_debug_types::Uri,
874 ) -> Option<Arc<miden_debug_types::SourceFile>> {
875 let uri_str = uri.as_str();
876 let normalized_uri = uri_str.strip_prefix("file://").unwrap_or(uri_str);
877 let path = Path::new(normalized_uri);
878 if path.exists() {
879 return self.source_manager.load_file(path).ok();
880 }
881
882 if let Some(source_file) = self.source_manager.get_by_uri(uri) {
883 return Some(source_file);
884 }
885
886 for candidate in source_path_candidates(normalized_uri, &self.source_path_prefixes()) {
887 if candidate.exists()
888 && let Ok(source_file) = self.source_manager.load_file(&candidate)
889 {
890 return Some(source_file);
891 }
892 }
893
894 None
895 }
896
897 pub fn should_defer_called_breakpoint(
898 &self,
899 proc: &str,
900 current_loc: Option<&ResolvedLocation>,
901 ) -> bool {
902 let executor = self.executor();
903 if executor.debug_vars.current_variables().any(|variable| {
904 variable.clk == miden_processor::trace::RowIndex::from(executor.cycle as u32)
905 }) {
906 return false;
907 }
908 (!is_internal_procedure(proc)
909 && current_loc
910 .is_none_or(|loc| crate::debug::is_internal_source_uri(loc.source_file.uri())))
911 || executor.should_wait_for_entry_variables(proc)
912 }
913
914 pub fn execution_failed(&self) -> Option<&miden_processor::ExecutionError> {
915 match &self.session {
916 SessionState::Local(local) => local.execution_failed.as_ref(),
917 #[cfg(feature = "dap")]
918 SessionState::Remote(_) => None,
919 }
920 }
921
922 fn execution_completed(&self) -> bool {
923 self.executor().stopped && self.execution_failed().is_none()
924 }
925
926 pub fn typed_result(&self) -> Result<Option<String>, String> {
928 if !self.execution_completed() {
929 return Ok(None);
930 }
931 let local = match &self.session {
932 SessionState::Local(local) => local,
933 #[cfg(feature = "dap")]
934 SessionState::Remote(_) => return Ok(None),
935 };
936 let Some(procedure) = local.typed_procedure.as_ref() else {
937 return Ok(None);
938 };
939
940 procedure
941 .decode_result(local.executor.stack_outputs.get_num_elements(16))
942 .map_err(|err| format!("failed to decode program result: {err}"))
943 }
944
945 pub fn set_execution_failed(&mut self, error: miden_processor::ExecutionError) {
946 match &mut self.session {
947 SessionState::Local(local) => local.execution_failed = Some(error),
948 #[cfg(feature = "dap")]
949 SessionState::Remote(_) => {
950 panic!("cannot record local execution failure while in remote mode")
951 }
952 }
953 }
954}
955
956macro_rules! write_with_format_type {
957 ($out:ident, $read_expr:ident, $value:expr) => {
958 match $read_expr.format {
959 crate::debug::FormatType::Decimal => write!(&mut $out, "{}", $value).unwrap(),
960 crate::debug::FormatType::Hex => write!(&mut $out, "{:#x}", $value).unwrap(),
961 crate::debug::FormatType::Binary => write!(&mut $out, "{:#b}", $value).unwrap(),
962 }
963 };
964}
965
966impl State {
967 pub fn read_memory(&mut self, expr: &ReadMemoryExpr) -> Result<String, String> {
968 use core::fmt::Write;
969
970 use miden_assembly_syntax::ast::types::Type;
971
972 use crate::debug::FormatType;
973
974 #[cfg(feature = "dap")]
975 if self.debug_mode == DebugMode::Remote {
976 let SessionState::Remote(remote) = &mut self.session else {
977 return Err("no remote debug session".into());
978 };
979 return remote.read_memory(expr);
980 }
981
982 #[cfg(not(feature = "dap"))]
983 if self.debug_mode == DebugMode::Remote {
984 return Err("remote debug mode requires the `dap` feature".into());
985 }
986
987 let executor = self.executor();
988 let cycle = miden_processor::trace::RowIndex::from(executor.cycle);
989 let context = executor.current_context;
990 let memory = executor.processor.memory();
991 let read_element = |addr: u32| -> Option<Felt> {
992 memory
993 .read_element(context, Felt::new(addr as u64).expect("value exceeds field modulus"))
994 .ok()
995 };
996 let mut output = String::new();
997 if expr.count > 1 {
998 return Err("-count with value > 1 is not yet implemented".into());
999 } else if matches!(expr.ty, Type::Felt) {
1000 if !expr.addr.is_element_aligned() {
1001 return Err(
1002 "read failed: type 'felt' must be aligned to an element boundary".into()
1003 );
1004 }
1005 let felt = read_element(expr.addr.addr).unwrap_or(Felt::ZERO);
1006 write_with_format_type!(output, expr, felt.as_canonical_u64());
1007 } else if matches!(
1008 expr.ty,
1009 Type::Array(ref array_ty) if array_ty.element_type() == &Type::Felt && array_ty.len() == 4
1010 ) {
1011 if !expr.addr.is_word_aligned() {
1012 return Err("read failed: type 'word' must be aligned to a word boundary".into());
1013 }
1014 let word = memory
1015 .read_word(
1016 context,
1017 Felt::new(expr.addr.addr as u64).expect("value exceeds field modulus"),
1018 cycle,
1019 )
1020 .unwrap_or_default();
1021 output.push('[');
1022 for (i, elem) in word.iter().enumerate() {
1023 if i > 0 {
1024 output.push_str(", ");
1025 }
1026 write_with_format_type!(output, expr, elem.as_canonical_u64());
1027 }
1028 output.push(']');
1029 } else {
1030 if !expr.addr.is_element_aligned() {
1031 return Err("invalid read: unaligned reads are not supported yet".into());
1032 }
1033
1034 const U32_MASK: u64 = u32::MAX as u64;
1035 let size = expr.ty.size_in_bytes();
1036 let size_in_felts = expr.ty.size_in_felts();
1037 let mut bytes = Vec::with_capacity(size);
1038 let mut needed = size;
1039 for i in 0..size_in_felts {
1040 let addr = expr.addr.addr.checked_add(i as u32).ok_or_else(|| {
1041 "invalid read: attempted to read beyond end of linear memory".to_string()
1042 })?;
1043 let elem = read_element(addr).unwrap_or_default();
1044 let elem_bytes = ((elem.as_canonical_u64() & U32_MASK) as u32).to_le_bytes();
1045 let take = core::cmp::min(needed, 4);
1046 bytes.extend(&elem_bytes[..take]);
1047 needed -= take;
1048 }
1049
1050 match &expr.ty {
1051 Type::I1 => match expr.format {
1052 FormatType::Decimal => write!(&mut output, "{}", bytes[0] != 0).unwrap(),
1053 FormatType::Hex => {
1054 write!(&mut output, "{:#0x}", (bytes[0] != 0) as u8).unwrap()
1055 }
1056 FormatType::Binary => {
1057 write!(&mut output, "{:#0b}", (bytes[0] != 0) as u8).unwrap()
1058 }
1059 },
1060 Type::I8 => write_with_format_type!(output, expr, bytes[0] as i8),
1061 Type::U8 => write_with_format_type!(output, expr, bytes[0]),
1062 Type::I16 => {
1063 write_with_format_type!(output, expr, i16::from_le_bytes([bytes[0], bytes[1]]))
1064 }
1065 Type::U16 => {
1066 write_with_format_type!(output, expr, u16::from_le_bytes([bytes[0], bytes[1]]))
1067 }
1068 Type::I32 => write_with_format_type!(
1069 output,
1070 expr,
1071 i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
1072 ),
1073 Type::U32 => write_with_format_type!(
1074 output,
1075 expr,
1076 u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
1077 ),
1078 ty @ (Type::I64 | Type::U64) => {
1079 let val = u64::from_le_bytes(bytes[..8].try_into().unwrap());
1080 if matches!(ty, Type::I64) {
1081 write_with_format_type!(output, expr, val as i64)
1082 } else {
1083 write_with_format_type!(output, expr, val)
1084 }
1085 }
1086 ty => {
1087 return Err(format!(
1088 "support for reads of type '{ty}' are not implemented yet"
1089 ));
1090 }
1091 }
1092 }
1093
1094 Ok(output)
1095 }
1096
1097 pub fn current_variables(&self, show_all: bool) -> Vec<DebugVariableValue> {
1105 if self.execution_completed() {
1106 return Vec::new();
1107 }
1108
1109 let executor = self.executor();
1110 let debug_vars = &executor.debug_vars;
1111
1112 let stack = executor.current_stack.clone();
1113 let context = executor.current_context;
1114
1115 let read_mem = |addr: u32| -> Option<Felt> {
1117 executor
1118 .processor
1119 .memory()
1120 .read_element(context, Felt::new(addr as u64).expect("value exceeds field modulus"))
1121 .ok()
1122 };
1123
1124 let current_source = if show_all {
1125 None
1126 } else {
1127 self.current_display_location()
1128 };
1129 let source_path_prefixes = self.source_path_prefixes();
1130
1131 let mut variables = Vec::new();
1132
1133 for var_snapshot in debug_vars.current_variables() {
1134 let name = var_snapshot.info.name();
1135
1136 if !show_all && is_compiler_generated_name(name) {
1137 continue;
1138 }
1139
1140 if let (Some(current), Some(var_loc)) =
1141 (current_source.as_ref(), var_snapshot.info.location())
1142 && let Some(var_loc) = self.resolve_op_location(var_loc)
1143 && !source_var_location_is_visible(
1144 var_loc.source_file.uri().as_str(),
1145 var_loc.line,
1146 current.source_file.uri().as_str(),
1147 current.line,
1148 &source_path_prefixes,
1149 )
1150 {
1151 continue;
1152 }
1153
1154 let location = var_snapshot.info.value_location();
1155 let resolve_local = |offset: i16| {
1156 let fmp_addr = miden_core::FMP_ADDR.as_canonical_u64() as u32;
1158 let fmp = read_mem(fmp_addr)?;
1159 let addr = (fmp.as_canonical_u64() as i64 + offset as i64) as u32;
1160 read_mem(addr)
1161 };
1162
1163 let display_value = var_snapshot.info.ty().and_then(|ty| {
1164 format_value(ty, |count| {
1165 debug_vars
1166 .captured_values(name)
1167 .filter(|values| values.len() == count)
1168 .map(<[Felt]>::to_vec)
1169 .or_else(|| {
1170 resolve_typed_variable_values(
1171 location,
1172 ty,
1173 count,
1174 &stack,
1175 read_mem,
1176 resolve_local,
1177 )
1178 })
1179 })
1180 });
1181
1182 let value = debug_vars
1183 .captured_values(name)
1184 .and_then(|values| values.first().copied())
1185 .or_else(|| resolve_variable_value(location, &stack, read_mem, resolve_local));
1186
1187 let source = var_snapshot.info.location().and_then(|loc| {
1188 let loc = self.resolve_op_location(loc)?;
1189 Some(DebugVariableSource {
1190 path: loc.source_file.uri().as_str().to_string(),
1191 line: loc.line,
1192 column: loc.col,
1193 })
1194 });
1195
1196 variables.push(DebugVariableValue {
1197 name: name.to_string(),
1198 value,
1199 display_value,
1200 location: location.to_string(),
1201 source,
1202 });
1203 }
1204
1205 variables
1206 }
1207
1208 pub fn format_variables(&self, show_all: bool) -> String {
1213 use core::fmt::Write;
1214
1215 if self.execution_completed() {
1216 return "Program has terminated; no live variables".to_string();
1217 }
1218
1219 if !self.executor().debug_vars.has_variables() {
1220 return "No debug variables tracked".to_string();
1221 }
1222
1223 let variables = self.current_variables(show_all);
1224 if variables.is_empty() {
1225 "No source-level variables (use ':vars all' to show compiler locals)".to_string()
1226 } else {
1227 let mut output = String::new();
1228 for variable in variables {
1229 if !output.is_empty() {
1230 output.push_str(", ");
1231 }
1232
1233 if let Some(value) = variable.display_value {
1234 write!(&mut output, "{}={value}", variable.name).unwrap();
1235 continue;
1236 }
1237
1238 match variable.value {
1239 Some(felt) => {
1240 write!(&mut output, "{}={}", variable.name, felt.as_canonical_u64())
1241 .unwrap();
1242 }
1243 None => {
1244 write!(&mut output, "{}={}", variable.name, variable.location).unwrap();
1245 }
1246 }
1247 }
1248 output
1249 }
1250 }
1251}
1252
1253fn is_internal_procedure(proc: &str) -> bool {
1254 proc.contains("::intrinsics::")
1255}
1256
1257fn is_compiler_generated_name(name: &str) -> bool {
1260 name.strip_prefix("local")
1261 .is_some_and(|suffix| !suffix.is_empty() && suffix.chars().all(|c| c.is_ascii_digit()))
1262}
1263
1264fn source_var_location_is_visible(
1265 var_path: &str,
1266 var_line: u32,
1267 current_path: &str,
1268 current_line: u32,
1269 source_path_prefixes: &[String],
1270) -> bool {
1271 source_paths_match(var_path, current_path, source_path_prefixes) && var_line < current_line
1272}
1273
1274fn strip_source_prefix(path: &str, prefix: &str) -> Option<String> {
1275 let path = path.trim_start_matches('/');
1276 let prefix = prefix.trim_start_matches('/').trim_end_matches('/');
1277 path.strip_prefix(prefix)
1278 .and_then(|rest| rest.strip_prefix('/'))
1279 .map(ToOwned::to_owned)
1280}
1281
1282fn source_paths_match(left: &str, right: &str, trim_prefixes: &[String]) -> bool {
1283 let left = normalize_source_path(left);
1284 let right = normalize_source_path(right);
1285 if left.is_empty() || right.is_empty() {
1286 return false;
1287 }
1288
1289 if left == right {
1290 return true;
1291 }
1292
1293 for prefix in trim_prefixes {
1294 if strip_source_prefix(&left, prefix).is_some_and(|stripped| stripped == right) {
1295 return true;
1296 }
1297 if strip_source_prefix(&right, prefix).is_some_and(|stripped| stripped == left) {
1298 return true;
1299 }
1300 }
1301
1302 false
1303}
1304
1305fn source_path_candidates(uri: &str, source_path_prefixes: &[String]) -> Vec<PathBuf> {
1306 let normalized = normalize_source_path(uri);
1307 if normalized.is_empty() || Path::new(&normalized).is_absolute() {
1308 return Vec::new();
1309 }
1310
1311 source_path_prefixes
1312 .iter()
1313 .map(|prefix| Path::new(prefix).join(&normalized))
1314 .collect()
1315}
1316
1317#[cfg(feature = "dap")]
1321impl State {
1322 pub fn new_for_dap(addr: &str) -> Result<Self, Report> {
1327 let source_manager: Arc<dyn SourceManager> = Arc::new(DefaultSourceManager::default());
1328 let remote = RemoteState::connect(addr, &source_manager)?;
1329
1330 Ok(Self {
1331 source_manager,
1332 config: Box::default(),
1333 input_mode: InputMode::Normal,
1334 breakpoints: vec![],
1335 breakpoints_hit: vec![],
1336 next_breakpoint_id: 0,
1337 stopped: true,
1338 debug_mode: DebugMode::Remote,
1339 selected_stack_frame: 0,
1340 session: SessionState::Remote(Box::new(remote)),
1341 })
1342 }
1343
1344 pub fn step_remote(&mut self) -> Result<crate::exec::DapStopReason, Report> {
1345 let source_manager = self.source_manager.clone();
1346 let SessionState::Remote(remote) = &mut self.session else {
1347 return Err(Report::msg("no remote debug session"));
1348 };
1349 let result = remote.resume(&self.breakpoints).map_err(Report::msg)?;
1350
1351 self.breakpoints.retain(|bp| !bp.is_one_shot());
1352
1353 match &result {
1354 crate::exec::DapStopReason::Stopped(snapshot) => {
1355 remote.refresh_executor(&source_manager, snapshot);
1356 self.selected_stack_frame = 0;
1357 self.stopped = true;
1358 }
1359 crate::exec::DapStopReason::Terminated => {
1360 remote.executor.stopped = true;
1361 self.stopped = true;
1362 }
1363 crate::exec::DapStopReason::Restarting => {
1364 return Err(Report::msg("unexpected Phase 2 restart signal during step"));
1365 }
1366 }
1367
1368 Ok(result)
1369 }
1370}
1371
1372#[cfg(feature = "dap")]
1375fn convert_ui_state(
1376 snapshot: &crate::exec::DapUiState,
1377 source_manager: &Arc<dyn SourceManager>,
1378) -> RemoteSnapshot {
1379 use crate::debug::{CallFrame, CallStack};
1380
1381 let call_frames: Vec<CallFrame> = snapshot
1382 .callstack
1383 .iter()
1384 .rev()
1385 .map(|frame| {
1386 let resolved = resolve_remote_frame(frame, source_manager);
1387 CallFrame::from_remote(Some(frame.name.clone()), resolved)
1388 })
1389 .collect();
1390
1391 let current_stack = snapshot
1392 .current_stack
1393 .iter()
1394 .copied()
1395 .map(|v| Felt::new(v).expect("value exceeds field modulus"))
1396 .collect();
1397
1398 RemoteSnapshot {
1399 callstack: CallStack::from_remote_frames(call_frames),
1400 current_stack,
1401 cycle: snapshot.cycle,
1402 }
1403}
1404
1405#[cfg(feature = "dap")]
1407fn resolve_remote_frame(
1408 frame: &crate::exec::DapUiFrame,
1409 source_manager: &Arc<dyn SourceManager>,
1410) -> Option<crate::debug::ResolvedLocation> {
1411 use std::path::Path;
1412
1413 use miden_debug_types::{SourceManagerExt, SourceSpan, Uri};
1414
1415 let path_str = frame.source_path.as_ref()?;
1416 let path = crate::debug::resolve_source_path(&Uri::new(path_str))
1417 .unwrap_or_else(|| Path::new(path_str).to_path_buf());
1418 let source_file = source_manager.load_file(&path).ok()?;
1419 let line = frame.line.max(1) as u32;
1420 let col = frame.column.max(1) as u32;
1421
1422 let content = source_file.content();
1424 let line_index = miden_debug_types::LineIndex::from(line.saturating_sub(1));
1425 let range = content.line_range(line_index)?;
1426 let span = SourceSpan::new(source_file.id(), range);
1427
1428 Some(crate::debug::ResolvedLocation {
1429 source_file,
1430 line,
1431 col,
1432 span,
1433 })
1434}
1435
1436fn create_local_state(
1437 config: &DebuggerConfig,
1438 source_manager: Arc<dyn SourceManager>,
1439) -> Result<LocalState, Report> {
1440 let loaded = crate::program_loader::load_debug_executor(config, source_manager, "state")?;
1441 Ok(LocalState {
1442 executor: loaded.executor,
1443 execution_failed: None,
1444 typed_procedure: loaded.typed_procedure,
1445 })
1446}
1447
1448#[cfg(test)]
1449mod tests {
1450 use super::*;
1451
1452 fn state_with_entry_variables(source: &str) -> State {
1453 use miden_assembly_syntax::{
1454 Parse,
1455 ast::{Block, DebugVarInfo, DebugVarLocation, Instruction, Op},
1456 debuginfo::Span,
1457 };
1458
1459 fn inject_variables(block: &mut Block) {
1460 for operation in block.iter_mut() {
1461 if let Op::If {
1462 then_blk, else_blk, ..
1463 } = operation
1464 {
1465 inject_variables(then_blk);
1466 inject_variables(else_blk);
1467 }
1468 let Op::Inst(instruction) = operation else {
1469 continue;
1470 };
1471 let location = match instruction.inner() {
1472 Instruction::Nop => DebugVarLocation::Stack(0),
1473 Instruction::Not => DebugVarLocation::Unavailable,
1474 _ => continue,
1475 };
1476 *instruction = Span::new(
1477 SourceSpan::default(),
1478 Instruction::DebugVar(DebugVarInfo::new("n", location)),
1479 );
1480 }
1481 }
1482 let source_manager = Arc::new(DefaultSourceManager::default());
1483 let mut module = Parse::parse(source, false, source_manager.clone()).unwrap();
1484 for procedure in module.procedures_mut() {
1485 inject_variables(procedure.body_mut());
1486 }
1487 let package = miden_assembly::Assembler::new(source_manager.clone())
1488 .assemble_program("program", module)
1489 .unwrap();
1490 let executor = Executor::new(Vec::new()).into_debug(package.into(), source_manager.clone());
1491 let mut state = State::new_local(
1492 source_manager,
1493 Box::<DebuggerConfig>::default(),
1494 DebugMode::Program,
1495 LocalState {
1496 executor,
1497 execution_failed: None,
1498 typed_procedure: None,
1499 },
1500 );
1501 state.create_breakpoint("in *entrypoint".parse().unwrap());
1502 state
1503 }
1504
1505 fn state_with_variables(source: &str) -> State {
1506 use miden_assembly_syntax::{
1507 Parse,
1508 ast::{DebugVarInfo, DebugVarLocation},
1509 };
1510 use miden_processor::trace::RowIndex;
1511
1512 let source_manager = Arc::new(DefaultSourceManager::default());
1513 let module = Parse::parse(source, false, source_manager.clone()).unwrap();
1514 let package = miden_assembly::Assembler::new(source_manager.clone())
1515 .assemble_program("program", module)
1516 .unwrap();
1517 let executor = Executor::new(Vec::new()).into_debug(package.into(), source_manager.clone());
1518 let mut state = State::new_local(
1519 source_manager,
1520 Box::<DebuggerConfig>::default(),
1521 DebugMode::Program,
1522 LocalState {
1523 executor,
1524 execution_failed: None,
1525 typed_procedure: None,
1526 },
1527 );
1528
1529 let tracker = &mut state.executor_mut().debug_vars;
1530 tracker.record_events_with_stack(
1531 RowIndex::from(0),
1532 vec![
1533 DebugVarInfo::new("answer", DebugVarLocation::Stack(0)),
1534 DebugVarInfo::new("local0", DebugVarLocation::Const(Felt::from(9u32))),
1535 ],
1536 &[Felt::from(7u32)],
1537 );
1538 tracker.update_to_cycle(RowIndex::from(0));
1539 state
1540 }
1541
1542 #[test]
1543 fn completed_execution_has_no_live_variables() {
1544 let mut state = state_with_variables("begin push.1 drop end");
1545
1546 assert_eq!(state.format_variables(false), "answer=7");
1547 assert_eq!(state.format_variables(true), "answer=7, local0=9");
1548
1549 state.run_until_stopped();
1550
1551 assert!(state.executor().stopped);
1552 assert!(state.execution_failed().is_none());
1553 assert!(state.executor().debug_vars.has_variables());
1554 for show_all in [false, true] {
1555 assert!(state.current_variables(show_all).is_empty());
1556 assert_eq!(
1557 state.format_variables(show_all),
1558 "Program has terminated; no live variables"
1559 );
1560 }
1561 }
1562
1563 #[test]
1564 fn failed_execution_preserves_variables_for_inspection() {
1565 let mut state = state_with_variables("begin push.0 assert end");
1566
1567 state.run_until_stopped();
1568
1569 assert!(state.executor().stopped);
1570 assert!(state.execution_failed().is_some());
1571 assert_eq!(state.current_variables(false).len(), 1);
1572 assert_eq!(state.current_variables(true).len(), 2);
1573 assert_eq!(state.format_variables(false), "answer=7");
1574 assert_eq!(state.format_variables(true), "answer=7, local0=9");
1575 }
1576
1577 #[test]
1578 fn function_breakpoint_waits_for_entry_variables() {
1579 let mut state = state_with_entry_variables(
1580 "proc entrypoint push.2 push.3 add nop drop push.1 if.true push.1 drop end end begin \
1581 exec.entrypoint end",
1582 );
1583 state.run_until_stopped();
1584 assert!(!state.executor().stopped);
1585 assert_eq!(state.breakpoints_hit.len(), 1);
1586 assert_eq!(state.format_variables(true), "n=5");
1587 }
1588
1589 #[test]
1590 fn function_breakpoint_does_not_wait_for_missing_variables() {
1591 let mut state = state_with_entry_variables(
1592 "proc entrypoint push.2 push.3 add drop end begin exec.entrypoint end",
1593 );
1594 state.run_until_stopped();
1595 assert!(!state.executor().stopped);
1596 assert_eq!(state.breakpoints_hit.len(), 1);
1597 assert!(state.current_variables(true).is_empty());
1598 }
1599
1600 #[test]
1601 fn function_breakpoint_accepts_variables_without_resolved_source() {
1602 let mut state = state_with_entry_variables(
1603 "proc entrypoint push.2 push.3 add nop drop end begin exec.entrypoint end",
1604 );
1605 state.source_manager = Arc::new(DefaultSourceManager::default());
1606 state.run_until_stopped();
1607 assert!(!state.executor().stopped);
1608 assert_eq!(state.breakpoints_hit.len(), 1);
1609 assert!(state.current_display_location().is_none());
1610 assert_eq!(state.format_variables(true), "n=5");
1611 }
1612
1613 #[test]
1614 fn function_breakpoint_ignores_caller_variables_and_kills() {
1615 let mut state = state_with_entry_variables(
1616 "proc entrypoint push.2 not push.3 add nop drop end begin push.91 nop drop \
1617 exec.entrypoint end",
1618 );
1619 state.run_until_stopped();
1620 assert!(!state.executor().stopped);
1621 assert_eq!(state.breakpoints_hit.len(), 1);
1622 assert_eq!(state.format_variables(true), "n=5");
1623 }
1624
1625 #[test]
1626 fn function_breakpoint_does_not_enter_branches_to_find_variables() {
1627 let mut state = state_with_entry_variables(
1628 "proc entrypoint push.0 if.true push.5 nop drop end push.1 drop end begin \
1629 exec.entrypoint end",
1630 );
1631 state.run_until_stopped();
1632 assert!(!state.executor().stopped);
1633 assert_eq!(state.breakpoints_hit.len(), 1);
1634 assert!(state.current_variables(true).is_empty());
1635 assert_eq!(state.executor().current_op, Some(miden_processor::operation::Operation::Pad));
1636 }
1637
1638 #[test]
1639 fn successful_reload_epilogue_resets_stack_selection() {
1640 let config = DebuggerConfig {
1641 input: Some(crate::program_loader::test_package_input()),
1642 ..Default::default()
1643 };
1644 let mut state = State::new(Box::new(config)).expect("state should build");
1645 state.selected_stack_frame = 3;
1646 state.breakpoints_hit.push(Breakpoint::default());
1647 state.stopped = false;
1648 state.executor_mut().stopped = true;
1649
1650 state.finish_reload();
1651
1652 assert!(!state.executor().stopped);
1653 assert_eq!(state.selected_stack_frame, 0);
1654 assert!(state.breakpoints_hit.is_empty());
1655 assert!(state.stopped);
1656 }
1657}