1use std::{
2 collections::VecDeque,
3 path::{Path, PathBuf},
4 rc::Rc,
5 sync::Arc,
6};
7
8use miden_assembly::{DefaultSourceManager, SourceManager};
9use miden_assembly_syntax::diagnostics::{IntoDiagnostic, Report};
10use miden_core::{
11 mast::{MastNode, MastNodeId},
12 operations::AssemblyOp,
13 program::Program,
14 serde::Deserializable,
15};
16use miden_debug_types::{Location, SourceManagerExt, SourceSpan};
17use miden_processor::{
18 Felt, StackInputs,
19 advice::{AdviceInputs, AdviceMutation},
20 mast::MastForest,
21};
22
23use crate::{
24 config::DebuggerConfig,
25 debug::{Breakpoint, BreakpointType, ReadMemoryExpr, ResolvedLocation, resolve_variable_value},
26 exec::{DebugExecutor, Executor},
27 input::InputFile,
28};
29
30#[derive(Debug, Copy, Clone, PartialEq, Eq)]
32pub enum DebugMode {
33 Program,
35 Transaction,
37 Remote,
39}
40
41fn clone_advice_mutation(mutation: &AdviceMutation) -> AdviceMutation {
42 match mutation {
43 AdviceMutation::ExtendStack { values } => AdviceMutation::ExtendStack {
44 values: values.clone(),
45 },
46 AdviceMutation::ExtendMap { other } => AdviceMutation::ExtendMap {
47 other: other.clone(),
48 },
49 AdviceMutation::ExtendMerkleStore { infos } => AdviceMutation::ExtendMerkleStore {
50 infos: infos.clone(),
51 },
52 AdviceMutation::ExtendPrecompileRequests { data } => {
53 AdviceMutation::ExtendPrecompileRequests { data: data.clone() }
54 }
55 }
56}
57
58fn clone_event_replay_queue(event_replay: &[Vec<AdviceMutation>]) -> VecDeque<Vec<AdviceMutation>> {
59 event_replay
60 .iter()
61 .map(|batch| batch.iter().map(clone_advice_mutation).collect())
62 .collect()
63}
64
65pub struct State {
66 pub source_manager: Arc<dyn SourceManager>,
67 pub config: Box<DebuggerConfig>,
68 pub input_mode: InputMode,
69 pub breakpoints: Vec<Breakpoint>,
70 pub breakpoints_hit: Vec<Breakpoint>,
71 pub next_breakpoint_id: u8,
72 pub stopped: bool,
73 pub debug_mode: DebugMode,
74 session: SessionState,
75}
76
77#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
78pub enum InputMode {
79 #[default]
80 Normal,
81 #[allow(dead_code)]
82 Insert,
83 Command,
84}
85
86struct LocalState {
87 executor: DebugExecutor,
88 execution_failed: Option<miden_processor::ExecutionError>,
89}
90
91#[cfg(feature = "dap")]
92struct RemoteState {
93 client: crate::exec::DapClient,
94 executor: DebugExecutor,
95 addr: String,
96 synced_bp_files: std::collections::BTreeSet<String>,
99}
100
101enum SessionState {
102 Local(Box<LocalState>),
103 #[cfg(feature = "dap")]
104 Remote(Box<RemoteState>),
105}
106
107#[cfg(feature = "dap")]
108struct RemoteSnapshot {
109 callstack: crate::debug::CallStack,
110 current_stack: Vec<Felt>,
111 cycle: usize,
112}
113
114#[cfg(feature = "dap")]
115impl RemoteState {
116 fn connect(addr: &str, source_manager: &Arc<dyn SourceManager>) -> Result<Self, Report> {
117 use std::{cell::RefCell, collections::BTreeSet, rc::Rc};
118
119 use miden_debug_engine::debug::DebugVarTracker;
120 use miden_processor::{ContextId, FastProcessor};
121
122 use crate::exec::DebuggerHost;
123
124 let mut client = crate::exec::DapClient::connect(addr).map_err(Report::msg)?;
125 let ui_state = client.handshake().map_err(Report::msg)?;
126 let snapshot = convert_ui_state(&ui_state, source_manager);
127
128 let debug_vars = DebugVarTracker::new(Rc::new(RefCell::new(Default::default())));
129 let executor = DebugExecutor {
130 processor: FastProcessor::new(StackInputs::default()),
131 host: DebuggerHost::new(source_manager.clone()),
132 resume_ctx: None,
133 current_stack: snapshot.current_stack,
134 current_op: None,
135 current_asmop: None,
136 stack_outputs: Default::default(),
137 contexts: BTreeSet::new(),
138 root_context: ContextId::root(),
139 current_context: ContextId::root(),
140 callstack: snapshot.callstack,
141 current_proc: None,
142 debug_vars,
143 last_debug_var_count: 0,
144 recent: VecDeque::new(),
145 cycle: snapshot.cycle,
146 stopped: false,
147 };
148
149 Ok(Self {
150 client,
151 executor,
152 addr: addr.to_string(),
153 synced_bp_files: std::collections::BTreeSet::new(),
154 })
155 }
156
157 fn read_memory(&mut self, expr: &ReadMemoryExpr) -> Result<String, String> {
158 self.client.read_memory(expr)
159 }
160
161 fn sync_breakpoints(&mut self, breakpoints: &[Breakpoint]) {
162 use std::collections::BTreeMap;
163
164 let mut by_file: BTreeMap<String, Vec<i64>> = BTreeMap::new();
166 let mut func_names: Vec<String> = Vec::new();
168
169 for bp in breakpoints {
170 match &bp.ty {
171 BreakpointType::Line { pattern, line } => {
172 by_file.entry(pattern.as_str().to_string()).or_default().push(*line as i64);
173 }
174 BreakpointType::Called(pattern) | BreakpointType::File(pattern) => {
175 func_names.push(pattern.as_str().to_string());
176 }
177 _ => {}
178 }
179 }
180
181 let stale_files: Vec<String> = self
184 .synced_bp_files
185 .iter()
186 .filter(|f| !by_file.contains_key(f.as_str()))
187 .cloned()
188 .collect();
189 for file in &stale_files {
190 let _ = self.client.set_breakpoints(file, &[]);
191 }
192
193 for (file, lines) in &by_file {
195 let _ = self.client.set_breakpoints(file, lines);
196 }
197
198 let _ = self.client.set_function_breakpoints(&func_names);
200
201 self.synced_bp_files = by_file.into_keys().collect();
203 }
204
205 fn resume(&mut self, breakpoints: &[Breakpoint]) -> Result<crate::exec::DapStopReason, String> {
206 self.sync_breakpoints(breakpoints);
208
209 let has_step = breakpoints.iter().any(|bp| matches!(bp.ty, BreakpointType::Step));
210 let has_next = breakpoints
211 .iter()
212 .any(|bp| matches!(bp.ty, BreakpointType::Next | BreakpointType::NextLine));
213 let has_finish = breakpoints.iter().any(|bp| matches!(bp.ty, BreakpointType::Finish));
214
215 if has_step {
216 self.client.step_in()
217 } else if has_next {
218 self.client.step_over()
219 } else if has_finish {
220 self.client.step_out()
221 } else {
222 self.client.continue_()
223 }
224 }
225
226 fn refresh_executor(
227 &mut self,
228 source_manager: &Arc<dyn SourceManager>,
229 pushed: &crate::exec::DapUiState,
230 ) {
231 let snapshot = convert_ui_state(pushed, source_manager);
237 self.executor.current_stack = snapshot.current_stack;
238 self.executor.callstack = snapshot.callstack;
239 self.executor.cycle = snapshot.cycle;
240 }
241
242 fn reconnect(&mut self, source_manager: &Arc<dyn SourceManager>) -> Result<(), Report> {
243 let timeout = std::time::Duration::from_secs(30);
244 let mut new_client =
245 crate::exec::DapClient::connect_with_retry(&self.addr, timeout).map_err(Report::msg)?;
246 let ui_state = new_client.handshake().map_err(Report::msg)?;
247 let snapshot = convert_ui_state(&ui_state, source_manager);
248
249 self.client = new_client;
250 self.executor.current_stack = snapshot.current_stack;
251 self.executor.callstack = snapshot.callstack;
252 self.executor.cycle = snapshot.cycle;
253 Ok(())
254 }
255}
256
257impl State {
258 fn new_local(
259 source_manager: Arc<dyn SourceManager>,
260 config: Box<DebuggerConfig>,
261 debug_mode: DebugMode,
262 local: LocalState,
263 ) -> Self {
264 Self {
265 source_manager,
266 config,
267 input_mode: InputMode::Normal,
268 breakpoints: vec![],
269 breakpoints_hit: vec![],
270 next_breakpoint_id: 0,
271 stopped: true,
272 debug_mode,
273 session: SessionState::Local(Box::new(local)),
274 }
275 }
276
277 pub fn new(config: Box<DebuggerConfig>) -> Result<Self, Report> {
278 let source_manager = Arc::new(DefaultSourceManager::default());
279 let mut inputs = config.inputs.clone().unwrap_or_default();
280 if !config.args.is_empty() {
281 let args = config.args.iter().rev().map(|felt| felt.0).collect::<Vec<_>>();
283 inputs.inputs = StackInputs::new(&args).into_diagnostic()?;
284 }
285 let args = inputs.inputs.iter().copied().collect::<Vec<_>>();
286 let package = load_package(&config)?;
287
288 let mut libs = Vec::with_capacity(config.link_libraries.len());
290 for link_library in config.link_libraries.iter() {
291 log::debug!(target: "state", "loading link library {}", link_library.name());
292 let lib = link_library.load(&config, source_manager.clone())?;
293 libs.push(lib.clone());
294 }
295
296 if let Some(toolchain_dir) = config.toolchain_dir() {
298 libs.extend(load_sysroot_libs(&toolchain_dir)?);
299 }
300
301 let mut executor = Executor::new(args.clone());
303 for lib in libs.iter() {
304 executor.register_library_dependency(lib.clone());
305 executor.with_library(lib.clone());
306 }
307
308 let dependencies = package.manifest.dependencies();
310 executor.with_dependencies(dependencies)?;
311 executor.with_advice_inputs(inputs.advice_inputs);
312
313 let program = package.unwrap_program();
314 let executor = executor.into_debug(&program, source_manager.clone());
315
316 Ok(Self::new_local(
317 source_manager,
318 config,
319 DebugMode::Program,
320 LocalState {
321 executor,
322 execution_failed: None,
323 },
324 ))
325 }
326
327 pub fn new_for_transaction(
333 program: Arc<Program>,
334 stack_inputs: StackInputs,
335 advice_inputs: AdviceInputs,
336 source_manager: Arc<dyn SourceManager>,
337 mast_forests: Vec<Arc<MastForest>>,
338 event_replay: Vec<Vec<AdviceMutation>>,
339 ) -> Result<Self, Report> {
340 let args = stack_inputs.iter().copied().rev().collect::<Vec<_>>();
341
342 let mut executor = Executor::new(args);
344 executor.with_advice_inputs(advice_inputs);
345 let debug_executor = executor.into_debug_with_replay(
346 &program,
347 source_manager.clone(),
348 mast_forests,
349 clone_event_replay_queue(&event_replay),
350 );
351
352 Ok(Self::new_local(
353 source_manager,
354 Box::default(),
355 DebugMode::Transaction,
356 LocalState {
357 executor: debug_executor,
358 execution_failed: None,
359 },
360 ))
361 }
362
363 pub fn reload(&mut self) -> Result<(), Report> {
364 if self.debug_mode == DebugMode::Transaction {
365 return Err(Report::msg("reload is not supported in transaction debug mode"));
366 }
367 if self.debug_mode == DebugMode::Remote {
368 #[cfg(feature = "dap")]
369 {
370 let source_manager = self.source_manager.clone();
371 let SessionState::Remote(remote) = &mut self.session else {
372 return Err(Report::msg("no remote debug session"));
373 };
374 let result = remote.client.restart_phase2().map_err(Report::msg)?;
375 match result {
376 crate::exec::DapStopReason::Restarting => {
377 remote.reconnect(&source_manager)?;
378 }
379 crate::exec::DapStopReason::Stopped(snapshot) => {
380 remote.refresh_executor(&source_manager, &snapshot);
382 }
383 crate::exec::DapStopReason::Terminated => {
384 return Err(Report::msg("server terminated without restart signal"));
385 }
386 }
387 self.breakpoints_hit.clear();
388 self.stopped = true;
389 return Ok(());
390 }
391 #[cfg(not(feature = "dap"))]
392 return Err(Report::msg("remote debug mode requires the `dap` feature"));
393 }
394
395 log::debug!("reloading program");
396 let package = load_package(&self.config)?;
397
398 let mut inputs = self.config.inputs.clone().unwrap_or_default();
399 if !self.config.args.is_empty() {
400 let args = self.config.args.iter().rev().map(|felt| felt.0).collect::<Vec<_>>();
402 inputs.inputs = StackInputs::new(&args).into_diagnostic()?;
403 }
404 let args = inputs.inputs.iter().copied().collect::<Vec<_>>();
405
406 let mut libs = Vec::with_capacity(self.config.link_libraries.len());
408 for link_library in self.config.link_libraries.iter() {
409 let lib = link_library.load(&self.config, self.source_manager.clone())?;
410 libs.push(lib.clone());
411 }
412
413 if let Some(toolchain_dir) = self.config.toolchain_dir() {
415 libs.extend(load_sysroot_libs(&toolchain_dir)?);
416 }
417
418 let mut executor = Executor::new(args.clone());
420 for lib in libs.iter() {
421 executor.register_library_dependency(lib.clone());
422 executor.with_library(lib.clone());
423 }
424
425 let dependencies = package.manifest.dependencies();
427 executor.with_dependencies(dependencies)?;
428 executor.with_advice_inputs(inputs.advice_inputs);
429
430 let program = package.unwrap_program();
431 let executor = executor.into_debug(&program, self.source_manager.clone());
432
433 self.session = SessionState::Local(Box::new(LocalState {
434 executor,
435 execution_failed: None,
436 }));
437 self.breakpoints_hit.clear();
438 let breakpoints = core::mem::take(&mut self.breakpoints);
439 self.breakpoints.reserve(breakpoints.len());
440 self.next_breakpoint_id = 0;
441 self.stopped = true;
442 for bp in breakpoints {
443 self.create_breakpoint(bp.ty);
444 }
445 Ok(())
446 }
447
448 pub fn create_breakpoint(&mut self, ty: BreakpointType) {
449 let id = self.next_breakpoint_id();
450 let creation_cycle = self.executor().cycle;
451 log::trace!("created breakpoint with id {id} at cycle {creation_cycle}");
452 if matches!(ty, BreakpointType::Finish)
453 && let Some(frame) = self.executor_mut().callstack.current_frame_mut()
454 {
455 frame.break_on_exit();
456 }
457 self.breakpoints.push(Breakpoint {
458 id,
459 creation_cycle,
460 ty,
461 });
462 }
463
464 fn next_breakpoint_id(&mut self) -> u8 {
465 let mut candidate = self.next_breakpoint_id;
466 let initial = candidate;
467 let mut next = candidate.wrapping_add(1);
468 loop {
469 assert_ne!(initial, next, "unable to allocate a breakpoint id: too many breakpoints");
470 if self
471 .breakpoints
472 .iter()
473 .chain(self.breakpoints_hit.iter())
474 .any(|bp| bp.id == candidate)
475 {
476 candidate = next;
477 next = candidate.wrapping_add(1);
478 continue;
479 }
480 self.next_breakpoint_id = next;
481 break candidate;
482 }
483 }
484
485 pub fn executor(&self) -> &DebugExecutor {
486 match &self.session {
487 SessionState::Local(local) => &local.executor,
488 #[cfg(feature = "dap")]
489 SessionState::Remote(remote) => &remote.executor,
490 }
491 }
492
493 pub fn executor_mut(&mut self) -> &mut DebugExecutor {
494 match &mut self.session {
495 SessionState::Local(local) => &mut local.executor,
496 #[cfg(feature = "dap")]
497 SessionState::Remote(remote) => &mut remote.executor,
498 }
499 }
500
501 pub fn current_procedure(&self) -> Option<Rc<str>> {
502 let live_proc = self
503 .executor()
504 .current_asmop
505 .as_ref()
506 .map(|op| Rc::from(op.context_name()))
507 .or_else(|| self.executor().current_proc.clone());
508 let frame_proc =
509 self.executor().callstack.current_frame().and_then(|frame| frame.procedure(""));
510 live_proc.or(frame_proc)
511 }
512
513 pub fn current_location(&self) -> Option<ResolvedLocation> {
514 self.executor()
515 .callstack
516 .current_frame()
517 .and_then(|frame| frame.recent().back())
518 .and_then(|detail| self.resolve_op_location(detail.location()?))
519 }
520
521 pub fn current_display_location(&self) -> Option<ResolvedLocation> {
522 let frame = self.executor().callstack.current_frame()?;
523 for detail in frame.recent().iter().rev() {
524 if let Some(location) = detail.location()
525 && let Some(resolved) = self.resolve_op_location(location)
526 {
527 return Some(resolved);
528 }
529 }
530 None
531 }
532
533 pub fn is_next_source_line(
534 start_proc: Option<&str>,
535 start_loc: Option<&ResolvedLocation>,
536 current_proc: Option<&str>,
537 current_loc: Option<&ResolvedLocation>,
538 source_path_prefixes: &[String],
539 minimum_source_line: Option<u32>,
540 ) -> bool {
541 let same_proc = match (start_proc, current_proc) {
542 (Some(start), Some(current)) => start == current,
543 (Some(_), None) => false,
544 _ => true,
545 };
546 if !same_proc {
547 return false;
548 }
549
550 if let (Some(minimum_source_line), Some(current)) = (minimum_source_line, current_loc)
551 && current.line < minimum_source_line
552 {
553 return false;
554 }
555
556 match (start_loc, current_loc) {
557 (Some(start), Some(current)) => {
558 source_paths_match(
559 start.source_file.uri().as_str(),
560 current.source_file.uri().as_str(),
561 source_path_prefixes,
562 ) && start.line != current.line
563 }
564 (None, Some(_)) => true,
565 _ => false,
566 }
567 }
568
569 pub(crate) fn minimum_source_line_for_proc(
570 &self,
571 procedure: &str,
572 source_path: &str,
573 ) -> Option<u32> {
574 let forest = self.executor().resume_ctx.as_ref()?.current_forest();
575 let source_path_prefixes = self.source_path_prefixes();
576 let mut min_line = None;
577
578 for (node_idx, node) in forest.nodes().iter().enumerate() {
579 let MastNode::Block(block) = node else {
580 continue;
581 };
582
583 let node_id = MastNodeId::new_unchecked(node_idx as u32);
584 for op_idx in 0..block.num_operations() as usize {
585 let Some(asmop) = forest.get_assembly_op(node_id, Some(op_idx)) else {
586 continue;
587 };
588 if asmop.context_name() != procedure {
589 continue;
590 }
591 let Some((path, line)) = self.resolve_asmop_location(asmop) else {
592 continue;
593 };
594 if line > 1 && source_paths_match(&path, source_path, &source_path_prefixes) {
595 min_line = Some(min_line.map_or(line, |current: u32| current.min(line)));
596 }
597 }
598 }
599
600 min_line
601 }
602
603 pub(crate) fn source_path_prefixes(&self) -> Vec<String> {
604 #[cfg(feature = "dap")]
605 {
606 let mut prefixes = self
607 .config
608 .source_path_prefixes
609 .iter()
610 .map(|path| path.to_string_lossy().into_owned())
611 .collect::<Vec<_>>();
612 if let Ok(cwd) = std::env::current_dir() {
613 let cwd = cwd.to_string_lossy().into_owned();
614 if !prefixes
615 .iter()
616 .any(|prefix| normalize_source_path(prefix) == normalize_source_path(&cwd))
617 {
618 prefixes.push(cwd);
619 }
620 }
621 prefixes
622 }
623
624 #[cfg(not(feature = "dap"))]
625 {
626 Vec::new()
627 }
628 }
629
630 fn resolve_op_location(&self, loc: &Location) -> Option<ResolvedLocation> {
631 let source_file = self.load_source_file_for_uri(loc.uri())?;
632 let span = SourceSpan::new(source_file.id(), loc.start..loc.end);
633 let file_line_col = source_file.location(span);
634 Some(ResolvedLocation {
635 source_file,
636 line: file_line_col.line.to_u32(),
637 col: file_line_col.column.to_u32(),
638 span,
639 })
640 }
641
642 fn resolve_asmop_location(&self, asmop: &AssemblyOp) -> Option<(String, u32)> {
643 let resolved = self.resolve_op_location(asmop.location()?)?;
644 Some((resolved.source_file.uri().as_str().to_string(), resolved.line))
645 }
646
647 fn load_source_file_for_uri(
648 &self,
649 uri: &miden_debug_types::Uri,
650 ) -> Option<Arc<miden_debug_types::SourceFile>> {
651 let uri_str = uri.as_str();
652 let normalized_uri = uri_str.strip_prefix("file://").unwrap_or(uri_str);
653 let path = Path::new(normalized_uri);
654 if path.exists() {
655 return self.source_manager.load_file(path).ok();
656 }
657
658 if let Some(source_file) = self.source_manager.get_by_uri(uri) {
659 return Some(source_file);
660 }
661
662 for candidate in source_path_candidates(normalized_uri, &self.source_path_prefixes()) {
663 if candidate.exists()
664 && let Ok(source_file) = self.source_manager.load_file(&candidate)
665 {
666 return Some(source_file);
667 }
668 }
669
670 None
671 }
672
673 pub fn should_defer_called_breakpoint(
674 &self,
675 proc: &str,
676 current_loc: Option<&ResolvedLocation>,
677 ) -> bool {
678 let executor = self.executor();
679 (!is_internal_procedure(proc)
680 && current_loc
681 .is_none_or(|loc| crate::debug::is_internal_source_uri(loc.source_file.uri())))
682 || (executor.procedure_has_debug_vars(proc) && executor.last_debug_var_count == 0)
683 }
684
685 pub fn deferred_called_breakpoint_is_ready(
686 &self,
687 current_loc: Option<&ResolvedLocation>,
688 ) -> bool {
689 current_loc.is_some_and(|loc| !crate::debug::is_internal_source_uri(loc.source_file.uri()))
690 || self.executor().last_debug_var_count > 0
691 }
692
693 pub fn execution_failed(&self) -> Option<&miden_processor::ExecutionError> {
694 match &self.session {
695 SessionState::Local(local) => local.execution_failed.as_ref(),
696 #[cfg(feature = "dap")]
697 SessionState::Remote(_) => None,
698 }
699 }
700
701 pub fn set_execution_failed(&mut self, error: miden_processor::ExecutionError) {
702 match &mut self.session {
703 SessionState::Local(local) => local.execution_failed = Some(error),
704 #[cfg(feature = "dap")]
705 SessionState::Remote(_) => {
706 panic!("cannot record local execution failure while in remote mode")
707 }
708 }
709 }
710}
711
712macro_rules! write_with_format_type {
713 ($out:ident, $read_expr:ident, $value:expr) => {
714 match $read_expr.format {
715 crate::debug::FormatType::Decimal => write!(&mut $out, "{}", $value).unwrap(),
716 crate::debug::FormatType::Hex => write!(&mut $out, "{:0x}", $value).unwrap(),
717 crate::debug::FormatType::Binary => write!(&mut $out, "{:0b}", $value).unwrap(),
718 }
719 };
720}
721
722impl State {
723 pub fn read_memory(&mut self, expr: &ReadMemoryExpr) -> Result<String, String> {
724 use core::fmt::Write;
725
726 use miden_assembly_syntax::ast::types::Type;
727
728 use crate::debug::FormatType;
729
730 #[cfg(feature = "dap")]
731 if self.debug_mode == DebugMode::Remote {
732 let SessionState::Remote(remote) = &mut self.session else {
733 return Err("no remote debug session".into());
734 };
735 return remote.read_memory(expr);
736 }
737
738 #[cfg(not(feature = "dap"))]
739 if self.debug_mode == DebugMode::Remote {
740 return Err("remote debug mode requires the `dap` feature".into());
741 }
742
743 let executor = self.executor();
744 let cycle = miden_processor::trace::RowIndex::from(executor.cycle);
745 let context = executor.current_context;
746 let memory = executor.processor.memory();
747 let read_element = |addr: u32| -> Option<Felt> {
748 memory
749 .read_element(context, Felt::new(addr as u64).expect("value exceeds field modulus"))
750 .ok()
751 };
752 let mut output = String::new();
753 if expr.count > 1 {
754 return Err("-count with value > 1 is not yet implemented".into());
755 } else if matches!(expr.ty, Type::Felt) {
756 if !expr.addr.is_element_aligned() {
757 return Err(
758 "read failed: type 'felt' must be aligned to an element boundary".into()
759 );
760 }
761 let felt = read_element(expr.addr.addr).unwrap_or(Felt::ZERO);
762 write_with_format_type!(output, expr, felt.as_canonical_u64());
763 } else if matches!(
764 expr.ty,
765 Type::Array(ref array_ty) if array_ty.element_type() == &Type::Felt && array_ty.len() == 4
766 ) {
767 if !expr.addr.is_word_aligned() {
768 return Err("read failed: type 'word' must be aligned to a word boundary".into());
769 }
770 let word = memory
771 .read_word(
772 context,
773 Felt::new(expr.addr.addr as u64).expect("value exceeds field modulus"),
774 cycle,
775 )
776 .unwrap_or_default();
777 output.push('[');
778 for (i, elem) in word.iter().enumerate() {
779 if i > 0 {
780 output.push_str(", ");
781 }
782 write_with_format_type!(output, expr, elem.as_canonical_u64());
783 }
784 output.push(']');
785 } else {
786 if !expr.addr.is_element_aligned() {
787 return Err("invalid read: unaligned reads are not supported yet".into());
788 }
789
790 const U32_MASK: u64 = u32::MAX as u64;
791 let size = expr.ty.size_in_bytes();
792 let size_in_felts = expr.ty.size_in_felts();
793 let mut bytes = Vec::with_capacity(size);
794 let mut needed = size;
795 for i in 0..size_in_felts {
796 let addr = expr.addr.addr.checked_add(i as u32).ok_or_else(|| {
797 "invalid read: attempted to read beyond end of linear memory".to_string()
798 })?;
799 let elem = read_element(addr).unwrap_or_default();
800 let elem_bytes = ((elem.as_canonical_u64() & U32_MASK) as u32).to_le_bytes();
801 let take = core::cmp::min(needed, 4);
802 bytes.extend(&elem_bytes[..take]);
803 needed -= take;
804 }
805
806 match &expr.ty {
807 Type::I1 => match expr.format {
808 FormatType::Decimal => write!(&mut output, "{}", bytes[0] != 0).unwrap(),
809 FormatType::Hex => {
810 write!(&mut output, "{:#0x}", (bytes[0] != 0) as u8).unwrap()
811 }
812 FormatType::Binary => {
813 write!(&mut output, "{:#0b}", (bytes[0] != 0) as u8).unwrap()
814 }
815 },
816 Type::I8 => write_with_format_type!(output, expr, bytes[0] as i8),
817 Type::U8 => write_with_format_type!(output, expr, bytes[0]),
818 Type::I16 => {
819 write_with_format_type!(output, expr, i16::from_le_bytes([bytes[0], bytes[1]]))
820 }
821 Type::U16 => {
822 write_with_format_type!(output, expr, u16::from_le_bytes([bytes[0], bytes[1]]))
823 }
824 Type::I32 => write_with_format_type!(
825 output,
826 expr,
827 i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
828 ),
829 Type::U32 => write_with_format_type!(
830 output,
831 expr,
832 u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
833 ),
834 ty @ (Type::I64 | Type::U64) => {
835 let val = u64::from_le_bytes(bytes[..8].try_into().unwrap());
836 if matches!(ty, Type::I64) {
837 write_with_format_type!(output, expr, val as i64)
838 } else {
839 write_with_format_type!(output, expr, val)
840 }
841 }
842 ty => {
843 return Err(format!(
844 "support for reads of type '{ty}' are not implemented yet"
845 ));
846 }
847 }
848 }
849
850 Ok(output)
851 }
852
853 pub fn format_variables(&self, show_all: bool) -> String {
858 use core::fmt::Write;
859
860 let executor = self.executor();
861 let debug_vars = &executor.debug_vars;
862
863 let mut output = String::new();
864 let stack = executor.current_stack.clone();
865 let context = executor.current_context;
866
867 let read_mem = |addr: u32| -> Option<Felt> {
869 executor
870 .processor
871 .memory()
872 .read_element(context, Felt::new(addr as u64).expect("value exceeds field modulus"))
873 .ok()
874 };
875
876 let current_source = if show_all {
877 None
878 } else {
879 self.current_display_location()
880 };
881 let source_path_prefixes = self.source_path_prefixes();
882
883 if !debug_vars.has_variables() {
884 return "No debug variables tracked".to_string();
885 }
886
887 for var_snapshot in debug_vars.current_variables() {
888 let name = var_snapshot.info.name();
889
890 if !show_all && is_compiler_generated_name(name) {
891 continue;
892 }
893
894 if let (Some(current), Some(var_loc)) =
895 (current_source.as_ref(), var_snapshot.info.location())
896 && !source_var_location_is_visible(
897 var_loc.uri.as_str(),
898 var_loc.line.to_u32(),
899 current.source_file.uri().as_str(),
900 current.line,
901 &source_path_prefixes,
902 )
903 {
904 continue;
905 }
906
907 if !output.is_empty() {
908 output.push_str(", ");
909 }
910
911 let location = var_snapshot.info.value_location();
912
913 let value = resolve_variable_value(location, &stack, read_mem, |offset| {
914 let fmp_addr = miden_core::FMP_ADDR.as_canonical_u64() as u32;
916 let fmp = read_mem(fmp_addr)?;
917 let addr = (fmp.as_canonical_u64() as i64 + offset as i64) as u32;
918 read_mem(addr)
919 });
920
921 match value {
922 Some(felt) => {
923 write!(&mut output, "{name}={}", felt.as_canonical_u64()).unwrap();
924 }
925 None => {
926 write!(&mut output, "{name}={location}").unwrap();
927 }
928 }
929 }
930
931 if output.is_empty() {
932 "No source-level variables (use ':vars all' to show compiler locals)".to_string()
933 } else {
934 output
935 }
936 }
937}
938
939fn is_internal_procedure(proc: &str) -> bool {
940 proc.contains("::intrinsics::")
941}
942
943fn is_compiler_generated_name(name: &str) -> bool {
946 name.strip_prefix("local")
947 .is_some_and(|suffix| !suffix.is_empty() && suffix.chars().all(|c| c.is_ascii_digit()))
948}
949
950fn source_var_location_is_visible(
951 var_path: &str,
952 var_line: u32,
953 current_path: &str,
954 current_line: u32,
955 source_path_prefixes: &[String],
956) -> bool {
957 source_paths_match(var_path, current_path, source_path_prefixes) && var_line < current_line
958}
959
960fn normalize_source_path(path: &str) -> String {
961 let path = path.trim();
962 let path = path.strip_prefix("file://").unwrap_or(path);
963 let path = path.replace('\\', "/");
964
965 let is_absolute = path.starts_with('/');
966 let mut parts = Vec::new();
967 for part in path.split('/') {
968 match part {
969 "" | "." => {}
970 ".." => {
971 if parts.last().is_some_and(|last| *last != "..") {
972 parts.pop();
973 } else {
974 parts.push(part);
975 }
976 }
977 _ => parts.push(part),
978 }
979 }
980
981 let normalized = parts.join("/");
982 if is_absolute && !normalized.is_empty() {
983 format!("/{normalized}")
984 } else {
985 normalized
986 }
987}
988
989fn strip_source_prefix(path: &str, prefix: &str) -> Option<String> {
990 let path = path.trim_start_matches('/');
991 let prefix = prefix.trim_start_matches('/').trim_end_matches('/');
992 path.strip_prefix(prefix)
993 .and_then(|rest| rest.strip_prefix('/'))
994 .map(ToOwned::to_owned)
995}
996
997fn source_paths_match(left: &str, right: &str, trim_prefixes: &[String]) -> bool {
998 let left = normalize_source_path(left);
999 let right = normalize_source_path(right);
1000 if left.is_empty() || right.is_empty() {
1001 return false;
1002 }
1003
1004 if left == right {
1005 return true;
1006 }
1007
1008 for prefix in trim_prefixes {
1009 if strip_source_prefix(&left, prefix).is_some_and(|stripped| stripped == right) {
1010 return true;
1011 }
1012 if strip_source_prefix(&right, prefix).is_some_and(|stripped| stripped == left) {
1013 return true;
1014 }
1015 }
1016
1017 false
1018}
1019
1020fn source_path_candidates(uri: &str, source_path_prefixes: &[String]) -> Vec<PathBuf> {
1021 let normalized = normalize_source_path(uri);
1022 if normalized.is_empty() || Path::new(&normalized).is_absolute() {
1023 return Vec::new();
1024 }
1025
1026 source_path_prefixes
1027 .iter()
1028 .map(|prefix| Path::new(prefix).join(&normalized))
1029 .collect()
1030}
1031
1032#[cfg(feature = "dap")]
1036impl State {
1037 pub fn new_for_dap(addr: &str) -> Result<Self, Report> {
1042 let source_manager: Arc<dyn SourceManager> = Arc::new(DefaultSourceManager::default());
1043 let remote = RemoteState::connect(addr, &source_manager)?;
1044
1045 Ok(Self {
1046 source_manager,
1047 config: Box::default(),
1048 input_mode: InputMode::Normal,
1049 breakpoints: vec![],
1050 breakpoints_hit: vec![],
1051 next_breakpoint_id: 0,
1052 stopped: true,
1053 debug_mode: DebugMode::Remote,
1054 session: SessionState::Remote(Box::new(remote)),
1055 })
1056 }
1057
1058 pub fn step_remote(&mut self) -> Result<crate::exec::DapStopReason, Report> {
1059 let source_manager = self.source_manager.clone();
1060 let SessionState::Remote(remote) = &mut self.session else {
1061 return Err(Report::msg("no remote debug session"));
1062 };
1063 let result = remote.resume(&self.breakpoints).map_err(Report::msg)?;
1064
1065 self.breakpoints.retain(|bp| !bp.is_one_shot());
1066
1067 match &result {
1068 crate::exec::DapStopReason::Stopped(snapshot) => {
1069 remote.refresh_executor(&source_manager, snapshot);
1070 self.stopped = true;
1071 }
1072 crate::exec::DapStopReason::Terminated => {
1073 remote.executor.stopped = true;
1074 self.stopped = true;
1075 }
1076 crate::exec::DapStopReason::Restarting => {
1077 return Err(Report::msg("unexpected Phase 2 restart signal during step"));
1078 }
1079 }
1080
1081 Ok(result)
1082 }
1083}
1084
1085#[cfg(feature = "dap")]
1088fn convert_ui_state(
1089 snapshot: &crate::exec::DapUiState,
1090 source_manager: &Arc<dyn SourceManager>,
1091) -> RemoteSnapshot {
1092 use crate::debug::{CallFrame, CallStack};
1093
1094 let call_frames: Vec<CallFrame> = snapshot
1095 .callstack
1096 .iter()
1097 .map(|frame| {
1098 let resolved = resolve_remote_frame(frame, source_manager);
1099 CallFrame::from_remote(Some(frame.name.clone()), resolved)
1100 })
1101 .collect();
1102
1103 let current_stack = snapshot
1104 .current_stack
1105 .iter()
1106 .copied()
1107 .map(|v| Felt::new(v).expect("value exceeds field modulus"))
1108 .collect();
1109
1110 RemoteSnapshot {
1111 callstack: CallStack::from_remote_frames(call_frames),
1112 current_stack,
1113 cycle: snapshot.cycle,
1114 }
1115}
1116
1117#[cfg(feature = "dap")]
1119fn resolve_remote_frame(
1120 frame: &crate::exec::DapUiFrame,
1121 source_manager: &Arc<dyn SourceManager>,
1122) -> Option<crate::debug::ResolvedLocation> {
1123 use std::path::Path;
1124
1125 use miden_debug_types::{SourceManagerExt, SourceSpan, Uri};
1126
1127 let path_str = frame.source_path.as_ref()?;
1128 let path = crate::debug::resolve_source_path(&Uri::new(path_str))
1129 .unwrap_or_else(|| Path::new(path_str).to_path_buf());
1130 let source_file = source_manager.load_file(&path).ok()?;
1131 let line = frame.line.max(1) as u32;
1132 let col = frame.column.max(1) as u32;
1133
1134 let content = source_file.content();
1136 let line_index = miden_debug_types::LineIndex::from(line.saturating_sub(1));
1137 let range = content.line_range(line_index)?;
1138 let span = SourceSpan::new(source_file.id(), range);
1139
1140 Some(crate::debug::ResolvedLocation {
1141 source_file,
1142 line,
1143 col,
1144 span,
1145 })
1146}
1147
1148fn load_sysroot_libs(
1157 toolchain_dir: &std::path::Path,
1158) -> Result<Vec<Arc<miden_assembly_syntax::Library>>, Report> {
1159 let mut libs = Vec::new();
1160
1161 let entries = match std::fs::read_dir(toolchain_dir) {
1162 Ok(entries) => entries,
1163 Err(_) => {
1164 log::debug!(target: "state", "could not read sysroot directory: {}", toolchain_dir.display());
1165 return Ok(libs);
1166 }
1167 };
1168
1169 for entry in entries {
1170 let entry = entry.into_diagnostic()?;
1171 let path = entry.path();
1172 let Some(ext) = path.extension() else {
1173 continue;
1174 };
1175
1176 if ext == "masp" {
1177 log::debug!(target: "state", "loading library from sysroot: {}", path.display());
1178 let bytes = std::fs::read(&path).into_diagnostic()?;
1179 let package = miden_mast_package::Package::read_from_bytes(&bytes).map_err(|e| {
1180 Report::msg(format!("failed to load package '{}': {e}", path.display()))
1181 })?;
1182 libs.push(package.mast.clone());
1183 } else if ext == "masl" {
1184 log::debug!(target: "state", "loading library from sysroot: {}", path.display());
1185 let bytes = std::fs::read(&path).into_diagnostic()?;
1186 let lib = miden_assembly_syntax::Library::read_from_bytes(&bytes).map_err(|e| {
1187 Report::msg(format!("failed to load library '{}': {e}", path.display()))
1188 })?;
1189 libs.push(Arc::new(lib));
1190 }
1191 }
1192
1193 if libs.is_empty() {
1194 log::debug!(target: "state", "no libraries found in sysroot: {}", toolchain_dir.display());
1195 }
1196
1197 Ok(libs)
1198}
1199
1200fn load_package(config: &DebuggerConfig) -> Result<Arc<miden_mast_package::Package>, Report> {
1201 let input = config.input.as_ref().ok_or_else(|| Report::msg("no input file specified"))?;
1202 let package = match input {
1203 InputFile::Real(path) => {
1204 let bytes = std::fs::read(path).into_diagnostic()?;
1205 miden_mast_package::Package::read_from_bytes(&bytes)
1206 .map(Arc::new)
1207 .map_err(|e| {
1208 Report::msg(format!(
1209 "failed to load Miden package from {}: {e}",
1210 path.display()
1211 ))
1212 })?
1213 }
1214 InputFile::Stdin(bytes) => miden_mast_package::Package::read_from_bytes(bytes)
1215 .map(Arc::new)
1216 .map_err(|e| Report::msg(format!("failed to load Miden package from stdin: {e}")))?,
1217 };
1218
1219 if let Some(entry) = config.entrypoint.as_ref() {
1220 let id = entry
1222 .parse::<miden_assembly::ast::QualifiedProcedureName>()
1223 .map_err(|_| Report::msg(format!("invalid function identifier: '{entry}'")))?;
1224 if !package.is_library() {
1225 return Err(Report::msg("cannot use --entrypoint with executable packages"));
1226 }
1227
1228 package.make_executable(&id).map(Arc::new)
1229 } else {
1230 Ok(package)
1231 }
1232}