Skip to main content

soldb_repl/
lib.rs

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