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 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    /// Format the current debug variables as a string for display.
854    ///
855    /// When `show_all` is false, compiler-generated locals (named `local0`, `local1`, etc.)
856    /// are hidden. Use `show_all` = true (`:vars all`) to include them.
857    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        // Use live processor state, not the pre-recorded trace, for current-cycle values.
868        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                // Read FMP from live memory, then compute address as FMP + offset
915                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
943/// Returns true if the variable name looks compiler-generated (e.g. "local0", "local12").
944/// Source-level variables have DWARF-derived names like "a", "sum", "_info".
945fn 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// DAP CLIENT MODE
1033// ================================================================================================
1034
1035#[cfg(feature = "dap")]
1036impl State {
1037    /// Create a new debugger state for remote DAP debugging.
1038    ///
1039    /// Connects to a DAP server, performs the handshake, and queries the
1040    /// initial state to populate the executor fields that the TUI panes read.
1041    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/// Convert a server-pushed [`DapUiState`](crate::exec::DapUiState) snapshot into a
1086/// [`RemoteSnapshot`] that the TUI executor can consume.
1087#[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/// Resolve a remote frame to a [ResolvedLocation] by loading the source file from disk.
1118#[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    // Compute a span from the line number — use the byte range of the line
1135    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
1148/// Attempts to load the standard library from the sysroot/toolchain directory.
1149///
1150/// Supports both formats:
1151/// - `.masp` (package format) - used by the midenup toolchain
1152/// - `.masl` (serialized Library) - legacy format
1153///   Load all library files (.masp and .masl) from the sysroot directory.
1154///
1155/// The toolchain determines what libraries are available in the sysroot.
1156fn 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        // Input must be a library, not a program
1221        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}