1use soldb_debugger::{
10 state_value, state_variables, ChainStorage, DebugValueStatus, DebugVariable, StateSource,
11 StateVariable, StorageWords, INFERRED_LOCALS_WARNING,
12};
13
14use crate::command::{command_spec, CommandGroup, COMMANDS};
15use crate::response::{
16 ArgumentInfo, BreakpointEvent, BreakpointInfo, FrameInfo, Level, ListedLine, LoadedContract,
17 MemoryInfo, MemoryWord, Output, ResourceInfo, SlotInfo, StateInfo, Stop, StopLocation,
18 StopReason, ValueStatus, VariableInfo,
19};
20use crate::{
21 BreakpointKind, DebuggerCommand, DebuggerInfoCommand, DebuggerState, DisplayMode, StepOutcome,
22 VariablesOrigin,
23};
24
25pub const FRAME_ARGUMENTS_WARNING: &str = "frame arguments are read off the stack, not from \
27compiler-reported variable locations";
28
29pub const FRAME_ARGUMENTS_NOTE: &str = "the calling convention was proved from this trace's \
31calldata; ETHDebug variable locations will replace this once the compiler emits them";
32
33pub const LISTING_RADIUS: u64 = 5;
35
36pub struct Session {
38 state: DebuggerState,
39 chain: Option<Box<dyn ChainStorage>>,
40 locals_warning_shown: bool,
41 arguments_warning_shown: bool,
42}
43
44impl std::fmt::Debug for Session {
45 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 formatter
47 .debug_struct("Session")
48 .field("step", &self.state.current_step)
49 .field(
50 "chain",
51 &self.chain.as_ref().map(|chain| chain.label().to_owned()),
52 )
53 .finish_non_exhaustive()
54 }
55}
56
57impl Session {
58 #[must_use]
59 pub fn new(state: DebuggerState) -> Self {
60 Self {
61 state,
62 chain: None,
63 locals_warning_shown: false,
64 arguments_warning_shown: false,
65 }
66 }
67
68 #[must_use]
70 pub fn with_chain(mut self, chain: Option<Box<dyn ChainStorage>>) -> Self {
71 self.chain = chain;
72 self
73 }
74
75 #[must_use]
76 pub fn state(&self) -> &DebuggerState {
77 &self.state
78 }
79
80 pub fn state_mut(&mut self) -> &mut DebuggerState {
81 &mut self.state
82 }
83
84 #[must_use]
87 pub fn storage_words(&self) -> Option<StorageWords<'_>> {
88 self.state.storage_words_with_chain(self.chain.as_deref())
89 }
90
91 #[must_use]
93 pub fn loaded(&self) -> Output {
94 let mut contracts = Vec::<LoadedContract>::new();
95 for contract in self.state.step_map().map_or(&[][..], |map| map.contracts()) {
96 if contract.info.instructions.is_empty() && contract.source_contents.is_empty() {
97 continue;
98 }
99 let entry = LoadedContract {
100 name: contract.name.clone(),
101 address: contract.address.clone(),
102 };
103 if !contracts.contains(&entry) {
104 contracts.push(entry);
105 }
106 }
107 Output::Loaded {
108 steps: self.state.step_count(),
109 contracts,
110 }
111 }
112
113 #[must_use]
115 pub fn initial_stop(&self) -> Vec<Output> {
116 match self.stop(StopReason::Initial) {
117 Some(stop) => vec![Output::Stop(stop)],
118 None => vec![message(Level::Warning, "No trace loaded.")],
119 }
120 }
121
122 pub fn execute(&mut self, command: DebuggerCommand) -> Vec<Output> {
124 match command {
125 DebuggerCommand::Empty => Vec::new(),
126 DebuggerCommand::Quit => vec![message(Level::Info, "Exiting debugger."), Output::Quit],
127 DebuggerCommand::Tui => vec![Output::Tui],
128 DebuggerCommand::Help(topic) => vec![Self::help(topic.as_deref())],
129 DebuggerCommand::Mode(None) => vec![message(
130 Level::Info,
131 format!("Mode: {}", self.state.display_mode.as_str()),
132 )],
133 DebuggerCommand::Info(DebuggerInfoCommand::Resources { json }) => {
134 vec![self.resources(json)]
135 }
136 DebuggerCommand::Info(DebuggerInfoCommand::Breakpoints) => vec![self.breakpoints()],
137 DebuggerCommand::Info(DebuggerInfoCommand::Storage) => vec![self.storage()],
138 DebuggerCommand::Vars => vec![self.variables(None)],
139 DebuggerCommand::Print(name) => {
140 let name = name.trim();
141 if name.is_empty() {
142 vec![message(Level::Warning, "Usage: print <variable>")]
143 } else {
144 vec![self.variables(Some(name))]
145 }
146 }
147 DebuggerCommand::Backtrace => vec![self.backtrace()],
148 DebuggerCommand::List => vec![self.listing(LISTING_RADIUS)],
149 DebuggerCommand::Memory { offset, length } => vec![self.memory(offset, length)],
150 DebuggerCommand::Calldata => vec![self.calldata()],
151 DebuggerCommand::Stack { limit } => vec![self.stack(limit)],
152 DebuggerCommand::Unknown(text) => vec![message(
153 Level::Warning,
154 format!("Unknown command: {text}; type `help` for the list"),
155 )],
156 command => {
157 let mut outputs = match self.state.apply_command(command) {
158 Some(outcome) => self.outcome(outcome),
159 None => Vec::new(),
160 };
161 if let Some(note) = self.state.take_note() {
162 match outputs.last_mut() {
163 Some(Output::Stop(stop)) => stop.note = Some(note),
164 _ => outputs.push(Output::Message {
165 level: Level::Note,
166 text: note,
167 note: None,
168 }),
169 }
170 }
171 outputs
172 }
173 }
174 }
175
176 fn outcome(&self, outcome: StepOutcome) -> Vec<Output> {
178 let stop = |reason: StopReason| -> Vec<Output> {
179 match self.stop(reason) {
180 Some(stop) => vec![Output::Stop(stop)],
181 None => vec![message(Level::Warning, "No trace loaded.")],
182 }
183 };
184 match outcome {
185 StepOutcome::NoTrace => vec![message(Level::Warning, "No trace loaded.")],
186 StepOutcome::Moved { .. } => stop(StopReason::Moved),
187 StepOutcome::BreakpointHit { breakpoint, .. } => stop(StopReason::Breakpoint {
188 id: breakpoint.id,
189 label: breakpoint.label(),
190 }),
191 StepOutcome::AtEnd { .. } => stop(StopReason::End),
192 StepOutcome::AtStart { .. } => stop(StopReason::Start),
193 StepOutcome::InvalidStep {
194 requested,
195 max_step,
196 } => vec![message(
197 Level::Warning,
198 match max_step {
199 Some(max_step) => format!("Invalid step {requested}; max step is {max_step}"),
200 None => format!("Invalid step {requested}; trace is empty"),
201 },
202 )],
203 StepOutcome::ModeChanged(mode) => {
204 vec![message(Level::Info, format!("Mode: {}", mode.as_str()))]
205 }
206 StepOutcome::BreakpointSet(breakpoint) => vec![Output::Breakpoint {
207 event: BreakpointEvent::Set,
208 id: breakpoint.id,
209 label: breakpoint.label(),
210 }],
211 StepOutcome::BreakpointCleared(breakpoint) => vec![Output::Breakpoint {
212 event: BreakpointEvent::Cleared,
213 id: breakpoint.id,
214 label: breakpoint.label(),
215 }],
216 StepOutcome::BreakpointMissing(label) => vec![message(
217 Level::Warning,
218 format!("No breakpoint set at {label}"),
219 )],
220 StepOutcome::BreakpointError(text) => vec![message(
221 Level::Warning,
222 format!("Could not set breakpoint: {text}"),
223 )],
224 }
225 }
226
227 #[must_use]
229 pub fn stop(&self, reason: StopReason) -> Option<Stop> {
230 let step = self.state.current_step_data()?;
231 let location = self.state.location().map(|location| StopLocation {
232 text: self
233 .state
234 .step_map()
235 .and_then(|map| map.contracts().get(location.key.contract))
236 .and_then(|contract| contract.line_text(location.key.source_id, location.line))
237 .map(str::to_owned),
238 path: location.path,
239 line: location.line,
240 function: location.function_name,
241 generated: location.generated,
242 });
243 let address = self
244 .state
245 .step_map()
246 .and_then(|map| map.executing_address(self.state.current_step))
247 .map(str::to_owned);
248 let stack = (self.state.display_mode == DisplayMode::Assembly).then(|| {
249 step.snapshot_ref()
250 .stack
251 .iter()
252 .rev()
253 .map(|word| word.to_string())
254 .collect()
255 });
256 Some(Stop {
257 reason,
258 step: self.state.current_step,
259 last_step: self.state.step_count().saturating_sub(1),
260 pc: step.pc,
261 op: step.op.to_string(),
262 gas: step.gas,
263 location,
264 address,
265 has_source: self.state.has_source(),
266 stack,
267 note: None,
268 })
269 }
270
271 #[must_use]
272 pub fn breakpoints(&self) -> Output {
273 Output::Breakpoints {
274 breakpoints: self
275 .state
276 .breakpoints()
277 .iter()
278 .map(|breakpoint| BreakpointInfo {
279 id: breakpoint.id,
280 label: breakpoint.label(),
281 })
282 .collect(),
283 }
284 }
285
286 pub fn backtrace(&mut self) -> Output {
288 let frames = self.state.frames();
289 if frames.is_empty() {
290 return message(Level::Warning, "No trace loaded.");
291 }
292 let show_warning =
293 !self.arguments_warning_shown && frames.iter().any(|frame| !frame.arguments.is_empty());
294 let (warning, note) = if show_warning {
295 self.arguments_warning_shown = true;
296 (
297 Some(FRAME_ARGUMENTS_WARNING.to_owned()),
298 Some(FRAME_ARGUMENTS_NOTE.to_owned()),
299 )
300 } else {
301 (None, None)
302 };
303 let frames = frames
304 .iter()
305 .enumerate()
306 .map(|(index, frame)| FrameInfo {
307 index,
308 name: frame
309 .function_name
310 .clone()
311 .or_else(|| frame.contract_name.clone())
312 .or_else(|| frame.address.clone())
313 .unwrap_or_else(|| "<unknown>".to_owned()),
314 arguments: frame
315 .arguments
316 .iter()
317 .map(|argument| ArgumentInfo {
318 name: argument.name.clone(),
319 value: argument.value.display.clone(),
320 })
321 .collect(),
322 location: frame
323 .location
324 .as_ref()
325 .map(|location| format!("{}:{}", location.path, location.line)),
326 address: (frame.external && frame.function_name.is_some())
327 .then(|| frame.address.clone())
328 .flatten(),
329 step: frame.step,
330 pc: frame.pc,
331 })
332 .collect();
333 Output::Backtrace {
334 warning,
335 note,
336 frames,
337 }
338 }
339
340 #[must_use]
342 pub fn listing(&self, radius: u64) -> Output {
343 let Some(listing) = self.state.source_listing(radius) else {
344 return if self.state.has_source() {
345 message(Level::Warning, "No source for this step.")
346 } else {
347 message(
348 Level::Warning,
349 "Cannot list source: no ETHDebug metadata is loaded; start the session with \
350 `--ethdebug-dir <address>:<contract>:<dir>`",
351 )
352 };
353 };
354 Output::Listing {
355 path: listing.path,
356 current_line: listing.current_line,
357 lines: listing
358 .lines
359 .into_iter()
360 .map(|(line, text)| ListedLine {
361 line,
362 text,
363 current: line == listing.current_line,
364 })
365 .collect(),
366 }
367 }
368
369 #[must_use]
371 pub fn memory(&self, offset: Option<u64>, length: Option<u64>) -> Output {
372 let Some(step) = self.state.current_step_data() else {
373 return message(Level::Warning, "No trace loaded.");
374 };
375 let Some(memory) = step.snapshot_ref().memory else {
376 let captured = self
377 .state
378 .trace()
379 .is_some_and(|trace| trace.capabilities.memory);
380 return if captured {
381 message(Level::Info, "Memory is empty at this step.")
382 } else {
383 message(Level::Warning, "Memory was not captured by this backend.")
384 };
385 };
386 let hex = memory.trim_start_matches("0x");
387 if hex.is_empty() {
388 return message(Level::Info, "Memory is empty at this step.");
389 }
390 if !hex.is_ascii() {
391 return message(Level::Info, format!("Memory: {hex}"));
392 }
393 let total = hex.len() / 2;
394 let start = usize::try_from(offset.unwrap_or(0)).unwrap_or(usize::MAX);
395 if start >= total {
396 return message(
397 Level::Warning,
398 format!("Nothing to show: memory is {total} bytes; offset {start} is past the end"),
399 );
400 }
401 let end = length
402 .and_then(|length| usize::try_from(length).ok())
403 .and_then(|length| start.checked_add(length))
404 .map_or(total, |end| end.min(total));
405 let bytes = hex.as_bytes();
406 let mut words = Vec::new();
407 let mut word_start = start;
408 while word_start < end {
409 let word_end = word_start.saturating_add(32).min(end);
410 words.push(MemoryWord {
411 offset: word_start,
412 hex: std::str::from_utf8(&bytes[word_start * 2..word_end * 2])
413 .unwrap_or("")
414 .to_owned(),
415 });
416 word_start = word_end;
417 }
418 Output::Memory(MemoryInfo {
419 total,
420 start,
421 end,
422 words,
423 })
424 }
425
426 #[must_use]
429 pub fn storage(&self) -> Output {
430 let Some(step) = self.state.current_step_data() else {
431 return message(Level::Warning, "No trace loaded.");
432 };
433 let captured = self
434 .state
435 .trace()
436 .is_some_and(|trace| trace.capabilities.storage);
437 if !captured {
438 return message(
439 Level::Warning,
440 "Storage was not captured by this backend; no per-step storage was recorded.",
441 );
442 }
443 let known = self
444 .state
445 .storage_words()
446 .map(|words| words.known())
447 .unwrap_or_default();
448 let changed = step.snapshot_ref().storage_diff;
450 let slots = known
451 .into_iter()
452 .map(|(slot, value)| {
453 let slot = soldb_debugger::short_hex(&slot);
454 let was = changed
455 .iter()
456 .find(|(candidate, _)| normalize_storage_slot(candidate) == slot)
457 .map(|(_, change)| change.before.clone().unwrap_or_else(|| "0x0".to_owned()));
458 SlotInfo {
459 slot,
460 value: soldb_debugger::short_hex(&value),
461 was,
462 }
463 })
464 .collect();
465 Output::Storage {
466 address: self.state.storage_address().map(str::to_owned),
467 slots,
468 }
469 }
470
471 #[must_use]
472 pub fn calldata(&self) -> Output {
473 if self.state.trace().is_none() {
474 return message(Level::Warning, "No trace loaded.");
475 }
476 match self.state.calldata() {
477 Some(data) => Output::Calldata {
478 bytes: data.trim_start_matches("0x").len() / 2,
479 data,
480 },
481 None => message(
482 Level::Warning,
483 "Calldata for this frame was not recorded by the backend; only the root frame's \
484 is known.",
485 ),
486 }
487 }
488
489 #[must_use]
491 pub fn stack(&self, limit: Option<usize>) -> Output {
492 let Some(step) = self.state.current_step_data() else {
493 return message(Level::Warning, "No trace loaded.");
494 };
495 let words = step
496 .snapshot_ref()
497 .stack
498 .iter()
499 .map(|word| word.to_string())
500 .collect::<Vec<_>>();
501 let shown = limit.map_or(words.len(), |limit| limit.min(words.len()));
502 Output::Stack { words, shown }
503 }
504
505 pub fn variables(&mut self, filter: Option<&str>) -> Output {
508 let Some(step) = self.state.current_step_data() else {
509 return message(Level::Warning, "Cannot read variables: no trace is loaded");
510 };
511 let pc = step.pc;
512 let no_metadata = self.state.step_map().is_none_or(|map| {
515 map.contracts()
516 .iter()
517 .all(|c| c.info.instructions.is_empty())
518 });
519 if no_metadata {
520 return message(
521 Level::Warning,
522 "Cannot read variables: no ETHDebug metadata is loaded; start the session with \
523 `--ethdebug-dir <address>:<contract>:<dir>`",
524 );
525 }
526 let variables = self.state.variables();
527 let warning = match &variables {
528 Ok(step_variables)
529 if step_variables.origin == VariablesOrigin::Inferred
530 && !self.locals_warning_shown =>
531 {
532 self.locals_warning_shown = true;
533 Some(INFERRED_LOCALS_WARNING.to_owned())
534 }
535 _ => None,
536 };
537 let (locals, unavailable) = match variables {
538 Ok(step_variables) => (step_variables.variables, None),
539 Err(reason) => (Vec::new(), Some(reason)),
540 };
541 let words = self.storage_words();
542 let layout = self.state.storage_layout();
543 let chain_label = words.as_ref().and_then(StorageWords::chain_label);
544
545 if let Some(name) = filter {
546 if let Some(variable) = locals.iter().find(|variable| variable.name == name) {
547 return Output::Variable {
548 warning,
549 variable: local_info(variable),
550 };
551 }
552 match self.state.local_path(name) {
554 Some(Ok(variable)) => {
555 return Output::Variable {
556 warning,
557 variable: local_info(&variable),
558 }
559 }
560 Some(Err(reason)) => {
561 return message(Level::Warning, format!("Cannot read variable: {reason}"))
562 }
563 None => {}
564 }
565 let (Some(layout), Some(words)) = (layout, words.as_ref()) else {
566 return message(
567 Level::Warning,
568 format!(
569 "No such variable: `{name}` is not in scope at PC {pc}; state variables \
570 need a storage layout, compile with `--storage-layout`"
571 ),
572 );
573 };
574 return match state_value(layout, words, name) {
575 Ok(variable) => Output::Variable {
576 warning,
577 variable: state_info(&variable, chain_label),
578 },
579 Err(error) => Output::Message {
580 level: Level::Warning,
581 text: format!("No such variable: `{name}` is not in scope at PC {pc}; {error}"),
582 note: unavailable.map(|reason| {
583 format!(
584 "locals are unavailable here: {reason}, so a local of that name \
585 cannot be looked up"
586 )
587 }),
588 },
589 };
590 }
591
592 let state = match (layout, words.as_ref()) {
593 (Some(layout), _) if layout.variables.is_empty() => StateInfo::None,
594 (Some(layout), Some(words)) => StateInfo::Variables {
595 variables: state_variables(layout, words)
596 .iter()
597 .map(|variable| state_info(variable, chain_label))
598 .collect(),
599 },
600 (Some(_), None) => StateInfo::NoStorage,
601 (None, _) => StateInfo::NoLayout,
602 };
603 Output::Variables {
604 warning,
605 pc,
606 locals: locals.iter().map(local_info).collect(),
607 unavailable,
608 state,
609 }
610 }
611
612 #[must_use]
614 pub fn resources(&self, json: bool) -> Output {
615 let contracts = self
616 .state
617 .step_map()
618 .map_or(&[][..], |map| map.contracts())
619 .iter()
620 .filter(|contract| !contract.info.instructions.is_empty())
621 .map(|contract| ResourceInfo {
622 name: contract.name.clone(),
623 address: contract.address.clone(),
624 environment: contract.info.environment.clone(),
625 sources: contract.info.sources.values().cloned().collect(),
626 })
627 .collect();
628 Output::Resources { contracts, json }
629 }
630
631 #[must_use]
633 pub fn help(topic: Option<&str>) -> Output {
634 let lines = match topic.map(str::trim).filter(|topic| !topic.is_empty()) {
635 Some(topic) => match command_spec(topic) {
636 Some(spec) => {
637 let mut lines = vec![format!("{} {}", spec.usage, spec.summary)];
638 if !spec.aliases.is_empty() {
639 lines.push(format!("aliases: {}", spec.aliases.join(", ")));
640 }
641 lines.extend(spec.details.iter().map(|line| (*line).to_owned()));
642 lines
643 }
644 None => vec![format!("No help for `{topic}`; type `help` for the list")],
645 },
646 None => {
647 let width = COMMANDS
648 .iter()
649 .map(|spec| spec.usage.len())
650 .max()
651 .unwrap_or(0);
652 let mut lines = Vec::new();
653 for group in CommandGroup::ALL {
654 lines.push(format!("{}:", group.title()));
655 for spec in COMMANDS.iter().filter(|spec| spec.group == group) {
656 lines.push(format!(
657 " {:<width$} {}",
658 spec.usage,
659 spec.summary,
660 width = width
661 ));
662 }
663 }
664 lines.push("Aliases: n s ni fin c | rn rs rni rfin rc | b d i | bt l mem | p | h q; `help <command>` says more.".to_owned());
665 lines
666 }
667 };
668 Output::Help { lines }
669 }
670}
671
672fn message(level: Level, text: impl Into<String>) -> Output {
673 Output::Message {
674 level,
675 text: text.into(),
676 note: None,
677 }
678}
679
680fn status(status: DebugValueStatus) -> ValueStatus {
681 match status {
682 DebugValueStatus::Decoded => ValueStatus::Decoded,
683 DebugValueStatus::Raw => ValueStatus::Raw,
684 DebugValueStatus::Unavailable => ValueStatus::Unavailable,
685 }
686}
687
688fn local_info(variable: &DebugVariable) -> VariableInfo {
691 VariableInfo {
692 name: variable.name.clone(),
693 ty: variable.ty.clone(),
694 value: variable.value.display.clone(),
695 raw: variable.value.raw.clone(),
696 status: status(variable.value.status),
697 place: (variable.value.status != DebugValueStatus::Unavailable)
698 .then(|| format!("{}+{}", variable.location.kind, variable.location.offset)),
699 }
700}
701
702fn state_info(variable: &StateVariable, chain_label: Option<&str>) -> VariableInfo {
703 let mut place = if variable.offset == 0 {
704 format!("slot {}", variable.slot)
705 } else {
706 format!("slot {} + {}", variable.slot, variable.offset)
707 };
708 if variable.source == StateSource::Chain {
709 place.push_str(&format!(", from {}", chain_label.unwrap_or("the chain")));
710 }
711 VariableInfo {
712 name: variable.name.clone(),
713 ty: variable.ty.clone(),
714 value: variable.value.display.clone(),
715 raw: variable.value.raw.clone(),
716 status: status(variable.value.status),
717 place: Some(place),
718 }
719}
720
721fn normalize_storage_slot(slot: &str) -> String {
723 let digits = slot.trim_start_matches("0x").trim_start_matches('0');
724 if digits.is_empty() {
725 "0x0".to_owned()
726 } else {
727 format!("0x{}", digits.to_ascii_lowercase())
728 }
729}
730
731#[must_use]
733pub fn breakpoint_lines(state: &DebuggerState) -> Vec<(String, u64)> {
734 state
735 .breakpoints()
736 .iter()
737 .flat_map(|breakpoint| match &breakpoint.kind {
738 BreakpointKind::Line(lines) => lines
739 .iter()
740 .map(|line| (line.path.clone(), line.key.line))
741 .collect(),
742 _ => Vec::new(),
743 })
744 .collect()
745}