1use crate::{BreakpointTarget, DisplayMode, SourceBreakpointTarget};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum DebuggerCommand {
14 Next,
15 NextInstruction,
16 Step,
17 Continue,
18 Finish,
20 ReverseNext,
22 ReverseNextInstruction,
24 ReverseStep,
26 ReverseContinue,
28 ReverseFinish,
30 Goto(usize),
31 Vars,
33 Print(String),
38 Info(DebuggerInfoCommand),
39 Backtrace,
41 List,
43 Memory {
45 offset: Option<u64>,
46 length: Option<u64>,
47 },
48 Calldata,
50 ReturnData,
52 Stack {
54 limit: Option<usize>,
55 },
56 Mode(Option<DisplayMode>),
57 Break(BreakpointTarget, Option<String>),
59 Clear(BreakpointTarget),
60 Delete(u32),
62 Help(Option<String>),
63 Tui,
65 Quit,
66 Empty,
67 Unknown(String),
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub enum DebuggerInfoCommand {
72 Resources { json: bool },
73 Breakpoints,
74 Storage,
75 TransientStorage,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum CommandGroup {
81 Stepping,
82 Reverse,
83 Breakpoints,
84 Inspection,
85 Variables,
86 Session,
87}
88
89impl CommandGroup {
90 pub const ALL: [Self; 6] = [
92 Self::Stepping,
93 Self::Reverse,
94 Self::Breakpoints,
95 Self::Inspection,
96 Self::Variables,
97 Self::Session,
98 ];
99
100 #[must_use]
101 pub fn title(self) -> &'static str {
102 match self {
103 Self::Stepping => "Stepping",
104 Self::Reverse => "Reverse",
105 Self::Breakpoints => "Breakpoints",
106 Self::Inspection => "Inspection",
107 Self::Variables => "Variables",
108 Self::Session => "Session",
109 }
110 }
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub struct CommandSpec {
116 pub name: &'static str,
117 pub aliases: &'static [&'static str],
118 pub usage: &'static str,
120 pub summary: &'static str,
122 pub details: &'static [&'static str],
124 pub group: CommandGroup,
125}
126
127impl CommandSpec {
128 #[must_use]
130 pub fn names(&self) -> String {
131 if self.aliases.is_empty() {
132 self.name.to_owned()
133 } else {
134 format!("{} ({})", self.name, self.aliases.join(", "))
135 }
136 }
137
138 fn answers_to(&self, word: &str) -> bool {
139 self.name == word || self.aliases.contains(&word)
140 }
141}
142
143pub const COMMANDS: &[CommandSpec] = &[
145 CommandSpec {
146 name: "next",
147 aliases: &["n"],
148 usage: "next",
149 summary: "run to the next source line, stepping over calls",
150 details: &["Without debug info, moves one instruction."],
151 group: CommandGroup::Stepping,
152 },
153 CommandSpec {
154 name: "step",
155 aliases: &["s"],
156 usage: "step",
157 summary: "run to the next source line, entering calls",
158 details: &["Without debug info, moves one instruction."],
159 group: CommandGroup::Stepping,
160 },
161 CommandSpec {
162 name: "nexti",
163 aliases: &["ni", "stepi", "si"],
164 usage: "nexti",
165 summary: "execute one EVM instruction",
166 details: &[],
167 group: CommandGroup::Stepping,
168 },
169 CommandSpec {
170 name: "finish",
171 aliases: &["fin"],
172 usage: "finish",
173 summary: "run until the current frame returns",
174 details: &[],
175 group: CommandGroup::Stepping,
176 },
177 CommandSpec {
178 name: "continue",
179 aliases: &["c"],
180 usage: "continue",
181 summary: "run to the next breakpoint, or the end of the trace",
182 details: &[],
183 group: CommandGroup::Stepping,
184 },
185 CommandSpec {
186 name: "goto",
187 aliases: &[],
188 usage: "goto <step>",
189 summary: "jump to a step of the recording",
190 details: &[],
191 group: CommandGroup::Stepping,
192 },
193 CommandSpec {
194 name: "reverse-next",
195 aliases: &["rn", "rnext"],
196 usage: "reverse-next",
197 summary: "back to the previous source line, stepping over calls",
198 details: &[],
199 group: CommandGroup::Reverse,
200 },
201 CommandSpec {
202 name: "reverse-step",
203 aliases: &["rs", "rstep"],
204 usage: "reverse-step",
205 summary: "back to the previous source line, entering calls",
206 details: &[],
207 group: CommandGroup::Reverse,
208 },
209 CommandSpec {
210 name: "reverse-nexti",
211 aliases: &["rni", "back", "rnexti", "reverse-stepi", "rsi"],
212 usage: "reverse-nexti",
213 summary: "back one EVM instruction",
214 details: &[],
215 group: CommandGroup::Reverse,
216 },
217 CommandSpec {
218 name: "reverse-finish",
219 aliases: &["rfin", "rfinish"],
220 usage: "reverse-finish",
221 summary: "back to the step that entered the current frame",
222 details: &[],
223 group: CommandGroup::Reverse,
224 },
225 CommandSpec {
226 name: "reverse-continue",
227 aliases: &["rc", "rcontinue"],
228 usage: "reverse-continue",
229 summary: "back to the previous breakpoint, or the start of the trace",
230 details: &[],
231 group: CommandGroup::Reverse,
232 },
233 CommandSpec {
234 name: "break",
235 aliases: &["b"],
236 usage: "break <target> [if <condition>]",
237 summary: "stop at a place, or there only when a condition holds",
238 details: &[
239 "Targets:",
240 " <pc> a program counter",
241 " <file>:<line>, line <line> entering a source line",
242 " <function>, <Contract>.<f> entering a function",
243 " <state variable> a write to it: counter, balances[0xabc], config.limit",
244 " storage <slot> an SSTORE to a slot",
245 " revert a REVERT, or a failing step",
246 " call [<address>] a call, to one address or to any",
247 " op <OPCODE> every execution of an opcode",
248 "A condition compares locals and paths into them (item.tags[1]), state variables,",
249 "the frame's arguments, enum literals (Color.Blue), and pc, gas, depth, op, step,",
250 "with == != < <= > >=, joined with && and ||:",
251 " break Shop.sol:40 if price > 10 && item.color == Color.Blue",
252 ],
253 group: CommandGroup::Breakpoints,
254 },
255 CommandSpec {
256 name: "clear",
257 aliases: &[],
258 usage: "clear <target>",
259 summary: "remove the breakpoint set with that target",
260 details: &[],
261 group: CommandGroup::Breakpoints,
262 },
263 CommandSpec {
264 name: "delete",
265 aliases: &["d"],
266 usage: "delete <n>",
267 summary: "remove breakpoint number n",
268 details: &[],
269 group: CommandGroup::Breakpoints,
270 },
271 CommandSpec {
272 name: "info",
273 aliases: &["i"],
274 usage: "info breakpoints|storage|transient|resources [--json]",
275 summary: "the breakpoints, the storage or transient storage known here, or the loaded debug resources",
276 details: &[
277 "info breakpoints (i b) every breakpoint with its number",
278 "info storage every storage slot known at this step",
279 "info transient every transient storage slot known at this step",
280 "info resources the contracts whose sources are loaded, as text or JSON",
281 ],
282 group: CommandGroup::Breakpoints,
283 },
284 CommandSpec {
285 name: "backtrace",
286 aliases: &["bt", "where"],
287 usage: "backtrace",
288 summary: "the call frames at the current step, innermost first",
289 details: &[
290 "Each frame names its function, or the contract or address executing, where it",
291 "is, and the step and program counter it sits at. A frame entered at its function's",
292 "entry point shows the arguments it was entered with.",
293 ],
294 group: CommandGroup::Inspection,
295 },
296 CommandSpec {
297 name: "list",
298 aliases: &["l"],
299 usage: "list",
300 summary: "the source around the current step",
301 details: &[],
302 group: CommandGroup::Inspection,
303 },
304 CommandSpec {
305 name: "stack",
306 aliases: &[],
307 usage: "stack [<n>]",
308 summary: "the EVM stack at the current step, top first, all of it or the top n words",
309 details: &[
310 "Each word is shown with its index from the bottom of the stack, which is the",
311 "index `vars` reports a variable's slot as.",
312 ],
313 group: CommandGroup::Inspection,
314 },
315 CommandSpec {
316 name: "memory",
317 aliases: &["mem"],
318 usage: "memory [<offset> [<length>]]",
319 summary: "memory at the current step, in 32-byte words",
320 details: &[],
321 group: CommandGroup::Inspection,
322 },
323 CommandSpec {
324 name: "storage",
325 aliases: &[],
326 usage: "storage",
327 summary: "every storage slot known at the current step",
328 details: &["The same as `info storage`."],
329 group: CommandGroup::Inspection,
330 },
331 CommandSpec {
332 name: "transient",
333 aliases: &[],
334 usage: "transient",
335 summary: "every transient storage slot known at the current step",
336 details: &[
337 "The slots `TLOAD` and `TSTORE` touched so far in this frame's account, read",
338 "from the trace. A slot not listed is zero: transient storage starts empty in",
339 "every transaction. The same as `info transient`.",
340 ],
341 group: CommandGroup::Inspection,
342 },
343 CommandSpec {
344 name: "calldata",
345 aliases: &[],
346 usage: "calldata",
347 summary: "the calldata of the current frame",
348 details: &[],
349 group: CommandGroup::Inspection,
350 },
351 CommandSpec {
352 name: "returndata",
353 aliases: &[],
354 usage: "returndata",
355 summary: "the return data buffer at the current step",
356 details: &[
357 "What the frame's last call or create returned or reverted with, which",
358 "`RETURNDATASIZE` and `RETURNDATACOPY` read. Empty until the frame makes a call.",
359 ],
360 group: CommandGroup::Inspection,
361 },
362 CommandSpec {
363 name: "vars",
364 aliases: &["locals"],
365 usage: "vars",
366 summary: "the variables in scope at the current step, and every state variable",
367 details: &[
368 "Locals come from the artifact's ETHDebug variable locations when it carries any.",
369 "For solc's legacy pipeline they are read off the stack through the fixed layout",
370 "its code generator keeps, which the first `vars` of a session says: values can",
371 "be wrong under the optimizer inside one basic block, and a frame that could not",
372 "be placed, or a function the optimizer inlined, shows as unavailable. State",
373 "variables are read through the storage layout of `solc --storage-layout`.",
374 ],
375 group: CommandGroup::Variables,
376 },
377 CommandSpec {
378 name: "print",
379 aliases: &["p"],
380 usage: "print <variable>|<path>",
381 summary: "one variable, or a path into one: item.tags[1], balances[0xabc], stored.owner",
382 details: &[
383 "A path follows struct members, array elements, mapping entries, and `length`",
384 "from a local or a state variable, in any chain.",
385 ],
386 group: CommandGroup::Variables,
387 },
388 CommandSpec {
389 name: "mode",
390 aliases: &[],
391 usage: "mode [source|asm]",
392 summary: "show or switch what a stop prints: the source line, or the stack",
393 details: &[],
394 group: CommandGroup::Session,
395 },
396 CommandSpec {
397 name: "tui",
398 aliases: &[],
399 usage: "tui",
400 summary: "open the full-screen view; q returns to this prompt",
401 details: &[],
402 group: CommandGroup::Session,
403 },
404 CommandSpec {
405 name: "help",
406 aliases: &["h", "?"],
407 usage: "help [<command>]",
408 summary: "this list, or one command's details",
409 details: &[],
410 group: CommandGroup::Session,
411 },
412 CommandSpec {
413 name: "quit",
414 aliases: &["q", "exit"],
415 usage: "quit",
416 summary: "leave the debugger",
417 details: &[],
418 group: CommandGroup::Session,
419 },
420];
421
422#[must_use]
424pub fn command_spec(word: &str) -> Option<&'static CommandSpec> {
425 let word = word.trim().to_ascii_lowercase();
426 COMMANDS.iter().find(|spec| spec.answers_to(&word))
427}
428
429impl DebuggerCommand {
430 #[must_use]
432 pub fn parse(line: &str) -> Self {
433 let line = line.trim();
434 if line.is_empty() {
435 return Self::Empty;
436 }
437 let mut parts = line.split_whitespace();
438 let word = parts.next().unwrap_or_default();
439 let rest = parts.collect::<Vec<_>>().join(" ");
440 let Some(spec) = command_spec(word) else {
441 return Self::Unknown(line.to_owned());
442 };
443 let unknown = || Self::Unknown(line.to_owned());
444 match spec.name {
445 "next" => Self::Next,
446 "nexti" => Self::NextInstruction,
447 "step" => Self::Step,
448 "continue" => Self::Continue,
449 "finish" => Self::Finish,
450 "reverse-next" => Self::ReverseNext,
451 "reverse-nexti" => Self::ReverseNextInstruction,
452 "reverse-step" => Self::ReverseStep,
453 "reverse-continue" => Self::ReverseContinue,
454 "reverse-finish" => Self::ReverseFinish,
455 "goto" => rest.parse::<usize>().map_or_else(|_| unknown(), Self::Goto),
456 "vars" => Self::Vars,
457 "print" => Self::Print(rest),
458 "backtrace" => Self::Backtrace,
459 "list" => Self::List,
460 "memory" => parse_memory_command(&rest).unwrap_or_else(unknown),
461 "calldata" => Self::Calldata,
462 "returndata" => Self::ReturnData,
463 "stack" => {
464 if rest.is_empty() {
465 Self::Stack { limit: None }
466 } else {
467 rest.parse::<usize>()
468 .map_or_else(|_| unknown(), |limit| Self::Stack { limit: Some(limit) })
469 }
470 }
471 "storage" => Self::Info(DebuggerInfoCommand::Storage),
472 "transient" => Self::Info(DebuggerInfoCommand::TransientStorage),
473 "info" => parse_info_command(&rest).map_or_else(unknown, Self::Info),
474 "mode" => Self::Mode(
475 (!rest.is_empty())
476 .then(|| DisplayMode::parse(&rest))
477 .flatten(),
478 ),
479 "break" => {
480 let (target, condition) = split_condition(&rest);
483 parse_breakpoint_target(target)
484 .map_or_else(unknown, |target| Self::Break(target, condition))
485 }
486 "clear" => parse_breakpoint_target(&rest).map_or_else(unknown, Self::Clear),
487 "delete" => rest
488 .trim()
489 .trim_start_matches('#')
490 .parse::<u32>()
491 .map_or_else(|_| unknown(), Self::Delete),
492 "help" => Self::Help((!rest.is_empty()).then_some(rest)),
493 "tui" => Self::Tui,
494 "quit" => Self::Quit,
495 _ => unknown(),
496 }
497 }
498}
499
500fn parse_info_command(input: &str) -> Option<DebuggerInfoCommand> {
501 match input.trim() {
502 "resources" => Some(DebuggerInfoCommand::Resources { json: false }),
503 "resources --json" | "resources json" => {
504 Some(DebuggerInfoCommand::Resources { json: true })
505 }
506 "breakpoints" | "break" | "b" => Some(DebuggerInfoCommand::Breakpoints),
507 "storage" => Some(DebuggerInfoCommand::Storage),
508 "transient" => Some(DebuggerInfoCommand::TransientStorage),
509 _ => None,
510 }
511}
512
513fn parse_memory_command(input: &str) -> Option<DebuggerCommand> {
514 let mut parts = input.split_whitespace();
515 let offset = match parts.next() {
516 Some(text) => Some(parse_u64_arg(text)?),
517 None => None,
518 };
519 let length = match parts.next() {
520 Some(text) => Some(parse_u64_arg(text)?),
521 None => None,
522 };
523 if parts.next().is_some() {
524 return None;
525 }
526 Some(DebuggerCommand::Memory { offset, length })
527}
528
529fn parse_u64_arg(input: &str) -> Option<u64> {
530 let input = input.trim();
531 if let Some(hex) = input.strip_prefix("0x") {
532 u64::from_str_radix(hex, 16).ok()
533 } else {
534 input.parse::<u64>().ok()
535 }
536}
537
538fn parse_breakpoint_target(input: &str) -> Option<BreakpointTarget> {
539 let input = input.trim();
540 if input.is_empty() {
541 return None;
542 }
543 let mut parts = input.splitn(2, char::is_whitespace);
544 let head = parts.next().unwrap_or_default();
545 let rest = parts.next().map(str::trim).unwrap_or_default();
546 match head.to_ascii_lowercase().as_str() {
547 "storage" | "slot" => {
548 (!rest.is_empty()).then(|| BreakpointTarget::Storage(rest.to_owned()))
549 }
550 "revert" if rest.is_empty() => Some(BreakpointTarget::Revert),
551 "call" => Some(BreakpointTarget::Call(
552 (!rest.is_empty()).then(|| rest.to_owned()),
553 )),
554 "op" | "opcode" => (!rest.is_empty() && !rest.contains(char::is_whitespace))
555 .then(|| BreakpointTarget::Opcode(rest.to_owned())),
556 _ => {
557 if let Some(target) = parse_source_breakpoint_target(input) {
558 return Some(BreakpointTarget::SourceLine(target));
559 }
560 if let Some(pc) = parse_u64_arg(input) {
561 return Some(BreakpointTarget::Pc(pc));
562 }
563 if is_function_name(input) {
564 return Some(BreakpointTarget::Function(input.to_owned()));
565 }
566 is_state_path(input).then(|| BreakpointTarget::State(input.to_owned()))
569 }
570 }
571}
572
573fn split_condition(input: &str) -> (&str, Option<String>) {
576 let input = input.trim();
577 match input.find(" if ") {
578 Some(index) => {
579 let condition = input[index + 4..].trim();
580 (
581 input[..index].trim(),
582 (!condition.is_empty()).then(|| condition.to_owned()),
583 )
584 }
585 None => (input, None),
586 }
587}
588
589fn is_state_path(input: &str) -> bool {
592 let head = input.split(['[', '.']).next().unwrap_or_default();
593 !head.is_empty() && is_function_name(head) && input.len() > head.len()
594}
595
596fn is_function_name(input: &str) -> bool {
597 let mut segments = input.split('.');
598 segments.all(|segment| {
599 let mut bytes = segment.bytes();
600 bytes
601 .next()
602 .is_some_and(|byte| byte.is_ascii_alphabetic() || byte == b'_')
603 && bytes.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
604 })
605}
606
607fn parse_source_breakpoint_target(input: &str) -> Option<SourceBreakpointTarget> {
608 let input = input.trim();
609 if let Some(line) = input.strip_prefix("line ") {
610 return parse_source_line_number(line)
611 .map(|line| SourceBreakpointTarget { file: None, line });
612 }
613
614 let (file, line) = input.rsplit_once(':')?;
615 let file = file.trim();
616 if file.is_empty() {
617 return None;
618 }
619 parse_source_line_number(line).map(|line| SourceBreakpointTarget {
620 file: Some(file.to_owned()),
621 line,
622 })
623}
624
625fn parse_source_line_number(input: &str) -> Option<u64> {
626 input.trim().parse::<u64>().ok().filter(|line| *line > 0)
627}
628
629#[cfg(test)]
630mod tests {
631 use super::{command_spec, CommandGroup, DebuggerCommand, COMMANDS};
632
633 #[test]
634 fn every_command_is_named_once_and_parses_by_each_alias() {
635 let mut seen = Vec::new();
636 for spec in COMMANDS {
637 for word in std::iter::once(&spec.name).chain(spec.aliases) {
638 assert!(!seen.contains(word), "`{word}` names two commands");
639 seen.push(word);
640 assert_eq!(command_spec(word).map(|found| found.name), Some(spec.name));
641 }
642 assert!(!spec.summary.is_empty());
643 assert!(spec.usage.starts_with(spec.name), "{}", spec.usage);
644 }
645 assert_eq!(
646 CommandGroup::ALL.len(),
647 6,
648 "every group must be listed for help"
649 );
650 assert_eq!(
651 DebuggerCommand::parse("STACK 4"),
652 DebuggerCommand::Stack { limit: Some(4) }
653 );
654 assert_eq!(DebuggerCommand::parse("tui"), DebuggerCommand::Tui);
655 assert_eq!(DebuggerCommand::parse("?"), DebuggerCommand::Help(None));
656 assert_eq!(
657 DebuggerCommand::parse("help break"),
658 DebuggerCommand::Help(Some("break".to_owned()))
659 );
660 assert_eq!(
661 DebuggerCommand::parse("frobnicate"),
662 DebuggerCommand::Unknown("frobnicate".to_owned())
663 );
664 }
665}