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