ya_advent_lib/
vm_debugger.rs

1use crossterm::{
2    event::{self, Event, KeyCode},
3    execute,
4    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
5};
6use itertools::Itertools;
7use lazy_static::lazy_static;
8use ratatui::{
9    backend::{Backend, CrosstermBackend},
10    layout::{Constraint, Direction, Layout, Rect},
11    style::{Color, Modifier, Style},
12    text::{Line, Span},
13    widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
14    Frame, Terminal,
15};
16use std::collections::{HashMap, HashSet};
17use std::error::Error;
18use std::fmt;
19use std::hash::Hash;
20use std::io;
21use std::iter;
22use std::time::{Duration, Instant};
23use tui_input::backend::crossterm::EventHandler;
24use tui_input::Input;
25
26use crate::vm_display::{Formatter, InstructionDisplay, Token};
27use crate::vm_shell::{RunResult, VMShell, CPU};
28
29type Result<T> = std::result::Result<T, Box<dyn Error>>;
30
31const OPCODE_COLOR: Color = Color::Red;
32const REG_COLOR: Color = Color::Blue;
33const INT_COLOR: Color = Color::Cyan;
34const ADDR_COLOR: Color = Color::Green;
35const BP_COLOR: Color = Color::Blue;
36
37lazy_static! {
38    static ref STYLES: HashMap<char, Style> = HashMap::from_iter([
39        ('o', Style::default().fg(OPCODE_COLOR)),
40        ('r', Style::default().fg(REG_COLOR)),
41        ('i', Style::default().fg(INT_COLOR)),
42        ('a', Style::default().fg(ADDR_COLOR)),
43        ('b', Style::default().fg(BP_COLOR)),
44    ]);
45}
46
47#[derive(Clone, Copy, Eq, PartialEq)]
48enum RunState {
49    Pause,
50    Walk,
51    Run,
52    Halt,
53    Error,
54}
55
56enum Focus {
57    Normal,
58    BreakpointInput,
59}
60
61pub struct Debugger<'a, RegKey, RegVal: TryFrom<usize>, Instr> {
62    shell: &'a mut VMShell<RegKey, RegVal, Instr>,
63    cpu: &'a dyn CPU<RegKey, RegVal, Instr>,
64    run_state: RunState,
65    walk_delay: Duration,
66    breakpoint: bool,
67    pc_breakpoints: HashSet<usize>,
68    hex_mode: bool,
69    focus: Focus,
70    pc_input: Input,
71}
72
73impl<
74        'a,
75        RegKey: Clone + Hash + Eq + Ord + PartialOrd + Into<String>,
76        RegVal: Copy + fmt::Display + fmt::LowerHex + fmt::UpperHex + TryFrom<usize> + Copy + Clone,
77        Instr: Clone + InstructionDisplay<RegVal>,
78    > Debugger<'a, RegKey, RegVal, Instr>
79{
80    pub fn run(
81        shell: &'a mut VMShell<RegKey, RegVal, Instr>,
82        cpu: &'a dyn CPU<RegKey, RegVal, Instr>,
83    ) -> Result<()> {
84        let terminal = Self::init_terminal()?;
85        let original_hook = std::panic::take_hook();
86        std::panic::set_hook(Box::new(move |panic| {
87            Self::reset_terminal().unwrap();
88            original_hook(panic);
89        }));
90
91        let mut inst = Self {
92            shell,
93            cpu,
94            run_state: RunState::Pause,
95            walk_delay: Duration::from_millis(10),
96            breakpoint: false,
97            hex_mode: false,
98            pc_breakpoints: HashSet::new(),
99            focus: Focus::Normal,
100            pc_input: Input::default(),
101        };
102        inst.run_impl(terminal)?;
103
104        Ok(())
105    }
106
107    fn init_terminal() -> Result<Terminal<CrosstermBackend<io::Stdout>>> {
108        enable_raw_mode()?;
109        execute!(io::stdout(), EnterAlternateScreen)?;
110        let backend = CrosstermBackend::new(io::stdout());
111        let mut terminal = Terminal::new(backend)?;
112        terminal.hide_cursor()?;
113        Ok(terminal)
114    }
115
116    fn reset_terminal() -> Result<()> {
117        disable_raw_mode()?;
118        execute!(io::stdout(), LeaveAlternateScreen)?;
119        Ok(())
120    }
121
122    fn is_halted(&self) -> bool {
123        matches!(self.run_state, RunState::Halt | RunState::Error)
124    }
125
126    fn run_impl<B: Backend>(&mut self, mut terminal: Terminal<B>) -> io::Result<()> {
127        let mut last_tick = Instant::now();
128        loop {
129            if self.run_state != RunState::Run {
130                terminal.draw(|f| self.ui::<B>(f))?;
131            }
132            self.breakpoint = false;
133
134            let timeout = if self.run_state == RunState::Walk {
135                self.walk_delay
136                    .checked_sub(last_tick.elapsed())
137                    .unwrap_or_else(|| Duration::from_secs(0))
138            } else {
139                Duration::from_secs(0)
140            };
141
142            let mut do_step = false;
143
144            if matches!(self.focus, Focus::BreakpointInput) {
145                let e = event::read()?;
146                if let Event::Key(key) = e {
147                    match key.code {
148                        KeyCode::Enter => {
149                            if let Ok(n) = self.pc_input.value().parse::<usize>() {
150                                if n < self.shell.vm.program.len() {
151                                    if self.pc_breakpoints.contains(&n) {
152                                        self.pc_breakpoints.remove(&n);
153                                    } else {
154                                        self.pc_breakpoints.insert(n);
155                                    }
156                                }
157                            }
158                            self.pc_input.reset();
159                            self.focus = Focus::Normal;
160                        }
161                        KeyCode::Esc => {
162                            self.pc_input.reset();
163                            self.focus = Focus::Normal;
164                        }
165                        _ => {
166                            self.pc_input.handle_event(&e);
167                        }
168                    }
169                }
170            } else if self.run_state != RunState::Walk && self.run_state != RunState::Run
171                || event::poll(timeout)?
172            {
173                if let Event::Key(key) = event::read()? {
174                    match key.code {
175                        KeyCode::Char('s') => {
176                            self.cmd_pause();
177                            do_step = true;
178                        }
179                        KeyCode::Char('r') => {
180                            self.cmd_run();
181                            terminal.draw(|f| self.ui::<B>(f))?;
182                        }
183                        KeyCode::Char('w') => self.cmd_walk(),
184                        KeyCode::Char('p') => self.cmd_pause(),
185                        KeyCode::Char('h') => {
186                            self.hex_mode = !self.hex_mode;
187                            if self.run_state == RunState::Run {
188                                terminal.draw(|f| self.ui::<B>(f))?;
189                            }
190                        }
191                        KeyCode::Char('b') => {
192                            self.cmd_pause();
193                            self.focus = Focus::BreakpointInput;
194                        }
195                        KeyCode::Char('q') => break,
196                        _ => {}
197                    }
198                }
199            }
200
201            if match self.run_state {
202                RunState::Run => true,
203                RunState::Pause => do_step,
204                RunState::Walk => {
205                    if last_tick.elapsed() >= self.walk_delay {
206                        last_tick = Instant::now();
207                        true
208                    } else {
209                        false
210                    }
211                }
212                _ => false,
213            } {
214                let rr = self.shell.step(self.cpu);
215                match rr {
216                    RunResult::Break => {
217                        self.run_state = RunState::Pause;
218                        self.breakpoint = true;
219                    }
220                    RunResult::Halt => {
221                        self.run_state = RunState::Halt;
222                    }
223                    RunResult::Err => {
224                        self.run_state = RunState::Error;
225                    }
226                    _ => {
227                        if self.pc_breakpoints.contains(&self.shell.vm.pc) {
228                            self.run_state = RunState::Pause;
229                            self.breakpoint = true;
230                        }
231                    }
232                }
233            }
234        }
235        let _ = Self::reset_terminal();
236        Ok(())
237    }
238
239    fn cmd_run(&mut self) {
240        if !self.is_halted() {
241            self.run_state = RunState::Run;
242        }
243    }
244    fn cmd_walk(&mut self) {
245        if !self.is_halted() {
246            self.run_state = RunState::Walk;
247        }
248    }
249    fn cmd_pause(&mut self) {
250        if !self.is_halted() {
251            self.run_state = RunState::Pause;
252        }
253    }
254
255    fn ui<B: Backend>(&mut self, f: &mut Frame) {
256        let (statuspanel, sp_height) = self.render_statuspanel();
257        let root = Layout::default()
258            .direction(Direction::Horizontal)
259            .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
260            .split(f.area());
261        let rightside = Layout::default()
262            .direction(Direction::Vertical)
263            .constraints([Constraint::Min(10), Constraint::Length(sp_height)].as_ref())
264            .split(root[1]);
265        self.render_code::<B>(f, root[0]);
266        self.render_registers::<B>(f, rightside[0]);
267        f.render_widget(statuspanel, rightside[1]);
268        if matches!(self.focus, Focus::BreakpointInput) {
269            let rect = Rect::new(
270                rightside[1].x + 20,
271                rightside[1].y + 3,
272                rightside[1].width - 23,
273                3,
274            );
275            self.render_pc_input::<B>(f, rect);
276        }
277    }
278
279    fn render_statuspanel(&self) -> (List, u16) {
280        let items: Vec<ListItem> = vec![
281            ListItem::new("[p] Pause           [q] Quit"),
282            ListItem::new("[s] Step"),
283            ListItem::new("[w] Walk            [b] Breakpoint"),
284            ListItem::new("[r] Run"),
285            ListItem::new("[h] Toggle decimal/hex"),
286        ];
287        let title = match (self.breakpoint, self.run_state) {
288            (true, _) => "BREAKPOINT",
289            (false, RunState::Pause) => "PAUSED",
290            (false, RunState::Walk) => "WALKING",
291            (false, RunState::Run) => "RUNNING",
292            (false, RunState::Halt) => "HALTED",
293            (false, RunState::Error) => "ERROR",
294        };
295        let height = items.len() as u16 + 2;
296
297        (
298            List::new(items).block(Block::default().title(title).borders(Borders::ALL)),
299            height,
300        )
301    }
302
303    fn render_pc_input<B: Backend>(&self, f: &mut Frame, rect: Rect) {
304        let scroll = self.pc_input.visual_scroll((rect.width - 2) as usize);
305        let input = Paragraph::new(self.pc_input.value())
306            .scroll((0, scroll as u16))
307            .block(
308                Block::default()
309                    .borders(Borders::ALL)
310                    .title("Enter address"),
311            );
312        f.render_widget(input, rect);
313        f.set_cursor_position((
314            rect.x + ((self.pc_input.visual_cursor()).max(scroll) - scroll) as u16 + 1,
315            rect.y + 1,
316        ));
317    }
318
319    fn fmtint(&self, val: &RegVal) -> String {
320        if self.hex_mode {
321            format!("{val:#x}")
322        } else {
323            format!("{val}")
324        }
325    }
326
327    fn token_list(&self, iter: &mut dyn Iterator<Item = Vec<Token<RegVal>>>) -> Vec<Line> {
328        let rows: Vec<Vec<Span>> = iter
329            .map(|row| {
330                row.iter()
331                    .map(|t| match t {
332                        Token::Opcode(s) => Span::styled(s.clone(), STYLES[&'o']),
333                        Token::Register(s) => Span::styled(s.clone(), STYLES[&'r']),
334                        Token::Integer(i) => Span::styled(self.fmtint(i), STYLES[&'i']),
335                        Token::Address(i) => Span::styled(self.fmtint(i), STYLES[&'a']),
336                    })
337                    .collect()
338            })
339            .collect();
340
341        let maxcols = rows.iter().map(|r| r.len()).max().unwrap();
342        let mut colwidths: Vec<usize> = vec![0; maxcols];
343        rows.iter().for_each(|row| {
344            row.iter()
345                .enumerate()
346                .for_each(|(idx, t)| colwidths[idx] = colwidths[idx].max(t.width()));
347        });
348
349        rows.iter()
350            .map(|row| {
351                Line::from_iter(row.iter().enumerate().flat_map(|(idx, t)| {
352                    let w = t.width();
353                    let pad = Span::raw(" ".repeat(colwidths[idx] - w));
354                    let mut spans = if idx == 0 {
355                        vec![pad, t.clone()]
356                    } else {
357                        vec![t.clone(), pad]
358                    };
359                    if idx < row.len() - 1 {
360                        spans.push(Span::raw(" "));
361                    }
362                    spans
363                }))
364            })
365            .collect()
366    }
367
368    fn render_code<B: Backend>(&self, f: &mut Frame, rect: Rect) {
369        let mut itr = self
370            .shell
371            .vm
372            .program
373            .iter()
374            .enumerate()
375            .map(|(idx, instr)| {
376                let mut fmt = Formatter::new();
377                instr.fmt(&mut fmt);
378                iter::once(&Token::Address(idx.try_into().ok().unwrap()))
379                    .chain(fmt.get_tokens())
380                    .cloned()
381                    .collect()
382            });
383        let rows: Vec<ListItem> = self
384            .token_list(&mut itr)
385            .into_iter()
386            .enumerate()
387            .map(|(idx, mut line)| {
388                let bp: String = if self.pc_breakpoints.contains(&idx) {
389                    "\u{25c8} ".into()
390                } else {
391                    "  ".into()
392                };
393                line.spans.insert(0, Span::styled(bp, STYLES[&'b']));
394                ListItem::new(line)
395            })
396            .collect();
397
398        let list = List::new(rows)
399            .block(Block::default().borders(Borders::ALL).title("Program"))
400            .highlight_style(Style::default().add_modifier(Modifier::REVERSED));
401        let mut liststate = ListState::default();
402        if self.shell.vm.pc < self.shell.vm.program.len() {
403            liststate.select(Some(self.shell.vm.pc));
404        }
405        f.render_stateful_widget(list, rect, &mut liststate);
406    }
407
408    fn render_registers<B: Backend>(&self, f: &mut Frame, rect: Rect) {
409        let mut itr = self
410            .shell
411            .vm
412            .registers
413            .iter()
414            .sorted_by(|a, b| Ord::cmp(&a.0, &b.0))
415            .map(|(k, v)| vec![Token::Register(k.clone().into()), Token::Integer(*v)]);
416        let rows: Vec<ListItem> = self
417            .token_list(&mut itr)
418            .into_iter()
419            .map(ListItem::new)
420            .collect();
421
422        let list = List::new(rows).block(Block::default().borders(Borders::ALL).title("Registers"));
423
424        f.render_widget(list, rect);
425    }
426}