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