Skip to main content

foundry_debugger/tui/
context.rs

1//! Debugger context and event handler implementation.
2
3use crate::{DebugNode, ExitReason, debugger::DebuggerContext};
4use alloy_primitives::{Address, hex};
5use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};
6use foundry_evm_core::buffer::BufferKind;
7use foundry_tui::TuiApp;
8use ratatui::Frame;
9use revm::bytecode::opcode::OpCode;
10use revm_inspectors::tracing::types::{CallKind, CallTraceStep};
11use std::ops::ControlFlow;
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub(crate) enum StatusKind {
15    Info,
16    Error,
17}
18
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub(crate) struct StatusMessage {
21    pub(crate) kind: StatusKind,
22    pub(crate) text: String,
23}
24
25/// This is currently used to remember last scroll position so screen doesn't wiggle as much.
26#[derive(Default)]
27pub(crate) struct DrawMemory {
28    pub(crate) inner_call_index: usize,
29    pub(crate) current_buf_startline: usize,
30    pub(crate) current_stack_startline: usize,
31}
32
33pub(crate) struct TUIContext<'a> {
34    pub(crate) debugger_context: &'a mut DebuggerContext,
35
36    /// Buffer for keys prior to execution, i.e. '10' + 'k' => move up 10 operations.
37    pub(crate) key_buffer: String,
38    /// Current goto program counter prompt contents, if the prompt is active.
39    pub(crate) pc_input: Option<String>,
40    /// Last status or error message to show in the footer.
41    pub(crate) status: Option<StatusMessage>,
42    /// Current step in the debug steps.
43    pub(crate) current_step: usize,
44    pub(crate) draw_memory: DrawMemory,
45    pub(crate) opcode_list: Vec<String>,
46    pub(crate) last_index: usize,
47
48    pub(crate) stack_labels: bool,
49    /// Whether to decode active buffer as utf8 or not.
50    pub(crate) buf_utf: bool,
51    pub(crate) show_shortcuts: bool,
52    /// The currently active buffer (memory, calldata, returndata) to be drawn.
53    pub(crate) active_buffer: BufferKind,
54}
55
56impl<'a> TUIContext<'a> {
57    pub(crate) fn new(debugger_context: &'a mut DebuggerContext) -> Self {
58        TUIContext {
59            debugger_context,
60
61            key_buffer: String::with_capacity(64),
62            pc_input: None,
63            status: None,
64            current_step: 0,
65            draw_memory: DrawMemory::default(),
66            opcode_list: Vec::new(),
67            last_index: 0,
68
69            stack_labels: false,
70            buf_utf: false,
71            show_shortcuts: true,
72            active_buffer: BufferKind::Memory,
73        }
74    }
75
76    pub(crate) fn init(&mut self) {
77        self.gen_opcode_list();
78    }
79
80    pub(crate) fn debug_arena(&self) -> &[DebugNode] {
81        &self.debugger_context.debug_arena
82    }
83
84    pub(crate) fn debug_call(&self) -> &DebugNode {
85        &self.debug_arena()[self.draw_memory.inner_call_index]
86    }
87
88    /// Returns the current call address.
89    pub(crate) fn address(&self) -> &Address {
90        &self.debug_call().address
91    }
92
93    /// Returns the current call kind.
94    pub(crate) fn call_kind(&self) -> CallKind {
95        self.debug_call().kind
96    }
97
98    /// Returns the current debug steps.
99    pub(crate) fn debug_steps(&self) -> &[CallTraceStep] {
100        &self.debug_call().steps
101    }
102
103    /// Returns the current debug step.
104    pub(crate) fn current_step(&self) -> &CallTraceStep {
105        &self.debug_steps()[self.current_step]
106    }
107
108    fn gen_opcode_list(&mut self) {
109        self.opcode_list.clear();
110        let debug_steps =
111            &self.debugger_context.debug_arena[self.draw_memory.inner_call_index].steps;
112        for step in debug_steps {
113            self.opcode_list.push(pretty_opcode(step));
114        }
115    }
116
117    fn gen_opcode_list_if_necessary(&mut self) {
118        if self.last_index != self.draw_memory.inner_call_index {
119            self.gen_opcode_list();
120            self.last_index = self.draw_memory.inner_call_index;
121        }
122    }
123
124    fn active_buffer(&self) -> &[u8] {
125        match self.active_buffer {
126            BufferKind::Memory => self.current_step().memory.as_ref().unwrap().as_bytes(),
127            BufferKind::Calldata => &self.debug_call().calldata,
128            BufferKind::Returndata => &self.current_step().returndata,
129        }
130    }
131}
132
133impl TUIContext<'_> {
134    pub(crate) fn handle_event(&mut self, event: Event) -> ControlFlow<ExitReason> {
135        let ret = match event {
136            Event::Key(event) => self.handle_key_event(event),
137            Event::Mouse(event) => self.handle_mouse_event(event),
138            _ => ControlFlow::Continue(()),
139        };
140        // Generate the list after the event has been handled.
141        self.gen_opcode_list_if_necessary();
142        ret
143    }
144
145    fn handle_key_event(&mut self, event: KeyEvent) -> ControlFlow<ExitReason> {
146        if self.pc_input.is_some() {
147            self.handle_pc_input_key_event(event);
148            return ControlFlow::Continue(());
149        }
150
151        // Breakpoints
152        if let KeyCode::Char(c) = event.code
153            && c.is_alphabetic()
154            && self.key_buffer.starts_with('\'')
155        {
156            self.handle_breakpoint(c);
157            return ControlFlow::Continue(());
158        }
159
160        let control = event.modifiers.contains(KeyModifiers::CONTROL);
161
162        match event.code {
163            // Exit
164            KeyCode::Char('q') => return ControlFlow::Break(ExitReason::CharExit),
165
166            // Scroll up the memory buffer
167            KeyCode::Char('k') | KeyCode::Up if control => self.repeat(|this| {
168                this.draw_memory.current_buf_startline =
169                    this.draw_memory.current_buf_startline.saturating_sub(1);
170            }),
171            // Scroll down the memory buffer
172            KeyCode::Char('j') | KeyCode::Down if control => self.repeat(|this| {
173                let max_buf = (this.active_buffer().len() / 32).saturating_sub(1);
174                if this.draw_memory.current_buf_startline < max_buf {
175                    this.draw_memory.current_buf_startline += 1;
176                }
177            }),
178
179            // Move up
180            KeyCode::Char('k') | KeyCode::Up => self.repeat(Self::step_back),
181            // Move down
182            KeyCode::Char('j') | KeyCode::Down => self.repeat(Self::step),
183
184            // Scroll up the stack
185            KeyCode::Char('K') => self.repeat(|this| {
186                this.draw_memory.current_stack_startline =
187                    this.draw_memory.current_stack_startline.saturating_sub(1);
188            }),
189            // Scroll down the stack
190            KeyCode::Char('J') => self.repeat(|this| {
191                let max_stack =
192                    this.current_step().stack.as_ref().map_or(0, |s| s.len()).saturating_sub(1);
193                if this.draw_memory.current_stack_startline < max_stack {
194                    this.draw_memory.current_stack_startline += 1;
195                }
196            }),
197
198            // Cycle buffers
199            KeyCode::Char('b') => {
200                self.active_buffer = self.active_buffer.next();
201                self.draw_memory.current_buf_startline = 0;
202            }
203
204            // Go to top of file
205            KeyCode::Char('g') => {
206                self.draw_memory.inner_call_index = 0;
207                self.current_step = 0;
208            }
209
210            // Go to bottom of file
211            KeyCode::Char('G') => {
212                self.draw_memory.inner_call_index = self.debug_arena().len() - 1;
213                self.current_step = self.n_steps() - 1;
214            }
215
216            // Go to previous call
217            KeyCode::Char('c') => {
218                self.draw_memory.inner_call_index =
219                    self.draw_memory.inner_call_index.saturating_sub(1);
220                self.current_step = self.n_steps() - 1;
221            }
222
223            // Go to next call
224            KeyCode::Char('C')
225                if self.debug_arena().len() > self.draw_memory.inner_call_index + 1 =>
226            {
227                self.draw_memory.inner_call_index += 1;
228                self.current_step = 0;
229            }
230
231            // Step forward
232            KeyCode::Char('s') => self.repeat(|this| {
233                let remaining_steps = &this.debug_steps()[this.current_step..];
234                if let Some((i, _)) =
235                    remaining_steps.iter().enumerate().skip(1).find(|(i, step)| {
236                        let prev = &remaining_steps[*i - 1];
237                        is_jump(step, prev)
238                    })
239                {
240                    this.current_step += i
241                }
242            }),
243
244            // Step backwards
245            KeyCode::Char('a') => self.repeat(|this| {
246                let ops = &this.debug_steps()[..this.current_step];
247                this.current_step = ops
248                    .iter()
249                    .enumerate()
250                    .skip(1)
251                    .rev()
252                    .find(|&(i, op)| {
253                        let prev = &ops[i - 1];
254                        is_jump(op, prev)
255                    })
256                    .map(|(i, _)| i)
257                    .unwrap_or_default();
258            }),
259
260            // Toggle stack labels
261            KeyCode::Char('t') => self.stack_labels = !self.stack_labels,
262
263            // Toggle memory UTF-8 decoding
264            KeyCode::Char('m') => self.buf_utf = !self.buf_utf,
265
266            // Go to program counter
267            KeyCode::Char('p') => {
268                self.key_buffer.clear();
269                self.status = None;
270                self.pc_input = Some(String::new());
271            }
272
273            // Toggle help notice
274            KeyCode::Char('h') => self.show_shortcuts = !self.show_shortcuts,
275
276            // Numbers for repeating commands or breakpoints
277            KeyCode::Char(
278                other @ ('0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '\''),
279            ) => {
280                // Early return to not clear the buffer.
281                self.key_buffer.push(other);
282                return ControlFlow::Continue(());
283            }
284
285            // Unknown/unhandled key code
286            _ => {}
287        };
288
289        self.key_buffer.clear();
290        ControlFlow::Continue(())
291    }
292
293    fn handle_pc_input_key_event(&mut self, event: KeyEvent) {
294        match event.code {
295            KeyCode::Esc => {
296                self.pc_input = None;
297            }
298            KeyCode::Enter => {
299                let input = self.pc_input.take().unwrap_or_default();
300                self.goto_pc_from_input(&input);
301            }
302            KeyCode::Backspace => {
303                if let Some(input) = &mut self.pc_input {
304                    input.pop();
305                }
306            }
307            KeyCode::Char(c)
308                if !event.modifiers.contains(KeyModifiers::CONTROL) && is_pc_input_char(c) =>
309            {
310                if let Some(input) = &mut self.pc_input {
311                    input.push(c);
312                }
313            }
314            _ => {}
315        }
316    }
317
318    fn goto_pc_from_input(&mut self, input: &str) {
319        let candidates = match parse_pc_candidates(input) {
320            Ok(candidates) => candidates,
321            Err(err) => {
322                self.set_error(err);
323                return;
324            }
325        };
326
327        let mut found = Vec::new();
328        for &candidate in &candidates {
329            if let Some(target) = find_pc_target(
330                self.debug_arena(),
331                self.draw_memory.inner_call_index,
332                self.current_step,
333                candidate.pc,
334            ) {
335                found.push((candidate, target));
336            }
337        }
338
339        match found.as_slice() {
340            [] => {
341                let current = self.debug_call();
342                let outside = candidates.iter().any(|candidate| {
343                    pc_exists_outside_code_context(self.debug_arena(), current, candidate.pc)
344                });
345                let pc = if let [candidate] = candidates.as_slice() {
346                    let pc = candidate.pc;
347                    format!("PC 0x{pc:x} ({pc})")
348                } else {
349                    format!("PC `{}`", input.trim())
350                };
351                let mut msg = format!("{pc} not found in current contract");
352                if outside {
353                    msg.push_str("; it exists in another contract, switch calls first");
354                }
355                self.set_error(msg);
356            }
357            [(candidate, target)] => self.apply_pc_target(*candidate, *target),
358            _ => {
359                let input = input.trim();
360                let options = found
361                    .iter()
362                    .map(|(candidate, _)| candidate.describe())
363                    .collect::<Vec<_>>()
364                    .join(" and ");
365                self.set_error(format!(
366                    "Ambiguous PC `{input}`: {options} both exist; use d:<pc> or 0x<pc>"
367                ));
368            }
369        }
370    }
371
372    fn apply_pc_target(&mut self, candidate: PcCandidate, target: PcTarget) {
373        let already_at_target = self.draw_memory.inner_call_index == target.node_index
374            && self.current_step == target.step_index;
375
376        self.draw_memory.inner_call_index = target.node_index;
377        self.current_step = target.step_index;
378        self.draw_memory.current_buf_startline = 0;
379        self.draw_memory.current_stack_startline = 0;
380        self.key_buffer.clear();
381
382        let pc = candidate.pc;
383        let scope = match target.scope {
384            PcTargetScope::CurrentNode => "current trace",
385            PcTargetScope::SameCodeContext => "same contract",
386        };
387        let action = if already_at_target { "Already at" } else { "Jumped to" };
388        self.set_info(format!("{action} PC 0x{pc:x} ({pc}) in {scope}"));
389    }
390
391    fn set_info(&mut self, text: String) {
392        self.status = Some(StatusMessage { kind: StatusKind::Info, text });
393    }
394
395    fn set_error(&mut self, text: String) {
396        self.status = Some(StatusMessage { kind: StatusKind::Error, text });
397    }
398
399    fn handle_breakpoint(&mut self, c: char) {
400        // Find the location of the called breakpoint in the whole debug arena (at this address with
401        // this pc)
402        if let Some((caller, pc)) = self.debugger_context.breakpoints.get(&c) {
403            for (i, node) in self.debug_arena().iter().enumerate() {
404                if node.address == *caller
405                    && let Some(step) = node.steps.iter().position(|step| step.pc == *pc)
406                {
407                    self.draw_memory.inner_call_index = i;
408                    self.current_step = step;
409                    break;
410                }
411            }
412        }
413        self.key_buffer.clear();
414    }
415
416    fn handle_mouse_event(&mut self, event: MouseEvent) -> ControlFlow<ExitReason> {
417        if self.pc_input.is_some() {
418            return ControlFlow::Continue(());
419        }
420
421        match event.kind {
422            MouseEventKind::ScrollUp => self.step_back(),
423            MouseEventKind::ScrollDown => self.step(),
424            _ => {}
425        }
426
427        ControlFlow::Continue(())
428    }
429
430    fn step_back(&mut self) {
431        if self.current_step > 0 {
432            self.current_step -= 1;
433        } else if self.draw_memory.inner_call_index > 0 {
434            self.draw_memory.inner_call_index -= 1;
435            self.current_step = self.n_steps() - 1;
436        }
437    }
438
439    fn step(&mut self) {
440        if self.current_step < self.n_steps() - 1 {
441            self.current_step += 1;
442        } else if self.draw_memory.inner_call_index < self.debug_arena().len() - 1 {
443            self.draw_memory.inner_call_index += 1;
444            self.current_step = 0;
445        }
446    }
447
448    /// Calls a closure `f` the number of times specified in the key buffer, and at least once.
449    fn repeat(&mut self, mut f: impl FnMut(&mut Self)) {
450        for _ in 0..buffer_as_number(&self.key_buffer) {
451            f(self);
452        }
453    }
454
455    fn n_steps(&self) -> usize {
456        self.debug_steps().len()
457    }
458}
459
460impl TuiApp for TUIContext<'_> {
461    type Exit = ExitReason;
462
463    fn draw(&mut self, frame: &mut Frame<'_>) {
464        self.draw_layout(frame);
465    }
466
467    fn handle_event(&mut self, event: Event) -> ControlFlow<Self::Exit> {
468        TUIContext::handle_event(self, event)
469    }
470}
471
472/// Grab number from buffer. Used for something like '10k' to move up 10 operations
473fn buffer_as_number(s: &str) -> usize {
474    const MIN: usize = 1;
475    const MAX: usize = 100_000;
476    s.parse().unwrap_or(MIN).clamp(MIN, MAX)
477}
478
479const fn is_pc_input_char(c: char) -> bool {
480    c.is_ascii_hexdigit() || matches!(c, 'x' | 'X' | ':')
481}
482
483#[derive(Clone, Copy, Debug, PartialEq, Eq)]
484enum PcBase {
485    Hex,
486    Decimal,
487}
488
489#[derive(Clone, Copy, Debug, PartialEq, Eq)]
490struct PcCandidate {
491    pc: usize,
492    base: PcBase,
493}
494
495impl PcCandidate {
496    fn describe(self) -> String {
497        match self.base {
498            PcBase::Hex => format!("hex 0x{:x}", self.pc),
499            PcBase::Decimal => format!("decimal {}", self.pc),
500        }
501    }
502}
503
504#[derive(Clone, Copy, Debug, PartialEq, Eq)]
505enum PcTargetScope {
506    CurrentNode,
507    SameCodeContext,
508}
509
510#[derive(Clone, Copy, Debug, PartialEq, Eq)]
511struct PcTarget {
512    node_index: usize,
513    step_index: usize,
514    scope: PcTargetScope,
515}
516
517fn parse_pc_candidates(input: &str) -> Result<Vec<PcCandidate>, String> {
518    let input = input.trim();
519    if input.is_empty() {
520        return Err("Enter a program counter".to_string());
521    }
522
523    if let Some(rest) = input.strip_prefix("0x").or_else(|| input.strip_prefix("0X")) {
524        return parse_pc_candidate(rest, 16, PcBase::Hex, input);
525    }
526
527    if let Some(rest) = input.strip_prefix("d:").or_else(|| input.strip_prefix("dec:")) {
528        return parse_pc_candidate(rest, 10, PcBase::Decimal, input);
529    }
530
531    if input.chars().any(|c| c.is_ascii_hexdigit() && c.is_ascii_alphabetic()) {
532        return parse_pc_candidate(input, 16, PcBase::Hex, input);
533    }
534
535    if input.chars().all(|c| c.is_ascii_digit()) {
536        let decimal = parse_pc(input, 10, input)?;
537        let hex = parse_pc(input, 16, input)?;
538        if decimal == hex {
539            return Ok(vec![PcCandidate { pc: decimal, base: PcBase::Decimal }]);
540        }
541        return Ok(vec![
542            PcCandidate { pc: decimal, base: PcBase::Decimal },
543            PcCandidate { pc: hex, base: PcBase::Hex },
544        ]);
545    }
546
547    Err(format!("Invalid PC `{input}`; use 0x2a, 2a, or d:42"))
548}
549
550fn parse_pc_candidate(
551    input: &str,
552    radix: u32,
553    base: PcBase,
554    original: &str,
555) -> Result<Vec<PcCandidate>, String> {
556    Ok(vec![PcCandidate { pc: parse_pc(input, radix, original)?, base }])
557}
558
559fn parse_pc(input: &str, radix: u32, original: &str) -> Result<usize, String> {
560    if input.is_empty() {
561        return Err(format!("Invalid PC `{original}`; use 0x2a, 2a, or d:42"));
562    }
563    usize::from_str_radix(input, radix)
564        .map_err(|_| format!("Invalid PC `{original}`; use 0x2a, 2a, or d:42"))
565}
566
567fn find_pc_target(
568    arena: &[DebugNode],
569    current_node_index: usize,
570    current_step: usize,
571    pc: usize,
572) -> Option<PcTarget> {
573    let current_node = arena.get(current_node_index)?;
574
575    if let Some(step_index) = find_pc_in_current_node(&current_node.steps, current_step, pc) {
576        return Some(PcTarget {
577            node_index: current_node_index,
578            step_index,
579            scope: PcTargetScope::CurrentNode,
580        });
581    }
582
583    for (node_index, node) in arena.iter().enumerate().skip(current_node_index + 1) {
584        if same_code_context(current_node, node)
585            && let Some(step_index) = node.steps.iter().position(|step| step.pc == pc)
586        {
587            return Some(PcTarget {
588                node_index,
589                step_index,
590                scope: PcTargetScope::SameCodeContext,
591            });
592        }
593    }
594
595    for (node_index, node) in arena.iter().enumerate().take(current_node_index).rev() {
596        if same_code_context(current_node, node)
597            && let Some(step_index) = node.steps.iter().rposition(|step| step.pc == pc)
598        {
599            return Some(PcTarget {
600                node_index,
601                step_index,
602                scope: PcTargetScope::SameCodeContext,
603            });
604        }
605    }
606
607    None
608}
609
610fn find_pc_in_current_node(
611    steps: &[CallTraceStep],
612    current_step: usize,
613    pc: usize,
614) -> Option<usize> {
615    if steps.get(current_step).is_some_and(|step| step.pc == pc) {
616        return Some(current_step);
617    }
618
619    steps
620        .iter()
621        .enumerate()
622        .skip(current_step.saturating_add(1))
623        .find_map(|(i, step)| (step.pc == pc).then_some(i))
624        .or_else(|| {
625            steps[..current_step.min(steps.len())]
626                .iter()
627                .enumerate()
628                .rev()
629                .find_map(|(i, step)| (step.pc == pc).then_some(i))
630        })
631}
632
633fn same_code_context(a: &DebugNode, b: &DebugNode) -> bool {
634    a.address == b.address && a.kind.is_any_create() == b.kind.is_any_create()
635}
636
637fn pc_exists_outside_code_context(arena: &[DebugNode], current: &DebugNode, pc: usize) -> bool {
638    arena.iter().any(|node| {
639        !same_code_context(current, node) && node.steps.iter().any(|step| step.pc == pc)
640    })
641}
642
643fn pretty_opcode(step: &CallTraceStep) -> String {
644    if let Some(immediate) = step.immediate_bytes.as_ref().filter(|b| !b.is_empty()) {
645        format!("{}(0x{})", step.op, hex::encode(immediate))
646    } else {
647        step.op.to_string()
648    }
649}
650
651fn is_jump(step: &CallTraceStep, prev: &CallTraceStep) -> bool {
652    if !matches!(prev.op, OpCode::JUMP | OpCode::JUMPI) {
653        return false;
654    }
655
656    let immediate_len = prev.immediate_bytes.as_ref().map_or(0, |b| b.len());
657
658    step.pc != prev.pc + 1 + immediate_len
659}
660
661#[cfg(test)]
662mod tests {
663    use super::*;
664    use alloy_primitives::Bytes;
665    use foundry_evm_core::Breakpoints;
666    use foundry_evm_traces::debug::ContractSources;
667    use revm::interpreter::InstructionResult;
668
669    fn step(pc: usize) -> CallTraceStep {
670        CallTraceStep {
671            pc,
672            op: OpCode::STOP,
673            stack: None,
674            push_stack: None,
675            memory: None,
676            returndata: Bytes::new(),
677            gas_remaining: 0,
678            gas_refund_counter: 0,
679            gas_used: 0,
680            gas_cost: 0,
681            storage_change: None,
682            status: Some(InstructionResult::Stop),
683            immediate_bytes: None,
684            decoded: None,
685        }
686    }
687
688    fn node(address: Address, kind: CallKind, pcs: &[usize]) -> DebugNode {
689        DebugNode::new(address, kind, pcs.iter().copied().map(step).collect(), Bytes::new(), 0)
690    }
691
692    fn context_with_arena(arena: Vec<DebugNode>) -> DebuggerContext {
693        DebuggerContext {
694            debug_arena: arena,
695            stats: None,
696            identified_contracts: Default::default(),
697            contracts_sources: ContractSources::default(),
698            breakpoints: Breakpoints::default(),
699        }
700    }
701
702    fn key(code: KeyCode) -> KeyEvent {
703        KeyEvent::new(code, KeyModifiers::empty())
704    }
705
706    #[test]
707    fn parses_prefixed_hex_pc() {
708        assert_eq!(
709            parse_pc_candidates("0x2a").unwrap(),
710            vec![PcCandidate { pc: 42, base: PcBase::Hex }]
711        );
712        assert_eq!(
713            parse_pc_candidates("0X2A").unwrap(),
714            vec![PcCandidate { pc: 42, base: PcBase::Hex }]
715        );
716    }
717
718    #[test]
719    fn parses_bare_hex_pc_with_letters() {
720        assert_eq!(
721            parse_pc_candidates("2a").unwrap(),
722            vec![PcCandidate { pc: 42, base: PcBase::Hex }]
723        );
724    }
725
726    #[test]
727    fn parses_explicit_decimal_pc() {
728        assert_eq!(
729            parse_pc_candidates("d:42").unwrap(),
730            vec![PcCandidate { pc: 42, base: PcBase::Decimal }]
731        );
732        assert_eq!(
733            parse_pc_candidates("dec:42").unwrap(),
734            vec![PcCandidate { pc: 42, base: PcBase::Decimal }]
735        );
736    }
737
738    #[test]
739    fn parses_bare_digits_as_decimal_and_hex_candidates() {
740        assert_eq!(
741            parse_pc_candidates("10").unwrap(),
742            vec![
743                PcCandidate { pc: 10, base: PcBase::Decimal },
744                PcCandidate { pc: 16, base: PcBase::Hex },
745            ]
746        );
747        assert_eq!(
748            parse_pc_candidates("9").unwrap(),
749            vec![PcCandidate { pc: 9, base: PcBase::Decimal }]
750        );
751    }
752
753    #[test]
754    fn rejects_invalid_pc_input() {
755        assert!(parse_pc_candidates("").is_err());
756        assert!(parse_pc_candidates("0x").is_err());
757        assert!(parse_pc_candidates("xyz").is_err());
758        assert!(parse_pc_candidates("184467440737095516160").is_err());
759    }
760
761    #[test]
762    fn finds_pc_in_current_node() {
763        let address = Address::repeat_byte(1);
764        let arena = vec![node(address, CallKind::Call, &[1, 2, 3])];
765
766        assert_eq!(
767            find_pc_target(&arena, 0, 0, 3),
768            Some(PcTarget { node_index: 0, step_index: 2, scope: PcTargetScope::CurrentNode })
769        );
770    }
771
772    #[test]
773    fn repeated_pc_stays_current_then_prefers_next_then_previous() {
774        let address = Address::repeat_byte(1);
775        let arena = vec![node(address, CallKind::Call, &[1, 2, 3, 2])];
776
777        assert_eq!(find_pc_target(&arena, 0, 1, 2).unwrap().step_index, 1);
778        assert_eq!(find_pc_target(&arena, 0, 0, 2).unwrap().step_index, 1);
779        assert_eq!(find_pc_target(&arena, 0, 3, 2).unwrap().step_index, 3);
780        assert_eq!(find_pc_target(&arena, 0, 2, 2).unwrap().step_index, 3);
781    }
782
783    #[test]
784    fn searches_later_then_earlier_same_code_context() {
785        let address = Address::repeat_byte(1);
786        let arena = vec![
787            node(address, CallKind::Call, &[1]),
788            node(address, CallKind::Call, &[2]),
789            node(address, CallKind::Call, &[3]),
790        ];
791
792        assert_eq!(find_pc_target(&arena, 1, 0, 3).unwrap().node_index, 2);
793        assert_eq!(find_pc_target(&arena, 1, 0, 1).unwrap().node_index, 0);
794    }
795
796    #[test]
797    fn does_not_search_different_address_or_creation_context() {
798        let address = Address::repeat_byte(1);
799        let other = Address::repeat_byte(2);
800        let arena = vec![
801            node(address, CallKind::Call, &[1]),
802            node(other, CallKind::Call, &[2]),
803            node(address, CallKind::Create, &[3]),
804        ];
805
806        assert!(find_pc_target(&arena, 0, 0, 2).is_none());
807        assert!(find_pc_target(&arena, 0, 0, 3).is_none());
808        assert!(pc_exists_outside_code_context(&arena, &arena[0], 2));
809        assert!(pc_exists_outside_code_context(&arena, &arena[0], 3));
810    }
811
812    #[test]
813    fn goto_resolves_unambiguous_bare_digits_and_reports_ambiguity() {
814        let address = Address::repeat_byte(1);
815        let mut context = context_with_arena(vec![node(address, CallKind::Call, &[10, 16, 42])]);
816        let mut tui = TUIContext::new(&mut context);
817        tui.init();
818
819        tui.goto_pc_from_input("2a");
820        assert_eq!(tui.current_step, 2);
821        assert_eq!(tui.status.as_ref().unwrap().kind, StatusKind::Info);
822
823        tui.current_step = 0;
824        tui.goto_pc_from_input("10");
825        assert_eq!(tui.current_step, 0);
826        assert!(tui.status.as_ref().unwrap().text.contains("Ambiguous PC"));
827
828        tui.goto_pc_from_input("d:10");
829        assert_eq!(tui.current_step, 0);
830        assert_eq!(tui.status.as_ref().unwrap().kind, StatusKind::Info);
831    }
832
833    #[test]
834    fn goto_reports_pc_in_other_contract_without_moving() {
835        let address = Address::repeat_byte(1);
836        let other = Address::repeat_byte(2);
837        let mut context = context_with_arena(vec![
838            node(address, CallKind::Call, &[1]),
839            node(other, CallKind::Call, &[42]),
840        ]);
841        let mut tui = TUIContext::new(&mut context);
842        tui.init();
843
844        tui.goto_pc_from_input("2a");
845        assert_eq!(tui.draw_memory.inner_call_index, 0);
846        assert_eq!(tui.current_step, 0);
847        let status = tui.status.as_ref().unwrap();
848        assert_eq!(status.kind, StatusKind::Error);
849        assert!(status.text.contains("exists in another contract"));
850    }
851
852    #[test]
853    fn goto_reports_ambiguous_input_in_other_contract_without_choosing_first_candidate() {
854        let address = Address::repeat_byte(1);
855        let other = Address::repeat_byte(2);
856        let mut context = context_with_arena(vec![
857            node(address, CallKind::Call, &[1]),
858            node(other, CallKind::Call, &[16]),
859        ]);
860        let mut tui = TUIContext::new(&mut context);
861        tui.init();
862
863        tui.goto_pc_from_input("10");
864        let status = tui.status.as_ref().unwrap();
865        assert_eq!(status.kind, StatusKind::Error);
866        assert!(status.text.starts_with("PC `10` not found"));
867        assert!(status.text.contains("exists in another contract"));
868    }
869
870    #[test]
871    fn pc_input_mode_handles_keys_and_blocks_normal_commands() {
872        let address = Address::repeat_byte(1);
873        let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1, 42])]);
874        let mut tui = TUIContext::new(&mut context);
875        tui.init();
876
877        assert!(matches!(tui.handle_key_event(key(KeyCode::Char('p'))), ControlFlow::Continue(())));
878        assert_eq!(tui.pc_input.as_deref(), Some(""));
879
880        let _ = tui.handle_key_event(key(KeyCode::Char('q')));
881        assert_eq!(tui.pc_input.as_deref(), Some(""));
882        assert_eq!(tui.current_step, 0);
883
884        let _ = tui.handle_key_event(key(KeyCode::Char('2')));
885        let _ = tui.handle_key_event(key(KeyCode::Char('a')));
886        assert_eq!(tui.pc_input.as_deref(), Some("2a"));
887
888        let _ = tui.handle_key_event(key(KeyCode::Backspace));
889        assert_eq!(tui.pc_input.as_deref(), Some("2"));
890        let _ = tui.handle_key_event(key(KeyCode::Char('a')));
891        let _ = tui.handle_key_event(key(KeyCode::Enter));
892
893        assert_eq!(tui.pc_input, None);
894        assert_eq!(tui.current_step, 1);
895        assert_eq!(tui.status.as_ref().unwrap().kind, StatusKind::Info);
896    }
897
898    #[test]
899    fn pc_input_escape_cancels_without_moving() {
900        let address = Address::repeat_byte(1);
901        let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1, 42])]);
902        let mut tui = TUIContext::new(&mut context);
903        tui.init();
904
905        let _ = tui.handle_key_event(key(KeyCode::Char('p')));
906        let _ = tui.handle_key_event(key(KeyCode::Char('2')));
907        let _ = tui.handle_key_event(key(KeyCode::Esc));
908
909        assert_eq!(tui.pc_input, None);
910        assert_eq!(tui.current_step, 0);
911        assert_eq!(tui.status, None);
912    }
913}