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