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