Skip to main content

miden_debug/ui/
state.rs

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