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}