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