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