Skip to main content

miden_debug/ui/
state.rs

1use std::{collections::VecDeque, rc::Rc, sync::Arc};
2
3use miden_assembly::{DefaultSourceManager, SourceManager};
4use miden_assembly_syntax::diagnostics::{IntoDiagnostic, Report};
5use miden_core::{program::Program, serde::Deserializable};
6use miden_processor::{
7    Felt, StackInputs,
8    advice::{AdviceInputs, AdviceMutation},
9    mast::MastForest,
10};
11
12use crate::{
13    config::DebuggerConfig,
14    debug::{Breakpoint, BreakpointType, ReadMemoryExpr, ResolvedLocation, resolve_variable_value},
15    exec::{DebugExecutor, Executor},
16    input::InputFile,
17};
18
19/// Whether the debugger is debugging a plain program or a transaction.
20#[derive(Debug, Copy, Clone, PartialEq, Eq)]
21pub enum DebugMode {
22    /// Debugging a plain MASM program loaded from a package.
23    Program,
24    /// Debugging a Miden transaction with pre-recorded event replay.
25    Transaction,
26    /// Debugging remotely via a DAP server connection.
27    Remote,
28}
29
30fn clone_advice_mutation(mutation: &AdviceMutation) -> AdviceMutation {
31    match mutation {
32        AdviceMutation::ExtendStack { values } => AdviceMutation::ExtendStack {
33            values: values.clone(),
34        },
35        AdviceMutation::ExtendMap { other } => AdviceMutation::ExtendMap {
36            other: other.clone(),
37        },
38        AdviceMutation::ExtendMerkleStore { infos } => AdviceMutation::ExtendMerkleStore {
39            infos: infos.clone(),
40        },
41        AdviceMutation::ExtendPrecompileRequests { data } => {
42            AdviceMutation::ExtendPrecompileRequests { data: data.clone() }
43        }
44    }
45}
46
47fn clone_event_replay_queue(event_replay: &[Vec<AdviceMutation>]) -> VecDeque<Vec<AdviceMutation>> {
48    event_replay
49        .iter()
50        .map(|batch| batch.iter().map(clone_advice_mutation).collect())
51        .collect()
52}
53
54pub struct State {
55    pub source_manager: Arc<dyn SourceManager>,
56    pub config: Box<DebuggerConfig>,
57    pub input_mode: InputMode,
58    pub breakpoints: Vec<Breakpoint>,
59    pub breakpoints_hit: Vec<Breakpoint>,
60    pub next_breakpoint_id: u8,
61    pub stopped: bool,
62    pub debug_mode: DebugMode,
63    session: SessionState,
64}
65
66#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
67pub enum InputMode {
68    #[default]
69    Normal,
70    #[allow(dead_code)]
71    Insert,
72    Command,
73}
74
75struct LocalState {
76    executor: DebugExecutor,
77    execution_failed: Option<miden_processor::ExecutionError>,
78}
79
80#[cfg(feature = "dap")]
81struct RemoteState {
82    client: crate::exec::DapClient,
83    executor: DebugExecutor,
84    addr: String,
85    /// Tracks which source files have had breakpoints synced to the DAP server,
86    /// so we can send empty breakpoint lists when all breakpoints for a file are removed.
87    synced_bp_files: std::collections::BTreeSet<String>,
88}
89
90enum SessionState {
91    Local(Box<LocalState>),
92    #[cfg(feature = "dap")]
93    Remote(Box<RemoteState>),
94}
95
96#[cfg(feature = "dap")]
97struct RemoteSnapshot {
98    callstack: crate::debug::CallStack,
99    current_stack: Vec<Felt>,
100    cycle: usize,
101}
102
103#[cfg(feature = "dap")]
104impl RemoteState {
105    fn connect(addr: &str, source_manager: &Arc<dyn SourceManager>) -> Result<Self, Report> {
106        use std::{cell::RefCell, collections::BTreeSet, rc::Rc};
107
108        use miden_debug_engine::debug::DebugVarTracker;
109        use miden_processor::{ContextId, FastProcessor};
110
111        use crate::exec::DebuggerHost;
112
113        let mut client = crate::exec::DapClient::connect(addr).map_err(Report::msg)?;
114        let ui_state = client.handshake().map_err(Report::msg)?;
115        let snapshot = convert_ui_state(&ui_state, source_manager);
116
117        let debug_vars = DebugVarTracker::new(Rc::new(RefCell::new(Default::default())));
118        let executor = DebugExecutor {
119            processor: FastProcessor::new(StackInputs::default()),
120            host: DebuggerHost::new(source_manager.clone()),
121            resume_ctx: None,
122            current_stack: snapshot.current_stack,
123            current_op: None,
124            current_asmop: None,
125            stack_outputs: Default::default(),
126            contexts: BTreeSet::new(),
127            root_context: ContextId::root(),
128            current_context: ContextId::root(),
129            callstack: snapshot.callstack,
130            current_proc: None,
131            debug_vars,
132            last_debug_var_count: 0,
133            recent: VecDeque::new(),
134            cycle: snapshot.cycle,
135            stopped: false,
136        };
137
138        Ok(Self {
139            client,
140            executor,
141            addr: addr.to_string(),
142            synced_bp_files: std::collections::BTreeSet::new(),
143        })
144    }
145
146    fn read_memory(&mut self, expr: &ReadMemoryExpr) -> Result<String, String> {
147        self.client.read_memory(expr)
148    }
149
150    fn sync_breakpoints(&mut self, breakpoints: &[Breakpoint]) {
151        use std::collections::BTreeMap;
152
153        // Group Line breakpoints by their file pattern string.
154        let mut by_file: BTreeMap<String, Vec<i64>> = BTreeMap::new();
155        // Collect Called and File patterns as function breakpoints.
156        let mut func_names: Vec<String> = Vec::new();
157
158        for bp in breakpoints {
159            match &bp.ty {
160                BreakpointType::Line { pattern, line } => {
161                    by_file.entry(pattern.as_str().to_string()).or_default().push(*line as i64);
162                }
163                BreakpointType::Called(pattern) | BreakpointType::File(pattern) => {
164                    func_names.push(pattern.as_str().to_string());
165                }
166                _ => {}
167            }
168        }
169
170        // Send empty breakpoint lists for files that were previously synced but no longer have
171        // breakpoints.
172        let stale_files: Vec<String> = self
173            .synced_bp_files
174            .iter()
175            .filter(|f| !by_file.contains_key(f.as_str()))
176            .cloned()
177            .collect();
178        for file in &stale_files {
179            let _ = self.client.set_breakpoints(file, &[]);
180        }
181
182        // Send breakpoints for each file.
183        for (file, lines) in &by_file {
184            let _ = self.client.set_breakpoints(file, lines);
185        }
186
187        // Send function/pattern breakpoints (replaces the full set each time).
188        let _ = self.client.set_function_breakpoints(&func_names);
189
190        // Update tracked set.
191        self.synced_bp_files = by_file.into_keys().collect();
192    }
193
194    fn resume(&mut self, breakpoints: &[Breakpoint]) -> Result<crate::exec::DapStopReason, String> {
195        // Sync user-defined breakpoints to the DAP server before choosing a step command.
196        self.sync_breakpoints(breakpoints);
197
198        let has_step = breakpoints.iter().any(|bp| matches!(bp.ty, BreakpointType::Step));
199        let has_next = breakpoints
200            .iter()
201            .any(|bp| matches!(bp.ty, BreakpointType::Next | BreakpointType::NextLine));
202        let has_finish = breakpoints.iter().any(|bp| matches!(bp.ty, BreakpointType::Finish));
203
204        if has_step {
205            self.client.step_in()
206        } else if has_next {
207            self.client.step_over()
208        } else if has_finish {
209            self.client.step_out()
210        } else {
211            self.client.continue_()
212        }
213    }
214
215    fn refresh_executor(
216        &mut self,
217        source_manager: &Arc<dyn SourceManager>,
218        pushed: &crate::exec::DapUiState,
219    ) {
220        // Standard DAP `stopped` events tell us execution paused, but do not
221        // carry the refreshed VM state (stack, callstack, cycle). The server
222        // pushes a custom `miden/uiState` event with the bundled snapshot
223        // immediately before each `stopped` event, so we consume that here
224        // instead of issuing an extra evaluate round-trip.
225        let snapshot = convert_ui_state(pushed, source_manager);
226        self.executor.current_stack = snapshot.current_stack;
227        self.executor.callstack = snapshot.callstack;
228        self.executor.cycle = snapshot.cycle;
229    }
230
231    fn reconnect(&mut self, source_manager: &Arc<dyn SourceManager>) -> Result<(), Report> {
232        let timeout = std::time::Duration::from_secs(30);
233        let mut new_client =
234            crate::exec::DapClient::connect_with_retry(&self.addr, timeout).map_err(Report::msg)?;
235        let ui_state = new_client.handshake().map_err(Report::msg)?;
236        let snapshot = convert_ui_state(&ui_state, source_manager);
237
238        self.client = new_client;
239        self.executor.current_stack = snapshot.current_stack;
240        self.executor.callstack = snapshot.callstack;
241        self.executor.cycle = snapshot.cycle;
242        Ok(())
243    }
244}
245
246impl State {
247    fn new_local(
248        source_manager: Arc<dyn SourceManager>,
249        config: Box<DebuggerConfig>,
250        debug_mode: DebugMode,
251        local: LocalState,
252    ) -> Self {
253        Self {
254            source_manager,
255            config,
256            input_mode: InputMode::Normal,
257            breakpoints: vec![],
258            breakpoints_hit: vec![],
259            next_breakpoint_id: 0,
260            stopped: true,
261            debug_mode,
262            session: SessionState::Local(Box::new(local)),
263        }
264    }
265
266    pub fn new(config: Box<DebuggerConfig>) -> Result<Self, Report> {
267        let source_manager = Arc::new(DefaultSourceManager::default());
268        let mut inputs = config.inputs.clone().unwrap_or_default();
269        if !config.args.is_empty() {
270            // CLI args model sequential pushes, but StackInputs expects the top element first.
271            let args = config.args.iter().rev().map(|felt| felt.0).collect::<Vec<_>>();
272            inputs.inputs = StackInputs::new(&args).into_diagnostic()?;
273        }
274        let args = inputs.inputs.iter().copied().collect::<Vec<_>>();
275        let package = load_package(&config)?;
276
277        // Load libraries from link_libraries and sysroot BEFORE resolving dependencies
278        let mut libs = Vec::with_capacity(config.link_libraries.len());
279        for link_library in config.link_libraries.iter() {
280            log::debug!(target: "state", "loading link library {}", link_library.name());
281            let lib = link_library.load(&config, source_manager.clone())?;
282            libs.push(lib.clone());
283        }
284
285        // Load std and base libraries from sysroot if available
286        if let Some(toolchain_dir) = config.toolchain_dir() {
287            libs.extend(load_sysroot_libs(&toolchain_dir)?);
288        }
289
290        // Create executor and register libraries with dependency resolver before resolving
291        let mut executor = Executor::new(args.clone());
292        for lib in libs.iter() {
293            executor.register_library_dependency(lib.clone());
294            executor.with_library(lib.clone());
295        }
296
297        // Now resolve package dependencies (they should find the registered libraries)
298        let dependencies = package.manifest.dependencies();
299        executor.with_dependencies(dependencies)?;
300        executor.with_advice_inputs(inputs.advice_inputs);
301
302        let program = package.unwrap_program();
303        let executor = executor.into_debug(&program, source_manager.clone());
304
305        Ok(Self::new_local(
306            source_manager,
307            config,
308            DebugMode::Program,
309            LocalState {
310                executor,
311                execution_failed: None,
312            },
313        ))
314    }
315
316    /// Create a new debugger state for transaction debugging.
317    ///
318    /// This uses pre-recorded event mutations to replay host events during
319    /// step-by-step debugging, since the debugger's host doesn't have access
320    /// to the real transaction host.
321    pub fn new_for_transaction(
322        program: Arc<Program>,
323        stack_inputs: StackInputs,
324        advice_inputs: AdviceInputs,
325        source_manager: Arc<dyn SourceManager>,
326        mast_forests: Vec<Arc<MastForest>>,
327        event_replay: Vec<Vec<AdviceMutation>>,
328    ) -> Result<Self, Report> {
329        let args = stack_inputs.iter().copied().rev().collect::<Vec<_>>();
330
331        // Create debug executor with event replay
332        let mut executor = Executor::new(args);
333        executor.with_advice_inputs(advice_inputs);
334        let debug_executor = executor.into_debug_with_replay(
335            &program,
336            source_manager.clone(),
337            mast_forests,
338            clone_event_replay_queue(&event_replay),
339        );
340
341        Ok(Self::new_local(
342            source_manager,
343            Box::default(),
344            DebugMode::Transaction,
345            LocalState {
346                executor: debug_executor,
347                execution_failed: None,
348            },
349        ))
350    }
351
352    pub fn reload(&mut self) -> Result<(), Report> {
353        if self.debug_mode == DebugMode::Transaction {
354            return Err(Report::msg("reload is not supported in transaction debug mode"));
355        }
356        if self.debug_mode == DebugMode::Remote {
357            #[cfg(feature = "dap")]
358            {
359                let source_manager = self.source_manager.clone();
360                let SessionState::Remote(remote) = &mut self.session else {
361                    return Err(Report::msg("no remote debug session"));
362                };
363                let result = remote.client.restart_phase2().map_err(Report::msg)?;
364                match result {
365                    crate::exec::DapStopReason::Restarting => {
366                        remote.reconnect(&source_manager)?;
367                    }
368                    crate::exec::DapStopReason::Stopped(snapshot) => {
369                        // Fallback: server treated it as Phase 1.
370                        remote.refresh_executor(&source_manager, &snapshot);
371                    }
372                    crate::exec::DapStopReason::Terminated => {
373                        return Err(Report::msg("server terminated without restart signal"));
374                    }
375                }
376                self.breakpoints_hit.clear();
377                self.stopped = true;
378                return Ok(());
379            }
380            #[cfg(not(feature = "dap"))]
381            return Err(Report::msg("remote debug mode requires the `dap` feature"));
382        }
383
384        log::debug!("reloading program");
385        let package = load_package(&self.config)?;
386
387        let mut inputs = self.config.inputs.clone().unwrap_or_default();
388        if !self.config.args.is_empty() {
389            // CLI args model sequential pushes, but StackInputs expects the top element first.
390            let args = self.config.args.iter().rev().map(|felt| felt.0).collect::<Vec<_>>();
391            inputs.inputs = StackInputs::new(&args).into_diagnostic()?;
392        }
393        let args = inputs.inputs.iter().copied().collect::<Vec<_>>();
394
395        // Load libraries from link_libraries and sysroot BEFORE resolving dependencies
396        let mut libs = Vec::with_capacity(self.config.link_libraries.len());
397        for link_library in self.config.link_libraries.iter() {
398            let lib = link_library.load(&self.config, self.source_manager.clone())?;
399            libs.push(lib.clone());
400        }
401
402        // Load std and base libraries from sysroot if available
403        if let Some(toolchain_dir) = self.config.toolchain_dir() {
404            libs.extend(load_sysroot_libs(&toolchain_dir)?);
405        }
406
407        // Create executor and register libraries with dependency resolver before resolving
408        let mut executor = Executor::new(args.clone());
409        for lib in libs.iter() {
410            executor.register_library_dependency(lib.clone());
411            executor.with_library(lib.clone());
412        }
413
414        // Now resolve package dependencies
415        let dependencies = package.manifest.dependencies();
416        executor.with_dependencies(dependencies)?;
417        executor.with_advice_inputs(inputs.advice_inputs);
418
419        let program = package.unwrap_program();
420        let executor = executor.into_debug(&program, self.source_manager.clone());
421
422        self.session = SessionState::Local(Box::new(LocalState {
423            executor,
424            execution_failed: None,
425        }));
426        self.breakpoints_hit.clear();
427        let breakpoints = core::mem::take(&mut self.breakpoints);
428        self.breakpoints.reserve(breakpoints.len());
429        self.next_breakpoint_id = 0;
430        self.stopped = true;
431        for bp in breakpoints {
432            self.create_breakpoint(bp.ty);
433        }
434        Ok(())
435    }
436
437    pub fn create_breakpoint(&mut self, ty: BreakpointType) {
438        let id = self.next_breakpoint_id();
439        let creation_cycle = self.executor().cycle;
440        log::trace!("created breakpoint with id {id} at cycle {creation_cycle}");
441        if matches!(ty, BreakpointType::Finish)
442            && let Some(frame) = self.executor_mut().callstack.current_frame_mut()
443        {
444            frame.break_on_exit();
445        }
446        self.breakpoints.push(Breakpoint {
447            id,
448            creation_cycle,
449            ty,
450        });
451    }
452
453    fn next_breakpoint_id(&mut self) -> u8 {
454        let mut candidate = self.next_breakpoint_id;
455        let initial = candidate;
456        let mut next = candidate.wrapping_add(1);
457        loop {
458            assert_ne!(initial, next, "unable to allocate a breakpoint id: too many breakpoints");
459            if self
460                .breakpoints
461                .iter()
462                .chain(self.breakpoints_hit.iter())
463                .any(|bp| bp.id == candidate)
464            {
465                candidate = next;
466                next = candidate.wrapping_add(1);
467                continue;
468            }
469            self.next_breakpoint_id = next;
470            break candidate;
471        }
472    }
473
474    pub fn executor(&self) -> &DebugExecutor {
475        match &self.session {
476            SessionState::Local(local) => &local.executor,
477            #[cfg(feature = "dap")]
478            SessionState::Remote(remote) => &remote.executor,
479        }
480    }
481
482    pub fn executor_mut(&mut self) -> &mut DebugExecutor {
483        match &mut self.session {
484            SessionState::Local(local) => &mut local.executor,
485            #[cfg(feature = "dap")]
486            SessionState::Remote(remote) => &mut remote.executor,
487        }
488    }
489
490    pub fn current_procedure(&self) -> Option<Rc<str>> {
491        let live_proc = self
492            .executor()
493            .current_asmop
494            .as_ref()
495            .map(|op| Rc::from(op.context_name()))
496            .or_else(|| self.executor().current_proc.clone());
497        let frame_proc =
498            self.executor().callstack.current_frame().and_then(|frame| frame.procedure(""));
499        live_proc.or(frame_proc)
500    }
501
502    pub fn current_location(&self) -> Option<ResolvedLocation> {
503        self.executor()
504            .callstack
505            .current_frame()
506            .and_then(|frame| frame.recent().back())
507            .and_then(|detail| detail.resolve(&*self.source_manager))
508            .cloned()
509    }
510
511    pub fn current_display_location(&self) -> Option<ResolvedLocation> {
512        self.executor()
513            .callstack
514            .current_frame()
515            .and_then(|frame| frame.last_resolved(&*self.source_manager))
516            .cloned()
517    }
518
519    pub fn is_next_source_line(
520        start_proc: Option<&str>,
521        start_loc: Option<&ResolvedLocation>,
522        current_proc: Option<&str>,
523        current_loc: Option<&ResolvedLocation>,
524    ) -> bool {
525        let same_proc = match (start_proc, current_proc) {
526            (Some(start), Some(current)) => start == current,
527            (Some(_), None) => false,
528            _ => true,
529        };
530        if !same_proc {
531            return false;
532        }
533
534        match (start_loc, current_loc) {
535            (Some(start), Some(current)) => {
536                start.source_file.uri().as_str() == current.source_file.uri().as_str()
537                    && start.line != current.line
538            }
539            (None, Some(_)) => true,
540            _ => false,
541        }
542    }
543
544    pub fn execution_failed(&self) -> Option<&miden_processor::ExecutionError> {
545        match &self.session {
546            SessionState::Local(local) => local.execution_failed.as_ref(),
547            #[cfg(feature = "dap")]
548            SessionState::Remote(_) => None,
549        }
550    }
551
552    pub fn set_execution_failed(&mut self, error: miden_processor::ExecutionError) {
553        match &mut self.session {
554            SessionState::Local(local) => local.execution_failed = Some(error),
555            #[cfg(feature = "dap")]
556            SessionState::Remote(_) => {
557                panic!("cannot record local execution failure while in remote mode")
558            }
559        }
560    }
561}
562
563macro_rules! write_with_format_type {
564    ($out:ident, $read_expr:ident, $value:expr) => {
565        match $read_expr.format {
566            crate::debug::FormatType::Decimal => write!(&mut $out, "{}", $value).unwrap(),
567            crate::debug::FormatType::Hex => write!(&mut $out, "{:0x}", $value).unwrap(),
568            crate::debug::FormatType::Binary => write!(&mut $out, "{:0b}", $value).unwrap(),
569        }
570    };
571}
572
573impl State {
574    pub fn read_memory(&mut self, expr: &ReadMemoryExpr) -> Result<String, String> {
575        use core::fmt::Write;
576
577        use miden_assembly_syntax::ast::types::Type;
578
579        use crate::debug::FormatType;
580
581        #[cfg(feature = "dap")]
582        if self.debug_mode == DebugMode::Remote {
583            let SessionState::Remote(remote) = &mut self.session else {
584                return Err("no remote debug session".into());
585            };
586            return remote.read_memory(expr);
587        }
588
589        #[cfg(not(feature = "dap"))]
590        if self.debug_mode == DebugMode::Remote {
591            return Err("remote debug mode requires the `dap` feature".into());
592        }
593
594        let executor = self.executor();
595        let cycle = miden_processor::trace::RowIndex::from(executor.cycle);
596        let context = executor.current_context;
597        let memory = executor.processor.memory();
598        let read_element = |addr: u32| -> Option<Felt> {
599            memory.read_element(context, Felt::new(addr as u64)).ok()
600        };
601        let mut output = String::new();
602        if expr.count > 1 {
603            return Err("-count with value > 1 is not yet implemented".into());
604        } else if matches!(expr.ty, Type::Felt) {
605            if !expr.addr.is_element_aligned() {
606                return Err(
607                    "read failed: type 'felt' must be aligned to an element boundary".into()
608                );
609            }
610            let felt = read_element(expr.addr.addr).unwrap_or(Felt::ZERO);
611            write_with_format_type!(output, expr, felt.as_canonical_u64());
612        } else if matches!(
613            expr.ty,
614            Type::Array(ref array_ty) if array_ty.element_type() == &Type::Felt && array_ty.len() == 4
615        ) {
616            if !expr.addr.is_word_aligned() {
617                return Err("read failed: type 'word' must be aligned to a word boundary".into());
618            }
619            let word = memory
620                .read_word(context, Felt::new(expr.addr.addr as u64), cycle)
621                .unwrap_or_default();
622            output.push('[');
623            for (i, elem) in word.iter().enumerate() {
624                if i > 0 {
625                    output.push_str(", ");
626                }
627                write_with_format_type!(output, expr, elem.as_canonical_u64());
628            }
629            output.push(']');
630        } else {
631            if !expr.addr.is_element_aligned() {
632                return Err("invalid read: unaligned reads are not supported yet".into());
633            }
634
635            const U32_MASK: u64 = u32::MAX as u64;
636            let size = expr.ty.size_in_bytes();
637            let size_in_felts = expr.ty.size_in_felts();
638            let mut bytes = Vec::with_capacity(size);
639            let mut needed = size;
640            for i in 0..size_in_felts {
641                let addr = expr.addr.addr.checked_add(i as u32).ok_or_else(|| {
642                    "invalid read: attempted to read beyond end of linear memory".to_string()
643                })?;
644                let elem = read_element(addr).unwrap_or_default();
645                let elem_bytes = ((elem.as_canonical_u64() & U32_MASK) as u32).to_le_bytes();
646                let take = core::cmp::min(needed, 4);
647                bytes.extend(&elem_bytes[..take]);
648                needed -= take;
649            }
650
651            match &expr.ty {
652                Type::I1 => match expr.format {
653                    FormatType::Decimal => write!(&mut output, "{}", bytes[0] != 0).unwrap(),
654                    FormatType::Hex => {
655                        write!(&mut output, "{:#0x}", (bytes[0] != 0) as u8).unwrap()
656                    }
657                    FormatType::Binary => {
658                        write!(&mut output, "{:#0b}", (bytes[0] != 0) as u8).unwrap()
659                    }
660                },
661                Type::I8 => write_with_format_type!(output, expr, bytes[0] as i8),
662                Type::U8 => write_with_format_type!(output, expr, bytes[0]),
663                Type::I16 => {
664                    write_with_format_type!(output, expr, i16::from_le_bytes([bytes[0], bytes[1]]))
665                }
666                Type::U16 => {
667                    write_with_format_type!(output, expr, u16::from_le_bytes([bytes[0], bytes[1]]))
668                }
669                Type::I32 => write_with_format_type!(
670                    output,
671                    expr,
672                    i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
673                ),
674                Type::U32 => write_with_format_type!(
675                    output,
676                    expr,
677                    u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
678                ),
679                ty @ (Type::I64 | Type::U64) => {
680                    let val = u64::from_le_bytes(bytes[..8].try_into().unwrap());
681                    if matches!(ty, Type::I64) {
682                        write_with_format_type!(output, expr, val as i64)
683                    } else {
684                        write_with_format_type!(output, expr, val)
685                    }
686                }
687                ty => {
688                    return Err(format!(
689                        "support for reads of type '{ty}' are not implemented yet"
690                    ));
691                }
692            }
693        }
694
695        Ok(output)
696    }
697
698    /// Format the current debug variables as a string for display.
699    ///
700    /// When `show_all` is false, compiler-generated locals (named `local0`, `local1`, etc.)
701    /// are hidden. Use `show_all` = true (`:vars all`) to include them.
702    pub fn format_variables(&self, show_all: bool) -> String {
703        use core::fmt::Write;
704
705        let executor = self.executor();
706        let debug_vars = &executor.debug_vars;
707
708        if !debug_vars.has_variables() {
709            return "No debug variables tracked".to_string();
710        }
711
712        let mut output = String::new();
713        let stack = executor.current_stack.clone();
714        let context = executor.current_context;
715
716        // Use live processor state, not the pre-recorded trace, for current-cycle values.
717        let read_mem = |addr: u32| -> Option<Felt> {
718            executor.processor.memory().read_element(context, Felt::new(addr as u64)).ok()
719        };
720
721        let current_source = if show_all {
722            None
723        } else {
724            self.current_display_location()
725        };
726
727        for var_snapshot in debug_vars.current_variables() {
728            let name = var_snapshot.info.name();
729
730            if !show_all && is_compiler_generated_name(name) {
731                continue;
732            }
733
734            if let (Some(current), Some(var_loc)) =
735                (current_source.as_ref(), var_snapshot.info.location())
736                && (var_loc.uri.as_str() != current.source_file.uri().as_str()
737                    || var_loc.line.to_u32() > current.line)
738            {
739                continue;
740            }
741
742            if !output.is_empty() {
743                output.push_str(", ");
744            }
745
746            let location = var_snapshot.info.value_location();
747
748            let value = resolve_variable_value(location, &stack, read_mem, |offset| {
749                // Read FMP from live memory, then compute address as FMP + offset
750                let fmp_addr = miden_core::FMP_ADDR.as_canonical_u64() as u32;
751                let fmp = read_mem(fmp_addr)?;
752                let addr = (fmp.as_canonical_u64() as i64 + offset as i64) as u32;
753                read_mem(addr)
754            });
755
756            match value {
757                Some(felt) => {
758                    write!(&mut output, "{name}={}", felt.as_canonical_u64()).unwrap();
759                }
760                None => {
761                    write!(&mut output, "{name}={location}").unwrap();
762                }
763            }
764        }
765
766        if output.is_empty() {
767            "No source-level variables (use ':vars all' to show compiler locals)".to_string()
768        } else {
769            output
770        }
771    }
772}
773
774/// Returns true if the variable name looks compiler-generated (e.g. "local0", "local12").
775/// Source-level variables have DWARF-derived names like "a", "sum", "_info".
776fn is_compiler_generated_name(name: &str) -> bool {
777    name.strip_prefix("local")
778        .is_some_and(|suffix| !suffix.is_empty() && suffix.chars().all(|c| c.is_ascii_digit()))
779}
780
781// DAP CLIENT MODE
782// ================================================================================================
783
784#[cfg(feature = "dap")]
785impl State {
786    /// Create a new debugger state for remote DAP debugging.
787    ///
788    /// Connects to a DAP server, performs the handshake, and queries the
789    /// initial state to populate the executor fields that the TUI panes read.
790    pub fn new_for_dap(addr: &str) -> Result<Self, Report> {
791        let source_manager: Arc<dyn SourceManager> = Arc::new(DefaultSourceManager::default());
792        let remote = RemoteState::connect(addr, &source_manager)?;
793
794        Ok(Self {
795            source_manager,
796            config: Box::default(),
797            input_mode: InputMode::Normal,
798            breakpoints: vec![],
799            breakpoints_hit: vec![],
800            next_breakpoint_id: 0,
801            stopped: true,
802            debug_mode: DebugMode::Remote,
803            session: SessionState::Remote(Box::new(remote)),
804        })
805    }
806
807    pub fn step_remote(&mut self) -> Result<crate::exec::DapStopReason, Report> {
808        let source_manager = self.source_manager.clone();
809        let SessionState::Remote(remote) = &mut self.session else {
810            return Err(Report::msg("no remote debug session"));
811        };
812        let result = remote.resume(&self.breakpoints).map_err(Report::msg)?;
813
814        self.breakpoints.retain(|bp| !bp.is_one_shot());
815
816        match &result {
817            crate::exec::DapStopReason::Stopped(snapshot) => {
818                remote.refresh_executor(&source_manager, snapshot);
819                self.stopped = true;
820            }
821            crate::exec::DapStopReason::Terminated => {
822                remote.executor.stopped = true;
823                self.stopped = true;
824            }
825            crate::exec::DapStopReason::Restarting => {
826                return Err(Report::msg("unexpected Phase 2 restart signal during step"));
827            }
828        }
829
830        Ok(result)
831    }
832}
833
834/// Convert a server-pushed [`DapUiState`](crate::exec::DapUiState) snapshot into a
835/// [`RemoteSnapshot`] that the TUI executor can consume.
836#[cfg(feature = "dap")]
837fn convert_ui_state(
838    snapshot: &crate::exec::DapUiState,
839    source_manager: &Arc<dyn SourceManager>,
840) -> RemoteSnapshot {
841    use crate::debug::{CallFrame, CallStack};
842
843    let call_frames: Vec<CallFrame> = snapshot
844        .callstack
845        .iter()
846        .map(|frame| {
847            let resolved = resolve_remote_frame(frame, source_manager);
848            CallFrame::from_remote(Some(frame.name.clone()), resolved)
849        })
850        .collect();
851
852    let current_stack = snapshot.current_stack.iter().copied().map(Felt::new).collect();
853
854    RemoteSnapshot {
855        callstack: CallStack::from_remote_frames(call_frames),
856        current_stack,
857        cycle: snapshot.cycle,
858    }
859}
860
861/// Resolve a remote frame to a [ResolvedLocation] by loading the source file from disk.
862#[cfg(feature = "dap")]
863fn resolve_remote_frame(
864    frame: &crate::exec::DapUiFrame,
865    source_manager: &Arc<dyn SourceManager>,
866) -> Option<crate::debug::ResolvedLocation> {
867    use std::path::Path;
868
869    use miden_debug_types::{SourceManagerExt, SourceSpan};
870
871    let path_str = frame.source_path.as_ref()?;
872    let path = Path::new(path_str);
873    let source_file = source_manager.load_file(path).ok()?;
874    let line = frame.line.max(1) as u32;
875    let col = frame.column.max(1) as u32;
876
877    // Compute a span from the line number — use the byte range of the line
878    let content = source_file.content();
879    let line_index = miden_debug_types::LineIndex::from(line.saturating_sub(1));
880    let range = content.line_range(line_index)?;
881    let span = SourceSpan::new(source_file.id(), range);
882
883    Some(crate::debug::ResolvedLocation {
884        source_file,
885        line,
886        col,
887        span,
888    })
889}
890
891/// Attempts to load the standard library from the sysroot/toolchain directory.
892///
893/// Supports both formats:
894/// - `.masp` (package format) - used by the midenup toolchain
895/// - `.masl` (serialized Library) - legacy format
896///   Load all library files (.masp and .masl) from the sysroot directory.
897///
898/// The toolchain determines what libraries are available in the sysroot.
899fn load_sysroot_libs(
900    toolchain_dir: &std::path::Path,
901) -> Result<Vec<Arc<miden_assembly_syntax::Library>>, Report> {
902    let mut libs = Vec::new();
903
904    let entries = match std::fs::read_dir(toolchain_dir) {
905        Ok(entries) => entries,
906        Err(_) => {
907            log::debug!(target: "state", "could not read sysroot directory: {}", toolchain_dir.display());
908            return Ok(libs);
909        }
910    };
911
912    for entry in entries {
913        let entry = entry.into_diagnostic()?;
914        let path = entry.path();
915        let Some(ext) = path.extension() else {
916            continue;
917        };
918
919        if ext == "masp" {
920            log::debug!(target: "state", "loading library from sysroot: {}", path.display());
921            let bytes = std::fs::read(&path).into_diagnostic()?;
922            let package = miden_mast_package::Package::read_from_bytes(&bytes).map_err(|e| {
923                Report::msg(format!("failed to load package '{}': {e}", path.display()))
924            })?;
925            libs.push(package.mast.clone());
926        } else if ext == "masl" {
927            log::debug!(target: "state", "loading library from sysroot: {}", path.display());
928            let bytes = std::fs::read(&path).into_diagnostic()?;
929            let lib = miden_assembly_syntax::Library::read_from_bytes(&bytes).map_err(|e| {
930                Report::msg(format!("failed to load library '{}': {e}", path.display()))
931            })?;
932            libs.push(Arc::new(lib));
933        }
934    }
935
936    if libs.is_empty() {
937        log::debug!(target: "state", "no libraries found in sysroot: {}", toolchain_dir.display());
938    }
939
940    Ok(libs)
941}
942
943fn load_package(config: &DebuggerConfig) -> Result<Arc<miden_mast_package::Package>, Report> {
944    let input = config.input.as_ref().ok_or_else(|| Report::msg("no input file specified"))?;
945    let package = match input {
946        InputFile::Real(path) => {
947            let bytes = std::fs::read(path).into_diagnostic()?;
948            miden_mast_package::Package::read_from_bytes(&bytes)
949                .map(Arc::new)
950                .map_err(|e| {
951                    Report::msg(format!(
952                        "failed to load Miden package from {}: {e}",
953                        path.display()
954                    ))
955                })?
956        }
957        InputFile::Stdin(bytes) => miden_mast_package::Package::read_from_bytes(bytes)
958            .map(Arc::new)
959            .map_err(|e| Report::msg(format!("failed to load Miden package from stdin: {e}")))?,
960    };
961
962    if let Some(entry) = config.entrypoint.as_ref() {
963        // Input must be a library, not a program
964        let id = entry
965            .parse::<miden_assembly::ast::QualifiedProcedureName>()
966            .map_err(|_| Report::msg(format!("invalid function identifier: '{entry}'")))?;
967        if !package.is_library() {
968            return Err(Report::msg("cannot use --entrypoint with executable packages"));
969        }
970
971        package.make_executable(&id).map(Arc::new)
972    } else {
973        Ok(package)
974    }
975}