1use soldb_core::encode_hex;
10use soldb_debugger::{
11 state_value, state_variables, ChainStorage, DebugValueStatus, DebugVariable, StateSource,
12 StateVariable, StorageWords, INFERRED_LOCALS_WARNING,
13};
14
15use crate::command::{command_spec, CommandGroup, COMMANDS};
16use crate::response::{
17 ArgumentInfo, BreakpointEvent, BreakpointInfo, FrameInfo, Level, ListedLine, LoadedContract,
18 MemoryInfo, MemoryWord, Output, ResourceInfo, SlotInfo, StateInfo, Stop, StopLocation,
19 StopReason, ValueStatus, VariableInfo,
20};
21use crate::{
22 BreakpointKind, DebuggerCommand, DebuggerInfoCommand, DebuggerState, DisplayMode, StepOutcome,
23 VariablesOrigin,
24};
25
26pub const FRAME_ARGUMENTS_WARNING: &str = "frame arguments are read off the stack, not from \
28compiler-reported variable locations";
29
30pub const FRAME_ARGUMENTS_NOTE: &str = "the calling convention was proved from this trace's \
32calldata; ETHDebug variable locations will replace this once the compiler emits them";
33
34pub const LISTING_RADIUS: u64 = 5;
36
37pub struct Session {
39 state: DebuggerState,
40 chain: Option<Box<dyn ChainStorage>>,
41 locals_warning_shown: bool,
42 arguments_warning_shown: bool,
43}
44
45impl std::fmt::Debug for Session {
46 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 formatter
48 .debug_struct("Session")
49 .field("step", &self.state.current_step)
50 .field(
51 "chain",
52 &self.chain.as_ref().map(|chain| chain.label().to_owned()),
53 )
54 .finish_non_exhaustive()
55 }
56}
57
58impl Session {
59 #[must_use]
60 pub fn new(state: DebuggerState) -> Self {
61 Self {
62 state,
63 chain: None,
64 locals_warning_shown: false,
65 arguments_warning_shown: false,
66 }
67 }
68
69 #[must_use]
71 pub fn with_chain(mut self, chain: Option<Box<dyn ChainStorage>>) -> Self {
72 self.chain = chain;
73 self
74 }
75
76 #[must_use]
77 pub fn state(&self) -> &DebuggerState {
78 &self.state
79 }
80
81 pub fn state_mut(&mut self) -> &mut DebuggerState {
82 &mut self.state
83 }
84
85 #[must_use]
88 pub fn storage_words(&self) -> Option<StorageWords<'_>> {
89 self.state.storage_words_with_chain(self.chain.as_deref())
90 }
91
92 #[must_use]
94 pub fn loaded(&self) -> Output {
95 let mut contracts = Vec::<LoadedContract>::new();
96 for contract in self.state.step_map().map_or(&[][..], |map| map.contracts()) {
97 if contract.info.instructions.is_empty() && contract.source_contents.is_empty() {
98 continue;
99 }
100 let entry = LoadedContract {
101 name: contract.name.clone(),
102 address: contract.address.clone(),
103 };
104 if !contracts.contains(&entry) {
105 contracts.push(entry);
106 }
107 }
108 Output::Loaded {
109 steps: self.state.step_count(),
110 contracts,
111 }
112 }
113
114 #[must_use]
116 pub fn initial_stop(&self) -> Vec<Output> {
117 match self.stop(StopReason::Initial) {
118 Some(stop) => vec![Output::Stop(stop)],
119 None => vec![message(Level::Warning, "No trace loaded.")],
120 }
121 }
122
123 pub fn execute(&mut self, command: DebuggerCommand) -> Vec<Output> {
125 match command {
126 DebuggerCommand::Empty => Vec::new(),
127 DebuggerCommand::Quit => vec![message(Level::Info, "Exiting debugger."), Output::Quit],
128 DebuggerCommand::Tui => vec![Output::Tui],
129 DebuggerCommand::Help(topic) => vec![Self::help(topic.as_deref())],
130 DebuggerCommand::Mode(None) => vec![message(
131 Level::Info,
132 format!("Mode: {}", self.state.display_mode.as_str()),
133 )],
134 DebuggerCommand::Info(DebuggerInfoCommand::Resources { json }) => {
135 vec![self.resources(json)]
136 }
137 DebuggerCommand::Info(DebuggerInfoCommand::Breakpoints) => vec![self.breakpoints()],
138 DebuggerCommand::Info(DebuggerInfoCommand::Storage) => vec![self.storage()],
139 DebuggerCommand::Info(DebuggerInfoCommand::TransientStorage) => {
140 vec![self.transient_storage()]
141 }
142 DebuggerCommand::Vars => vec![self.variables(None)],
143 DebuggerCommand::Print(name) => {
144 let name = name.trim();
145 if name.is_empty() {
146 vec![message(Level::Warning, "Usage: print <variable>")]
147 } else {
148 vec![self.variables(Some(name))]
149 }
150 }
151 DebuggerCommand::Backtrace => vec![self.backtrace()],
152 DebuggerCommand::List => vec![self.listing(LISTING_RADIUS)],
153 DebuggerCommand::Memory { offset, length } => vec![self.memory(offset, length)],
154 DebuggerCommand::Calldata => vec![self.calldata()],
155 DebuggerCommand::ReturnData => vec![self.return_data()],
156 DebuggerCommand::Stack { limit } => vec![self.stack(limit)],
157 DebuggerCommand::Unknown(text) => vec![message(
158 Level::Warning,
159 format!("Unknown command: {text}; type `help` for the list"),
160 )],
161 command => {
162 let mut outputs = match self.state.apply_command(command) {
163 Some(outcome) => self.outcome(outcome),
164 None => Vec::new(),
165 };
166 if let Some(note) = self.state.take_note() {
167 match outputs.last_mut() {
168 Some(Output::Stop(stop)) => stop.note = Some(note),
169 _ => outputs.push(Output::Message {
170 level: Level::Note,
171 text: note,
172 note: None,
173 }),
174 }
175 }
176 outputs
177 }
178 }
179 }
180
181 fn outcome(&self, outcome: StepOutcome) -> Vec<Output> {
183 let stop = |reason: StopReason| -> Vec<Output> {
184 match self.stop(reason) {
185 Some(stop) => vec![Output::Stop(stop)],
186 None => vec![message(Level::Warning, "No trace loaded.")],
187 }
188 };
189 match outcome {
190 StepOutcome::NoTrace => vec![message(Level::Warning, "No trace loaded.")],
191 StepOutcome::Moved { .. } => stop(StopReason::Moved),
192 StepOutcome::BreakpointHit { breakpoint, .. } => stop(StopReason::Breakpoint {
193 id: breakpoint.id,
194 label: breakpoint.label(),
195 }),
196 StepOutcome::AtEnd { .. } => stop(StopReason::End),
197 StepOutcome::AtStart { .. } => stop(StopReason::Start),
198 StepOutcome::InvalidStep {
199 requested,
200 max_step,
201 } => vec![message(
202 Level::Warning,
203 match max_step {
204 Some(max_step) => format!("Invalid step {requested}; max step is {max_step}"),
205 None => format!("Invalid step {requested}; trace is empty"),
206 },
207 )],
208 StepOutcome::ModeChanged(mode) => {
209 vec![message(Level::Info, format!("Mode: {}", mode.as_str()))]
210 }
211 StepOutcome::BreakpointSet(breakpoint) => vec![Output::Breakpoint {
212 event: BreakpointEvent::Set,
213 id: breakpoint.id,
214 label: breakpoint.label(),
215 }],
216 StepOutcome::BreakpointCleared(breakpoint) => vec![Output::Breakpoint {
217 event: BreakpointEvent::Cleared,
218 id: breakpoint.id,
219 label: breakpoint.label(),
220 }],
221 StepOutcome::BreakpointMissing(label) => vec![message(
222 Level::Warning,
223 format!("No breakpoint set at {label}"),
224 )],
225 StepOutcome::BreakpointError(text) => vec![message(
226 Level::Warning,
227 format!("Could not set breakpoint: {text}"),
228 )],
229 }
230 }
231
232 #[must_use]
234 pub fn stop(&self, reason: StopReason) -> Option<Stop> {
235 let step = self.state.current_step_data()?;
236 let location = self.state.location().map(|location| StopLocation {
237 text: self
238 .state
239 .step_map()
240 .and_then(|map| map.contracts().get(location.key.contract))
241 .and_then(|contract| contract.line_text(location.key.source_id, location.line))
242 .map(str::to_owned),
243 path: location.path,
244 line: location.line,
245 function: location.function_name,
246 generated: location.generated,
247 });
248 let address = self
249 .state
250 .step_map()
251 .and_then(|map| map.executing_address(self.state.current_step))
252 .map(str::to_owned);
253 let stack = (self.state.display_mode == DisplayMode::Assembly).then(|| {
254 step.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.mnemonic().to_owned(),
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.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 if memory.is_empty() {
392 return message(Level::Info, "Memory is empty at this step.");
393 }
394 let total = memory.len();
395 let start = usize::try_from(offset.unwrap_or(0)).unwrap_or(usize::MAX);
396 if start >= total {
397 return message(
398 Level::Warning,
399 format!("Nothing to show: memory is {total} bytes; offset {start} is past the end"),
400 );
401 }
402 let end = length
403 .and_then(|length| usize::try_from(length).ok())
404 .and_then(|length| start.checked_add(length))
405 .map_or(total, |end| end.min(total));
406 let words = memory[start..end]
407 .chunks(32)
408 .enumerate()
409 .map(|(index, word)| MemoryWord {
410 offset: start + index * 32,
411 hex: encode_hex(word),
412 })
413 .collect();
414 Output::Memory(MemoryInfo {
415 total,
416 start,
417 end,
418 words,
419 })
420 }
421
422 #[must_use]
425 pub fn storage(&self) -> Output {
426 let Some(step) = self.state.current_step_data() else {
427 return message(Level::Warning, "No trace loaded.");
428 };
429 let captured = self
430 .state
431 .trace()
432 .is_some_and(|trace| trace.capabilities.storage);
433 if !captured {
434 return message(
435 Level::Warning,
436 "Storage was not captured by this backend; no per-step storage was recorded.",
437 );
438 }
439 let known = self
440 .state
441 .storage_words()
442 .map(|words| words.known())
443 .unwrap_or_default();
444 let changed = step.storage_diff();
446 let slots = known
447 .into_iter()
448 .map(|(slot, value)| {
449 let slot = soldb_debugger::short_hex(&slot);
450 let was = changed
451 .iter()
452 .find(|(candidate, _)| normalize_storage_slot(candidate) == slot)
453 .map(|(_, change)| change.before.clone().unwrap_or_else(|| "0x0".to_owned()));
454 SlotInfo {
455 slot,
456 value: soldb_debugger::short_hex(&value),
457 was,
458 }
459 })
460 .collect();
461 Output::Storage {
462 address: self.state.storage_address().map(str::to_owned),
463 slots,
464 }
465 }
466
467 #[must_use]
469 pub fn transient_storage(&self) -> Output {
470 if self.state.current_step_data().is_none() {
471 return message(Level::Warning, "No trace loaded.");
472 }
473 let captured = self
474 .state
475 .trace()
476 .is_some_and(|trace| trace.capabilities.transient_storage);
477 if !captured {
478 return message(
479 Level::Warning,
480 "Transient storage is not in this trace; it was recorded without `TLOAD` and `TSTORE` values.",
481 );
482 }
483 let (Some(words), Some(address)) =
484 (self.state.transient_words(), self.state.storage_address())
485 else {
486 return message(
487 Level::Warning,
488 "Transient storage unavailable: this frame's storage belongs to no known account.",
489 );
490 };
491 let slots = words
492 .known()
493 .into_iter()
494 .map(|(slot, value)| SlotInfo {
495 slot: soldb_debugger::short_hex(&slot),
496 value: soldb_debugger::short_hex(&value),
497 was: None,
498 })
499 .collect();
500 Output::TransientStorage {
501 address: Some(address.to_owned()),
502 slots,
503 }
504 }
505
506 #[must_use]
508 pub fn return_data(&self) -> Output {
509 let Some(step) = self.state.current_step_data() else {
510 return message(Level::Warning, "No trace loaded.");
511 };
512 let captured = self
513 .state
514 .trace()
515 .is_some_and(|trace| trace.capabilities.return_data);
516 if !captured {
517 return message(
518 Level::Warning,
519 "Return data is not in this trace; the backend did not record it per step.",
520 );
521 }
522 let data = step.return_data().unwrap_or_default();
523 Output::ReturnData {
524 bytes: data.len() / 2,
525 data: format!("0x{data}"),
526 }
527 }
528
529 #[must_use]
530 pub fn calldata(&self) -> Output {
531 if self.state.trace().is_none() {
532 return message(Level::Warning, "No trace loaded.");
533 }
534 match self.state.calldata() {
535 Some(data) => Output::Calldata {
536 bytes: data.trim_start_matches("0x").len() / 2,
537 data,
538 },
539 None => message(
540 Level::Warning,
541 "Calldata for this frame was not recorded by the backend; only the root frame's \
542 is known.",
543 ),
544 }
545 }
546
547 #[must_use]
549 pub fn stack(&self, limit: Option<usize>) -> Output {
550 let Some(step) = self.state.current_step_data() else {
551 return message(Level::Warning, "No trace loaded.");
552 };
553 let words = step
554 .stack()
555 .iter()
556 .map(|word| word.to_string())
557 .collect::<Vec<_>>();
558 let shown = limit.map_or(words.len(), |limit| limit.min(words.len()));
559 Output::Stack { words, shown }
560 }
561
562 pub fn variables(&mut self, filter: Option<&str>) -> Output {
565 let Some(step) = self.state.current_step_data() else {
566 return message(Level::Warning, "Cannot read variables: no trace is loaded");
567 };
568 let pc = step.pc();
569 let no_metadata = self.state.step_map().is_none_or(|map| {
572 map.contracts()
573 .iter()
574 .all(|c| c.info.instructions.is_empty())
575 });
576 if no_metadata {
577 return message(
578 Level::Warning,
579 "Cannot read variables: no ETHDebug metadata is loaded; start the session with \
580 `--ethdebug-dir <address>:<contract>:<dir>`",
581 );
582 }
583 let variables = self.state.variables();
584 let warning = match &variables {
585 Ok(step_variables)
586 if step_variables.origin == VariablesOrigin::Inferred
587 && !self.locals_warning_shown =>
588 {
589 self.locals_warning_shown = true;
590 Some(INFERRED_LOCALS_WARNING.to_owned())
591 }
592 _ => None,
593 };
594 let (locals, unavailable) = match variables {
595 Ok(step_variables) => (step_variables.variables, None),
596 Err(reason) => (Vec::new(), Some(reason)),
597 };
598 let words = self.storage_words();
599 let layout = self.state.storage_layout();
600 let chain_label = words.as_ref().and_then(StorageWords::chain_label);
601 let transient_words = self.state.transient_words();
602 let transient_layout = self.state.transient_storage_layout();
603
604 if let Some(name) = filter {
605 if let Some(variable) = locals.iter().find(|variable| variable.name == name) {
606 return Output::Variable {
607 warning,
608 variable: local_info(variable),
609 };
610 }
611 match self.state.local_path(name) {
613 Some(Ok(variable)) => {
614 return Output::Variable {
615 warning,
616 variable: local_info(&variable),
617 }
618 }
619 Some(Err(reason)) => {
620 return message(Level::Warning, format!("Cannot read variable: {reason}"))
621 }
622 None => {}
623 }
624 if let (Some(transient_layout), Some(transient_words)) =
626 (transient_layout, transient_words.as_ref())
627 {
628 if let Ok(variable) = state_value(transient_layout, transient_words, name) {
629 return Output::Variable {
630 warning,
631 variable: transient_info(&variable),
632 };
633 }
634 }
635 let (Some(layout), Some(words)) = (layout, words.as_ref()) else {
636 return message(
637 Level::Warning,
638 format!(
639 "No such variable: `{name}` is not in scope at PC {pc}; state variables \
640 need a storage layout, compile with `--storage-layout`"
641 ),
642 );
643 };
644 return match state_value(layout, words, name) {
645 Ok(variable) => Output::Variable {
646 warning,
647 variable: state_info(&variable, chain_label),
648 },
649 Err(error) => Output::Message {
650 level: Level::Warning,
651 text: format!("No such variable: `{name}` is not in scope at PC {pc}; {error}"),
652 note: unavailable.map(|reason| {
653 format!(
654 "locals are unavailable here: {reason}, so a local of that name \
655 cannot be looked up"
656 )
657 }),
658 },
659 };
660 }
661
662 let state = match (layout, words.as_ref()) {
663 (Some(layout), _) if layout.variables.is_empty() => StateInfo::None,
664 (Some(layout), Some(words)) => StateInfo::Variables {
665 variables: state_variables(layout, words)
666 .iter()
667 .map(|variable| state_info(variable, chain_label))
668 .collect(),
669 },
670 (Some(_), None) => StateInfo::NoStorage,
671 (None, _) => StateInfo::NoLayout,
672 };
673 let transient = match (transient_layout, transient_words.as_ref()) {
674 (Some(layout), Some(words)) => state_variables(layout, words)
675 .iter()
676 .map(transient_info)
677 .collect(),
678 _ => Vec::new(),
679 };
680 Output::Variables {
681 warning,
682 pc,
683 locals: locals.iter().map(local_info).collect(),
684 unavailable,
685 state,
686 transient,
687 }
688 }
689
690 #[must_use]
692 pub fn resources(&self, json: bool) -> Output {
693 let contracts = self
694 .state
695 .step_map()
696 .map_or(&[][..], |map| map.contracts())
697 .iter()
698 .filter(|contract| !contract.info.instructions.is_empty())
699 .map(|contract| ResourceInfo {
700 name: contract.name.clone(),
701 address: contract.address.clone(),
702 environment: contract.info.environment.clone(),
703 sources: contract.info.sources.values().cloned().collect(),
704 })
705 .collect();
706 Output::Resources { contracts, json }
707 }
708
709 #[must_use]
711 pub fn help(topic: Option<&str>) -> Output {
712 let lines = match topic.map(str::trim).filter(|topic| !topic.is_empty()) {
713 Some(topic) => match command_spec(topic) {
714 Some(spec) => {
715 let mut lines = vec![format!("{} {}", spec.usage, spec.summary)];
716 if !spec.aliases.is_empty() {
717 lines.push(format!("aliases: {}", spec.aliases.join(", ")));
718 }
719 lines.extend(spec.details.iter().map(|line| (*line).to_owned()));
720 lines
721 }
722 None => vec![format!("No help for `{topic}`; type `help` for the list")],
723 },
724 None => {
725 let width = COMMANDS
726 .iter()
727 .map(|spec| spec.usage.len())
728 .max()
729 .unwrap_or(0);
730 let mut lines = Vec::new();
731 for group in CommandGroup::ALL {
732 lines.push(format!("{}:", group.title()));
733 for spec in COMMANDS.iter().filter(|spec| spec.group == group) {
734 lines.push(format!(
735 " {:<width$} {}",
736 spec.usage,
737 spec.summary,
738 width = width
739 ));
740 }
741 }
742 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());
743 lines
744 }
745 };
746 Output::Help { lines }
747 }
748}
749
750fn message(level: Level, text: impl Into<String>) -> Output {
751 Output::Message {
752 level,
753 text: text.into(),
754 note: None,
755 }
756}
757
758fn status(status: DebugValueStatus) -> ValueStatus {
759 match status {
760 DebugValueStatus::Decoded => ValueStatus::Decoded,
761 DebugValueStatus::Raw => ValueStatus::Raw,
762 DebugValueStatus::Unavailable => ValueStatus::Unavailable,
763 }
764}
765
766fn local_info(variable: &DebugVariable) -> VariableInfo {
769 VariableInfo {
770 name: variable.name.clone(),
771 ty: variable.ty.clone(),
772 value: variable.value.display.clone(),
773 raw: variable.value.raw.clone(),
774 status: status(variable.value.status),
775 place: (variable.value.status != DebugValueStatus::Unavailable)
776 .then(|| format!("{}+{}", variable.location.kind, variable.location.offset)),
777 }
778}
779
780fn state_info(variable: &StateVariable, chain_label: Option<&str>) -> VariableInfo {
781 let mut place = if variable.offset == 0 {
782 format!("slot {}", variable.slot)
783 } else {
784 format!("slot {} + {}", variable.slot, variable.offset)
785 };
786 if variable.source == StateSource::Chain {
787 place.push_str(&format!(", from {}", chain_label.unwrap_or("the chain")));
788 }
789 VariableInfo {
790 name: variable.name.clone(),
791 ty: variable.ty.clone(),
792 value: variable.value.display.clone(),
793 raw: variable.value.raw.clone(),
794 status: status(variable.value.status),
795 place: Some(place),
796 }
797}
798
799fn transient_info(variable: &StateVariable) -> VariableInfo {
801 let place = if variable.offset == 0 {
802 format!("transient slot {}", variable.slot)
803 } else {
804 format!("transient slot {} + {}", variable.slot, variable.offset)
805 };
806 VariableInfo {
807 place: Some(place),
808 ..state_info(variable, None)
809 }
810}
811
812fn normalize_storage_slot(slot: &str) -> String {
814 let digits = slot.trim_start_matches("0x").trim_start_matches('0');
815 if digits.is_empty() {
816 "0x0".to_owned()
817 } else {
818 format!("0x{}", digits.to_ascii_lowercase())
819 }
820}
821
822#[must_use]
824pub fn breakpoint_lines(state: &DebuggerState) -> Vec<(String, u64)> {
825 state
826 .breakpoints()
827 .iter()
828 .flat_map(|breakpoint| match &breakpoint.kind {
829 BreakpointKind::Line(lines) => lines
830 .iter()
831 .map(|line| (line.path.clone(), line.key.line))
832 .collect(),
833 _ => Vec::new(),
834 })
835 .collect()
836}