Skip to main content

miden_debug/ui/
state.rs

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/// Whether the debugger is debugging a plain program or a transaction.
31#[derive(Debug, Copy, Clone, PartialEq, Eq)]
32pub enum DebugMode {
33    /// Debugging a plain MASM program loaded from a package.
34    Program,
35    /// Debugging a Miden transaction with pre-recorded event replay.
36    Transaction,
37    /// Debugging remotely via a DAP server connection.
38    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    /// Tracks which source files have had breakpoints synced to the DAP server,
97    /// so we can send empty breakpoint lists when all breakpoints for a file are removed.
98    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        // Group Line breakpoints by their file pattern string.
165        let mut by_file: BTreeMap<String, Vec<i64>> = BTreeMap::new();
166        // Collect Called and File patterns as function breakpoints.
167        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        // Send empty breakpoint lists for files that were previously synced but no longer have
182        // breakpoints.
183        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        // Send breakpoints for each file.
194        for (file, lines) in &by_file {
195            let _ = self.client.set_breakpoints(file, lines);
196        }
197
198        // Send function/pattern breakpoints (replaces the full set each time).
199        let _ = self.client.set_function_breakpoints(&func_names);
200
201        // Update tracked set.
202        self.synced_bp_files = by_file.into_keys().collect();
203    }
204
205    fn resume(&mut self, breakpoints: &[Breakpoint]) -> Result<crate::exec::DapStopReason, String> {
206        // Sync user-defined breakpoints to the DAP server before choosing a step command.
207        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        // Standard DAP `stopped` events tell us execution paused, but do not
232        // carry the refreshed VM state (stack, callstack, cycle). The server
233        // pushes a custom `miden/uiState` event with the bundled snapshot
234        // immediately before each `stopped` event, so we consume that here
235        // instead of issuing an extra evaluate round-trip.
236        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            // CLI args model sequential pushes, but StackInputs expects the top element first.
282            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        // Load libraries from link_libraries and sysroot BEFORE resolving dependencies
289        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        // Load std and base libraries from sysroot if available
297        if let Some(toolchain_dir) = config.toolchain_dir() {
298            libs.extend(load_sysroot_libs(&toolchain_dir)?);
299        }
300
301        // Create executor and register libraries with dependency resolver before resolving
302        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        // Now resolve package dependencies (they should find the registered libraries)
309        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    /// Create a new debugger state for transaction debugging.
328    ///
329    /// This uses pre-recorded event mutations to replay host events during
330    /// step-by-step debugging, since the debugger's host doesn't have access
331    /// to the real transaction host.
332    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        // Create debug executor with event replay
343        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                        // Fallback: server treated it as Phase 1.
381                        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            // CLI args model sequential pushes, but StackInputs expects the top element first.
401            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        // Load libraries from link_libraries and sysroot BEFORE resolving dependencies
407        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        // Load std and base libraries from sysroot if available
414        if let Some(toolchain_dir) = self.config.toolchain_dir() {
415            libs.extend(load_sysroot_libs(&toolchain_dir)?);
416        }
417
418        // Create executor and register libraries with dependency resolver before resolving
419        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        // Now resolve package dependencies
426        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    /// Format the current debug variables as a string for display.
834    ///
835    /// When `show_all` is false, compiler-generated locals (named `local0`, `local1`, etc.)
836    /// are hidden. Use `show_all` = true (`:vars all`) to include them.
837    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        // Use live processor state, not the pre-recorded trace, for current-cycle values.
848        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                // Read FMP from live memory, then compute address as FMP + offset
895                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
919/// Returns true if the variable name looks compiler-generated (e.g. "local0", "local12").
920/// Source-level variables have DWARF-derived names like "a", "sum", "_info".
921fn 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// DAP CLIENT MODE
1009// ================================================================================================
1010
1011#[cfg(feature = "dap")]
1012impl State {
1013    /// Create a new debugger state for remote DAP debugging.
1014    ///
1015    /// Connects to a DAP server, performs the handshake, and queries the
1016    /// initial state to populate the executor fields that the TUI panes read.
1017    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/// Convert a server-pushed [`DapUiState`](crate::exec::DapUiState) snapshot into a
1062/// [`RemoteSnapshot`] that the TUI executor can consume.
1063#[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/// Resolve a remote frame to a [ResolvedLocation] by loading the source file from disk.
1094#[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    // Compute a span from the line number — use the byte range of the line
1110    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
1123/// Attempts to load the standard library from the sysroot/toolchain directory.
1124///
1125/// Supports both formats:
1126/// - `.masp` (package format) - used by the midenup toolchain
1127/// - `.masl` (serialized Library) - legacy format
1128///   Load all library files (.masp and .masl) from the sysroot directory.
1129///
1130/// The toolchain determines what libraries are available in the sysroot.
1131fn 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        // Input must be a library, not a program
1196        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}