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