bench5186cbab6
move assembler and simulator into a bench51-core crate
The workspace now has a library crate shared by the front ends. The debugger logic that lived in the TUI's App (build, run, step, breakpoints, poke, HEX disassembly) is now bench51_core::session::Session, and syntax highlighting is split into a tokenizer (core) and colours (TUI). The TUI behaves as before.
+5148 −1278
22 files changed
- M Cargo.lock +4268 −794
- M Cargo.toml +20 −3
- A crates/bench51-core/Cargo.toml +7 −0
- R src/asm/expr.rs → crates/bench51-core/src/asm/expr.rs +0 −0
- R src/asm/hex.rs → crates/bench51-core/src/asm/hex.rs +0 −0
- R src/asm/lexer.rs → crates/bench51-core/src/asm/lexer.rs +0 −0
- R src/asm/listing.rs → crates/bench51-core/src/asm/listing.rs +0 −0
- R src/asm/mod.rs → crates/bench51-core/src/asm/mod.rs +0 −0
- A crates/bench51-core/src/files.rs +95 −0
- R src/isa.rs → crates/bench51-core/src/isa.rs +1 −1
- A crates/bench51-core/src/lib.rs +10 −0
- A crates/bench51-core/src/session.rs +417 −0
- R src/sfr.rs → crates/bench51-core/src/sfr.rs +0 −0
- R src/sim/cpu.rs → crates/bench51-core/src/sim/cpu.rs +0 −0
- R src/sim/disasm.rs → crates/bench51-core/src/sim/disasm.rs +0 −0
- R src/sim/mod.rs → crates/bench51-core/src/sim/mod.rs +5 −5
- R src/sim/periph.rs → crates/bench51-core/src/sim/periph.rs +0 −0
- A crates/bench51-core/src/syntax.rs +192 −0
- M src/main.rs +1 −4
- M src/ui/app.rs +81 −309
- M src/ui/draw.rs +34 −50
- M src/ui/syntax.rs +17 −112
[Diff too large] View file
| @@ -1,10 +1,27 @@ | |||
| 1 | − | [package] | |
| 2 | − | name = "bench51" | |
| 1 | + | [workspace] | |
| 2 | + | members = ["crates/*"] | |
| 3 | + | ||
| 4 | + | [workspace.package] | |
| 3 | 5 | version = "0.1.0" | |
| 4 | 6 | edition = "2024" | |
| 7 | + | ||
| 8 | + | [workspace.dependencies] | |
| 9 | + | bench51-core = { path = "crates/bench51-core" } | |
| 10 | + | clap = { version = "4.5.54", features = ["derive"] } | |
| 11 | + | ||
| 12 | + | [package] | |
| 13 | + | name = "bench51" | |
| 14 | + | version.workspace = true | |
| 15 | + | edition.workspace = true | |
| 5 | 16 | description = "A TUI Intel 8051 assembler, simulator and debugger" | |
| 6 | 17 | ||
| 7 | 18 | [dependencies] | |
| 8 | − | clap = { version = "4.5.54", features = ["derive"] } | |
| 19 | + | bench51-core.workspace = true | |
| 20 | + | clap.workspace = true | |
| 9 | 21 | ratatui = "0.30" | |
| 10 | 22 | ratatui-textarea = "0.9" | |
| 23 | + | ||
| 24 | + | # egui and wgpu are unusably slow without optimisation; keep our own crates | |
| 25 | + | # quick to rebuild. | |
| 26 | + | [profile.dev.package."*"] | |
| 27 | + | opt-level = 2 | |
| @@ -0,0 +1,7 @@ | |||
| 1 | + | [package] | |
| 2 | + | name = "bench51-core" | |
| 3 | + | version.workspace = true | |
| 4 | + | edition.workspace = true | |
| 5 | + | description = "Intel 8051 assembler, simulator and debugger session shared by the bench51 front ends" | |
| 6 | + | ||
| 7 | + | [dependencies] | |
[No content changes]
[No content changes]
[No content changes]
[No content changes]
[No content changes]
| @@ -0,0 +1,95 @@ | |||
| 1 | + | //! Loading programs: assembly sources as they are, Intel HEX images as | |
| 2 | + | //! disassembled source. | |
| 3 | + | ||
| 4 | + | use std::path::Path; | |
| 5 | + | ||
| 6 | + | use crate::asm::hex; | |
| 7 | + | use crate::sfr::{hex8, hex16}; | |
| 8 | + | use crate::sim::disasm; | |
| 9 | + | ||
| 10 | + | pub enum Loaded { | |
| 11 | + | Source(String), | |
| 12 | + | /// A HEX image turned into source that reassembles to the same bytes. | |
| 13 | + | Hex { | |
| 14 | + | source: String, | |
| 15 | + | bytes: usize, | |
| 16 | + | }, | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | pub fn is_hex_path(path: &Path) -> bool { | |
| 20 | + | path.extension() | |
| 21 | + | .is_some_and(|e| e.eq_ignore_ascii_case("hex") || e.eq_ignore_ascii_case("ihx")) | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | /// Reads `path`. Errors are ready to show to the user. | |
| 25 | + | pub fn load(path: &Path) -> Result<Loaded, String> { | |
| 26 | + | let text = std::fs::read_to_string(path) | |
| 27 | + | .map_err(|e| format!("cannot open {}: {e}", path.display()))?; | |
| 28 | + | if !is_hex_path(path) { | |
| 29 | + | return Ok(Loaded::Source(text)); | |
| 30 | + | } | |
| 31 | + | let bytes = hex::read(&text).map_err(|e| format!("invalid HEX file: {e}"))?; | |
| 32 | + | Ok(Loaded::Hex { | |
| 33 | + | source: disassemble_image(&bytes, path), | |
| 34 | + | bytes: bytes.len(), | |
| 35 | + | }) | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | /// Builds an assembler source from a HEX image by disassembling each | |
| 39 | + | /// contiguous run of bytes. Reassembling it reproduces the image exactly. | |
| 40 | + | pub fn disassemble_image(bytes: &[(u16, u8)], path: &Path) -> String { | |
| 41 | + | let mut code = Box::new([0u8; 0x10000]); | |
| 42 | + | let mut used = vec![false; 0x10000]; | |
| 43 | + | for &(a, b) in bytes { | |
| 44 | + | code[a as usize] = b; | |
| 45 | + | used[a as usize] = true; | |
| 46 | + | } | |
| 47 | + | let mut src = format!("; disassembled from {}\n", path.display()); | |
| 48 | + | let mut addr = 0usize; | |
| 49 | + | while addr < 0x10000 { | |
| 50 | + | if !used[addr] { | |
| 51 | + | addr += 1; | |
| 52 | + | continue; | |
| 53 | + | } | |
| 54 | + | src.push_str(&format!("\n ORG {}\n", hex16(addr as u16))); | |
| 55 | + | while addr < 0x10000 && used[addr] { | |
| 56 | + | let d = disasm::disassemble(&code, addr as u16); | |
| 57 | + | let size = d.size as usize; | |
| 58 | + | if (addr..addr + size).all(|a| a < 0x10000 && used[a]) { | |
| 59 | + | src.push_str(&format!(" {:<24}; {:04X}\n", d.text, addr)); | |
| 60 | + | addr += size; | |
| 61 | + | } else { | |
| 62 | + | src.push_str(&format!( | |
| 63 | + | " DB {:<21}; {:04X}\n", | |
| 64 | + | hex8(code[addr]), | |
| 65 | + | addr | |
| 66 | + | )); | |
| 67 | + | addr += 1; | |
| 68 | + | } | |
| 69 | + | } | |
| 70 | + | } | |
| 71 | + | src.push_str(" END\n"); | |
| 72 | + | src | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | #[cfg(test)] | |
| 76 | + | mod tests { | |
| 77 | + | use std::path::Path; | |
| 78 | + | ||
| 79 | + | use crate::asm::{assemble, hex}; | |
| 80 | + | ||
| 81 | + | #[test] | |
| 82 | + | fn hex_disassembly_reassembles_to_the_same_image() { | |
| 83 | + | let out = assemble(include_str!("../../../examples/serial_hello.asm")); | |
| 84 | + | assert!(out.ok()); | |
| 85 | + | let image = hex::read(&out.intel_hex()).unwrap(); | |
| 86 | + | let src = super::disassemble_image(&image, Path::new("x.hex")); | |
| 87 | + | let again = assemble(&src); | |
| 88 | + | assert!(again.ok(), "{:?}", again.diagnostics); | |
| 89 | + | let mut a: Vec<_> = out.bytes().collect(); | |
| 90 | + | let mut b: Vec<_> = again.bytes().collect(); | |
| 91 | + | a.sort(); | |
| 92 | + | b.sort(); | |
| 93 | + | assert_eq!(a, b); | |
| 94 | + | } | |
| 95 | + | } | |
| @@ -28,7 +28,7 @@ pub enum Operand { | |||
| 28 | 28 | ||
| 29 | 29 | impl Operand { | |
| 30 | 30 | /// Bytes this operand occupies after the opcode byte. | |
| 31 | − | pub fn len(self) -> u8 { | |
| 31 | + | pub(crate) fn len(self) -> u8 { | |
| 32 | 32 | match self { | |
| 33 | 33 | Operand::Imm8 | Operand::Direct | Operand::Bit | Operand::NotBit => 1, | |
| 34 | 34 | Operand::Addr11 | Operand::Rel => 1, | |
| @@ -0,0 +1,10 @@ | |||
| 1 | + | //! Intel 8051 assembler, simulator and debugger session shared by the | |
| 2 | + | //! bench51 front ends (the `bench51` TUI and the `bench51-gui` desktop app). | |
| 3 | + | ||
| 4 | + | pub mod asm; | |
| 5 | + | pub mod files; | |
| 6 | + | pub mod isa; | |
| 7 | + | pub mod session; | |
| 8 | + | pub mod sfr; | |
| 9 | + | pub mod sim; | |
| 10 | + | pub mod syntax; | |
| @@ -0,0 +1,417 @@ | |||
| 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 | + | ||
| 5 | + | use std::collections::{HashMap, HashSet}; | |
| 6 | + | use std::path::Path; | |
| 7 | + | ||
| 8 | + | use crate::asm::{self, AsmOutput, Diagnostic}; | |
| 9 | + | use crate::sim::{Sim, Stop}; | |
| 10 | + | ||
| 11 | + | /// Result of the last successful assembly, which the simulator runs. | |
| 12 | + | pub struct Build { | |
| 13 | + | pub source: Vec<String>, | |
| 14 | + | pub out: AsmOutput, | |
| 15 | + | pub addr_to_line: HashMap<u16, usize>, | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | impl 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. | |
| 30 | + | pub 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. | |
| 33 | + | const STEP_BUDGET: u32 = 5_000_000; | |
| 34 | + | ||
| 35 | + | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] | |
| 36 | + | pub enum MemSpace { | |
| 37 | + | Iram, | |
| 38 | + | Sfr, | |
| 39 | + | Xram, | |
| 40 | + | Code, | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | impl 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 | + | ||
| 61 | + | pub 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 | + | ||
| 82 | + | impl 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`. | |
| 336 | + | pub 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)] | |
| 348 | + | mod tests { | |
| 349 | + | use super::{MemSpace, Session}; | |
| 350 | + | ||
| 351 | + | const PROG: &str = "\ | |
| 352 | + | ORG 0 | |
| 353 | + | LJMP main | |
| 354 | + | ORG 30h | |
| 355 | + | main: MOV R0,#0 | |
| 356 | + | loop: ACALL bump | |
| 357 | + | SJMP loop | |
| 358 | + | bump: 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 | + | } | |
[No content changes]
[No content changes]
[No content changes]
| @@ -385,7 +385,7 @@ mod example_tests { | |||
| 385 | 385 | ||
| 386 | 386 | #[test] | |
| 387 | 387 | fn serial_hello_greets_and_echoes() { | |
| 388 | − | let mut s = load(include_str!("../../examples/serial_hello.asm")); | |
| 388 | + | let mut s = load(include_str!("../../../../examples/serial_hello.asm")); | |
| 389 | 389 | steps(&mut s, 60_000); | |
| 390 | 390 | assert_eq!( | |
| 391 | 391 | String::from_utf8_lossy(&s.uart.tx_log), | |
| @@ -398,7 +398,7 @@ mod example_tests { | |||
| 398 | 398 | ||
| 399 | 399 | #[test] | |
| 400 | 400 | fn timer_irq_ticks() { | |
| 401 | − | let mut s = load(include_str!("../../examples/timer_irq.asm")); | |
| 401 | + | let mut s = load(include_str!("../../../../examples/timer_irq.asm")); | |
| 402 | 402 | steps(&mut s, 5_000); | |
| 403 | 403 | // ~10000 cycles at one interrupt per 1000 cycles. | |
| 404 | 404 | assert!((3..=10).contains(&s.reg(7)), "R7 = {}", s.reg(7)); | |
| @@ -406,7 +406,7 @@ mod example_tests { | |||
| 406 | 406 | ||
| 407 | 407 | #[test] | |
| 408 | 408 | fn button_counts_edges_while_idle() { | |
| 409 | − | let mut s = load(include_str!("../../examples/button_int0.asm")); | |
| 409 | + | let mut s = load(include_str!("../../../../examples/button_int0.asm")); | |
| 410 | 410 | steps(&mut s, 20); | |
| 411 | 411 | assert!(s.idle); | |
| 412 | 412 | for _ in 0..3 { | |
| @@ -420,10 +420,10 @@ mod example_tests { | |||
| 420 | 420 | ||
| 421 | 421 | #[test] | |
| 422 | 422 | fn bcd_and_lookup() { | |
| 423 | − | let mut s = load(include_str!("../../examples/bcd_add.asm")); | |
| 423 | + | let mut s = load(include_str!("../../../../examples/bcd_add.asm")); | |
| 424 | 424 | steps(&mut s, 20); | |
| 425 | 425 | assert_eq!((s.iram[0x31], s.iram[0x30]), (0x69, 0x12)); | |
| 426 | − | let mut s = load(include_str!("../../examples/lookup_movc.asm")); | |
| 426 | + | let mut s = load(include_str!("../../../../examples/lookup_movc.asm")); | |
| 427 | 427 | steps(&mut s, 3 + 4 * 5 + 3); // setup, digits 0-3, then up to MOV P2,A | |
| 428 | 428 | assert_eq!(s.sfr(crate::sfr::P2), 0x66); // digit 4 | |
| 429 | 429 | } | |
[No content changes]
| @@ -0,0 +1,192 @@ | |||
| 1 | + | //! Minimal 8051 assembly tokenizer for syntax highlighting. | |
| 2 | + | ||
| 3 | + | use std::ops::Range; | |
| 4 | + | ||
| 5 | + | use crate::{isa, sfr}; | |
| 6 | + | ||
| 7 | + | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| 8 | + | pub enum Tok { | |
| 9 | + | /// Whitespace, punctuation and identifiers without a special meaning. | |
| 10 | + | Plain, | |
| 11 | + | Comment, | |
| 12 | + | Str, | |
| 13 | + | /// A label definition (`name:` at the start of a line). | |
| 14 | + | Label, | |
| 15 | + | Mnemonic, | |
| 16 | + | /// Directives and expression operators (`ORG`, `DB`, `HIGH`, `MOD`, …). | |
| 17 | + | Directive, | |
| 18 | + | Register, | |
| 19 | + | /// Predefined SFR and bit names. | |
| 20 | + | Sfr, | |
| 21 | + | Number, | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | fn is_directive(w: &str) -> bool { | |
| 25 | + | matches!( | |
| 26 | + | w, | |
| 27 | + | "ORG" | |
| 28 | + | | "DB" | |
| 29 | + | | "DW" | |
| 30 | + | | "DS" | |
| 31 | + | | "END" | |
| 32 | + | | "EQU" | |
| 33 | + | | "SET" | |
| 34 | + | | "BIT" | |
| 35 | + | | "DATA" | |
| 36 | + | | "IDATA" | |
| 37 | + | | "XDATA" | |
| 38 | + | | "CODE" | |
| 39 | + | | "CSEG" | |
| 40 | + | | "XSEG" | |
| 41 | + | | "DSEG" | |
| 42 | + | | "AT" | |
| 43 | + | | "USING" | |
| 44 | + | | "NAME" | |
| 45 | + | | "BYTE" | |
| 46 | + | | "WORD" | |
| 47 | + | ) | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | fn word_kind(w: &str, first_word: bool) -> Tok { | |
| 51 | + | let u = w.to_ascii_uppercase(); | |
| 52 | + | let reg = matches!(u.as_str(), "A" | "AB" | "C" | "DPTR" | "PC") | |
| 53 | + | || (u.len() == 2 | |
| 54 | + | && u.starts_with('R') | |
| 55 | + | && u.as_bytes()[1].is_ascii_digit() | |
| 56 | + | && u.as_bytes()[1] < b'8'); | |
| 57 | + | if reg { | |
| 58 | + | Tok::Register | |
| 59 | + | } else if is_directive(&u) { | |
| 60 | + | Tok::Directive | |
| 61 | + | } else if first_word && isa::is_mnemonic(&u) { | |
| 62 | + | Tok::Mnemonic | |
| 63 | + | } else if sfr::SFRS.iter().chain(sfr::BITS).any(|(n, _)| *n == u) { | |
| 64 | + | Tok::Sfr | |
| 65 | + | } else if matches!( | |
| 66 | + | u.as_str(), | |
| 67 | + | "HIGH" | "LOW" | "MOD" | "AND" | "OR" | "XOR" | "NOT" | "SHL" | "SHR" | |
| 68 | + | ) { | |
| 69 | + | Tok::Directive | |
| 70 | + | } else { | |
| 71 | + | Tok::Plain | |
| 72 | + | } | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | /// Splits a source line into classified byte ranges that together cover | |
| 76 | + | /// the whole line. | |
| 77 | + | pub fn tokens(line: &str) -> Vec<(Range<usize>, Tok)> { | |
| 78 | + | let b = line.as_bytes(); | |
| 79 | + | let n = b.len(); | |
| 80 | + | let mut out = Vec::new(); | |
| 81 | + | let mut i = 0; | |
| 82 | + | let mut seen_word = false; | |
| 83 | + | while i < n { | |
| 84 | + | let c = b[i]; | |
| 85 | + | let start = i; | |
| 86 | + | let tok = if c == b';' { | |
| 87 | + | i = n; | |
| 88 | + | Tok::Comment | |
| 89 | + | } else if c == b'\'' || c == b'"' { | |
| 90 | + | i += 1; | |
| 91 | + | while i < n && b[i] != c { | |
| 92 | + | i += 1; | |
| 93 | + | } | |
| 94 | + | i = (i + 1).min(n); | |
| 95 | + | Tok::Str | |
| 96 | + | } else if c.is_ascii_alphabetic() || c == b'_' || c == b'?' { | |
| 97 | + | while i < n && (b[i].is_ascii_alphanumeric() || b[i] == b'_' || b[i] == b'?') { | |
| 98 | + | i += 1; | |
| 99 | + | } | |
| 100 | + | if !seen_word && b.get(i) == Some(&b':') { | |
| 101 | + | Tok::Label | |
| 102 | + | } else { | |
| 103 | + | let t = word_kind(&line[start..i], !seen_word); | |
| 104 | + | seen_word = true; | |
| 105 | + | t | |
| 106 | + | } | |
| 107 | + | } else if c.is_ascii_digit() || c == b'#' || c == b'$' { | |
| 108 | + | i += 1; | |
| 109 | + | while i < n && b[i].is_ascii_alphanumeric() { | |
| 110 | + | i += 1; | |
| 111 | + | } | |
| 112 | + | Tok::Number | |
| 113 | + | } else { | |
| 114 | + | // Non-ASCII bytes land here too, so ranges stay on char boundaries. | |
| 115 | + | while i < n && !(b[i].is_ascii_alphanumeric() || b"_?;'\"#$".contains(&b[i])) { | |
| 116 | + | i += 1; | |
| 117 | + | } | |
| 118 | + | Tok::Plain | |
| 119 | + | }; | |
| 120 | + | out.push((start..i, tok)); | |
| 121 | + | } | |
| 122 | + | out | |
| 123 | + | } | |
| 124 | + | ||
| 125 | + | #[cfg(test)] | |
| 126 | + | mod tests { | |
| 127 | + | use super::{Tok, tokens}; | |
| 128 | + | ||
| 129 | + | fn kinds(line: &str) -> Vec<(&str, Tok)> { | |
| 130 | + | tokens(line) | |
| 131 | + | .into_iter() | |
| 132 | + | .filter(|(_, t)| *t != Tok::Plain) | |
| 133 | + | .map(|(r, t)| (&line[r], t)) | |
| 134 | + | .collect() | |
| 135 | + | } | |
| 136 | + | ||
| 137 | + | #[test] | |
| 138 | + | fn classifies_an_instruction_line() { | |
| 139 | + | assert_eq!( | |
| 140 | + | kinds("main: MOV TMOD,#20h ; timer 1"), | |
| 141 | + | vec![ | |
| 142 | + | ("main", Tok::Label), | |
| 143 | + | ("MOV", Tok::Mnemonic), | |
| 144 | + | ("TMOD", Tok::Sfr), | |
| 145 | + | ("#20h", Tok::Number), | |
| 146 | + | ("; timer 1", Tok::Comment), | |
| 147 | + | ] | |
| 148 | + | ); | |
| 149 | + | assert_eq!( | |
| 150 | + | kinds(" mov a,r7"), | |
| 151 | + | vec![ | |
| 152 | + | ("mov", Tok::Mnemonic), | |
| 153 | + | ("a", Tok::Register), | |
| 154 | + | ("r7", Tok::Register) | |
| 155 | + | ] | |
| 156 | + | ); | |
| 157 | + | } | |
| 158 | + | ||
| 159 | + | #[test] | |
| 160 | + | fn directives_strings_and_operators() { | |
| 161 | + | assert_eq!( | |
| 162 | + | kinds("msg: DB \"Hi; there\", HIGH(x) ; c"), | |
| 163 | + | vec![ | |
| 164 | + | ("msg", Tok::Label), | |
| 165 | + | ("DB", Tok::Directive), | |
| 166 | + | ("\"Hi; there\"", Tok::Str), | |
| 167 | + | ("HIGH", Tok::Directive), | |
| 168 | + | ("; c", Tok::Comment), | |
| 169 | + | ] | |
| 170 | + | ); | |
| 171 | + | } | |
| 172 | + | ||
| 173 | + | #[test] | |
| 174 | + | fn ranges_cover_the_line_even_with_unicode() { | |
| 175 | + | for line in [ | |
| 176 | + | "", | |
| 177 | + | " ", | |
| 178 | + | "x DB 'é', 1 ; µs → ok", | |
| 179 | + | "\"unterminated", | |
| 180 | + | "€€ MOV", | |
| 181 | + | ] { | |
| 182 | + | let t = tokens(line); | |
| 183 | + | let mut pos = 0; | |
| 184 | + | for (r, _) in &t { | |
| 185 | + | assert_eq!(r.start, pos, "{line:?}"); | |
| 186 | + | assert!(line.is_char_boundary(r.end)); | |
| 187 | + | pos = r.end; | |
| 188 | + | } | |
| 189 | + | assert_eq!(pos, line.len()); | |
| 190 | + | } | |
| 191 | + | } | |
| 192 | + | } | |
| @@ -1,12 +1,9 @@ | |||
| 1 | − | mod asm; | |
| 2 | − | mod isa; | |
| 3 | − | mod sfr; | |
| 4 | − | mod sim; | |
| 5 | 1 | mod ui; | |
| 6 | 2 | ||
| 7 | 3 | use std::path::PathBuf; | |
| 8 | 4 | use std::process::ExitCode; | |
| 9 | 5 | ||
| 6 | + | use bench51_core::asm; | |
| 10 | 7 | use clap::Parser; | |
| 11 | 8 | ||
| 12 | 9 | /// Intel 8051 assembler, simulator and TUI debugger. | |
| @@ -1,13 +1,11 @@ | |||
| 1 | − | use std::collections::{HashMap, HashSet}; | |
| 2 | 1 | use std::path::{Path, PathBuf}; | |
| 3 | 2 | use std::process::Command; | |
| 4 | 3 | ||
| 4 | + | use bench51_core::files::{self, Loaded}; | |
| 5 | + | use bench51_core::session::{MemSpace, SPEEDS, Session}; | |
| 5 | 6 | use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind}; | |
| 6 | 7 | use ratatui_textarea::{CursorMove, TextArea}; | |
| 7 | 8 | ||
| 8 | − | use crate::asm::{self, AsmOutput}; | |
| 9 | − | use crate::sim::{Sim, Stop, disasm}; | |
| 10 | − | ||
| 11 | 9 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| 12 | 10 | pub enum Mode { | |
| 13 | 11 | Edit, | |
| @@ -31,13 +29,7 @@ pub enum Focus { | |||
| 31 | 29 | Serial, | |
| 32 | 30 | } | |
| 33 | 31 | ||
| 34 | − | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| 35 | − | pub enum MemTab { | |
| 36 | − | Iram, | |
| 37 | − | Sfr, | |
| 38 | − | Xram, | |
| 39 | − | Code, | |
| 40 | − | } | |
| 32 | + | pub type MemTab = MemSpace; | |
| 41 | 33 | ||
| 42 | 34 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| 43 | 35 | pub enum PromptKind { | |
| @@ -52,27 +44,6 @@ pub struct Prompt { | |||
| 52 | 44 | pub input: String, | |
| 53 | 45 | } | |
| 54 | 46 | ||
| 55 | − | /// Result of the last successful assembly, which the debugger runs. | |
| 56 | − | pub struct Build { | |
| 57 | − | pub source: Vec<String>, | |
| 58 | − | pub out: AsmOutput, | |
| 59 | − | pub addr_to_line: HashMap<u16, usize>, | |
| 60 | − | } | |
| 61 | − | ||
| 62 | − | impl Build { | |
| 63 | − | /// First address emitted by `line`, if any. | |
| 64 | − | pub fn line_addr(&self, line: usize) -> Option<u16> { | |
| 65 | − | self.out | |
| 66 | − | .lines | |
| 67 | − | .get(line) | |
| 68 | − | .filter(|l| !l.bytes.is_empty()) | |
| 69 | − | .and_then(|l| l.addr) | |
| 70 | − | } | |
| 71 | − | } | |
| 72 | − | ||
| 73 | − | /// Instructions executed per frame (~30 fps) for each speed setting. | |
| 74 | − | pub const SPEEDS: [u32; 8] = [1, 5, 25, 100, 500, 2_000, 10_000, 40_000]; | |
| 75 | − | ||
| 76 | 47 | pub struct App { | |
| 77 | 48 | pub path: Option<PathBuf>, | |
| 78 | 49 | pub editor: TextArea<'static>, | |
| @@ -82,27 +53,14 @@ pub struct App { | |||
| 82 | 53 | pub focus: Focus, | |
| 83 | 54 | pub mem_tab: MemTab, | |
| 84 | 55 | pub mem_scroll: usize, | |
| 85 | − | /// Last assembler result, even if it had errors (for diagnostics). | |
| 86 | − | pub last_asm: Option<(String, AsmOutput)>, | |
| 87 | − | /// Last error-free build, loaded into the simulator. | |
| 88 | − | pub build: Option<Build>, | |
| 89 | − | pub sim: Sim, | |
| 90 | − | /// Breakpoints by source line. | |
| 91 | − | pub breakpoints: HashSet<usize>, | |
| 92 | − | pub running: bool, | |
| 93 | − | /// The next run chunk starts a fresh run and may leave a breakpoint. | |
| 94 | − | resume: bool, | |
| 95 | − | pub speed: usize, | |
| 96 | − | pub clock_mhz: f64, | |
| 97 | − | pub status: String, | |
| 56 | + | /// Simulator, build, breakpoints and run state. | |
| 57 | + | pub session: Session, | |
| 98 | 58 | /// Selected line in the debug source view. | |
| 99 | 59 | pub src_cursor: usize, | |
| 100 | 60 | pub src_scroll: usize, | |
| 101 | 61 | pub out_scroll: usize, | |
| 102 | 62 | pub port_sel: (usize, u8), | |
| 103 | 63 | pub serial_input: String, | |
| 104 | − | pub prev_iram: [u8; 256], | |
| 105 | − | pub prev_sfr: [u8; 128], | |
| 106 | 64 | pub show_help: bool, | |
| 107 | 65 | pub prompt: Option<Prompt>, | |
| 108 | 66 | pub quit: bool, | |
| @@ -123,28 +81,19 @@ impl App { | |||
| 123 | 81 | focus: Focus::Source, | |
| 124 | 82 | mem_tab: MemTab::Iram, | |
| 125 | 83 | mem_scroll: 0, | |
| 126 | − | last_asm: None, | |
| 127 | − | build: None, | |
| 128 | − | sim: Sim::new(), | |
| 129 | − | breakpoints: HashSet::new(), | |
| 130 | − | running: false, | |
| 131 | − | resume: false, | |
| 132 | − | speed: 5, | |
| 133 | − | clock_mhz, | |
| 134 | − | status: "F1 help · F7 assemble · F5 run · F11 step".into(), | |
| 84 | + | session: Session::new(clock_mhz), | |
| 135 | 85 | src_cursor: 0, | |
| 136 | 86 | src_scroll: 0, | |
| 137 | 87 | out_scroll: 0, | |
| 138 | 88 | port_sel: (1, 0), | |
| 139 | 89 | serial_input: String::new(), | |
| 140 | − | prev_iram: [0; 256], | |
| 141 | − | prev_sfr: [0; 128], | |
| 142 | 90 | show_help: false, | |
| 143 | 91 | prompt: None, | |
| 144 | 92 | quit: false, | |
| 145 | 93 | quit_armed: false, | |
| 146 | 94 | external_edit: false, | |
| 147 | 95 | }; | |
| 96 | + | app.session.status = "F1 help · F7 assemble · F5 run · F11 step".into(); | |
| 148 | 97 | match path { | |
| 149 | 98 | Some(p) => app.open(&p), | |
| 150 | 99 | None => app.set_text(TEMPLATE), | |
| @@ -161,9 +110,7 @@ impl App { | |||
| 161 | 110 | }); | |
| 162 | 111 | crate::ui::draw::style_editor(&mut self.editor); | |
| 163 | 112 | self.saved_text = self.text(); | |
| 164 | − | self.breakpoints.clear(); | |
| 165 | − | self.build = None; | |
| 166 | − | self.last_asm = None; | |
| 113 | + | self.session.clear(); | |
| 167 | 114 | self.focus = Focus::Source; | |
| 168 | 115 | self.serial_input.clear(); | |
| 169 | 116 | } | |
| @@ -178,9 +125,8 @@ impl App { | |||
| 178 | 125 | ||
| 179 | 126 | /// Source differs from what the simulator is running. | |
| 180 | 127 | pub fn stale(&self) -> bool { | |
| 181 | − | self.build | |
| 182 | − | .as_ref() | |
| 183 | − | .is_none_or(|b| b.source != self.editor.lines()) | |
| 128 | + | self.session | |
| 129 | + | .is_stale(self.editor.lines().iter().map(String::as_str)) | |
| 184 | 130 | } | |
| 185 | 131 | ||
| 186 | 132 | pub fn title(&self) -> String { | |
| @@ -194,37 +140,24 @@ impl App { | |||
| 194 | 140 | // ---- Files ---- | |
| 195 | 141 | ||
| 196 | 142 | fn open(&mut self, path: &Path) { | |
| 197 | − | let text = match std::fs::read_to_string(path) { | |
| 198 | − | Ok(t) => t, | |
| 199 | − | Err(e) => { | |
| 200 | − | self.status = format!("cannot open {}: {e}", path.display()); | |
| 201 | − | return; | |
| 143 | + | match files::load(path) { | |
| 144 | + | Err(e) => self.session.status = e, | |
| 145 | + | Ok(Loaded::Hex { source, bytes }) => { | |
| 146 | + | self.set_text(&source); | |
| 147 | + | self.saved_text.clear(); | |
| 148 | + | self.path = None; | |
| 149 | + | self.assemble(); | |
| 150 | + | self.session.status = format!( | |
| 151 | + | "loaded {} ({bytes} bytes) and disassembled it", | |
| 152 | + | path.display() | |
| 153 | + | ); | |
| 202 | 154 | } | |
| 203 | − | }; | |
| 204 | − | if path | |
| 205 | − | .extension() | |
| 206 | − | .is_some_and(|e| e.eq_ignore_ascii_case("hex") || e.eq_ignore_ascii_case("ihx")) | |
| 207 | − | { | |
| 208 | − | match asm::hex::read(&text) { | |
| 209 | − | Ok(bytes) => { | |
| 210 | − | let src = disassemble_image(&bytes, path); | |
| 211 | − | self.set_text(&src); | |
| 212 | − | self.saved_text.clear(); | |
| 213 | − | self.path = None; | |
| 214 | − | self.assemble(); | |
| 215 | − | self.status = format!( | |
| 216 | − | "loaded {} ({} bytes) and disassembled it", | |
| 217 | − | path.display(), | |
| 218 | − | bytes.len() | |
| 219 | − | ); | |
| 220 | − | } | |
| 221 | − | Err(e) => self.status = format!("invalid HEX file: {e}"), | |
| 155 | + | Ok(Loaded::Source(text)) => { | |
| 156 | + | self.set_text(&text); | |
| 157 | + | self.path = Some(path.to_path_buf()); | |
| 158 | + | self.mode = Mode::Edit; | |
| 159 | + | self.session.status = format!("opened {}", path.display()); | |
| 222 | 160 | } | |
| 223 | − | } else { | |
| 224 | − | self.set_text(&text); | |
| 225 | − | self.path = Some(path.to_path_buf()); | |
| 226 | − | self.mode = Mode::Edit; | |
| 227 | − | self.status = format!("opened {}", path.display()); | |
| 228 | 161 | } | |
| 229 | 162 | } | |
| 230 | 163 | ||
| @@ -241,10 +174,10 @@ impl App { | |||
| 241 | 174 | match std::fs::write(&path, text) { | |
| 242 | 175 | Ok(()) => { | |
| 243 | 176 | self.saved_text = self.text(); | |
| 244 | − | self.status = format!("saved {}", path.display()); | |
| 177 | + | self.session.status = format!("saved {}", path.display()); | |
| 245 | 178 | self.path = Some(path); | |
| 246 | 179 | } | |
| 247 | − | Err(e) => self.status = format!("cannot save {}: {e}", path.display()), | |
| 180 | + | Err(e) => self.session.status = format!("cannot save {}: {e}", path.display()), | |
| 248 | 181 | } | |
| 249 | 182 | } | |
| 250 | 183 | ||
| @@ -266,7 +199,7 @@ impl App { | |||
| 266 | 199 | None => { | |
| 267 | 200 | let p = std::env::temp_dir().join(format!("bench51-{}.asm", std::process::id())); | |
| 268 | 201 | if let Err(e) = std::fs::write(&p, self.text() + "\n") { | |
| 269 | − | self.status = format!("cannot write {}: {e}", p.display()); | |
| 202 | + | self.session.status = format!("cannot write {}: {e}", p.display()); | |
| 270 | 203 | return; | |
| 271 | 204 | } | |
| 272 | 205 | (p, true) | |
| @@ -280,11 +213,11 @@ impl App { | |||
| 280 | 213 | let _ = std::fs::remove_file(&path); | |
| 281 | 214 | } | |
| 282 | 215 | match (result, text) { | |
| 283 | − | (Err(e), _) => self.status = format!("cannot start editor: {e}"), | |
| 216 | + | (Err(e), _) => self.session.status = format!("cannot start editor: {e}"), | |
| 284 | 217 | (Ok(s), _) if !s.success() => { | |
| 285 | − | self.status = format!("editor exited with {s}; changes not loaded") | |
| 218 | + | self.session.status = format!("editor exited with {s}; changes not loaded") | |
| 286 | 219 | } | |
| 287 | − | (Ok(_), Err(e)) => self.status = format!("cannot read {}: {e}", path.display()), | |
| 220 | + | (Ok(_), Err(e)) => self.session.status = format!("cannot read {}: {e}", path.display()), | |
| 288 | 221 | (Ok(_), Ok(text)) => self.reload(&text, !temp), | |
| 289 | 222 | } | |
| 290 | 223 | } | |
| @@ -299,7 +232,7 @@ impl App { | |||
| 299 | 232 | lines | |
| 300 | 233 | }; | |
| 301 | 234 | if lines == self.editor.lines() { | |
| 302 | − | self.status = "no changes from external editor".into(); | |
| 235 | + | self.session.status = "no changes from external editor".into(); | |
| 303 | 236 | return; | |
| 304 | 237 | } | |
| 305 | 238 | let cursor = self.editor.cursor(); | |
| @@ -312,61 +245,33 @@ impl App { | |||
| 312 | 245 | self.saved_text = self.text(); | |
| 313 | 246 | } | |
| 314 | 247 | let n = self.editor.lines().len(); | |
| 315 | − | self.breakpoints.retain(|&l| l < n); | |
| 248 | + | self.session.breakpoints.retain(|&l| l < n); | |
| 316 | 249 | if self.assemble() { | |
| 317 | − | self.status = format!("reloaded from external editor; {}", self.status); | |
| 250 | + | self.session.status = format!("reloaded from external editor; {}", self.session.status); | |
| 318 | 251 | } | |
| 319 | 252 | } | |
| 320 | 253 | ||
| 321 | 254 | // ---- Build & run ---- | |
| 322 | 255 | ||
| 323 | 256 | /// Assembles the editor contents. On success the program is loaded | |
| 324 | − | /// into a freshly reset simulator. | |
| 257 | + | /// into a freshly reset simulator; on errors the cursor jumps to the | |
| 258 | + | /// first one. | |
| 325 | 259 | pub fn assemble(&mut self) -> bool { | |
| 326 | 260 | let source = self.text(); | |
| 327 | − | let out = asm::assemble(&source); | |
| 328 | − | let ok = out.ok(); | |
| 261 | + | let ok = self.session.assemble(&source, self.path.as_deref()); | |
| 329 | 262 | if ok { | |
| 330 | − | self.sim.load_code(out.bytes()); | |
| 331 | − | self.sim.reset(); | |
| 332 | − | self.running = false; | |
| 333 | − | self.status = format!("assembled: {} bytes, 0 errors", out.code_size()); | |
| 334 | − | self.write_outputs(&out, &source); | |
| 335 | − | self.build = Some(Build { | |
| 336 | − | source: self.editor.lines().to_vec(), | |
| 337 | − | addr_to_line: out.addr_to_line(), | |
| 338 | − | out: out.clone(), | |
| 339 | − | }); | |
| 340 | − | self.snapshot(); | |
| 341 | 263 | self.follow_pc(); | |
| 342 | − | } else { | |
| 264 | + | } else if let Some((_, out)) = &self.session.last_asm { | |
| 343 | 265 | let first = &out.diagnostics[0]; | |
| 344 | − | self.status = format!( | |
| 345 | − | "{} error(s); line {}: {}", | |
| 346 | − | out.diagnostics.len(), | |
| 347 | − | first.line + 1, | |
| 348 | − | first.msg | |
| 349 | − | ); | |
| 350 | 266 | self.editor | |
| 351 | 267 | .move_cursor(CursorMove::Jump(first.line as u16, first.col as u16)); | |
| 352 | 268 | self.mode = Mode::Edit; | |
| 353 | 269 | self.out_tab = OutTab::Errors; | |
| 354 | 270 | } | |
| 355 | 271 | self.out_scroll = 0; | |
| 356 | − | self.last_asm = Some((source, out)); | |
| 357 | 272 | ok | |
| 358 | 273 | } | |
| 359 | 274 | ||
| 360 | − | /// Writes `.lst` and `.hex` next to a saved source file. | |
| 361 | − | fn write_outputs(&mut self, out: &AsmOutput, source: &str) { | |
| 362 | − | let Some(path) = &self.path else { return }; | |
| 363 | − | let res = std::fs::write(path.with_extension("lst"), out.listing(source)) | |
| 364 | − | .and_then(|_| std::fs::write(path.with_extension("hex"), out.intel_hex())); | |
| 365 | − | if let Err(e) = res { | |
| 366 | − | self.status = format!("assembled, but writing .lst/.hex failed: {e}"); | |
| 367 | − | } | |
| 368 | − | } | |
| 369 | − | ||
| 370 | 275 | /// Makes sure an up-to-date build is loaded; returns false on errors. | |
| 371 | 276 | fn ensure_built(&mut self) -> bool { | |
| 372 | 277 | if self.stale() && !self.assemble() { | |
| @@ -379,132 +284,65 @@ impl App { | |||
| 379 | 284 | true | |
| 380 | 285 | } | |
| 381 | 286 | ||
| 382 | − | pub fn bp_addrs(&self) -> HashSet<u16> { | |
| 383 | − | let Some(b) = &self.build else { | |
| 384 | − | return HashSet::new(); | |
| 385 | − | }; | |
| 386 | − | self.breakpoints | |
| 387 | − | .iter() | |
| 388 | − | .filter_map(|&l| b.line_addr(l)) | |
| 389 | − | .collect() | |
| 390 | − | } | |
| 391 | − | ||
| 392 | − | pub fn snapshot(&mut self) { | |
| 393 | − | self.prev_iram = self.sim.iram; | |
| 394 | − | self.prev_sfr = self.sim.sfr; | |
| 395 | − | } | |
| 396 | − | ||
| 397 | − | pub fn pc_line(&self) -> Option<usize> { | |
| 398 | − | self.build.as_ref()?.addr_to_line.get(&self.sim.pc).copied() | |
| 399 | − | } | |
| 400 | − | ||
| 401 | − | /// Scrolls the source view so the PC line is visible. | |
| 287 | + | /// Moves the source cursor to the PC line. | |
| 402 | 288 | pub fn follow_pc(&mut self) { | |
| 403 | − | if let Some(l) = self.pc_line() { | |
| 289 | + | if let Some(l) = self.session.pc_line() { | |
| 404 | 290 | self.src_cursor = l; | |
| 405 | 291 | } | |
| 406 | 292 | } | |
| 407 | 293 | ||
| 408 | − | fn report(&mut self, stop: Stop) { | |
| 409 | − | self.running = false; | |
| 410 | − | self.status = match stop { | |
| 411 | − | Stop::Breakpoint(pc) => format!("breakpoint at {pc:04X}h"), | |
| 412 | − | Stop::ReservedOpcode { pc, opcode } => { | |
| 413 | − | format!("reserved opcode {opcode:02X}h at {pc:04X}h — stopped") | |
| 414 | − | } | |
| 415 | − | Stop::PowerDown => "CPU in power-down mode (PCON.PD) — reset to continue".into(), | |
| 416 | − | Stop::Target | Stop::Budget => format!("stopped at {:04X}h", self.sim.pc), | |
| 417 | − | }; | |
| 418 | − | self.follow_pc(); | |
| 419 | − | } | |
| 420 | − | ||
| 421 | − | fn step_cmd(&mut self, f: impl FnOnce(&mut Sim, &HashSet<u16>) -> Stop) { | |
| 294 | + | /// Runs a session command once a build is loaded, then follows the PC. | |
| 295 | + | fn debug_cmd(&mut self, f: impl FnOnce(&mut Session)) { | |
| 422 | 296 | if !self.ensure_built() { | |
| 423 | 297 | return; | |
| 424 | 298 | } | |
| 425 | − | self.running = false; | |
| 426 | − | self.snapshot(); | |
| 427 | − | let bps = self.bp_addrs(); | |
| 428 | − | let stop = f(&mut self.sim, &bps); | |
| 429 | − | match stop { | |
| 430 | − | Stop::Target => { | |
| 431 | − | self.status = format!("PC = {:04X}h", self.sim.pc); | |
| 432 | − | self.follow_pc(); | |
| 433 | − | } | |
| 434 | − | Stop::Budget => self.report(Stop::Budget), | |
| 435 | − | s => self.report(s), | |
| 436 | − | } | |
| 299 | + | f(&mut self.session); | |
| 300 | + | self.follow_pc(); | |
| 437 | 301 | } | |
| 438 | 302 | ||
| 439 | 303 | fn toggle_run(&mut self) { | |
| 440 | − | if self.running { | |
| 441 | − | self.running = false; | |
| 442 | − | self.status = format!("paused at {:04X}h", self.sim.pc); | |
| 304 | + | if self.session.running { | |
| 305 | + | self.session.pause(); | |
| 443 | 306 | self.follow_pc(); | |
| 444 | 307 | } else if self.ensure_built() { | |
| 445 | − | self.running = true; | |
| 446 | − | self.resume = true; | |
| 447 | − | self.status = "running…".into(); | |
| 308 | + | self.session.start(); | |
| 448 | 309 | } | |
| 449 | 310 | } | |
| 450 | 311 | ||
| 451 | − | fn run_to_cursor(&mut self) { | |
| 452 | − | if !self.ensure_built() { | |
| 453 | − | return; | |
| 454 | − | } | |
| 455 | − | let line = if self.mode == Mode::Debug { | |
| 312 | + | /// Source line the cursor is on in the current mode. | |
| 313 | + | fn cursor_line(&self) -> usize { | |
| 314 | + | if self.mode == Mode::Debug { | |
| 456 | 315 | self.src_cursor | |
| 457 | 316 | } else { | |
| 458 | 317 | self.editor.cursor().0 | |
| 459 | − | }; | |
| 460 | − | let Some(target) = self.build.as_ref().and_then(|b| b.line_addr(line)) else { | |
| 461 | − | self.status = "no code on that line".into(); | |
| 318 | + | } | |
| 319 | + | } | |
| 320 | + | ||
| 321 | + | fn run_to_cursor(&mut self) { | |
| 322 | + | if !self.ensure_built() { | |
| 462 | 323 | return; | |
| 463 | − | }; | |
| 464 | − | self.step_cmd(|s, bps| { | |
| 465 | − | let mut bps = bps.clone(); | |
| 466 | − | bps.insert(target); | |
| 467 | − | match s.run(5_000_000, &bps, true) { | |
| 468 | − | Stop::Breakpoint(pc) if pc == target => Stop::Target, | |
| 469 | − | other => other, | |
| 470 | − | } | |
| 324 | + | } | |
| 325 | + | let line = self.cursor_line(); | |
| 326 | + | self.debug_cmd(|s| { | |
| 327 | + | s.run_to_line(line); | |
| 471 | 328 | }); | |
| 472 | 329 | } | |
| 473 | 330 | ||
| 474 | 331 | fn reset(&mut self) { | |
| 475 | − | self.sim.reset(); | |
| 476 | − | self.running = false; | |
| 477 | − | self.snapshot(); | |
| 332 | + | self.session.reset(); | |
| 478 | 333 | self.follow_pc(); | |
| 479 | − | self.status = "reset".into(); | |
| 480 | 334 | } | |
| 481 | 335 | ||
| 482 | 336 | fn toggle_breakpoint(&mut self) { | |
| 483 | − | let line = if self.mode == Mode::Debug { | |
| 484 | − | self.src_cursor | |
| 485 | − | } else { | |
| 486 | − | self.editor.cursor().0 | |
| 487 | − | }; | |
| 488 | − | if !self.breakpoints.remove(&line) { | |
| 489 | − | self.breakpoints.insert(line); | |
| 490 | − | } | |
| 337 | + | let line = self.cursor_line(); | |
| 338 | + | self.session.toggle_breakpoint(line); | |
| 491 | 339 | } | |
| 492 | 340 | ||
| 493 | 341 | /// Called once per frame. | |
| 494 | 342 | pub fn tick(&mut self) { | |
| 495 | − | if !self.running { | |
| 496 | − | return; | |
| 497 | − | } | |
| 498 | − | self.snapshot(); | |
| 499 | − | let bps = self.bp_addrs(); | |
| 500 | − | let resume = std::mem::take(&mut self.resume); | |
| 501 | − | match self.sim.run(SPEEDS[self.speed], &bps, resume) { | |
| 502 | − | Stop::Budget => { | |
| 503 | − | if let Some(l) = self.pc_line() { | |
| 504 | − | self.src_cursor = l; | |
| 505 | − | } | |
| 506 | − | } | |
| 507 | − | s => self.report(s), | |
| 343 | + | if self.session.running { | |
| 344 | + | self.session.tick(); | |
| 345 | + | self.follow_pc(); | |
| 508 | 346 | } | |
| 509 | 347 | } | |
| 510 | 348 | ||
| @@ -541,7 +379,7 @@ impl App { | |||
| 541 | 379 | (KeyCode::Char('q'), true) => { | |
| 542 | 380 | if self.dirty() && !self.quit_armed { | |
| 543 | 381 | self.quit_armed = true; | |
| 544 | − | self.status = "unsaved changes — press Ctrl-Q again to quit".into(); | |
| 382 | + | self.session.status = "unsaved changes — press Ctrl-Q again to quit".into(); | |
| 545 | 383 | } else { | |
| 546 | 384 | self.quit = true; | |
| 547 | 385 | } | |
| @@ -571,12 +409,9 @@ impl App { | |||
| 571 | 409 | } | |
| 572 | 410 | (KeyCode::F(8), _) => self.run_to_cursor(), | |
| 573 | 411 | (KeyCode::F(9), _) => self.toggle_breakpoint(), | |
| 574 | − | (KeyCode::F(10), _) => self.step_cmd(|s, b| s.step_over(5_000_000, b)), | |
| 575 | − | (KeyCode::F(11), _) => self.step_cmd(|s, _| match s.step() { | |
| 576 | − | Ok(()) => Stop::Target, | |
| 577 | − | Err(e) => e, | |
| 578 | − | }), | |
| 579 | − | (KeyCode::F(12), _) => self.step_cmd(|s, b| s.step_out(5_000_000, b)), | |
| 412 | + | (KeyCode::F(10), _) => self.debug_cmd(Session::step_over), | |
| 413 | + | (KeyCode::F(11), _) => self.debug_cmd(Session::step_into), | |
| 414 | + | (KeyCode::F(12), _) => self.debug_cmd(Session::step_out), | |
| 580 | 415 | _ => match self.mode { | |
| 581 | 416 | Mode::Edit => self.on_edit_key(key), | |
| 582 | 417 | Mode::Debug => self.on_debug_key(key), | |
| @@ -595,7 +430,7 @@ impl App { | |||
| 595 | 430 | KeyCode::Down if alt => self.out_scroll += 1, | |
| 596 | 431 | KeyCode::PageUp if alt => self.out_scroll = self.out_scroll.saturating_sub(20), | |
| 597 | 432 | KeyCode::PageDown if alt => self.out_scroll += 20, | |
| 598 | − | KeyCode::Esc if self.running => self.toggle_run(), | |
| 433 | + | KeyCode::Esc if self.session.running => self.toggle_run(), | |
| 599 | 434 | _ => { | |
| 600 | 435 | self.editor.input(key); | |
| 601 | 436 | } | |
| @@ -608,7 +443,7 @@ impl App { | |||
| 608 | 443 | KeyCode::Enter => { | |
| 609 | 444 | let mut bytes = std::mem::take(&mut self.serial_input).into_bytes(); | |
| 610 | 445 | bytes.push(b'\r'); | |
| 611 | − | self.sim.uart_send(&bytes); | |
| 446 | + | self.session.sim.uart_send(&bytes); | |
| 612 | 447 | return; | |
| 613 | 448 | } | |
| 614 | 449 | KeyCode::Backspace => { | |
| @@ -628,7 +463,7 @@ impl App { | |||
| 628 | 463 | KeyCode::Tab => self.cycle_focus(true), | |
| 629 | 464 | KeyCode::BackTab => self.cycle_focus(false), | |
| 630 | 465 | KeyCode::Esc => { | |
| 631 | − | if self.running { | |
| 466 | + | if self.session.running { | |
| 632 | 467 | self.toggle_run(); | |
| 633 | 468 | } else if self.focus == Focus::Serial { | |
| 634 | 469 | self.focus = Focus::Source; | |
| @@ -637,9 +472,9 @@ impl App { | |||
| 637 | 472 | } | |
| 638 | 473 | } | |
| 639 | 474 | KeyCode::Char('+') | KeyCode::Char('=') => { | |
| 640 | − | self.speed = (self.speed + 1).min(SPEEDS.len() - 1) | |
| 475 | + | self.session.speed = (self.session.speed + 1).min(SPEEDS.len() - 1) | |
| 641 | 476 | } | |
| 642 | − | KeyCode::Char('-') => self.speed = self.speed.saturating_sub(1), | |
| 477 | + | KeyCode::Char('-') => self.session.speed = self.session.speed.saturating_sub(1), | |
| 643 | 478 | KeyCode::Char('?') => self.show_help = true, | |
| 644 | 479 | // Letter aliases for terminals that swallow function keys. | |
| 645 | 480 | KeyCode::Char('r') => self.toggle_run(), | |
| @@ -684,6 +519,7 @@ impl App { | |||
| 684 | 519 | ||
| 685 | 520 | fn move_src(&mut self, code: KeyCode, page: usize) { | |
| 686 | 521 | let max = self | |
| 522 | + | .session | |
| 687 | 523 | .build | |
| 688 | 524 | .as_ref() | |
| 689 | 525 | .map_or(0, |b| b.source.len().saturating_sub(1)); | |
| @@ -694,7 +530,7 @@ impl App { | |||
| 694 | 530 | KeyCode::PageDown => self.src_cursor + page, | |
| 695 | 531 | KeyCode::Home => 0, | |
| 696 | 532 | KeyCode::End => max, | |
| 697 | − | KeyCode::Char('.') => self.pc_line().unwrap_or(self.src_cursor), | |
| 533 | + | KeyCode::Char('.') => self.session.pc_line().unwrap_or(self.src_cursor), | |
| 698 | 534 | _ => self.src_cursor, | |
| 699 | 535 | } | |
| 700 | 536 | .min(max); | |
| @@ -745,7 +581,7 @@ impl App { | |||
| 745 | 581 | KeyCode::Left => *bit = (*bit + 1).min(7), | |
| 746 | 582 | KeyCode::Right => *bit = bit.saturating_sub(1), | |
| 747 | 583 | KeyCode::Char(' ') | KeyCode::Enter => { | |
| 748 | − | self.sim.pins[*port] ^= 1 << *bit; | |
| 584 | + | self.session.sim.pins[*port] ^= 1 << *bit; | |
| 749 | 585 | } | |
| 750 | 586 | _ => {} | |
| 751 | 587 | } | |
| @@ -786,38 +622,14 @@ impl App { | |||
| 786 | 622 | parts.first().and_then(|s| parse(s)), | |
| 787 | 623 | parts.get(1).and_then(|s| parse(s)), | |
| 788 | 624 | ) else { | |
| 789 | − | self.status = "usage: <addr> <value> (hex), e.g. 30 FF".into(); | |
| 625 | + | self.session.status = "usage: <addr> <value> (hex), e.g. 30 FF".into(); | |
| 790 | 626 | return; | |
| 791 | 627 | }; | |
| 792 | 628 | if val > 0xFF { | |
| 793 | − | self.status = "value must be a byte".into(); | |
| 629 | + | self.session.status = "value must be a byte".into(); | |
| 794 | 630 | return; | |
| 795 | 631 | } | |
| 796 | − | let v = val as u8; | |
| 797 | − | let ok = match self.mem_tab { | |
| 798 | − | MemTab::Iram if addr <= 0xFF => { | |
| 799 | − | self.sim.iram[addr as usize] = v; | |
| 800 | − | true | |
| 801 | − | } | |
| 802 | − | MemTab::Sfr if (0x80..=0xFF).contains(&addr) => { | |
| 803 | − | self.sim.set_sfr(addr as u8, v); | |
| 804 | − | true | |
| 805 | − | } | |
| 806 | − | MemTab::Xram if addr <= 0xFFFF => { | |
| 807 | − | self.sim.xram[addr as usize] = v; | |
| 808 | − | true | |
| 809 | − | } | |
| 810 | − | MemTab::Code if addr <= 0xFFFF => { | |
| 811 | − | self.sim.code[addr as usize] = v; | |
| 812 | − | true | |
| 813 | − | } | |
| 814 | − | _ => false, | |
| 815 | − | }; | |
| 816 | − | self.status = if ok { | |
| 817 | − | format!("wrote {v:02X}h at {addr:X}h") | |
| 818 | − | } else { | |
| 819 | − | "address out of range".into() | |
| 820 | − | }; | |
| 632 | + | self.session.poke(self.mem_tab, addr, val as u8); | |
| 821 | 633 | } | |
| 822 | 634 | } | |
| 823 | 635 | ||
| @@ -848,46 +660,6 @@ fn editor_command(path: &Path, line: usize) -> Command { | |||
| 848 | 660 | cmd | |
| 849 | 661 | } | |
| 850 | 662 | ||
| 851 | − | /// Builds an assembler source from a HEX image by disassembling each | |
| 852 | − | /// contiguous run of bytes. Reassembling it reproduces the image exactly. | |
| 853 | − | fn disassemble_image(bytes: &[(u16, u8)], path: &Path) -> String { | |
| 854 | − | let mut code = Box::new([0u8; 0x10000]); | |
| 855 | − | let mut used = vec![false; 0x10000]; | |
| 856 | − | for &(a, b) in bytes { | |
| 857 | − | code[a as usize] = b; | |
| 858 | − | used[a as usize] = true; | |
| 859 | − | } | |
| 860 | − | let mut src = format!("; disassembled from {}\n", path.display()); | |
| 861 | − | let mut addr = 0usize; | |
| 862 | − | while addr < 0x10000 { | |
| 863 | − | if !used[addr] { | |
| 864 | − | addr += 1; | |
| 865 | − | continue; | |
| 866 | − | } | |
| 867 | − | src.push_str(&format!( | |
| 868 | − | "\n ORG {}\n", | |
| 869 | − | crate::sfr::hex16(addr as u16) | |
| 870 | − | )); | |
| 871 | − | while addr < 0x10000 && used[addr] { | |
| 872 | − | let d = disasm::disassemble(&code, addr as u16); | |
| 873 | − | let size = d.size as usize; | |
| 874 | − | if (addr..addr + size).all(|a| a < 0x10000 && used[a]) { | |
| 875 | − | src.push_str(&format!(" {:<24}; {:04X}\n", d.text, addr)); | |
| 876 | − | addr += size; | |
| 877 | − | } else { | |
| 878 | − | src.push_str(&format!( | |
| 879 | − | " DB {:<21}; {:04X}\n", | |
| 880 | − | crate::sfr::hex8(code[addr]), | |
| 881 | − | addr | |
| 882 | − | )); | |
| 883 | − | addr += 1; | |
| 884 | − | } | |
| 885 | − | } | |
| 886 | − | } | |
| 887 | − | src.push_str(" END\n"); | |
| 888 | − | src | |
| 889 | − | } | |
| 890 | − | ||
| 891 | 663 | const TEMPLATE: &str = "\ | |
| 892 | 664 | ; New 8051 program. F7 assembles, F5 runs, F11 steps, F1 shows all keys. | |
| 893 | 665 | ORG 0000h | |
| @@ -5,9 +5,10 @@ use ratatui::text::{Line, Span}; | |||
| 5 | 5 | use ratatui::widgets::{Block, BorderType, Borders, Clear, Paragraph, Tabs, Wrap}; | |
| 6 | 6 | use ratatui_textarea::TextArea; | |
| 7 | 7 | ||
| 8 | − | use super::app::{App, Focus, MemTab, Mode, OutTab, PromptKind, SPEEDS}; | |
| 8 | + | use super::app::{App, Focus, MemTab, Mode, OutTab, PromptKind}; | |
| 9 | 9 | use super::syntax::highlight; | |
| 10 | − | use crate::sfr::{self, IE, IP, PCON, PSW, SCON, TCON, TH0, TH1, TL0, TL1, TMOD}; | |
| 10 | + | use bench51_core::session::{SPEEDS, vector_name}; | |
| 11 | + | use bench51_core::sfr::{self, IE, IP, PCON, PSW, SCON, TCON, TH0, TH1, TL0, TL1, TMOD}; | |
| 11 | 12 | ||
| 12 | 13 | const ACCENT: Color = Color::Cyan; | |
| 13 | 14 | const CHANGED: Style = Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD); | |
| @@ -73,12 +74,12 @@ fn draw_header(f: &mut Frame, app: &App, area: Rect) { | |||
| 73 | 74 | Style::new().bg(Color::Magenta).fg(Color::White).bold(), | |
| 74 | 75 | ), | |
| 75 | 76 | }; | |
| 76 | − | let (state, state_style) = if app.running { | |
| 77 | + | let (state, state_style) = if app.session.running { | |
| 77 | 78 | ( | |
| 78 | 79 | " RUNNING ", | |
| 79 | 80 | Style::new().bg(Color::Green).fg(Color::Black).bold(), | |
| 80 | 81 | ) | |
| 81 | − | } else if app.build.is_none() { | |
| 82 | + | } else if app.session.build.is_none() { | |
| 82 | 83 | ( | |
| 83 | 84 | " NOT BUILT ", | |
| 84 | 85 | Style::new().bg(Color::DarkGray).fg(Color::White), | |
| @@ -96,14 +97,14 @@ fn draw_header(f: &mut Frame, app: &App, area: Rect) { | |||
| 96 | 97 | Span::styled(state, state_style), | |
| 97 | 98 | Span::raw(format!(" {}", app.title())), | |
| 98 | 99 | ]; | |
| 99 | − | if app.build.is_some() && app.stale() { | |
| 100 | + | if app.session.build.is_some() && app.stale() { | |
| 100 | 101 | spans.push(Span::styled( | |
| 101 | 102 | " (source changed — F7 to rebuild)", | |
| 102 | 103 | Style::new().fg(Color::Yellow), | |
| 103 | 104 | )); | |
| 104 | 105 | } | |
| 105 | 106 | spans.push(Span::styled( | |
| 106 | − | format!(" speed {}/frame (+/-)", SPEEDS[app.speed]), | |
| 107 | + | format!(" speed {}/frame (+/-)", SPEEDS[app.session.speed]), | |
| 107 | 108 | DIM, | |
| 108 | 109 | )); | |
| 109 | 110 | f.render_widget(Line::from(spans), area); | |
| @@ -137,7 +138,7 @@ fn draw_footer(f: &mut Frame, app: &App, area: Rect) { | |||
| 137 | 138 | } | |
| 138 | 139 | }; | |
| 139 | 140 | let width = area.width as usize; | |
| 140 | − | let status = format!(" {}", app.status); | |
| 141 | + | let status = format!(" {}", app.session.status); | |
| 141 | 142 | let pad = width.saturating_sub(status.chars().count() + keys.len() + 1); | |
| 142 | 143 | let line = Line::from(vec![ | |
| 143 | 144 | Span::styled(status, Style::new().fg(Color::White)), | |
| @@ -163,16 +164,16 @@ fn draw_edit(f: &mut Frame, app: &mut App, area: Rect) { | |||
| 163 | 164 | // Breakpoint / error markers aligned with the editor's scroll position. | |
| 164 | 165 | let top = app.editor.scroll_offset().0 as usize; | |
| 165 | 166 | let errors: Vec<usize> = app | |
| 166 | − | .last_asm | |
| 167 | − | .as_ref() | |
| 168 | − | .filter(|(src, _)| *src == app.text()) | |
| 169 | − | .map(|(_, o)| o.diagnostics.iter().map(|d| d.line).collect()) | |
| 170 | − | .unwrap_or_default(); | |
| 167 | + | .session | |
| 168 | + | .diagnostics_for(&app.text()) | |
| 169 | + | .iter() | |
| 170 | + | .map(|d| d.line) | |
| 171 | + | .collect(); | |
| 171 | 172 | let marks: Vec<Line> = (top..top + gutter.height as usize) | |
| 172 | 173 | .map(|l| { | |
| 173 | 174 | if errors.contains(&l) { | |
| 174 | 175 | Line::styled("✗", Style::new().fg(Color::Red).bold()) | |
| 175 | − | } else if app.breakpoints.contains(&l) { | |
| 176 | + | } else if app.session.breakpoints.contains(&l) { | |
| 176 | 177 | Line::styled("●", Style::new().fg(Color::Red)) | |
| 177 | 178 | } else { | |
| 178 | 179 | Line::raw("") | |
| @@ -186,6 +187,7 @@ fn draw_edit(f: &mut Frame, app: &mut App, area: Rect) { | |||
| 186 | 187 | ||
| 187 | 188 | fn draw_output(f: &mut Frame, app: &mut App, area: Rect) { | |
| 188 | 189 | let n_err = app | |
| 190 | + | .session | |
| 189 | 191 | .last_asm | |
| 190 | 192 | .as_ref() | |
| 191 | 193 | .map_or(0, |(_, o)| o.diagnostics.len()); | |
| @@ -214,7 +216,7 @@ fn draw_output(f: &mut Frame, app: &mut App, area: Rect) { | |||
| 214 | 216 | .highlight_style(Style::new().fg(ACCENT).bold().underlined()); | |
| 215 | 217 | f.render_widget(tabs, tabs_area); | |
| 216 | 218 | ||
| 217 | − | let lines: Vec<Line> = match (app.out_tab, &app.last_asm) { | |
| 219 | + | let lines: Vec<Line> = match (app.out_tab, &app.session.last_asm) { | |
| 218 | 220 | (_, None) => vec![Line::styled("Press F7 to assemble.", DIM)], | |
| 219 | 221 | (OutTab::Listing, Some((src, out))) => out | |
| 220 | 222 | .listing(src) | |
| @@ -312,7 +314,7 @@ fn draw_source(f: &mut Frame, app: &mut App, area: Rect) { | |||
| 312 | 314 | let block = panel("Source", focused); | |
| 313 | 315 | let inner = block.inner(area); | |
| 314 | 316 | f.render_widget(block, area); | |
| 315 | − | let Some(build) = &app.build else { | |
| 317 | + | let Some(build) = &app.session.build else { | |
| 316 | 318 | f.render_widget( | |
| 317 | 319 | Paragraph::new("Nothing assembled yet — press F7.").style(DIM), | |
| 318 | 320 | inner, | |
| @@ -326,7 +328,7 @@ fn draw_source(f: &mut Frame, app: &mut App, area: Rect) { | |||
| 326 | 328 | } else if app.src_cursor >= app.src_scroll + h { | |
| 327 | 329 | app.src_scroll = app.src_cursor + 1 - h; | |
| 328 | 330 | } | |
| 329 | − | let pc_line = build.addr_to_line.get(&app.sim.pc).copied(); | |
| 331 | + | let pc_line = build.addr_to_line.get(&app.session.sim.pc).copied(); | |
| 330 | 332 | let lines: Vec<Line> = build | |
| 331 | 333 | .source | |
| 332 | 334 | .iter() | |
| @@ -343,7 +345,7 @@ fn draw_source(f: &mut Frame, app: &mut App, area: Rect) { | |||
| 343 | 345 | } else { | |
| 344 | 346 | Style::new() | |
| 345 | 347 | }; | |
| 346 | − | let bp = if app.breakpoints.contains(&i) { | |
| 348 | + | let bp = if app.session.breakpoints.contains(&i) { | |
| 347 | 349 | Span::styled("●", bg.fg(Color::Red)) | |
| 348 | 350 | } else { | |
| 349 | 351 | Span::styled(" ", bg) | |
| @@ -394,27 +396,20 @@ fn flags(byte: u8, names: [&'static str; 8]) -> Vec<Span<'static>> { | |||
| 394 | 396 | } | |
| 395 | 397 | ||
| 396 | 398 | fn draw_registers(f: &mut Frame, app: &App, area: Rect) { | |
| 397 | − | let s = &app.sim; | |
| 399 | + | let s = &app.session.sim; | |
| 398 | 400 | let block = panel("Registers", false); | |
| 399 | 401 | let inner = block.inner(area); | |
| 400 | 402 | f.render_widget(block, area); | |
| 401 | − | let ch = |addr: u8| s.sfr(addr) != app.prev_sfr[(addr & 0x7F) as usize]; | |
| 403 | + | let ch = |addr: u8| s.sfr(addr) != app.session.prev_sfr[(addr & 0x7F) as usize]; | |
| 402 | 404 | let reg = |n: u8| { | |
| 403 | 405 | let a = s.reg_addr(n) as usize; | |
| 404 | 406 | vec![ | |
| 405 | 407 | Span::styled(format!("R{n} "), LABEL), | |
| 406 | − | hex2(s.iram[a], s.iram[a] != app.prev_iram[a]), | |
| 408 | + | hex2(s.iram[a], s.iram[a] != app.session.prev_iram[a]), | |
| 407 | 409 | Span::raw(" "), | |
| 408 | 410 | ] | |
| 409 | 411 | }; | |
| 410 | − | let micros = s.cycles as f64 * 12.0 / app.clock_mhz; | |
| 411 | − | let time = if micros >= 1_000_000.0 { | |
| 412 | − | format!("{:.3} s", micros / 1e6) | |
| 413 | − | } else if micros >= 1000.0 { | |
| 414 | − | format!("{:.3} ms", micros / 1e3) | |
| 415 | − | } else { | |
| 416 | − | format!("{micros:.2} µs") | |
| 417 | − | }; | |
| 412 | + | let time = app.session.elapsed(); | |
| 418 | 413 | let lines = vec![ | |
| 419 | 414 | Line::from(vec![ | |
| 420 | 415 | Span::styled("PC ", LABEL), | |
| @@ -461,18 +456,18 @@ fn draw_registers(f: &mut Frame, app: &App, area: Rect) { | |||
| 461 | 456 | Line::from(vec![ | |
| 462 | 457 | Span::styled("Time ", LABEL), | |
| 463 | 458 | Span::raw(time), | |
| 464 | − | Span::styled(format!(" @{}MHz", app.clock_mhz), DIM), | |
| 459 | + | Span::styled(format!(" @{}MHz", app.session.clock_mhz), DIM), | |
| 465 | 460 | ]), | |
| 466 | 461 | ]; | |
| 467 | 462 | f.render_widget(Paragraph::new(lines), inner); | |
| 468 | 463 | } | |
| 469 | 464 | ||
| 470 | 465 | fn draw_peripherals(f: &mut Frame, app: &App, area: Rect) { | |
| 471 | − | let s = &app.sim; | |
| 466 | + | let s = &app.session.sim; | |
| 472 | 467 | let block = panel("Peripherals", false); | |
| 473 | 468 | let inner = block.inner(area); | |
| 474 | 469 | f.render_widget(block, area); | |
| 475 | − | let ch = |addr: u8| s.sfr(addr) != app.prev_sfr[(addr & 0x7F) as usize]; | |
| 470 | + | let ch = |addr: u8| s.sfr(addr) != app.session.prev_sfr[(addr & 0x7F) as usize]; | |
| 476 | 471 | let sfr_val = |name: &'static str, addr: u8| { | |
| 477 | 472 | vec![ | |
| 478 | 473 | Span::styled(name, LABEL), | |
| @@ -609,17 +604,6 @@ fn draw_peripherals(f: &mut Frame, app: &App, area: Rect) { | |||
| 609 | 604 | f.render_widget(Paragraph::new(lines), inner); | |
| 610 | 605 | } | |
| 611 | 606 | ||
| 612 | − | fn vector_name(v: u16) -> &'static str { | |
| 613 | − | match v { | |
| 614 | − | 0x03 => "INT0", | |
| 615 | − | 0x0B => "T0", | |
| 616 | − | 0x13 => "INT1", | |
| 617 | − | 0x1B => "T1", | |
| 618 | − | 0x23 => "UART", | |
| 619 | − | _ => "?", | |
| 620 | − | } | |
| 621 | − | } | |
| 622 | − | ||
| 623 | 607 | fn draw_memory(f: &mut Frame, app: &mut App, area: Rect) { | |
| 624 | 608 | let focused = app.focus == Focus::Memory; | |
| 625 | 609 | let block = panel("Memory", focused); | |
| @@ -646,7 +630,7 @@ fn draw_memory(f: &mut Frame, app: &mut App, area: Rect) { | |||
| 646 | 630 | return; | |
| 647 | 631 | } | |
| 648 | 632 | ||
| 649 | − | let s = &app.sim; | |
| 633 | + | let s = &app.session.sim; | |
| 650 | 634 | let per_row: usize = if content.width >= 74 { 16 } else { 8 }; | |
| 651 | 635 | let total = match app.mem_tab { | |
| 652 | 636 | MemTab::Iram => 256, | |
| @@ -655,9 +639,9 @@ fn draw_memory(f: &mut Frame, app: &mut App, area: Rect) { | |||
| 655 | 639 | let rows = total / per_row; | |
| 656 | 640 | let h = content.height as usize; | |
| 657 | 641 | app.mem_scroll = app.mem_scroll.min(rows.saturating_sub(h)); | |
| 658 | − | let bps = app.bp_addrs(); | |
| 642 | + | let bps = app.session.bp_addrs(); | |
| 659 | 643 | let pc = s.pc as usize; | |
| 660 | − | let pc_len = crate::isa::info(s.code[pc]).map_or(1, |i| i.size as usize); | |
| 644 | + | let pc_len = bench51_core::isa::info(s.code[pc]).map_or(1, |i| i.size as usize); | |
| 661 | 645 | let bank = s.reg_addr(0) as usize; | |
| 662 | 646 | ||
| 663 | 647 | let lines: Vec<Line> = (app.mem_scroll..rows.min(app.mem_scroll + h)) | |
| @@ -679,7 +663,7 @@ fn draw_memory(f: &mut Frame, app: &mut App, area: Rect) { | |||
| 679 | 663 | let v = s.iram[a]; | |
| 680 | 664 | let st = if a == s.sp() as usize { | |
| 681 | 665 | Style::new().fg(Color::Black).bg(Color::LightMagenta) | |
| 682 | − | } else if v != app.prev_iram[a] { | |
| 666 | + | } else if v != app.session.prev_iram[a] { | |
| 683 | 667 | CHANGED | |
| 684 | 668 | } else if (bank..bank + 8).contains(&a) { | |
| 685 | 669 | Style::new().fg(Color::LightBlue) | |
| @@ -724,7 +708,7 @@ fn draw_memory(f: &mut Frame, app: &mut App, area: Rect) { | |||
| 724 | 708 | } | |
| 725 | 709 | ||
| 726 | 710 | fn draw_sfr_table(f: &mut Frame, app: &App, area: Rect) { | |
| 727 | − | let s = &app.sim; | |
| 711 | + | let s = &app.session.sim; | |
| 728 | 712 | let cols = if area.width >= 64 { 3 } else { 2 }; | |
| 729 | 713 | let per_col = sfr::SFRS.len().div_ceil(cols); | |
| 730 | 714 | let lines: Vec<Line> = (0..per_col) | |
| @@ -733,7 +717,7 @@ fn draw_sfr_table(f: &mut Frame, app: &App, area: Rect) { | |||
| 733 | 717 | for c in 0..cols { | |
| 734 | 718 | if let Some(&(name, addr)) = sfr::SFRS.get(c * per_col + r) { | |
| 735 | 719 | let v = s.read_direct(addr, true); | |
| 736 | − | let changed = s.sfr(addr) != app.prev_sfr[(addr & 0x7F) as usize]; | |
| 720 | + | let changed = s.sfr(addr) != app.session.prev_sfr[(addr & 0x7F) as usize]; | |
| 737 | 721 | spans.push(Span::styled(format!("{name:<5}"), LABEL)); | |
| 738 | 722 | spans.push(Span::styled(format!("{addr:02X} "), DIM)); | |
| 739 | 723 | spans.push(hex2(v, changed)); | |
| @@ -754,7 +738,7 @@ fn draw_ports(f: &mut Frame, app: &App, area: Rect) { | |||
| 754 | 738 | let block = panel("Ports", focused); | |
| 755 | 739 | let inner = block.inner(area); | |
| 756 | 740 | f.render_widget(block, area); | |
| 757 | − | let s = &app.sim; | |
| 741 | + | let s = &app.session.sim; | |
| 758 | 742 | let mut lines: Vec<Line> = (0..4) | |
| 759 | 743 | .map(|p| { | |
| 760 | 744 | let latch = s.sfr(sfr::P0 + 0x10 * p as u8); | |
| @@ -797,7 +781,7 @@ fn draw_serial(f: &mut Frame, app: &App, area: Rect) { | |||
| 797 | 781 | Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).areas(inner); | |
| 798 | 782 | ||
| 799 | 783 | let mut text = String::new(); | |
| 800 | − | for &b in &app.sim.uart.tx_log { | |
| 784 | + | for &b in &app.session.sim.uart.tx_log { | |
| 801 | 785 | match b { | |
| 802 | 786 | b'\n' => text.push('\n'), | |
| 803 | 787 | b'\r' => {} | |
| @@ -1,122 +1,27 @@ | |||
| 1 | − | //! Minimal 8051 assembly syntax highlighting. | |
| 1 | + | //! 8051 assembly syntax highlighting for the TUI. | |
| 2 | 2 | ||
| 3 | + | use bench51_core::syntax::{Tok, tokens}; | |
| 3 | 4 | use ratatui::style::{Color, Modifier, Style}; | |
| 4 | 5 | use ratatui::text::Span; | |
| 5 | 6 | ||
| 6 | − | use crate::{isa, sfr}; | |
| 7 | − | ||
| 8 | − | const COMMENT: Style = Style::new().fg(Color::DarkGray); | |
| 9 | − | const LABEL: Style = Style::new().fg(Color::Yellow); | |
| 10 | − | const MNEMONIC: Style = Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD); | |
| 11 | − | const DIRECTIVE: Style = Style::new().fg(Color::Magenta); | |
| 12 | − | const REGISTER: Style = Style::new().fg(Color::LightBlue); | |
| 13 | − | const NUMBER: Style = Style::new().fg(Color::LightGreen); | |
| 14 | − | const STRING: Style = Style::new().fg(Color::Green); | |
| 15 | − | const SFR: Style = Style::new().fg(Color::LightMagenta); | |
| 16 | − | ||
| 17 | − | fn is_directive(w: &str) -> bool { | |
| 18 | − | matches!( | |
| 19 | − | w, | |
| 20 | − | "ORG" | |
| 21 | − | | "DB" | |
| 22 | − | | "DW" | |
| 23 | − | | "DS" | |
| 24 | − | | "END" | |
| 25 | − | | "EQU" | |
| 26 | − | | "SET" | |
| 27 | − | | "BIT" | |
| 28 | − | | "DATA" | |
| 29 | − | | "IDATA" | |
| 30 | − | | "XDATA" | |
| 31 | − | | "CODE" | |
| 32 | − | | "CSEG" | |
| 33 | − | | "XSEG" | |
| 34 | − | | "DSEG" | |
| 35 | − | | "AT" | |
| 36 | − | | "USING" | |
| 37 | − | | "NAME" | |
| 38 | − | | "BYTE" | |
| 39 | − | | "WORD" | |
| 40 | − | ) | |
| 41 | − | } | |
| 42 | − | ||
| 43 | − | fn word_style(w: &str, first_word: bool) -> Style { | |
| 44 | − | let u = w.to_ascii_uppercase(); | |
| 45 | − | let reg = matches!(u.as_str(), "A" | "AB" | "C" | "DPTR" | "PC") | |
| 46 | − | || (u.len() == 2 | |
| 47 | − | && u.starts_with('R') | |
| 48 | − | && u.as_bytes()[1].is_ascii_digit() | |
| 49 | − | && u.as_bytes()[1] < b'8'); | |
| 50 | − | if reg { | |
| 51 | − | REGISTER | |
| 52 | − | } else if is_directive(&u) { | |
| 53 | − | DIRECTIVE | |
| 54 | − | } else if first_word && isa::is_mnemonic(&u) { | |
| 55 | − | MNEMONIC | |
| 56 | − | } else if sfr::SFRS.iter().chain(sfr::BITS).any(|(n, _)| *n == u) { | |
| 57 | − | SFR | |
| 58 | − | } else if matches!( | |
| 59 | − | u.as_str(), | |
| 60 | − | "HIGH" | "LOW" | "MOD" | "AND" | "OR" | "XOR" | "NOT" | "SHL" | "SHR" | |
| 61 | − | ) { | |
| 62 | − | DIRECTIVE | |
| 63 | − | } else { | |
| 64 | − | Style::new() | |
| 7 | + | fn style(t: Tok) -> Style { | |
| 8 | + | match t { | |
| 9 | + | Tok::Plain => Style::new(), | |
| 10 | + | Tok::Comment => Style::new().fg(Color::DarkGray), | |
| 11 | + | Tok::Label => Style::new().fg(Color::Yellow), | |
| 12 | + | Tok::Mnemonic => Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD), | |
| 13 | + | Tok::Directive => Style::new().fg(Color::Magenta), | |
| 14 | + | Tok::Register => Style::new().fg(Color::LightBlue), | |
| 15 | + | Tok::Number => Style::new().fg(Color::LightGreen), | |
| 16 | + | Tok::Str => Style::new().fg(Color::Green), | |
| 17 | + | Tok::Sfr => Style::new().fg(Color::LightMagenta), | |
| 65 | 18 | } | |
| 66 | 19 | } | |
| 67 | 20 | ||
| 68 | 21 | /// Splits a source line into styled spans, applying `base` underneath. | |
| 69 | 22 | pub fn highlight(line: &str, base: Style) -> Vec<Span<'static>> { | |
| 70 | − | let chars: Vec<char> = line.chars().collect(); | |
| 71 | − | let mut spans = Vec::new(); | |
| 72 | − | let mut i = 0; | |
| 73 | − | let mut seen_word = false; | |
| 74 | − | let push = |spans: &mut Vec<Span<'static>>, s: String, st: Style| { | |
| 75 | − | spans.push(Span::styled(s, base.patch(st))) | |
| 76 | − | }; | |
| 77 | − | while i < chars.len() { | |
| 78 | − | let c = chars[i]; | |
| 79 | − | let start = i; | |
| 80 | − | if c == ';' { | |
| 81 | − | push(&mut spans, chars[i..].iter().collect(), COMMENT); | |
| 82 | − | break; | |
| 83 | − | } else if c == '\'' || c == '"' { | |
| 84 | − | i += 1; | |
| 85 | − | while i < chars.len() && chars[i] != c { | |
| 86 | − | i += 1; | |
| 87 | − | } | |
| 88 | − | i = (i + 1).min(chars.len()); | |
| 89 | − | push(&mut spans, chars[start..i].iter().collect(), STRING); | |
| 90 | − | } else if c.is_ascii_alphabetic() || c == '_' || c == '?' { | |
| 91 | − | while i < chars.len() | |
| 92 | − | && (chars[i].is_ascii_alphanumeric() || chars[i] == '_' || chars[i] == '?') | |
| 93 | − | { | |
| 94 | − | i += 1; | |
| 95 | − | } | |
| 96 | − | let w: String = chars[start..i].iter().collect(); | |
| 97 | − | let is_label = !seen_word && chars.get(i) == Some(&':'); | |
| 98 | − | let style = if is_label { | |
| 99 | − | LABEL | |
| 100 | − | } else { | |
| 101 | − | let st = word_style(&w, !seen_word); | |
| 102 | − | seen_word = true; | |
| 103 | − | st | |
| 104 | − | }; | |
| 105 | − | push(&mut spans, w, style); | |
| 106 | − | } else if c.is_ascii_digit() || c == '#' || c == '$' { | |
| 107 | − | i += 1; | |
| 108 | − | while i < chars.len() && chars[i].is_ascii_alphanumeric() { | |
| 109 | − | i += 1; | |
| 110 | − | } | |
| 111 | − | push(&mut spans, chars[start..i].iter().collect(), NUMBER); | |
| 112 | − | } else { | |
| 113 | − | while i < chars.len() | |
| 114 | − | && !(chars[i].is_ascii_alphanumeric() || "_?;'\"#$".contains(chars[i])) | |
| 115 | − | { | |
| 116 | − | i += 1; | |
| 117 | − | } | |
| 118 | − | push(&mut spans, chars[start..i].iter().collect(), Style::new()); | |
| 119 | − | } | |
| 120 | − | } | |
| 121 | − | spans | |
| 23 | + | tokens(line) | |
| 24 | + | .into_iter() | |
| 25 | + | .map(|(r, t)| Span::styled(line[r].to_string(), base.patch(style(t)))) | |
| 26 | + | .collect() | |
| 122 | 27 | } | |