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 execution_failed(&self) -> Option<&miden_processor::ExecutionError> {
674 match &self.session {
675 SessionState::Local(local) => local.execution_failed.as_ref(),
676 #[cfg(feature = "dap")]
677 SessionState::Remote(_) => None,
678 }
679 }
680
681 pub fn set_execution_failed(&mut self, error: miden_processor::ExecutionError) {
682 match &mut self.session {
683 SessionState::Local(local) => local.execution_failed = Some(error),
684 #[cfg(feature = "dap")]
685 SessionState::Remote(_) => {
686 panic!("cannot record local execution failure while in remote mode")
687 }
688 }
689 }
690}
691
692macro_rules! write_with_format_type {
693 ($out:ident, $read_expr:ident, $value:expr) => {
694 match $read_expr.format {
695 crate::debug::FormatType::Decimal => write!(&mut $out, "{}", $value).unwrap(),
696 crate::debug::FormatType::Hex => write!(&mut $out, "{:0x}", $value).unwrap(),
697 crate::debug::FormatType::Binary => write!(&mut $out, "{:0b}", $value).unwrap(),
698 }
699 };
700}
701
702impl State {
703 pub fn read_memory(&mut self, expr: &ReadMemoryExpr) -> Result<String, String> {
704 use core::fmt::Write;
705
706 use miden_assembly_syntax::ast::types::Type;
707
708 use crate::debug::FormatType;
709
710 #[cfg(feature = "dap")]
711 if self.debug_mode == DebugMode::Remote {
712 let SessionState::Remote(remote) = &mut self.session else {
713 return Err("no remote debug session".into());
714 };
715 return remote.read_memory(expr);
716 }
717
718 #[cfg(not(feature = "dap"))]
719 if self.debug_mode == DebugMode::Remote {
720 return Err("remote debug mode requires the `dap` feature".into());
721 }
722
723 let executor = self.executor();
724 let cycle = miden_processor::trace::RowIndex::from(executor.cycle);
725 let context = executor.current_context;
726 let memory = executor.processor.memory();
727 let read_element = |addr: u32| -> Option<Felt> {
728 memory
729 .read_element(context, Felt::new(addr as u64).expect("value exceeds field modulus"))
730 .ok()
731 };
732 let mut output = String::new();
733 if expr.count > 1 {
734 return Err("-count with value > 1 is not yet implemented".into());
735 } else if matches!(expr.ty, Type::Felt) {
736 if !expr.addr.is_element_aligned() {
737 return Err(
738 "read failed: type 'felt' must be aligned to an element boundary".into()
739 );
740 }
741 let felt = read_element(expr.addr.addr).unwrap_or(Felt::ZERO);
742 write_with_format_type!(output, expr, felt.as_canonical_u64());
743 } else if matches!(
744 expr.ty,
745 Type::Array(ref array_ty) if array_ty.element_type() == &Type::Felt && array_ty.len() == 4
746 ) {
747 if !expr.addr.is_word_aligned() {
748 return Err("read failed: type 'word' must be aligned to a word boundary".into());
749 }
750 let word = memory
751 .read_word(
752 context,
753 Felt::new(expr.addr.addr as u64).expect("value exceeds field modulus"),
754 cycle,
755 )
756 .unwrap_or_default();
757 output.push('[');
758 for (i, elem) in word.iter().enumerate() {
759 if i > 0 {
760 output.push_str(", ");
761 }
762 write_with_format_type!(output, expr, elem.as_canonical_u64());
763 }
764 output.push(']');
765 } else {
766 if !expr.addr.is_element_aligned() {
767 return Err("invalid read: unaligned reads are not supported yet".into());
768 }
769
770 const U32_MASK: u64 = u32::MAX as u64;
771 let size = expr.ty.size_in_bytes();
772 let size_in_felts = expr.ty.size_in_felts();
773 let mut bytes = Vec::with_capacity(size);
774 let mut needed = size;
775 for i in 0..size_in_felts {
776 let addr = expr.addr.addr.checked_add(i as u32).ok_or_else(|| {
777 "invalid read: attempted to read beyond end of linear memory".to_string()
778 })?;
779 let elem = read_element(addr).unwrap_or_default();
780 let elem_bytes = ((elem.as_canonical_u64() & U32_MASK) as u32).to_le_bytes();
781 let take = core::cmp::min(needed, 4);
782 bytes.extend(&elem_bytes[..take]);
783 needed -= take;
784 }
785
786 match &expr.ty {
787 Type::I1 => match expr.format {
788 FormatType::Decimal => write!(&mut output, "{}", bytes[0] != 0).unwrap(),
789 FormatType::Hex => {
790 write!(&mut output, "{:#0x}", (bytes[0] != 0) as u8).unwrap()
791 }
792 FormatType::Binary => {
793 write!(&mut output, "{:#0b}", (bytes[0] != 0) as u8).unwrap()
794 }
795 },
796 Type::I8 => write_with_format_type!(output, expr, bytes[0] as i8),
797 Type::U8 => write_with_format_type!(output, expr, bytes[0]),
798 Type::I16 => {
799 write_with_format_type!(output, expr, i16::from_le_bytes([bytes[0], bytes[1]]))
800 }
801 Type::U16 => {
802 write_with_format_type!(output, expr, u16::from_le_bytes([bytes[0], bytes[1]]))
803 }
804 Type::I32 => write_with_format_type!(
805 output,
806 expr,
807 i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
808 ),
809 Type::U32 => write_with_format_type!(
810 output,
811 expr,
812 u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
813 ),
814 ty @ (Type::I64 | Type::U64) => {
815 let val = u64::from_le_bytes(bytes[..8].try_into().unwrap());
816 if matches!(ty, Type::I64) {
817 write_with_format_type!(output, expr, val as i64)
818 } else {
819 write_with_format_type!(output, expr, val)
820 }
821 }
822 ty => {
823 return Err(format!(
824 "support for reads of type '{ty}' are not implemented yet"
825 ));
826 }
827 }
828 }
829
830 Ok(output)
831 }
832
833 pub fn format_variables(&self, show_all: bool) -> String {
838 use core::fmt::Write;
839
840 let executor = self.executor();
841 let debug_vars = &executor.debug_vars;
842
843 let mut output = String::new();
844 let stack = executor.current_stack.clone();
845 let context = executor.current_context;
846
847 let read_mem = |addr: u32| -> Option<Felt> {
849 executor
850 .processor
851 .memory()
852 .read_element(context, Felt::new(addr as u64).expect("value exceeds field modulus"))
853 .ok()
854 };
855
856 let current_source = if show_all {
857 None
858 } else {
859 self.current_display_location()
860 };
861 let source_path_prefixes = self.source_path_prefixes();
862
863 if !debug_vars.has_variables() {
864 return "No debug variables tracked".to_string();
865 }
866
867 for var_snapshot in debug_vars.current_variables() {
868 let name = var_snapshot.info.name();
869
870 if !show_all && is_compiler_generated_name(name) {
871 continue;
872 }
873
874 if let (Some(current), Some(var_loc)) =
875 (current_source.as_ref(), var_snapshot.info.location())
876 && !source_var_location_is_visible(
877 var_loc.uri.as_str(),
878 var_loc.line.to_u32(),
879 current.source_file.uri().as_str(),
880 current.line,
881 &source_path_prefixes,
882 )
883 {
884 continue;
885 }
886
887 if !output.is_empty() {
888 output.push_str(", ");
889 }
890
891 let location = var_snapshot.info.value_location();
892
893 let value = resolve_variable_value(location, &stack, read_mem, |offset| {
894 let fmp_addr = miden_core::FMP_ADDR.as_canonical_u64() as u32;
896 let fmp = read_mem(fmp_addr)?;
897 let addr = (fmp.as_canonical_u64() as i64 + offset as i64) as u32;
898 read_mem(addr)
899 });
900
901 match value {
902 Some(felt) => {
903 write!(&mut output, "{name}={}", felt.as_canonical_u64()).unwrap();
904 }
905 None => {
906 write!(&mut output, "{name}={location}").unwrap();
907 }
908 }
909 }
910
911 if output.is_empty() {
912 "No source-level variables (use ':vars all' to show compiler locals)".to_string()
913 } else {
914 output
915 }
916 }
917}
918
919fn is_compiler_generated_name(name: &str) -> bool {
922 name.strip_prefix("local")
923 .is_some_and(|suffix| !suffix.is_empty() && suffix.chars().all(|c| c.is_ascii_digit()))
924}
925
926fn source_var_location_is_visible(
927 var_path: &str,
928 var_line: u32,
929 current_path: &str,
930 current_line: u32,
931 source_path_prefixes: &[String],
932) -> bool {
933 source_paths_match(var_path, current_path, source_path_prefixes) && var_line < current_line
934}
935
936fn normalize_source_path(path: &str) -> String {
937 let path = path.trim();
938 let path = path.strip_prefix("file://").unwrap_or(path);
939 let path = path.replace('\\', "/");
940
941 let is_absolute = path.starts_with('/');
942 let mut parts = Vec::new();
943 for part in path.split('/') {
944 match part {
945 "" | "." => {}
946 ".." => {
947 if parts.last().is_some_and(|last| *last != "..") {
948 parts.pop();
949 } else {
950 parts.push(part);
951 }
952 }
953 _ => parts.push(part),
954 }
955 }
956
957 let normalized = parts.join("/");
958 if is_absolute && !normalized.is_empty() {
959 format!("/{normalized}")
960 } else {
961 normalized
962 }
963}
964
965fn strip_source_prefix(path: &str, prefix: &str) -> Option<String> {
966 let path = path.trim_start_matches('/');
967 let prefix = prefix.trim_start_matches('/').trim_end_matches('/');
968 path.strip_prefix(prefix)
969 .and_then(|rest| rest.strip_prefix('/'))
970 .map(ToOwned::to_owned)
971}
972
973fn source_paths_match(left: &str, right: &str, trim_prefixes: &[String]) -> bool {
974 let left = normalize_source_path(left);
975 let right = normalize_source_path(right);
976 if left.is_empty() || right.is_empty() {
977 return false;
978 }
979
980 if left == right {
981 return true;
982 }
983
984 for prefix in trim_prefixes {
985 if strip_source_prefix(&left, prefix).is_some_and(|stripped| stripped == right) {
986 return true;
987 }
988 if strip_source_prefix(&right, prefix).is_some_and(|stripped| stripped == left) {
989 return true;
990 }
991 }
992
993 false
994}
995
996fn source_path_candidates(uri: &str, source_path_prefixes: &[String]) -> Vec<PathBuf> {
997 let normalized = normalize_source_path(uri);
998 if normalized.is_empty() || Path::new(&normalized).is_absolute() {
999 return Vec::new();
1000 }
1001
1002 source_path_prefixes
1003 .iter()
1004 .map(|prefix| Path::new(prefix).join(&normalized))
1005 .collect()
1006}
1007
1008#[cfg(feature = "dap")]
1012impl State {
1013 pub fn new_for_dap(addr: &str) -> Result<Self, Report> {
1018 let source_manager: Arc<dyn SourceManager> = Arc::new(DefaultSourceManager::default());
1019 let remote = RemoteState::connect(addr, &source_manager)?;
1020
1021 Ok(Self {
1022 source_manager,
1023 config: Box::default(),
1024 input_mode: InputMode::Normal,
1025 breakpoints: vec![],
1026 breakpoints_hit: vec![],
1027 next_breakpoint_id: 0,
1028 stopped: true,
1029 debug_mode: DebugMode::Remote,
1030 session: SessionState::Remote(Box::new(remote)),
1031 })
1032 }
1033
1034 pub fn step_remote(&mut self) -> Result<crate::exec::DapStopReason, Report> {
1035 let source_manager = self.source_manager.clone();
1036 let SessionState::Remote(remote) = &mut self.session else {
1037 return Err(Report::msg("no remote debug session"));
1038 };
1039 let result = remote.resume(&self.breakpoints).map_err(Report::msg)?;
1040
1041 self.breakpoints.retain(|bp| !bp.is_one_shot());
1042
1043 match &result {
1044 crate::exec::DapStopReason::Stopped(snapshot) => {
1045 remote.refresh_executor(&source_manager, snapshot);
1046 self.stopped = true;
1047 }
1048 crate::exec::DapStopReason::Terminated => {
1049 remote.executor.stopped = true;
1050 self.stopped = true;
1051 }
1052 crate::exec::DapStopReason::Restarting => {
1053 return Err(Report::msg("unexpected Phase 2 restart signal during step"));
1054 }
1055 }
1056
1057 Ok(result)
1058 }
1059}
1060
1061#[cfg(feature = "dap")]
1064fn convert_ui_state(
1065 snapshot: &crate::exec::DapUiState,
1066 source_manager: &Arc<dyn SourceManager>,
1067) -> RemoteSnapshot {
1068 use crate::debug::{CallFrame, CallStack};
1069
1070 let call_frames: Vec<CallFrame> = snapshot
1071 .callstack
1072 .iter()
1073 .map(|frame| {
1074 let resolved = resolve_remote_frame(frame, source_manager);
1075 CallFrame::from_remote(Some(frame.name.clone()), resolved)
1076 })
1077 .collect();
1078
1079 let current_stack = snapshot
1080 .current_stack
1081 .iter()
1082 .copied()
1083 .map(|v| Felt::new(v).expect("value exceeds field modulus"))
1084 .collect();
1085
1086 RemoteSnapshot {
1087 callstack: CallStack::from_remote_frames(call_frames),
1088 current_stack,
1089 cycle: snapshot.cycle,
1090 }
1091}
1092
1093#[cfg(feature = "dap")]
1095fn resolve_remote_frame(
1096 frame: &crate::exec::DapUiFrame,
1097 source_manager: &Arc<dyn SourceManager>,
1098) -> Option<crate::debug::ResolvedLocation> {
1099 use std::path::Path;
1100
1101 use miden_debug_types::{SourceManagerExt, SourceSpan};
1102
1103 let path_str = frame.source_path.as_ref()?;
1104 let path = Path::new(path_str);
1105 let source_file = source_manager.load_file(path).ok()?;
1106 let line = frame.line.max(1) as u32;
1107 let col = frame.column.max(1) as u32;
1108
1109 let content = source_file.content();
1111 let line_index = miden_debug_types::LineIndex::from(line.saturating_sub(1));
1112 let range = content.line_range(line_index)?;
1113 let span = SourceSpan::new(source_file.id(), range);
1114
1115 Some(crate::debug::ResolvedLocation {
1116 source_file,
1117 line,
1118 col,
1119 span,
1120 })
1121}
1122
1123fn load_sysroot_libs(
1132 toolchain_dir: &std::path::Path,
1133) -> Result<Vec<Arc<miden_assembly_syntax::Library>>, Report> {
1134 let mut libs = Vec::new();
1135
1136 let entries = match std::fs::read_dir(toolchain_dir) {
1137 Ok(entries) => entries,
1138 Err(_) => {
1139 log::debug!(target: "state", "could not read sysroot directory: {}", toolchain_dir.display());
1140 return Ok(libs);
1141 }
1142 };
1143
1144 for entry in entries {
1145 let entry = entry.into_diagnostic()?;
1146 let path = entry.path();
1147 let Some(ext) = path.extension() else {
1148 continue;
1149 };
1150
1151 if ext == "masp" {
1152 log::debug!(target: "state", "loading library from sysroot: {}", path.display());
1153 let bytes = std::fs::read(&path).into_diagnostic()?;
1154 let package = miden_mast_package::Package::read_from_bytes(&bytes).map_err(|e| {
1155 Report::msg(format!("failed to load package '{}': {e}", path.display()))
1156 })?;
1157 libs.push(package.mast.clone());
1158 } else if ext == "masl" {
1159 log::debug!(target: "state", "loading library from sysroot: {}", path.display());
1160 let bytes = std::fs::read(&path).into_diagnostic()?;
1161 let lib = miden_assembly_syntax::Library::read_from_bytes(&bytes).map_err(|e| {
1162 Report::msg(format!("failed to load library '{}': {e}", path.display()))
1163 })?;
1164 libs.push(Arc::new(lib));
1165 }
1166 }
1167
1168 if libs.is_empty() {
1169 log::debug!(target: "state", "no libraries found in sysroot: {}", toolchain_dir.display());
1170 }
1171
1172 Ok(libs)
1173}
1174
1175fn load_package(config: &DebuggerConfig) -> Result<Arc<miden_mast_package::Package>, Report> {
1176 let input = config.input.as_ref().ok_or_else(|| Report::msg("no input file specified"))?;
1177 let package = match input {
1178 InputFile::Real(path) => {
1179 let bytes = std::fs::read(path).into_diagnostic()?;
1180 miden_mast_package::Package::read_from_bytes(&bytes)
1181 .map(Arc::new)
1182 .map_err(|e| {
1183 Report::msg(format!(
1184 "failed to load Miden package from {}: {e}",
1185 path.display()
1186 ))
1187 })?
1188 }
1189 InputFile::Stdin(bytes) => miden_mast_package::Package::read_from_bytes(bytes)
1190 .map(Arc::new)
1191 .map_err(|e| Report::msg(format!("failed to load Miden package from stdin: {e}")))?,
1192 };
1193
1194 if let Some(entry) = config.entrypoint.as_ref() {
1195 let id = entry
1197 .parse::<miden_assembly::ast::QualifiedProcedureName>()
1198 .map_err(|_| Report::msg(format!("invalid function identifier: '{entry}'")))?;
1199 if !package.is_library() {
1200 return Err(Report::msg("cannot use --entrypoint with executable packages"));
1201 }
1202
1203 package.make_executable(&id).map(Arc::new)
1204 } else {
1205 Ok(package)
1206 }
1207}