Skip to main content

miden_debug/ui/
state.rs

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