Skip to main content

soldb_repl/
lib.rs

1//! The interactive debugger's command parser and state machine.
2//!
3//! [`DebuggerCommand::parse`] turns a typed line into a command, and
4//! [`DebuggerState::apply_command`] applies the ones that move the debugger or change its
5//! breakpoints, returning a [`StepOutcome`] describing what happened.
6//!
7//! This crate performs no I/O: it neither reads stdin nor prints. The frontend owns the
8//! terminal and renders outcomes, which is what makes stepping, breakpoints, and command
9//! parsing testable without a terminal or a node. Commands that only display something
10//! (`vars`, `backtrace`, `list`, `info`) return `None` from `apply_command`; the frontend
11//! reads the data it needs through the state's accessors and formats it.
12//!
13//! The trace is a complete recording, so every movement is a search over it, forward or
14//! backward, and a breakpoint is a predicate on a step: a program counter, the start of
15//! a source line, the entry of a function, a storage write, a revert, a call, an opcode.
16//! Source-level movement comes from [`soldb_debugger::StepMap`], which every frontend
17//! shares, so `next` means the same thing in the terminal and in an editor.
18
19use std::cell::RefCell;
20
21use soldb_core::{ExecutionCall, TraceStep, TransactionTrace};
22use soldb_debugger::{
23    call_target, normalize_address, ChainStorage, Condition, ConditionContext, ContractDebugInfo,
24    Evaluation, Frame, FrameState, ResolvedFunction, ResolvedLine, SourceListing, StepLocation,
25    StepMap, StorageLayout, StorageTape, StorageWords,
26};
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum DisplayMode {
30    Source,
31    Assembly,
32}
33
34impl DisplayMode {
35    pub fn parse(input: &str) -> Option<Self> {
36        match input.trim().to_ascii_lowercase().as_str() {
37            "source" | "src" => Some(Self::Source),
38            "asm" | "assembly" => Some(Self::Assembly),
39            _ => None,
40        }
41    }
42
43    pub fn as_str(self) -> &'static str {
44        match self {
45            Self::Source => "source",
46            Self::Assembly => "asm",
47        }
48    }
49}
50
51/// What the user asked to break on, before it is resolved against the trace.
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub enum BreakpointTarget {
54    Pc(u64),
55    SourceLine(SourceBreakpointTarget),
56    /// A function by name, optionally qualified as `Contract.function`.
57    Function(String),
58    /// A write to one storage slot, given in decimal or hex.
59    Storage(String),
60    /// A write to a state variable, named the way `print` names it: `counter`,
61    /// `balances[0xabc…]`, `config.limit`.
62    State(String),
63    /// Any `REVERT`, or any step the backend marked as failing.
64    Revert,
65    /// A call, to one address or to any.
66    Call(Option<String>),
67    /// Every execution of one opcode.
68    Opcode(String),
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct SourceBreakpointTarget {
73    pub file: Option<String>,
74    pub line: u64,
75}
76
77/// A breakpoint as set, with a stable number the user can delete it by.
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct Breakpoint {
80    pub id: u32,
81    pub kind: BreakpointKind,
82    /// Stops only when this holds at a step the kind matched.
83    pub condition: Option<Condition>,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub enum BreakpointKind {
88    Pc(u64),
89    /// Hits when execution enters one of these lines.
90    Line(Vec<ResolvedLine>),
91    /// Hits when execution enters one of these functions.
92    Function(Vec<ResolvedFunction>),
93    /// Hits on an `SSTORE` to this slot, normalized to lowercase hex without leading
94    /// zeros.
95    Storage(String),
96    /// The same, for a slot named as a state variable: the path the user wrote and the
97    /// slot the storage layout put it at.
98    StateWrite {
99        path: String,
100        slot: String,
101    },
102    Revert,
103    /// Hits on a call instruction, to this lowercase address or to any.
104    Call(Option<String>),
105    /// Hits on every execution of this uppercase mnemonic.
106    Opcode(String),
107}
108
109impl Breakpoint {
110    /// How the frontend names this breakpoint to the user.
111    #[must_use]
112    pub fn label(&self) -> String {
113        let target = match &self.kind {
114            BreakpointKind::Pc(pc) => format!("PC {pc}"),
115            BreakpointKind::Line(lines) => lines
116                .iter()
117                .map(|line| {
118                    if line.requested_line == line.key.line {
119                        format!("{}:{}", line.path, line.key.line)
120                    } else {
121                        format!(
122                            "{}:{} (the statement containing line {})",
123                            line.path, line.key.line, line.requested_line
124                        )
125                    }
126                })
127                .collect::<Vec<_>>()
128                .join(", "),
129            BreakpointKind::Function(functions) => functions
130                .iter()
131                .map(|function| {
132                    format!(
133                        "function {}.{} at {}:{}",
134                        function.contract_name, function.name, function.path, function.line
135                    )
136                })
137                .collect::<Vec<_>>()
138                .join(", "),
139            BreakpointKind::Storage(slot) => format!("storage slot 0x{slot}"),
140            BreakpointKind::StateWrite { path, slot } => {
141                format!("a write to `{path}` (storage slot 0x{slot})")
142            }
143            BreakpointKind::Revert => "revert".to_owned(),
144            BreakpointKind::Call(Some(address)) => format!("call to {address}"),
145            BreakpointKind::Call(None) => "any call".to_owned(),
146            BreakpointKind::Opcode(mnemonic) => format!("opcode {mnemonic}"),
147        };
148        match &self.condition {
149            Some(condition) => format!("{target} if {}", condition.text()),
150            None => target,
151        }
152    }
153}
154
155#[derive(Debug, Clone, PartialEq, Eq)]
156pub enum DebuggerCommand {
157    Next,
158    NextInstruction,
159    Step,
160    Continue,
161    /// Run until the current frame returns to its caller.
162    Finish,
163    /// Step back to the previous source step.
164    ReverseNext,
165    /// Step back one EVM instruction.
166    ReverseNextInstruction,
167    /// Step back into the previous instruction, the mirror of `step`.
168    ReverseStep,
169    /// Run backward until a breakpoint or the first step.
170    ReverseContinue,
171    /// Run backward to the step that entered the current frame.
172    ReverseFinish,
173    Goto(usize),
174    /// List every source variable ETHDebug reports as live at the current program counter.
175    Vars,
176    /// Print one source variable by name at the current program counter.
177    ///
178    /// An empty name means the user typed `print` with no argument; the frontend reports
179    /// the usage rather than treating it as an unknown command.
180    Print(String),
181    Info(DebuggerInfoCommand),
182    /// The call structure at the current step.
183    Backtrace,
184    /// The source around the current step.
185    List,
186    /// Memory at the current step, optionally one range of it.
187    Memory {
188        offset: Option<u64>,
189        length: Option<u64>,
190    },
191    /// The calldata of the current call frame.
192    Calldata,
193    /// The stack at the current step.
194    Stack,
195    Mode(Option<DisplayMode>),
196    /// Set a breakpoint, optionally one that stops only when a condition holds.
197    Break(BreakpointTarget, Option<String>),
198    Clear(BreakpointTarget),
199    /// Remove a breakpoint by number.
200    Delete(u32),
201    Help(Option<String>),
202    Quit,
203    Empty,
204    Unknown(String),
205}
206
207#[derive(Debug, Clone, PartialEq, Eq)]
208pub enum DebuggerInfoCommand {
209    Resources { json: bool },
210    Breakpoints,
211    Storage,
212}
213
214impl DebuggerCommand {
215    pub fn parse(line: &str) -> Self {
216        let line = line.trim();
217        if line.is_empty() {
218            return Self::Empty;
219        }
220
221        let mut parts = line.split_whitespace();
222        let command = parts.next().unwrap_or_default().to_ascii_lowercase();
223        let rest = parts.collect::<Vec<_>>().join(" ");
224        match command.as_str() {
225            "next" | "n" => Self::Next,
226            "nexti" | "ni" | "stepi" | "si" => Self::NextInstruction,
227            "step" | "s" => Self::Step,
228            "continue" | "c" => Self::Continue,
229            "finish" | "fin" => Self::Finish,
230            "reverse-next" | "rnext" | "rn" => Self::ReverseNext,
231            "reverse-nexti" | "rnexti" | "rni" | "reverse-stepi" | "rsi" | "back" => {
232                Self::ReverseNextInstruction
233            }
234            "reverse-step" | "rstep" | "rs" => Self::ReverseStep,
235            "reverse-continue" | "rcontinue" | "rc" => Self::ReverseContinue,
236            "reverse-finish" | "rfinish" | "rfin" => Self::ReverseFinish,
237            "vars" | "locals" => Self::Vars,
238            "print" | "p" => Self::Print(rest),
239            "backtrace" | "bt" | "where" => Self::Backtrace,
240            "list" | "l" => Self::List,
241            "memory" | "mem" => {
242                parse_memory_command(&rest).unwrap_or_else(|| Self::Unknown(line.to_owned()))
243            }
244            "calldata" => Self::Calldata,
245            "stack" => Self::Stack,
246            "storage" => Self::Info(DebuggerInfoCommand::Storage),
247            "goto" => rest
248                .parse::<usize>()
249                .map(Self::Goto)
250                .unwrap_or_else(|_| Self::Unknown(line.to_owned())),
251            "info" | "i" => parse_info_command(&rest)
252                .map(Self::Info)
253                .unwrap_or_else(|| Self::Unknown(line.to_owned())),
254            "mode" => Self::Mode(
255                (!rest.is_empty())
256                    .then(|| DisplayMode::parse(&rest))
257                    .flatten(),
258            ),
259            "break" | "b" => {
260                // `break <target> if <condition>`: the condition is everything after the
261                // first ` if `, so a target containing `if` in a name still parses.
262                let (target, condition) = split_condition(&rest);
263                parse_breakpoint_target(target)
264                    .map(|target| Self::Break(target, condition))
265                    .unwrap_or_else(|| Self::Unknown(line.to_owned()))
266            }
267            "clear" => parse_breakpoint_target(&rest)
268                .map(Self::Clear)
269                .unwrap_or_else(|| Self::Unknown(line.to_owned())),
270            "delete" | "d" => rest
271                .trim()
272                .trim_start_matches('#')
273                .parse::<u32>()
274                .map(Self::Delete)
275                .unwrap_or_else(|_| Self::Unknown(line.to_owned())),
276            "help" => Self::Help((!rest.is_empty()).then_some(rest)),
277            "exit" | "quit" | "q" => Self::Quit,
278            _ => Self::Unknown(line.to_owned()),
279        }
280    }
281}
282
283#[derive(Debug, Clone, PartialEq, Eq)]
284pub enum StepOutcome {
285    NoTrace,
286    Moved {
287        step: usize,
288        pc: u64,
289        op: String,
290    },
291    BreakpointHit {
292        step: usize,
293        pc: u64,
294        breakpoint: Breakpoint,
295    },
296    AtEnd {
297        step: usize,
298    },
299    /// A backward step was asked for at the first step of the trace.
300    AtStart {
301        step: usize,
302    },
303    InvalidStep {
304        requested: usize,
305        max_step: Option<usize>,
306    },
307    ModeChanged(DisplayMode),
308    BreakpointSet(Breakpoint),
309    BreakpointCleared(Breakpoint),
310    /// No breakpoint matched what the user asked to clear; the text names the target.
311    BreakpointMissing(String),
312    /// The target could not be resolved; the text says why.
313    BreakpointError(String),
314}
315
316#[derive(Debug, Clone, PartialEq, Eq)]
317pub struct DebuggerState {
318    pub current_step: usize,
319    pub display_mode: DisplayMode,
320    breakpoints: Vec<Breakpoint>,
321    next_breakpoint_id: u32,
322    trace: Option<TransactionTrace>,
323    step_map: Option<StepMap>,
324    storage_tape: Option<StorageTape>,
325    /// Something the frontend should tell the user, kept until it does: a breakpoint that
326    /// cannot be hit, or a condition that could not be evaluated. Both are silent traps
327    /// otherwise.
328    note: RefCell<Option<String>>,
329}
330
331impl Default for DebuggerState {
332    fn default() -> Self {
333        Self {
334            current_step: 0,
335            display_mode: DisplayMode::Source,
336            breakpoints: Vec::new(),
337            next_breakpoint_id: 1,
338            trace: None,
339            step_map: None,
340            storage_tape: None,
341            note: RefCell::new(None),
342        }
343    }
344}
345
346impl DebuggerState {
347    pub fn new() -> Self {
348        Self::default()
349    }
350
351    /// Loads a trace and maps its call structure. Source lines arrive with
352    /// [`DebuggerState::attach_debug_info`]; until then stepping is by instruction.
353    pub fn load_trace(&mut self, trace: TransactionTrace) {
354        let map = StepMap::new(&trace, Vec::new());
355        self.storage_tape = Some(StorageTape::new(&trace, &map));
356        self.step_map = Some(map);
357        self.trace = Some(trace);
358        self.current_step = 0;
359    }
360
361    /// Maps every step through the contracts' debug info, enabling source-level stepping,
362    /// line and function breakpoints, and source locations in frames.
363    pub fn attach_debug_info(&mut self, contracts: Vec<ContractDebugInfo>) {
364        if let Some(trace) = &self.trace {
365            let map = StepMap::new(trace, contracts);
366            self.storage_tape = Some(StorageTape::new(trace, &map));
367            self.step_map = Some(map);
368        }
369    }
370
371    /// The storage words known at the current step, in the storage context it runs in.
372    #[must_use]
373    pub fn storage_words(&self) -> Option<StorageWords<'_>> {
374        let map = self.step_map.as_ref()?;
375        Some(self.storage_tape.as_ref()?.at_step(map, self.current_step))
376    }
377
378    /// The same words, with a chain the frontend can read slots the transaction never
379    /// touched from.
380    #[must_use]
381    pub fn storage_words_with_chain<'a>(
382        &'a self,
383        chain: Option<&'a dyn ChainStorage>,
384    ) -> Option<StorageWords<'a>> {
385        Some(self.storage_words()?.with_chain(chain))
386    }
387
388    /// The account whose storage the current step reads and writes.
389    #[must_use]
390    pub fn storage_address(&self) -> Option<&str> {
391        self.step_map.as_ref()?.storage_address(self.current_step)
392    }
393
394    /// The storage layout of the contract executing at the current step, when it was
395    /// compiled with one.
396    #[must_use]
397    pub fn storage_layout(&self) -> Option<&StorageLayout> {
398        self.step_map
399            .as_ref()?
400            .contract_at_step(self.current_step)?
401            .storage_layout
402            .as_ref()
403    }
404
405    pub fn trace(&self) -> Option<&TransactionTrace> {
406        self.trace.as_ref()
407    }
408
409    #[must_use]
410    pub fn step_map(&self) -> Option<&StepMap> {
411        self.step_map.as_ref()
412    }
413
414    /// Whether any step maps to a source line, which is what source-level stepping needs.
415    #[must_use]
416    pub fn has_source(&self) -> bool {
417        self.step_map.as_ref().is_some_and(StepMap::has_source)
418    }
419
420    pub fn current_step_data(&self) -> Option<&TraceStep> {
421        self.trace
422            .as_ref()
423            .and_then(|trace| trace.steps.get(self.current_step))
424    }
425
426    /// The source location of the current step, when it has one.
427    #[must_use]
428    pub fn location(&self) -> Option<StepLocation> {
429        self.step_map.as_ref()?.location(self.current_step)
430    }
431
432    pub fn step_count(&self) -> usize {
433        self.trace
434            .as_ref()
435            .map(|trace| trace.steps.len())
436            .unwrap_or(0)
437    }
438
439    #[must_use]
440    pub fn breakpoints(&self) -> &[Breakpoint] {
441        &self.breakpoints
442    }
443
444    pub fn set_display_mode(&mut self, mode: DisplayMode) -> StepOutcome {
445        self.display_mode = mode;
446        StepOutcome::ModeChanged(mode)
447    }
448
449    pub fn next_instruction(&mut self) -> StepOutcome {
450        let Some(trace) = &self.trace else {
451            return StepOutcome::NoTrace;
452        };
453        if self.current_step >= trace.steps.len().saturating_sub(1) {
454            return StepOutcome::AtEnd {
455                step: self.current_step,
456            };
457        }
458
459        self.current_step += 1;
460        self.outcome_for_current_step()
461    }
462
463    /// Moves to the start of the next source line in this frame or a caller's, stopping
464    /// early at a breakpoint in code it steps over. Without source information this is
465    /// one instruction.
466    pub fn next_source(&mut self) -> StepOutcome {
467        if !self.has_source() {
468            return self.next_instruction();
469        }
470        let target = self
471            .step_map
472            .as_ref()
473            .and_then(|map| map.next_source(self.current_step));
474        self.advance_to(target)
475    }
476
477    /// Moves to the start of the next source line anywhere, entering calls.
478    pub fn step_into(&mut self) -> StepOutcome {
479        if !self.has_source() {
480            return self.next_instruction();
481        }
482        let target = self
483            .step_map
484            .as_ref()
485            .and_then(|map| map.step_into(self.current_step));
486        self.advance_to(target)
487    }
488
489    /// Runs until the current frame has returned to its caller, or to the end of the
490    /// recording when there is no caller.
491    pub fn finish(&mut self) -> StepOutcome {
492        if self.trace.is_none() {
493            return StepOutcome::NoTrace;
494        }
495        let target = self
496            .step_map
497            .as_ref()
498            .and_then(|map| map.finish(self.current_step));
499        self.advance_to(target)
500    }
501
502    pub fn continue_execution(&mut self) -> StepOutcome {
503        if self.trace.is_none() {
504            return StepOutcome::NoTrace;
505        }
506        self.advance_to(None)
507    }
508
509    /// Steps back one instruction. The trace is a complete recording, so moving
510    /// backward is as cheap and exact as moving forward.
511    pub fn previous_instruction(&mut self) -> StepOutcome {
512        if self.trace.is_none() {
513            return StepOutcome::NoTrace;
514        }
515        if self.current_step == 0 {
516            return StepOutcome::AtStart { step: 0 };
517        }
518        self.current_step -= 1;
519        self.outcome_for_current_step()
520    }
521
522    /// Moves back to the start of the previous source line in this frame or a caller's,
523    /// skipping over calls it made and stopping at a breakpoint on the way.
524    pub fn previous_source(&mut self) -> StepOutcome {
525        if !self.has_source() {
526            return self.previous_instruction();
527        }
528        let target = self
529            .step_map
530            .as_ref()
531            .and_then(|map| map.previous_source(self.current_step));
532        self.retreat_to(target)
533    }
534
535    /// Moves back to the start of the previous source line anywhere, entering calls.
536    pub fn reverse_step_into(&mut self) -> StepOutcome {
537        if !self.has_source() {
538            return self.previous_instruction();
539        }
540        let target = self
541            .step_map
542            .as_ref()
543            .and_then(|map| map.reverse_step_into(self.current_step));
544        self.retreat_to(target)
545    }
546
547    /// Runs backward to the step in the caller that entered the current frame, or to the
548    /// first step when there is no caller.
549    pub fn reverse_finish(&mut self) -> StepOutcome {
550        if self.trace.is_none() {
551            return StepOutcome::NoTrace;
552        }
553        let target = self
554            .step_map
555            .as_ref()
556            .and_then(|map| map.reverse_finish(self.current_step));
557        self.retreat_to(target)
558    }
559
560    /// Runs backward to the nearest earlier breakpoint, or to the first step.
561    pub fn reverse_continue(&mut self) -> StepOutcome {
562        if self.trace.is_none() {
563            return StepOutcome::NoTrace;
564        }
565        self.retreat_to(None)
566    }
567
568    /// Walks forward to `target`, or to the end without one, stopping at the first
569    /// breakpoint on the way.
570    fn advance_to(&mut self, target: Option<usize>) -> StepOutcome {
571        let Some(trace) = &self.trace else {
572            return StepOutcome::NoTrace;
573        };
574        let last = trace.steps.len().saturating_sub(1);
575        if self.current_step >= last {
576            return StepOutcome::AtEnd {
577                step: self.current_step,
578            };
579        }
580        let target = target.unwrap_or(last).min(last);
581        while self.current_step < target {
582            self.current_step += 1;
583            if let Some(breakpoint) = self.breakpoint_hit(self.current_step) {
584                return StepOutcome::BreakpointHit {
585                    step: self.current_step,
586                    pc: self.pc_at(self.current_step),
587                    breakpoint,
588                };
589            }
590        }
591        if target == last {
592            return StepOutcome::AtEnd { step: last };
593        }
594        self.outcome_for_current_step()
595    }
596
597    /// Walks backward to `target`, or to the first step without one, stopping at the
598    /// first breakpoint on the way.
599    fn retreat_to(&mut self, target: Option<usize>) -> StepOutcome {
600        if self.trace.is_none() {
601            return StepOutcome::NoTrace;
602        }
603        if self.current_step == 0 {
604            return StepOutcome::AtStart { step: 0 };
605        }
606        let target = target.unwrap_or(0);
607        while self.current_step > target {
608            self.current_step -= 1;
609            if let Some(breakpoint) = self.breakpoint_hit(self.current_step) {
610                return StepOutcome::BreakpointHit {
611                    step: self.current_step,
612                    pc: self.pc_at(self.current_step),
613                    breakpoint,
614                };
615            }
616        }
617        if target == 0 {
618            return StepOutcome::AtStart { step: 0 };
619        }
620        self.outcome_for_current_step()
621    }
622
623    pub fn goto_step(&mut self, step: usize) -> StepOutcome {
624        let Some(trace) = &self.trace else {
625            return StepOutcome::NoTrace;
626        };
627        if step >= trace.steps.len() {
628            return StepOutcome::InvalidStep {
629                requested: step,
630                max_step: trace.steps.len().checked_sub(1),
631            };
632        }
633        self.current_step = step;
634        self.outcome_for_current_step()
635    }
636
637    /// Sets a breakpoint on a program counter.
638    pub fn set_breakpoint(&mut self, pc: u64) -> StepOutcome {
639        self.add_breakpoint(BreakpointKind::Pc(pc), None)
640    }
641
642    /// Clears the breakpoint on a program counter.
643    pub fn clear_breakpoint(&mut self, pc: u64) -> StepOutcome {
644        self.remove_breakpoint(&BreakpointKind::Pc(pc), &format!("PC {pc}"))
645    }
646
647    /// Resolves a target against the trace and its debug info and sets a breakpoint on
648    /// it.
649    pub fn set_breakpoint_target(&mut self, target: &BreakpointTarget) -> StepOutcome {
650        self.set_conditional_breakpoint_target(target, None)
651    }
652
653    /// Sets a breakpoint that only stops when `condition` holds there.
654    pub fn set_conditional_breakpoint_target(
655        &mut self,
656        target: &BreakpointTarget,
657        condition: Option<&str>,
658    ) -> StepOutcome {
659        let condition = match condition.map(Condition::parse).transpose() {
660            Ok(condition) => condition,
661            Err(message) => return StepOutcome::BreakpointError(message),
662        };
663        match self.resolve_target(target) {
664            Ok(kind) => {
665                if !self.matches_any_step(&kind) {
666                    let label = Breakpoint {
667                        id: 0,
668                        kind: kind.clone(),
669                        condition: None,
670                    }
671                    .label();
672                    self.push_note(format!(
673                        "no step of this trace reaches {label}, so this breakpoint cannot be hit"
674                    ));
675                }
676                self.add_breakpoint(kind, condition)
677            }
678            Err(message) => StepOutcome::BreakpointError(message),
679        }
680    }
681
682    /// Clears the breakpoint that `target` resolves to.
683    pub fn clear_breakpoint_target(&mut self, target: &BreakpointTarget) -> StepOutcome {
684        match self.resolve_target(target) {
685            Ok(kind) => {
686                let label = Breakpoint {
687                    id: 0,
688                    kind: kind.clone(),
689                    condition: None,
690                }
691                .label();
692                self.remove_breakpoint(&kind, &label)
693            }
694            Err(message) => StepOutcome::BreakpointError(message),
695        }
696    }
697
698    /// Removes a breakpoint by its number.
699    pub fn delete_breakpoint(&mut self, id: u32) -> StepOutcome {
700        match self
701            .breakpoints
702            .iter()
703            .position(|breakpoint| breakpoint.id == id)
704        {
705            Some(index) => StepOutcome::BreakpointCleared(self.breakpoints.remove(index)),
706            None => StepOutcome::BreakpointMissing(format!("#{id}")),
707        }
708    }
709
710    fn add_breakpoint(
711        &mut self,
712        kind: BreakpointKind,
713        condition: Option<Condition>,
714    ) -> StepOutcome {
715        if let Some(existing) = self
716            .breakpoints
717            .iter()
718            .find(|breakpoint| breakpoint.kind == kind && breakpoint.condition == condition)
719        {
720            return StepOutcome::BreakpointSet(existing.clone());
721        }
722        let breakpoint = Breakpoint {
723            id: self.next_breakpoint_id,
724            kind,
725            condition,
726        };
727        self.next_breakpoint_id += 1;
728        self.breakpoints.push(breakpoint.clone());
729        StepOutcome::BreakpointSet(breakpoint)
730    }
731
732    fn remove_breakpoint(&mut self, kind: &BreakpointKind, label: &str) -> StepOutcome {
733        match self
734            .breakpoints
735            .iter()
736            .position(|breakpoint| breakpoint.kind == *kind)
737        {
738            Some(index) => StepOutcome::BreakpointCleared(self.breakpoints.remove(index)),
739            None => StepOutcome::BreakpointMissing(label.to_owned()),
740        }
741    }
742
743    /// A state variable's slot, for stopping where it is written.
744    fn resolve_state_target(&self, path: &str) -> Result<BreakpointKind, String> {
745        let map = self.source_map_for("state breakpoints")?;
746        let layouts = map
747            .contracts()
748            .iter()
749            .filter_map(|contract| contract.storage_layout.as_ref());
750        let mut failure = None;
751        for layout in layouts {
752            match layout.resolve(path) {
753                Ok(reference) => {
754                    let slot = soldb_debugger::short_hex(&reference.slot);
755                    return Ok(BreakpointKind::StateWrite {
756                        path: reference.path,
757                        slot: slot.trim_start_matches("0x").to_owned(),
758                    });
759                }
760                Err(error) => failure = Some(error.to_string()),
761            }
762        }
763        Err(failure.unwrap_or_else(|| {
764            "no storage layout is loaded, so state variables cannot be named; compile with `--storage-layout`".to_owned()
765        }))
766    }
767
768    fn resolve_target(&self, target: &BreakpointTarget) -> Result<BreakpointKind, String> {
769        match target {
770            BreakpointTarget::Pc(pc) => Ok(BreakpointKind::Pc(*pc)),
771            BreakpointTarget::SourceLine(target) => {
772                let map = self.source_map_for("source breakpoints")?;
773                map.resolve_line(target.file.as_deref(), target.line)
774                    .map(BreakpointKind::Line)
775            }
776            BreakpointTarget::Function(name) => {
777                let map = self.source_map_for("function breakpoints")?;
778                match map.resolve_function(name) {
779                    Ok(functions) => Ok(BreakpointKind::Function(functions)),
780                    // A name that is not a function may be a state variable: `break
781                    // counter` stops where the counter is written.
782                    Err(error) => self.resolve_state_target(name).map_err(|state_error| {
783                        if state_error.contains("no storage layout") {
784                            error
785                        } else {
786                            format!("{error}; and {state_error}")
787                        }
788                    }),
789                }
790            }
791            BreakpointTarget::State(path) => self.resolve_state_target(path),
792            BreakpointTarget::Storage(slot) => normalize_slot(slot)
793                .map(BreakpointKind::Storage)
794                .ok_or_else(|| format!("invalid storage slot `{slot}`; expected decimal or hex")),
795            BreakpointTarget::Revert => Ok(BreakpointKind::Revert),
796            BreakpointTarget::Call(None) => Ok(BreakpointKind::Call(None)),
797            BreakpointTarget::Call(Some(address)) => {
798                let normalized = normalize_address(address);
799                let hex = &normalized[2..];
800                if hex.len() != 40 || !hex.bytes().all(|byte| byte.is_ascii_hexdigit()) {
801                    return Err(format!(
802                        "invalid address `{address}`; expected 20 bytes of hex"
803                    ));
804                }
805                Ok(BreakpointKind::Call(Some(normalized)))
806            }
807            BreakpointTarget::Opcode(mnemonic) => {
808                Ok(BreakpointKind::Opcode(mnemonic.to_ascii_uppercase()))
809            }
810        }
811    }
812
813    fn source_map_for(&self, what: &str) -> Result<&StepMap, String> {
814        if self.trace.is_none() {
815            return Err("no trace is loaded".to_owned());
816        }
817        self.step_map
818            .as_ref()
819            .filter(|map| !map.contracts().is_empty())
820            .ok_or_else(|| {
821                format!("{what} require compiler debug metadata; start the session with `--ethdebug-dir <address>:<contract>:<dir>`")
822            })
823    }
824
825    /// The first breakpoint that `step` triggers.
826    #[must_use]
827    pub fn breakpoint_hit(&self, step: usize) -> Option<Breakpoint> {
828        let trace_step = self.trace.as_ref()?.steps.get(step)?;
829        self.breakpoints
830            .iter()
831            .filter(|breakpoint| self.kind_matches(&breakpoint.kind, step, trace_step))
832            // A condition is checked only where the target matched, so the cost falls on
833            // the handful of steps that got that far, not on every step passed through.
834            .find(|breakpoint| match &breakpoint.condition {
835                Some(condition) => {
836                    matches!(self.evaluate_condition(condition, step), Evaluation::True)
837                }
838                None => true,
839            })
840            .cloned()
841    }
842
843    /// Whether a breakpoint's target matches this step, before any condition.
844    fn kind_matches(&self, kind: &BreakpointKind, step: usize, trace_step: &TraceStep) -> bool {
845        let map = self.step_map.as_ref();
846        match kind {
847            BreakpointKind::Pc(pc) => trace_step.pc == *pc,
848            BreakpointKind::Line(lines) => map.is_some_and(|map| {
849                map.is_line_start(step)
850                    && map
851                        .line_key(step)
852                        .is_some_and(|key| lines.iter().any(|line| line.key == key))
853            }),
854            BreakpointKind::Function(functions) => map.is_some_and(|map| {
855                map.is_frame_entry(step)
856                    && map
857                        .function_id(step)
858                        .is_some_and(|id| functions.iter().any(|function| function.id == id))
859            }),
860            BreakpointKind::Storage(slot) | BreakpointKind::StateWrite { slot, .. } => {
861                &*trace_step.op == "SSTORE"
862                    && trace_step
863                        .snapshot_ref()
864                        .stack
865                        .last()
866                        .and_then(|word| normalize_slot(word))
867                        .is_some_and(|written| written == *slot)
868            }
869            BreakpointKind::Revert => &*trace_step.op == "REVERT" || trace_step.error.is_some(),
870            BreakpointKind::Call(address) => {
871                let target = call_target(trace_step);
872                match address {
873                    Some(address) => target.as_deref() == Some(address.as_str()),
874                    None => target.is_some() || is_call_opcode(&trace_step.op),
875                }
876            }
877            BreakpointKind::Opcode(mnemonic) => trace_step.op.eq_ignore_ascii_case(mnemonic),
878        }
879    }
880
881    /// Whether any step of the loaded trace matches this target.
882    ///
883    /// A trace is a finished recording, so a breakpoint that matches nothing in it will
884    /// never fire, and saying that when it is set beats leaving the user to wonder why
885    /// `continue` ran to the end. It happens for real reasons: a line that this
886    /// transaction did not execute, and a function of a base contract, whose code solc
887    /// currently attributes to the deriving contract's own file.
888    fn matches_any_step(&self, kind: &BreakpointKind) -> bool {
889        let Some(trace) = &self.trace else {
890            return true;
891        };
892        trace
893            .steps
894            .iter()
895            .enumerate()
896            .any(|(step, trace_step)| self.kind_matches(kind, step, trace_step))
897    }
898
899    /// Evaluates a breakpoint condition at `step`.
900    ///
901    /// A condition that cannot be read there — an untouched slot, an unknown name — does
902    /// not stop, and the reason is kept for [`DebuggerState::take_note`].
903    #[must_use]
904    pub fn evaluate_condition(&self, condition: &Condition, step: usize) -> Evaluation {
905        let (Some(map), Some(trace)) = (&self.step_map, &self.trace) else {
906            return Evaluation::Unavailable("no trace is loaded".to_owned());
907        };
908        let Some(trace_step) = trace.steps.get(step) else {
909            return Evaluation::Unavailable(format!("step {step} is outside the trace"));
910        };
911        let words = self
912            .storage_tape
913            .as_ref()
914            .map(|tape| tape.at_step(map, step));
915        let frames = self.frames_at(step);
916        let frame = frames.iter().find(|frame| !frame.arguments.is_empty());
917        let context = ConditionContext::new(map, step, trace_step, words).with_frame(frame);
918        let outcome = condition.evaluate(&context);
919        if let Evaluation::Unavailable(reason) = &outcome {
920            self.push_note(format!(
921                "`{}` could not be evaluated: {reason}",
922                condition.text()
923            ));
924        }
925        outcome
926    }
927
928    /// Records something for the frontend to report, keeping the first of several.
929    fn push_note(&self, note: String) {
930        let mut slot = self.note.borrow_mut();
931        if slot.is_none() {
932            *slot = Some(note);
933        }
934    }
935
936    /// Takes what the frontend should tell the user since the last call: a breakpoint that
937    /// cannot be hit, or a condition that could not be evaluated.
938    #[must_use]
939    pub fn take_note(&self) -> Option<String> {
940        self.note.borrow_mut().take()
941    }
942
943    /// The call structure at any step, with each frame's arguments.
944    #[must_use]
945    pub fn frames_at(&self, step: usize) -> Vec<Frame> {
946        let (Some(map), Some(trace)) = (&self.step_map, &self.trace) else {
947            return Vec::new();
948        };
949        let mut frames = map.frames(step);
950        for frame in &mut frames {
951            let Some(entry) = trace.steps.get(frame.entry_step) else {
952                continue;
953            };
954            let snapshot = entry.snapshot_ref();
955            frame.arguments = map.frame_arguments(
956                frame,
957                FrameState {
958                    stack: snapshot.stack,
959                    memory: snapshot.memory,
960                },
961            );
962        }
963        frames
964    }
965
966    /// The call structure at the current step, innermost frame first.
967    #[must_use]
968    pub fn frames(&self) -> Vec<Frame> {
969        self.frames_at(self.current_step)
970    }
971
972    /// Source lines around the current step.
973    #[must_use]
974    pub fn source_listing(&self, radius: u64) -> Option<SourceListing> {
975        self.step_map
976            .as_ref()?
977            .source_listing(self.current_step, radius)
978    }
979
980    /// The innermost recorded call that contains the current step, when the backend
981    /// recorded calls.
982    #[must_use]
983    pub fn current_call(&self) -> Option<&ExecutionCall> {
984        let trace = self.trace.as_ref()?;
985        trace
986            .artifacts
987            .calls
988            .iter()
989            .filter(|call| {
990                call.entry_step
991                    .is_some_and(|entry| entry <= self.current_step)
992                    && call.exit_step.is_none_or(|exit| self.current_step < exit)
993            })
994            .max_by_key(|call| call.depth)
995    }
996
997    /// The calldata the current frame was called with: the transaction's input at the
998    /// root, the recorded call's input in a nested frame, or nothing when the backend did
999    /// not record calls.
1000    #[must_use]
1001    pub fn calldata(&self) -> Option<String> {
1002        let trace = self.trace.as_ref()?;
1003        let step = trace.steps.get(self.current_step)?;
1004        let root_depth = trace.steps.first().map_or(0, |first| first.depth);
1005        if step.depth == root_depth {
1006            return Some(trace.input_data.clone());
1007        }
1008        self.current_call().map(|call| call.input.clone())
1009    }
1010
1011    pub fn apply_command(&mut self, command: DebuggerCommand) -> Option<StepOutcome> {
1012        match command {
1013            DebuggerCommand::Next => Some(self.next_source()),
1014            DebuggerCommand::NextInstruction => Some(self.next_instruction()),
1015            DebuggerCommand::Step => Some(self.step_into()),
1016            DebuggerCommand::Continue => Some(self.continue_execution()),
1017            DebuggerCommand::Finish => Some(self.finish()),
1018            DebuggerCommand::ReverseNext => Some(self.previous_source()),
1019            DebuggerCommand::ReverseNextInstruction => Some(self.previous_instruction()),
1020            DebuggerCommand::ReverseStep => Some(self.reverse_step_into()),
1021            DebuggerCommand::ReverseContinue => Some(self.reverse_continue()),
1022            DebuggerCommand::ReverseFinish => Some(self.reverse_finish()),
1023            DebuggerCommand::Goto(step) => Some(self.goto_step(step)),
1024            DebuggerCommand::Mode(Some(mode)) => Some(self.set_display_mode(mode)),
1025            DebuggerCommand::Break(target, condition) => {
1026                Some(self.set_conditional_breakpoint_target(&target, condition.as_deref()))
1027            }
1028            DebuggerCommand::Clear(target) => Some(self.clear_breakpoint_target(&target)),
1029            DebuggerCommand::Delete(id) => Some(self.delete_breakpoint(id)),
1030            DebuggerCommand::Empty
1031            | DebuggerCommand::Help(_)
1032            | DebuggerCommand::Info(_)
1033            | DebuggerCommand::Backtrace
1034            | DebuggerCommand::List
1035            | DebuggerCommand::Memory { .. }
1036            | DebuggerCommand::Calldata
1037            | DebuggerCommand::Stack
1038            | DebuggerCommand::Mode(None)
1039            | DebuggerCommand::Print(_)
1040            | DebuggerCommand::Quit
1041            | DebuggerCommand::Unknown(_)
1042            | DebuggerCommand::Vars => None,
1043        }
1044    }
1045
1046    fn pc_at(&self, step: usize) -> u64 {
1047        self.trace
1048            .as_ref()
1049            .and_then(|trace| trace.steps.get(step))
1050            .map_or(0, |step| step.pc)
1051    }
1052
1053    fn outcome_for_current_step(&self) -> StepOutcome {
1054        let Some(step) = self.current_step_data() else {
1055            return StepOutcome::NoTrace;
1056        };
1057        if let Some(breakpoint) = self.breakpoint_hit(self.current_step) {
1058            return StepOutcome::BreakpointHit {
1059                step: self.current_step,
1060                pc: step.pc,
1061                breakpoint,
1062            };
1063        }
1064        StepOutcome::Moved {
1065            step: self.current_step,
1066            pc: step.pc,
1067            op: step.op.to_string(),
1068        }
1069    }
1070}
1071
1072fn is_call_opcode(op: &str) -> bool {
1073    matches!(op, "CALL" | "CALLCODE" | "DELEGATECALL" | "STATICCALL")
1074}
1075
1076/// A storage slot as lowercase hex without `0x` or leading zeros, from decimal or hex
1077/// input; `None` when the text is neither.
1078fn normalize_slot(input: &str) -> Option<String> {
1079    let input = input.trim();
1080    let hex = if let Some(hex) = input
1081        .strip_prefix("0x")
1082        .or_else(|| input.strip_prefix("0X"))
1083    {
1084        if hex.is_empty() || !hex.bytes().all(|byte| byte.is_ascii_hexdigit()) {
1085            return None;
1086        }
1087        hex.to_ascii_lowercase()
1088    } else {
1089        format!("{:x}", input.parse::<u128>().ok()?)
1090    };
1091    let trimmed = hex.trim_start_matches('0');
1092    Some(if trimmed.is_empty() {
1093        "0".to_owned()
1094    } else {
1095        trimmed.to_owned()
1096    })
1097}
1098
1099fn parse_info_command(input: &str) -> Option<DebuggerInfoCommand> {
1100    match input.trim() {
1101        "resources" => Some(DebuggerInfoCommand::Resources { json: false }),
1102        "resources --json" | "resources json" => {
1103            Some(DebuggerInfoCommand::Resources { json: true })
1104        }
1105        "breakpoints" | "break" | "b" => Some(DebuggerInfoCommand::Breakpoints),
1106        "storage" => Some(DebuggerInfoCommand::Storage),
1107        _ => None,
1108    }
1109}
1110
1111fn parse_memory_command(input: &str) -> Option<DebuggerCommand> {
1112    let mut parts = input.split_whitespace();
1113    let offset = match parts.next() {
1114        Some(text) => Some(parse_u64_arg(text)?),
1115        None => None,
1116    };
1117    let length = match parts.next() {
1118        Some(text) => Some(parse_u64_arg(text)?),
1119        None => None,
1120    };
1121    if parts.next().is_some() {
1122        return None;
1123    }
1124    Some(DebuggerCommand::Memory { offset, length })
1125}
1126
1127fn parse_u64_arg(input: &str) -> Option<u64> {
1128    let input = input.trim();
1129    if let Some(hex) = input.strip_prefix("0x") {
1130        u64::from_str_radix(hex, 16).ok()
1131    } else {
1132        input.parse::<u64>().ok()
1133    }
1134}
1135
1136fn parse_breakpoint_target(input: &str) -> Option<BreakpointTarget> {
1137    let input = input.trim();
1138    if input.is_empty() {
1139        return None;
1140    }
1141    let mut parts = input.splitn(2, char::is_whitespace);
1142    let head = parts.next().unwrap_or_default();
1143    let rest = parts.next().map(str::trim).unwrap_or_default();
1144    match head.to_ascii_lowercase().as_str() {
1145        "storage" | "slot" => {
1146            (!rest.is_empty()).then(|| BreakpointTarget::Storage(rest.to_owned()))
1147        }
1148        "revert" if rest.is_empty() => Some(BreakpointTarget::Revert),
1149        "call" => Some(BreakpointTarget::Call(
1150            (!rest.is_empty()).then(|| rest.to_owned()),
1151        )),
1152        "op" | "opcode" => (!rest.is_empty() && !rest.contains(char::is_whitespace))
1153            .then(|| BreakpointTarget::Opcode(rest.to_owned())),
1154        _ => {
1155            if let Some(target) = parse_source_breakpoint_target(input) {
1156                return Some(BreakpointTarget::SourceLine(target));
1157            }
1158            if let Some(pc) = parse_u64_arg(input) {
1159                return Some(BreakpointTarget::Pc(pc));
1160            }
1161            if is_function_name(input) {
1162                return Some(BreakpointTarget::Function(input.to_owned()));
1163            }
1164            // `balances[0xabc…]` and the like: a place in storage, named the way `print`
1165            // names it.
1166            is_state_path(input).then(|| BreakpointTarget::State(input.to_owned()))
1167        }
1168    }
1169}
1170
1171/// Splits `<target> if <condition>` at the first ` if `, which cannot appear inside a
1172/// target: a file name, a function name, a slot, and an opcode all lack spaces.
1173fn split_condition(input: &str) -> (&str, Option<String>) {
1174    let input = input.trim();
1175    match input.find(" if ") {
1176        Some(index) => {
1177            let condition = input[index + 4..].trim();
1178            (
1179                input[..index].trim(),
1180                (!condition.is_empty()).then(|| condition.to_owned()),
1181            )
1182        }
1183        None => (input, None),
1184    }
1185}
1186
1187/// Whether the text looks like a state variable path: a name followed by any number of
1188/// `[key]` and `.member` steps.
1189fn is_state_path(input: &str) -> bool {
1190    let head = input.split(['[', '.']).next().unwrap_or_default();
1191    !head.is_empty() && is_function_name(head) && input.len() > head.len()
1192}
1193
1194fn is_function_name(input: &str) -> bool {
1195    let mut segments = input.split('.');
1196    segments.all(|segment| {
1197        let mut bytes = segment.bytes();
1198        bytes
1199            .next()
1200            .is_some_and(|byte| byte.is_ascii_alphabetic() || byte == b'_')
1201            && bytes.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
1202    })
1203}
1204
1205fn parse_source_breakpoint_target(input: &str) -> Option<SourceBreakpointTarget> {
1206    let input = input.trim();
1207    if let Some(line) = input.strip_prefix("line ") {
1208        return parse_source_line_number(line)
1209            .map(|line| SourceBreakpointTarget { file: None, line });
1210    }
1211
1212    let (file, line) = input.rsplit_once(':')?;
1213    let file = file.trim();
1214    if file.is_empty() {
1215        return None;
1216    }
1217    parse_source_line_number(line).map(|line| SourceBreakpointTarget {
1218        file: Some(file.to_owned()),
1219        line,
1220    })
1221}
1222
1223fn parse_source_line_number(input: &str) -> Option<u64> {
1224    input.trim().parse::<u64>().ok().filter(|line| *line > 0)
1225}
1226
1227#[cfg(test)]
1228mod tests {
1229    use std::collections::BTreeMap;
1230
1231    use serde_json::json;
1232    use soldb_core::{StepSnapshot, TraceStep, TransactionTrace};
1233    use soldb_debugger::ContractDebugInfo;
1234    use soldb_ethdebug::{EthdebugInfo, Instruction};
1235
1236    use super::{
1237        BreakpointKind, BreakpointTarget, DebuggerCommand, DebuggerInfoCommand, DebuggerState,
1238        DisplayMode, SourceBreakpointTarget, StepOutcome,
1239    };
1240
1241    #[test]
1242    fn parses_repl_commands_and_aliases() {
1243        assert_eq!(DebuggerCommand::parse(""), DebuggerCommand::Empty);
1244        assert_eq!(DebuggerCommand::parse("n"), DebuggerCommand::Next);
1245        assert_eq!(
1246            DebuggerCommand::parse("stepi"),
1247            DebuggerCommand::NextInstruction
1248        );
1249        assert_eq!(DebuggerCommand::parse("s"), DebuggerCommand::Step);
1250        assert_eq!(DebuggerCommand::parse("c"), DebuggerCommand::Continue);
1251        assert_eq!(DebuggerCommand::parse("finish"), DebuggerCommand::Finish);
1252        assert_eq!(DebuggerCommand::parse("fin"), DebuggerCommand::Finish);
1253        assert_eq!(DebuggerCommand::parse("goto 2"), DebuggerCommand::Goto(2));
1254        assert_eq!(DebuggerCommand::parse("vars"), DebuggerCommand::Vars);
1255        assert_eq!(DebuggerCommand::parse("LOCALS"), DebuggerCommand::Vars);
1256        assert_eq!(
1257            DebuggerCommand::parse("print balance"),
1258            DebuggerCommand::Print("balance".to_owned())
1259        );
1260        assert_eq!(
1261            DebuggerCommand::parse("p   balance  "),
1262            DebuggerCommand::Print("balance".to_owned())
1263        );
1264        assert_eq!(
1265            DebuggerCommand::parse("print"),
1266            DebuggerCommand::Print(String::new())
1267        );
1268        assert_eq!(
1269            DebuggerCommand::parse("info resources"),
1270            DebuggerCommand::Info(DebuggerInfoCommand::Resources { json: false })
1271        );
1272        assert_eq!(
1273            DebuggerCommand::parse("info resources --json"),
1274            DebuggerCommand::Info(DebuggerInfoCommand::Resources { json: true })
1275        );
1276        assert_eq!(
1277            DebuggerCommand::parse("info breakpoints"),
1278            DebuggerCommand::Info(DebuggerInfoCommand::Breakpoints)
1279        );
1280        assert_eq!(
1281            DebuggerCommand::parse("i b"),
1282            DebuggerCommand::Info(DebuggerInfoCommand::Breakpoints)
1283        );
1284        assert_eq!(
1285            DebuggerCommand::parse("info storage"),
1286            DebuggerCommand::Info(DebuggerInfoCommand::Storage)
1287        );
1288        assert_eq!(
1289            DebuggerCommand::parse("storage"),
1290            DebuggerCommand::Info(DebuggerInfoCommand::Storage)
1291        );
1292        assert_eq!(
1293            DebuggerCommand::parse("mode assembly"),
1294            DebuggerCommand::Mode(Some(DisplayMode::Assembly))
1295        );
1296        assert_eq!(
1297            DebuggerCommand::parse("break 0x10"),
1298            DebuggerCommand::Break(BreakpointTarget::Pc(16), None)
1299        );
1300        assert_eq!(
1301            DebuggerCommand::parse("break Counter.sol:7"),
1302            DebuggerCommand::Break(
1303                BreakpointTarget::SourceLine(SourceBreakpointTarget {
1304                    file: Some("Counter.sol".to_owned()),
1305                    line: 7
1306                }),
1307                None
1308            )
1309        );
1310        assert_eq!(
1311            DebuggerCommand::parse("break line 7"),
1312            DebuggerCommand::Break(
1313                BreakpointTarget::SourceLine(SourceBreakpointTarget {
1314                    file: None,
1315                    line: 7
1316                }),
1317                None
1318            )
1319        );
1320        assert_eq!(
1321            DebuggerCommand::parse("break increment"),
1322            DebuggerCommand::Break(BreakpointTarget::Function("increment".to_owned()), None)
1323        );
1324        assert_eq!(
1325            DebuggerCommand::parse("b Counter.increment"),
1326            DebuggerCommand::Break(
1327                BreakpointTarget::Function("Counter.increment".to_owned()),
1328                None
1329            )
1330        );
1331        assert_eq!(
1332            DebuggerCommand::parse("break storage 0x0"),
1333            DebuggerCommand::Break(BreakpointTarget::Storage("0x0".to_owned()), None)
1334        );
1335        assert_eq!(
1336            DebuggerCommand::parse("break revert"),
1337            DebuggerCommand::Break(BreakpointTarget::Revert, None)
1338        );
1339        assert_eq!(
1340            DebuggerCommand::parse("break call"),
1341            DebuggerCommand::Break(BreakpointTarget::Call(None), None)
1342        );
1343        assert_eq!(
1344            DebuggerCommand::parse("break call 0xabc"),
1345            DebuggerCommand::Break(BreakpointTarget::Call(Some("0xabc".to_owned())), None)
1346        );
1347        assert_eq!(
1348            DebuggerCommand::parse("break op sstore"),
1349            DebuggerCommand::Break(BreakpointTarget::Opcode("sstore".to_owned()), None)
1350        );
1351        assert_eq!(
1352            DebuggerCommand::parse("break"),
1353            DebuggerCommand::Unknown("break".to_owned())
1354        );
1355        assert_eq!(
1356            DebuggerCommand::parse("break 12abc"),
1357            DebuggerCommand::Unknown("break 12abc".to_owned())
1358        );
1359        assert_eq!(
1360            DebuggerCommand::parse("clear 8"),
1361            DebuggerCommand::Clear(BreakpointTarget::Pc(8))
1362        );
1363        assert_eq!(
1364            DebuggerCommand::parse("delete 3"),
1365            DebuggerCommand::Delete(3)
1366        );
1367        assert_eq!(DebuggerCommand::parse("d #3"), DebuggerCommand::Delete(3));
1368        assert_eq!(DebuggerCommand::parse("bt"), DebuggerCommand::Backtrace);
1369        assert_eq!(DebuggerCommand::parse("where"), DebuggerCommand::Backtrace);
1370        assert_eq!(DebuggerCommand::parse("l"), DebuggerCommand::List);
1371        assert_eq!(
1372            DebuggerCommand::parse("memory"),
1373            DebuggerCommand::Memory {
1374                offset: None,
1375                length: None
1376            }
1377        );
1378        assert_eq!(
1379            DebuggerCommand::parse("mem 0x40 32"),
1380            DebuggerCommand::Memory {
1381                offset: Some(64),
1382                length: Some(32)
1383            }
1384        );
1385        assert_eq!(
1386            DebuggerCommand::parse("mem 1 2 3"),
1387            DebuggerCommand::Unknown("mem 1 2 3".to_owned())
1388        );
1389        assert_eq!(
1390            DebuggerCommand::parse("calldata"),
1391            DebuggerCommand::Calldata
1392        );
1393        assert_eq!(DebuggerCommand::parse("stack"), DebuggerCommand::Stack);
1394        assert_eq!(
1395            DebuggerCommand::parse("help mode"),
1396            DebuggerCommand::Help(Some("mode".to_owned()))
1397        );
1398        assert_eq!(DebuggerCommand::parse("q"), DebuggerCommand::Quit);
1399        assert_eq!(
1400            DebuggerCommand::parse("wat"),
1401            DebuggerCommand::Unknown("wat".to_owned())
1402        );
1403    }
1404
1405    #[test]
1406    fn loads_trace_and_steps_instruction_by_instruction() {
1407        let mut state = DebuggerState::new();
1408        assert_eq!(state.next_instruction(), StepOutcome::NoTrace);
1409        assert_eq!(state.next_source(), StepOutcome::NoTrace);
1410        assert_eq!(state.finish(), StepOutcome::NoTrace);
1411
1412        state.load_trace(sample_trace());
1413        assert_eq!(state.step_count(), 4);
1414        assert_eq!(&*state.current_step_data().expect("step").op, "PUSH1");
1415        assert!(!state.has_source());
1416
1417        assert_eq!(
1418            state.next_instruction(),
1419            StepOutcome::Moved {
1420                step: 1,
1421                pc: 2,
1422                op: "MSTORE".into()
1423            }
1424        );
1425        assert_eq!(state.current_step, 1);
1426        // Without source information `next` and `step` are one instruction each.
1427        assert!(matches!(
1428            state.next_source(),
1429            StepOutcome::Moved { step: 2, .. }
1430        ));
1431        assert!(matches!(
1432            state.step_into(),
1433            StepOutcome::Moved { step: 3, .. }
1434        ));
1435        assert!(matches!(
1436            state.previous_source(),
1437            StepOutcome::Moved { step: 2, .. }
1438        ));
1439        assert!(matches!(
1440            state.reverse_step_into(),
1441            StepOutcome::Moved { step: 1, .. }
1442        ));
1443    }
1444
1445    #[test]
1446    fn continues_until_breakpoint_or_end() {
1447        let mut state = DebuggerState::new();
1448        state.load_trace(sample_trace());
1449        let set = state.set_breakpoint(3);
1450        let StepOutcome::BreakpointSet(breakpoint) = &set else {
1451            panic!("{set:?}");
1452        };
1453        assert_eq!(breakpoint.id, 1);
1454        assert_eq!(breakpoint.kind, BreakpointKind::Pc(3));
1455        assert_eq!(breakpoint.label(), "PC 3");
1456        // Setting it again returns the same breakpoint instead of a duplicate.
1457        assert_eq!(state.set_breakpoint(3), set);
1458        assert_eq!(state.breakpoints().len(), 1);
1459
1460        assert!(matches!(
1461            state.continue_execution(),
1462            StepOutcome::BreakpointHit { step: 2, pc: 3, .. }
1463        ));
1464        assert_eq!(state.current_step, 2);
1465
1466        assert!(matches!(
1467            state.clear_breakpoint(3),
1468            StepOutcome::BreakpointCleared(_)
1469        ));
1470        assert_eq!(
1471            state.clear_breakpoint(3),
1472            StepOutcome::BreakpointMissing("PC 3".to_owned())
1473        );
1474        assert_eq!(state.continue_execution(), StepOutcome::AtEnd { step: 3 });
1475        assert_eq!(state.continue_execution(), StepOutcome::AtEnd { step: 3 });
1476    }
1477
1478    #[test]
1479    fn numbered_breakpoints_can_be_deleted() {
1480        let mut state = DebuggerState::new();
1481        state.load_trace(sample_trace());
1482        state.set_breakpoint(2);
1483        state.set_breakpoint(3);
1484        assert_eq!(
1485            state
1486                .breakpoints()
1487                .iter()
1488                .map(|breakpoint| breakpoint.id)
1489                .collect::<Vec<_>>(),
1490            vec![1, 2]
1491        );
1492        assert!(matches!(
1493            state.delete_breakpoint(1),
1494            StepOutcome::BreakpointCleared(breakpoint) if breakpoint.kind == BreakpointKind::Pc(2)
1495        ));
1496        assert_eq!(
1497            state.delete_breakpoint(1),
1498            StepOutcome::BreakpointMissing("#1".to_owned())
1499        );
1500        // Numbers are never reused.
1501        assert!(matches!(
1502            state.set_breakpoint(4),
1503            StepOutcome::BreakpointSet(breakpoint) if breakpoint.id == 3
1504        ));
1505    }
1506
1507    #[test]
1508    fn goto_validates_trace_bounds() {
1509        let mut state = DebuggerState::new();
1510        assert_eq!(state.goto_step(1), StepOutcome::NoTrace);
1511
1512        state.load_trace(sample_trace());
1513        assert_eq!(
1514            state.goto_step(3),
1515            StepOutcome::Moved {
1516                step: 3,
1517                pc: 4,
1518                op: "CALL".into()
1519            }
1520        );
1521        assert_eq!(
1522            state.goto_step(99),
1523            StepOutcome::InvalidStep {
1524                requested: 99,
1525                max_step: Some(3)
1526            }
1527        );
1528    }
1529
1530    #[test]
1531    fn parses_reverse_commands_and_aliases() {
1532        for alias in ["reverse-next", "rnext", "rn"] {
1533            assert_eq!(DebuggerCommand::parse(alias), DebuggerCommand::ReverseNext);
1534        }
1535        for alias in [
1536            "reverse-nexti",
1537            "rnexti",
1538            "rni",
1539            "reverse-stepi",
1540            "rsi",
1541            "back",
1542        ] {
1543            assert_eq!(
1544                DebuggerCommand::parse(alias),
1545                DebuggerCommand::ReverseNextInstruction,
1546                "{alias}"
1547            );
1548        }
1549        for alias in ["reverse-step", "rstep", "rs"] {
1550            assert_eq!(DebuggerCommand::parse(alias), DebuggerCommand::ReverseStep);
1551        }
1552        for alias in ["reverse-continue", "rcontinue", "rc", "RC"] {
1553            assert_eq!(
1554                DebuggerCommand::parse(alias),
1555                DebuggerCommand::ReverseContinue
1556            );
1557        }
1558        for alias in ["reverse-finish", "rfinish", "rfin"] {
1559            assert_eq!(
1560                DebuggerCommand::parse(alias),
1561                DebuggerCommand::ReverseFinish
1562            );
1563        }
1564    }
1565
1566    #[test]
1567    fn steps_backward_over_the_recording_like_a_tape() {
1568        let mut state = DebuggerState::new();
1569        assert_eq!(state.previous_instruction(), StepOutcome::NoTrace);
1570        assert_eq!(state.reverse_continue(), StepOutcome::NoTrace);
1571
1572        state.load_trace(sample_trace());
1573        assert_eq!(
1574            state.previous_instruction(),
1575            StepOutcome::AtStart { step: 0 }
1576        );
1577
1578        // Forward then back lands on the same step every time.
1579        state.goto_step(2);
1580        assert!(matches!(
1581            state.previous_instruction(),
1582            StepOutcome::Moved { step: 1, .. }
1583        ));
1584        assert!(matches!(
1585            state.previous_instruction(),
1586            StepOutcome::Moved { step: 0, .. }
1587        ));
1588        assert_eq!(
1589            state.previous_instruction(),
1590            StepOutcome::AtStart { step: 0 }
1591        );
1592        assert_eq!(state.current_step, 0);
1593
1594        // Rewinding to any step is a jump; it does not have to walk.
1595        assert!(matches!(
1596            state.goto_step(2),
1597            StepOutcome::Moved { step: 2, .. }
1598        ));
1599        assert_eq!(
1600            state.apply_command(DebuggerCommand::ReverseNextInstruction),
1601            Some(StepOutcome::Moved {
1602                step: 1,
1603                pc: sample_trace().steps[1].pc,
1604                op: sample_trace().steps[1].op.to_string(),
1605            })
1606        );
1607    }
1608
1609    #[test]
1610    fn reverse_continue_stops_at_earlier_breakpoints_then_the_start() {
1611        let mut state = DebuggerState::new();
1612        state.load_trace(sample_trace());
1613        let last = state.step_count() - 1;
1614        state.goto_step(last);
1615        let breakpoint_pc = sample_trace().steps[1].pc;
1616        state.set_breakpoint(breakpoint_pc);
1617
1618        assert!(matches!(
1619            state.reverse_continue(),
1620            StepOutcome::BreakpointHit { step: 1, pc, .. } if pc == breakpoint_pc
1621        ));
1622        assert_eq!(state.reverse_continue(), StepOutcome::AtStart { step: 0 });
1623        assert_eq!(state.current_step, 0);
1624        assert_eq!(
1625            state.apply_command(DebuggerCommand::ReverseContinue),
1626            Some(StepOutcome::AtStart { step: 0 })
1627        );
1628
1629        // A breakpoint on the current step is not "earlier": it is passed over.
1630        state.goto_step(1);
1631        assert_eq!(state.reverse_continue(), StepOutcome::AtStart { step: 0 });
1632    }
1633
1634    #[test]
1635    fn applies_state_changing_commands() {
1636        let mut state = DebuggerState::new();
1637        state.load_trace(sample_trace());
1638
1639        assert_eq!(
1640            state.apply_command(DebuggerCommand::Mode(Some(DisplayMode::Assembly))),
1641            Some(StepOutcome::ModeChanged(DisplayMode::Assembly))
1642        );
1643        assert_eq!(state.display_mode.as_str(), "asm");
1644        assert!(matches!(
1645            state.apply_command(DebuggerCommand::Break(BreakpointTarget::Pc(2), None)),
1646            Some(StepOutcome::BreakpointSet(_))
1647        ));
1648        assert!(matches!(
1649            state.apply_command(DebuggerCommand::NextInstruction),
1650            Some(StepOutcome::BreakpointHit { step: 1, pc: 2, .. })
1651        ));
1652        assert_eq!(state.apply_command(DebuggerCommand::Help(None)), None);
1653        assert_eq!(state.apply_command(DebuggerCommand::Backtrace), None);
1654        assert_eq!(state.apply_command(DebuggerCommand::Quit), None);
1655    }
1656
1657    #[test]
1658    fn a_state_variable_can_be_named_where_a_slot_would_go() {
1659        // `break counter` is parsed as a name; without a layout it stays a function
1660        // breakpoint error, and with one it becomes a write to that variable's slot.
1661        assert_eq!(
1662            DebuggerCommand::parse("break counter"),
1663            DebuggerCommand::Break(BreakpointTarget::Function("counter".to_owned()), None)
1664        );
1665        assert_eq!(
1666            DebuggerCommand::parse("break balances[0xabc]"),
1667            DebuggerCommand::Break(BreakpointTarget::State("balances[0xabc]".to_owned()), None)
1668        );
1669        assert_eq!(
1670            DebuggerCommand::parse("break config.limit"),
1671            DebuggerCommand::Break(BreakpointTarget::Function("config.limit".to_owned()), None)
1672        );
1673
1674        let mut state = DebuggerState::new();
1675        state.load_trace(sample_trace());
1676        // No debug info at all: the message is about the missing metadata.
1677        assert!(matches!(
1678            state.set_breakpoint_target(&BreakpointTarget::State("counter".to_owned())),
1679            StepOutcome::BreakpointError(message) if message.contains("metadata")
1680        ));
1681    }
1682
1683    #[test]
1684    fn a_condition_gates_a_breakpoint_and_says_when_it_cannot_be_read() {
1685        let mut state = DebuggerState::new();
1686        state.load_trace(sample_trace());
1687
1688        // `pc` and `gas` are facts about the step, so they need no debug info.
1689        assert!(matches!(
1690            state.set_conditional_breakpoint_target(
1691                &BreakpointTarget::Opcode("sstore".to_owned()),
1692                Some("pc == 3")
1693            ),
1694            StepOutcome::BreakpointSet(breakpoint)
1695                if breakpoint.label() == "opcode SSTORE if pc == 3"
1696        ));
1697        assert!(matches!(
1698            state.continue_execution(),
1699            StepOutcome::BreakpointHit { step: 2, .. }
1700        ));
1701
1702        // The same target with a condition that never holds runs to the end.
1703        state.goto_step(0);
1704        state.delete_breakpoint(1);
1705        assert!(matches!(
1706            state.set_conditional_breakpoint_target(
1707                &BreakpointTarget::Opcode("sstore".to_owned()),
1708                Some("pc == 999")
1709            ),
1710            StepOutcome::BreakpointSet(_)
1711        ));
1712        assert!(matches!(
1713            state.continue_execution(),
1714            StepOutcome::AtEnd { .. }
1715        ));
1716        assert_eq!(state.take_note(), None);
1717
1718        // `&&` and `||`, and a bare value.
1719        state.goto_step(0);
1720        state.delete_breakpoint(2);
1721        assert!(matches!(
1722            state.set_conditional_breakpoint_target(
1723                &BreakpointTarget::Opcode("sstore".to_owned()),
1724                Some("pc == 999 || gas >= 94 && depth == 1")
1725            ),
1726            StepOutcome::BreakpointSet(_)
1727        ));
1728        assert!(matches!(
1729            state.continue_execution(),
1730            StepOutcome::BreakpointHit { step: 2, .. }
1731        ));
1732
1733        // A name nothing defines cannot be read: the breakpoint does not stop, and the
1734        // reason is kept for the frontend to report rather than failing silently.
1735        state.goto_step(0);
1736        state.delete_breakpoint(3);
1737        assert!(matches!(
1738            state.set_conditional_breakpoint_target(
1739                &BreakpointTarget::Opcode("sstore".to_owned()),
1740                Some("counter > 1")
1741            ),
1742            StepOutcome::BreakpointSet(_)
1743        ));
1744        assert!(matches!(
1745            state.continue_execution(),
1746            StepOutcome::AtEnd { .. }
1747        ));
1748        let note = state.take_note().expect("a reason");
1749        assert!(
1750            note.contains("`counter > 1` could not be evaluated"),
1751            "{note}"
1752        );
1753        assert_eq!(state.take_note(), None, "the note is taken once");
1754
1755        // A condition that does not parse is refused when the breakpoint is set.
1756        assert!(matches!(
1757            state.set_conditional_breakpoint_target(
1758                &BreakpointTarget::Opcode("sstore".to_owned()),
1759                Some("pc ==")
1760            ),
1761            StepOutcome::BreakpointError(_)
1762        ));
1763    }
1764
1765    #[test]
1766    fn tape_breakpoints_search_the_recording() {
1767        let mut state = DebuggerState::new();
1768        state.load_trace(sample_trace());
1769
1770        // The storage write at step 2 is an SSTORE to slot 0; a call to 0x..02 at step 3.
1771        assert!(matches!(
1772            state.set_breakpoint_target(&BreakpointTarget::Storage("0x00".to_owned())),
1773            StepOutcome::BreakpointSet(breakpoint)
1774                if breakpoint.kind == BreakpointKind::Storage("0".to_owned())
1775                    && breakpoint.label() == "storage slot 0x0"
1776        ));
1777        assert!(matches!(
1778            state.continue_execution(),
1779            StepOutcome::BreakpointHit { step: 2, .. }
1780        ));
1781        assert_eq!(
1782            state.set_breakpoint_target(&BreakpointTarget::Storage("zz".to_owned())),
1783            StepOutcome::BreakpointError(
1784                "invalid storage slot `zz`; expected decimal or hex".to_owned()
1785            )
1786        );
1787        assert!(matches!(
1788            state.clear_breakpoint_target(&BreakpointTarget::Storage("0".to_owned())),
1789            StepOutcome::BreakpointCleared(_)
1790        ));
1791
1792        state.goto_step(0);
1793        let callee = "0x0000000000000000000000000000000000000002";
1794        assert!(matches!(
1795            state.set_breakpoint_target(&BreakpointTarget::Call(Some(callee.to_owned()))),
1796            StepOutcome::BreakpointSet(breakpoint) if breakpoint.label() == format!("call to {callee}")
1797        ));
1798        assert!(matches!(
1799            state.continue_execution(),
1800            StepOutcome::BreakpointHit { step: 3, .. }
1801        ));
1802        assert!(matches!(
1803            state.set_breakpoint_target(&BreakpointTarget::Call(Some("0x12".to_owned()))),
1804            StepOutcome::BreakpointError(message) if message.contains("20 bytes")
1805        ));
1806        state.goto_step(0);
1807        assert!(matches!(
1808            state.set_breakpoint_target(&BreakpointTarget::Opcode("mstore".to_owned())),
1809            StepOutcome::BreakpointSet(breakpoint) if breakpoint.label() == "opcode MSTORE"
1810        ));
1811        assert!(matches!(
1812            state.continue_execution(),
1813            StepOutcome::BreakpointHit { step: 1, .. }
1814        ));
1815        assert!(matches!(
1816            state.set_breakpoint_target(&BreakpointTarget::Revert),
1817            StepOutcome::BreakpointSet(breakpoint) if breakpoint.label() == "revert"
1818        ));
1819        // The last step carries an error, so `revert` hits there.
1820        assert!(matches!(
1821            state.continue_execution(),
1822            StepOutcome::BreakpointHit { step: 3, .. }
1823        ));
1824        assert_eq!(state.breakpoints().len(), 3);
1825
1826        // Line and function breakpoints need debug info.
1827        assert_eq!(
1828            state.set_breakpoint_target(&BreakpointTarget::Function("f".to_owned())),
1829            StepOutcome::BreakpointError(
1830                "function breakpoints require compiler debug metadata; start the session with `--ethdebug-dir <address>:<contract>:<dir>`"
1831                    .to_owned()
1832            )
1833        );
1834        assert!(matches!(
1835            state.set_breakpoint_target(&BreakpointTarget::SourceLine(SourceBreakpointTarget {
1836                file: None,
1837                line: 1
1838            })),
1839            StepOutcome::BreakpointError(message) if message.starts_with("source breakpoints require")
1840        ));
1841    }
1842
1843    #[test]
1844    fn calldata_and_frames_without_debug_info() {
1845        let mut state = DebuggerState::new();
1846        assert!(state.frames().is_empty());
1847        assert_eq!(state.calldata(), None);
1848        state.load_trace(sample_trace());
1849        assert_eq!(state.calldata().as_deref(), Some("0xabcd"));
1850        let frames = state.frames();
1851        assert_eq!(frames.len(), 1);
1852        assert_eq!(frames[0].address.as_deref(), Some("0x2"));
1853        assert!(state.location().is_none());
1854        assert!(state.source_listing(2).is_none());
1855        assert!(state.current_call().is_none());
1856    }
1857
1858    // A contract whose `outer` calls `inner` internally, mapped onto a trace whose steps
1859    // are: dispatcher (0, 1), outer's declaration (2), line 3 (3), line 4 before the call
1860    // (4), inner's declaration (5), inner's line 8 (6, 7), back on line 4 (8), line 5 (9),
1861    // dispatcher (10).
1862    const SOURCE: &str = "\
1863contract C {
1864    function outer(uint256 a) public {
1865        uint256 b = a + 1;
1866        inner(b);
1867        b = 0;
1868    }
1869    function inner(uint256 x) internal {
1870        x += 1;
1871    }
1872}
1873";
1874
1875    fn source_state() -> DebuggerState {
1876        let offset = |needle: &str| SOURCE.find(needle).expect(needle) as u64;
1877        let whole = SOURCE.len() as u64;
1878        let instruction = |pc: u64, offset: u64, length: u64| -> Instruction {
1879            serde_json::from_value(json!({
1880                "offset": pc,
1881                "operation": {"mnemonic": "JUMPDEST"},
1882                "context": {"code": {"source": {"id": 0}, "range": {"offset": offset, "length": length}}}
1883            }))
1884            .expect("instruction")
1885        };
1886        let info = EthdebugInfo {
1887            compilation: serde_json::Value::Null,
1888            contract_name: "C".to_owned(),
1889            environment: "runtime".to_owned(),
1890            instructions: vec![
1891                instruction(0, 0, whole),
1892                instruction(1, 0, whole),
1893                instruction(10, offset("function outer"), 90),
1894                instruction(11, offset("uint256 b = a + 1;"), 18),
1895                instruction(12, offset("inner(b);"), 9),
1896                instruction(13, offset("inner(b);"), 9),
1897                instruction(14, offset("b = 0;"), 6),
1898                instruction(20, offset("function inner"), 50),
1899                instruction(21, offset("x += 1;"), 7),
1900                instruction(30, 0, whole),
1901            ],
1902            sources: BTreeMap::from([(0, "C.sol".to_owned())]),
1903            variable_locations: BTreeMap::new(),
1904        };
1905        let contract =
1906            ContractDebugInfo::new(None, "C", info, BTreeMap::from([(0, SOURCE.to_owned())]));
1907        let pcs = [0, 1, 10, 11, 12, 20, 21, 21, 13, 14, 30];
1908        let mut trace = sample_trace();
1909        trace.steps = pcs.iter().map(|pc| step(*pc, "JUMPDEST", 0, &[])).collect();
1910        trace.steps[6].op = "SSTORE".into();
1911        trace.steps[6].stack = vec!["0x2a".into(), "0x5".into()];
1912        let mut state = DebuggerState::new();
1913        state.load_trace(trace);
1914        state.attach_debug_info(vec![contract]);
1915        state
1916    }
1917
1918    #[test]
1919    fn source_stepping_moves_by_line_and_frame() {
1920        let mut state = source_state();
1921        assert!(state.has_source());
1922        assert!(matches!(
1923            state.next_source(),
1924            StepOutcome::Moved { step: 2, .. }
1925        ));
1926        assert_eq!(state.location().expect("location").line, 2);
1927        assert!(matches!(
1928            state.next_source(),
1929            StepOutcome::Moved { step: 3, .. }
1930        ));
1931        assert!(matches!(
1932            state.next_source(),
1933            StepOutcome::Moved { step: 4, .. }
1934        ));
1935        // `next` steps over the internal call; `step` enters it.
1936        assert!(matches!(
1937            state.next_source(),
1938            StepOutcome::Moved { step: 9, .. }
1939        ));
1940        state.goto_step(4);
1941        assert!(matches!(
1942            state.step_into(),
1943            StepOutcome::Moved { step: 5, .. }
1944        ));
1945        assert_eq!(
1946            state.location().expect("location").function_name.as_deref(),
1947            Some("inner")
1948        );
1949        assert!(matches!(
1950            state.step_into(),
1951            StepOutcome::Moved { step: 6, .. }
1952        ));
1953        // `finish` returns to the call site in outer; `reverse-finish` goes back to it too.
1954        assert!(matches!(state.finish(), StepOutcome::Moved { step: 8, .. }));
1955        state.goto_step(6);
1956        assert!(matches!(
1957            state.reverse_finish(),
1958            StepOutcome::Moved { step: 4, .. }
1959        ));
1960        // Reverse stepping mirrors forward stepping.
1961        state.goto_step(9);
1962        assert!(matches!(
1963            state.previous_source(),
1964            StepOutcome::Moved { step: 4, .. }
1965        ));
1966        assert!(matches!(
1967            state.reverse_step_into(),
1968            StepOutcome::Moved { step: 3, .. }
1969        ));
1970        state.goto_step(9);
1971        assert!(matches!(
1972            state.reverse_step_into(),
1973            StepOutcome::Moved { step: 8, .. }
1974        ));
1975        // Running off either end reports it.
1976        state.goto_step(10);
1977        assert_eq!(state.next_source(), StepOutcome::AtEnd { step: 10 });
1978        assert_eq!(state.finish(), StepOutcome::AtEnd { step: 10 });
1979        state.goto_step(9);
1980        assert_eq!(state.finish(), StepOutcome::AtEnd { step: 10 });
1981        state.goto_step(0);
1982        assert_eq!(state.reverse_finish(), StepOutcome::AtStart { step: 0 });
1983        state.goto_step(1);
1984        assert_eq!(state.previous_source(), StepOutcome::AtStart { step: 0 });
1985
1986        let frames = state.frames();
1987        assert_eq!(frames.len(), 1);
1988        state.goto_step(6);
1989        let frames = state.frames();
1990        assert_eq!(frames.len(), 3);
1991        assert_eq!(frames[0].function_name.as_deref(), Some("inner"));
1992        assert_eq!(frames[1].function_name.as_deref(), Some("outer"));
1993        let listing = state.source_listing(1).expect("listing");
1994        assert_eq!(listing.current_line, 8);
1995    }
1996
1997    #[test]
1998    fn line_and_function_breakpoints_stop_on_entry() {
1999        let mut state = source_state();
2000        let set =
2001            state.set_breakpoint_target(&BreakpointTarget::SourceLine(SourceBreakpointTarget {
2002                file: Some("C.sol".to_owned()),
2003                line: 4,
2004            }));
2005        let StepOutcome::BreakpointSet(breakpoint) = &set else {
2006            panic!("{set:?}");
2007        };
2008        assert_eq!(breakpoint.label(), "C.sol:4");
2009        // The line is entered once at step 4; returning into it at step 8 does not count.
2010        assert!(matches!(
2011            state.continue_execution(),
2012            StepOutcome::BreakpointHit { step: 4, .. }
2013        ));
2014        assert_eq!(state.continue_execution(), StepOutcome::AtEnd { step: 10 });
2015        // `next` stops at a breakpoint inside the code it steps over.
2016        state.goto_step(3);
2017        assert!(matches!(
2018            state.next_source(),
2019            StepOutcome::BreakpointHit { step: 4, .. }
2020        ));
2021        assert!(matches!(
2022            state.clear_breakpoint_target(&BreakpointTarget::SourceLine(SourceBreakpointTarget {
2023                file: None,
2024                line: 4,
2025            })),
2026            StepOutcome::BreakpointCleared(_)
2027        ));
2028        assert_eq!(
2029            state.set_breakpoint_target(&BreakpointTarget::SourceLine(SourceBreakpointTarget {
2030                file: Some("Other.sol".to_owned()),
2031                line: 4,
2032            })),
2033            StepOutcome::BreakpointError("source file not found: Other.sol".to_owned())
2034        );
2035        // A line inside a statement resolves to the statement and says so.
2036        assert!(matches!(
2037            state.set_breakpoint_target(&BreakpointTarget::SourceLine(SourceBreakpointTarget {
2038                file: None,
2039                line: 10,
2040            })),
2041            StepOutcome::BreakpointSet(breakpoint)
2042                if breakpoint.label() == "C.sol:1 (the statement containing line 10)"
2043        ));
2044
2045        state.goto_step(0);
2046        let set = state.set_breakpoint_target(&BreakpointTarget::Function("inner".to_owned()));
2047        let StepOutcome::BreakpointSet(breakpoint) = &set else {
2048            panic!("{set:?}");
2049        };
2050        assert_eq!(breakpoint.label(), "function C.inner at C.sol:7");
2051        assert!(matches!(
2052            state.continue_execution(),
2053            StepOutcome::BreakpointHit { step: 5, .. }
2054        ));
2055        // `reverse-continue` finds it again from later on.
2056        state.goto_step(10);
2057        assert!(matches!(
2058            state.reverse_continue(),
2059            StepOutcome::BreakpointHit { step: 5, .. }
2060        ));
2061        assert!(matches!(
2062            state.set_breakpoint_target(&BreakpointTarget::Function("nothing".to_owned())),
2063            StepOutcome::BreakpointError(message) if message.contains("no function named")
2064        ));
2065        assert!(matches!(
2066            state.clear_breakpoint_target(&BreakpointTarget::Function("C.inner".to_owned())),
2067            StepOutcome::BreakpointCleared(_)
2068        ));
2069        assert!(matches!(
2070            state.clear_breakpoint_target(&BreakpointTarget::Function("C.inner".to_owned())),
2071            StepOutcome::BreakpointMissing(label) if label == "function C.inner at C.sol:7"
2072        ));
2073    }
2074
2075    fn sample_trace() -> TransactionTrace {
2076        TransactionTrace {
2077            tx_hash: Some("0xabc".to_owned()),
2078            from_addr: "0x1".to_owned(),
2079            to_addr: Some("0x2".to_owned()),
2080            value: "0x0".to_owned(),
2081            input_data: "0xabcd".to_owned(),
2082            gas_used: 21_000,
2083            output: "0x".to_owned(),
2084            success: true,
2085            error: None,
2086            debug_trace_available: true,
2087            contract_address: None,
2088            backend: Some("debug-rpc".to_owned()),
2089            capabilities: Default::default(),
2090            artifacts: Default::default(),
2091            steps: vec![
2092                step(0, "PUSH1", 100, &[]),
2093                step(2, "MSTORE", 97, &[]),
2094                step(3, "SSTORE", 94, &["0x2a", "0x0"]),
2095                {
2096                    let mut last = step(
2097                        4,
2098                        "CALL",
2099                        92,
2100                        &[
2101                            "0x0",
2102                            "0x0",
2103                            "0x0",
2104                            "0x0",
2105                            "0x0",
2106                            "0x0000000000000000000000000000000000000000000000000000000000000002",
2107                            "0x5208",
2108                        ],
2109                    );
2110                    last.error = Some("execution reverted".to_owned());
2111                    last
2112                },
2113            ],
2114        }
2115    }
2116
2117    fn step(pc: u64, op: &str, gas: u64, stack: &[&str]) -> TraceStep {
2118        TraceStep {
2119            pc,
2120            op: op.into(),
2121            gas,
2122            gas_cost: 1,
2123            depth: 1,
2124            stack: stack
2125                .iter()
2126                .map(|word| soldb_core::Word::from(*word))
2127                .collect(),
2128            memory: None,
2129            storage: Some(BTreeMap::new()),
2130            error: None,
2131            snapshot: StepSnapshot::default(),
2132        }
2133    }
2134}