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