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::Info(DebuggerInfoCommand::TransientStorage) => {
139 vec![self.transient_storage()]
140 }
141 DebuggerCommand::Vars => vec![self.variables(None)],
142 DebuggerCommand::Print(name) => {
143 let name = name.trim();
144 if name.is_empty() {
145 vec![message(Level::Warning, "Usage: print <variable>")]
146 } else {
147 vec![self.variables(Some(name))]
148 }
149 }
150 DebuggerCommand::Backtrace => vec![self.backtrace()],
151 DebuggerCommand::List => vec![self.listing(LISTING_RADIUS)],
152 DebuggerCommand::Memory { offset, length } => vec![self.memory(offset, length)],
153 DebuggerCommand::Calldata => vec![self.calldata()],
154 DebuggerCommand::ReturnData => vec![self.return_data()],
155 DebuggerCommand::Stack { limit } => vec![self.stack(limit)],
156 DebuggerCommand::Unknown(text) => vec![message(
157 Level::Warning,
158 format!("Unknown command: {text}; type `help` for the list"),
159 )],
160 command => {
161 let mut outputs = match self.state.apply_command(command) {
162 Some(outcome) => self.outcome(outcome),
163 None => Vec::new(),
164 };
165 if let Some(note) = self.state.take_note() {
166 match outputs.last_mut() {
167 Some(Output::Stop(stop)) => stop.note = Some(note),
168 _ => outputs.push(Output::Message {
169 level: Level::Note,
170 text: note,
171 note: None,
172 }),
173 }
174 }
175 outputs
176 }
177 }
178 }
179
180 fn outcome(&self, outcome: StepOutcome) -> Vec<Output> {
182 let stop = |reason: StopReason| -> Vec<Output> {
183 match self.stop(reason) {
184 Some(stop) => vec![Output::Stop(stop)],
185 None => vec![message(Level::Warning, "No trace loaded.")],
186 }
187 };
188 match outcome {
189 StepOutcome::NoTrace => vec![message(Level::Warning, "No trace loaded.")],
190 StepOutcome::Moved { .. } => stop(StopReason::Moved),
191 StepOutcome::BreakpointHit { breakpoint, .. } => stop(StopReason::Breakpoint {
192 id: breakpoint.id,
193 label: breakpoint.label(),
194 }),
195 StepOutcome::AtEnd { .. } => stop(StopReason::End),
196 StepOutcome::AtStart { .. } => stop(StopReason::Start),
197 StepOutcome::InvalidStep {
198 requested,
199 max_step,
200 } => vec![message(
201 Level::Warning,
202 match max_step {
203 Some(max_step) => format!("Invalid step {requested}; max step is {max_step}"),
204 None => format!("Invalid step {requested}; trace is empty"),
205 },
206 )],
207 StepOutcome::ModeChanged(mode) => {
208 vec![message(Level::Info, format!("Mode: {}", mode.as_str()))]
209 }
210 StepOutcome::BreakpointSet(breakpoint) => vec![Output::Breakpoint {
211 event: BreakpointEvent::Set,
212 id: breakpoint.id,
213 label: breakpoint.label(),
214 }],
215 StepOutcome::BreakpointCleared(breakpoint) => vec![Output::Breakpoint {
216 event: BreakpointEvent::Cleared,
217 id: breakpoint.id,
218 label: breakpoint.label(),
219 }],
220 StepOutcome::BreakpointMissing(label) => vec![message(
221 Level::Warning,
222 format!("No breakpoint set at {label}"),
223 )],
224 StepOutcome::BreakpointError(text) => vec![message(
225 Level::Warning,
226 format!("Could not set breakpoint: {text}"),
227 )],
228 }
229 }
230
231 #[must_use]
233 pub fn stop(&self, reason: StopReason) -> Option<Stop> {
234 let step = self.state.current_step_data()?;
235 let location = self.state.location().map(|location| StopLocation {
236 text: self
237 .state
238 .step_map()
239 .and_then(|map| map.contracts().get(location.key.contract))
240 .and_then(|contract| contract.line_text(location.key.source_id, location.line))
241 .map(str::to_owned),
242 path: location.path,
243 line: location.line,
244 function: location.function_name,
245 generated: location.generated,
246 });
247 let address = self
248 .state
249 .step_map()
250 .and_then(|map| map.executing_address(self.state.current_step))
251 .map(str::to_owned);
252 let stack = (self.state.display_mode == DisplayMode::Assembly).then(|| {
253 step.snapshot_ref()
254 .stack
255 .iter()
256 .rev()
257 .map(|word| word.to_string())
258 .collect()
259 });
260 Some(Stop {
261 reason,
262 step: self.state.current_step,
263 last_step: self.state.step_count().saturating_sub(1),
264 pc: step.pc,
265 op: step.op.to_string(),
266 gas: step.gas,
267 location,
268 address,
269 has_source: self.state.has_source(),
270 stack,
271 note: None,
272 })
273 }
274
275 #[must_use]
276 pub fn breakpoints(&self) -> Output {
277 Output::Breakpoints {
278 breakpoints: self
279 .state
280 .breakpoints()
281 .iter()
282 .map(|breakpoint| BreakpointInfo {
283 id: breakpoint.id,
284 label: breakpoint.label(),
285 })
286 .collect(),
287 }
288 }
289
290 pub fn backtrace(&mut self) -> Output {
292 let frames = self.state.frames();
293 if frames.is_empty() {
294 return message(Level::Warning, "No trace loaded.");
295 }
296 let show_warning =
297 !self.arguments_warning_shown && frames.iter().any(|frame| !frame.arguments.is_empty());
298 let (warning, note) = if show_warning {
299 self.arguments_warning_shown = true;
300 (
301 Some(FRAME_ARGUMENTS_WARNING.to_owned()),
302 Some(FRAME_ARGUMENTS_NOTE.to_owned()),
303 )
304 } else {
305 (None, None)
306 };
307 let frames = frames
308 .iter()
309 .enumerate()
310 .map(|(index, frame)| FrameInfo {
311 index,
312 name: frame
313 .function_name
314 .clone()
315 .or_else(|| frame.contract_name.clone())
316 .or_else(|| frame.address.clone())
317 .unwrap_or_else(|| "<unknown>".to_owned()),
318 arguments: frame
319 .arguments
320 .iter()
321 .map(|argument| ArgumentInfo {
322 name: argument.name.clone(),
323 value: argument.value.display.clone(),
324 })
325 .collect(),
326 location: frame
327 .location
328 .as_ref()
329 .map(|location| format!("{}:{}", location.path, location.line)),
330 address: (frame.external && frame.function_name.is_some())
331 .then(|| frame.address.clone())
332 .flatten(),
333 step: frame.step,
334 pc: frame.pc,
335 kind: frame.kind.clone(),
336 })
337 .collect();
338 Output::Backtrace {
339 warning,
340 note,
341 frames,
342 }
343 }
344
345 #[must_use]
347 pub fn listing(&self, radius: u64) -> Output {
348 let Some(listing) = self.state.source_listing(radius) else {
349 return if self.state.has_source() {
350 message(Level::Warning, "No source for this step.")
351 } else {
352 message(
353 Level::Warning,
354 "Cannot list source: no ETHDebug metadata is loaded; start the session with \
355 `--ethdebug-dir <address>:<contract>:<dir>`",
356 )
357 };
358 };
359 Output::Listing {
360 path: listing.path,
361 current_line: listing.current_line,
362 lines: listing
363 .lines
364 .into_iter()
365 .map(|(line, text)| ListedLine {
366 line,
367 text,
368 current: line == listing.current_line,
369 })
370 .collect(),
371 }
372 }
373
374 #[must_use]
376 pub fn memory(&self, offset: Option<u64>, length: Option<u64>) -> Output {
377 let Some(step) = self.state.current_step_data() else {
378 return message(Level::Warning, "No trace loaded.");
379 };
380 let Some(memory) = step.snapshot_ref().memory else {
381 let captured = self
382 .state
383 .trace()
384 .is_some_and(|trace| trace.capabilities.memory);
385 return if captured {
386 message(Level::Info, "Memory is empty at this step.")
387 } else {
388 message(Level::Warning, "Memory was not captured by this backend.")
389 };
390 };
391 let hex = memory.trim_start_matches("0x");
392 if hex.is_empty() {
393 return message(Level::Info, "Memory is empty at this step.");
394 }
395 if !hex.is_ascii() {
396 return message(Level::Info, format!("Memory: {hex}"));
397 }
398 let total = hex.len() / 2;
399 let start = usize::try_from(offset.unwrap_or(0)).unwrap_or(usize::MAX);
400 if start >= total {
401 return message(
402 Level::Warning,
403 format!("Nothing to show: memory is {total} bytes; offset {start} is past the end"),
404 );
405 }
406 let end = length
407 .and_then(|length| usize::try_from(length).ok())
408 .and_then(|length| start.checked_add(length))
409 .map_or(total, |end| end.min(total));
410 let bytes = hex.as_bytes();
411 let mut words = Vec::new();
412 let mut word_start = start;
413 while word_start < end {
414 let word_end = word_start.saturating_add(32).min(end);
415 words.push(MemoryWord {
416 offset: word_start,
417 hex: std::str::from_utf8(&bytes[word_start * 2..word_end * 2])
418 .unwrap_or("")
419 .to_owned(),
420 });
421 word_start = word_end;
422 }
423 Output::Memory(MemoryInfo {
424 total,
425 start,
426 end,
427 words,
428 })
429 }
430
431 #[must_use]
434 pub fn storage(&self) -> Output {
435 let Some(step) = self.state.current_step_data() else {
436 return message(Level::Warning, "No trace loaded.");
437 };
438 let captured = self
439 .state
440 .trace()
441 .is_some_and(|trace| trace.capabilities.storage);
442 if !captured {
443 return message(
444 Level::Warning,
445 "Storage was not captured by this backend; no per-step storage was recorded.",
446 );
447 }
448 let known = self
449 .state
450 .storage_words()
451 .map(|words| words.known())
452 .unwrap_or_default();
453 let changed = step.snapshot_ref().storage_diff;
455 let slots = known
456 .into_iter()
457 .map(|(slot, value)| {
458 let slot = soldb_debugger::short_hex(&slot);
459 let was = changed
460 .iter()
461 .find(|(candidate, _)| normalize_storage_slot(candidate) == slot)
462 .map(|(_, change)| change.before.clone().unwrap_or_else(|| "0x0".to_owned()));
463 SlotInfo {
464 slot,
465 value: soldb_debugger::short_hex(&value),
466 was,
467 }
468 })
469 .collect();
470 Output::Storage {
471 address: self.state.storage_address().map(str::to_owned),
472 slots,
473 }
474 }
475
476 #[must_use]
478 pub fn transient_storage(&self) -> Output {
479 if self.state.current_step_data().is_none() {
480 return message(Level::Warning, "No trace loaded.");
481 }
482 let captured = self
483 .state
484 .trace()
485 .is_some_and(|trace| trace.capabilities.transient_storage);
486 if !captured {
487 return message(
488 Level::Warning,
489 "Transient storage is not in this trace; it was recorded without `TLOAD` and `TSTORE` values.",
490 );
491 }
492 let (Some(words), Some(address)) =
493 (self.state.transient_words(), self.state.storage_address())
494 else {
495 return message(
496 Level::Warning,
497 "Transient storage unavailable: this frame's storage belongs to no known account.",
498 );
499 };
500 let slots = words
501 .known()
502 .into_iter()
503 .map(|(slot, value)| SlotInfo {
504 slot: soldb_debugger::short_hex(&slot),
505 value: soldb_debugger::short_hex(&value),
506 was: None,
507 })
508 .collect();
509 Output::TransientStorage {
510 address: Some(address.to_owned()),
511 slots,
512 }
513 }
514
515 #[must_use]
517 pub fn return_data(&self) -> Output {
518 let Some(step) = self.state.current_step_data() else {
519 return message(Level::Warning, "No trace loaded.");
520 };
521 let captured = self
522 .state
523 .trace()
524 .is_some_and(|trace| trace.capabilities.return_data);
525 if !captured {
526 return message(
527 Level::Warning,
528 "Return data is not in this trace; the backend did not record it per step.",
529 );
530 }
531 let data = step.snapshot_ref().return_data.unwrap_or_default();
532 Output::ReturnData {
533 bytes: data.len() / 2,
534 data: format!("0x{data}"),
535 }
536 }
537
538 #[must_use]
539 pub fn calldata(&self) -> Output {
540 if self.state.trace().is_none() {
541 return message(Level::Warning, "No trace loaded.");
542 }
543 match self.state.calldata() {
544 Some(data) => Output::Calldata {
545 bytes: data.trim_start_matches("0x").len() / 2,
546 data,
547 },
548 None => message(
549 Level::Warning,
550 "Calldata for this frame was not recorded by the backend; only the root frame's \
551 is known.",
552 ),
553 }
554 }
555
556 #[must_use]
558 pub fn stack(&self, limit: Option<usize>) -> Output {
559 let Some(step) = self.state.current_step_data() else {
560 return message(Level::Warning, "No trace loaded.");
561 };
562 let words = step
563 .snapshot_ref()
564 .stack
565 .iter()
566 .map(|word| word.to_string())
567 .collect::<Vec<_>>();
568 let shown = limit.map_or(words.len(), |limit| limit.min(words.len()));
569 Output::Stack { words, shown }
570 }
571
572 pub fn variables(&mut self, filter: Option<&str>) -> Output {
575 let Some(step) = self.state.current_step_data() else {
576 return message(Level::Warning, "Cannot read variables: no trace is loaded");
577 };
578 let pc = step.pc;
579 let no_metadata = self.state.step_map().is_none_or(|map| {
582 map.contracts()
583 .iter()
584 .all(|c| c.info.instructions.is_empty())
585 });
586 if no_metadata {
587 return message(
588 Level::Warning,
589 "Cannot read variables: no ETHDebug metadata is loaded; start the session with \
590 `--ethdebug-dir <address>:<contract>:<dir>`",
591 );
592 }
593 let variables = self.state.variables();
594 let warning = match &variables {
595 Ok(step_variables)
596 if step_variables.origin == VariablesOrigin::Inferred
597 && !self.locals_warning_shown =>
598 {
599 self.locals_warning_shown = true;
600 Some(INFERRED_LOCALS_WARNING.to_owned())
601 }
602 _ => None,
603 };
604 let (locals, unavailable) = match variables {
605 Ok(step_variables) => (step_variables.variables, None),
606 Err(reason) => (Vec::new(), Some(reason)),
607 };
608 let words = self.storage_words();
609 let layout = self.state.storage_layout();
610 let chain_label = words.as_ref().and_then(StorageWords::chain_label);
611 let transient_words = self.state.transient_words();
612 let transient_layout = self.state.transient_storage_layout();
613
614 if let Some(name) = filter {
615 if let Some(variable) = locals.iter().find(|variable| variable.name == name) {
616 return Output::Variable {
617 warning,
618 variable: local_info(variable),
619 };
620 }
621 match self.state.local_path(name) {
623 Some(Ok(variable)) => {
624 return Output::Variable {
625 warning,
626 variable: local_info(&variable),
627 }
628 }
629 Some(Err(reason)) => {
630 return message(Level::Warning, format!("Cannot read variable: {reason}"))
631 }
632 None => {}
633 }
634 if let (Some(transient_layout), Some(transient_words)) =
636 (transient_layout, transient_words.as_ref())
637 {
638 if let Ok(variable) = state_value(transient_layout, transient_words, name) {
639 return Output::Variable {
640 warning,
641 variable: transient_info(&variable),
642 };
643 }
644 }
645 let (Some(layout), Some(words)) = (layout, words.as_ref()) else {
646 return message(
647 Level::Warning,
648 format!(
649 "No such variable: `{name}` is not in scope at PC {pc}; state variables \
650 need a storage layout, compile with `--storage-layout`"
651 ),
652 );
653 };
654 return match state_value(layout, words, name) {
655 Ok(variable) => Output::Variable {
656 warning,
657 variable: state_info(&variable, chain_label),
658 },
659 Err(error) => Output::Message {
660 level: Level::Warning,
661 text: format!("No such variable: `{name}` is not in scope at PC {pc}; {error}"),
662 note: unavailable.map(|reason| {
663 format!(
664 "locals are unavailable here: {reason}, so a local of that name \
665 cannot be looked up"
666 )
667 }),
668 },
669 };
670 }
671
672 let state = match (layout, words.as_ref()) {
673 (Some(layout), _) if layout.variables.is_empty() => StateInfo::None,
674 (Some(layout), Some(words)) => StateInfo::Variables {
675 variables: state_variables(layout, words)
676 .iter()
677 .map(|variable| state_info(variable, chain_label))
678 .collect(),
679 },
680 (Some(_), None) => StateInfo::NoStorage,
681 (None, _) => StateInfo::NoLayout,
682 };
683 let transient = match (transient_layout, transient_words.as_ref()) {
684 (Some(layout), Some(words)) => state_variables(layout, words)
685 .iter()
686 .map(transient_info)
687 .collect(),
688 _ => Vec::new(),
689 };
690 Output::Variables {
691 warning,
692 pc,
693 locals: locals.iter().map(local_info).collect(),
694 unavailable,
695 state,
696 transient,
697 }
698 }
699
700 #[must_use]
702 pub fn resources(&self, json: bool) -> Output {
703 let contracts = self
704 .state
705 .step_map()
706 .map_or(&[][..], |map| map.contracts())
707 .iter()
708 .filter(|contract| !contract.info.instructions.is_empty())
709 .map(|contract| ResourceInfo {
710 name: contract.name.clone(),
711 address: contract.address.clone(),
712 environment: contract.info.environment.clone(),
713 sources: contract.info.sources.values().cloned().collect(),
714 })
715 .collect();
716 Output::Resources { contracts, json }
717 }
718
719 #[must_use]
721 pub fn help(topic: Option<&str>) -> Output {
722 let lines = match topic.map(str::trim).filter(|topic| !topic.is_empty()) {
723 Some(topic) => match command_spec(topic) {
724 Some(spec) => {
725 let mut lines = vec![format!("{} {}", spec.usage, spec.summary)];
726 if !spec.aliases.is_empty() {
727 lines.push(format!("aliases: {}", spec.aliases.join(", ")));
728 }
729 lines.extend(spec.details.iter().map(|line| (*line).to_owned()));
730 lines
731 }
732 None => vec![format!("No help for `{topic}`; type `help` for the list")],
733 },
734 None => {
735 let width = COMMANDS
736 .iter()
737 .map(|spec| spec.usage.len())
738 .max()
739 .unwrap_or(0);
740 let mut lines = Vec::new();
741 for group in CommandGroup::ALL {
742 lines.push(format!("{}:", group.title()));
743 for spec in COMMANDS.iter().filter(|spec| spec.group == group) {
744 lines.push(format!(
745 " {:<width$} {}",
746 spec.usage,
747 spec.summary,
748 width = width
749 ));
750 }
751 }
752 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());
753 lines
754 }
755 };
756 Output::Help { lines }
757 }
758}
759
760fn message(level: Level, text: impl Into<String>) -> Output {
761 Output::Message {
762 level,
763 text: text.into(),
764 note: None,
765 }
766}
767
768fn status(status: DebugValueStatus) -> ValueStatus {
769 match status {
770 DebugValueStatus::Decoded => ValueStatus::Decoded,
771 DebugValueStatus::Raw => ValueStatus::Raw,
772 DebugValueStatus::Unavailable => ValueStatus::Unavailable,
773 }
774}
775
776fn local_info(variable: &DebugVariable) -> VariableInfo {
779 VariableInfo {
780 name: variable.name.clone(),
781 ty: variable.ty.clone(),
782 value: variable.value.display.clone(),
783 raw: variable.value.raw.clone(),
784 status: status(variable.value.status),
785 place: (variable.value.status != DebugValueStatus::Unavailable)
786 .then(|| format!("{}+{}", variable.location.kind, variable.location.offset)),
787 }
788}
789
790fn state_info(variable: &StateVariable, chain_label: Option<&str>) -> VariableInfo {
791 let mut place = if variable.offset == 0 {
792 format!("slot {}", variable.slot)
793 } else {
794 format!("slot {} + {}", variable.slot, variable.offset)
795 };
796 if variable.source == StateSource::Chain {
797 place.push_str(&format!(", from {}", chain_label.unwrap_or("the chain")));
798 }
799 VariableInfo {
800 name: variable.name.clone(),
801 ty: variable.ty.clone(),
802 value: variable.value.display.clone(),
803 raw: variable.value.raw.clone(),
804 status: status(variable.value.status),
805 place: Some(place),
806 }
807}
808
809fn transient_info(variable: &StateVariable) -> VariableInfo {
811 let place = if variable.offset == 0 {
812 format!("transient slot {}", variable.slot)
813 } else {
814 format!("transient slot {} + {}", variable.slot, variable.offset)
815 };
816 VariableInfo {
817 place: Some(place),
818 ..state_info(variable, None)
819 }
820}
821
822fn normalize_storage_slot(slot: &str) -> String {
824 let digits = slot.trim_start_matches("0x").trim_start_matches('0');
825 if digits.is_empty() {
826 "0x0".to_owned()
827 } else {
828 format!("0x{}", digits.to_ascii_lowercase())
829 }
830}
831
832#[must_use]
834pub fn breakpoint_lines(state: &DebuggerState) -> Vec<(String, u64)> {
835 state
836 .breakpoints()
837 .iter()
838 .flat_map(|breakpoint| match &breakpoint.kind {
839 BreakpointKind::Line(lines) => lines
840 .iter()
841 .map(|line| (line.path.clone(), line.key.line))
842 .collect(),
843 _ => Vec::new(),
844 })
845 .collect()
846}