1use std::collections::BTreeSet;
14
15use soldb_core::{TraceStep, TransactionTrace};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum DisplayMode {
19 Source,
20 Assembly,
21}
22
23impl DisplayMode {
24 pub fn parse(input: &str) -> Option<Self> {
25 match input.trim().to_ascii_lowercase().as_str() {
26 "source" | "src" => Some(Self::Source),
27 "asm" | "assembly" => Some(Self::Assembly),
28 _ => None,
29 }
30 }
31
32 pub fn as_str(self) -> &'static str {
33 match self {
34 Self::Source => "source",
35 Self::Assembly => "asm",
36 }
37 }
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum BreakpointTarget {
42 Pc(u64),
43 SourceLine(SourceBreakpointTarget),
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct SourceBreakpointTarget {
48 pub file: Option<String>,
49 pub line: u64,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub enum DebuggerCommand {
54 Next,
55 NextInstruction,
56 Step,
57 Continue,
58 Goto(usize),
59 Vars,
61 Print(String),
66 Info(DebuggerInfoCommand),
67 Mode(Option<DisplayMode>),
68 Break(BreakpointTarget),
69 Clear(BreakpointTarget),
70 Help(Option<String>),
71 Quit,
72 Empty,
73 Unknown(String),
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub enum DebuggerInfoCommand {
78 Resources { json: bool },
79}
80
81impl DebuggerCommand {
82 pub fn parse(line: &str) -> Self {
83 let line = line.trim();
84 if line.is_empty() {
85 return Self::Empty;
86 }
87
88 let mut parts = line.split_whitespace();
89 let command = parts.next().unwrap_or_default().to_ascii_lowercase();
90 let rest = parts.collect::<Vec<_>>().join(" ");
91 match command.as_str() {
92 "next" | "n" => Self::Next,
93 "nexti" | "ni" | "stepi" | "si" => Self::NextInstruction,
94 "step" | "s" => Self::Step,
95 "continue" | "c" => Self::Continue,
96 "vars" | "locals" => Self::Vars,
97 "print" | "p" => Self::Print(rest),
98 "goto" => rest
99 .parse::<usize>()
100 .map(Self::Goto)
101 .unwrap_or_else(|_| Self::Unknown(line.to_owned())),
102 "info" => parse_info_command(&rest)
103 .map(Self::Info)
104 .unwrap_or_else(|| Self::Unknown(line.to_owned())),
105 "mode" => Self::Mode(
106 (!rest.is_empty())
107 .then(|| DisplayMode::parse(&rest))
108 .flatten(),
109 ),
110 "break" | "b" => parse_breakpoint_target(&rest)
111 .map(Self::Break)
112 .unwrap_or_else(|| Self::Unknown(line.to_owned())),
113 "clear" => parse_breakpoint_target(&rest)
114 .map(Self::Clear)
115 .unwrap_or_else(|| Self::Unknown(line.to_owned())),
116 "help" => Self::Help((!rest.is_empty()).then_some(rest)),
117 "exit" | "quit" | "q" => Self::Quit,
118 _ => Self::Unknown(line.to_owned()),
119 }
120 }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub enum StepOutcome {
125 NoTrace,
126 Moved {
127 step: usize,
128 pc: u64,
129 op: String,
130 },
131 BreakpointHit {
132 step: usize,
133 pc: u64,
134 },
135 AtEnd {
136 step: usize,
137 },
138 InvalidStep {
139 requested: usize,
140 max_step: Option<usize>,
141 },
142 ModeChanged(DisplayMode),
143 BreakpointSet(u64),
144 BreakpointCleared(u64),
145 BreakpointMissing(u64),
146}
147
148#[derive(Debug, Clone, PartialEq, Eq)]
149pub struct DebuggerState {
150 pub current_step: usize,
151 pub display_mode: DisplayMode,
152 pub breakpoints: BTreeSet<u64>,
153 trace: Option<TransactionTrace>,
154}
155
156impl Default for DebuggerState {
157 fn default() -> Self {
158 Self {
159 current_step: 0,
160 display_mode: DisplayMode::Source,
161 breakpoints: BTreeSet::new(),
162 trace: None,
163 }
164 }
165}
166
167impl DebuggerState {
168 pub fn new() -> Self {
169 Self::default()
170 }
171
172 pub fn load_trace(&mut self, trace: TransactionTrace) {
173 self.trace = Some(trace);
174 self.current_step = 0;
175 }
176
177 pub fn trace(&self) -> Option<&TransactionTrace> {
178 self.trace.as_ref()
179 }
180
181 pub fn current_step_data(&self) -> Option<&TraceStep> {
182 self.trace
183 .as_ref()
184 .and_then(|trace| trace.steps.get(self.current_step))
185 }
186
187 pub fn step_count(&self) -> usize {
188 self.trace
189 .as_ref()
190 .map(|trace| trace.steps.len())
191 .unwrap_or(0)
192 }
193
194 pub fn set_display_mode(&mut self, mode: DisplayMode) -> StepOutcome {
195 self.display_mode = mode;
196 StepOutcome::ModeChanged(mode)
197 }
198
199 pub fn next_instruction(&mut self) -> StepOutcome {
200 let Some(trace) = &self.trace else {
201 return StepOutcome::NoTrace;
202 };
203 if self.current_step >= trace.steps.len().saturating_sub(1) {
204 return StepOutcome::AtEnd {
205 step: self.current_step,
206 };
207 }
208
209 self.current_step += 1;
210 self.outcome_for_current_step()
211 }
212
213 pub fn next_source(&mut self) -> StepOutcome {
214 self.next_instruction()
215 }
216
217 pub fn step_into(&mut self) -> StepOutcome {
218 self.next_instruction()
219 }
220
221 pub fn continue_execution(&mut self) -> StepOutcome {
222 let Some(trace) = &self.trace else {
223 return StepOutcome::NoTrace;
224 };
225 if self.current_step >= trace.steps.len().saturating_sub(1) {
226 return StepOutcome::AtEnd {
227 step: self.current_step,
228 };
229 }
230
231 while self.current_step < trace.steps.len().saturating_sub(1) {
232 self.current_step += 1;
233 let step = &trace.steps[self.current_step];
234 if self.breakpoints.contains(&step.pc) {
235 return StepOutcome::BreakpointHit {
236 step: self.current_step,
237 pc: step.pc,
238 };
239 }
240 }
241
242 StepOutcome::AtEnd {
243 step: self.current_step,
244 }
245 }
246
247 pub fn goto_step(&mut self, step: usize) -> StepOutcome {
248 let Some(trace) = &self.trace else {
249 return StepOutcome::NoTrace;
250 };
251 if step >= trace.steps.len() {
252 return StepOutcome::InvalidStep {
253 requested: step,
254 max_step: trace.steps.len().checked_sub(1),
255 };
256 }
257 self.current_step = step;
258 self.outcome_for_current_step()
259 }
260
261 pub fn set_breakpoint(&mut self, pc: u64) -> StepOutcome {
262 self.breakpoints.insert(pc);
263 StepOutcome::BreakpointSet(pc)
264 }
265
266 pub fn clear_breakpoint(&mut self, pc: u64) -> StepOutcome {
267 if self.breakpoints.remove(&pc) {
268 StepOutcome::BreakpointCleared(pc)
269 } else {
270 StepOutcome::BreakpointMissing(pc)
271 }
272 }
273
274 pub fn apply_command(&mut self, command: DebuggerCommand) -> Option<StepOutcome> {
275 match command {
276 DebuggerCommand::Next => Some(self.next_source()),
277 DebuggerCommand::NextInstruction => Some(self.next_instruction()),
278 DebuggerCommand::Step => Some(self.step_into()),
279 DebuggerCommand::Continue => Some(self.continue_execution()),
280 DebuggerCommand::Goto(step) => Some(self.goto_step(step)),
281 DebuggerCommand::Mode(Some(mode)) => Some(self.set_display_mode(mode)),
282 DebuggerCommand::Break(BreakpointTarget::Pc(pc)) => Some(self.set_breakpoint(pc)),
283 DebuggerCommand::Clear(BreakpointTarget::Pc(pc)) => Some(self.clear_breakpoint(pc)),
284 DebuggerCommand::Break(BreakpointTarget::SourceLine(_))
285 | DebuggerCommand::Clear(BreakpointTarget::SourceLine(_)) => None,
286 DebuggerCommand::Empty
287 | DebuggerCommand::Help(_)
288 | DebuggerCommand::Info(_)
289 | DebuggerCommand::Mode(None)
290 | DebuggerCommand::Print(_)
291 | DebuggerCommand::Quit
292 | DebuggerCommand::Unknown(_)
293 | DebuggerCommand::Vars => None,
294 }
295 }
296
297 fn outcome_for_current_step(&self) -> StepOutcome {
298 let Some(step) = self.current_step_data() else {
299 return StepOutcome::NoTrace;
300 };
301 if self.breakpoints.contains(&step.pc) {
302 return StepOutcome::BreakpointHit {
303 step: self.current_step,
304 pc: step.pc,
305 };
306 }
307 StepOutcome::Moved {
308 step: self.current_step,
309 pc: step.pc,
310 op: step.op.clone(),
311 }
312 }
313}
314
315fn parse_info_command(input: &str) -> Option<DebuggerInfoCommand> {
316 match input.trim() {
317 "resources" => Some(DebuggerInfoCommand::Resources { json: false }),
318 "resources --json" | "resources json" => {
319 Some(DebuggerInfoCommand::Resources { json: true })
320 }
321 _ => None,
322 }
323}
324
325fn parse_u64_arg(input: &str) -> Option<u64> {
326 let input = input.trim();
327 if let Some(hex) = input.strip_prefix("0x") {
328 u64::from_str_radix(hex, 16).ok()
329 } else {
330 input.parse::<u64>().ok()
331 }
332}
333
334fn parse_breakpoint_target(input: &str) -> Option<BreakpointTarget> {
335 parse_source_breakpoint_target(input)
336 .map(BreakpointTarget::SourceLine)
337 .or_else(|| parse_u64_arg(input).map(BreakpointTarget::Pc))
338}
339
340fn parse_source_breakpoint_target(input: &str) -> Option<SourceBreakpointTarget> {
341 let input = input.trim();
342 if let Some(line) = input.strip_prefix("line ") {
343 return parse_source_line_number(line)
344 .map(|line| SourceBreakpointTarget { file: None, line });
345 }
346
347 let (file, line) = input.rsplit_once(':')?;
348 let file = file.trim();
349 if file.is_empty() {
350 return None;
351 }
352 parse_source_line_number(line).map(|line| SourceBreakpointTarget {
353 file: Some(file.to_owned()),
354 line,
355 })
356}
357
358fn parse_source_line_number(input: &str) -> Option<u64> {
359 input.trim().parse::<u64>().ok().filter(|line| *line > 0)
360}
361
362#[cfg(test)]
363mod tests {
364 use std::collections::BTreeMap;
365
366 use super::{
367 BreakpointTarget, DebuggerCommand, DebuggerInfoCommand, DebuggerState, DisplayMode,
368 SourceBreakpointTarget, StepOutcome,
369 };
370 use soldb_core::{TraceStep, TransactionTrace};
371
372 #[test]
373 fn parses_repl_commands_and_aliases() {
374 assert_eq!(DebuggerCommand::parse(""), DebuggerCommand::Empty);
375 assert_eq!(DebuggerCommand::parse("n"), DebuggerCommand::Next);
376 assert_eq!(
377 DebuggerCommand::parse("stepi"),
378 DebuggerCommand::NextInstruction
379 );
380 assert_eq!(DebuggerCommand::parse("s"), DebuggerCommand::Step);
381 assert_eq!(DebuggerCommand::parse("c"), DebuggerCommand::Continue);
382 assert_eq!(DebuggerCommand::parse("goto 2"), DebuggerCommand::Goto(2));
383 assert_eq!(DebuggerCommand::parse("vars"), DebuggerCommand::Vars);
384 assert_eq!(DebuggerCommand::parse("LOCALS"), DebuggerCommand::Vars);
385 assert_eq!(
386 DebuggerCommand::parse("print balance"),
387 DebuggerCommand::Print("balance".to_owned())
388 );
389 assert_eq!(
390 DebuggerCommand::parse("p balance "),
391 DebuggerCommand::Print("balance".to_owned())
392 );
393 assert_eq!(
394 DebuggerCommand::parse("print"),
395 DebuggerCommand::Print(String::new())
396 );
397 assert_eq!(
398 DebuggerCommand::parse("info resources"),
399 DebuggerCommand::Info(DebuggerInfoCommand::Resources { json: false })
400 );
401 assert_eq!(
402 DebuggerCommand::parse("info resources --json"),
403 DebuggerCommand::Info(DebuggerInfoCommand::Resources { json: true })
404 );
405 assert_eq!(
406 DebuggerCommand::parse("mode assembly"),
407 DebuggerCommand::Mode(Some(DisplayMode::Assembly))
408 );
409 assert_eq!(
410 DebuggerCommand::parse("break 0x10"),
411 DebuggerCommand::Break(BreakpointTarget::Pc(16))
412 );
413 assert_eq!(
414 DebuggerCommand::parse("break Counter.sol:7"),
415 DebuggerCommand::Break(BreakpointTarget::SourceLine(SourceBreakpointTarget {
416 file: Some("Counter.sol".to_owned()),
417 line: 7
418 }))
419 );
420 assert_eq!(
421 DebuggerCommand::parse("break line 7"),
422 DebuggerCommand::Break(BreakpointTarget::SourceLine(SourceBreakpointTarget {
423 file: None,
424 line: 7
425 }))
426 );
427 assert_eq!(
428 DebuggerCommand::parse("clear 8"),
429 DebuggerCommand::Clear(BreakpointTarget::Pc(8))
430 );
431 assert_eq!(
432 DebuggerCommand::parse("help mode"),
433 DebuggerCommand::Help(Some("mode".to_owned()))
434 );
435 assert_eq!(DebuggerCommand::parse("q"), DebuggerCommand::Quit);
436 assert_eq!(
437 DebuggerCommand::parse("wat"),
438 DebuggerCommand::Unknown("wat".to_owned())
439 );
440 }
441
442 #[test]
443 fn loads_trace_and_steps_instruction_by_instruction() {
444 let mut state = DebuggerState::new();
445 assert_eq!(state.next_instruction(), StepOutcome::NoTrace);
446
447 state.load_trace(sample_trace());
448 assert_eq!(state.step_count(), 4);
449 assert_eq!(state.current_step_data().expect("step").op, "PUSH1");
450
451 assert_eq!(
452 state.next_instruction(),
453 StepOutcome::Moved {
454 step: 1,
455 pc: 2,
456 op: "MSTORE".to_owned()
457 }
458 );
459 assert_eq!(state.current_step, 1);
460 }
461
462 #[test]
463 fn continues_until_breakpoint_or_end() {
464 let mut state = DebuggerState::new();
465 state.load_trace(sample_trace());
466 assert_eq!(state.set_breakpoint(3), StepOutcome::BreakpointSet(3));
467
468 assert_eq!(
469 state.continue_execution(),
470 StepOutcome::BreakpointHit { step: 2, pc: 3 }
471 );
472 assert_eq!(state.current_step, 2);
473
474 assert_eq!(state.clear_breakpoint(3), StepOutcome::BreakpointCleared(3));
475 assert_eq!(state.clear_breakpoint(3), StepOutcome::BreakpointMissing(3));
476 assert_eq!(state.continue_execution(), StepOutcome::AtEnd { step: 3 });
477 assert_eq!(state.continue_execution(), StepOutcome::AtEnd { step: 3 });
478 }
479
480 #[test]
481 fn goto_validates_trace_bounds() {
482 let mut state = DebuggerState::new();
483 assert_eq!(state.goto_step(1), StepOutcome::NoTrace);
484
485 state.load_trace(sample_trace());
486 assert_eq!(
487 state.goto_step(3),
488 StepOutcome::Moved {
489 step: 3,
490 pc: 4,
491 op: "STOP".to_owned()
492 }
493 );
494 assert_eq!(
495 state.goto_step(99),
496 StepOutcome::InvalidStep {
497 requested: 99,
498 max_step: Some(3)
499 }
500 );
501 }
502
503 #[test]
504 fn applies_state_changing_commands() {
505 let mut state = DebuggerState::new();
506 state.load_trace(sample_trace());
507
508 assert_eq!(
509 state.apply_command(DebuggerCommand::Mode(Some(DisplayMode::Assembly))),
510 Some(StepOutcome::ModeChanged(DisplayMode::Assembly))
511 );
512 assert_eq!(state.display_mode.as_str(), "asm");
513 assert_eq!(
514 state.apply_command(DebuggerCommand::Break(BreakpointTarget::Pc(2))),
515 Some(StepOutcome::BreakpointSet(2))
516 );
517 assert_eq!(
518 state.apply_command(DebuggerCommand::NextInstruction),
519 Some(StepOutcome::BreakpointHit { step: 1, pc: 2 })
520 );
521 assert_eq!(state.apply_command(DebuggerCommand::Help(None)), None);
522 assert_eq!(state.apply_command(DebuggerCommand::Quit), None);
523 }
524
525 fn sample_trace() -> TransactionTrace {
526 TransactionTrace {
527 tx_hash: Some("0xabc".to_owned()),
528 from_addr: "0x1".to_owned(),
529 to_addr: Some("0x2".to_owned()),
530 value: "0x0".to_owned(),
531 input_data: "0x".to_owned(),
532 gas_used: 21_000,
533 output: "0x".to_owned(),
534 success: true,
535 error: None,
536 debug_trace_available: true,
537 contract_address: None,
538 backend: Some("debug-rpc".to_owned()),
539 capabilities: Default::default(),
540 artifacts: Default::default(),
541 steps: vec![
542 step(0, "PUSH1", 100),
543 step(2, "MSTORE", 97),
544 step(3, "CALLDATASIZE", 94),
545 step(4, "STOP", 92),
546 ],
547 }
548 }
549
550 fn step(pc: u64, op: &str, gas: u64) -> TraceStep {
551 TraceStep {
552 pc,
553 op: op.to_owned(),
554 gas,
555 gas_cost: 1,
556 depth: 0,
557 stack: Vec::new(),
558 memory: None,
559 storage: Some(BTreeMap::new()),
560 error: None,
561 snapshot: Default::default(),
562 }
563 }
564}