1use super::storage::{
4 StorageAccess, StorageSpace, hex_u256, storage_access_at, storage_accesses_until,
5};
6use crate::{DebugNode, DebuggerLayout, ExitReason, debugger::DebuggerContext};
7use alloy_primitives::{Address, U256, hex, map::IndexMap};
8use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};
9use foundry_compilers::artifacts::sourcemap::SourceElement;
10use foundry_evm_core::buffer::{BufferKind, get_buffer_accesses};
11use foundry_evm_traces::debug::SourceData;
12use foundry_tui::TuiApp;
13use ratatui::Frame;
14use revm::bytecode::opcode::OpCode;
15use revm_inspectors::tracing::types::{CallKind, CallTraceStep};
16use std::ops::ControlFlow;
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub(crate) enum StatusKind {
20 Info,
21 Error,
22}
23
24#[derive(Clone, Debug, PartialEq, Eq)]
25pub(crate) struct StatusMessage {
26 pub(crate) kind: StatusKind,
27 pub(crate) text: String,
28}
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub(crate) struct ActiveInternalCallLocation {
32 pub(crate) trace_node_idx: usize,
33 pub(crate) marker_node_idx: usize,
34 pub(crate) marker_step_idx: usize,
35 pub(crate) entry_step: usize,
36 pub(crate) end_step: usize,
37}
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub(crate) struct ActiveInternalCallCache {
41 pub(crate) current_node_idx: usize,
42 pub(crate) trace_node_idx: usize,
43 pub(crate) absolute_step: usize,
44 pub(crate) location: Option<ActiveInternalCallLocation>,
45}
46
47impl ActiveInternalCallCache {
48 pub(crate) const fn matches(
49 self,
50 current_node_idx: usize,
51 trace_node_idx: usize,
52 absolute_step: usize,
53 ) -> bool {
54 self.current_node_idx == current_node_idx
55 && self.trace_node_idx == trace_node_idx
56 && self.absolute_step == absolute_step
57 }
58}
59
60#[derive(Default)]
62pub(crate) struct DrawMemory {
63 pub(crate) inner_call_index: usize,
64 pub(crate) current_buf_startline: usize,
65 pub(crate) current_storage_startline: usize,
66 pub(crate) current_stack_startline: usize,
67 pub(crate) active_internal_call: Option<ActiveInternalCallCache>,
68}
69
70pub(crate) struct TUIContext<'a> {
71 pub(crate) debugger_context: &'a mut DebuggerContext,
72
73 pub(crate) key_buffer: String,
75 pub(crate) pc_input: Option<String>,
77 pub(crate) buffer_offset_input: Option<String>,
79 pub(crate) command_input: Option<String>,
81 pub(crate) opcode_search_input: Option<String>,
83 pub(crate) last_opcode_search: Option<String>,
85 pub(crate) status: Option<StatusMessage>,
87 pub(crate) current_step: usize,
89 pub(crate) draw_memory: DrawMemory,
90 pub(crate) opcode_list: Vec<String>,
91 pub(crate) last_index: usize,
92
93 pub(crate) stack_labels: bool,
94 pub(crate) buf_utf: bool,
96 pub(crate) show_shortcuts: bool,
97 pub(crate) show_opcodes: bool,
98 pub(crate) show_source: bool,
99 pub(crate) show_variables: bool,
100 pub(crate) show_stack: bool,
101 pub(crate) show_data: bool,
102 pub(crate) active_buffer: BufferKind,
104 active_storage: Option<StorageSpace>,
105}
106
107impl<'a> TUIContext<'a> {
108 pub(crate) fn new(debugger_context: &'a mut DebuggerContext) -> Self {
109 TUIContext {
110 debugger_context,
111
112 key_buffer: String::with_capacity(64),
113 pc_input: None,
114 buffer_offset_input: None,
115 command_input: None,
116 opcode_search_input: None,
117 last_opcode_search: None,
118 status: None,
119 current_step: 0,
120 draw_memory: DrawMemory::default(),
121 opcode_list: Vec::new(),
122 last_index: 0,
123
124 stack_labels: false,
125 buf_utf: false,
126 show_shortcuts: true,
127 show_opcodes: true,
128 show_source: true,
129 show_variables: true,
130 show_stack: true,
131 show_data: true,
132 active_buffer: BufferKind::Memory,
133 active_storage: None,
134 }
135 }
136
137 pub(crate) fn init(&mut self) {
138 self.gen_opcode_list();
139 }
140
141 pub(crate) fn debug_arena(&self) -> &[DebugNode] {
142 &self.debugger_context.debug_arena
143 }
144
145 pub(crate) const fn layout(&self) -> DebuggerLayout {
146 self.debugger_context.layout
147 }
148
149 pub(crate) fn debug_call(&self) -> &DebugNode {
150 &self.debug_arena()[self.draw_memory.inner_call_index]
151 }
152
153 pub(crate) fn address(&self) -> &Address {
155 &self.debug_call().address
156 }
157
158 pub(crate) fn call_kind(&self) -> CallKind {
160 self.debug_call().kind
161 }
162
163 pub(crate) fn debug_steps(&self) -> &[CallTraceStep] {
165 &self.debug_call().steps
166 }
167
168 pub(crate) fn current_step(&self) -> &CallTraceStep {
170 &self.debug_steps()[self.current_step]
171 }
172
173 fn gen_opcode_list(&mut self) {
174 self.opcode_list.clear();
175 let debug_steps =
176 &self.debugger_context.debug_arena[self.draw_memory.inner_call_index].steps;
177 for step in debug_steps {
178 self.opcode_list.push(pretty_opcode(step));
179 }
180 }
181
182 fn gen_opcode_list_if_necessary(&mut self) {
183 if self.last_index != self.draw_memory.inner_call_index {
184 self.gen_opcode_list();
185 self.last_index = self.draw_memory.inner_call_index;
186 }
187 }
188
189 fn active_buffer(&self) -> &[u8] {
190 self.buffer(&self.active_buffer)
191 }
192
193 pub(super) const fn active_storage(&self) -> Option<StorageSpace> {
194 self.active_storage
195 }
196
197 pub(super) fn storage_accesses(&self, space: StorageSpace) -> IndexMap<U256, StorageAccess> {
198 storage_accesses_until(
199 self.debug_arena(),
200 self.draw_memory.inner_call_index,
201 self.current_step,
202 space,
203 )
204 }
205
206 fn active_data_len(&self) -> usize {
207 self.active_storage.map_or_else(
208 || self.active_buffer().len().div_ceil(32),
209 |space| self.storage_accesses(space).len(),
210 )
211 }
212
213 fn buffer(&self, buffer: &BufferKind) -> &[u8] {
214 match buffer {
215 BufferKind::Memory => self.current_step().memory.as_ref().map_or(&[], |m| m.as_bytes()),
216 BufferKind::Calldata => &self.debug_call().calldata,
217 BufferKind::Returndata => &self.current_step().returndata,
218 }
219 }
220
221 pub(crate) const fn active_buffer_name(&self) -> &'static str {
222 buffer_name(&self.active_buffer)
223 }
224
225 pub(crate) fn src_map(&self) -> Result<(SourceElement, &SourceData), String> {
227 let address = self.address();
228 let Some(contract_name) = self.debugger_context.identified_contracts.get(address) else {
229 return Err(format!("Unknown contract at address {address}"));
230 };
231
232 self.debugger_context
233 .contracts_sources
234 .find_source_mapping(
235 contract_name,
236 self.current_step().pc as u32,
237 self.debug_call().kind.is_any_create(),
238 )
239 .ok_or_else(|| format!("No source map for contract {contract_name}"))
240 }
241}
242
243impl TUIContext<'_> {
244 pub(crate) fn handle_event(&mut self, event: Event) -> ControlFlow<ExitReason> {
245 let ret = match event {
246 Event::Key(event) => self.handle_key_event(event),
247 Event::Mouse(event) => self.handle_mouse_event(event),
248 _ => ControlFlow::Continue(()),
249 };
250 self.gen_opcode_list_if_necessary();
252 ret
253 }
254
255 fn handle_key_event(&mut self, event: KeyEvent) -> ControlFlow<ExitReason> {
256 if self.opcode_search_input.is_some() {
257 self.handle_opcode_search_input_key_event(event);
258 return ControlFlow::Continue(());
259 }
260
261 if self.pc_input.is_some() {
262 self.handle_pc_input_key_event(event);
263 return ControlFlow::Continue(());
264 }
265
266 if self.buffer_offset_input.is_some() {
267 self.handle_buffer_offset_input_key_event(event);
268 return ControlFlow::Continue(());
269 }
270
271 if self.command_input.is_some() {
272 self.handle_command_input_key_event(event);
273 return ControlFlow::Continue(());
274 }
275
276 if let KeyCode::Char(c) = event.code
278 && c.is_alphabetic()
279 && self.key_buffer.starts_with('\'')
280 {
281 self.handle_breakpoint(c);
282 return ControlFlow::Continue(());
283 }
284
285 let control = event.modifiers.contains(KeyModifiers::CONTROL);
286
287 match event.code {
288 KeyCode::Char('q') => return ControlFlow::Break(ExitReason::CharExit),
290
291 KeyCode::Char('k') | KeyCode::Up if control => self.repeat(|this| {
293 if this.active_storage.is_some() {
294 this.draw_memory.current_storage_startline =
295 this.draw_memory.current_storage_startline.saturating_sub(1);
296 } else {
297 this.draw_memory.current_buf_startline =
298 this.draw_memory.current_buf_startline.saturating_sub(1);
299 }
300 }),
301 KeyCode::Char('j') | KeyCode::Down if control => {
303 let max_line = self.active_data_len().saturating_sub(1);
304 self.repeat(|this| {
305 if this.active_storage.is_some() {
306 if this.draw_memory.current_storage_startline < max_line {
307 this.draw_memory.current_storage_startline += 1;
308 }
309 } else if this.draw_memory.current_buf_startline < max_line {
310 this.draw_memory.current_buf_startline += 1;
311 }
312 });
313 }
314
315 KeyCode::Char('k') | KeyCode::Up => self.repeat(Self::step_back),
317 KeyCode::Char('j') | KeyCode::Down => self.repeat(Self::step),
319
320 KeyCode::Char('K') => self.repeat(|this| {
322 this.draw_memory.current_stack_startline =
323 this.draw_memory.current_stack_startline.saturating_sub(1);
324 }),
325 KeyCode::Char('J') => self.repeat(|this| {
327 let max_stack =
328 this.current_step().stack.as_ref().map_or(0, |s| s.len()).saturating_sub(1);
329 if this.draw_memory.current_stack_startline < max_stack {
330 this.draw_memory.current_stack_startline += 1;
331 }
332 }),
333
334 KeyCode::Char('b') => {
336 if self.active_storage.take().is_none() {
337 self.active_buffer = self.active_buffer.next();
338 }
339 self.draw_memory.current_buf_startline = 0;
340 self.set_info(format!("Active buffer: {}", self.active_buffer_name()));
341 }
342
343 KeyCode::Char('l') => self.cycle_layout(),
345
346 KeyCode::Char('g') => {
348 self.draw_memory.inner_call_index = 0;
349 self.current_step = 0;
350 self.update_scroll_positions();
351 }
352
353 KeyCode::Char('G') => {
355 self.draw_memory.inner_call_index = self.debug_arena().len() - 1;
356 self.current_step = self.n_steps() - 1;
357 self.update_scroll_positions();
358 }
359
360 KeyCode::Char('c') if self.draw_memory.inner_call_index > 0 => {
362 self.draw_memory.inner_call_index -= 1;
363 self.current_step = self.n_steps() - 1;
364 self.update_scroll_positions();
365 }
366
367 KeyCode::Char('C')
369 if self.debug_arena().len() > self.draw_memory.inner_call_index + 1 =>
370 {
371 self.draw_memory.inner_call_index += 1;
372 self.current_step = 0;
373 self.update_scroll_positions();
374 }
375
376 KeyCode::Char('s') => self.repeat(|this| {
378 let remaining_steps = &this.debug_steps()[this.current_step..];
379 if let Some((i, _)) =
380 remaining_steps.iter().enumerate().skip(1).find(|(i, step)| {
381 let prev = &remaining_steps[*i - 1];
382 is_jump(step, prev)
383 })
384 {
385 this.current_step += i;
386 this.update_scroll_positions();
387 }
388 }),
389
390 KeyCode::Char('a') => self.repeat(|this| {
392 let ops = &this.debug_steps()[..this.current_step];
393 this.current_step = ops
394 .iter()
395 .enumerate()
396 .skip(1)
397 .rev()
398 .find(|&(i, op)| {
399 let prev = &ops[i - 1];
400 is_jump(op, prev)
401 })
402 .map(|(i, _)| i)
403 .unwrap_or_default();
404 this.update_scroll_positions();
405 }),
406
407 KeyCode::Char('t') => {
409 self.stack_labels = !self.stack_labels;
410 self.set_info(format!("Stack labels: {}", toggle_state(self.stack_labels)));
411 }
412
413 KeyCode::Char('m') => {
415 self.buf_utf = !self.buf_utf;
416 self.set_info(format!("UTF-8 decoding: {}", toggle_state(self.buf_utf)));
417 }
418
419 KeyCode::Char('p') => {
421 self.key_buffer.clear();
422 self.status = None;
423 self.pc_input = Some(String::new());
424 }
425
426 KeyCode::Char('o') => {
428 self.key_buffer.clear();
429 self.status = None;
430 if let Some(space) = self.active_storage {
431 self.command_input = Some(format!("{} ", space.command()));
432 } else {
433 self.buffer_offset_input = Some(String::new());
434 }
435 }
436
437 KeyCode::Char(':') => {
439 self.key_buffer.clear();
440 self.status = None;
441 self.command_input = Some(String::new());
442 }
443
444 KeyCode::Char('/') => {
446 self.key_buffer.clear();
447 self.status = None;
448 self.opcode_search_input = Some(String::new());
449 }
450
451 KeyCode::Char('n') => self.repeat(|this| {
453 this.repeat_opcode_search(SearchDirection::Forward);
454 }),
455
456 KeyCode::Char('N') => self.repeat(|this| {
458 this.repeat_opcode_search(SearchDirection::Backward);
459 }),
460
461 KeyCode::Char('h') => {
463 self.show_shortcuts = !self.show_shortcuts;
464 let state = if self.show_shortcuts { "shown" } else { "hidden" };
465 self.set_info(format!("Shortcut help: {state}"));
466 }
467
468 KeyCode::Char(
470 other @ ('0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '\''),
471 ) => {
472 self.key_buffer.push(other);
474 return ControlFlow::Continue(());
475 }
476
477 _ => {}
479 };
480
481 self.key_buffer.clear();
482 ControlFlow::Continue(())
483 }
484
485 fn handle_pc_input_key_event(&mut self, event: KeyEvent) {
486 if let Some(input) = handle_prompt_input_key_event(&mut self.pc_input, event, |_, c| {
487 c.is_ascii_hexdigit() || matches!(c, 'x' | 'X' | ':')
488 }) {
489 self.goto_pc_from_input(&input);
490 }
491 }
492
493 fn handle_buffer_offset_input_key_event(&mut self, event: KeyEvent) {
494 if let Some(input) = handle_prompt_input_key_event(
495 &mut self.buffer_offset_input,
496 event,
497 is_buffer_offset_input_char,
498 ) {
499 self.goto_buffer_offset_from_input(&input);
500 }
501 }
502
503 fn handle_command_input_key_event(&mut self, event: KeyEvent) {
504 if let Some(input) =
505 handle_prompt_input_key_event(&mut self.command_input, event, |_, c| !c.is_control())
506 {
507 self.run_command_from_input(&input);
508 }
509 }
510
511 fn handle_opcode_search_input_key_event(&mut self, event: KeyEvent) {
512 match event.code {
513 KeyCode::Esc => {
514 self.opcode_search_input = None;
515 }
516 KeyCode::Enter => {
517 let input = self.opcode_search_input.take().unwrap_or_default();
518 self.search_opcode_from_input(&input);
519 }
520 KeyCode::Backspace => {
521 if let Some(input) = &mut self.opcode_search_input {
522 input.pop();
523 }
524 }
525 KeyCode::Char(c) if !event.modifiers.contains(KeyModifiers::CONTROL) => {
526 if let Some(input) = &mut self.opcode_search_input {
527 input.push(c);
528 }
529 }
530 _ => {}
531 }
532 }
533
534 fn search_opcode_from_input(&mut self, input: &str) {
535 let query = input.trim();
536 if query.is_empty() {
537 self.set_error("Enter an opcode search term".to_string());
538 return;
539 }
540
541 self.last_opcode_search = Some(query.to_string());
542 self.search_opcode(query, SearchDirection::Forward);
543 }
544
545 fn repeat_opcode_search(&mut self, direction: SearchDirection) {
546 let Some(query) = self.last_opcode_search.clone() else {
547 self.set_error("No previous opcode search".to_string());
548 return;
549 };
550
551 self.search_opcode(&query, direction);
552 }
553
554 fn search_opcode(&mut self, query: &str, direction: SearchDirection) {
555 let Some((node_index, step_index)) = find_opcode_match(
556 self.debug_arena(),
557 self.draw_memory.inner_call_index,
558 self.current_step,
559 query,
560 direction,
561 ) else {
562 self.set_error(format!("No opcode matching `{query}` in current call"));
563 return;
564 };
565
566 self.draw_memory.inner_call_index = node_index;
567 self.current_step = step_index;
568 self.update_scroll_positions();
569
570 let pc = self.current_step().pc;
571 let opcode = pretty_opcode(self.current_step());
572 self.set_info(format!("Found `{query}` at PC 0x{pc:x} ({pc}): {opcode}"));
573 }
574
575 fn goto_pc_from_input(&mut self, input: &str) {
576 self.move_to_pc_from_input(input, find_pc_target);
577 }
578
579 fn move_to_pc_from_input(
580 &mut self,
581 input: &str,
582 find_target: impl Fn(&[DebugNode], usize, usize, usize) -> Option<StepTarget>,
583 ) {
584 let candidates = match parse_pc_candidates(input) {
585 Ok(candidates) => candidates,
586 Err(err) => {
587 self.set_error(err);
588 return;
589 }
590 };
591
592 let mut found = Vec::new();
593 for &candidate in &candidates {
594 if let Some(target) = find_target(
595 self.debug_arena(),
596 self.draw_memory.inner_call_index,
597 self.current_step,
598 candidate.pc,
599 ) {
600 found.push((candidate, target));
601 }
602 }
603
604 match found.as_slice() {
605 [] => {
606 let current = self.debug_call();
607 let outside = candidates.iter().any(|candidate| {
608 pc_exists_outside_code_context(self.debug_arena(), current, candidate.pc)
609 });
610 let pc = if let [candidate] = candidates.as_slice() {
611 let pc = candidate.pc;
612 format!("PC 0x{pc:x} ({pc})")
613 } else {
614 format!("PC `{}`", input.trim())
615 };
616 let mut msg = format!("{pc} not found in current contract");
617 if outside {
618 msg.push_str("; it exists in another contract, switch calls first");
619 }
620 self.set_error(msg);
621 }
622 [(candidate, target)] => self.apply_pc_target(*candidate, *target),
623 _ => {
624 let input = input.trim();
625 let options = found
626 .iter()
627 .map(|(candidate, _)| candidate.describe())
628 .collect::<Vec<_>>()
629 .join(" and ");
630 self.set_error(format!(
631 "Ambiguous PC `{input}`: {options} both exist; use d:<pc> or 0x<pc>"
632 ));
633 }
634 }
635 }
636
637 fn apply_pc_target(&mut self, candidate: PcCandidate, target: StepTarget) {
638 let already_at_target = self.draw_memory.inner_call_index == target.node_index
639 && self.current_step == target.step_index;
640
641 self.draw_memory.inner_call_index = target.node_index;
642 self.current_step = target.step_index;
643 self.draw_memory.current_buf_startline = 0;
644 self.draw_memory.current_stack_startline = 0;
645 self.update_scroll_positions();
646 self.key_buffer.clear();
647
648 let pc = candidate.pc;
649 let scope = match target.scope {
650 StepTargetScope::CurrentNode => "current trace",
651 StepTargetScope::SameCodeContext => "same contract",
652 };
653 let action = if already_at_target { "Already at" } else { "Jumped to" };
654 self.set_info(format!("{action} PC 0x{pc:x} ({pc}) in {scope}"));
655 }
656
657 fn goto_buffer_offset_from_input(&mut self, input: &str) {
658 self.goto_buffer_offset(self.active_buffer, input);
659 }
660
661 fn goto_buffer_offset(&mut self, buffer: BufferKind, input: &str) {
662 let offset = match parse_buffer_offset(input) {
663 Ok(offset) => offset,
664 Err(err) => {
665 self.set_error(err);
666 return;
667 }
668 };
669
670 let buffer_name = buffer_name(&buffer);
671 let buffer_len = self.buffer(&buffer).len();
672 if buffer_len == 0 {
673 self.set_error(format!("Current {buffer_name} buffer is empty"));
674 return;
675 }
676
677 if offset >= buffer_len {
678 self.set_error(format!(
679 "{buffer_name} offset 0x{offset:x} ({offset}) is outside the {buffer_len}-byte buffer"
680 ));
681 return;
682 }
683
684 self.active_buffer = buffer;
685 self.active_storage = None;
686 self.apply_buffer_offset(offset);
687 }
688
689 fn run_command_from_input(&mut self, input: &str) {
690 let input = input.trim();
691 let input = input.strip_prefix(':').unwrap_or(input).trim_start();
692 if input.is_empty() {
693 self.set_error("Enter a debugger command".to_string());
694 return;
695 }
696
697 let mut parts = input.split_whitespace();
698 let command = parts.next().unwrap();
699 let is_continue_command = CONTINUE_COMMANDS.contains(&command);
700 if is_continue_command || PC_COMMANDS.contains(&command) {
701 let Some(pc) = parts.next() else {
702 return self.set_error(command_usage(command, "<pc>"));
703 };
704 if parts.next().is_some() {
705 return self.set_error(command_usage(command, "<pc>"));
706 }
707 if is_continue_command {
708 self.move_to_pc_from_input(pc, find_next_pc_target);
709 } else {
710 self.goto_pc_from_input(pc);
711 }
712 } else if MEMORY_COMMANDS.contains(&command) {
713 self.run_buffer_command(command, BufferKind::Memory, parts);
714 } else if CALLDATA_COMMANDS.contains(&command) {
715 self.run_buffer_command(command, BufferKind::Calldata, parts);
716 } else if RETURNDATA_COMMANDS.contains(&command) {
717 self.run_buffer_command(command, BufferKind::Returndata, parts);
718 } else if STORAGE_COMMANDS.contains(&command) {
719 self.run_storage_command(command, StorageSpace::Persistent, parts);
720 } else if TRANSIENT_STORAGE_COMMANDS.contains(&command) {
721 self.run_storage_command(command, StorageSpace::Transient, parts);
722 } else if LINE_COMMANDS.contains(&command) {
723 let Some(line) = parts.next() else {
724 return self.set_error(command_usage(command, "<line>"));
725 };
726 if parts.next().is_some() {
727 return self.set_error(command_usage(command, "<line>"));
728 }
729 self.goto_source_line_from_input(line);
730 } else if OPCODE_COMMANDS.contains(&command) {
731 self.run_pane_command(command, PaneCommand::Opcodes, parts);
732 } else if SOURCE_COMMANDS.contains(&command) {
733 self.run_pane_command(command, PaneCommand::Source, parts);
734 } else if VARIABLES_COMMANDS.contains(&command) {
735 self.run_pane_command(command, PaneCommand::Variables, parts);
736 } else if STACK_COMMANDS.contains(&command) {
737 self.run_pane_command(command, PaneCommand::Stack, parts);
738 } else if DATA_COMMANDS.contains(&command) {
739 self.run_pane_command(command, PaneCommand::Data, parts);
740 } else if HELP_COMMANDS.contains(&command) {
741 self.set_info(command_help());
742 } else {
743 self.set_error(format!("Unknown command `{command}`; try `help`"));
744 }
745 }
746
747 fn run_buffer_command<'a>(
748 &mut self,
749 command: &str,
750 buffer: BufferKind,
751 mut args: impl Iterator<Item = &'a str>,
752 ) {
753 let Some(offset) = args.next() else {
754 self.select_buffer(buffer);
755 return;
756 };
757 if args.next().is_some() {
758 return self.set_error(command_usage(command, "<offset>"));
759 }
760 self.goto_buffer_offset(buffer, offset);
761 }
762
763 fn run_storage_command<'a>(
764 &mut self,
765 command: &str,
766 space: StorageSpace,
767 mut args: impl Iterator<Item = &'a str>,
768 ) {
769 let Some(slot) = args.next() else {
770 self.select_storage(space);
771 return;
772 };
773 if args.next().is_some() {
774 return self.set_error(command_usage(command, "<slot>"));
775 }
776 self.goto_storage_slot_from_input(slot, space);
777 }
778
779 fn select_buffer(&mut self, buffer: BufferKind) {
780 self.active_buffer = buffer;
781 self.active_storage = None;
782 self.draw_memory.current_buf_startline = 0;
783 self.set_info(format!("Active buffer: {}", self.active_buffer_name()));
784 }
785
786 fn select_storage(&mut self, space: StorageSpace) {
787 self.active_storage = Some(space);
788 self.draw_memory.current_storage_startline = 0;
789 self.set_info(format!("Active data: {}", space.noun()));
790 }
791
792 fn goto_source_line_from_input(&mut self, input: &str) {
793 let line = match input.parse::<usize>() {
794 Ok(line) if line > 0 => line,
795 _ => {
796 self.set_error(format!(
797 "Invalid source line `{input}`; use a positive decimal line number"
798 ));
799 return;
800 }
801 };
802
803 let (source_path, source_line, contract_name) = {
804 let (_, source) = match self.src_map() {
805 Ok(source) => source,
806 Err(err) => {
807 self.set_error(err);
808 return;
809 }
810 };
811 let Some(source_line) = source_line_range(&source.source, line) else {
812 let line_count = source.source.lines().count().max(1);
813 self.set_error(format!(
814 "Source line {line} is outside {} ({line_count} lines)",
815 source.path.display()
816 ));
817 return;
818 };
819 let contract_name = self
820 .debugger_context
821 .identified_contracts
822 .get(self.address())
823 .expect("source mapping requires an identified contract")
824 .clone();
825 (source.path.clone(), source_line, contract_name)
826 };
827
828 let sources = &self.debugger_context.contracts_sources;
829 let Some(target) = find_step_target(
830 self.debug_arena(),
831 self.draw_memory.inner_call_index,
832 self.current_step,
833 |node, step| {
834 let Some((source_element, source)) = sources.find_source_mapping(
835 &contract_name,
836 step.pc as u32,
837 node.kind.is_any_create(),
838 ) else {
839 return false;
840 };
841 source.path == source_path
842 && source_line.contains(&(source_element.offset() as usize))
843 },
844 ) else {
845 self.set_error(format!(
846 "No opcode mapped to {}:{line} in current contract",
847 source_path.display()
848 ));
849 return;
850 };
851
852 let already_at_target = self.draw_memory.inner_call_index == target.node_index
853 && self.current_step == target.step_index;
854 self.draw_memory.inner_call_index = target.node_index;
855 self.current_step = target.step_index;
856 self.draw_memory.current_buf_startline = 0;
857 self.draw_memory.current_stack_startline = 0;
858 self.update_scroll_positions();
859 self.key_buffer.clear();
860
861 let pc = self.current_step().pc;
862 let action = if already_at_target { "Already at" } else { "Jumped to" };
863 self.set_info(format!("{action} {}:{line} at PC 0x{pc:x} ({pc})", source_path.display()));
864 }
865
866 fn run_pane_command<'a>(
867 &mut self,
868 command: &str,
869 pane: PaneCommand,
870 mut args: impl Iterator<Item = &'a str>,
871 ) {
872 if args.next().is_some() {
873 return self.set_error(command_usage(command, ""));
874 }
875 let shown = match pane {
876 PaneCommand::Opcodes => {
877 self.show_opcodes = !self.show_opcodes;
878 self.show_opcodes
879 }
880 PaneCommand::Source => {
881 self.show_source = !self.show_source;
882 self.show_source
883 }
884 PaneCommand::Variables => {
885 self.show_variables = !self.show_variables;
886 self.show_variables
887 }
888 PaneCommand::Stack => {
889 self.show_stack = !self.show_stack;
890 self.show_stack
891 }
892 PaneCommand::Data => {
893 self.show_data = !self.show_data;
894 self.show_data
895 }
896 };
897 let state = if shown { "shown" } else { "hidden" };
898 self.set_info(format!("{} pane: {state}", pane.label()));
899 }
900
901 fn goto_storage_slot_from_input(&mut self, input: &str, space: StorageSpace) {
902 let slot = match parse_storage_slot(input) {
903 Ok(slot) => slot,
904 Err(err) => {
905 self.set_error(err);
906 return;
907 }
908 };
909
910 let Some(target) = find_storage_target(
911 self.debug_arena(),
912 self.draw_memory.inner_call_index,
913 self.current_step,
914 slot,
915 space,
916 ) else {
917 self.set_error(format!(
918 "{} slot {} not accessed in current call",
919 space.label(),
920 hex_u256(slot)
921 ));
922 return;
923 };
924
925 let access = target.access;
926 self.draw_memory.inner_call_index = target.node_index;
927 self.current_step = access.step_index();
928 self.draw_memory.current_buf_startline = 0;
929 self.draw_memory.current_stack_startline = 0;
930 self.active_storage = Some(space);
931 self.draw_memory.current_storage_startline =
932 self.storage_accesses(space).get_index_of(&access.slot()).unwrap_or_default();
933 self.update_scroll_positions();
934 self.key_buffer.clear();
935 self.set_info(format!(
936 "Jumped to {} at PC 0x{:x} ({})",
937 access.describe(),
938 access.pc(),
939 access.pc()
940 ));
941 }
942
943 fn apply_buffer_offset(&mut self, offset: usize) {
944 self.draw_memory.current_buf_startline = offset / 32;
945 self.key_buffer.clear();
946 let buffer_name = self.active_buffer_name();
947 self.set_info(format!("Jumped to {buffer_name} offset 0x{offset:x} ({offset})"));
948 }
949
950 fn set_info(&mut self, text: String) {
951 self.status = Some(StatusMessage { kind: StatusKind::Info, text });
952 }
953
954 fn set_error(&mut self, text: String) {
955 self.status = Some(StatusMessage { kind: StatusKind::Error, text });
956 }
957
958 fn handle_breakpoint(&mut self, c: char) {
959 self.key_buffer.clear();
960
961 let Some((caller, pc)) = self.debugger_context.breakpoints.get(&c).copied() else {
962 self.set_error(format!("Breakpoint '{c}' not found"));
963 return;
964 };
965
966 let Some((inner_call_index, step_index)) = find_next_step_target(
967 self.debug_arena(),
968 self.draw_memory.inner_call_index,
969 self.current_step,
970 |node, step| node.address == caller && step.pc == pc,
971 ) else {
972 self.set_error(format!("Breakpoint '{c}' target not found in trace"));
973 return;
974 };
975
976 let already_at_target = self.draw_memory.inner_call_index == inner_call_index
977 && self.current_step == step_index;
978
979 self.draw_memory.inner_call_index = inner_call_index;
980 self.current_step = step_index;
981 self.update_scroll_positions();
982
983 let action = if already_at_target { "Already at" } else { "Jumped to" };
984 self.set_info(format!("{action} breakpoint '{c}' at PC 0x{pc:x} ({pc})"));
985 }
986
987 fn handle_mouse_event(&mut self, event: MouseEvent) -> ControlFlow<ExitReason> {
988 if self.pc_input.is_some()
989 || self.buffer_offset_input.is_some()
990 || self.command_input.is_some()
991 || self.opcode_search_input.is_some()
992 {
993 return ControlFlow::Continue(());
994 }
995
996 match event.kind {
997 MouseEventKind::ScrollUp => self.step_back(),
998 MouseEventKind::ScrollDown => self.step(),
999 _ => {}
1000 }
1001
1002 ControlFlow::Continue(())
1003 }
1004
1005 fn step_back(&mut self) {
1006 if self.current_step > 0 {
1007 self.current_step -= 1;
1008 } else if self.draw_memory.inner_call_index > 0 {
1009 self.draw_memory.inner_call_index -= 1;
1010 self.current_step = self.n_steps() - 1;
1011 }
1012 self.update_scroll_positions();
1013 }
1014
1015 fn step(&mut self) {
1016 if self.current_step < self.n_steps() - 1 {
1017 self.current_step += 1;
1018 } else if self.draw_memory.inner_call_index < self.debug_arena().len() - 1 {
1019 self.draw_memory.inner_call_index += 1;
1020 self.current_step = 0;
1021 }
1022 self.update_scroll_positions();
1023 }
1024
1025 fn update_scroll_positions(&mut self) {
1026 if let Some(stack) = &self.current_step().stack
1027 && !stack.is_empty()
1028 {
1029 self.draw_memory.current_stack_startline =
1030 self.draw_memory.current_stack_startline.min(stack.len().saturating_sub(1));
1031 }
1032
1033 if self.active_buffer == BufferKind::Memory
1034 && let Some(line) = self.current_memory_write_line()
1035 {
1036 self.draw_memory.current_buf_startline = line;
1037 }
1038
1039 let buffer_len = self.active_buffer().len();
1040 if buffer_len > 0 {
1041 let max_line = buffer_len.div_ceil(32) - 1;
1042 self.draw_memory.current_buf_startline =
1043 self.draw_memory.current_buf_startline.min(max_line);
1044 }
1045 }
1046
1047 fn current_memory_write_line(&self) -> Option<usize> {
1048 let memory_len = self.current_step().memory.as_ref()?.len();
1049
1050 if self.current_step > 0 {
1051 let prev_step = &self.debug_steps()[self.current_step - 1];
1052 if let Some(line) = bounded_memory_write_start_line(prev_step, memory_len) {
1053 return Some(line);
1054 }
1055 }
1056
1057 bounded_memory_write_start_line(self.current_step(), memory_len)
1058 }
1059
1060 fn repeat(&mut self, mut f: impl FnMut(&mut Self)) {
1062 for _ in 0..buffer_as_number(&self.key_buffer) {
1063 f(self);
1064 }
1065 }
1066
1067 fn n_steps(&self) -> usize {
1068 self.debug_steps().len()
1069 }
1070
1071 fn cycle_layout(&mut self) {
1072 let layout = self.debugger_context.layout.next();
1073 self.debugger_context.layout = layout;
1074 self.status = Some(StatusMessage {
1075 kind: StatusKind::Info,
1076 text: format!("Debugger layout: {}", layout.as_str()),
1077 });
1078 }
1079}
1080
1081impl TuiApp for TUIContext<'_> {
1082 type Exit = ExitReason;
1083
1084 fn draw(&mut self, frame: &mut Frame<'_>) {
1085 self.draw_layout(frame);
1086 }
1087
1088 fn handle_event(&mut self, event: Event) -> ControlFlow<Self::Exit> {
1089 TUIContext::handle_event(self, event)
1090 }
1091}
1092
1093fn buffer_as_number(s: &str) -> usize {
1095 const MIN: usize = 1;
1096 const MAX: usize = 100_000;
1097 s.parse().unwrap_or(MIN).clamp(MIN, MAX)
1098}
1099
1100const fn toggle_state(enabled: bool) -> &'static str {
1101 if enabled { "on" } else { "off" }
1102}
1103
1104const fn buffer_name(buffer: &BufferKind) -> &'static str {
1105 match buffer {
1106 BufferKind::Memory => "memory",
1107 BufferKind::Calldata => "calldata",
1108 BufferKind::Returndata => "returndata",
1109 }
1110}
1111
1112const CONTINUE_COMMANDS: &[&str] = &["continue", "cont", "c"];
1113const PC_COMMANDS: &[&str] = &["pc", "p"];
1114const MEMORY_COMMANDS: &[&str] = &["mem", "memory"];
1115const CALLDATA_COMMANDS: &[&str] = &["calldata", "cd"];
1116const RETURNDATA_COMMANDS: &[&str] = &["returndata", "ret", "rd"];
1117const STORAGE_COMMANDS: &[&str] = &["storage", "store", "slot"];
1118const TRANSIENT_STORAGE_COMMANDS: &[&str] = &["transient", "tslot"];
1119const LINE_COMMANDS: &[&str] = &["line", "ln"];
1120const OPCODE_COMMANDS: &[&str] = &["opcodes", "opcode", "ops"];
1121const SOURCE_COMMANDS: &[&str] = &["source", "src"];
1122const VARIABLES_COMMANDS: &[&str] = &["variables", "vars"];
1123const STACK_COMMANDS: &[&str] = &["stack"];
1124const DATA_COMMANDS: &[&str] = &["data"];
1125const HELP_COMMANDS: &[&str] = &["help", "h"];
1126
1127#[derive(Clone, Copy)]
1128enum PaneCommand {
1129 Opcodes,
1130 Source,
1131 Variables,
1132 Stack,
1133 Data,
1134}
1135
1136impl PaneCommand {
1137 const fn label(self) -> &'static str {
1138 match self {
1139 Self::Opcodes => "Opcodes",
1140 Self::Source => "Source",
1141 Self::Variables => "Variables",
1142 Self::Stack => "Stack",
1143 Self::Data => "Data",
1144 }
1145 }
1146}
1147
1148fn command_usage(command: &str, arg: &str) -> String {
1149 if arg.is_empty() { format!("Usage: :{command}") } else { format!("Usage: :{command} {arg}") }
1150}
1151
1152fn command_help() -> String {
1153 format!(
1154 "Commands: {} <pc>, {} <pc>, {} [<offset>], {} [<offset>], {} [<offset>], {} [<slot>], {} [<slot>], {} <line>, {}, {}, {}, {}, {}",
1155 command_aliases(CONTINUE_COMMANDS),
1156 command_aliases(PC_COMMANDS),
1157 command_aliases(MEMORY_COMMANDS),
1158 command_aliases(CALLDATA_COMMANDS),
1159 command_aliases(RETURNDATA_COMMANDS),
1160 command_aliases(STORAGE_COMMANDS),
1161 command_aliases(TRANSIENT_STORAGE_COMMANDS),
1162 command_aliases(LINE_COMMANDS),
1163 command_aliases(OPCODE_COMMANDS),
1164 command_aliases(SOURCE_COMMANDS),
1165 command_aliases(VARIABLES_COMMANDS),
1166 command_aliases(STACK_COMMANDS),
1167 command_aliases(DATA_COMMANDS)
1168 )
1169}
1170
1171fn command_aliases(commands: &[&str]) -> String {
1172 commands.iter().map(|command| format!(":{command}")).collect::<Vec<_>>().join("/")
1173}
1174
1175fn handle_prompt_input_key_event(
1176 input: &mut Option<String>,
1177 event: KeyEvent,
1178 is_input_char: impl Fn(&str, char) -> bool,
1179) -> Option<String> {
1180 match event.code {
1181 KeyCode::Esc => {
1182 *input = None;
1183 }
1184 KeyCode::Enter => {
1185 return Some(input.take().unwrap_or_default());
1186 }
1187 KeyCode::Backspace => {
1188 if let Some(input) = input {
1189 input.pop();
1190 }
1191 }
1192 KeyCode::Char(c) if !event.modifiers.contains(KeyModifiers::CONTROL) => {
1193 if let Some(input) = input
1194 && is_input_char(input, c)
1195 {
1196 input.push(c);
1197 }
1198 }
1199 _ => {}
1200 }
1201
1202 None
1203}
1204
1205fn is_buffer_offset_input_char(input: &str, c: char) -> bool {
1206 if !(c.is_ascii_hexdigit() || matches!(c, 'x' | 'X' | ':')) {
1207 return false;
1208 }
1209
1210 let mut next = String::with_capacity(input.len() + c.len_utf8());
1211 next.push_str(input);
1212 next.push(c);
1213 is_buffer_offset_input_prefix(&next)
1214}
1215
1216fn is_buffer_offset_input_prefix(input: &str) -> bool {
1217 if let Some(rest) = input.strip_prefix("0x").or_else(|| input.strip_prefix("0X")) {
1218 return rest.chars().all(|c| c.is_ascii_hexdigit());
1219 }
1220
1221 if let Some(rest) = input.strip_prefix("d:").or_else(|| input.strip_prefix("dec:")) {
1222 return rest.chars().all(|c| c.is_ascii_digit());
1223 }
1224
1225 input.chars().all(|c| c.is_ascii_hexdigit())
1226 || "d:".starts_with(input)
1227 || "dec:".starts_with(input)
1228}
1229
1230#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1231enum SearchDirection {
1232 Forward,
1233 Backward,
1234}
1235
1236#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1237enum PcBase {
1238 Hex,
1239 Decimal,
1240}
1241
1242#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1243struct PcCandidate {
1244 pc: usize,
1245 base: PcBase,
1246}
1247
1248impl PcCandidate {
1249 fn describe(self) -> String {
1250 match self.base {
1251 PcBase::Hex => format!("hex 0x{:x}", self.pc),
1252 PcBase::Decimal => format!("decimal {}", self.pc),
1253 }
1254 }
1255}
1256
1257#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1258enum StepTargetScope {
1259 CurrentNode,
1260 SameCodeContext,
1261}
1262
1263#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1264struct StepTarget {
1265 node_index: usize,
1266 step_index: usize,
1267 scope: StepTargetScope,
1268}
1269
1270#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1271struct StorageTarget {
1272 node_index: usize,
1273 access: StorageAccess,
1274}
1275
1276fn parse_pc_candidates(input: &str) -> Result<Vec<PcCandidate>, String> {
1277 let input = input.trim();
1278 if input.is_empty() {
1279 return Err("Enter a program counter".to_string());
1280 }
1281
1282 if let Some(rest) = input.strip_prefix("0x").or_else(|| input.strip_prefix("0X")) {
1283 return parse_pc_candidate(rest, 16, PcBase::Hex, input);
1284 }
1285
1286 if let Some(rest) = input.strip_prefix("d:").or_else(|| input.strip_prefix("dec:")) {
1287 return parse_pc_candidate(rest, 10, PcBase::Decimal, input);
1288 }
1289
1290 if input.chars().any(|c| c.is_ascii_hexdigit() && c.is_ascii_alphabetic()) {
1291 return parse_pc_candidate(input, 16, PcBase::Hex, input);
1292 }
1293
1294 if input.chars().all(|c| c.is_ascii_digit()) {
1295 let decimal = parse_pc(input, 10, input)?;
1296 let hex = parse_pc(input, 16, input)?;
1297 if decimal == hex {
1298 return Ok(vec![PcCandidate { pc: decimal, base: PcBase::Decimal }]);
1299 }
1300 return Ok(vec![
1301 PcCandidate { pc: decimal, base: PcBase::Decimal },
1302 PcCandidate { pc: hex, base: PcBase::Hex },
1303 ]);
1304 }
1305
1306 Err(format!("Invalid PC `{input}`; use 0x2a, 2a, or d:42"))
1307}
1308
1309fn parse_pc_candidate(
1310 input: &str,
1311 radix: u32,
1312 base: PcBase,
1313 original: &str,
1314) -> Result<Vec<PcCandidate>, String> {
1315 Ok(vec![PcCandidate { pc: parse_pc(input, radix, original)?, base }])
1316}
1317
1318fn parse_pc(input: &str, radix: u32, original: &str) -> Result<usize, String> {
1319 if input.is_empty() {
1320 return Err(format!("Invalid PC `{original}`; use 0x2a, 2a, or d:42"));
1321 }
1322 usize::from_str_radix(input, radix)
1323 .map_err(|_| format!("Invalid PC `{original}`; use 0x2a, 2a, or d:42"))
1324}
1325
1326fn parse_buffer_offset(input: &str) -> Result<usize, String> {
1327 let input = input.trim();
1328 if input.is_empty() {
1329 return Err("Enter a buffer offset".to_string());
1330 }
1331
1332 let (digits, radix) =
1333 if let Some(rest) = input.strip_prefix("0x").or_else(|| input.strip_prefix("0X")) {
1334 (rest, 16)
1335 } else if let Some(rest) = input.strip_prefix("d:").or_else(|| input.strip_prefix("dec:")) {
1336 (rest, 10)
1337 } else {
1338 (input, 16)
1339 };
1340
1341 if digits.is_empty() {
1342 return Err(invalid_buffer_offset(input));
1343 }
1344
1345 usize::from_str_radix(digits, radix).map_err(|_| invalid_buffer_offset(input))
1346}
1347
1348fn invalid_buffer_offset(input: &str) -> String {
1349 format!("Invalid buffer offset `{input}`; use hex 0x20/20 or decimal d:32")
1350}
1351
1352fn parse_storage_slot(input: &str) -> Result<U256, String> {
1353 let input = input.trim();
1354 if input.is_empty() {
1355 return Err("Enter a storage slot".to_string());
1356 }
1357
1358 let (digits, radix) =
1359 if let Some(rest) = input.strip_prefix("0x").or_else(|| input.strip_prefix("0X")) {
1360 (rest, 16)
1361 } else if let Some(rest) = input.strip_prefix("d:").or_else(|| input.strip_prefix("dec:")) {
1362 (rest, 10)
1363 } else {
1364 (input, 16)
1365 };
1366
1367 let valid_digits = match radix {
1368 10 => digits.bytes().all(|b| b.is_ascii_digit()),
1369 16 => digits.bytes().all(|b| b.is_ascii_hexdigit()),
1370 _ => unreachable!(),
1371 };
1372 if digits.is_empty() || !valid_digits {
1373 return Err(invalid_storage_slot(input));
1374 }
1375
1376 U256::from_str_radix(digits, radix).map_err(|_| invalid_storage_slot(input))
1377}
1378
1379fn invalid_storage_slot(input: &str) -> String {
1380 format!("Invalid storage slot `{input}`; use hex 0x20/20 or decimal d:32")
1381}
1382
1383fn find_storage_target(
1384 arena: &[DebugNode],
1385 current_node_index: usize,
1386 current_step: usize,
1387 slot: U256,
1388 space: StorageSpace,
1389) -> Option<StorageTarget> {
1390 let current_node = arena.get(current_node_index)?;
1391 let trace_node_idx = current_node.trace_node_idx;
1392 let current_absolute_step = current_node.step_offset.saturating_add(current_step);
1393
1394 storage_target_at(arena, current_node_index, current_step, slot, space)
1395 .or_else(|| {
1396 find_storage_target_after(arena, trace_node_idx, current_absolute_step, slot, space)
1397 })
1398 .or_else(|| {
1399 find_storage_target_before(arena, trace_node_idx, current_absolute_step, slot, space)
1400 })
1401}
1402
1403fn storage_target_at(
1404 arena: &[DebugNode],
1405 node_index: usize,
1406 step_index: usize,
1407 slot: U256,
1408 space: StorageSpace,
1409) -> Option<StorageTarget> {
1410 let node = arena.get(node_index)?;
1411 storage_access_at(&node.steps, step_index)
1412 .filter(|access| access.slot() == slot && access.space() == space)
1413 .map(|access| StorageTarget { node_index, access })
1414}
1415
1416fn find_storage_target_after(
1417 arena: &[DebugNode],
1418 trace_node_idx: usize,
1419 current_absolute_step: usize,
1420 slot: U256,
1421 space: StorageSpace,
1422) -> Option<StorageTarget> {
1423 let mut best = None;
1424
1425 for (node_index, node) in arena.iter().enumerate() {
1426 if node.trace_node_idx != trace_node_idx {
1427 continue;
1428 }
1429
1430 for step_index in 0..node.steps.len() {
1431 let absolute_step = node.step_offset.saturating_add(step_index);
1432 if absolute_step <= current_absolute_step {
1433 continue;
1434 }
1435
1436 let Some(access) = storage_access_at(&node.steps, step_index)
1437 .filter(|access| access.slot() == slot && access.space() == space)
1438 else {
1439 continue;
1440 };
1441
1442 match best {
1443 Some((best_absolute_step, _, _)) if absolute_step >= best_absolute_step => {}
1444 _ => best = Some((absolute_step, node_index, access)),
1445 }
1446 break;
1447 }
1448 }
1449
1450 best.map(|(_, node_index, access)| StorageTarget { node_index, access })
1451}
1452
1453fn find_storage_target_before(
1454 arena: &[DebugNode],
1455 trace_node_idx: usize,
1456 current_absolute_step: usize,
1457 slot: U256,
1458 space: StorageSpace,
1459) -> Option<StorageTarget> {
1460 let mut best = None;
1461
1462 for (node_index, node) in arena.iter().enumerate() {
1463 if node.trace_node_idx != trace_node_idx {
1464 continue;
1465 }
1466
1467 for step_index in (0..node.steps.len()).rev() {
1468 let absolute_step = node.step_offset.saturating_add(step_index);
1469 if absolute_step >= current_absolute_step {
1470 continue;
1471 }
1472
1473 let Some(access) = storage_access_at(&node.steps, step_index)
1474 .filter(|access| access.slot() == slot && access.space() == space)
1475 else {
1476 continue;
1477 };
1478
1479 match best {
1480 Some((best_absolute_step, _, _)) if absolute_step <= best_absolute_step => {}
1481 _ => best = Some((absolute_step, node_index, access)),
1482 }
1483 break;
1484 }
1485 }
1486
1487 best.map(|(_, node_index, access)| StorageTarget { node_index, access })
1488}
1489
1490fn find_pc_target(
1491 arena: &[DebugNode],
1492 current_node_index: usize,
1493 current_step: usize,
1494 pc: usize,
1495) -> Option<StepTarget> {
1496 find_step_target(arena, current_node_index, current_step, |_, step| step.pc == pc)
1497}
1498
1499fn find_next_pc_target(
1500 arena: &[DebugNode],
1501 current_node_index: usize,
1502 current_step: usize,
1503 pc: usize,
1504) -> Option<StepTarget> {
1505 let current_node = arena.get(current_node_index)?;
1506 let (node_index, step_index) =
1507 find_next_step_target(arena, current_node_index, current_step, |node, step| {
1508 same_code_context(current_node, node) && step.pc == pc
1509 })?;
1510 let scope = if node_index == current_node_index {
1511 StepTargetScope::CurrentNode
1512 } else {
1513 StepTargetScope::SameCodeContext
1514 };
1515
1516 Some(StepTarget { node_index, step_index, scope })
1517}
1518
1519fn find_next_step_target(
1520 arena: &[DebugNode],
1521 current_node_index: usize,
1522 current_step: usize,
1523 mut matches: impl FnMut(&DebugNode, &CallTraceStep) -> bool,
1524) -> Option<(usize, usize)> {
1525 let current_node = arena.get(current_node_index)?;
1526
1527 if let Some(step_index) = current_node
1528 .steps
1529 .iter()
1530 .enumerate()
1531 .skip(current_step.saturating_add(1))
1532 .find_map(|(i, step)| matches(current_node, step).then_some(i))
1533 {
1534 return Some((current_node_index, step_index));
1535 }
1536
1537 for (node_index, node) in arena.iter().enumerate().skip(current_node_index + 1) {
1538 if let Some(step_index) = node.steps.iter().position(|step| matches(node, step)) {
1539 return Some((node_index, step_index));
1540 }
1541 }
1542
1543 for (node_index, node) in arena.iter().enumerate().take(current_node_index) {
1544 if let Some(step_index) = node.steps.iter().position(|step| matches(node, step)) {
1545 return Some((node_index, step_index));
1546 }
1547 }
1548
1549 current_node
1550 .steps
1551 .iter()
1552 .enumerate()
1553 .take(current_step.saturating_add(1))
1554 .find_map(|(i, step)| matches(current_node, step).then_some((current_node_index, i)))
1555}
1556
1557fn find_step_target(
1558 arena: &[DebugNode],
1559 current_node_index: usize,
1560 current_step: usize,
1561 mut matches: impl FnMut(&DebugNode, &CallTraceStep) -> bool,
1562) -> Option<StepTarget> {
1563 let current_node = arena.get(current_node_index)?;
1564
1565 if let Some(step_index) = find_step_in_current_node(current_node, current_step, &mut matches) {
1566 return Some(StepTarget {
1567 node_index: current_node_index,
1568 step_index,
1569 scope: StepTargetScope::CurrentNode,
1570 });
1571 }
1572
1573 for (node_index, node) in arena.iter().enumerate().skip(current_node_index + 1) {
1574 if same_code_context(current_node, node)
1575 && let Some(step_index) = node.steps.iter().position(|step| matches(node, step))
1576 {
1577 return Some(StepTarget {
1578 node_index,
1579 step_index,
1580 scope: StepTargetScope::SameCodeContext,
1581 });
1582 }
1583 }
1584
1585 for (node_index, node) in arena.iter().enumerate().take(current_node_index).rev() {
1586 if same_code_context(current_node, node)
1587 && let Some(step_index) = node.steps.iter().rposition(|step| matches(node, step))
1588 {
1589 return Some(StepTarget {
1590 node_index,
1591 step_index,
1592 scope: StepTargetScope::SameCodeContext,
1593 });
1594 }
1595 }
1596
1597 None
1598}
1599
1600fn find_step_in_current_node(
1601 node: &DebugNode,
1602 current_step: usize,
1603 matches: &mut impl FnMut(&DebugNode, &CallTraceStep) -> bool,
1604) -> Option<usize> {
1605 if node.steps.get(current_step).is_some_and(|step| matches(node, step)) {
1606 return Some(current_step);
1607 }
1608
1609 node.steps
1610 .iter()
1611 .enumerate()
1612 .skip(current_step.saturating_add(1))
1613 .find_map(|(i, step)| matches(node, step).then_some(i))
1614 .or_else(|| {
1615 node.steps[..current_step.min(node.steps.len())]
1616 .iter()
1617 .enumerate()
1618 .rev()
1619 .find_map(|(i, step)| matches(node, step).then_some(i))
1620 })
1621}
1622
1623fn source_line_range(source: &str, line: usize) -> Option<std::ops::Range<usize>> {
1624 if line == 0 {
1625 return None;
1626 }
1627
1628 let mut start = 0;
1629 for _ in 1..line {
1630 start += source.get(start..)?.find('\n')? + 1;
1631 }
1632 if start >= source.len() {
1633 return None;
1634 }
1635 let end = source[start..].find('\n').map_or(source.len(), |offset| start + offset + 1);
1636 Some(start..end)
1637}
1638
1639fn same_code_context(a: &DebugNode, b: &DebugNode) -> bool {
1640 a.address == b.address && a.kind.is_any_create() == b.kind.is_any_create()
1641}
1642
1643fn pc_exists_outside_code_context(arena: &[DebugNode], current: &DebugNode, pc: usize) -> bool {
1644 arena.iter().any(|node| {
1645 !same_code_context(current, node) && node.steps.iter().any(|step| step.pc == pc)
1646 })
1647}
1648
1649fn find_opcode_match(
1650 arena: &[DebugNode],
1651 current_node_index: usize,
1652 current_step: usize,
1653 query: &str,
1654 direction: SearchDirection,
1655) -> Option<(usize, usize)> {
1656 let needle = query.trim().to_ascii_lowercase();
1657 if needle.is_empty() {
1658 return None;
1659 }
1660
1661 let current_node = arena.get(current_node_index)?;
1662 let trace_node_idx = current_node.trace_node_idx;
1663 let current_absolute_step = current_node.step_offset.saturating_add(current_step);
1664 let steps = || {
1665 arena.iter().enumerate().filter(|(_, node)| node.trace_node_idx == trace_node_idx).flat_map(
1666 |(node_index, node)| {
1667 node.steps.iter().enumerate().map(move |(step_index, step)| {
1668 (node_index, step_index, node.step_offset.saturating_add(step_index), step)
1669 })
1670 },
1671 )
1672 };
1673 let target = match direction {
1674 SearchDirection::Forward => steps()
1675 .filter(|(_, _, absolute_step, _)| *absolute_step > current_absolute_step)
1676 .chain(
1677 steps().filter(|(_, _, absolute_step, _)| *absolute_step <= current_absolute_step),
1678 )
1679 .find(|(_, _, _, step)| pretty_opcode(step).to_ascii_lowercase().contains(&needle)),
1680 SearchDirection::Backward => steps()
1681 .rev()
1682 .filter(|(_, _, absolute_step, _)| *absolute_step < current_absolute_step)
1683 .chain(
1684 steps()
1685 .rev()
1686 .filter(|(_, _, absolute_step, _)| *absolute_step >= current_absolute_step),
1687 )
1688 .find(|(_, _, _, step)| pretty_opcode(step).to_ascii_lowercase().contains(&needle)),
1689 };
1690 target.map(|(node_index, step_index, _, _)| (node_index, step_index))
1691}
1692
1693fn pretty_opcode(step: &CallTraceStep) -> String {
1694 if let Some(immediate) = step.immediate_bytes.as_ref().filter(|b| !b.is_empty()) {
1695 format!("{}(0x{})", step.op, hex::encode(immediate))
1696 } else {
1697 step.op.to_string()
1698 }
1699}
1700
1701fn memory_write_start_line(step: &CallTraceStep) -> Option<usize> {
1702 let stack = step.stack.as_ref()?;
1703 let access = get_buffer_accesses(step.op.get(), stack)?.write?;
1704 if access.len == 0 {
1705 return None;
1706 }
1707 Some(access.offset / 32)
1708}
1709
1710fn bounded_memory_write_start_line(step: &CallTraceStep, memory_len: usize) -> Option<usize> {
1711 let line = memory_write_start_line(step)?;
1712 (line < memory_len.div_ceil(32)).then_some(line)
1713}
1714
1715fn is_jump(step: &CallTraceStep, prev: &CallTraceStep) -> bool {
1716 if !matches!(prev.op, OpCode::JUMP | OpCode::JUMPI) {
1717 return false;
1718 }
1719
1720 let immediate_len = prev.immediate_bytes.as_ref().map_or(0, |b| b.len());
1721
1722 step.pc != prev.pc + 1 + immediate_len
1723}
1724
1725#[cfg(test)]
1726mod tests {
1727 use super::*;
1728 use alloy_primitives::Bytes;
1729 use foundry_compilers::artifacts::sourcemap::Parser;
1730 use foundry_evm_core::{Breakpoints, ic::PcIcMap};
1731 use foundry_evm_traces::debug::{ArtifactData, ContractSources};
1732 use revm::interpreter::InstructionResult;
1733 use revm_inspectors::tracing::types::{StorageChange, StorageChangeReason};
1734 use std::{path::PathBuf, sync::Arc};
1735
1736 fn step(pc: usize) -> CallTraceStep {
1737 step_with_stack(pc, OpCode::STOP, &[])
1738 }
1739
1740 fn step_with_immediate(pc: usize, op: OpCode, immediate: &'static [u8]) -> CallTraceStep {
1741 CallTraceStep {
1742 immediate_bytes: Some(Bytes::from_static(immediate)),
1743 ..step_with_stack(pc, op, &[])
1744 }
1745 }
1746
1747 fn step_with_stack(pc: usize, op: OpCode, stack: &[usize]) -> CallTraceStep {
1748 CallTraceStep {
1749 pc,
1750 op,
1751 stack: (!stack.is_empty()).then(|| {
1752 stack.iter().copied().map(U256::from).collect::<Vec<_>>().into_boxed_slice()
1753 }),
1754 push_stack: None,
1755 memory: None,
1756 returndata: Bytes::new(),
1757 gas_remaining: 0,
1758 gas_refund_counter: 0,
1759 gas_used: 0,
1760 gas_cost: 0,
1761 storage_change: None,
1762 status: Some(InstructionResult::Stop),
1763 immediate_bytes: None,
1764 decoded: None,
1765 }
1766 }
1767
1768 fn node(address: Address, kind: CallKind, pcs: &[usize]) -> DebugNode {
1769 DebugNode::new(
1770 address,
1771 kind,
1772 pcs.iter().copied().map(step).collect(),
1773 Bytes::new(),
1774 0,
1775 None,
1776 )
1777 }
1778
1779 fn context_with_arena(arena: Vec<DebugNode>) -> DebuggerContext {
1780 DebuggerContext {
1781 debug_arena: arena,
1782 stats: None,
1783 identified_contracts: Default::default(),
1784 contracts_sources: ContractSources::default(),
1785 breakpoints: Breakpoints::default(),
1786 layout: Default::default(),
1787 }
1788 }
1789
1790 fn context_with_source_lines(address: Address) -> DebuggerContext {
1791 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[0, 1, 2])]);
1792 context.identified_contracts.insert(address, "Test".to_string());
1793
1794 let build_id = "test-build".to_string();
1795 context.contracts_sources.sources_by_id.entry(build_id.clone()).or_default().insert(
1796 0,
1797 Arc::new(SourceData {
1798 source: Arc::new("line one\nline two\nline three\n".to_string()),
1799 language: Default::default(),
1800 path: PathBuf::from("src/Test.sol"),
1801 contract_definitions: Vec::new(),
1802 debug_scopes: Vec::new(),
1803 }),
1804 );
1805 context.contracts_sources.artifacts_by_name.insert(
1806 "Test".to_string(),
1807 vec![ArtifactData {
1808 source_map: None,
1809 source_map_runtime: Some(
1810 Parser::new("0:8:0;9:8:0;18:10:0").collect::<Result<_, _>>().unwrap(),
1811 ),
1812 pc_ic_map: None,
1813 pc_ic_map_runtime: Some(PcIcMap::new(&[0x00, 0x00, 0x00])),
1814 build_id,
1815 file_id: 0,
1816 }],
1817 );
1818 context
1819 }
1820
1821 fn key(code: KeyCode) -> KeyEvent {
1822 KeyEvent::new(code, KeyModifiers::empty())
1823 }
1824
1825 fn ctrl_key(code: KeyCode) -> KeyEvent {
1826 KeyEvent::new(code, KeyModifiers::CONTROL)
1827 }
1828
1829 #[test]
1830 fn layout_shortcut_cycles_only_concrete_layouts() {
1831 let address = Address::repeat_byte(1);
1832 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1])]);
1833 let mut tui = TUIContext::new(&mut context);
1834 tui.init();
1835
1836 assert_eq!(tui.debugger_context.layout, DebuggerLayout::Auto);
1837
1838 let _ = tui.handle_key_event(key(KeyCode::Char('l')));
1839 assert_eq!(tui.debugger_context.layout, DebuggerLayout::Horizontal);
1840 assert_eq!(tui.status.as_ref().unwrap().text, "Debugger layout: horizontal");
1841
1842 let _ = tui.handle_key_event(key(KeyCode::Char('l')));
1843 assert_eq!(tui.debugger_context.layout, DebuggerLayout::Vertical);
1844 assert_eq!(tui.status.as_ref().unwrap().text, "Debugger layout: vertical");
1845
1846 let _ = tui.handle_key_event(key(KeyCode::Char('l')));
1847 assert_eq!(tui.debugger_context.layout, DebuggerLayout::Horizontal);
1848 assert_eq!(tui.status.as_ref().unwrap().text, "Debugger layout: horizontal");
1849 }
1850
1851 #[test]
1852 fn view_shortcuts_report_status() {
1853 let address = Address::repeat_byte(1);
1854 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1])]);
1855 let mut tui = TUIContext::new(&mut context);
1856 tui.init();
1857
1858 assert_eq!(tui.active_buffer, BufferKind::Memory);
1859 let _ = tui.handle_key_event(key(KeyCode::Char('b')));
1860 assert_eq!(tui.active_buffer, BufferKind::Calldata);
1861 assert_eq!(tui.status.as_ref().unwrap().text, "Active buffer: calldata");
1862
1863 let _ = tui.handle_key_event(key(KeyCode::Char('t')));
1864 assert!(tui.stack_labels);
1865 assert_eq!(tui.status.as_ref().unwrap().text, "Stack labels: on");
1866 let _ = tui.handle_key_event(key(KeyCode::Char('t')));
1867 assert!(!tui.stack_labels);
1868 assert_eq!(tui.status.as_ref().unwrap().text, "Stack labels: off");
1869
1870 let _ = tui.handle_key_event(key(KeyCode::Char('m')));
1871 assert!(tui.buf_utf);
1872 assert_eq!(tui.status.as_ref().unwrap().text, "UTF-8 decoding: on");
1873 let _ = tui.handle_key_event(key(KeyCode::Char('m')));
1874 assert!(!tui.buf_utf);
1875 assert_eq!(tui.status.as_ref().unwrap().text, "UTF-8 decoding: off");
1876
1877 let _ = tui.handle_key_event(key(KeyCode::Char('h')));
1878 assert!(!tui.show_shortcuts);
1879 assert_eq!(tui.status.as_ref().unwrap().text, "Shortcut help: hidden");
1880 let _ = tui.handle_key_event(key(KeyCode::Char('h')));
1881 assert!(tui.show_shortcuts);
1882 assert_eq!(tui.status.as_ref().unwrap().text, "Shortcut help: shown");
1883 }
1884
1885 #[test]
1886 fn previous_call_shortcut_respects_root_boundary() {
1887 let address = Address::repeat_byte(1);
1888 let mut context = context_with_arena(vec![
1889 node(address, CallKind::Call, &[1, 2]),
1890 node(address, CallKind::Call, &[3]),
1891 ]);
1892 let mut tui = TUIContext::new(&mut context);
1893 tui.init();
1894
1895 let _ = tui.handle_key_event(key(KeyCode::Char('c')));
1896 assert_eq!((tui.draw_memory.inner_call_index, tui.current_step), (0, 0));
1897
1898 tui.draw_memory.inner_call_index = 1;
1899 let _ = tui.handle_key_event(key(KeyCode::Char('c')));
1900 assert_eq!((tui.draw_memory.inner_call_index, tui.current_step), (0, 1));
1901 }
1902
1903 #[test]
1904 fn breakpoint_shortcut_cycles_trace_hits() {
1905 let address = Address::repeat_byte(1);
1906 let other = Address::repeat_byte(2);
1907 let mut context = context_with_arena(vec![
1908 node(other, CallKind::Call, &[1]),
1909 node(address, CallKind::Call, &[42, 7, 42]),
1910 node(address, CallKind::Call, &[8, 42]),
1911 ]);
1912 context.breakpoints.insert('a', (address, 42));
1913 let mut tui = TUIContext::new(&mut context);
1914 tui.init();
1915
1916 let _ = tui.handle_key_event(key(KeyCode::Char('\'')));
1917 let _ = tui.handle_key_event(key(KeyCode::Char('a')));
1918
1919 assert_eq!(tui.draw_memory.inner_call_index, 1);
1920 assert_eq!(tui.current_step, 0);
1921 let status = tui.status.as_ref().unwrap();
1922 assert_eq!(status.kind, StatusKind::Info);
1923 assert_eq!(status.text, "Jumped to breakpoint 'a' at PC 0x2a (42)");
1924
1925 let _ = tui.handle_key_event(key(KeyCode::Char('\'')));
1926 let _ = tui.handle_key_event(key(KeyCode::Char('a')));
1927 assert_eq!((tui.draw_memory.inner_call_index, tui.current_step), (1, 2));
1928
1929 let _ = tui.handle_key_event(key(KeyCode::Char('\'')));
1930 let _ = tui.handle_key_event(key(KeyCode::Char('a')));
1931 assert_eq!((tui.draw_memory.inner_call_index, tui.current_step), (2, 1));
1932
1933 let _ = tui.handle_key_event(key(KeyCode::Char('\'')));
1934 let _ = tui.handle_key_event(key(KeyCode::Char('a')));
1935 assert_eq!((tui.draw_memory.inner_call_index, tui.current_step), (1, 0));
1936 }
1937
1938 #[test]
1939 fn breakpoint_shortcut_reports_missing_key() {
1940 let address = Address::repeat_byte(1);
1941 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1])]);
1942 let mut tui = TUIContext::new(&mut context);
1943 tui.init();
1944
1945 let _ = tui.handle_key_event(key(KeyCode::Char('\'')));
1946 let _ = tui.handle_key_event(key(KeyCode::Char('z')));
1947
1948 assert_eq!(tui.draw_memory.inner_call_index, 0);
1949 assert_eq!(tui.current_step, 0);
1950 let status = tui.status.as_ref().unwrap();
1951 assert_eq!(status.kind, StatusKind::Error);
1952 assert_eq!(status.text, "Breakpoint 'z' not found");
1953 }
1954
1955 #[test]
1956 fn breakpoint_shortcut_reports_missing_trace_target() {
1957 let address = Address::repeat_byte(1);
1958 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1])]);
1959 context.breakpoints.insert('a', (address, 42));
1960 let mut tui = TUIContext::new(&mut context);
1961 tui.init();
1962
1963 let _ = tui.handle_key_event(key(KeyCode::Char('\'')));
1964 let _ = tui.handle_key_event(key(KeyCode::Char('a')));
1965
1966 assert_eq!(tui.draw_memory.inner_call_index, 0);
1967 assert_eq!(tui.current_step, 0);
1968 let status = tui.status.as_ref().unwrap();
1969 assert_eq!(status.kind, StatusKind::Error);
1970 assert_eq!(status.text, "Breakpoint 'a' target not found in trace");
1971 }
1972
1973 #[test]
1974 fn parses_prefixed_hex_pc() {
1975 assert_eq!(
1976 parse_pc_candidates("0x2a").unwrap(),
1977 vec![PcCandidate { pc: 42, base: PcBase::Hex }]
1978 );
1979 assert_eq!(
1980 parse_pc_candidates("0X2A").unwrap(),
1981 vec![PcCandidate { pc: 42, base: PcBase::Hex }]
1982 );
1983 }
1984
1985 #[test]
1986 fn parses_bare_hex_pc_with_letters() {
1987 assert_eq!(
1988 parse_pc_candidates("2a").unwrap(),
1989 vec![PcCandidate { pc: 42, base: PcBase::Hex }]
1990 );
1991 }
1992
1993 #[test]
1994 fn parses_explicit_decimal_pc() {
1995 assert_eq!(
1996 parse_pc_candidates("d:42").unwrap(),
1997 vec![PcCandidate { pc: 42, base: PcBase::Decimal }]
1998 );
1999 assert_eq!(
2000 parse_pc_candidates("dec:42").unwrap(),
2001 vec![PcCandidate { pc: 42, base: PcBase::Decimal }]
2002 );
2003 }
2004
2005 #[test]
2006 fn parses_bare_digits_as_decimal_and_hex_candidates() {
2007 assert_eq!(
2008 parse_pc_candidates("10").unwrap(),
2009 vec![
2010 PcCandidate { pc: 10, base: PcBase::Decimal },
2011 PcCandidate { pc: 16, base: PcBase::Hex },
2012 ]
2013 );
2014 assert_eq!(
2015 parse_pc_candidates("9").unwrap(),
2016 vec![PcCandidate { pc: 9, base: PcBase::Decimal }]
2017 );
2018 }
2019
2020 #[test]
2021 fn rejects_invalid_pc_input() {
2022 assert!(parse_pc_candidates("").is_err());
2023 assert!(parse_pc_candidates("0x").is_err());
2024 assert!(parse_pc_candidates("xyz").is_err());
2025 assert!(parse_pc_candidates("184467440737095516160").is_err());
2026 }
2027
2028 #[test]
2029 fn parses_buffer_offsets_as_visible_hex_labels() {
2030 assert_eq!(parse_buffer_offset("0x20").unwrap(), 32);
2031 assert_eq!(parse_buffer_offset("d:32").unwrap(), 32);
2032 assert_eq!(parse_buffer_offset("dec:32").unwrap(), 32);
2033 assert_eq!(parse_buffer_offset("20").unwrap(), 32);
2034 assert_eq!(parse_buffer_offset("2a").unwrap(), 42);
2035 assert_eq!(parse_buffer_offset("a").unwrap(), 10);
2036
2037 assert_eq!(parse_buffer_offset("").unwrap_err(), "Enter a buffer offset");
2038 assert_eq!(
2039 parse_buffer_offset("0x").unwrap_err(),
2040 "Invalid buffer offset `0x`; use hex 0x20/20 or decimal d:32"
2041 );
2042 assert_eq!(
2043 parse_buffer_offset("2x3").unwrap_err(),
2044 "Invalid buffer offset `2x3`; use hex 0x20/20 or decimal d:32"
2045 );
2046 }
2047
2048 #[test]
2049 fn parses_storage_slots_as_visible_hex_labels() {
2050 assert_eq!(parse_storage_slot("0x20").unwrap(), U256::from(32));
2051 assert_eq!(parse_storage_slot("d:32").unwrap(), U256::from(32));
2052 assert_eq!(parse_storage_slot("dec:32").unwrap(), U256::from(32));
2053 assert_eq!(parse_storage_slot("20").unwrap(), U256::from(32));
2054 assert_eq!(parse_storage_slot("2a").unwrap(), U256::from(42));
2055 assert_eq!(parse_storage_slot("a").unwrap(), U256::from(10));
2056
2057 assert_eq!(parse_storage_slot("").unwrap_err(), "Enter a storage slot");
2058 assert_eq!(
2059 parse_storage_slot("0x").unwrap_err(),
2060 "Invalid storage slot `0x`; use hex 0x20/20 or decimal d:32"
2061 );
2062 assert_eq!(
2063 parse_storage_slot("2x3").unwrap_err(),
2064 "Invalid storage slot `2x3`; use hex 0x20/20 or decimal d:32"
2065 );
2066 assert_eq!(
2067 parse_storage_slot("1_0").unwrap_err(),
2068 "Invalid storage slot `1_0`; use hex 0x20/20 or decimal d:32"
2069 );
2070 assert_eq!(
2071 parse_storage_slot("_").unwrap_err(),
2072 "Invalid storage slot `_`; use hex 0x20/20 or decimal d:32"
2073 );
2074 assert_eq!(
2075 parse_storage_slot("0x_").unwrap_err(),
2076 "Invalid storage slot `0x_`; use hex 0x20/20 or decimal d:32"
2077 );
2078 assert_eq!(
2079 parse_storage_slot("d:_").unwrap_err(),
2080 "Invalid storage slot `d:_`; use hex 0x20/20 or decimal d:32"
2081 );
2082 }
2083
2084 #[test]
2085 fn filters_buffer_offset_input_to_parser_prefixes() {
2086 assert!(is_buffer_offset_input_char("", '0'));
2087 assert!(is_buffer_offset_input_char("0", 'x'));
2088 assert!(is_buffer_offset_input_char("0x", '2'));
2089 assert!(is_buffer_offset_input_char("2", 'a'));
2090 assert!(is_buffer_offset_input_char("d", ':'));
2091 assert!(is_buffer_offset_input_char("dec", ':'));
2092 assert!(is_buffer_offset_input_char("dec:", '3'));
2093
2094 assert!(!is_buffer_offset_input_char("", 'x'));
2095 assert!(!is_buffer_offset_input_char("2", 'x'));
2096 assert!(!is_buffer_offset_input_char("1", ':'));
2097 assert!(!is_buffer_offset_input_char("DEC", ':'));
2098 assert!(!is_buffer_offset_input_char("d:", 'a'));
2099 }
2100
2101 #[test]
2102 fn finds_pc_in_current_node() {
2103 let address = Address::repeat_byte(1);
2104 let arena = vec![node(address, CallKind::Call, &[1, 2, 3])];
2105
2106 assert_eq!(
2107 find_pc_target(&arena, 0, 0, 3),
2108 Some(StepTarget { node_index: 0, step_index: 2, scope: StepTargetScope::CurrentNode })
2109 );
2110 }
2111
2112 #[test]
2113 fn repeated_pc_stays_current_then_prefers_next_then_previous() {
2114 let address = Address::repeat_byte(1);
2115 let arena = vec![node(address, CallKind::Call, &[1, 2, 3, 2])];
2116
2117 assert_eq!(find_pc_target(&arena, 0, 1, 2).unwrap().step_index, 1);
2118 assert_eq!(find_pc_target(&arena, 0, 0, 2).unwrap().step_index, 1);
2119 assert_eq!(find_pc_target(&arena, 0, 3, 2).unwrap().step_index, 3);
2120 assert_eq!(find_pc_target(&arena, 0, 2, 2).unwrap().step_index, 3);
2121 }
2122
2123 #[test]
2124 fn searches_later_then_earlier_same_code_context() {
2125 let address = Address::repeat_byte(1);
2126 let arena = vec![
2127 node(address, CallKind::Call, &[1]),
2128 node(address, CallKind::Call, &[2]),
2129 node(address, CallKind::Call, &[3]),
2130 ];
2131
2132 assert_eq!(find_pc_target(&arena, 1, 0, 3).unwrap().node_index, 2);
2133 assert_eq!(find_pc_target(&arena, 1, 0, 1).unwrap().node_index, 0);
2134 }
2135
2136 #[test]
2137 fn does_not_search_different_address_or_creation_context() {
2138 let address = Address::repeat_byte(1);
2139 let other = Address::repeat_byte(2);
2140 let arena = vec![
2141 node(address, CallKind::Call, &[1]),
2142 node(other, CallKind::Call, &[2]),
2143 node(address, CallKind::Create, &[3]),
2144 ];
2145
2146 assert!(find_pc_target(&arena, 0, 0, 2).is_none());
2147 assert!(find_pc_target(&arena, 0, 0, 3).is_none());
2148 assert!(pc_exists_outside_code_context(&arena, &arena[0], 2));
2149 assert!(pc_exists_outside_code_context(&arena, &arena[0], 3));
2150 }
2151
2152 #[test]
2153 fn goto_resolves_unambiguous_bare_digits_and_reports_ambiguity() {
2154 let address = Address::repeat_byte(1);
2155 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[10, 16, 42])]);
2156 let mut tui = TUIContext::new(&mut context);
2157 tui.init();
2158
2159 tui.goto_pc_from_input("2a");
2160 assert_eq!(tui.current_step, 2);
2161 assert_eq!(tui.status.as_ref().unwrap().kind, StatusKind::Info);
2162
2163 tui.current_step = 0;
2164 tui.goto_pc_from_input("10");
2165 assert_eq!(tui.current_step, 0);
2166 assert!(tui.status.as_ref().unwrap().text.contains("Ambiguous PC"));
2167
2168 tui.goto_pc_from_input("d:10");
2169 assert_eq!(tui.current_step, 0);
2170 assert_eq!(tui.status.as_ref().unwrap().kind, StatusKind::Info);
2171 }
2172
2173 #[test]
2174 fn goto_reports_pc_in_other_contract_without_moving() {
2175 let address = Address::repeat_byte(1);
2176 let other = Address::repeat_byte(2);
2177 let mut context = context_with_arena(vec![
2178 node(address, CallKind::Call, &[1]),
2179 node(other, CallKind::Call, &[42]),
2180 ]);
2181 let mut tui = TUIContext::new(&mut context);
2182 tui.init();
2183
2184 tui.goto_pc_from_input("2a");
2185 assert_eq!(tui.draw_memory.inner_call_index, 0);
2186 assert_eq!(tui.current_step, 0);
2187 let status = tui.status.as_ref().unwrap();
2188 assert_eq!(status.kind, StatusKind::Error);
2189 assert!(status.text.contains("exists in another contract"));
2190 }
2191
2192 #[test]
2193 fn goto_reports_ambiguous_input_in_other_contract_without_choosing_first_candidate() {
2194 let address = Address::repeat_byte(1);
2195 let other = Address::repeat_byte(2);
2196 let mut context = context_with_arena(vec![
2197 node(address, CallKind::Call, &[1]),
2198 node(other, CallKind::Call, &[16]),
2199 ]);
2200 let mut tui = TUIContext::new(&mut context);
2201 tui.init();
2202
2203 tui.goto_pc_from_input("10");
2204 let status = tui.status.as_ref().unwrap();
2205 assert_eq!(status.kind, StatusKind::Error);
2206 assert!(status.text.starts_with("PC `10` not found"));
2207 assert!(status.text.contains("exists in another contract"));
2208 }
2209
2210 #[test]
2211 fn pc_input_mode_handles_keys_and_blocks_normal_commands() {
2212 let address = Address::repeat_byte(1);
2213 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1, 42])]);
2214 let mut tui = TUIContext::new(&mut context);
2215 tui.init();
2216
2217 assert!(matches!(tui.handle_key_event(key(KeyCode::Char('p'))), ControlFlow::Continue(())));
2218 assert_eq!(tui.pc_input.as_deref(), Some(""));
2219
2220 let _ = tui.handle_key_event(key(KeyCode::Char('q')));
2221 assert_eq!(tui.pc_input.as_deref(), Some(""));
2222 assert_eq!(tui.current_step, 0);
2223
2224 let _ = tui.handle_key_event(key(KeyCode::Char('2')));
2225 let _ = tui.handle_key_event(key(KeyCode::Char('a')));
2226 assert_eq!(tui.pc_input.as_deref(), Some("2a"));
2227
2228 let _ = tui.handle_key_event(key(KeyCode::Backspace));
2229 assert_eq!(tui.pc_input.as_deref(), Some("2"));
2230 let _ = tui.handle_key_event(key(KeyCode::Char('a')));
2231 let _ = tui.handle_key_event(key(KeyCode::Enter));
2232
2233 assert_eq!(tui.pc_input, None);
2234 assert_eq!(tui.current_step, 1);
2235 assert_eq!(tui.status.as_ref().unwrap().kind, StatusKind::Info);
2236 }
2237
2238 #[test]
2239 fn pc_input_escape_cancels_without_moving() {
2240 let address = Address::repeat_byte(1);
2241 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1, 42])]);
2242 let mut tui = TUIContext::new(&mut context);
2243 tui.init();
2244
2245 let _ = tui.handle_key_event(key(KeyCode::Char('p')));
2246 let _ = tui.handle_key_event(key(KeyCode::Char('2')));
2247 let _ = tui.handle_key_event(key(KeyCode::Esc));
2248
2249 assert_eq!(tui.pc_input, None);
2250 assert_eq!(tui.current_step, 0);
2251 assert_eq!(tui.status, None);
2252 }
2253
2254 #[test]
2255 fn command_input_mode_handles_keys_and_blocks_normal_commands() {
2256 let address = Address::repeat_byte(1);
2257 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1, 42])]);
2258 let mut tui = TUIContext::new(&mut context);
2259 tui.init();
2260
2261 assert!(matches!(tui.handle_key_event(key(KeyCode::Char(':'))), ControlFlow::Continue(())));
2262 assert_eq!(tui.command_input.as_deref(), Some(""));
2263
2264 let _ = tui.handle_key_event(key(KeyCode::Char('q')));
2265 assert_eq!(tui.command_input.as_deref(), Some("q"));
2266 assert_eq!(tui.current_step, 0);
2267
2268 let _ = tui.handle_key_event(key(KeyCode::Backspace));
2269 for c in "continue 2a".chars() {
2270 let _ = tui.handle_key_event(key(KeyCode::Char(c)));
2271 }
2272 let _ = tui.handle_key_event(key(KeyCode::Enter));
2273
2274 assert_eq!(tui.command_input, None);
2275 assert_eq!(tui.current_step, 1);
2276 let status = tui.status.as_ref().unwrap();
2277 assert_eq!(status.kind, StatusKind::Info);
2278 assert_eq!(status.text, "Jumped to PC 0x2a (42) in current trace");
2279 }
2280
2281 #[test]
2282 fn command_prompt_jumps_to_named_buffer_offset() {
2283 let address = Address::repeat_byte(1);
2284 let mut context = context_with_arena(vec![DebugNode::new(
2285 address,
2286 CallKind::Call,
2287 vec![step(1)],
2288 Bytes::from(vec![0; 96]),
2289 0,
2290 None,
2291 )]);
2292 let mut tui = TUIContext::new(&mut context);
2293 tui.init();
2294
2295 tui.run_command_from_input("calldata 40");
2296
2297 assert_eq!(tui.active_buffer, BufferKind::Calldata);
2298 assert_eq!(tui.draw_memory.current_buf_startline, 2);
2299 assert_eq!(tui.status.as_ref().unwrap().text, "Jumped to calldata offset 0x40 (64)");
2300 }
2301
2302 #[test]
2303 fn command_prompt_jumps_to_storage_slot_access() {
2304 let address = Address::repeat_byte(1);
2305 let mut first_store = step(2);
2306 first_store.storage_change = Some(Box::new(StorageChange {
2307 key: U256::ZERO,
2308 value: U256::from(7),
2309 had_value: None,
2310 reason: StorageChangeReason::SSTORE,
2311 }));
2312 let mut store = step(42);
2313 store.storage_change = Some(Box::new(StorageChange {
2314 key: U256::from(1),
2315 value: U256::from(42),
2316 had_value: Some(U256::from(7)),
2317 reason: StorageChangeReason::SSTORE,
2318 }));
2319 let mut context = context_with_arena(vec![DebugNode::new(
2320 address,
2321 CallKind::Call,
2322 vec![step(1), first_store, store],
2323 Bytes::new(),
2324 0,
2325 None,
2326 )]);
2327 let mut tui = TUIContext::new(&mut context);
2328 tui.init();
2329
2330 tui.run_command_from_input("storage 1");
2331
2332 assert_eq!(tui.current_step, 2);
2333 assert_eq!(tui.active_storage, Some(StorageSpace::Persistent));
2334 assert_eq!(tui.storage_accesses(StorageSpace::Persistent).len(), 2);
2335 assert_eq!(
2336 tui.status.as_ref().unwrap().text,
2337 "Jumped to storage SSTORE slot 0x1: 0x7 -> 0x2a at PC 0x2a (42)"
2338 );
2339 }
2340
2341 #[test]
2342 fn command_prompt_jumps_to_transient_storage_slot_access() {
2343 let address = Address::repeat_byte(1);
2344 let steps = vec![step(1), step_with_stack(42, OpCode::TSTORE, &[0xbeef, 0x2a])];
2345 let mut context = context_with_arena(vec![DebugNode::new(
2346 address,
2347 CallKind::Call,
2348 steps,
2349 Bytes::new(),
2350 0,
2351 None,
2352 )]);
2353 let mut tui = TUIContext::new(&mut context);
2354 tui.init();
2355
2356 tui.run_command_from_input("transient 2a");
2357
2358 assert_eq!(tui.current_step, 1);
2359 assert_eq!(tui.active_storage, Some(StorageSpace::Transient));
2360 assert_eq!(
2361 tui.status.as_ref().unwrap().text,
2362 "Jumped to transient storage TSTORE slot 0x2a = 0xbeef at PC 0x2a (42)"
2363 );
2364
2365 tui.run_command_from_input("storage 2a");
2366 assert_eq!(tui.current_step, 1);
2367 assert_eq!(
2368 tui.status.as_ref().unwrap().text,
2369 "Storage slot 0x2a not accessed in current call"
2370 );
2371 }
2372
2373 #[test]
2374 fn command_prompt_searches_storage_across_split_call_segments() {
2375 let address = Address::repeat_byte(1);
2376 let mut store = step(42);
2377 store.storage_change = Some(Box::new(StorageChange {
2378 key: U256::from(1),
2379 value: U256::from(42),
2380 had_value: None,
2381 reason: StorageChangeReason::SSTORE,
2382 }));
2383
2384 let mut first_store = step(1);
2385 first_store.storage_change = Some(Box::new(StorageChange {
2386 key: U256::ZERO,
2387 value: U256::from(7),
2388 had_value: None,
2389 reason: StorageChangeReason::SSTORE,
2390 }));
2391 let mut first =
2392 DebugNode::new(address, CallKind::Call, vec![first_store], Bytes::new(), 0, None);
2393 first.trace_node_idx = 7;
2394 first.step_offset = 0;
2395
2396 let mut child_store = step(2);
2397 child_store.storage_change = Some(Box::new(StorageChange {
2398 key: U256::from(9),
2399 value: U256::from(99),
2400 had_value: None,
2401 reason: StorageChangeReason::SSTORE,
2402 }));
2403 let mut child = DebugNode::new(
2404 Address::repeat_byte(2),
2405 CallKind::Call,
2406 vec![child_store],
2407 Bytes::new(),
2408 0,
2409 None,
2410 );
2411 child.trace_node_idx = 8;
2412 child.step_offset = 1;
2413
2414 let mut second =
2415 DebugNode::new(address, CallKind::Call, vec![store], Bytes::new(), 0, None);
2416 second.trace_node_idx = 7;
2417 second.step_offset = 2;
2418
2419 let mut context = context_with_arena(vec![first, child, second]);
2420 let mut tui = TUIContext::new(&mut context);
2421 tui.init();
2422
2423 tui.run_command_from_input("storage 1");
2424
2425 assert_eq!(tui.draw_memory.inner_call_index, 2);
2426 assert_eq!(tui.current_step, 0);
2427 let accesses = tui.storage_accesses(StorageSpace::Persistent);
2428 assert_eq!(accesses.len(), 2);
2429 assert!(!accesses.contains_key(&U256::from(9)));
2430 assert_eq!(
2431 tui.status.as_ref().unwrap().text,
2432 "Jumped to storage SSTORE slot 0x1 = 0x2a at PC 0x2a (42)"
2433 );
2434 }
2435
2436 #[test]
2437 fn command_prompt_finds_warm_sload_from_stack_snapshots() {
2438 let address = Address::repeat_byte(1);
2439 let steps =
2440 vec![step_with_stack(1, OpCode::SLOAD, &[1]), step_with_stack(2, OpCode::STOP, &[42])];
2441 let mut context = context_with_arena(vec![DebugNode::new(
2442 address,
2443 CallKind::Call,
2444 steps,
2445 Bytes::new(),
2446 0,
2447 None,
2448 )]);
2449 let mut tui = TUIContext::new(&mut context);
2450 tui.init();
2451
2452 tui.run_command_from_input("store 1");
2453
2454 assert_eq!(tui.current_step, 0);
2455 assert_eq!(
2456 tui.status.as_ref().unwrap().text,
2457 "Jumped to storage SLOAD slot 0x1 = 0x2a at PC 0x1 (1)"
2458 );
2459 }
2460
2461 #[test]
2462 fn command_prompt_finds_warm_sstore_from_stack_snapshot() {
2463 let address = Address::repeat_byte(1);
2464 let steps = vec![step_with_stack(42, OpCode::SSTORE, &[42, 1])];
2465 let mut context = context_with_arena(vec![DebugNode::new(
2466 address,
2467 CallKind::Call,
2468 steps,
2469 Bytes::new(),
2470 0,
2471 None,
2472 )]);
2473 let mut tui = TUIContext::new(&mut context);
2474 tui.init();
2475
2476 tui.run_command_from_input("slot 1");
2477
2478 assert_eq!(tui.current_step, 0);
2479 assert_eq!(
2480 tui.status.as_ref().unwrap().text,
2481 "Jumped to storage SSTORE slot 0x1 = 0x2a at PC 0x2a (42)"
2482 );
2483 }
2484
2485 #[test]
2486 fn command_prompt_ignores_failed_sstore_stack_snapshot() {
2487 let address = Address::repeat_byte(1);
2488 let mut store = step_with_stack(42, OpCode::SSTORE, &[42, 1]);
2489 store.status = Some(InstructionResult::StateChangeDuringStaticCall);
2490 let mut context = context_with_arena(vec![DebugNode::new(
2491 address,
2492 CallKind::Call,
2493 vec![store],
2494 Bytes::new(),
2495 0,
2496 None,
2497 )]);
2498 let mut tui = TUIContext::new(&mut context);
2499 tui.init();
2500
2501 tui.run_command_from_input("slot 1");
2502
2503 assert_eq!(tui.current_step, 0);
2504 let status = tui.status.as_ref().unwrap();
2505 assert_eq!(status.kind, StatusKind::Error);
2506 assert_eq!(status.text, "Storage slot 0x1 not accessed in current call");
2507 }
2508
2509 #[test]
2510 fn command_prompt_ignores_failed_sstore_storage_change() {
2511 let address = Address::repeat_byte(1);
2512 let mut store = step_with_stack(42, OpCode::SSTORE, &[42, 1]);
2513 store.storage_change = Some(Box::new(StorageChange {
2514 key: U256::from(1),
2515 value: U256::from(42),
2516 had_value: Some(U256::ZERO),
2517 reason: StorageChangeReason::SSTORE,
2518 }));
2519 store.status = Some(InstructionResult::OutOfGas);
2520 let mut context = context_with_arena(vec![DebugNode::new(
2521 address,
2522 CallKind::Call,
2523 vec![store],
2524 Bytes::new(),
2525 0,
2526 None,
2527 )]);
2528 let mut tui = TUIContext::new(&mut context);
2529 tui.init();
2530
2531 tui.run_command_from_input("slot 1");
2532
2533 assert_eq!(tui.current_step, 0);
2534 let status = tui.status.as_ref().unwrap();
2535 assert_eq!(status.kind, StatusKind::Error);
2536 assert_eq!(status.text, "Storage slot 0x1 not accessed in current call");
2537 }
2538
2539 #[test]
2540 fn command_prompt_accepts_optional_leading_colon() {
2541 let address = Address::repeat_byte(1);
2542 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1, 42])]);
2543 let mut tui = TUIContext::new(&mut context);
2544 tui.init();
2545
2546 tui.run_command_from_input(":pc 2a");
2547
2548 assert_eq!(tui.current_step, 1);
2549 assert_eq!(tui.status.as_ref().unwrap().text, "Jumped to PC 0x2a (42) in current trace");
2550 }
2551
2552 #[test]
2553 fn command_prompt_continues_to_next_pc_hit() {
2554 let address = Address::repeat_byte(1);
2555 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1, 42, 2, 42])]);
2556 let mut tui = TUIContext::new(&mut context);
2557 tui.init();
2558 tui.current_step = 1;
2559
2560 tui.run_command_from_input("pc 2a");
2561 assert_eq!(tui.current_step, 1);
2562
2563 tui.run_command_from_input("continue 2a");
2564 assert_eq!(tui.current_step, 3);
2565 }
2566
2567 #[test]
2568 fn command_prompt_jumps_to_source_line() {
2569 let address = Address::repeat_byte(1);
2570 let mut context = context_with_source_lines(address);
2571 let mut tui = TUIContext::new(&mut context);
2572 tui.init();
2573
2574 tui.run_command_from_input("line 2");
2575
2576 assert_eq!(tui.current_step, 1);
2577 assert_eq!(tui.status.as_ref().unwrap().text, "Jumped to src/Test.sol:2 at PC 0x1 (1)");
2578 }
2579
2580 #[test]
2581 fn command_prompt_reports_help_and_usage_errors() {
2582 let address = Address::repeat_byte(1);
2583 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1])]);
2584 let mut tui = TUIContext::new(&mut context);
2585 tui.init();
2586
2587 tui.run_command_from_input("help");
2588 assert_eq!(tui.status.as_ref().unwrap().kind, StatusKind::Info);
2589 let help = &tui.status.as_ref().unwrap().text;
2590 for commands in [
2591 CONTINUE_COMMANDS,
2592 PC_COMMANDS,
2593 MEMORY_COMMANDS,
2594 CALLDATA_COMMANDS,
2595 RETURNDATA_COMMANDS,
2596 STORAGE_COMMANDS,
2597 TRANSIENT_STORAGE_COMMANDS,
2598 LINE_COMMANDS,
2599 OPCODE_COMMANDS,
2600 SOURCE_COMMANDS,
2601 VARIABLES_COMMANDS,
2602 STACK_COMMANDS,
2603 DATA_COMMANDS,
2604 ] {
2605 assert!(help.contains(&command_aliases(commands)));
2606 }
2607
2608 tui.run_command_from_input("mem");
2609 let status = tui.status.as_ref().unwrap();
2610 assert_eq!(status.kind, StatusKind::Info);
2611 assert_eq!(status.text, "Active buffer: memory");
2612
2613 tui.run_command_from_input("store");
2614 let status = tui.status.as_ref().unwrap();
2615 assert_eq!(status.kind, StatusKind::Info);
2616 assert_eq!(status.text, "Active data: storage");
2617
2618 tui.run_command_from_input("store 1 2");
2619 let status = tui.status.as_ref().unwrap();
2620 assert_eq!(status.kind, StatusKind::Error);
2621 assert_eq!(status.text, "Usage: :store <slot>");
2622
2623 tui.run_command_from_input("line");
2624 let status = tui.status.as_ref().unwrap();
2625 assert_eq!(status.kind, StatusKind::Error);
2626 assert_eq!(status.text, "Usage: :line <line>");
2627 }
2628
2629 #[test]
2630 fn command_prompt_toggles_panes() {
2631 let address = Address::repeat_byte(1);
2632 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1])]);
2633 let mut tui = TUIContext::new(&mut context);
2634 tui.init();
2635
2636 tui.run_command_from_input("opcodes");
2637 assert!(!tui.show_opcodes);
2638 assert_eq!(tui.status.as_ref().unwrap().text, "Opcodes pane: hidden");
2639
2640 tui.run_command_from_input(":ops");
2641 assert!(tui.show_opcodes);
2642 assert_eq!(tui.status.as_ref().unwrap().text, "Opcodes pane: shown");
2643
2644 tui.run_command_from_input("source");
2645 assert!(!tui.show_source);
2646 assert_eq!(tui.status.as_ref().unwrap().text, "Source pane: hidden");
2647
2648 tui.run_command_from_input(":src");
2649 assert!(tui.show_source);
2650 assert_eq!(tui.status.as_ref().unwrap().text, "Source pane: shown");
2651
2652 tui.run_command_from_input("variables");
2653 assert!(!tui.show_variables);
2654 assert_eq!(tui.status.as_ref().unwrap().text, "Variables pane: hidden");
2655
2656 tui.run_command_from_input(":vars");
2657 assert!(tui.show_variables);
2658 assert_eq!(tui.status.as_ref().unwrap().text, "Variables pane: shown");
2659
2660 tui.run_command_from_input("stack");
2661 assert!(!tui.show_stack);
2662 assert_eq!(tui.status.as_ref().unwrap().text, "Stack pane: hidden");
2663
2664 tui.run_command_from_input(":stack");
2665 assert!(tui.show_stack);
2666 assert_eq!(tui.status.as_ref().unwrap().text, "Stack pane: shown");
2667
2668 tui.run_command_from_input("data");
2669 assert!(!tui.show_data);
2670 assert_eq!(tui.status.as_ref().unwrap().text, "Data pane: hidden");
2671
2672 tui.run_command_from_input(":data");
2673 assert!(tui.show_data);
2674 assert_eq!(tui.status.as_ref().unwrap().text, "Data pane: shown");
2675
2676 tui.run_command_from_input("stack extra");
2677 let status = tui.status.as_ref().unwrap();
2678 assert_eq!(status.kind, StatusKind::Error);
2679 assert_eq!(status.text, "Usage: :stack");
2680 }
2681
2682 #[test]
2683 fn buffer_offset_input_mode_handles_calldata_offsets_and_blocks_normal_commands() {
2684 let address = Address::repeat_byte(1);
2685 let mut context = context_with_arena(vec![DebugNode::new(
2686 address,
2687 CallKind::Call,
2688 vec![step(1)],
2689 Bytes::from(vec![0; 96]),
2690 0,
2691 None,
2692 )]);
2693 let mut tui = TUIContext::new(&mut context);
2694 tui.init();
2695 tui.active_buffer = BufferKind::Calldata;
2696
2697 assert!(matches!(tui.handle_key_event(key(KeyCode::Char('o'))), ControlFlow::Continue(())));
2698 assert_eq!(tui.buffer_offset_input.as_deref(), Some(""));
2699
2700 let _ = tui.handle_key_event(key(KeyCode::Char('q')));
2701 assert_eq!(tui.buffer_offset_input.as_deref(), Some(""));
2702 assert_eq!(tui.draw_memory.current_buf_startline, 0);
2703
2704 for c in "40".chars() {
2705 let _ = tui.handle_key_event(key(KeyCode::Char(c)));
2706 }
2707 let _ = tui.handle_key_event(key(KeyCode::Enter));
2708
2709 assert_eq!(tui.buffer_offset_input, None);
2710 assert_eq!(tui.draw_memory.current_buf_startline, 2);
2711 let status = tui.status.as_ref().unwrap();
2712 assert_eq!(status.kind, StatusKind::Info);
2713 assert_eq!(status.text, "Jumped to calldata offset 0x40 (64)");
2714 }
2715
2716 #[test]
2717 fn buffer_offset_jumps_in_active_calldata_buffer() {
2718 let address = Address::repeat_byte(1);
2719 let mut context = context_with_arena(vec![DebugNode::new(
2720 address,
2721 CallKind::Call,
2722 vec![step(1)],
2723 Bytes::from(vec![0; 96]),
2724 0,
2725 None,
2726 )]);
2727 let mut tui = TUIContext::new(&mut context);
2728 tui.init();
2729 tui.active_buffer = BufferKind::Calldata;
2730
2731 tui.goto_buffer_offset_from_input("20");
2732
2733 assert_eq!(tui.draw_memory.current_buf_startline, 1);
2734 assert_eq!(tui.status.as_ref().unwrap().text, "Jumped to calldata offset 0x20 (32)");
2735 }
2736
2737 #[test]
2738 fn buffer_offset_jumps_to_visible_hex_label_on_partial_last_line() {
2739 let address = Address::repeat_byte(1);
2740 let mut context = context_with_arena(vec![DebugNode::new(
2741 address,
2742 CallKind::Call,
2743 vec![step(1)],
2744 Bytes::from(vec![0; 65]),
2745 0,
2746 None,
2747 )]);
2748 let mut tui = TUIContext::new(&mut context);
2749 tui.init();
2750 tui.active_buffer = BufferKind::Calldata;
2751
2752 tui.goto_buffer_offset_from_input("40");
2753
2754 assert_eq!(tui.draw_memory.current_buf_startline, 2);
2755 assert_eq!(tui.status.as_ref().unwrap().text, "Jumped to calldata offset 0x40 (64)");
2756 }
2757
2758 #[test]
2759 fn buffer_offset_reports_out_of_range_offsets_without_moving() {
2760 let address = Address::repeat_byte(1);
2761 let mut context = context_with_arena(vec![DebugNode::new(
2762 address,
2763 CallKind::Call,
2764 vec![step(1)],
2765 Bytes::from(vec![0; 64]),
2766 0,
2767 None,
2768 )]);
2769 let mut tui = TUIContext::new(&mut context);
2770 tui.init();
2771 tui.active_buffer = BufferKind::Calldata;
2772 tui.draw_memory.current_buf_startline = 1;
2773
2774 tui.goto_buffer_offset_from_input("20");
2775 assert_eq!(tui.draw_memory.current_buf_startline, 1);
2776 let status = tui.status.as_ref().unwrap();
2777 assert_eq!(status.kind, StatusKind::Info);
2778 assert_eq!(status.text, "Jumped to calldata offset 0x20 (32)");
2779
2780 tui.draw_memory.current_buf_startline = 0;
2781 tui.goto_buffer_offset_from_input("0x80");
2782 assert_eq!(tui.draw_memory.current_buf_startline, 0);
2783 let status = tui.status.as_ref().unwrap();
2784 assert_eq!(status.kind, StatusKind::Error);
2785 assert_eq!(status.text, "calldata offset 0x80 (128) is outside the 64-byte buffer");
2786 }
2787
2788 #[test]
2789 fn buffer_offset_escape_cancels_and_empty_buffer_reports_error() {
2790 let address = Address::repeat_byte(1);
2791 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1])]);
2792 let mut tui = TUIContext::new(&mut context);
2793 tui.init();
2794
2795 let _ = tui.handle_key_event(key(KeyCode::Char('o')));
2796 let _ = tui.handle_key_event(key(KeyCode::Char('2')));
2797 let _ = tui.handle_key_event(key(KeyCode::Esc));
2798
2799 assert_eq!(tui.buffer_offset_input, None);
2800 assert_eq!(tui.draw_memory.current_buf_startline, 0);
2801 assert_eq!(tui.status, None);
2802
2803 tui.goto_buffer_offset_from_input("0");
2804 let status = tui.status.as_ref().unwrap();
2805 assert_eq!(status.kind, StatusKind::Error);
2806 assert_eq!(status.text, "Current memory buffer is empty");
2807 }
2808
2809 #[test]
2810 fn buffer_scroll_reaches_partial_last_line() {
2811 let address = Address::repeat_byte(1);
2812 let mut context = context_with_arena(vec![DebugNode::new(
2813 address,
2814 CallKind::Call,
2815 vec![step(1)],
2816 Bytes::from(vec![0; 65]),
2817 0,
2818 None,
2819 )]);
2820 let mut tui = TUIContext::new(&mut context);
2821 tui.init();
2822 tui.active_buffer = BufferKind::Calldata;
2823
2824 let _ = tui.handle_key_event(ctrl_key(KeyCode::Char('j')));
2825 assert_eq!(tui.draw_memory.current_buf_startline, 1);
2826 let _ = tui.handle_key_event(ctrl_key(KeyCode::Char('j')));
2827 assert_eq!(tui.draw_memory.current_buf_startline, 2);
2828 let _ = tui.handle_key_event(ctrl_key(KeyCode::Char('j')));
2829 assert_eq!(tui.draw_memory.current_buf_startline, 2);
2830 }
2831
2832 #[test]
2833 fn storage_scroll_repeats_without_exceeding_last_slot() {
2834 let steps =
2835 (0..3).map(|slot| step_with_stack(slot, OpCode::TSTORE, &[slot, slot])).collect();
2836 let mut context = context_with_arena(vec![DebugNode::new(
2837 Address::ZERO,
2838 CallKind::Call,
2839 steps,
2840 Bytes::new(),
2841 0,
2842 None,
2843 )]);
2844 let mut tui = TUIContext::new(&mut context);
2845 tui.current_step = 2;
2846 tui.run_command_from_input("transient");
2847
2848 let _ = tui.handle_key_event(key(KeyCode::Char('2')));
2849 let _ = tui.handle_key_event(ctrl_key(KeyCode::Char('j')));
2850 assert_eq!(tui.draw_memory.current_storage_startline, 2);
2851
2852 let _ = tui.handle_key_event(ctrl_key(KeyCode::Char('j')));
2853 assert_eq!(tui.draw_memory.current_storage_startline, 2);
2854 }
2855
2856 #[test]
2857 fn opcode_search_wraps_and_is_case_insensitive() {
2858 let arena = vec![DebugNode::new(
2859 Address::repeat_byte(1),
2860 CallKind::Call,
2861 vec![
2862 step(1),
2863 step_with_immediate(2, OpCode::PUSH4, &[0x95, 0xd8, 0x9b, 0x41]),
2864 step_with_stack(3, OpCode::MSTORE, &[]),
2865 ],
2866 Bytes::new(),
2867 0,
2868 None,
2869 )];
2870
2871 assert_eq!(
2872 find_opcode_match(&arena, 0, 0, "push4", SearchDirection::Forward),
2873 Some((0, 1))
2874 );
2875 assert_eq!(
2876 find_opcode_match(&arena, 0, 0, "95D89B41", SearchDirection::Forward),
2877 Some((0, 1))
2878 );
2879 assert_eq!(
2880 find_opcode_match(&arena, 0, 0, "mstore", SearchDirection::Backward),
2881 Some((0, 2))
2882 );
2883 assert_eq!(find_opcode_match(&arena, 0, 0, "sload", SearchDirection::Forward), None);
2884 }
2885
2886 #[test]
2887 fn opcode_search_input_mode_handles_keys_and_blocks_normal_commands() {
2888 let address = Address::repeat_byte(1);
2889 let mut context = context_with_arena(vec![DebugNode::new(
2890 address,
2891 CallKind::Call,
2892 vec![
2893 step(1),
2894 step_with_immediate(2, OpCode::PUSH4, &[0x95, 0xd8, 0x9b, 0x41]),
2895 step_with_stack(3, OpCode::MSTORE, &[]),
2896 ],
2897 Bytes::new(),
2898 0,
2899 None,
2900 )]);
2901 let mut tui = TUIContext::new(&mut context);
2902 tui.init();
2903
2904 assert!(matches!(tui.handle_key_event(key(KeyCode::Char('/'))), ControlFlow::Continue(())));
2905 assert_eq!(tui.opcode_search_input.as_deref(), Some(""));
2906
2907 let _ = tui.handle_key_event(key(KeyCode::Char('q')));
2908 assert_eq!(tui.opcode_search_input.as_deref(), Some("q"));
2909 assert_eq!(tui.current_step, 0);
2910
2911 let _ = tui.handle_key_event(key(KeyCode::Backspace));
2912 let _ = tui.handle_key_event(key(KeyCode::Char('9')));
2913 let _ = tui.handle_key_event(key(KeyCode::Char('5')));
2914 let _ = tui.handle_key_event(key(KeyCode::Enter));
2915
2916 assert_eq!(tui.opcode_search_input, None);
2917 assert_eq!(tui.last_opcode_search.as_deref(), Some("95"));
2918 assert_eq!(tui.current_step, 1);
2919 assert_eq!(tui.status.as_ref().unwrap().kind, StatusKind::Info);
2920 }
2921
2922 #[test]
2923 fn opcode_search_repeats_forward_and_backward() {
2924 let address = Address::repeat_byte(1);
2925 let mut context = context_with_arena(vec![DebugNode::new(
2926 address,
2927 CallKind::Call,
2928 vec![
2929 step_with_stack(1, OpCode::MSTORE, &[]),
2930 step(2),
2931 step_with_stack(3, OpCode::MSTORE, &[]),
2932 ],
2933 Bytes::new(),
2934 0,
2935 None,
2936 )]);
2937 let mut tui = TUIContext::new(&mut context);
2938 tui.init();
2939
2940 let _ = tui.handle_key_event(key(KeyCode::Char('/')));
2941 for c in "mstore".chars() {
2942 let _ = tui.handle_key_event(key(KeyCode::Char(c)));
2943 }
2944 let _ = tui.handle_key_event(key(KeyCode::Enter));
2945 assert_eq!(tui.current_step, 2);
2946
2947 let _ = tui.handle_key_event(key(KeyCode::Char('n')));
2948 assert_eq!(tui.current_step, 0);
2949
2950 let _ = tui.handle_key_event(key(KeyCode::Char('N')));
2951 assert_eq!(tui.current_step, 2);
2952 }
2953
2954 #[test]
2955 fn opcode_search_crosses_split_call_segments() {
2956 let address = Address::repeat_byte(1);
2957 let node = |address, trace_node_idx, step_offset, steps| {
2958 let mut node = DebugNode::new(address, CallKind::Call, steps, Bytes::new(), 0, None);
2959 node.trace_node_idx = trace_node_idx;
2960 node.step_offset = step_offset;
2961 node
2962 };
2963 let arena = vec![
2964 node(address, 0, 0, vec![step_with_stack(1, OpCode::MSTORE, &[]), step(2)]),
2965 node(Address::repeat_byte(2), 1, 0, vec![step_with_stack(3, OpCode::MSTORE, &[])]),
2966 node(address, 0, 2, vec![step(4), step_with_stack(5, OpCode::MSTORE, &[])]),
2967 ];
2968
2969 assert_eq!(
2970 find_opcode_match(&arena, 0, 0, "mstore", SearchDirection::Forward),
2971 Some((2, 1))
2972 );
2973 assert_eq!(
2974 find_opcode_match(&arena, 2, 1, "mstore", SearchDirection::Forward),
2975 Some((0, 0))
2976 );
2977 assert_eq!(
2978 find_opcode_match(&arena, 0, 0, "mstore", SearchDirection::Backward),
2979 Some((2, 1))
2980 );
2981 }
2982
2983 #[test]
2984 fn opcode_search_escape_cancels_without_moving() {
2985 let address = Address::repeat_byte(1);
2986 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1, 42])]);
2987 let mut tui = TUIContext::new(&mut context);
2988 tui.init();
2989
2990 let _ = tui.handle_key_event(key(KeyCode::Char('/')));
2991 let _ = tui.handle_key_event(key(KeyCode::Char('s')));
2992 let _ = tui.handle_key_event(key(KeyCode::Esc));
2993
2994 assert_eq!(tui.opcode_search_input, None);
2995 assert_eq!(tui.last_opcode_search, None);
2996 assert_eq!(tui.current_step, 0);
2997 assert_eq!(tui.status, None);
2998 }
2999
3000 #[test]
3001 fn opcode_search_reports_empty_input_without_remembering_search() {
3002 let address = Address::repeat_byte(1);
3003 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1, 42])]);
3004 let mut tui = TUIContext::new(&mut context);
3005 tui.init();
3006
3007 let _ = tui.handle_key_event(key(KeyCode::Char('/')));
3008 let _ = tui.handle_key_event(key(KeyCode::Enter));
3009
3010 assert_eq!(tui.current_step, 0);
3011 assert_eq!(tui.last_opcode_search, None);
3012 let status = tui.status.as_ref().unwrap();
3013 assert_eq!(status.kind, StatusKind::Error);
3014 assert_eq!(status.text, "Enter an opcode search term");
3015 }
3016
3017 #[test]
3018 fn opcode_search_reports_repeat_without_previous_search() {
3019 let address = Address::repeat_byte(1);
3020 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1, 42])]);
3021 let mut tui = TUIContext::new(&mut context);
3022 tui.init();
3023
3024 let _ = tui.handle_key_event(key(KeyCode::Char('n')));
3025
3026 assert_eq!(tui.current_step, 0);
3027 let status = tui.status.as_ref().unwrap();
3028 assert_eq!(status.kind, StatusKind::Error);
3029 assert_eq!(status.text, "No previous opcode search");
3030 }
3031
3032 #[test]
3033 fn opcode_search_reports_no_match_without_moving() {
3034 let address = Address::repeat_byte(1);
3035 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1, 42])]);
3036 let mut tui = TUIContext::new(&mut context);
3037 tui.init();
3038
3039 let _ = tui.handle_key_event(key(KeyCode::Char('/')));
3040 for c in "sload".chars() {
3041 let _ = tui.handle_key_event(key(KeyCode::Char(c)));
3042 }
3043 let _ = tui.handle_key_event(key(KeyCode::Enter));
3044
3045 assert_eq!(tui.current_step, 0);
3046 assert_eq!(tui.last_opcode_search.as_deref(), Some("sload"));
3047 let status = tui.status.as_ref().unwrap();
3048 assert_eq!(status.kind, StatusKind::Error);
3049 assert_eq!(status.text, "No opcode matching `sload` in current call");
3050 }
3051
3052 #[test]
3053 fn memory_write_start_line_uses_write_offset() {
3054 assert_eq!(memory_write_start_line(&step_with_stack(0, OpCode::MSTORE, &[0, 96])), Some(3));
3055 assert_eq!(
3056 memory_write_start_line(&step_with_stack(0, OpCode::MSTORE8, &[0, 33])),
3057 Some(1)
3058 );
3059 assert_eq!(memory_write_start_line(&step(0)), None);
3060 }
3061
3062 #[test]
3063 fn bounded_memory_write_start_line_requires_visible_non_empty_write() {
3064 let write_at_128 = step_with_stack(0, OpCode::MSTORE, &[0, 128]);
3065 assert_eq!(bounded_memory_write_start_line(&write_at_128, 160), Some(4));
3066 assert_eq!(bounded_memory_write_start_line(&write_at_128, 128), None);
3067
3068 let zero_len_copy = step_with_stack(0, OpCode::CALLDATACOPY, &[0, 0, 1_000_000]);
3069 assert_eq!(memory_write_start_line(&zero_len_copy), None);
3070 assert_eq!(bounded_memory_write_start_line(&zero_len_copy, 32), None);
3071 }
3072
3073 #[test]
3074 fn stepping_past_memory_write_without_memory_snapshot_keeps_scroll_position() {
3075 let address = Address::repeat_byte(1);
3076 let mut context = context_with_arena(vec![DebugNode::new(
3077 address,
3078 CallKind::Call,
3079 vec![step_with_stack(1, OpCode::MSTORE, &[0, 128]), step(2)],
3080 Bytes::new(),
3081 0,
3082 None,
3083 )]);
3084 let mut tui = TUIContext::new(&mut context);
3085 tui.init();
3086 tui.draw_memory.current_buf_startline = 99;
3087
3088 tui.step();
3089
3090 assert_eq!(tui.current_step, 1);
3091 assert_eq!(tui.draw_memory.current_buf_startline, 99);
3092 }
3093
3094 #[test]
3095 fn memory_write_autoscroll_only_applies_to_memory_buffer() {
3096 let address = Address::repeat_byte(1);
3097 let mut context = context_with_arena(vec![DebugNode::new(
3098 address,
3099 CallKind::Call,
3100 vec![step_with_stack(1, OpCode::MSTORE, &[0, 128]), step(2)],
3101 Bytes::from(vec![0; 256]),
3102 0,
3103 None,
3104 )]);
3105 let mut tui = TUIContext::new(&mut context);
3106 tui.init();
3107 tui.active_buffer = BufferKind::Calldata;
3108 tui.draw_memory.current_buf_startline = 7;
3109
3110 tui.step();
3111
3112 assert_eq!(tui.current_step, 1);
3113 assert_eq!(tui.draw_memory.current_buf_startline, 7);
3114 }
3115
3116 #[test]
3117 fn navigation_clamps_scroll_positions_to_non_empty_data() {
3118 let address = Address::repeat_byte(1);
3119 let mut context = context_with_arena(vec![
3120 DebugNode::new(
3121 address,
3122 CallKind::Call,
3123 vec![step_with_stack(1, OpCode::STOP, &[0, 1, 2])],
3124 Bytes::from(vec![0; 64]),
3125 0,
3126 None,
3127 ),
3128 DebugNode::new(
3129 address,
3130 CallKind::Call,
3131 vec![step_with_stack(2, OpCode::STOP, &[0])],
3132 Bytes::from(vec![0; 4]),
3133 0,
3134 None,
3135 ),
3136 ]);
3137 let mut tui = TUIContext::new(&mut context);
3138 tui.init();
3139 tui.active_buffer = BufferKind::Calldata;
3140 tui.draw_memory.current_buf_startline = 1;
3141 tui.draw_memory.current_stack_startline = 2;
3142
3143 let _ = tui.handle_key_event(key(KeyCode::Char('C')));
3144
3145 assert_eq!(tui.draw_memory.inner_call_index, 1);
3146 assert_eq!(tui.draw_memory.current_buf_startline, 0);
3147 assert_eq!(tui.draw_memory.current_stack_startline, 0);
3148 }
3149
3150 #[test]
3151 fn navigation_preserves_stack_scroll_across_empty_snapshot() {
3152 let address = Address::repeat_byte(1);
3153 let mut empty_stack = step(2);
3154 empty_stack.stack = Some(Vec::new().into_boxed_slice());
3155 let mut context = context_with_arena(vec![DebugNode::new(
3156 address,
3157 CallKind::Call,
3158 vec![
3159 step_with_stack(1, OpCode::STOP, &[0, 1, 2]),
3160 empty_stack,
3161 step_with_stack(3, OpCode::STOP, &[0, 1, 2]),
3162 ],
3163 Bytes::new(),
3164 0,
3165 None,
3166 )]);
3167 let mut tui = TUIContext::new(&mut context);
3168 tui.init();
3169 tui.draw_memory.current_stack_startline = 2;
3170
3171 tui.step();
3172 assert_eq!(tui.draw_memory.current_stack_startline, 2);
3173
3174 tui.step();
3175 assert_eq!(tui.draw_memory.current_stack_startline, 2);
3176 }
3177}