Krolyxon
Commit 86cbab6

Branches

Tags

Refs →

bench51cratesbench51-coresrcsession.rs

417 lines · 12.6 KB · Rust

1//! A debugging session: the simulator, the program loaded into it,
2//! breakpoints and run state. Front ends own the source text and decide
3//! when to build; everything else about running a program lives here.
4
5use std::collections::{HashMap, HashSet};
6use std::path::Path;
7
8use crate::asm::{self, AsmOutput, Diagnostic};
9use crate::sim::{Sim, Stop};
10
11/// Result of the last successful assembly, which the simulator runs.
12pub struct Build {
13 pub source: Vec<String>,
14 pub out: AsmOutput,
15 pub addr_to_line: HashMap<u16, usize>,
16}
17
18impl Build {
19 /// First address emitted by `line`, if any.
20 pub fn line_addr(&self, line: usize) -> Option<u16> {
21 self.out
22 .lines
23 .get(line)
24 .filter(|l| !l.bytes.is_empty())
25 .and_then(|l| l.addr)
26 }
27}
28
29/// Instructions executed per tick (one tick ≈ 33 ms) for each speed setting.
30pub const SPEEDS: [u32; 8] = [1, 5, 25, 100, 500, 2_000, 10_000, 40_000];
31
32/// Instruction budget for step over / step out / run to cursor.
33const STEP_BUDGET: u32 = 5_000_000;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36pub enum MemSpace {
37 Iram,
38 Sfr,
39 Xram,
40 Code,
41}
42
43impl MemSpace {
44 pub const ALL: [MemSpace; 4] = [
45 MemSpace::Iram,
46 MemSpace::Sfr,
47 MemSpace::Xram,
48 MemSpace::Code,
49 ];
50
51 pub fn name(self) -> &'static str {
52 match self {
53 MemSpace::Iram => "IRAM",
54 MemSpace::Sfr => "SFR",
55 MemSpace::Xram => "XRAM",
56 MemSpace::Code => "CODE",
57 }
58 }
59}
60
61pub struct Session {
62 pub sim: Sim,
63 /// Last assembler result, even if it had errors (for diagnostics).
64 pub last_asm: Option<(String, AsmOutput)>,
65 /// Last error-free build, loaded into the simulator.
66 pub build: Option<Build>,
67 /// Breakpoints by source line.
68 pub breakpoints: HashSet<usize>,
69 pub running: bool,
70 /// The next run chunk starts a fresh run and may leave a breakpoint.
71 resume: bool,
72 /// Index into `SPEEDS`.
73 pub speed: usize,
74 pub clock_mhz: f64,
75 /// Memory before the last step or run chunk, to highlight changes.
76 pub prev_iram: [u8; 256],
77 pub prev_sfr: [u8; 128],
78 /// Outcome of the last command, for the status line.
79 pub status: String,
80}
81
82impl Session {
83 pub fn new(clock_mhz: f64) -> Self {
84 Self {
85 sim: Sim::new(),
86 last_asm: None,
87 build: None,
88 breakpoints: HashSet::new(),
89 running: false,
90 resume: false,
91 speed: 5,
92 clock_mhz,
93 prev_iram: [0; 256],
94 prev_sfr: [0; 128],
95 status: String::new(),
96 }
97 }
98
99 /// Forgets the program and breakpoints, for a newly loaded source.
100 pub fn clear(&mut self) {
101 self.breakpoints.clear();
102 self.build = None;
103 self.last_asm = None;
104 self.running = false;
105 }
106
107 /// Whether `lines` differ from the source the simulator is running.
108 pub fn is_stale<'a>(&self, lines: impl IntoIterator<Item = &'a str>) -> bool {
109 self.build
110 .as_ref()
111 .is_none_or(|b| !b.source.iter().map(String::as_str).eq(lines))
112 }
113
114 /// Diagnostics of the last assembly, if it was of exactly `source`.
115 pub fn diagnostics_for(&self, source: &str) -> &[Diagnostic] {
116 match &self.last_asm {
117 Some((src, out)) if src == source => &out.diagnostics,
118 _ => &[],
119 }
120 }
121
122 // ---- Build ----
123
124 /// Assembles `source`. On success the program is loaded into a freshly
125 /// reset simulator and, if `path` (the saved source file) is given,
126 /// `.lst` and `.hex` files are written next to it.
127 pub fn assemble(&mut self, source: &str, path: Option<&Path>) -> bool {
128 let out = asm::assemble(source);
129 let ok = out.ok();
130 if ok {
131 self.sim.load_code(out.bytes());
132 self.sim.reset();
133 self.running = false;
134 self.status = format!("assembled: {} bytes, 0 errors", out.code_size());
135 if let Some(path) = path {
136 self.write_outputs(&out, source, path);
137 }
138 self.build = Some(Build {
139 source: source.split('\n').map(String::from).collect(),
140 addr_to_line: out.addr_to_line(),
141 out: out.clone(),
142 });
143 self.snapshot();
144 } else {
145 let first = &out.diagnostics[0];
146 self.status = format!(
147 "{} error(s); line {}: {}",
148 out.diagnostics.len(),
149 first.line + 1,
150 first.msg
151 );
152 }
153 self.last_asm = Some((source.to_string(), out));
154 ok
155 }
156
157 /// Makes sure a build of `source` is loaded; returns false on errors.
158 pub fn ensure_built(&mut self, source: &str, path: Option<&Path>) -> bool {
159 !self.is_stale(source.split('\n')) || self.assemble(source, path)
160 }
161
162 fn write_outputs(&mut self, out: &AsmOutput, source: &str, path: &Path) {
163 let res = std::fs::write(path.with_extension("lst"), out.listing(source))
164 .and_then(|_| std::fs::write(path.with_extension("hex"), out.intel_hex()));
165 if let Err(e) = res {
166 self.status = format!("assembled, but writing .lst/.hex failed: {e}");
167 }
168 }
169
170 // ---- Breakpoints ----
171
172 pub fn toggle_breakpoint(&mut self, line: usize) {
173 if !self.breakpoints.remove(&line) {
174 self.breakpoints.insert(line);
175 }
176 }
177
178 pub fn bp_addrs(&self) -> HashSet<u16> {
179 let Some(b) = &self.build else {
180 return HashSet::new();
181 };
182 self.breakpoints
183 .iter()
184 .filter_map(|&l| b.line_addr(l))
185 .collect()
186 }
187
188 // ---- Execution ----
189
190 pub fn snapshot(&mut self) {
191 self.prev_iram = self.sim.iram;
192 self.prev_sfr = self.sim.sfr;
193 }
194
195 /// Source line of the instruction at the PC.
196 pub fn pc_line(&self) -> Option<usize> {
197 self.build.as_ref()?.addr_to_line.get(&self.sim.pc).copied()
198 }
199
200 /// Stops running and describes why in `status`.
201 pub fn report(&mut self, stop: Stop) {
202 self.running = false;
203 self.status = match stop {
204 Stop::Breakpoint(pc) => format!("breakpoint at {pc:04X}h"),
205 Stop::ReservedOpcode { pc, opcode } => {
206 format!("reserved opcode {opcode:02X}h at {pc:04X}h — stopped")
207 }
208 Stop::PowerDown => "CPU in power-down mode (PCON.PD) — reset to continue".into(),
209 Stop::Target | Stop::Budget => format!("stopped at {:04X}h", self.sim.pc),
210 };
211 }
212
213 fn step_with(&mut self, f: impl FnOnce(&mut Sim, &HashSet<u16>) -> Stop) {
214 self.running = false;
215 self.snapshot();
216 let bps = self.bp_addrs();
217 match f(&mut self.sim, &bps) {
218 Stop::Target => self.status = format!("PC = {:04X}h", self.sim.pc),
219 s => self.report(s),
220 }
221 }
222
223 pub fn step_into(&mut self) {
224 self.step_with(|s, _| match s.step() {
225 Ok(()) => Stop::Target,
226 Err(e) => e,
227 });
228 }
229
230 pub fn step_over(&mut self) {
231 self.step_with(|s, b| s.step_over(STEP_BUDGET, b));
232 }
233
234 pub fn step_out(&mut self) {
235 self.step_with(|s, b| s.step_out(STEP_BUDGET, b));
236 }
237
238 /// Runs until the first instruction of `line`, or a breakpoint.
239 /// Returns false if the line holds no code.
240 pub fn run_to_line(&mut self, line: usize) -> bool {
241 let Some(target) = self.build.as_ref().and_then(|b| b.line_addr(line)) else {
242 self.status = "no code on that line".into();
243 return false;
244 };
245 self.step_with(|s, bps| {
246 let mut bps = bps.clone();
247 bps.insert(target);
248 match s.run(STEP_BUDGET, &bps, true) {
249 Stop::Breakpoint(pc) if pc == target => Stop::Target,
250 other => other,
251 }
252 });
253 true
254 }
255
256 /// Starts free running; `tick` then advances the program. Needs a build.
257 pub fn start(&mut self) {
258 if self.build.is_none() {
259 return;
260 }
261 self.running = true;
262 self.resume = true;
263 self.status = "running…".into();
264 }
265
266 pub fn pause(&mut self) {
267 self.running = false;
268 self.status = format!("paused at {:04X}h", self.sim.pc);
269 }
270
271 /// Runs one chunk of `SPEEDS[speed]` instructions if running. Front ends
272 /// call this every ~33 ms.
273 pub fn tick(&mut self) {
274 if !self.running {
275 return;
276 }
277 self.snapshot();
278 let bps = self.bp_addrs();
279 let resume = std::mem::take(&mut self.resume);
280 match self.sim.run(SPEEDS[self.speed], &bps, resume) {
281 Stop::Budget => {}
282 s => self.report(s),
283 }
284 }
285
286 pub fn reset(&mut self) {
287 self.sim.reset();
288 self.running = false;
289 self.snapshot();
290 self.status = "reset".into();
291 }
292
293 /// Writes `value` at `addr` in `space`. Returns false if out of range.
294 pub fn poke(&mut self, space: MemSpace, addr: u32, value: u8) -> bool {
295 let ok = match space {
296 MemSpace::Iram if addr <= 0xFF => {
297 self.sim.iram[addr as usize] = value;
298 true
299 }
300 MemSpace::Sfr if (0x80..=0xFF).contains(&addr) => {
301 self.sim.set_sfr(addr as u8, value);
302 true
303 }
304 MemSpace::Xram if addr <= 0xFFFF => {
305 self.sim.xram[addr as usize] = value;
306 true
307 }
308 MemSpace::Code if addr <= 0xFFFF => {
309 self.sim.code[addr as usize] = value;
310 true
311 }
312 _ => false,
313 };
314 self.status = if ok {
315 format!("wrote {value:02X}h at {addr:X}h")
316 } else {
317 "address out of range".into()
318 };
319 ok
320 }
321
322 /// Simulated time since reset at `clock_mhz` (12 clocks per machine cycle).
323 pub fn elapsed(&self) -> String {
324 let micros = self.sim.cycles as f64 * 12.0 / self.clock_mhz;
325 if micros >= 1_000_000.0 {
326 format!("{:.3} s", micros / 1e6)
327 } else if micros >= 1000.0 {
328 format!("{:.3} ms", micros / 1e3)
329 } else {
330 format!("{micros:.2} µs")
331 }
332 }
333}
334
335/// Name of the interrupt source served at vector `v`.
336pub fn vector_name(v: u16) -> &'static str {
337 match v {
338 0x03 => "INT0",
339 0x0B => "T0",
340 0x13 => "INT1",
341 0x1B => "T1",
342 0x23 => "UART",
343 _ => "?",
344 }
345}
346
347#[cfg(test)]
348mod tests {
349 use super::{MemSpace, Session};
350
351 const PROG: &str = "\
352 ORG 0
353 LJMP main
354 ORG 30h
355main: MOV R0,#0
356loop: ACALL bump
357 SJMP loop
358bump: INC R0
359 NOP
360 RET
361 END";
362
363 fn session() -> Session {
364 let mut s = Session::new(12.0);
365 assert!(s.assemble(PROG, None), "{}", s.status);
366 s
367 }
368
369 #[test]
370 fn breakpoints_steps_and_run_to_line() {
371 let mut s = session();
372 s.toggle_breakpoint(6); // bump: INC R0
373 s.start();
374 s.tick();
375 assert!(!s.running);
376 assert_eq!(s.pc_line(), Some(6));
377 assert_eq!(s.status, format!("breakpoint at {:04X}h", s.sim.pc));
378
379 s.step_into(); // INC R0
380 assert_eq!(s.sim.reg(0), 1);
381 assert_eq!(s.pc_line(), Some(7));
382 s.step_out(); // back after ACALL
383 assert_eq!(s.pc_line(), Some(5));
384
385 s.toggle_breakpoint(6);
386 s.step_into(); // SJMP loop
387 assert_eq!(s.pc_line(), Some(4));
388 s.step_over(); // whole call
389 assert_eq!((s.pc_line(), s.sim.reg(0)), (Some(5), 2));
390
391 assert!(s.run_to_line(7));
392 assert_eq!((s.pc_line(), s.sim.reg(0)), (Some(7), 3));
393 assert!(!s.run_to_line(0)); // ORG: no code
394 }
395
396 #[test]
397 fn staleness_errors_and_poke() {
398 let mut s = session();
399 assert!(!s.is_stale(PROG.split('\n')));
400 let edited = PROG.replace("NOP", "NOP\n NOP");
401 assert!(s.is_stale(edited.split('\n')));
402 assert!(s.ensure_built(&edited, None));
403 assert!(!s.is_stale(edited.split('\n')));
404
405 let bad = " MOV A,\n FOO\n";
406 assert!(!s.assemble(bad, None));
407 assert_eq!(s.diagnostics_for(bad).len(), 2);
408 assert!(s.diagnostics_for(PROG).is_empty());
409 assert!(s.build.is_some(), "a failed build keeps the last good one");
410
411 assert!(s.poke(MemSpace::Iram, 0x30, 0xAB));
412 assert_eq!(s.sim.iram[0x30], 0xAB);
413 assert!(s.poke(MemSpace::Sfr, 0x90, 0x0F));
414 assert!(!s.poke(MemSpace::Sfr, 0x20, 1));
415 assert_eq!(s.status, "address out of range");
416 }
417}