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