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