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