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