Skip to main content

miden_debug/ui/
state.rs

1use std::{
2    borrow::ToOwned,
3    boxed::Box,
4    collections::{BTreeSet, VecDeque},
5    path::{Path, PathBuf},
6    string::{String, ToString},
7    sync::Arc,
8    vec::Vec,
9};
10
11use miden_assembly::{DefaultSourceManager, SourceManager};
12use miden_assembly_syntax::diagnostics::Report;
13use miden_debug_engine::{DebugQuery, normalize_source_path};
14use miden_debug_types::{Location, SourceManagerExt, SourceSpan};
15use miden_mast_package::Package;
16use miden_processor::{
17    Felt, LoadedMastForest, StackInputs,
18    advice::{AdviceInputs, AdviceMutation},
19};
20
21use crate::{
22    config::DebuggerConfig,
23    debug::{
24        Breakpoint, BreakpointType, OperationMatcher, ReadMemoryExpr, ResolvedLocation,
25        TypedProcedure, format_value, resolve_typed_variable_values, resolve_variable_value,
26    },
27    exec::{DebugExecutor, ExecutionConfig, Executor},
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_event_replay_queue(event_replay: &[Vec<AdviceMutation>]) -> VecDeque<Vec<AdviceMutation>> {
42    event_replay
43        .iter()
44        .map(|batch| crate::exec::clone_advice_mutations(batch))
45        .collect()
46}
47
48pub struct State {
49    pub source_manager: Arc<dyn SourceManager>,
50    pub config: Box<DebuggerConfig>,
51    pub input_mode: InputMode,
52    pub breakpoints: Vec<Breakpoint>,
53    pub breakpoints_hit: Vec<Breakpoint>,
54    pub next_breakpoint_id: u8,
55    pub stopped: bool,
56    pub debug_mode: DebugMode,
57    selected_stack_frame: usize,
58    session: SessionState,
59}
60
61#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
62pub enum InputMode {
63    #[default]
64    Normal,
65    #[allow(dead_code)]
66    Insert,
67    Command,
68}
69
70/// Source location attached to a source-level debug variable declaration.
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct DebugVariableSource {
73    pub path: String,
74    pub line: u32,
75    pub column: u32,
76}
77
78/// Structured view of a variable visible to debugger frontends.
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct DebugVariableValue {
81    pub name: String,
82    pub value: Option<Felt>,
83    pub display_value: Option<String>,
84    pub location: String,
85    pub source: Option<DebugVariableSource>,
86}
87
88struct LocalState {
89    executor: DebugExecutor,
90    execution_failed: Option<miden_processor::ExecutionError>,
91    typed_procedure: Option<TypedProcedure>,
92}
93
94#[cfg(feature = "dap")]
95struct RemoteState {
96    client: crate::exec::DapClient,
97    executor: DebugExecutor,
98    addr: String,
99    /// Tracks which source files have had breakpoints synced to the DAP server,
100    /// so we can send empty breakpoint lists when all breakpoints for a file are removed.
101    synced_bp_files: std::collections::BTreeSet<String>,
102}
103
104enum SessionState {
105    Local(Box<LocalState>),
106    #[cfg(feature = "dap")]
107    Remote(Box<RemoteState>),
108}
109
110#[cfg(feature = "dap")]
111struct RemoteSnapshot {
112    callstack: crate::debug::CallStack,
113    current_stack: Vec<Felt>,
114    cycle: usize,
115}
116
117#[cfg(feature = "dap")]
118impl RemoteState {
119    fn from_client(
120        addr: &str,
121        mut client: crate::exec::DapClient,
122        source_manager: &Arc<dyn SourceManager>,
123    ) -> Result<Self, Report> {
124        use std::{cell::RefCell, collections::BTreeSet, rc::Rc};
125
126        use miden_debug_engine::{debug::DebugVarTracker, profiling::Profiler};
127        use miden_processor::{ContextId, FastProcessor};
128
129        use crate::exec::DebuggerHost;
130
131        let ui_state = client.handshake().map_err(Report::msg)?;
132        let snapshot = convert_ui_state(&ui_state, source_manager);
133
134        let debug_vars = DebugVarTracker::new(Rc::new(RefCell::new(Default::default())));
135        let executor = DebugExecutor {
136            processor: FastProcessor::new(StackInputs::default()),
137            host: DebuggerHost::new(source_manager.clone()),
138            resume_ctx: None,
139            current_stack: snapshot.current_stack,
140            current_op: None,
141            current_asmop: None,
142            stack_outputs: Default::default(),
143            contexts: BTreeSet::new(),
144            root_context: ContextId::root(),
145            current_context: ContextId::root(),
146            callstack: snapshot.callstack,
147            current_proc: None,
148            debug_vars,
149            last_debug_var_count: 0,
150            recent: VecDeque::new(),
151            cycle: snapshot.cycle,
152            stopped: false,
153            profiler: Profiler::default(),
154        };
155
156        Ok(Self {
157            client,
158            executor,
159            addr: addr.to_string(),
160            synced_bp_files: std::collections::BTreeSet::new(),
161        })
162    }
163
164    fn read_memory(&mut self, expr: &ReadMemoryExpr) -> Result<String, String> {
165        self.client.read_memory(expr)
166    }
167
168    fn sync_breakpoints(&mut self, breakpoints: &[Breakpoint]) {
169        use std::collections::BTreeMap;
170
171        // Group Line breakpoints by their file pattern string.
172        let mut by_file: BTreeMap<String, Vec<i64>> = BTreeMap::new();
173        // Collect Called and File patterns as function breakpoints.
174        let mut func_names: Vec<String> = Vec::new();
175
176        for bp in breakpoints {
177            match &bp.ty {
178                BreakpointType::Line { pattern, line } => {
179                    by_file.entry(pattern.glob().to_string()).or_default().push(*line as i64);
180                }
181                BreakpointType::Called(pattern) | BreakpointType::File(pattern) => {
182                    func_names.push(pattern.glob().to_string());
183                }
184                _ => {}
185            }
186        }
187
188        // Send empty breakpoint lists for files that were previously synced but no longer have
189        // breakpoints.
190        let stale_files: Vec<String> = self
191            .synced_bp_files
192            .iter()
193            .filter(|f| !by_file.contains_key(f.as_str()))
194            .cloned()
195            .collect();
196        for file in &stale_files {
197            let _ = self.client.set_breakpoints(file, &[]);
198        }
199
200        // Send breakpoints for each file.
201        for (file, lines) in &by_file {
202            let _ = self.client.set_breakpoints(file, lines);
203        }
204
205        // Send function/pattern breakpoints (replaces the full set each time).
206        let _ = self.client.set_function_breakpoints(&func_names);
207
208        // Update tracked set.
209        self.synced_bp_files = by_file.into_keys().collect();
210    }
211
212    fn resume(&mut self, breakpoints: &[Breakpoint]) -> Result<crate::exec::DapStopReason, String> {
213        // Sync user-defined breakpoints to the DAP server before choosing a step command.
214        self.sync_breakpoints(breakpoints);
215
216        let has_step = breakpoints.iter().any(|bp| matches!(bp.ty, BreakpointType::Step));
217        let has_next = breakpoints
218            .iter()
219            .any(|bp| matches!(bp.ty, BreakpointType::Next | BreakpointType::NextLine));
220        let has_finish = breakpoints.iter().any(|bp| matches!(bp.ty, BreakpointType::Finish));
221
222        if has_step {
223            self.client.step_in()
224        } else if has_next {
225            self.client.step_over()
226        } else if has_finish {
227            self.client.step_out()
228        } else {
229            self.client.continue_()
230        }
231    }
232
233    fn refresh_executor(
234        &mut self,
235        source_manager: &Arc<dyn SourceManager>,
236        pushed: &crate::exec::DapUiState,
237    ) {
238        // Standard DAP `stopped` events tell us execution paused, but do not
239        // carry the refreshed VM state (stack, callstack, cycle). The server
240        // pushes a custom `miden/uiState` event with the bundled snapshot
241        // immediately before each `stopped` event, so we consume that here
242        // instead of issuing an extra evaluate round-trip.
243        let snapshot = convert_ui_state(pushed, source_manager);
244        self.executor.current_stack = snapshot.current_stack;
245        self.executor.callstack = snapshot.callstack;
246        self.executor.cycle = snapshot.cycle;
247    }
248
249    fn reconnect(&mut self, source_manager: &Arc<dyn SourceManager>) -> Result<(), Report> {
250        let timeout = std::time::Duration::from_secs(30);
251        let mut new_client =
252            crate::exec::DapClient::connect_with_retry(&self.addr, timeout).map_err(Report::msg)?;
253        let ui_state = new_client.handshake().map_err(Report::msg)?;
254        let snapshot = convert_ui_state(&ui_state, source_manager);
255
256        self.client = new_client;
257        self.executor.current_stack = snapshot.current_stack;
258        self.executor.callstack = snapshot.callstack;
259        self.executor.cycle = snapshot.cycle;
260        Ok(())
261    }
262}
263
264impl State {
265    fn new_local(
266        source_manager: Arc<dyn SourceManager>,
267        config: Box<DebuggerConfig>,
268        debug_mode: DebugMode,
269        local: LocalState,
270    ) -> Self {
271        Self {
272            source_manager,
273            config,
274            input_mode: InputMode::Normal,
275            breakpoints: vec![],
276            breakpoints_hit: vec![],
277            next_breakpoint_id: 0,
278            stopped: true,
279            debug_mode,
280            selected_stack_frame: 0,
281            session: SessionState::Local(Box::new(local)),
282        }
283    }
284
285    pub fn new(config: Box<DebuggerConfig>) -> Result<Self, Report> {
286        let source_manager = Arc::new(DefaultSourceManager::default());
287        let local = create_local_state(&config, source_manager.clone())?;
288
289        Ok(Self::new_local(source_manager, config, DebugMode::Program, local))
290    }
291
292    /// Create a new debugger state for transaction debugging.
293    ///
294    /// This uses pre-recorded event mutations to replay host events during
295    /// step-by-step debugging, since the debugger's host doesn't have access
296    /// to the real transaction host.
297    pub fn new_for_transaction(
298        package: Arc<Package>,
299        stack_inputs: StackInputs,
300        advice_inputs: AdviceInputs,
301        options: miden_processor::ExecutionOptions,
302        source_manager: Arc<dyn SourceManager>,
303        mast_forests: Vec<LoadedMastForest>,
304        event_replay: Vec<Vec<AdviceMutation>>,
305    ) -> Result<Self, Report> {
306        // Create debug executor with the exact recorded inputs and options.
307        let executor = Executor::from_config(ExecutionConfig {
308            inputs: stack_inputs,
309            advice_inputs,
310            options,
311        });
312        let debug_executor = executor.into_debug_with_replay(
313            package,
314            source_manager.clone(),
315            mast_forests,
316            clone_event_replay_queue(&event_replay),
317        );
318
319        Ok(Self::new_local(
320            source_manager,
321            Box::default(),
322            DebugMode::Transaction,
323            LocalState {
324                executor: debug_executor,
325                execution_failed: None,
326                typed_procedure: None,
327            },
328        ))
329    }
330
331    pub fn reload(&mut self) -> Result<(), Report> {
332        if self.debug_mode == DebugMode::Transaction {
333            return Err(Report::msg("reload is not supported in transaction debug mode"));
334        }
335        if self.debug_mode == DebugMode::Remote {
336            #[cfg(feature = "dap")]
337            {
338                let source_manager = self.source_manager.clone();
339                let SessionState::Remote(remote) = &mut self.session else {
340                    return Err(Report::msg("no remote debug session"));
341                };
342                let result = remote.client.restart_phase2().map_err(Report::msg)?;
343                match result {
344                    crate::exec::DapStopReason::Restarting => {
345                        remote.reconnect(&source_manager)?;
346                    }
347                    crate::exec::DapStopReason::Stopped(snapshot) => {
348                        // Fallback: server treated it as Phase 1.
349                        remote.refresh_executor(&source_manager, &snapshot);
350                    }
351                    crate::exec::DapStopReason::Terminated => {
352                        return Err(Report::msg("server terminated without restart signal"));
353                    }
354                }
355            }
356            #[cfg(not(feature = "dap"))]
357            return Err(Report::msg("remote debug mode requires the `dap` feature"));
358        } else {
359            log::debug!("reloading program");
360            let local = create_local_state(&self.config, self.source_manager.clone())?;
361
362            self.session = SessionState::Local(Box::new(local));
363            let breakpoints = core::mem::take(&mut self.breakpoints);
364            self.breakpoints.reserve(breakpoints.len());
365            self.next_breakpoint_id = 0;
366            for bp in breakpoints {
367                // Drop in-flight step breakpoints (next/next-line/finish): they
368                // refer to execution state (e.g. a frame flagged break-on-exit)
369                // that no longer exists after a restart. Carrying one over would
370                // also permanently suppress user breakpoints, since they are
371                // skipped while an internal breakpoint is pending.
372                if bp.is_internal() {
373                    continue;
374                }
375                self.create_breakpoint(bp.ty);
376            }
377        }
378
379        self.finish_reload();
380        Ok(())
381    }
382
383    fn finish_reload(&mut self) {
384        self.executor_mut().stopped = false;
385        self.selected_stack_frame = 0;
386        self.breakpoints_hit.clear();
387        self.stopped = true;
388    }
389
390    /// Resume local execution until the VM terminates, errors, or a breakpoint is hit.
391    pub fn run_until_stopped(&mut self) {
392        let start_cycle = self.executor().cycle;
393        let start_asmop = self.executor().current_asmop.clone();
394        let start_proc = self.current_procedure();
395        let start_line_loc = self.current_display_location();
396        let source_path_prefixes = self.source_path_prefixes();
397        let minimum_source_line =
398            start_proc.as_deref().zip(start_line_loc.as_ref()).and_then(|(proc, loc)| {
399                self.minimum_source_line_for_proc(proc, loc.source_file.uri().as_str())
400            });
401        let mut previous_proc = self.current_procedure();
402        let mut previous_source_loc = self.current_user_source_location();
403        let mut previous_internal_loc = self.current_internal_source_location();
404        let mut pending_called_breakpoints = Vec::new();
405        let mut breakpoints = core::mem::take(&mut self.breakpoints);
406        self.breakpoints_hit.clear();
407        self.stopped = false;
408
409        let stopped = loop {
410            if self.executor().stopped {
411                break true;
412            }
413
414            let mut consume_most_recent_finish = false;
415            match self.executor_mut().step() {
416                Ok(Some(exited)) if exited.should_break_on_exit() => {
417                    consume_most_recent_finish = true;
418                }
419                Ok(_) => {}
420                Err(err) => {
421                    self.set_execution_failed(err);
422                    break true;
423                }
424            }
425
426            if breakpoints.is_empty() {
427                continue;
428            }
429
430            let is_op_boundary = self.executor().current_asmop.is_some();
431            let user_source_loc = self.current_user_source_location();
432            let internal_source_loc = self.current_internal_source_location();
433            let line_loc = self.current_display_location();
434            let proc = self.current_procedure();
435            let current_cycle = self.executor().cycle;
436            let cycles_stepped = current_cycle - start_cycle;
437            let has_internal_breakpoint = breakpoints.iter().any(|bp| bp.is_internal());
438            let current_op = self.executor().current_op;
439            let current_asmop_str = if breakpoints
440                .iter()
441                .any(|bp| matches!(&bp.ty, BreakpointType::Opcode(OperationMatcher::Asm(_))))
442            {
443                self.executor().current_asmop.as_ref().map(|asmop| asmop.op().to_string())
444            } else {
445                None
446            };
447
448            breakpoints.retain_mut(|bp| {
449                if let Some(n) = bp.cycles_to_skip(current_cycle) {
450                    if cycles_stepped > 0 && n == 0 {
451                        let retained = !bp.is_one_shot();
452                        if retained {
453                            self.breakpoints_hit.push(bp.clone());
454                        } else {
455                            self.breakpoints_hit.push(core::mem::take(bp));
456                        }
457                        return retained;
458                    }
459                    return true;
460                }
461
462                if cycles_stepped > 0
463                    && is_op_boundary
464                    && matches!(&bp.ty, BreakpointType::Next)
465                    && self.executor().current_asmop != start_asmop
466                {
467                    self.breakpoints_hit.push(core::mem::take(bp));
468                    return false;
469                }
470
471                if cycles_stepped > 0
472                    && is_op_boundary
473                    && matches!(&bp.ty, BreakpointType::NextLine)
474                    && Self::is_next_source_line(
475                        start_proc.as_deref(),
476                        start_line_loc.as_ref(),
477                        proc.as_deref(),
478                        line_loc.as_ref(),
479                        &source_path_prefixes,
480                        minimum_source_line,
481                    )
482                {
483                    self.breakpoints_hit.push(core::mem::take(bp));
484                    return false;
485                }
486
487                if has_internal_breakpoint && !bp.is_internal() {
488                    return true;
489                }
490
491                // Opcode breakpoints: raw operation matchers fire on the op just
492                // executed; assembly-level matchers compare against the current
493                // asmop at instruction boundaries.
494                if cycles_stepped > 0
495                    && (current_op
496                        .is_some_and(|op| bp.should_break_for(&op, &self.executor().state()))
497                        || (is_op_boundary
498                            && matches!(
499                                (&bp.ty, current_asmop_str.as_deref()),
500                                (
501                                    BreakpointType::Opcode(OperationMatcher::Asm(expected)),
502                                    Some(current),
503                                ) if expected == current
504                            )))
505                {
506                    self.breakpoints_hit.push(bp.clone());
507                    return true;
508                }
509
510                // Line/File breakpoints fire on the transition onto a matching
511                // source position, so that a breakpoint inside a loop fires once
512                // per iteration and `continue` from a stop can leave the line.
513                if let Some(loc) = user_source_loc.as_ref()
514                    && bp.should_break_at(loc)
515                    && !previous_source_loc.as_ref().is_some_and(|prev| bp.should_break_at(prev))
516                {
517                    let retained = !bp.is_one_shot();
518                    if retained {
519                        self.breakpoints_hit.push(bp.clone());
520                    } else {
521                        self.breakpoints_hit.push(core::mem::take(bp));
522                    }
523                    return retained;
524                }
525
526                // The user-level position above intentionally skips frames executing
527                // compiler-internal code, so a breakpoint that explicitly targets an internal
528                // source file (e.g. a compiler intrinsic) is matched against the raw innermost
529                // position instead. Intrinsics stay debuggable like any other MASM, and since
530                // user source files never classify as internal, this cannot reintroduce
531                // mid-statement stops for user-level breakpoints.
532                if let Some(loc) = internal_source_loc.as_ref()
533                    && bp.should_break_at(loc)
534                    && !previous_internal_loc.as_ref().is_some_and(|prev| bp.should_break_at(prev))
535                {
536                    let retained = !bp.is_one_shot();
537                    if retained {
538                        self.breakpoints_hit.push(bp.clone());
539                    } else {
540                        self.breakpoints_hit.push(core::mem::take(bp));
541                    }
542                    return retained;
543                }
544
545                if matches!(&bp.ty, BreakpointType::Called(_))
546                    && let Some(proc) = proc.as_deref()
547                {
548                    let matched = bp.should_break_in(proc);
549                    if !matched {
550                        pending_called_breakpoints.retain(|id| *id != bp.id);
551                        return true;
552                    }
553
554                    let was_matched = previous_proc
555                        .as_deref()
556                        .is_some_and(|previous| bp.should_break_in(previous));
557                    let matched_at_start =
558                        start_proc.as_deref().is_some_and(|start| bp.should_break_in(start));
559                    let pending = pending_called_breakpoints.contains(&bp.id);
560                    let entered_matching_proc = !was_matched && !matched_at_start;
561
562                    if entered_matching_proc
563                        && self.should_defer_called_breakpoint(proc, line_loc.as_ref())
564                    {
565                        if !pending {
566                            pending_called_breakpoints.push(bp.id);
567                        }
568                        return true;
569                    }
570
571                    if entered_matching_proc
572                        || (pending
573                            && !self.should_defer_called_breakpoint(proc, line_loc.as_ref()))
574                    {
575                        pending_called_breakpoints.retain(|id| *id != bp.id);
576                        let retained = !bp.is_one_shot();
577                        if retained {
578                            self.breakpoints_hit.push(bp.clone());
579                        } else {
580                            self.breakpoints_hit.push(core::mem::take(bp));
581                        }
582                        return retained;
583                    }
584                }
585
586                true
587            });
588
589            if consume_most_recent_finish
590                && let Some(id) = breakpoints.iter().rev().find_map(|bp| {
591                    if matches!(bp.ty, BreakpointType::Finish) {
592                        Some(bp.id)
593                    } else {
594                        None
595                    }
596                })
597            {
598                breakpoints.retain(|bp| bp.id != id);
599                break true;
600            }
601
602            if !self.breakpoints_hit.is_empty() {
603                break true;
604            }
605
606            previous_proc = proc;
607            previous_source_loc = user_source_loc;
608            previous_internal_loc = internal_source_loc;
609        };
610
611        self.breakpoints = breakpoints;
612        self.stopped = stopped;
613        self.selected_stack_frame = 0;
614    }
615
616    pub fn create_breakpoint(&mut self, ty: BreakpointType) {
617        let id = self.next_breakpoint_id();
618        let creation_cycle = self.executor().cycle;
619        log::trace!("created breakpoint with id {id} at cycle {creation_cycle}");
620        if matches!(ty, BreakpointType::Finish)
621            && let Some(frame) = self.executor_mut().callstack.current_frame_mut()
622        {
623            frame.break_on_exit();
624        }
625        self.breakpoints.push(Breakpoint {
626            id,
627            creation_cycle,
628            ty,
629        });
630    }
631
632    fn next_breakpoint_id(&mut self) -> u8 {
633        let mut candidate = self.next_breakpoint_id;
634        let initial = candidate;
635        let mut next = candidate.wrapping_add(1);
636        loop {
637            assert_ne!(initial, next, "unable to allocate a breakpoint id: too many breakpoints");
638            if self
639                .breakpoints
640                .iter()
641                .chain(self.breakpoints_hit.iter())
642                .any(|bp| bp.id == candidate)
643            {
644                candidate = next;
645                next = candidate.wrapping_add(1);
646                continue;
647            }
648            self.next_breakpoint_id = next;
649            break candidate;
650        }
651    }
652
653    pub fn executor(&self) -> &DebugExecutor {
654        match &self.session {
655            SessionState::Local(local) => &local.executor,
656            #[cfg(feature = "dap")]
657            SessionState::Remote(remote) => &remote.executor,
658        }
659    }
660
661    pub fn executor_mut(&mut self) -> &mut DebugExecutor {
662        match &mut self.session {
663            SessionState::Local(local) => &mut local.executor,
664            #[cfg(feature = "dap")]
665            SessionState::Remote(remote) => &mut remote.executor,
666        }
667    }
668
669    pub fn current_procedure(&self) -> Option<Arc<str>> {
670        let live_proc = self
671            .executor()
672            .current_asmop
673            .as_ref()
674            .map(|op| op.context_name().clone())
675            .or_else(|| self.executor().current_proc.clone());
676        let frame_proc =
677            self.executor().callstack.current_frame().and_then(|frame| frame.procedure(""));
678        live_proc.or(frame_proc)
679    }
680
681    pub fn current_location(&self) -> Option<ResolvedLocation> {
682        self.executor()
683            .callstack
684            .current_frame()
685            .and_then(|frame| frame.recent().back())
686            .and_then(|detail| self.resolve_op_location(detail.location()?))
687    }
688
689    pub fn current_display_location(&self) -> Option<ResolvedLocation> {
690        let frame = self.executor().callstack.current_frame()?;
691        for detail in frame.recent().iter().rev() {
692            if let Some(location) = detail.location()
693                && let Some(resolved) = self.resolve_op_location(location)
694            {
695                return Some(resolved);
696            }
697        }
698        None
699    }
700
701    pub fn logical_stack_frames(&self) -> Vec<crate::debug::LogicalStackFrame> {
702        self.executor().callstack.logical_frames("")
703    }
704
705    pub fn selected_stack_frame(&self) -> usize {
706        self.selected_stack_frame
707    }
708
709    pub fn select_older_stack_frame(&mut self) {
710        let last = self.logical_stack_frames().len().saturating_sub(1);
711        self.selected_stack_frame = self.selected_stack_frame.saturating_add(1).min(last);
712    }
713
714    pub fn select_newer_stack_frame(&mut self) {
715        self.selected_stack_frame = self.selected_stack_frame.saturating_sub(1);
716    }
717
718    pub fn selected_display_location(&self) -> Option<ResolvedLocation> {
719        self.logical_stack_frames()
720            .iter()
721            .rev()
722            .nth(self.selected_stack_frame)
723            .and_then(|frame| frame.resolved(&*self.source_manager))
724    }
725
726    /// Return the current source position as seen from the nearest non-internal
727    /// (user) call frame.
728    ///
729    /// Excursions into compiler intrinsics do not change this position, which
730    /// makes it suitable for matching source-level (line/file) breakpoints: a
731    /// statement that calls into `::intrinsics::*` helpers mid-line still reads
732    /// as a single visit to that line.
733    fn current_user_source_location(&self) -> Option<ResolvedLocation> {
734        for frame in self.executor().callstack.frames().iter().rev() {
735            for detail in frame.recent().iter().rev() {
736                if let Some(location) = detail.location()
737                    && let Some(resolved) = self.resolve_op_location(location)
738                {
739                    if crate::debug::is_internal_source_uri(resolved.source_file.uri()) {
740                        // This frame is executing compiler-internal code; its
741                        // caller carries the user-source position.
742                        break;
743                    }
744                    return Some(resolved);
745                }
746            }
747        }
748        None
749    }
750
751    /// The innermost resolvable source position, only when it refers to compiler-internal
752    /// code (intrinsics, the Rust standard library).
753    ///
754    /// [Self::current_user_source_location] intentionally skips such frames so that a user
755    /// statement calling into helpers reads as a single visit to its line; this accessor is the
756    /// counterpart that lets breakpoints explicitly targeting internal sources keep firing —
757    /// compiler intrinsics remain debuggable like any other MASM.
758    fn current_internal_source_location(&self) -> Option<ResolvedLocation> {
759        self.current_location()
760            .filter(|loc| crate::debug::is_internal_source_uri(loc.source_file.uri()))
761    }
762
763    pub fn is_next_source_line(
764        start_proc: Option<&str>,
765        start_loc: Option<&ResolvedLocation>,
766        current_proc: Option<&str>,
767        current_loc: Option<&ResolvedLocation>,
768        source_path_prefixes: &[String],
769        minimum_source_line: Option<u32>,
770    ) -> bool {
771        let same_proc = match (start_proc, current_proc) {
772            (Some(start), Some(current)) => start == current,
773            (Some(_), None) => false,
774            _ => true,
775        };
776        if !same_proc {
777            return false;
778        }
779
780        if let (Some(minimum_source_line), Some(current)) = (minimum_source_line, current_loc)
781            && current.line < minimum_source_line
782        {
783            return false;
784        }
785
786        match (start_loc, current_loc) {
787            (Some(start), Some(current)) => {
788                source_paths_match(
789                    start.source_file.uri().as_str(),
790                    current.source_file.uri().as_str(),
791                    source_path_prefixes,
792                ) && start.line != current.line
793            }
794            (None, Some(_)) => true,
795            _ => false,
796        }
797    }
798
799    pub(crate) fn minimum_source_line_for_proc(
800        &self,
801        procedure: &str,
802        source_path: &str,
803    ) -> Option<u32> {
804        let executor = self.executor();
805        let ctx = executor.resume_ctx.as_ref()?;
806        let mast_forest = ctx.current_forest();
807        let debug_info = ctx.debug_info()?;
808
809        let source_path_prefixes = self.source_path_prefixes();
810
811        let mut lines = BTreeSet::<u32>::default();
812        for function_info in debug_info.functions() {
813            if debug_info[function_info.name_idx].as_ref() != procedure {
814                continue;
815            }
816            let source_node_id = function_info.source_node.into_option().or_else(|| {
817                mast_forest.find_procedure_root(function_info.mast_root).and_then(|exec_node| {
818                    debug_info.unique_source_root_for_exec_node(exec_node).ok().flatten()
819                })
820            })?;
821            for asm_op in debug_info[source_node_id].asm_ops.iter() {
822                let Some(location_idx) = asm_op.location_idx.into_option() else {
823                    continue;
824                };
825                let location = debug_info.get_location(location_idx).unwrap();
826                let Some(resolved_location) = self.resolve_op_location(&location) else {
827                    continue;
828                };
829                if resolved_location.line > 1
830                    && source_paths_match(
831                        resolved_location.source_file.uri().as_str(),
832                        source_path,
833                        &source_path_prefixes,
834                    )
835                {
836                    lines.insert(resolved_location.line);
837                }
838            }
839        }
840        lines.pop_first()
841    }
842
843    pub(crate) fn source_path_prefixes(&self) -> Vec<String> {
844        let mut prefixes = self
845            .config
846            .source_path_prefixes
847            .iter()
848            .map(|path| path.to_string_lossy().into_owned())
849            .collect::<Vec<_>>();
850        if let Ok(cwd) = std::env::current_dir() {
851            let cwd = cwd.to_string_lossy().into_owned();
852            if !prefixes
853                .iter()
854                .any(|prefix| normalize_source_path(prefix) == normalize_source_path(&cwd))
855            {
856                prefixes.push(cwd);
857            }
858        }
859        prefixes
860    }
861
862    fn resolve_op_location(&self, loc: &Location) -> Option<ResolvedLocation> {
863        let source_file = self.load_source_file_for_uri(loc.uri())?;
864        let span = SourceSpan::new(source_file.id(), loc.start..loc.end);
865        let file_line_col = source_file.location(span);
866        Some(ResolvedLocation {
867            source_file,
868            line: file_line_col.line.to_u32(),
869            col: file_line_col.column.to_u32(),
870            span,
871        })
872    }
873
874    fn load_source_file_for_uri(
875        &self,
876        uri: &miden_debug_types::Uri,
877    ) -> Option<Arc<miden_debug_types::SourceFile>> {
878        let uri_str = uri.as_str();
879        let normalized_uri = uri_str.strip_prefix("file://").unwrap_or(uri_str);
880        let path = Path::new(normalized_uri);
881        if path.exists() {
882            return self.source_manager.load_file(path).ok();
883        }
884
885        if let Some(source_file) = self.source_manager.get_by_uri(uri) {
886            return Some(source_file);
887        }
888
889        for candidate in source_path_candidates(normalized_uri, &self.source_path_prefixes()) {
890            if candidate.exists()
891                && let Ok(source_file) = self.source_manager.load_file(&candidate)
892            {
893                return Some(source_file);
894            }
895        }
896
897        None
898    }
899
900    pub fn should_defer_called_breakpoint(
901        &self,
902        proc: &str,
903        current_loc: Option<&ResolvedLocation>,
904    ) -> bool {
905        let executor = self.executor();
906        if executor.debug_vars.current_variables().any(|variable| {
907            variable.clk == miden_processor::trace::RowIndex::from(executor.cycle as u32)
908        }) {
909            return false;
910        }
911        (!is_internal_procedure(proc)
912            && current_loc
913                .is_none_or(|loc| crate::debug::is_internal_source_uri(loc.source_file.uri())))
914            || executor.should_wait_for_entry_variables(proc)
915    }
916
917    pub fn execution_failed(&self) -> Option<&miden_processor::ExecutionError> {
918        match &self.session {
919            SessionState::Local(local) => local.execution_failed.as_ref(),
920            #[cfg(feature = "dap")]
921            SessionState::Remote(_) => None,
922        }
923    }
924
925    fn execution_completed(&self) -> bool {
926        self.executor().stopped && self.execution_failed().is_none()
927    }
928
929    /// Decode the completed program's result using its component-model entrypoint signature.
930    pub fn typed_result(&self) -> Result<Option<String>, String> {
931        if !self.execution_completed() {
932            return Ok(None);
933        }
934        let local = match &self.session {
935            SessionState::Local(local) => local,
936            #[cfg(feature = "dap")]
937            SessionState::Remote(_) => return Ok(None),
938        };
939        let Some(procedure) = local.typed_procedure.as_ref() else {
940            return Ok(None);
941        };
942
943        procedure
944            .decode_result(local.executor.stack_outputs.get_num_elements(16))
945            .map_err(|err| format!("failed to decode program result: {err}"))
946    }
947
948    pub fn set_execution_failed(&mut self, error: miden_processor::ExecutionError) {
949        match &mut self.session {
950            SessionState::Local(local) => local.execution_failed = Some(error),
951            #[cfg(feature = "dap")]
952            SessionState::Remote(_) => {
953                panic!("cannot record local execution failure while in remote mode")
954            }
955        }
956    }
957}
958
959macro_rules! write_with_format_type {
960    ($out:ident, $read_expr:ident, $value:expr) => {
961        match $read_expr.format {
962            crate::debug::FormatType::Decimal => write!(&mut $out, "{}", $value).unwrap(),
963            crate::debug::FormatType::Hex => write!(&mut $out, "{:#x}", $value).unwrap(),
964            crate::debug::FormatType::Binary => write!(&mut $out, "{:#b}", $value).unwrap(),
965        }
966    };
967}
968
969impl State {
970    pub fn read_memory(&mut self, expr: &ReadMemoryExpr) -> Result<String, String> {
971        use core::fmt::Write;
972
973        use miden_assembly_syntax::ast::types::Type;
974
975        use crate::debug::FormatType;
976
977        #[cfg(feature = "dap")]
978        if self.debug_mode == DebugMode::Remote {
979            let SessionState::Remote(remote) = &mut self.session else {
980                return Err("no remote debug session".into());
981            };
982            return remote.read_memory(expr);
983        }
984
985        #[cfg(not(feature = "dap"))]
986        if self.debug_mode == DebugMode::Remote {
987            return Err("remote debug mode requires the `dap` feature".into());
988        }
989
990        let executor = self.executor();
991        let cycle = miden_processor::trace::RowIndex::from(executor.cycle);
992        let context = executor.current_context;
993        let memory = executor.processor.memory();
994        let read_element = |addr: u32| -> Option<Felt> {
995            memory
996                .read_element(context, Felt::new(addr as u64).expect("value exceeds field modulus"))
997                .ok()
998        };
999        let mut output = String::new();
1000        if expr.count > 1 {
1001            return Err("-count with value > 1 is not yet implemented".into());
1002        } else if matches!(expr.ty, Type::Felt) {
1003            if !expr.addr.is_element_aligned() {
1004                return Err(
1005                    "read failed: type 'felt' must be aligned to an element boundary".into()
1006                );
1007            }
1008            let felt = read_element(expr.addr.addr).unwrap_or(Felt::ZERO);
1009            write_with_format_type!(output, expr, felt.as_canonical_u64());
1010        } else if matches!(
1011            expr.ty,
1012            Type::Array(ref array_ty) if array_ty.element_type() == &Type::Felt && array_ty.len() == 4
1013        ) {
1014            if !expr.addr.is_word_aligned() {
1015                return Err("read failed: type 'word' must be aligned to a word boundary".into());
1016            }
1017            let word = memory
1018                .read_word(
1019                    context,
1020                    Felt::new(expr.addr.addr as u64).expect("value exceeds field modulus"),
1021                    cycle,
1022                )
1023                .unwrap_or_default();
1024            output.push('[');
1025            for (i, elem) in word.iter().enumerate() {
1026                if i > 0 {
1027                    output.push_str(", ");
1028                }
1029                write_with_format_type!(output, expr, elem.as_canonical_u64());
1030            }
1031            output.push(']');
1032        } else {
1033            if !expr.addr.is_element_aligned() {
1034                return Err("invalid read: unaligned reads are not supported yet".into());
1035            }
1036
1037            const U32_MASK: u64 = u32::MAX as u64;
1038            let size = expr.ty.size_in_bytes();
1039            let size_in_felts = expr.ty.size_in_felts();
1040            let mut bytes = Vec::with_capacity(size);
1041            let mut needed = size;
1042            for i in 0..size_in_felts {
1043                let addr = expr.addr.addr.checked_add(i as u32).ok_or_else(|| {
1044                    "invalid read: attempted to read beyond end of linear memory".to_string()
1045                })?;
1046                let elem = read_element(addr).unwrap_or_default();
1047                let elem_bytes = ((elem.as_canonical_u64() & U32_MASK) as u32).to_le_bytes();
1048                let take = core::cmp::min(needed, 4);
1049                bytes.extend(&elem_bytes[..take]);
1050                needed -= take;
1051            }
1052
1053            match &expr.ty {
1054                Type::I1 => match expr.format {
1055                    FormatType::Decimal => write!(&mut output, "{}", bytes[0] != 0).unwrap(),
1056                    FormatType::Hex => {
1057                        write!(&mut output, "{:#0x}", (bytes[0] != 0) as u8).unwrap()
1058                    }
1059                    FormatType::Binary => {
1060                        write!(&mut output, "{:#0b}", (bytes[0] != 0) as u8).unwrap()
1061                    }
1062                },
1063                Type::I8 => write_with_format_type!(output, expr, bytes[0] as i8),
1064                Type::U8 => write_with_format_type!(output, expr, bytes[0]),
1065                Type::I16 => {
1066                    write_with_format_type!(output, expr, i16::from_le_bytes([bytes[0], bytes[1]]))
1067                }
1068                Type::U16 => {
1069                    write_with_format_type!(output, expr, u16::from_le_bytes([bytes[0], bytes[1]]))
1070                }
1071                Type::I32 => write_with_format_type!(
1072                    output,
1073                    expr,
1074                    i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
1075                ),
1076                Type::U32 => write_with_format_type!(
1077                    output,
1078                    expr,
1079                    u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
1080                ),
1081                ty @ (Type::I64 | Type::U64) => {
1082                    let val = u64::from_le_bytes(bytes[..8].try_into().unwrap());
1083                    if matches!(ty, Type::I64) {
1084                        write_with_format_type!(output, expr, val as i64)
1085                    } else {
1086                        write_with_format_type!(output, expr, val)
1087                    }
1088                }
1089                ty => {
1090                    return Err(format!(
1091                        "support for reads of type '{ty}' are not implemented yet"
1092                    ));
1093                }
1094            }
1095        }
1096
1097        Ok(output)
1098    }
1099
1100    /// Collect the current debug variables as structured records.
1101    ///
1102    /// Successful completion has no live variables. Failed execution retains the last variable
1103    /// state for inspection.
1104    ///
1105    /// When `show_all` is false, compiler-generated locals (named `local0`, `local1`, etc.)
1106    /// are hidden. Use `show_all` = true (`:vars all`) to include them.
1107    pub fn current_variables(&self, show_all: bool) -> Vec<DebugVariableValue> {
1108        if self.execution_completed() {
1109            return Vec::new();
1110        }
1111
1112        let executor = self.executor();
1113        let debug_vars = &executor.debug_vars;
1114
1115        let stack = executor.current_stack.clone();
1116        let context = executor.current_context;
1117
1118        // Use live processor state, not the pre-recorded trace, for current-cycle values.
1119        let read_mem = |addr: u32| -> Option<Felt> {
1120            executor
1121                .processor
1122                .memory()
1123                .read_element(context, Felt::new(addr as u64).expect("value exceeds field modulus"))
1124                .ok()
1125        };
1126
1127        let current_source = if show_all {
1128            None
1129        } else {
1130            self.current_display_location()
1131        };
1132        let source_path_prefixes = self.source_path_prefixes();
1133
1134        let mut variables = Vec::new();
1135
1136        for var_snapshot in debug_vars.current_variables() {
1137            let name = var_snapshot.info.name();
1138
1139            if !show_all && is_compiler_generated_name(name) {
1140                continue;
1141            }
1142
1143            if let (Some(current), Some(var_loc)) =
1144                (current_source.as_ref(), var_snapshot.info.location())
1145                && let Some(var_loc) = self.resolve_op_location(var_loc)
1146                && !source_var_location_is_visible(
1147                    var_loc.source_file.uri().as_str(),
1148                    var_loc.line,
1149                    current.source_file.uri().as_str(),
1150                    current.line,
1151                    &source_path_prefixes,
1152                )
1153            {
1154                continue;
1155            }
1156
1157            let location = var_snapshot.info.value_location();
1158            let resolve_local = |offset: i16| {
1159                // Read FMP from live memory, then compute address as FMP + offset
1160                let fmp_addr = miden_core::FMP_ADDR.as_canonical_u64() as u32;
1161                let fmp = read_mem(fmp_addr)?;
1162                let addr = (fmp.as_canonical_u64() as i64 + offset as i64) as u32;
1163                read_mem(addr)
1164            };
1165
1166            let display_value = var_snapshot.info.ty().and_then(|ty| {
1167                format_value(ty, |count| {
1168                    debug_vars
1169                        .captured_values(name)
1170                        .filter(|values| values.len() == count)
1171                        .map(<[Felt]>::to_vec)
1172                        .or_else(|| {
1173                            resolve_typed_variable_values(
1174                                location,
1175                                ty,
1176                                count,
1177                                &stack,
1178                                read_mem,
1179                                resolve_local,
1180                            )
1181                        })
1182                })
1183            });
1184
1185            let value = debug_vars
1186                .captured_values(name)
1187                .and_then(|values| values.first().copied())
1188                .or_else(|| resolve_variable_value(location, &stack, read_mem, resolve_local));
1189
1190            let source = var_snapshot.info.location().and_then(|loc| {
1191                let loc = self.resolve_op_location(loc)?;
1192                Some(DebugVariableSource {
1193                    path: loc.source_file.uri().as_str().to_string(),
1194                    line: loc.line,
1195                    column: loc.col,
1196                })
1197            });
1198
1199            variables.push(DebugVariableValue {
1200                name: name.to_string(),
1201                value,
1202                display_value,
1203                location: location.to_string(),
1204                source,
1205            });
1206        }
1207
1208        variables
1209    }
1210
1211    /// Format the current debug variables as a string for display.
1212    ///
1213    /// When `show_all` is false, compiler-generated locals (named `local0`, `local1`, etc.)
1214    /// are hidden. Use `show_all` = true (`:vars all`) to include them.
1215    pub fn format_variables(&self, show_all: bool) -> String {
1216        use core::fmt::Write;
1217
1218        if self.execution_completed() {
1219            return "Program has terminated; no live variables".to_string();
1220        }
1221
1222        if !self.executor().debug_vars.has_variables() {
1223            return "No debug variables tracked".to_string();
1224        }
1225
1226        let variables = self.current_variables(show_all);
1227        if variables.is_empty() {
1228            "No source-level variables (use ':vars all' to show compiler locals)".to_string()
1229        } else {
1230            let mut output = String::new();
1231            for variable in variables {
1232                if !output.is_empty() {
1233                    output.push_str(", ");
1234                }
1235
1236                if let Some(value) = variable.display_value {
1237                    write!(&mut output, "{}={value}", variable.name).unwrap();
1238                    continue;
1239                }
1240
1241                match variable.value {
1242                    Some(felt) => {
1243                        write!(&mut output, "{}={}", variable.name, felt.as_canonical_u64())
1244                            .unwrap();
1245                    }
1246                    None => {
1247                        write!(&mut output, "{}={}", variable.name, variable.location).unwrap();
1248                    }
1249                }
1250            }
1251            output
1252        }
1253    }
1254}
1255
1256fn is_internal_procedure(proc: &str) -> bool {
1257    proc.contains("::intrinsics::")
1258}
1259
1260/// Returns true if the variable name looks compiler-generated (e.g. "local0", "local12").
1261/// Source-level variables have DWARF-derived names like "a", "sum", "_info".
1262fn is_compiler_generated_name(name: &str) -> bool {
1263    name.strip_prefix("local")
1264        .is_some_and(|suffix| !suffix.is_empty() && suffix.chars().all(|c| c.is_ascii_digit()))
1265}
1266
1267fn source_var_location_is_visible(
1268    var_path: &str,
1269    var_line: u32,
1270    current_path: &str,
1271    current_line: u32,
1272    source_path_prefixes: &[String],
1273) -> bool {
1274    source_paths_match(var_path, current_path, source_path_prefixes) && var_line < current_line
1275}
1276
1277fn strip_source_prefix(path: &str, prefix: &str) -> Option<String> {
1278    let path = path.trim_start_matches('/');
1279    let prefix = prefix.trim_start_matches('/').trim_end_matches('/');
1280    path.strip_prefix(prefix)
1281        .and_then(|rest| rest.strip_prefix('/'))
1282        .map(ToOwned::to_owned)
1283}
1284
1285fn source_paths_match(left: &str, right: &str, trim_prefixes: &[String]) -> bool {
1286    let left = normalize_source_path(left);
1287    let right = normalize_source_path(right);
1288    if left.is_empty() || right.is_empty() {
1289        return false;
1290    }
1291
1292    if left == right {
1293        return true;
1294    }
1295
1296    for prefix in trim_prefixes {
1297        if strip_source_prefix(&left, prefix).is_some_and(|stripped| stripped == right) {
1298            return true;
1299        }
1300        if strip_source_prefix(&right, prefix).is_some_and(|stripped| stripped == left) {
1301            return true;
1302        }
1303    }
1304
1305    false
1306}
1307
1308fn source_path_candidates(uri: &str, source_path_prefixes: &[String]) -> Vec<PathBuf> {
1309    let normalized = normalize_source_path(uri);
1310    if normalized.is_empty() || Path::new(&normalized).is_absolute() {
1311        return Vec::new();
1312    }
1313
1314    source_path_prefixes
1315        .iter()
1316        .map(|prefix| Path::new(prefix).join(&normalized))
1317        .collect()
1318}
1319
1320// DAP CLIENT MODE
1321// ================================================================================================
1322
1323#[cfg(feature = "dap")]
1324impl State {
1325    /// Create a new debugger state for remote DAP debugging.
1326    ///
1327    /// Connects to a DAP server, performs the handshake, and queries the
1328    /// initial state to populate the executor fields that the TUI panes read.
1329    pub fn new_for_dap(addr: &str) -> Result<Self, Report> {
1330        let client = crate::exec::DapClient::connect(addr).map_err(Report::msg)?;
1331        Self::from_dap_client(addr, client)
1332    }
1333
1334    pub(crate) fn from_dap_client(
1335        addr: &str,
1336        client: crate::exec::DapClient,
1337    ) -> Result<Self, Report> {
1338        let source_manager: Arc<dyn SourceManager> = Arc::new(DefaultSourceManager::default());
1339        let remote = RemoteState::from_client(addr, client, &source_manager)?;
1340
1341        Ok(Self {
1342            source_manager,
1343            config: Box::default(),
1344            input_mode: InputMode::Normal,
1345            breakpoints: vec![],
1346            breakpoints_hit: vec![],
1347            next_breakpoint_id: 0,
1348            stopped: true,
1349            debug_mode: DebugMode::Remote,
1350            selected_stack_frame: 0,
1351            session: SessionState::Remote(Box::new(remote)),
1352        })
1353    }
1354
1355    pub fn step_remote(&mut self) -> Result<crate::exec::DapStopReason, Report> {
1356        let source_manager = self.source_manager.clone();
1357        let SessionState::Remote(remote) = &mut self.session else {
1358            return Err(Report::msg("no remote debug session"));
1359        };
1360        let result = remote.resume(&self.breakpoints).map_err(Report::msg)?;
1361
1362        self.breakpoints.retain(|bp| !bp.is_one_shot());
1363
1364        match &result {
1365            crate::exec::DapStopReason::Stopped(snapshot) => {
1366                remote.refresh_executor(&source_manager, snapshot);
1367                self.selected_stack_frame = 0;
1368                self.stopped = true;
1369            }
1370            crate::exec::DapStopReason::Terminated => {
1371                remote.executor.stopped = true;
1372                self.stopped = true;
1373            }
1374            crate::exec::DapStopReason::Restarting => {
1375                return Err(Report::msg("unexpected Phase 2 restart signal during step"));
1376            }
1377        }
1378
1379        Ok(result)
1380    }
1381}
1382
1383/// Convert a server-pushed [`DapUiState`](crate::exec::DapUiState) snapshot into a
1384/// [`RemoteSnapshot`] that the TUI executor can consume.
1385#[cfg(feature = "dap")]
1386fn convert_ui_state(
1387    snapshot: &crate::exec::DapUiState,
1388    source_manager: &Arc<dyn SourceManager>,
1389) -> RemoteSnapshot {
1390    use crate::debug::{CallFrame, CallStack};
1391
1392    let call_frames: Vec<CallFrame> = snapshot
1393        .callstack
1394        .iter()
1395        .rev()
1396        .map(|frame| {
1397            let resolved = resolve_remote_frame(frame, source_manager);
1398            CallFrame::from_remote(Some(frame.name.clone()), resolved)
1399        })
1400        .collect();
1401
1402    let current_stack = snapshot
1403        .current_stack
1404        .iter()
1405        .copied()
1406        .map(|v| Felt::new(v).expect("value exceeds field modulus"))
1407        .collect();
1408
1409    RemoteSnapshot {
1410        callstack: CallStack::from_remote_frames(call_frames),
1411        current_stack,
1412        cycle: snapshot.cycle,
1413    }
1414}
1415
1416/// Resolve a remote frame to a [ResolvedLocation] by loading the source file from disk.
1417#[cfg(feature = "dap")]
1418fn resolve_remote_frame(
1419    frame: &crate::exec::DapUiFrame,
1420    source_manager: &Arc<dyn SourceManager>,
1421) -> Option<crate::debug::ResolvedLocation> {
1422    use std::path::Path;
1423
1424    use miden_debug_types::{SourceManagerExt, SourceSpan, Uri};
1425
1426    let path_str = frame.source_path.as_ref()?;
1427    let path = crate::debug::resolve_source_path(&Uri::new(path_str))
1428        .unwrap_or_else(|| Path::new(path_str).to_path_buf());
1429    let source_file = source_manager.load_file(&path).ok()?;
1430    let line = frame.line.max(1) as u32;
1431    let col = frame.column.max(1) as u32;
1432
1433    // Compute a span from the line number — use the byte range of the line
1434    let content = source_file.content();
1435    let line_index = miden_debug_types::LineIndex::from(line.saturating_sub(1));
1436    let range = content.line_range(line_index)?;
1437    let span = SourceSpan::new(source_file.id(), range);
1438
1439    Some(crate::debug::ResolvedLocation {
1440        source_file,
1441        line,
1442        col,
1443        span,
1444    })
1445}
1446
1447fn create_local_state(
1448    config: &DebuggerConfig,
1449    source_manager: Arc<dyn SourceManager>,
1450) -> Result<LocalState, Report> {
1451    let loaded = crate::program_loader::load_debug_executor(config, source_manager, "state")?;
1452    Ok(LocalState {
1453        executor: loaded.executor,
1454        execution_failed: None,
1455        typed_procedure: loaded.typed_procedure,
1456    })
1457}
1458
1459#[cfg(test)]
1460mod tests;