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