Skip to main content

miden_debug/ui/
state.rs

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