Skip to main content

miden_debug/ui/
state.rs

1use std::{collections::VecDeque, 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},
15    exec::{DebugExecutor, ExecutionTrace, 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_trace: ExecutionTrace,
78    execution_failed: Option<miden_processor::ExecutionError>,
79}
80
81#[cfg(feature = "dap")]
82struct RemoteState {
83    client: crate::exec::DapClient,
84    executor: DebugExecutor,
85    addr: String,
86    /// Tracks which source files have had breakpoints synced to the DAP server,
87    /// so we can send empty breakpoint lists when all breakpoints for a file are removed.
88    synced_bp_files: std::collections::BTreeSet<String>,
89}
90
91enum SessionState {
92    Local(Box<LocalState>),
93    #[cfg(feature = "dap")]
94    Remote(Box<RemoteState>),
95}
96
97#[cfg(feature = "dap")]
98struct RemoteSnapshot {
99    callstack: crate::debug::CallStack,
100    current_stack: Vec<Felt>,
101    cycle: usize,
102}
103
104#[cfg(feature = "dap")]
105impl RemoteState {
106    fn connect(addr: &str, source_manager: &Arc<dyn SourceManager>) -> Result<Self, Report> {
107        use std::collections::BTreeSet;
108
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 processor = FastProcessor::new(StackInputs::default());
118        let host = DebuggerHost::new(source_manager.clone());
119        let executor = DebugExecutor {
120            processor,
121            host,
122            resume_ctx: None,
123            current_stack: snapshot.current_stack,
124            current_op: None,
125            current_asmop: None,
126            stack_outputs: Default::default(),
127            contexts: BTreeSet::new(),
128            root_context: ContextId::root(),
129            current_context: ContextId::root(),
130            callstack: snapshot.callstack,
131            recent: VecDeque::new(),
132            cycle: snapshot.cycle,
133            stopped: false,
134        };
135
136        Ok(Self {
137            client,
138            executor,
139            addr: addr.to_string(),
140            synced_bp_files: std::collections::BTreeSet::new(),
141        })
142    }
143
144    fn read_memory(&mut self, expr: &ReadMemoryExpr) -> Result<String, String> {
145        self.client.read_memory(expr)
146    }
147
148    fn sync_breakpoints(&mut self, breakpoints: &[Breakpoint]) {
149        use std::collections::BTreeMap;
150
151        // Group Line breakpoints by their file pattern string.
152        let mut by_file: BTreeMap<String, Vec<i64>> = BTreeMap::new();
153        // Collect Called and File patterns as function breakpoints.
154        let mut func_names: Vec<String> = Vec::new();
155
156        for bp in breakpoints {
157            match &bp.ty {
158                BreakpointType::Line { pattern, line } => {
159                    by_file.entry(pattern.as_str().to_string()).or_default().push(*line as i64);
160                }
161                BreakpointType::Called(pattern) | BreakpointType::File(pattern) => {
162                    func_names.push(pattern.as_str().to_string());
163                }
164                _ => {}
165            }
166        }
167
168        // Send empty breakpoint lists for files that were previously synced but no longer have
169        // breakpoints.
170        let stale_files: Vec<String> = self
171            .synced_bp_files
172            .iter()
173            .filter(|f| !by_file.contains_key(f.as_str()))
174            .cloned()
175            .collect();
176        for file in &stale_files {
177            let _ = self.client.set_breakpoints(file, &[]);
178        }
179
180        // Send breakpoints for each file.
181        for (file, lines) in &by_file {
182            let _ = self.client.set_breakpoints(file, lines);
183        }
184
185        // Send function/pattern breakpoints (replaces the full set each time).
186        let _ = self.client.set_function_breakpoints(&func_names);
187
188        // Update tracked set.
189        self.synced_bp_files = by_file.into_keys().collect();
190    }
191
192    fn resume(&mut self, breakpoints: &[Breakpoint]) -> Result<crate::exec::DapStopReason, String> {
193        // Sync user-defined breakpoints to the DAP server before choosing a step command.
194        self.sync_breakpoints(breakpoints);
195
196        let has_step = breakpoints.iter().any(|bp| matches!(bp.ty, BreakpointType::Step));
197        let has_next = breakpoints.iter().any(|bp| matches!(bp.ty, BreakpointType::Next));
198        let has_finish = breakpoints.iter().any(|bp| matches!(bp.ty, BreakpointType::Finish));
199
200        if has_step {
201            self.client.step_in()
202        } else if has_next {
203            self.client.step_over()
204        } else if has_finish {
205            self.client.step_out()
206        } else {
207            self.client.continue_()
208        }
209    }
210
211    fn refresh_executor(
212        &mut self,
213        source_manager: &Arc<dyn SourceManager>,
214        pushed: &crate::exec::DapUiState,
215    ) {
216        // Standard DAP `stopped` events tell us execution paused, but do not
217        // carry the refreshed VM state (stack, callstack, cycle). The server
218        // pushes a custom `miden/uiState` event with the bundled snapshot
219        // immediately before each `stopped` event, so we consume that here
220        // instead of issuing an extra evaluate round-trip.
221        let snapshot = convert_ui_state(pushed, source_manager);
222        self.executor.current_stack = snapshot.current_stack;
223        self.executor.callstack = snapshot.callstack;
224        self.executor.cycle = snapshot.cycle;
225    }
226
227    fn reconnect(&mut self, source_manager: &Arc<dyn SourceManager>) -> Result<(), Report> {
228        let timeout = std::time::Duration::from_secs(30);
229        let mut new_client =
230            crate::exec::DapClient::connect_with_retry(&self.addr, timeout).map_err(Report::msg)?;
231        let ui_state = new_client.handshake().map_err(Report::msg)?;
232        let snapshot = convert_ui_state(&ui_state, source_manager);
233
234        self.client = new_client;
235        self.executor.current_stack = snapshot.current_stack;
236        self.executor.callstack = snapshot.callstack;
237        self.executor.cycle = snapshot.cycle;
238        Ok(())
239    }
240}
241
242impl State {
243    fn new_local(
244        source_manager: Arc<dyn SourceManager>,
245        config: Box<DebuggerConfig>,
246        debug_mode: DebugMode,
247        local: LocalState,
248    ) -> Self {
249        Self {
250            source_manager,
251            config,
252            input_mode: InputMode::Normal,
253            breakpoints: vec![],
254            breakpoints_hit: vec![],
255            next_breakpoint_id: 0,
256            stopped: true,
257            debug_mode,
258            session: SessionState::Local(Box::new(local)),
259        }
260    }
261
262    pub fn new(config: Box<DebuggerConfig>) -> Result<Self, Report> {
263        let source_manager = Arc::new(DefaultSourceManager::default());
264        let mut inputs = config.inputs.clone().unwrap_or_default();
265        if !config.args.is_empty() {
266            // CLI args model sequential pushes, but StackInputs expects the top element first.
267            let args = config.args.iter().rev().map(|felt| felt.0).collect::<Vec<_>>();
268            inputs.inputs = StackInputs::new(&args).into_diagnostic()?;
269        }
270        let args = inputs.inputs.iter().copied().collect::<Vec<_>>();
271        let package = load_package(&config)?;
272
273        // Load libraries from link_libraries and sysroot BEFORE resolving dependencies
274        let mut libs = Vec::with_capacity(config.link_libraries.len());
275        for link_library in config.link_libraries.iter() {
276            log::debug!(target: "state", "loading link library {}", link_library.name());
277            let lib = link_library.load(&config, source_manager.clone())?;
278            libs.push(lib.clone());
279        }
280
281        // Load std and base libraries from sysroot if available
282        if let Some(toolchain_dir) = config.toolchain_dir() {
283            libs.extend(load_sysroot_libs(&toolchain_dir)?);
284        }
285
286        // Create executor and register libraries with dependency resolver before resolving
287        let mut executor = Executor::new(args.clone());
288        for lib in libs.iter() {
289            executor.register_library_dependency(lib.clone());
290            executor.with_library(lib.clone());
291        }
292
293        // Now resolve package dependencies (they should find the registered libraries)
294        let dependencies = package.manifest.dependencies();
295        executor.with_dependencies(dependencies)?;
296        executor.with_advice_inputs(inputs.advice_inputs.clone());
297
298        let program = package.unwrap_program();
299        let executor = executor.into_debug(&program, source_manager.clone());
300
301        // Execute the program until it terminates to capture a full trace for use during debugging
302        let mut trace_executor = Executor::new(args);
303        for lib in libs.iter() {
304            trace_executor.register_library_dependency(lib.clone());
305            trace_executor.with_library(lib.clone());
306        }
307        let dependencies = package.manifest.dependencies();
308        trace_executor.with_dependencies(dependencies)?;
309        trace_executor.with_advice_inputs(inputs.advice_inputs.clone());
310
311        let execution_trace = trace_executor.capture_trace(&program, source_manager.clone());
312
313        Ok(Self::new_local(
314            source_manager,
315            config,
316            DebugMode::Program,
317            LocalState {
318                executor,
319                execution_trace,
320                execution_failed: None,
321            },
322        ))
323    }
324
325    /// Create a new debugger state for transaction debugging.
326    ///
327    /// This uses pre-recorded event mutations to replay host events during
328    /// step-by-step debugging, since the debugger's host doesn't have access
329    /// to the real transaction host.
330    pub fn new_for_transaction(
331        program: Arc<Program>,
332        stack_inputs: StackInputs,
333        advice_inputs: AdviceInputs,
334        source_manager: Arc<dyn SourceManager>,
335        mast_forests: Vec<Arc<MastForest>>,
336        event_replay: Vec<Vec<AdviceMutation>>,
337    ) -> Result<Self, Report> {
338        let args = stack_inputs.iter().copied().rev().collect::<Vec<_>>();
339
340        // Create debug executor with event replay
341        let mut executor = Executor::new(args.clone());
342        executor.with_advice_inputs(advice_inputs.clone());
343        let debug_executor = executor.into_debug_with_replay(
344            &program,
345            source_manager.clone(),
346            mast_forests.clone(),
347            clone_event_replay_queue(&event_replay),
348        );
349
350        // Create trace executor with a cloned replay queue
351        let mut trace_executor = Executor::new(args);
352        trace_executor.with_advice_inputs(advice_inputs);
353        let trace_debug = trace_executor.into_debug_with_replay(
354            &program,
355            source_manager.clone(),
356            mast_forests,
357            clone_event_replay_queue(&event_replay),
358        );
359
360        // Run trace executor to completion to capture execution trace
361        let execution_trace = run_to_trace(trace_debug);
362
363        Ok(Self::new_local(
364            source_manager,
365            Box::default(),
366            DebugMode::Transaction,
367            LocalState {
368                executor: debug_executor,
369                execution_trace,
370                execution_failed: None,
371            },
372        ))
373    }
374
375    pub fn reload(&mut self) -> Result<(), Report> {
376        if self.debug_mode == DebugMode::Transaction {
377            return Err(Report::msg("reload is not supported in transaction debug mode"));
378        }
379        if self.debug_mode == DebugMode::Remote {
380            #[cfg(feature = "dap")]
381            {
382                let source_manager = self.source_manager.clone();
383                let SessionState::Remote(remote) = &mut self.session else {
384                    return Err(Report::msg("no remote debug session"));
385                };
386                let result = remote.client.restart_phase2().map_err(Report::msg)?;
387                match result {
388                    crate::exec::DapStopReason::Restarting => {
389                        remote.reconnect(&source_manager)?;
390                    }
391                    crate::exec::DapStopReason::Stopped(snapshot) => {
392                        // Fallback: server treated it as Phase 1.
393                        remote.refresh_executor(&source_manager, &snapshot);
394                    }
395                    crate::exec::DapStopReason::Terminated => {
396                        return Err(Report::msg("server terminated without restart signal"));
397                    }
398                }
399                self.breakpoints_hit.clear();
400                self.stopped = true;
401                return Ok(());
402            }
403            #[cfg(not(feature = "dap"))]
404            return Err(Report::msg("remote debug mode requires the `dap` feature"));
405        }
406
407        log::debug!("reloading program");
408        let package = load_package(&self.config)?;
409
410        let mut inputs = self.config.inputs.clone().unwrap_or_default();
411        if !self.config.args.is_empty() {
412            // CLI args model sequential pushes, but StackInputs expects the top element first.
413            let args = self.config.args.iter().rev().map(|felt| felt.0).collect::<Vec<_>>();
414            inputs.inputs = StackInputs::new(&args).into_diagnostic()?;
415        }
416        let args = inputs.inputs.iter().copied().collect::<Vec<_>>();
417
418        // Load libraries from link_libraries and sysroot BEFORE resolving dependencies
419        let mut libs = Vec::with_capacity(self.config.link_libraries.len());
420        for link_library in self.config.link_libraries.iter() {
421            let lib = link_library.load(&self.config, self.source_manager.clone())?;
422            libs.push(lib.clone());
423        }
424
425        // Load std and base libraries from sysroot if available
426        if let Some(toolchain_dir) = self.config.toolchain_dir() {
427            libs.extend(load_sysroot_libs(&toolchain_dir)?);
428        }
429
430        // Create executor and register libraries with dependency resolver before resolving
431        let mut executor = Executor::new(args.clone());
432        for lib in libs.iter() {
433            executor.register_library_dependency(lib.clone());
434            executor.with_library(lib.clone());
435        }
436
437        // Now resolve package dependencies
438        let dependencies = package.manifest.dependencies();
439        executor.with_dependencies(dependencies)?;
440        executor.with_advice_inputs(inputs.advice_inputs.clone());
441
442        let program = package.unwrap_program();
443        let executor = executor.into_debug(&program, self.source_manager.clone());
444
445        // Execute the program until it terminates to capture a full trace for use during debugging
446        let mut trace_executor = Executor::new(args);
447        for lib in libs.iter() {
448            trace_executor.register_library_dependency(lib.clone());
449            trace_executor.with_library(lib.clone());
450        }
451        let dependencies = package.manifest.dependencies();
452        trace_executor.with_dependencies(dependencies)?;
453        trace_executor.with_advice_inputs(core::mem::take(&mut inputs.advice_inputs));
454        let execution_trace = trace_executor.capture_trace(&program, self.source_manager.clone());
455
456        self.session = SessionState::Local(Box::new(LocalState {
457            executor,
458            execution_trace,
459            execution_failed: None,
460        }));
461        self.breakpoints_hit.clear();
462        let breakpoints = core::mem::take(&mut self.breakpoints);
463        self.breakpoints.reserve(breakpoints.len());
464        self.next_breakpoint_id = 0;
465        self.stopped = true;
466        for bp in breakpoints {
467            self.create_breakpoint(bp.ty);
468        }
469        Ok(())
470    }
471
472    pub fn create_breakpoint(&mut self, ty: BreakpointType) {
473        let id = self.next_breakpoint_id();
474        let creation_cycle = self.executor().cycle;
475        log::trace!("created breakpoint with id {id} at cycle {creation_cycle}");
476        if matches!(ty, BreakpointType::Finish)
477            && let Some(frame) = self.executor_mut().callstack.current_frame_mut()
478        {
479            frame.break_on_exit();
480        }
481        self.breakpoints.push(Breakpoint {
482            id,
483            creation_cycle,
484            ty,
485        });
486    }
487
488    fn next_breakpoint_id(&mut self) -> u8 {
489        let mut candidate = self.next_breakpoint_id;
490        let initial = candidate;
491        let mut next = candidate.wrapping_add(1);
492        loop {
493            assert_ne!(initial, next, "unable to allocate a breakpoint id: too many breakpoints");
494            if self
495                .breakpoints
496                .iter()
497                .chain(self.breakpoints_hit.iter())
498                .any(|bp| bp.id == candidate)
499            {
500                candidate = next;
501                next = candidate.wrapping_add(1);
502                continue;
503            }
504            self.next_breakpoint_id = next;
505            break candidate;
506        }
507    }
508
509    pub fn executor(&self) -> &DebugExecutor {
510        match &self.session {
511            SessionState::Local(local) => &local.executor,
512            #[cfg(feature = "dap")]
513            SessionState::Remote(remote) => &remote.executor,
514        }
515    }
516
517    pub fn executor_mut(&mut self) -> &mut DebugExecutor {
518        match &mut self.session {
519            SessionState::Local(local) => &mut local.executor,
520            #[cfg(feature = "dap")]
521            SessionState::Remote(remote) => &mut remote.executor,
522        }
523    }
524
525    pub fn execution_failed(&self) -> Option<&miden_processor::ExecutionError> {
526        match &self.session {
527            SessionState::Local(local) => local.execution_failed.as_ref(),
528            #[cfg(feature = "dap")]
529            SessionState::Remote(_) => None,
530        }
531    }
532
533    pub fn set_execution_failed(&mut self, error: miden_processor::ExecutionError) {
534        match &mut self.session {
535            SessionState::Local(local) => local.execution_failed = Some(error),
536            #[cfg(feature = "dap")]
537            SessionState::Remote(_) => {
538                panic!("cannot record local execution failure while in remote mode")
539            }
540        }
541    }
542
543    fn local_session(&self) -> &LocalState {
544        match &self.session {
545            SessionState::Local(local) => local,
546            #[cfg(feature = "dap")]
547            SessionState::Remote(_) => panic!("local session requested while in remote mode"),
548        }
549    }
550}
551
552macro_rules! write_with_format_type {
553    ($out:ident, $read_expr:ident, $value:expr) => {
554        match $read_expr.format {
555            crate::debug::FormatType::Decimal => write!(&mut $out, "{}", $value).unwrap(),
556            crate::debug::FormatType::Hex => write!(&mut $out, "{:0x}", $value).unwrap(),
557            crate::debug::FormatType::Binary => write!(&mut $out, "{:0b}", $value).unwrap(),
558        }
559    };
560}
561
562impl State {
563    pub fn read_memory(&mut self, expr: &ReadMemoryExpr) -> Result<String, String> {
564        use core::fmt::Write;
565
566        use miden_assembly_syntax::ast::types::Type;
567
568        use crate::debug::FormatType;
569
570        #[cfg(feature = "dap")]
571        if self.debug_mode == DebugMode::Remote {
572            let SessionState::Remote(remote) = &mut self.session else {
573                return Err("no remote debug session".into());
574            };
575            return remote.read_memory(expr);
576        }
577
578        #[cfg(not(feature = "dap"))]
579        if self.debug_mode == DebugMode::Remote {
580            return Err("remote debug mode requires the `dap` feature".into());
581        }
582
583        let cycle = miden_processor::trace::RowIndex::from(self.executor().cycle);
584        let context = self.executor().current_context;
585        let local = self.local_session();
586        let mut output = String::new();
587        if expr.count > 1 {
588            return Err("-count with value > 1 is not yet implemented".into());
589        } else if matches!(expr.ty, Type::Felt) {
590            if !expr.addr.is_element_aligned() {
591                return Err(
592                    "read failed: type 'felt' must be aligned to an element boundary".into()
593                );
594            }
595            let felt = local
596                .execution_trace
597                .read_memory_element_in_context(expr.addr.addr, context, cycle)
598                .unwrap_or(Felt::ZERO);
599            write_with_format_type!(output, expr, felt.as_canonical_u64());
600        } else if matches!(
601            expr.ty,
602            Type::Array(ref array_ty) if array_ty.element_type() == &Type::Felt && array_ty.len() == 4
603        ) {
604            if !expr.addr.is_word_aligned() {
605                return Err("read failed: type 'word' must be aligned to a word boundary".into());
606            }
607            let word = local.execution_trace.read_memory_word(expr.addr.addr).unwrap_or_default();
608            output.push('[');
609            for (i, elem) in word.iter().enumerate() {
610                if i > 0 {
611                    output.push_str(", ");
612                }
613                write_with_format_type!(output, expr, elem.as_canonical_u64());
614            }
615            output.push(']');
616        } else {
617            let bytes = local
618                .execution_trace
619                .read_bytes_for_type(expr.addr, &expr.ty, context, cycle)
620                .map_err(|err| format!("invalid read: {err}"))?;
621            match &expr.ty {
622                Type::I1 => match expr.format {
623                    FormatType::Decimal => write!(&mut output, "{}", bytes[0] != 0).unwrap(),
624                    FormatType::Hex => {
625                        write!(&mut output, "{:#0x}", (bytes[0] != 0) as u8).unwrap()
626                    }
627                    FormatType::Binary => {
628                        write!(&mut output, "{:#0b}", (bytes[0] != 0) as u8).unwrap()
629                    }
630                },
631                Type::I8 => write_with_format_type!(output, expr, bytes[0] as i8),
632                Type::U8 => write_with_format_type!(output, expr, bytes[0]),
633                Type::I16 => {
634                    write_with_format_type!(output, expr, i16::from_le_bytes([bytes[0], bytes[1]]))
635                }
636                Type::U16 => {
637                    write_with_format_type!(output, expr, u16::from_le_bytes([bytes[0], bytes[1]]))
638                }
639                Type::I32 => write_with_format_type!(
640                    output,
641                    expr,
642                    i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
643                ),
644                Type::U32 => write_with_format_type!(
645                    output,
646                    expr,
647                    u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
648                ),
649                ty @ (Type::I64 | Type::U64) => {
650                    let val = u64::from_le_bytes(bytes[..8].try_into().unwrap());
651                    if matches!(ty, Type::I64) {
652                        write_with_format_type!(output, expr, val as i64)
653                    } else {
654                        write_with_format_type!(output, expr, val)
655                    }
656                }
657                ty => {
658                    return Err(format!(
659                        "support for reads of type '{ty}' are not implemented yet"
660                    ));
661                }
662            }
663        }
664
665        Ok(output)
666    }
667}
668
669// DAP CLIENT MODE
670// ================================================================================================
671
672#[cfg(feature = "dap")]
673impl State {
674    /// Create a new debugger state for remote DAP debugging.
675    ///
676    /// Connects to a DAP server, performs the handshake, and queries the
677    /// initial state to populate the executor fields that the TUI panes read.
678    pub fn new_for_dap(addr: &str) -> Result<Self, Report> {
679        let source_manager: Arc<dyn SourceManager> = Arc::new(DefaultSourceManager::default());
680        let remote = RemoteState::connect(addr, &source_manager)?;
681
682        Ok(Self {
683            source_manager,
684            config: Box::default(),
685            input_mode: InputMode::Normal,
686            breakpoints: vec![],
687            breakpoints_hit: vec![],
688            next_breakpoint_id: 0,
689            stopped: true,
690            debug_mode: DebugMode::Remote,
691            session: SessionState::Remote(Box::new(remote)),
692        })
693    }
694
695    pub fn step_remote(&mut self) -> Result<crate::exec::DapStopReason, Report> {
696        let source_manager = self.source_manager.clone();
697        let SessionState::Remote(remote) = &mut self.session else {
698            return Err(Report::msg("no remote debug session"));
699        };
700        let result = remote.resume(&self.breakpoints).map_err(Report::msg)?;
701
702        self.breakpoints.retain(|bp| !bp.is_one_shot());
703
704        match &result {
705            crate::exec::DapStopReason::Stopped(snapshot) => {
706                remote.refresh_executor(&source_manager, snapshot);
707                self.stopped = true;
708            }
709            crate::exec::DapStopReason::Terminated => {
710                remote.executor.stopped = true;
711                self.stopped = true;
712            }
713            crate::exec::DapStopReason::Restarting => {
714                return Err(Report::msg("unexpected Phase 2 restart signal during step"));
715            }
716        }
717
718        Ok(result)
719    }
720}
721
722/// Convert a server-pushed [`DapUiState`](crate::exec::DapUiState) snapshot into a
723/// [`RemoteSnapshot`] that the TUI executor can consume.
724#[cfg(feature = "dap")]
725fn convert_ui_state(
726    snapshot: &crate::exec::DapUiState,
727    source_manager: &Arc<dyn SourceManager>,
728) -> RemoteSnapshot {
729    use crate::debug::{CallFrame, CallStack};
730
731    let call_frames: Vec<CallFrame> = snapshot
732        .callstack
733        .iter()
734        .map(|frame| {
735            let resolved = resolve_remote_frame(frame, source_manager);
736            CallFrame::from_remote(Some(frame.name.clone()), resolved)
737        })
738        .collect();
739
740    let current_stack = snapshot.current_stack.iter().copied().map(Felt::new).collect();
741
742    RemoteSnapshot {
743        callstack: CallStack::from_remote_frames(call_frames),
744        current_stack,
745        cycle: snapshot.cycle,
746    }
747}
748
749/// Resolve a remote frame to a [ResolvedLocation] by loading the source file from disk.
750#[cfg(feature = "dap")]
751fn resolve_remote_frame(
752    frame: &crate::exec::DapUiFrame,
753    source_manager: &Arc<dyn SourceManager>,
754) -> Option<crate::debug::ResolvedLocation> {
755    use std::path::Path;
756
757    use miden_debug_types::{SourceManagerExt, SourceSpan};
758
759    let path_str = frame.source_path.as_ref()?;
760    let path = Path::new(path_str);
761    let source_file = source_manager.load_file(path).ok()?;
762    let line = frame.line.max(1) as u32;
763    let col = frame.column.max(1) as u32;
764
765    // Compute a span from the line number — use the byte range of the line
766    let content = source_file.content();
767    let line_index = miden_debug_types::LineIndex::from(line.saturating_sub(1));
768    let range = content.line_range(line_index)?;
769    let span = SourceSpan::new(source_file.id(), range);
770
771    Some(crate::debug::ResolvedLocation {
772        source_file,
773        line,
774        col,
775        span,
776    })
777}
778
779/// Attempts to load the standard library from the sysroot/toolchain directory.
780///
781/// Supports both formats:
782/// - `.masp` (package format) - used by the midenup toolchain
783/// - `.masl` (serialized Library) - legacy format
784///   Load all library files (.masp and .masl) from the sysroot directory.
785///
786/// The toolchain determines what libraries are available in the sysroot.
787fn load_sysroot_libs(
788    toolchain_dir: &std::path::Path,
789) -> Result<Vec<Arc<miden_assembly_syntax::Library>>, Report> {
790    let mut libs = Vec::new();
791
792    let entries = match std::fs::read_dir(toolchain_dir) {
793        Ok(entries) => entries,
794        Err(_) => {
795            log::debug!(target: "state", "could not read sysroot directory: {}", toolchain_dir.display());
796            return Ok(libs);
797        }
798    };
799
800    for entry in entries {
801        let entry = entry.into_diagnostic()?;
802        let path = entry.path();
803        let Some(ext) = path.extension() else {
804            continue;
805        };
806
807        if ext == "masp" {
808            log::debug!(target: "state", "loading library from sysroot: {}", path.display());
809            let bytes = std::fs::read(&path).into_diagnostic()?;
810            let package = miden_mast_package::Package::read_from_bytes(&bytes).map_err(|e| {
811                Report::msg(format!("failed to load package '{}': {e}", path.display()))
812            })?;
813            libs.push(package.mast.clone());
814        } else if ext == "masl" {
815            log::debug!(target: "state", "loading library from sysroot: {}", path.display());
816            let bytes = std::fs::read(&path).into_diagnostic()?;
817            let lib = miden_assembly_syntax::Library::read_from_bytes(&bytes).map_err(|e| {
818                Report::msg(format!("failed to load library '{}': {e}", path.display()))
819            })?;
820            libs.push(Arc::new(lib));
821        }
822    }
823
824    if libs.is_empty() {
825        log::debug!(target: "state", "no libraries found in sysroot: {}", toolchain_dir.display());
826    }
827
828    Ok(libs)
829}
830
831/// Run a [DebugExecutor] to completion and return the [ExecutionTrace].
832fn run_to_trace(mut executor: DebugExecutor) -> ExecutionTrace {
833    loop {
834        if executor.stopped {
835            break;
836        }
837        match executor.step() {
838            Ok(_) => continue,
839            Err(_) => break,
840        }
841    }
842    executor.into_execution_trace()
843}
844
845fn load_package(config: &DebuggerConfig) -> Result<Arc<miden_mast_package::Package>, Report> {
846    let input = config.input.as_ref().ok_or_else(|| Report::msg("no input file specified"))?;
847    let package = match input {
848        InputFile::Real(path) => {
849            let bytes = std::fs::read(path).into_diagnostic()?;
850            miden_mast_package::Package::read_from_bytes(&bytes)
851                .map(Arc::new)
852                .map_err(|e| {
853                    Report::msg(format!(
854                        "failed to load Miden package from {}: {e}",
855                        path.display()
856                    ))
857                })?
858        }
859        InputFile::Stdin(bytes) => miden_mast_package::Package::read_from_bytes(bytes)
860            .map(Arc::new)
861            .map_err(|e| Report::msg(format!("failed to load Miden package from stdin: {e}")))?,
862    };
863
864    if let Some(entry) = config.entrypoint.as_ref() {
865        // Input must be a library, not a program
866        let id = entry
867            .parse::<miden_assembly::ast::QualifiedProcedureName>()
868            .map_err(|_| Report::msg(format!("invalid function identifier: '{entry}'")))?;
869        if !package.is_library() {
870            return Err(Report::msg("cannot use --entrypoint with executable packages"));
871        }
872
873        package.make_executable(&id).map(Arc::new)
874    } else {
875        Ok(package)
876    }
877}