Krolyxon
Commit a921fe1

Branches

Tags

Refs →

bench51a921fe1

rewrite as an 8051 TUI assembler, simulator and debugger

Replace the custom 8-bit ISA with an Intel 8051 toolchain and a
ratatui front end, in the spirit of a lightweight Keil uVision.

- isa: single opcode table (all 255 opcodes, sizes, cycles) shared by
  the assembler, disassembler and simulator
- asm: two-pass assembler with expressions, bit addressing, predefined
  SFR/bit names, ORG/DB/DW/DS/EQU/SET/BIT/DATA/... directives, Keil-style
  .lst listing and Intel HEX read/write; headless --assemble mode
- sim: full instruction set with CY/AC/OV/P flags and cycle counts,
  register banks, ports with latch/pin semantics, timers 0/1 (modes
  0-3), interrupts with two priority levels, UART modes 0-3 timed from
  timer 1, idle/power-down; disassembler
- ui: edit mode (editor, listing/HEX/errors tabs) and debug mode
  (source with PC, registers, peripherals, memory viewer, ports, serial
  terminal); run/pause, step into/over/out, run to cursor, breakpoints,
  memory poke; .hex files open as re-assemblable disassembly; Ctrl-G
  edits the source in $EDITOR and reloads on exit
- new examples and README; tests for encodings, flags, peripherals and
  the example programs
Author
krolyxon
Date
2026-10-22 23:59 +0530
Parent
4251847
Commit
a921fe164b81a6465da1f4941a192e483be30e85

+7506 −909

38 files changed

  1. M .gitignore +2 −0
  2. M Cargo.lock +1695 −40
  3. M Cargo.toml +4 −1
  4. M README.md +94 −97
  5. D examples/arithmetic.asm +0 −9
  6. A examples/bcd_add.asm +21 −0
  7. A examples/blink.asm +23 −0
  8. A examples/button_int0.asm +20 −0
  9. D examples/call.asm +0 −7
  10. D examples/cmp.asm +0 −4
  11. D examples/div.asm +0 −4
  12. A examples/lookup_movc.asm +20 −0
  13. D examples/loops.asm +0 −13
  14. D examples/mul.asm +0 −4
  15. A examples/serial_hello.asm +43 −0
  16. D examples/syscall.asm +0 −7
  17. D examples/table.asm +0 −9
  18. A examples/timer_irq.asm +28 −0
  19. A src/asm/expr.rs +251 −0
  20. A src/asm/hex.rs +117 −0
  21. A src/asm/lexer.rs +175 −0
  22. A src/asm/listing.rs +53 −0
  23. A src/asm/mod.rs +940 −0
  24. D src/assembler.rs +0 −201
  25. D src/cpu.rs +0 −424
  26. D src/instructions.rs +0 −43
  27. A src/isa.rs +347 −0
  28. M src/main.rs +81 −29
  29. D src/memory.rs +0 −17
  30. A src/sfr.rs +146 −0
  31. A src/sim/cpu.rs +528 −0
  32. A src/sim/disasm.rs +91 −0
  33. A src/sim/mod.rs +449 −0
  34. A src/sim/periph.rs +403 −0
  35. A src/ui/app.rs +903 −0
  36. A src/ui/draw.rs +882 −0
  37. A src/ui/mod.rs +68 −0
  38. A src/ui/syntax.rs +122 −0
M .gitignore View
@@ -1 +1,3 @@
11 /target
2+examples/*.hex
3+examples/*.lst
M Cargo.lock View
@@ -2,6 +2,21 @@
22 # It is not intended for manual editing.
33 version = 4
44
5+[[package]]
6+name = "aho-corasick"
7+version = "1.1.5"
8+source = "registry+https://github.com/rust-lang/crates.io-index"
9+checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
10+dependencies = [
11+ "memchr",
12+]
13+
14+[[package]]
15+name = "allocator-api2"
16+version = "0.2.21"
17+source = "registry+https://github.com/rust-lang/crates.io-index"
18+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
19+
520 [[package]]
621 name = "anstream"
722 version = "0.6.21"
@@ -52,6 +67,127 @@ dependencies = [
5267 "windows-sys",
5368 ]
5469
70+[[package]]
71+name = "anyhow"
72+version = "1.0.104"
73+source = "registry+https://github.com/rust-lang/crates.io-index"
74+checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
75+
76+[[package]]
77+name = "approx"
78+version = "0.5.1"
79+source = "registry+https://github.com/rust-lang/crates.io-index"
80+checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
81+dependencies = [
82+ "num-traits",
83+]
84+
85+[[package]]
86+name = "atomic"
87+version = "0.6.1"
88+source = "registry+https://github.com/rust-lang/crates.io-index"
89+checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340"
90+dependencies = [
91+ "bytemuck",
92+]
93+
94+[[package]]
95+name = "autocfg"
96+version = "1.5.1"
97+source = "registry+https://github.com/rust-lang/crates.io-index"
98+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
99+
100+[[package]]
101+name = "base64"
102+version = "0.22.1"
103+source = "registry+https://github.com/rust-lang/crates.io-index"
104+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
105+
106+[[package]]
107+name = "bit-set"
108+version = "0.5.3"
109+source = "registry+https://github.com/rust-lang/crates.io-index"
110+checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
111+dependencies = [
112+ "bit-vec",
113+]
114+
115+[[package]]
116+name = "bit-vec"
117+version = "0.6.3"
118+source = "registry+https://github.com/rust-lang/crates.io-index"
119+checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
120+
121+[[package]]
122+name = "bitflags"
123+version = "1.3.2"
124+source = "registry+https://github.com/rust-lang/crates.io-index"
125+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
126+
127+[[package]]
128+name = "bitflags"
129+version = "2.13.2"
130+source = "registry+https://github.com/rust-lang/crates.io-index"
131+checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
132+
133+[[package]]
134+name = "block-buffer"
135+version = "0.10.4"
136+source = "registry+https://github.com/rust-lang/crates.io-index"
137+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
138+dependencies = [
139+ "generic-array",
140+]
141+
142+[[package]]
143+name = "bumpalo"
144+version = "3.20.3"
145+source = "registry+https://github.com/rust-lang/crates.io-index"
146+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
147+
148+[[package]]
149+name = "by_address"
150+version = "1.2.1"
151+source = "registry+https://github.com/rust-lang/crates.io-index"
152+checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06"
153+
154+[[package]]
155+name = "bytemuck"
156+version = "1.25.2"
157+source = "registry+https://github.com/rust-lang/crates.io-index"
158+checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
159+
160+[[package]]
161+name = "castaway"
162+version = "0.2.4"
163+source = "registry+https://github.com/rust-lang/crates.io-index"
164+checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
165+dependencies = [
166+ "rustversion",
167+]
168+
169+[[package]]
170+name = "cc"
171+version = "1.9.0"
172+source = "registry+https://github.com/rust-lang/crates.io-index"
173+checksum = "877bf67ac8e52449eb2f9e98a344ea163beb70dd1f9eebe3cc5357dcfeaee4df"
174+dependencies = [
175+ "find-msvc-tools",
176+ "shlex",
177+]
178+
179+[[package]]
180+name = "cfg-if"
181+version = "1.0.5"
182+source = "registry+https://github.com/rust-lang/crates.io-index"
183+checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
184+
185+[[package]]
186+name = "cfg_aliases"
187+version = "0.2.2"
188+source = "registry+https://github.com/rust-lang/crates.io-index"
189+checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
190+
55191 [[package]]
56192 name = "clap"
57193 version = "4.5.54"
@@ -83,7 +219,7 @@ dependencies = [
83219 "heck",
84220 "proc-macro2",
85221 "quote",
86− "syn",
222+ "syn 2.0.113",
87223 ]
88224
89225 [[package]]
@@ -99,88 +235,1607 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
99235 checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
100236
101237 [[package]]
102−name = "cpu-emulator"
103−version = "0.1.0"
238+name = "compact_str"
239+version = "0.9.1"
240+source = "registry+https://github.com/rust-lang/crates.io-index"
241+checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab"
104242 dependencies = [
105− "clap",
243+ "castaway",
244+ "cfg-if",
245+ "itoa",
246+ "rustversion",
247+ "ryu",
248+ "static_assertions",
106249 ]
107250
108251 [[package]]
109−name = "heck"
110−version = "0.5.0"
252+name = "convert_case"
253+version = "0.10.0"
111254 source = "registry+https://github.com/rust-lang/crates.io-index"
112−checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
255+checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
256+dependencies = [
257+ "unicode-segmentation",
258+]
113259
114260 [[package]]
115−name = "is_terminal_polyfill"
116−version = "1.70.2"
261+name = "cpufeatures"
262+version = "0.2.17"
117263 source = "registry+https://github.com/rust-lang/crates.io-index"
118−checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
264+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
265+dependencies = [
266+ "libc",
267+]
119268
120269 [[package]]
121−name = "once_cell_polyfill"
122−version = "1.70.2"
270+name = "critical-section"
271+version = "1.2.0"
123272 source = "registry+https://github.com/rust-lang/crates.io-index"
124−checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
273+checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
125274
126275 [[package]]
127−name = "proc-macro2"
128−version = "1.0.104"
276+name = "crossterm"
277+version = "0.29.0"
129278 source = "registry+https://github.com/rust-lang/crates.io-index"
130−checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0"
279+checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
131280 dependencies = [
132− "unicode-ident",
281+ "bitflags 2.13.2",
282+ "crossterm_winapi",
283+ "derive_more",
284+ "document-features",
285+ "mio",
286+ "parking_lot",
287+ "rustix",
288+ "signal-hook",
289+ "signal-hook-mio",
290+ "winapi",
133291 ]
134292
135293 [[package]]
136−name = "quote"
137−version = "1.0.42"
294+name = "crossterm_winapi"
295+version = "0.9.1"
138296 source = "registry+https://github.com/rust-lang/crates.io-index"
139−checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
297+checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
298+dependencies = [
299+ "winapi",
300+]
301+
302+[[package]]
303+name = "crypto-common"
304+version = "0.1.7"
305+source = "registry+https://github.com/rust-lang/crates.io-index"
306+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
307+dependencies = [
308+ "generic-array",
309+ "typenum",
310+]
311+
312+[[package]]
313+name = "csscolorparser"
314+version = "0.6.2"
315+source = "registry+https://github.com/rust-lang/crates.io-index"
316+checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf"
317+dependencies = [
318+ "lab",
319+ "phf",
320+]
321+
322+[[package]]
323+name = "darling"
324+version = "0.24.1"
325+source = "registry+https://github.com/rust-lang/crates.io-index"
326+checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec"
140327 dependencies = [
328+ "darling_core",
329+ "darling_macro",
330+]
331+
332+[[package]]
333+name = "darling_core"
334+version = "0.24.1"
335+source = "registry+https://github.com/rust-lang/crates.io-index"
336+checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff"
337+dependencies = [
338+ "ident_case",
141339 "proc-macro2",
340+ "quote",
341+ "strsim",
342+ "syn 3.0.9",
142343 ]
143344
144345 [[package]]
145−name = "strsim"
146−version = "0.11.1"
346+name = "darling_macro"
347+version = "0.24.1"
147348 source = "registry+https://github.com/rust-lang/crates.io-index"
148−checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
349+checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785"
350+dependencies = [
351+ "darling_core",
352+ "quote",
353+ "syn 3.0.9",
354+]
149355
150356 [[package]]
151−name = "syn"
152−version = "2.0.113"
357+name = "deltae"
358+version = "0.3.2"
153359 source = "registry+https://github.com/rust-lang/crates.io-index"
154−checksum = "678faa00651c9eb72dd2020cbdf275d92eccb2400d568e419efdd64838145cb4"
360+checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4"
361+
362+[[package]]
363+name = "deranged"
364+version = "0.5.9"
365+source = "registry+https://github.com/rust-lang/crates.io-index"
366+checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e"
367+
368+[[package]]
369+name = "derive_more"
370+version = "2.1.1"
371+source = "registry+https://github.com/rust-lang/crates.io-index"
372+checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
373+dependencies = [
374+ "derive_more-impl",
375+]
376+
377+[[package]]
378+name = "derive_more-impl"
379+version = "2.1.1"
380+source = "registry+https://github.com/rust-lang/crates.io-index"
381+checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
155382 dependencies = [
383+ "convert_case",
156384 "proc-macro2",
157385 "quote",
158− "unicode-ident",
386+ "rustc_version",
387+ "syn 2.0.113",
159388 ]
160389
161390 [[package]]
162−name = "unicode-ident"
163−version = "1.0.22"
391+name = "digest"
392+version = "0.10.7"
164393 source = "registry+https://github.com/rust-lang/crates.io-index"
165−checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
394+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
395+dependencies = [
396+ "block-buffer",
397+ "crypto-common",
398+]
166399
167400 [[package]]
168−name = "utf8parse"
169−version = "0.2.2"
401+name = "document-features"
402+version = "0.2.12"
170403 source = "registry+https://github.com/rust-lang/crates.io-index"
171−checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
404+checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
405+dependencies = [
406+ "litrs",
407+]
172408
173409 [[package]]
174−name = "windows-link"
175−version = "0.2.1"
410+name = "either"
411+version = "1.19.0"
176412 source = "registry+https://github.com/rust-lang/crates.io-index"
177−checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
413+checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be"
178414
179415 [[package]]
180−name = "windows-sys"
181−version = "0.61.2"
416+name = "equivalent"
417+version = "1.0.3"
182418 source = "registry+https://github.com/rust-lang/crates.io-index"
183−checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
419+checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826"
420+
421+[[package]]
422+name = "errno"
423+version = "0.3.14"
424+source = "registry+https://github.com/rust-lang/crates.io-index"
425+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
184426 dependencies = [
185− "windows-link",
427+ "libc",
428+ "windows-sys",
429+]
430+
431+[[package]]
432+name = "euclid"
433+version = "0.22.14"
434+source = "registry+https://github.com/rust-lang/crates.io-index"
435+checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06"
436+dependencies = [
437+ "num-traits",
438+]
439+
440+[[package]]
441+name = "fancy-regex"
442+version = "0.11.0"
443+source = "registry+https://github.com/rust-lang/crates.io-index"
444+checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2"
445+dependencies = [
446+ "bit-set",
447+ "regex",
448+]
449+
450+[[package]]
451+name = "filedescriptor"
452+version = "0.8.3"
453+source = "registry+https://github.com/rust-lang/crates.io-index"
454+checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d"
455+dependencies = [
456+ "libc",
457+ "thiserror 1.0.69",
458+ "winapi",
459+]
460+
461+[[package]]
462+name = "find-msvc-tools"
463+version = "0.1.15"
464+source = "registry+https://github.com/rust-lang/crates.io-index"
465+checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48"
466+
467+[[package]]
468+name = "finl_unicode"
469+version = "1.5.0"
470+source = "registry+https://github.com/rust-lang/crates.io-index"
471+checksum = "80bb028c8b4148c9ee0cca68fcd9add6044e81d3619f48577ddf13a263d047a2"
472+
473+[[package]]
474+name = "fixedbitset"
475+version = "0.4.2"
476+source = "registry+https://github.com/rust-lang/crates.io-index"
477+checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
478+
479+[[package]]
480+name = "fnv"
481+version = "1.0.7"
482+source = "registry+https://github.com/rust-lang/crates.io-index"
483+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
484+
485+[[package]]
486+name = "foldhash"
487+version = "0.2.0"
488+source = "registry+https://github.com/rust-lang/crates.io-index"
489+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
490+
491+[[package]]
492+name = "futures-core"
493+version = "0.3.34"
494+source = "registry+https://github.com/rust-lang/crates.io-index"
495+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
496+
497+[[package]]
498+name = "futures-task"
499+version = "0.3.34"
500+source = "registry+https://github.com/rust-lang/crates.io-index"
501+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
502+
503+[[package]]
504+name = "futures-util"
505+version = "0.3.34"
506+source = "registry+https://github.com/rust-lang/crates.io-index"
507+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
508+dependencies = [
509+ "futures-core",
510+ "futures-task",
511+ "pin-project-lite",
512+ "slab",
513+]
514+
515+[[package]]
516+name = "generic-array"
517+version = "0.14.7"
518+source = "registry+https://github.com/rust-lang/crates.io-index"
519+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
520+dependencies = [
521+ "typenum",
522+ "version_check",
523+]
524+
525+[[package]]
526+name = "getrandom"
527+version = "0.3.4"
528+source = "registry+https://github.com/rust-lang/crates.io-index"
529+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
530+dependencies = [
531+ "cfg-if",
532+ "libc",
533+ "r-efi 5.3.0",
534+ "wasip2",
535+]
536+
537+[[package]]
538+name = "getrandom"
539+version = "0.4.3"
540+source = "registry+https://github.com/rust-lang/crates.io-index"
541+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
542+dependencies = [
543+ "cfg-if",
544+ "libc",
545+ "r-efi 6.0.0",
546+]
547+
548+[[package]]
549+name = "hashbrown"
550+version = "0.17.1"
551+source = "registry+https://github.com/rust-lang/crates.io-index"
552+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
553+dependencies = [
554+ "allocator-api2",
555+ "equivalent",
556+ "foldhash",
557+]
558+
559+[[package]]
560+name = "heck"
561+version = "0.5.0"
562+source = "registry+https://github.com/rust-lang/crates.io-index"
563+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
564+
565+[[package]]
566+name = "hex"
567+version = "0.4.3"
568+source = "registry+https://github.com/rust-lang/crates.io-index"
569+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
570+
571+[[package]]
572+name = "i8051"
573+version = "0.1.0"
574+dependencies = [
575+ "clap",
576+ "ratatui",
577+ "ratatui-textarea",
578+]
579+
580+[[package]]
581+name = "ident_case"
582+version = "1.0.1"
583+source = "registry+https://github.com/rust-lang/crates.io-index"
584+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
585+
586+[[package]]
587+name = "indoc"
588+version = "2.0.8"
589+source = "registry+https://github.com/rust-lang/crates.io-index"
590+checksum = "a37b2691796cffeb8a8cd305ac66e65841559f147f4e63231d0eafa4db5384d1"
591+dependencies = [
592+ "rustversion",
593+]
594+
595+[[package]]
596+name = "instability"
597+version = "0.3.14"
598+source = "registry+https://github.com/rust-lang/crates.io-index"
599+checksum = "4c3b5acc1e2fd9375041a388da33d1eb8aed5f7a8c0dd3543e3ea2805adfbe20"
600+dependencies = [
601+ "darling",
602+ "indoc",
603+ "proc-macro2",
604+ "quote",
605+ "syn 3.0.9",
186606 ]
607+
608+[[package]]
609+name = "is_terminal_polyfill"
610+version = "1.70.2"
611+source = "registry+https://github.com/rust-lang/crates.io-index"
612+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
613+
614+[[package]]
615+name = "itertools"
616+version = "0.14.0"
617+source = "registry+https://github.com/rust-lang/crates.io-index"
618+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
619+dependencies = [
620+ "either",
621+]
622+
623+[[package]]
624+name = "itoa"
625+version = "1.0.18"
626+source = "registry+https://github.com/rust-lang/crates.io-index"
627+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
628+
629+[[package]]
630+name = "js-sys"
631+version = "0.3.106"
632+source = "registry+https://github.com/rust-lang/crates.io-index"
633+checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
634+dependencies = [
635+ "cfg-if",
636+ "futures-util",
637+ "wasm-bindgen",
638+]
639+
640+[[package]]
641+name = "kasuari"
642+version = "0.4.13"
643+source = "registry+https://github.com/rust-lang/crates.io-index"
644+checksum = "72f14c90e40a6133f4805b83542a7ed7364f56070b447f56e7f4e90297122209"
645+dependencies = [
646+ "hashbrown",
647+ "portable-atomic",
648+ "thiserror 2.0.21",
649+]
650+
651+[[package]]
652+name = "lab"
653+version = "0.11.0"
654+source = "registry+https://github.com/rust-lang/crates.io-index"
655+checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f"
656+
657+[[package]]
658+name = "lazy_static"
659+version = "1.5.1"
660+source = "registry+https://github.com/rust-lang/crates.io-index"
661+checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
662+
663+[[package]]
664+name = "libc"
665+version = "0.2.191"
666+source = "registry+https://github.com/rust-lang/crates.io-index"
667+checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
668+
669+[[package]]
670+name = "libm"
671+version = "0.2.16"
672+source = "registry+https://github.com/rust-lang/crates.io-index"
673+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
674+
675+[[package]]
676+name = "line-clipping"
677+version = "0.3.8"
678+source = "registry+https://github.com/rust-lang/crates.io-index"
679+checksum = "e752191d037c44ad111a8caa762921926658402f01cc1253f7bef2020ece4f5e"
680+dependencies = [
681+ "bitflags 2.13.2",
682+]
683+
684+[[package]]
685+name = "linux-raw-sys"
686+version = "0.12.1"
687+source = "registry+https://github.com/rust-lang/crates.io-index"
688+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
689+
690+[[package]]
691+name = "litrs"
692+version = "1.0.1"
693+source = "registry+https://github.com/rust-lang/crates.io-index"
694+checksum = "4744e383959f0db86ede514b809b1c53251889093803c05267acc7d4e7030d70"
695+
696+[[package]]
697+name = "lock_api"
698+version = "0.4.14"
699+source = "registry+https://github.com/rust-lang/crates.io-index"
700+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
701+dependencies = [
702+ "scopeguard",
703+]
704+
705+[[package]]
706+name = "log"
707+version = "0.4.34"
708+source = "registry+https://github.com/rust-lang/crates.io-index"
709+checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
710+
711+[[package]]
712+name = "lru"
713+version = "0.18.5"
714+source = "registry+https://github.com/rust-lang/crates.io-index"
715+checksum = "ef9ac18847474e638e3702b76c65d4eb93428471a74778ef0f1be711717f89b5"
716+dependencies = [
717+ "hashbrown",
718+]
719+
720+[[package]]
721+name = "mac_address"
722+version = "1.2.1"
723+source = "registry+https://github.com/rust-lang/crates.io-index"
724+checksum = "f3d0581a75c45969c63afd2bb87b1d8b25cfcc556c7a918cf9bb13b0fe8d2381"
725+dependencies = [
726+ "nix 0.31.3",
727+ "windows-sys",
728+]
729+
730+[[package]]
731+name = "memchr"
732+version = "2.8.3"
733+source = "registry+https://github.com/rust-lang/crates.io-index"
734+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
735+
736+[[package]]
737+name = "memmem"
738+version = "0.1.1"
739+source = "registry+https://github.com/rust-lang/crates.io-index"
740+checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15"
741+
742+[[package]]
743+name = "memoffset"
744+version = "0.9.1"
745+source = "registry+https://github.com/rust-lang/crates.io-index"
746+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
747+dependencies = [
748+ "autocfg",
749+]
750+
751+[[package]]
752+name = "minimal-lexical"
753+version = "0.2.1"
754+source = "registry+https://github.com/rust-lang/crates.io-index"
755+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
756+
757+[[package]]
758+name = "mio"
759+version = "1.2.4"
760+source = "registry+https://github.com/rust-lang/crates.io-index"
761+checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee"
762+dependencies = [
763+ "libc",
764+ "log",
765+ "wasi",
766+ "windows-sys",
767+]
768+
769+[[package]]
770+name = "nix"
771+version = "0.29.0"
772+source = "registry+https://github.com/rust-lang/crates.io-index"
773+checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
774+dependencies = [
775+ "bitflags 2.13.2",
776+ "cfg-if",
777+ "cfg_aliases",
778+ "libc",
779+]
780+
781+[[package]]
782+name = "nix"
783+version = "0.31.3"
784+source = "registry+https://github.com/rust-lang/crates.io-index"
785+checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
786+dependencies = [
787+ "bitflags 2.13.2",
788+ "cfg-if",
789+ "cfg_aliases",
790+ "libc",
791+ "memoffset",
792+]
793+
794+[[package]]
795+name = "nom"
796+version = "7.1.3"
797+source = "registry+https://github.com/rust-lang/crates.io-index"
798+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
799+dependencies = [
800+ "memchr",
801+ "minimal-lexical",
802+]
803+
804+[[package]]
805+name = "num-conv"
806+version = "0.2.2"
807+source = "registry+https://github.com/rust-lang/crates.io-index"
808+checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
809+
810+[[package]]
811+name = "num-derive"
812+version = "0.4.2"
813+source = "registry+https://github.com/rust-lang/crates.io-index"
814+checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
815+dependencies = [
816+ "proc-macro2",
817+ "quote",
818+ "syn 2.0.113",
819+]
820+
821+[[package]]
822+name = "num-traits"
823+version = "0.2.19"
824+source = "registry+https://github.com/rust-lang/crates.io-index"
825+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
826+dependencies = [
827+ "autocfg",
828+]
829+
830+[[package]]
831+name = "num_threads"
832+version = "0.1.8"
833+source = "registry+https://github.com/rust-lang/crates.io-index"
834+checksum = "cabcc654086861f9e1bc5115258de62ef92c79b20f4615a1b0cc57c4e82027d4"
835+dependencies = [
836+ "libc",
837+]
838+
839+[[package]]
840+name = "once_cell"
841+version = "1.21.4"
842+source = "registry+https://github.com/rust-lang/crates.io-index"
843+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
844+
845+[[package]]
846+name = "once_cell_polyfill"
847+version = "1.70.2"
848+source = "registry+https://github.com/rust-lang/crates.io-index"
849+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
850+
851+[[package]]
852+name = "ordered-float"
853+version = "4.6.0"
854+source = "registry+https://github.com/rust-lang/crates.io-index"
855+checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951"
856+dependencies = [
857+ "num-traits",
858+]
859+
860+[[package]]
861+name = "palette"
862+version = "0.7.7"
863+source = "registry+https://github.com/rust-lang/crates.io-index"
864+checksum = "ddeed8580d347d2abf3dcf06a5f0b3dc020258338526b277847cd4248a70fc64"
865+dependencies = [
866+ "approx",
867+ "libm",
868+ "palette_derive",
869+ "palette_math",
870+]
871+
872+[[package]]
873+name = "palette_derive"
874+version = "0.7.7"
875+source = "registry+https://github.com/rust-lang/crates.io-index"
876+checksum = "88537020289b719d81be994ccf1bbf4990f477e2f69ee52fe3e45f43a02e56be"
877+dependencies = [
878+ "by_address",
879+ "proc-macro2",
880+ "quote",
881+ "syn 2.0.113",
882+]
883+
884+[[package]]
885+name = "palette_math"
886+version = "0.7.7"
887+source = "registry+https://github.com/rust-lang/crates.io-index"
888+checksum = "6e6eb142958d64335fb0e345c5b9ead2ecd6fc438c307e9d7d3c4fd428dbaf12"
889+dependencies = [
890+ "libm",
891+]
892+
893+[[package]]
894+name = "parking_lot"
895+version = "0.12.5"
896+source = "registry+https://github.com/rust-lang/crates.io-index"
897+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
898+dependencies = [
899+ "lock_api",
900+ "parking_lot_core",
901+]
902+
903+[[package]]
904+name = "parking_lot_core"
905+version = "0.9.12"
906+source = "registry+https://github.com/rust-lang/crates.io-index"
907+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
908+dependencies = [
909+ "cfg-if",
910+ "libc",
911+ "redox_syscall",
912+ "smallvec",
913+ "windows-link",
914+]
915+
916+[[package]]
917+name = "pest"
918+version = "2.9.4"
919+source = "registry+https://github.com/rust-lang/crates.io-index"
920+checksum = "fd47a460a9cbc3b3429c7e22bf0d76653e779319bae45a499ca010c9a990bb23"
921+dependencies = [
922+ "memchr",
923+ "psm",
924+ "stacker",
925+ "ucd-trie",
926+]
927+
928+[[package]]
929+name = "pest_derive"
930+version = "2.9.4"
931+source = "registry+https://github.com/rust-lang/crates.io-index"
932+checksum = "203e46697708f7519ab1608393cf89239cafb46b8e6d08a8ffe359359b11ef44"
933+dependencies = [
934+ "pest",
935+ "pest_generator",
936+]
937+
938+[[package]]
939+name = "pest_generator"
940+version = "2.9.4"
941+source = "registry+https://github.com/rust-lang/crates.io-index"
942+checksum = "e84ae6340a458d3d6cfc73cf328cf3cfe90b2484cd0de25eb66aa954615ed4d5"
943+dependencies = [
944+ "pest",
945+ "pest_meta",
946+ "proc-macro2",
947+ "quote",
948+ "syn 3.0.9",
949+]
950+
951+[[package]]
952+name = "pest_meta"
953+version = "2.9.4"
954+source = "registry+https://github.com/rust-lang/crates.io-index"
955+checksum = "b57f92f32b3dbce6598fd8f5cdf1f06a6072570b8103fa21728fa9036923e710"
956+dependencies = [
957+ "pest",
958+]
959+
960+[[package]]
961+name = "phf"
962+version = "0.11.3"
963+source = "registry+https://github.com/rust-lang/crates.io-index"
964+checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
965+dependencies = [
966+ "phf_macros",
967+ "phf_shared",
968+]
969+
970+[[package]]
971+name = "phf_codegen"
972+version = "0.11.3"
973+source = "registry+https://github.com/rust-lang/crates.io-index"
974+checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
975+dependencies = [
976+ "phf_generator",
977+ "phf_shared",
978+]
979+
980+[[package]]
981+name = "phf_generator"
982+version = "0.11.3"
983+source = "registry+https://github.com/rust-lang/crates.io-index"
984+checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
985+dependencies = [
986+ "phf_shared",
987+ "rand",
988+]
989+
990+[[package]]
991+name = "phf_macros"
992+version = "0.11.3"
993+source = "registry+https://github.com/rust-lang/crates.io-index"
994+checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
995+dependencies = [
996+ "phf_generator",
997+ "phf_shared",
998+ "proc-macro2",
999+ "quote",
1000+ "syn 2.0.113",
1001+]
1002+
1003+[[package]]
1004+name = "phf_shared"
1005+version = "0.11.3"
1006+source = "registry+https://github.com/rust-lang/crates.io-index"
1007+checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
1008+dependencies = [
1009+ "siphasher",
1010+]
1011+
1012+[[package]]
1013+name = "pin-project-lite"
1014+version = "0.2.17"
1015+source = "registry+https://github.com/rust-lang/crates.io-index"
1016+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
1017+
1018+[[package]]
1019+name = "portable-atomic"
1020+version = "1.15.0"
1021+source = "registry+https://github.com/rust-lang/crates.io-index"
1022+checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
1023+
1024+[[package]]
1025+name = "powerfmt"
1026+version = "0.2.1"
1027+source = "registry+https://github.com/rust-lang/crates.io-index"
1028+checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958"
1029+
1030+[[package]]
1031+name = "proc-macro2"
1032+version = "1.0.104"
1033+source = "registry+https://github.com/rust-lang/crates.io-index"
1034+checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0"
1035+dependencies = [
1036+ "unicode-ident",
1037+]
1038+
1039+[[package]]
1040+name = "psm"
1041+version = "0.1.24"
1042+source = "registry+https://github.com/rust-lang/crates.io-index"
1043+checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810"
1044+dependencies = [
1045+ "cc",
1046+]
1047+
1048+[[package]]
1049+name = "quote"
1050+version = "1.0.42"
1051+source = "registry+https://github.com/rust-lang/crates.io-index"
1052+checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
1053+dependencies = [
1054+ "proc-macro2",
1055+]
1056+
1057+[[package]]
1058+name = "r-efi"
1059+version = "5.3.0"
1060+source = "registry+https://github.com/rust-lang/crates.io-index"
1061+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
1062+
1063+[[package]]
1064+name = "r-efi"
1065+version = "6.0.0"
1066+source = "registry+https://github.com/rust-lang/crates.io-index"
1067+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
1068+
1069+[[package]]
1070+name = "rand"
1071+version = "0.8.8"
1072+source = "registry+https://github.com/rust-lang/crates.io-index"
1073+checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c"
1074+dependencies = [
1075+ "rand_core",
1076+]
1077+
1078+[[package]]
1079+name = "rand_core"
1080+version = "0.6.4"
1081+source = "registry+https://github.com/rust-lang/crates.io-index"
1082+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
1083+
1084+[[package]]
1085+name = "ratatui"
1086+version = "0.30.2"
1087+source = "registry+https://github.com/rust-lang/crates.io-index"
1088+checksum = "3274ba0a2c5e1bcad2a2005d20f4dc59dad26b2eb0940fb094500dba4099d57d"
1089+dependencies = [
1090+ "instability",
1091+ "ratatui-core",
1092+ "ratatui-crossterm",
1093+ "ratatui-macros",
1094+ "ratatui-termina",
1095+ "ratatui-termwiz",
1096+ "ratatui-widgets",
1097+ "serde",
1098+]
1099+
1100+[[package]]
1101+name = "ratatui-core"
1102+version = "0.1.2"
1103+source = "registry+https://github.com/rust-lang/crates.io-index"
1104+checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c"
1105+dependencies = [
1106+ "bitflags 2.13.2",
1107+ "compact_str",
1108+ "critical-section",
1109+ "hashbrown",
1110+ "itertools",
1111+ "kasuari",
1112+ "lru",
1113+ "palette",
1114+ "serde",
1115+ "strum",
1116+ "thiserror 2.0.21",
1117+ "unicode-segmentation",
1118+ "unicode-truncate",
1119+ "unicode-width",
1120+]
1121+
1122+[[package]]
1123+name = "ratatui-crossterm"
1124+version = "0.1.2"
1125+source = "registry+https://github.com/rust-lang/crates.io-index"
1126+checksum = "567584a3b0e6a8203c23de40b4861497266725eb5363dbfd18a1edd603cca9f0"
1127+dependencies = [
1128+ "cfg-if",
1129+ "crossterm",
1130+ "instability",
1131+ "ratatui-core",
1132+]
1133+
1134+[[package]]
1135+name = "ratatui-macros"
1136+version = "0.7.2"
1137+source = "registry+https://github.com/rust-lang/crates.io-index"
1138+checksum = "ed7dc68daa7498a43e4d68e0eb078427e10c38fbcfbb1e42d955f1fa2140d814"
1139+dependencies = [
1140+ "ratatui-core",
1141+ "ratatui-widgets",
1142+]
1143+
1144+[[package]]
1145+name = "ratatui-termina"
1146+version = "0.1.0"
1147+source = "registry+https://github.com/rust-lang/crates.io-index"
1148+checksum = "c0bf912d9e66f057a759d92e386a280ea886b352ab757d6ac4d653c7ed2c43c2"
1149+dependencies = [
1150+ "instability",
1151+ "ratatui-core",
1152+ "termina",
1153+]
1154+
1155+[[package]]
1156+name = "ratatui-termwiz"
1157+version = "0.1.2"
1158+source = "registry+https://github.com/rust-lang/crates.io-index"
1159+checksum = "faf03e0380b7744054d6cb74224fe3adf062a029754933f575ca1e3b4c2ce977"
1160+dependencies = [
1161+ "ratatui-core",
1162+ "termwiz",
1163+]
1164+
1165+[[package]]
1166+name = "ratatui-textarea"
1167+version = "0.9.3"
1168+source = "registry+https://github.com/rust-lang/crates.io-index"
1169+checksum = "78cdce79220909bec1bb8307c29fad7d0df7434280da6ecaf44cb208393bed00"
1170+dependencies = [
1171+ "ratatui-core",
1172+ "ratatui-crossterm",
1173+ "ratatui-widgets",
1174+ "unicode-segmentation",
1175+ "unicode-width",
1176+]
1177+
1178+[[package]]
1179+name = "ratatui-widgets"
1180+version = "0.3.2"
1181+source = "registry+https://github.com/rust-lang/crates.io-index"
1182+checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1"
1183+dependencies = [
1184+ "bitflags 2.13.2",
1185+ "hashbrown",
1186+ "indoc",
1187+ "instability",
1188+ "itertools",
1189+ "line-clipping",
1190+ "ratatui-core",
1191+ "serde",
1192+ "strum",
1193+ "time",
1194+ "unicode-segmentation",
1195+ "unicode-width",
1196+]
1197+
1198+[[package]]
1199+name = "redox_syscall"
1200+version = "0.5.18"
1201+source = "registry+https://github.com/rust-lang/crates.io-index"
1202+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
1203+dependencies = [
1204+ "bitflags 2.13.2",
1205+]
1206+
1207+[[package]]
1208+name = "regex"
1209+version = "1.13.1"
1210+source = "registry+https://github.com/rust-lang/crates.io-index"
1211+checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
1212+dependencies = [
1213+ "aho-corasick",
1214+ "memchr",
1215+ "regex-automata",
1216+ "regex-syntax",
1217+]
1218+
1219+[[package]]
1220+name = "regex-automata"
1221+version = "0.4.18"
1222+source = "registry+https://github.com/rust-lang/crates.io-index"
1223+checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
1224+dependencies = [
1225+ "aho-corasick",
1226+ "memchr",
1227+ "regex-syntax",
1228+]
1229+
1230+[[package]]
1231+name = "regex-syntax"
1232+version = "0.8.11"
1233+source = "registry+https://github.com/rust-lang/crates.io-index"
1234+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
1235+
1236+[[package]]
1237+name = "rustc_version"
1238+version = "0.4.1"
1239+source = "registry+https://github.com/rust-lang/crates.io-index"
1240+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
1241+dependencies = [
1242+ "semver",
1243+]
1244+
1245+[[package]]
1246+name = "rustix"
1247+version = "1.1.5"
1248+source = "registry+https://github.com/rust-lang/crates.io-index"
1249+checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"
1250+dependencies = [
1251+ "bitflags 2.13.2",
1252+ "errno",
1253+ "libc",
1254+ "linux-raw-sys",
1255+ "windows-sys",
1256+]
1257+
1258+[[package]]
1259+name = "rustversion"
1260+version = "1.0.23"
1261+source = "registry+https://github.com/rust-lang/crates.io-index"
1262+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
1263+
1264+[[package]]
1265+name = "ryu"
1266+version = "1.0.23"
1267+source = "registry+https://github.com/rust-lang/crates.io-index"
1268+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
1269+
1270+[[package]]
1271+name = "scopeguard"
1272+version = "1.2.0"
1273+source = "registry+https://github.com/rust-lang/crates.io-index"
1274+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
1275+
1276+[[package]]
1277+name = "semver"
1278+version = "1.0.28"
1279+source = "registry+https://github.com/rust-lang/crates.io-index"
1280+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
1281+
1282+[[package]]
1283+name = "serde"
1284+version = "1.0.229"
1285+source = "registry+https://github.com/rust-lang/crates.io-index"
1286+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
1287+dependencies = [
1288+ "serde_core",
1289+ "serde_derive",
1290+]
1291+
1292+[[package]]
1293+name = "serde_core"
1294+version = "1.0.229"
1295+source = "registry+https://github.com/rust-lang/crates.io-index"
1296+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
1297+dependencies = [
1298+ "serde_derive",
1299+]
1300+
1301+[[package]]
1302+name = "serde_derive"
1303+version = "1.0.229"
1304+source = "registry+https://github.com/rust-lang/crates.io-index"
1305+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
1306+dependencies = [
1307+ "proc-macro2",
1308+ "quote",
1309+ "syn 3.0.9",
1310+]
1311+
1312+[[package]]
1313+name = "sha2"
1314+version = "0.10.9"
1315+source = "registry+https://github.com/rust-lang/crates.io-index"
1316+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
1317+dependencies = [
1318+ "cfg-if",
1319+ "cpufeatures",
1320+ "digest",
1321+]
1322+
1323+[[package]]
1324+name = "shlex"
1325+version = "2.0.1"
1326+source = "registry+https://github.com/rust-lang/crates.io-index"
1327+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
1328+
1329+[[package]]
1330+name = "signal-hook"
1331+version = "0.3.18"
1332+source = "registry+https://github.com/rust-lang/crates.io-index"
1333+checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
1334+dependencies = [
1335+ "libc",
1336+ "signal-hook-registry",
1337+]
1338+
1339+[[package]]
1340+name = "signal-hook-mio"
1341+version = "0.2.5"
1342+source = "registry+https://github.com/rust-lang/crates.io-index"
1343+checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
1344+dependencies = [
1345+ "libc",
1346+ "mio",
1347+ "signal-hook",
1348+]
1349+
1350+[[package]]
1351+name = "signal-hook-registry"
1352+version = "1.4.8"
1353+source = "registry+https://github.com/rust-lang/crates.io-index"
1354+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
1355+dependencies = [
1356+ "errno",
1357+ "libc",
1358+]
1359+
1360+[[package]]
1361+name = "siphasher"
1362+version = "1.0.4"
1363+source = "registry+https://github.com/rust-lang/crates.io-index"
1364+checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256"
1365+
1366+[[package]]
1367+name = "slab"
1368+version = "0.4.12"
1369+source = "registry+https://github.com/rust-lang/crates.io-index"
1370+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
1371+
1372+[[package]]
1373+name = "smallvec"
1374+version = "1.16.3"
1375+source = "registry+https://github.com/rust-lang/crates.io-index"
1376+checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b"
1377+
1378+[[package]]
1379+name = "stacker"
1380+version = "0.1.25"
1381+source = "registry+https://github.com/rust-lang/crates.io-index"
1382+checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967"
1383+dependencies = [
1384+ "cc",
1385+ "cfg-if",
1386+ "libc",
1387+ "psm",
1388+ "windows-sys",
1389+]
1390+
1391+[[package]]
1392+name = "static_assertions"
1393+version = "1.1.0"
1394+source = "registry+https://github.com/rust-lang/crates.io-index"
1395+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
1396+
1397+[[package]]
1398+name = "strsim"
1399+version = "0.11.1"
1400+source = "registry+https://github.com/rust-lang/crates.io-index"
1401+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
1402+
1403+[[package]]
1404+name = "strum"
1405+version = "0.28.0"
1406+source = "registry+https://github.com/rust-lang/crates.io-index"
1407+checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
1408+dependencies = [
1409+ "strum_macros",
1410+]
1411+
1412+[[package]]
1413+name = "strum_macros"
1414+version = "0.28.0"
1415+source = "registry+https://github.com/rust-lang/crates.io-index"
1416+checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"
1417+dependencies = [
1418+ "heck",
1419+ "proc-macro2",
1420+ "quote",
1421+ "syn 2.0.113",
1422+]
1423+
1424+[[package]]
1425+name = "syn"
1426+version = "1.0.109"
1427+source = "registry+https://github.com/rust-lang/crates.io-index"
1428+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
1429+dependencies = [
1430+ "proc-macro2",
1431+ "quote",
1432+ "unicode-ident",
1433+]
1434+
1435+[[package]]
1436+name = "syn"
1437+version = "2.0.113"
1438+source = "registry+https://github.com/rust-lang/crates.io-index"
1439+checksum = "678faa00651c9eb72dd2020cbdf275d92eccb2400d568e419efdd64838145cb4"
1440+dependencies = [
1441+ "proc-macro2",
1442+ "quote",
1443+ "unicode-ident",
1444+]
1445+
1446+[[package]]
1447+name = "syn"
1448+version = "3.0.9"
1449+source = "registry+https://github.com/rust-lang/crates.io-index"
1450+checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
1451+dependencies = [
1452+ "proc-macro2",
1453+ "quote",
1454+ "unicode-ident",
1455+]
1456+
1457+[[package]]
1458+name = "termina"
1459+version = "0.3.3"
1460+source = "registry+https://github.com/rust-lang/crates.io-index"
1461+checksum = "9048a889effe34a5cddee0af7f53285198b16dca3be510858d38dfdb3e62a04e"
1462+dependencies = [
1463+ "bitflags 2.13.2",
1464+ "parking_lot",
1465+ "rustix",
1466+ "signal-hook",
1467+ "windows-sys",
1468+]
1469+
1470+[[package]]
1471+name = "terminfo"
1472+version = "0.9.0"
1473+source = "registry+https://github.com/rust-lang/crates.io-index"
1474+checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662"
1475+dependencies = [
1476+ "fnv",
1477+ "nom",
1478+ "phf",
1479+ "phf_codegen",
1480+]
1481+
1482+[[package]]
1483+name = "termios"
1484+version = "0.3.3"
1485+source = "registry+https://github.com/rust-lang/crates.io-index"
1486+checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b"
1487+dependencies = [
1488+ "libc",
1489+]
1490+
1491+[[package]]
1492+name = "termwiz"
1493+version = "0.23.3"
1494+source = "registry+https://github.com/rust-lang/crates.io-index"
1495+checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7"
1496+dependencies = [
1497+ "anyhow",
1498+ "base64",
1499+ "bitflags 2.13.2",
1500+ "fancy-regex",
1501+ "filedescriptor",
1502+ "finl_unicode",
1503+ "fixedbitset",
1504+ "hex",
1505+ "lazy_static",
1506+ "libc",
1507+ "log",
1508+ "memmem",
1509+ "nix 0.29.0",
1510+ "num-derive",
1511+ "num-traits",
1512+ "ordered-float",
1513+ "pest",
1514+ "pest_derive",
1515+ "phf",
1516+ "sha2",
1517+ "signal-hook",
1518+ "siphasher",
1519+ "terminfo",
1520+ "termios",
1521+ "thiserror 1.0.69",
1522+ "ucd-trie",
1523+ "unicode-segmentation",
1524+ "vtparse",
1525+ "wezterm-bidi",
1526+ "wezterm-blob-leases",
1527+ "wezterm-color-types",
1528+ "wezterm-dynamic",
1529+ "wezterm-input-types",
1530+ "winapi",
1531+]
1532+
1533+[[package]]
1534+name = "thiserror"
1535+version = "1.0.69"
1536+source = "registry+https://github.com/rust-lang/crates.io-index"
1537+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
1538+dependencies = [
1539+ "thiserror-impl 1.0.69",
1540+]
1541+
1542+[[package]]
1543+name = "thiserror"
1544+version = "2.0.21"
1545+source = "registry+https://github.com/rust-lang/crates.io-index"
1546+checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
1547+dependencies = [
1548+ "thiserror-impl 2.0.21",
1549+]
1550+
1551+[[package]]
1552+name = "thiserror-impl"
1553+version = "1.0.69"
1554+source = "registry+https://github.com/rust-lang/crates.io-index"
1555+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
1556+dependencies = [
1557+ "proc-macro2",
1558+ "quote",
1559+ "syn 2.0.113",
1560+]
1561+
1562+[[package]]
1563+name = "thiserror-impl"
1564+version = "2.0.21"
1565+source = "registry+https://github.com/rust-lang/crates.io-index"
1566+checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
1567+dependencies = [
1568+ "proc-macro2",
1569+ "quote",
1570+ "syn 3.0.9",
1571+]
1572+
1573+[[package]]
1574+name = "time"
1575+version = "0.3.55"
1576+source = "registry+https://github.com/rust-lang/crates.io-index"
1577+checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
1578+dependencies = [
1579+ "deranged",
1580+ "libc",
1581+ "num-conv",
1582+ "num_threads",
1583+ "powerfmt",
1584+ "serde_core",
1585+ "time-core",
1586+]
1587+
1588+[[package]]
1589+name = "time-core"
1590+version = "0.1.9"
1591+source = "registry+https://github.com/rust-lang/crates.io-index"
1592+checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
1593+
1594+[[package]]
1595+name = "typenum"
1596+version = "1.20.1"
1597+source = "registry+https://github.com/rust-lang/crates.io-index"
1598+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
1599+
1600+[[package]]
1601+name = "ucd-trie"
1602+version = "0.1.7"
1603+source = "registry+https://github.com/rust-lang/crates.io-index"
1604+checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
1605+
1606+[[package]]
1607+name = "unicode-ident"
1608+version = "1.0.22"
1609+source = "registry+https://github.com/rust-lang/crates.io-index"
1610+checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
1611+
1612+[[package]]
1613+name = "unicode-segmentation"
1614+version = "1.13.3"
1615+source = "registry+https://github.com/rust-lang/crates.io-index"
1616+checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
1617+
1618+[[package]]
1619+name = "unicode-truncate"
1620+version = "2.0.1"
1621+source = "registry+https://github.com/rust-lang/crates.io-index"
1622+checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5"
1623+dependencies = [
1624+ "itertools",
1625+ "unicode-segmentation",
1626+ "unicode-width",
1627+]
1628+
1629+[[package]]
1630+name = "unicode-width"
1631+version = "0.2.2"
1632+source = "registry+https://github.com/rust-lang/crates.io-index"
1633+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
1634+
1635+[[package]]
1636+name = "utf8parse"
1637+version = "0.2.2"
1638+source = "registry+https://github.com/rust-lang/crates.io-index"
1639+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
1640+
1641+[[package]]
1642+name = "uuid"
1643+version = "1.28.0"
1644+source = "registry+https://github.com/rust-lang/crates.io-index"
1645+checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606"
1646+dependencies = [
1647+ "atomic",
1648+ "getrandom 0.4.3",
1649+ "js-sys",
1650+ "wasm-bindgen",
1651+]
1652+
1653+[[package]]
1654+name = "version_check"
1655+version = "0.9.5"
1656+source = "registry+https://github.com/rust-lang/crates.io-index"
1657+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
1658+
1659+[[package]]
1660+name = "vtparse"
1661+version = "0.6.2"
1662+source = "registry+https://github.com/rust-lang/crates.io-index"
1663+checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0"
1664+dependencies = [
1665+ "utf8parse",
1666+]
1667+
1668+[[package]]
1669+name = "wasi"
1670+version = "0.11.1+wasi-snapshot-preview1"
1671+source = "registry+https://github.com/rust-lang/crates.io-index"
1672+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
1673+
1674+[[package]]
1675+name = "wasip2"
1676+version = "1.0.4+wasi-0.2.12"
1677+source = "registry+https://github.com/rust-lang/crates.io-index"
1678+checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
1679+dependencies = [
1680+ "wit-bindgen",
1681+]
1682+
1683+[[package]]
1684+name = "wasm-bindgen"
1685+version = "0.2.129"
1686+source = "registry+https://github.com/rust-lang/crates.io-index"
1687+checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
1688+dependencies = [
1689+ "cfg-if",
1690+ "once_cell",
1691+ "rustversion",
1692+ "wasm-bindgen-macro",
1693+ "wasm-bindgen-shared",
1694+]
1695+
1696+[[package]]
1697+name = "wasm-bindgen-macro"
1698+version = "0.2.129"
1699+source = "registry+https://github.com/rust-lang/crates.io-index"
1700+checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
1701+dependencies = [
1702+ "quote",
1703+ "wasm-bindgen-macro-support",
1704+]
1705+
1706+[[package]]
1707+name = "wasm-bindgen-macro-support"
1708+version = "0.2.129"
1709+source = "registry+https://github.com/rust-lang/crates.io-index"
1710+checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
1711+dependencies = [
1712+ "bumpalo",
1713+ "proc-macro2",
1714+ "quote",
1715+ "syn 3.0.9",
1716+ "wasm-bindgen-shared",
1717+]
1718+
1719+[[package]]
1720+name = "wasm-bindgen-shared"
1721+version = "0.2.129"
1722+source = "registry+https://github.com/rust-lang/crates.io-index"
1723+checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
1724+dependencies = [
1725+ "unicode-ident",
1726+]
1727+
1728+[[package]]
1729+name = "wezterm-bidi"
1730+version = "0.2.3"
1731+source = "registry+https://github.com/rust-lang/crates.io-index"
1732+checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec"
1733+dependencies = [
1734+ "log",
1735+ "wezterm-dynamic",
1736+]
1737+
1738+[[package]]
1739+name = "wezterm-blob-leases"
1740+version = "0.1.1"
1741+source = "registry+https://github.com/rust-lang/crates.io-index"
1742+checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7"
1743+dependencies = [
1744+ "getrandom 0.3.4",
1745+ "mac_address",
1746+ "sha2",
1747+ "thiserror 1.0.69",
1748+ "uuid",
1749+]
1750+
1751+[[package]]
1752+name = "wezterm-color-types"
1753+version = "0.3.0"
1754+source = "registry+https://github.com/rust-lang/crates.io-index"
1755+checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296"
1756+dependencies = [
1757+ "csscolorparser",
1758+ "deltae",
1759+ "lazy_static",
1760+ "wezterm-dynamic",
1761+]
1762+
1763+[[package]]
1764+name = "wezterm-dynamic"
1765+version = "0.2.1"
1766+source = "registry+https://github.com/rust-lang/crates.io-index"
1767+checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac"
1768+dependencies = [
1769+ "log",
1770+ "ordered-float",
1771+ "strsim",
1772+ "thiserror 1.0.69",
1773+ "wezterm-dynamic-derive",
1774+]
1775+
1776+[[package]]
1777+name = "wezterm-dynamic-derive"
1778+version = "0.1.1"
1779+source = "registry+https://github.com/rust-lang/crates.io-index"
1780+checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b"
1781+dependencies = [
1782+ "proc-macro2",
1783+ "quote",
1784+ "syn 1.0.109",
1785+]
1786+
1787+[[package]]
1788+name = "wezterm-input-types"
1789+version = "0.1.0"
1790+source = "registry+https://github.com/rust-lang/crates.io-index"
1791+checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e"
1792+dependencies = [
1793+ "bitflags 1.3.2",
1794+ "euclid",
1795+ "lazy_static",
1796+ "serde",
1797+ "wezterm-dynamic",
1798+]
1799+
1800+[[package]]
1801+name = "winapi"
1802+version = "0.3.9"
1803+source = "registry+https://github.com/rust-lang/crates.io-index"
1804+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
1805+dependencies = [
1806+ "winapi-i686-pc-windows-gnu",
1807+ "winapi-x86_64-pc-windows-gnu",
1808+]
1809+
1810+[[package]]
1811+name = "winapi-i686-pc-windows-gnu"
1812+version = "0.4.0"
1813+source = "registry+https://github.com/rust-lang/crates.io-index"
1814+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
1815+
1816+[[package]]
1817+name = "winapi-x86_64-pc-windows-gnu"
1818+version = "0.4.0"
1819+source = "registry+https://github.com/rust-lang/crates.io-index"
1820+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
1821+
1822+[[package]]
1823+name = "windows-link"
1824+version = "0.2.1"
1825+source = "registry+https://github.com/rust-lang/crates.io-index"
1826+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
1827+
1828+[[package]]
1829+name = "windows-sys"
1830+version = "0.61.2"
1831+source = "registry+https://github.com/rust-lang/crates.io-index"
1832+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
1833+dependencies = [
1834+ "windows-link",
1835+]
1836+
1837+[[package]]
1838+name = "wit-bindgen"
1839+version = "0.57.1"
1840+source = "registry+https://github.com/rust-lang/crates.io-index"
1841+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
M Cargo.toml View
@@ -1,7 +1,10 @@
11 [package]
2−name = "cpu-emulator"
2+name = "i8051"
33 version = "0.1.0"
44 edition = "2024"
5+description = "A TUI Intel 8051 assembler, simulator and debugger"
56
67 [dependencies]
78 clap = { version = "4.5.54", features = ["derive"] }
9+ratatui = "0.30"
10+ratatui-textarea = "0.9"
M README.md View
@@ -1,113 +1,110 @@
1−# 8-Bit CPU Emulator
1+# i8051 — a TUI 8051 assembler, simulator & debugger
22
3−This project is an educational 8-bit CPU emulator written in Rust, it's build to understand core computer architecture concepts such as instruction sets, flags,
4−control flow, stacks and system calls.
3+A small, Keil µVision-style IDE for the Intel 8051 that runs in your terminal.
4+Write assembly in the built-in editor, assemble it, inspect the listing and
5+Intel HEX output, then run, pause and single-step the program while watching
6+registers, SFRs, memory, ports, timers, interrupts and the serial port.
57
6−This project is **not** a production CPU and **not** accurate. It is designed purely for learning and experimentation.
8+Built in Rust with [ratatui](https://ratatui.rs). It is a learning tool, not a
9+cycle-exact replacement for a vendor simulator.
710
811 -----
912
1013 ## Features
1114
12−- Custom 8-bit ISA (control-flow and arithmetic operations)
13−- 4 general-purpose registers
14−- 64 KB (0x0000-0xFFFF) byte-addressable memory
15−- Two-pass assembler with label support
16−- Stack-based function calls
17−- Stack is software-managed and grows downward
15+- **Editor** with undo/redo, breakpoint and error markers in the gutter —
16+ or press Ctrl-G to edit in your own `$EDITOR` (vim, nvim, nano, …).
17+- **Two-pass assembler** for the full 8051 instruction set (all 255 opcodes):
18+ - labels, expressions (`+ - * / MOD AND OR XOR NOT SHL SHR HIGH LOW`, `$`)
19+ - number formats `42`, `2Ah`, `0x2A`, `101010b`, `52o`, `'*'`
20+ - bit addressing (`P1.3`, `ACC.7`, `20h.0`) and all predefined SFR/bit names
21+ - directives `ORG DB DW DS END EQU SET BIT DATA IDATA XDATA CODE CSEG AT`
22+ - generic `JMP`/`CALL` (assembled as `LJMP`/`LCALL`)
23+ - Keil-style `.lst` listing with symbol table, Intel HEX output
24+- **Simulator**
25+ - all instructions with correct flags (CY, AC, OV, P) and machine-cycle counts
26+ - 4 register banks, 256 bytes IRAM, SFRs, 64K code, 64K XRAM
27+ - ports P0–P3 with latch vs. pin semantics (read-modify-write reads the latch)
28+ - timers 0/1 in modes 0–3, gate and counter inputs
29+ - interrupts: INT0, T0, INT1, T1, serial; two priority levels; edge/level triggers
30+ - UART in modes 0–3 with baud rate timing from timer 1
31+ - idle and power-down modes (PCON)
32+- **Debugger**: run / pause, step into / over / out, run to cursor,
33+ breakpoints, changed-value highlighting, memory poke, adjustable speed.
34+- **Open `.hex` files**: they're disassembled into editable source that
35+ re-assembles to the identical image.
1836
19−---
20−
21−
22−
23−## Registers
24−| Register | Size | Description |
25−| -------- | ------ | ------------------------------ |
26−| A | 8-bit | General-purpose register |
27−| B | 8-bit | General-purpose register |
28−| C | 8-bit | General-purpose register |
29−| D | 8-bit | General-purpose register |
30−| PC | 16-bit | Program Counter |
31−| SP | 16-bit | Stack pointer |
32−
33−
34−## Flags Register
35−
36−The CPU maintains a small flags register.
37−
38−| Flag | Description |
39−| ----- | ------------ |
40−| Z | Zero flag - set if last result was `0` |
41−| C | Carry/Borrow flag |
42−
43−- Control-flow instructions do **not** modify flags.
44−
45−## Supported Instructions
46−
47−| Instruction | Syntax |
48−| ----------- | ---------------- |
49−| MOV | mov dest, src OR mov reg, imm |
50−| ADD | add r1, r2 OR add reg, imm |
51−| SUB | sub r1, r2 OR sub reg, imm |
52−| JMP | jmp addr OR jmp addr |
53−| JZ | jz addr OR jz addr |
54−| JNZ | jnz addr OR jnz addr |
55−| CMP | cmp r1, r2 OR cmp reg, imp |
56−| MUL | mul r1, r2 |
57−| DIV | div r1, r2 |
58−| CALL | call \<label\> |
59−| SYS | sys \<syscall_no\> |
60−| HLT (Halt) | hlt |
61−
62−## Syscalls
63−
64−| Imm | Meaning |
65−| --- | ------- |
66−| 0 | Exit Program |
67−| 1 | Print register A as integer |
68−| 2 | Print register A as ASCII char |
69−
70−- Syscalls also do not modify flags
71−
72−## Example assembly program
73−
74−```assembly
75−mov b, 3
76−mov a, 1
37+## Usage
7738
78−loop:
79− sub b, a
80− jnz loop
39+```bash
40+cargo run --release -- examples/serial_hello.asm # open in the TUI
41+cargo run --release # start with a template
42+cargo run --release -- firmware.hex # disassemble & debug a HEX file
8143
82−sys 1 ; print register A
83−sys 0 ; exit
44+# Headless assembly: writes prog.hex and prog.lst
45+cargo run --release -- --assemble prog.asm
46+cargo run --release -- --assemble prog.asm -o out.hex --lst out.lst
8447 ```
8548
86−## Usage
49+`--clock <MHz>` sets the oscillator frequency used for the elapsed-time display
50+(default 11.0592).
51+
52+## Keys
53+
54+| Key | Action |
55+| --- | --- |
56+| F1 | help |
57+| F2 / Ctrl-S | save (assembling a saved file also writes `.lst` and `.hex` next to it) |
58+| F3 / Ctrl-O | open `.asm` or `.hex` |
59+| Ctrl-G | edit in `$EDITOR`; on save & exit the buffer reloads and reassembles |
60+| F4 | switch Edit ⇄ Debug |
61+| F7 | assemble, reset and load |
62+| F5 | run / pause (assembles first if the source changed) |
63+| F11 / F10 / F12 | step into / step over / step out |
64+| F8 | run to cursor |
65+| F9 | toggle breakpoint on the cursor line |
66+| F6 | reset |
67+| Ctrl-Q | quit |
68+
69+Edit mode: `Alt-1/2/3` switch between Listing, HEX and Errors; `Alt-↑/↓`,
70+`Alt-PgUp/PgDn` scroll them. The editor uses Emacs-style keys
71+(`Ctrl-U` undo, `Ctrl-R` redo, `Ctrl-Y` paste).
72+
73+Debug mode: `Tab` cycles focus between Source, Memory, Ports and Serial.
74+Letter shortcuts work too, for terminals that swallow function keys:
75+`r` run/pause, `s` step, `n` step over, `o` step out, `g` run to cursor,
76+`b` breakpoint, `x` reset, `i`/`f`/`d`/`c` show IRAM/SFR/XRAM/CODE,
77+`p` poke memory, `+`/`-` speed, `.` jump to PC, `Esc` pause / back to editor.
78+
79+- **Ports**: select a pin and press Space to pull it low from outside (a
80+ button press); the program sees the pin as `latch AND external level`.
81+- **Serial**: type a line and press Enter to send it (followed by CR) to the
82+ 8051's receiver; everything the 8051 transmits appears above.
83+
84+## Examples
85+
86+| File | Shows |
87+| --- | --- |
88+| `examples/blink.asm` | port toggling, delay loops, `ACALL`/`RET` |
89+| `examples/timer_irq.asm` | timer 0 mode 1 interrupt |
90+| `examples/serial_hello.asm` | UART setup, string output with `MOVC`, echo |
91+| `examples/button_int0.asm` | edge-triggered INT0, idle mode |
92+| `examples/bcd_add.asm` | `ADDC` and `DA A` with packed BCD |
93+| `examples/lookup_movc.asm` | 7-segment table lookup |
94+
95+## Notes and limitations
96+
97+- Indirect addressing reaches all 256 bytes of IRAM (8052-style upper 128).
98+- Serial input has no 9th data bit; in modes 2/3 `RB8` reads 0.
99+- No 8052 timer 2, external code memory or on-chip peripherals of derivatives.
100+
101+## Development
87102
88103 ```bash
89−cargo run -- --f <examples/filename.asc>
104+cargo test # assembler, CPU, peripheral and example-program tests
105+cargo clippy
90106 ```
91107
92−## Non-goals
93−
94−1. No pipelining
95−2. No interrupts
96−3. No virtual memory
97−4. No privilege levels
98−5. No hardware I/O
99−
100−## Goals
101−
102−- [x] Assembler
103− - [x] Lexer/Tokenizer
104− - [x] Add label support (supporting JMP/JZ/JNZ)
105−- [x] Add instructions
106− - [x] CMP
107− - [x] MUL
108− - [x] DIV
109− - [x] CALL
110− - [x] RET
111− - [x] SYS
112−- [ ] Better error-handling
113−- [ ] TUI debugger
108+Code layout: `src/isa.rs` (opcode table shared by everything), `src/asm/`
109+(assembler, listing, Intel HEX), `src/sim/` (CPU, peripherals, disassembler),
110+`src/ui/` (ratatui front end).
D examples/arithmetic.asm View
@@ -1,9 +0,0 @@
1−; move imm 10 to register A
2−mov a, 10
3−; move imm 5 to register B
4−mov b, 5
5−; Add a & B
6−add a, 2
7−sub a, 2
8−; Halt
9−hlt
A examples/bcd_add.asm View
@@ -0,0 +1,21 @@
1+; Packed BCD addition with DA A: 4-digit 1234 + 5678 = 6912.
2+; Watch A, CY and AC in the Registers panel while stepping.
3+
4+ ORG 0000h
5+ MOV R0,#34h ; low byte of first number
6+ MOV R1,#12h ; high byte
7+ MOV R2,#78h ; low byte of second number
8+ MOV R3,#56h ; high byte
9+
10+ MOV A,R0
11+ ADD A,R2 ; 34h + 78h = ACh (binary)
12+ DA A ; -> 12h, CY = 1
13+ MOV 30h,A
14+ MOV A,R1
15+ ADDC A,R3 ; 12h + 56h + carry
16+ DA A ; -> 69h
17+ MOV 31h,A ; result 6912 in 31h:30h
18+
19+ SJMP $
20+
21+ END
A examples/blink.asm View
@@ -0,0 +1,23 @@
1+; Blink: toggle all of P1 with a software delay loop.
2+; Step with F11, or run with F5 and watch the Ports panel.
3+
4+ ORG 0000h
5+ LJMP main
6+
7+ ORG 0030h
8+main: MOV P1,#0FFh
9+loop: CPL P1.0 ; toggle the LED on P1.0
10+ MOV A,P1
11+ CPL A
12+ MOV P2,A ; P2 mirrors P1 inverted
13+ ACALL delay
14+ SJMP loop
15+
16+; About 2 * 50 * 250 machine cycles.
17+delay: MOV R6,#50
18+d1: MOV R7,#250
19+d2: DJNZ R7,d2
20+ DJNZ R6,d1
21+ RET
22+
23+ END
A examples/button_int0.asm View
@@ -0,0 +1,20 @@
1+; External interrupt 0 (falling edge on P3.2) counts button presses into P1.
2+; In debug mode, Tab to the Ports panel, select P3 bit 2 and press Space
3+; twice (press + release) to generate one falling edge.
4+
5+ ORG 0000h
6+ LJMP main
7+
8+ ORG 0003h ; INT0 vector
9+ INC P1
10+ RETI
11+
12+ ORG 0030h
13+main: MOV P1,#00h
14+ SETB IT0 ; edge-triggered
15+ SETB EX0
16+ SETB EA
17+sleep: ORL PCON,#01h ; idle until an interrupt arrives
18+ SJMP sleep ; ...and go back to idle afterwards
19+
20+ END
D examples/call.asm View
@@ -1,7 +0,0 @@
1−mov a, 2
2−call square
3−hlt
4−
5−square:
6− mul a, a
7− ret
D examples/cmp.asm View
@@ -1,4 +0,0 @@
1−mov a, 10
2−mov b, 10
3−cmp a, b
4−hlt
D examples/div.asm View
@@ -1,4 +0,0 @@
1−mov a, 10
2−mov b, 10
3−div a, b
4−hlt
A examples/lookup_movc.asm View
@@ -0,0 +1,20 @@
1+; Count 0..9 on a common-cathode 7-segment display connected to P2,
2+; using a MOVC lookup table. The digit is also kept in R2.
3+
4+ ORG 0000h
5+ LJMP main
6+
7+ ORG 0030h
8+main: MOV DPTR,#segs
9+again: MOV R2,#0
10+next: MOV A,R2
11+ MOVC A,@A+DPTR ; table lookup
12+ MOV P2,A
13+ INC R2
14+ CJNE R2,#10,next
15+ SJMP again
16+
17+; 0 1 2 3 4 5 6 7 8 9
18+segs: DB 3Fh, 06h, 5Bh, 4Fh, 66h, 6Dh, 7Dh, 07h, 7Fh, 6Fh
19+
20+ END
D examples/loops.asm View
@@ -1,13 +0,0 @@
1−; move imm 3 to register b
2−mov b, 3
3−; move imm 1 to register a
4−mov a, 1
5−
6−; delcare a label
7−loop:
8− ; subtract a from b
9− sub b, a
10− ; jump to label "loop" until the zero flag is not set
11− jnz loop
12−
13−hlt
D examples/mul.asm View
@@ -1,4 +0,0 @@
1−mov a, 99
2−mov b, 99
3−mul a, b
4−hlt
A examples/serial_hello.asm View
@@ -0,0 +1,43 @@
1+; UART: 9600 baud at 11.0592 MHz (timer 1, mode 2, TH1 = FDh).
2+; Prints a greeting, then echoes everything typed in the Serial panel
3+; (Tab to the Serial panel in debug mode, type, press Enter).
4+
5+ ORG 0000h
6+ LJMP main
7+
8+ ORG 0030h
9+main: MOV TMOD,#20h ; timer 1: mode 2 (8-bit auto-reload)
10+ MOV TH1,#0FDh
11+ SETB TR1
12+ MOV SCON,#50h ; serial mode 1, receiver enabled
13+ MOV DPTR,#msg
14+ ACALL puts
15+echo: ACALL getc
16+ ACALL putc
17+ CJNE A,#0Dh,echo ; after CR, also send LF
18+ MOV A,#0Ah
19+ ACALL putc
20+ SJMP echo
21+
22+; Send the zero-terminated string at DPTR.
23+puts: CLR A
24+ MOVC A,@A+DPTR
25+ JZ done
26+ ACALL putc
27+ INC DPTR
28+ SJMP puts
29+done: RET
30+
31+putc: MOV SBUF,A
32+wait: JNB TI,wait
33+ CLR TI
34+ RET
35+
36+getc: JNB RI,getc
37+ MOV A,SBUF
38+ CLR RI
39+ RET
40+
41+msg: DB "Hello from the 8051!", 0Dh, 0Ah, "Type something: ", 0
42+
43+ END
D examples/syscall.asm View
@@ -1,7 +0,0 @@
1−mov a, 72 ; 'H'
2−sys 2
3−mov a, 105 ; 'i'
4−sys 2
5−mov a, 10 ; '\n'
6−sys 2
7−sys 0
D examples/table.asm View
@@ -1,9 +0,0 @@
1−; Table of 3
2−mov b, 3
3−mov c, 10 ; Counter
4−
5−loop:
6− add a, b
7− sub c, 1
8− jnz loop
9−hlt
A examples/timer_irq.asm View
@@ -0,0 +1,28 @@
1+; Timer 0 interrupt: mode 1 (16-bit) overflow every 1000 cycles toggles P1.0
2+; and counts ticks in R7. The main loop just idles.
3+
4+RELOAD EQU 65536 - 1000
5+
6+ ORG 0000h
7+ LJMP main
8+
9+ ORG 000Bh ; timer 0 overflow vector
10+ LJMP t0_isr
11+
12+ ORG 0030h
13+main: MOV SP,#5Fh
14+ MOV TMOD,#01h ; T0: mode 1, timer
15+ MOV TH0,#HIGH RELOAD
16+ MOV TL0,#LOW RELOAD
17+ SETB ET0 ; enable timer 0 interrupt
18+ SETB EA ; global interrupt enable
19+ SETB TR0 ; start timer 0
20+idle: SJMP idle
21+
22+t0_isr: MOV TH0,#HIGH RELOAD
23+ MOV TL0,#LOW RELOAD
24+ CPL P1.0
25+ INC R7
26+ RETI
27+
28+ END
A src/asm/expr.rs View
@@ -0,0 +1,251 @@
1+use super::lexer::{Tok, Token};
2+
3+#[derive(Debug, Clone)]
4+pub enum Expr {
5+ Num(i64),
6+ Sym(String, usize),
7+ /// `$`, the current location counter.
8+ Here,
9+ Neg(Box<Expr>),
10+ Not(Box<Expr>),
11+ High(Box<Expr>),
12+ Low(Box<Expr>),
13+ Bin(BinOp, Box<Expr>, Box<Expr>),
14+}
15+
16+#[derive(Debug, Clone, Copy)]
17+pub enum BinOp {
18+ Add,
19+ Sub,
20+ Mul,
21+ Div,
22+ Mod,
23+ And,
24+ Or,
25+ Xor,
26+ Shl,
27+ Shr,
28+ /// `byte.bit` bit addressing, e.g. `P1.3` or `20h.0`.
29+ Dot,
30+}
31+
32+pub enum EvalError {
33+ Undefined(String, usize),
34+ Other(String),
35+}
36+
37+pub trait Env {
38+ fn lookup(&self, name: &str) -> Option<i64>;
39+ fn here(&self) -> i64;
40+}
41+
42+impl Expr {
43+ pub fn eval(&self, env: &dyn Env) -> Result<i64, EvalError> {
44+ Ok(match self {
45+ Expr::Num(n) => *n,
46+ Expr::Here => env.here(),
47+ Expr::Sym(s, col) => env
48+ .lookup(s)
49+ .ok_or_else(|| EvalError::Undefined(s.clone(), *col))?,
50+ Expr::Neg(e) => e.eval(env)?.wrapping_neg(),
51+ Expr::Not(e) => !e.eval(env)? & 0xFFFF,
52+ Expr::High(e) => (e.eval(env)? >> 8) & 0xFF,
53+ Expr::Low(e) => e.eval(env)? & 0xFF,
54+ Expr::Bin(op, l, r) => {
55+ let (l, r) = (l.eval(env)?, r.eval(env)?);
56+ match op {
57+ BinOp::Add => l.wrapping_add(r),
58+ BinOp::Sub => l.wrapping_sub(r),
59+ BinOp::Mul => l.wrapping_mul(r),
60+ BinOp::Div | BinOp::Mod if r == 0 => {
61+ return Err(EvalError::Other("division by zero".into()));
62+ }
63+ BinOp::Div => l / r,
64+ BinOp::Mod => l % r,
65+ BinOp::And => l & r,
66+ BinOp::Or => l | r,
67+ BinOp::Xor => l ^ r,
68+ BinOp::Shl => l.wrapping_shl(r as u32),
69+ BinOp::Shr => l.wrapping_shr(r as u32),
70+ BinOp::Dot => bit_address(l, r)?,
71+ }
72+ }
73+ })
74+ }
75+}
76+
77+fn bit_address(byte: i64, bit: i64) -> Result<i64, EvalError> {
78+ if !(0..8).contains(&bit) {
79+ return Err(EvalError::Other(format!(
80+ "bit index {bit} out of range 0..7"
81+ )));
82+ }
83+ match byte {
84+ 0x20..=0x2F => Ok((byte - 0x20) * 8 + bit),
85+ 0x80..=0xFF if byte % 8 == 0 => Ok(byte + bit),
86+ _ => Err(EvalError::Other(format!(
87+ "address {byte:02X}h is not bit-addressable"
88+ ))),
89+ }
90+}
91+
92+/// Recursive-descent parser over a token slice.
93+pub struct Parser<'a> {
94+ toks: &'a [Token],
95+ pos: usize,
96+}
97+
98+type PResult<T> = Result<T, (usize, String)>;
99+
100+impl<'a> Parser<'a> {
101+ pub fn new(toks: &'a [Token]) -> Self {
102+ Self { toks, pos: 0 }
103+ }
104+
105+ /// Parses the whole slice as one expression.
106+ pub fn parse_all(toks: &'a [Token]) -> PResult<Expr> {
107+ let mut p = Self::new(toks);
108+ let e = p.expr()?;
109+ if let Some(t) = p.toks.get(p.pos) {
110+ return Err((t.col, "unexpected token in expression".into()));
111+ }
112+ Ok(e)
113+ }
114+
115+ fn peek(&self) -> Option<&Tok> {
116+ self.toks.get(self.pos).map(|t| &t.tok)
117+ }
118+
119+ fn peek_word(&self, w: &str) -> bool {
120+ matches!(self.peek(), Some(Tok::Ident(s)) if s == w)
121+ }
122+
123+ fn col(&self) -> usize {
124+ self.toks
125+ .get(self.pos)
126+ .or(self.toks.last())
127+ .map_or(0, |t| t.col)
128+ }
129+
130+ fn expr(&mut self) -> PResult<Expr> {
131+ let mut l = self.xor()?;
132+ while self.peek_word("OR") || self.peek() == Some(&Tok::Punct('|')) {
133+ self.pos += 1;
134+ l = Expr::Bin(BinOp::Or, Box::new(l), Box::new(self.xor()?));
135+ }
136+ Ok(l)
137+ }
138+
139+ fn xor(&mut self) -> PResult<Expr> {
140+ let mut l = self.and()?;
141+ while self.peek_word("XOR") || self.peek() == Some(&Tok::Punct('^')) {
142+ self.pos += 1;
143+ l = Expr::Bin(BinOp::Xor, Box::new(l), Box::new(self.and()?));
144+ }
145+ Ok(l)
146+ }
147+
148+ fn and(&mut self) -> PResult<Expr> {
149+ let mut l = self.shift()?;
150+ while self.peek_word("AND") || self.peek() == Some(&Tok::Punct('&')) {
151+ self.pos += 1;
152+ l = Expr::Bin(BinOp::And, Box::new(l), Box::new(self.shift()?));
153+ }
154+ Ok(l)
155+ }
156+
157+ fn shift(&mut self) -> PResult<Expr> {
158+ let mut l = self.add()?;
159+ loop {
160+ let op = match self.peek() {
161+ Some(Tok::Shl) => BinOp::Shl,
162+ Some(Tok::Shr) => BinOp::Shr,
163+ Some(Tok::Ident(s)) if s == "SHL" => BinOp::Shl,
164+ Some(Tok::Ident(s)) if s == "SHR" => BinOp::Shr,
165+ _ => return Ok(l),
166+ };
167+ self.pos += 1;
168+ l = Expr::Bin(op, Box::new(l), Box::new(self.add()?));
169+ }
170+ }
171+
172+ fn add(&mut self) -> PResult<Expr> {
173+ let mut l = self.mul()?;
174+ loop {
175+ let op = match self.peek() {
176+ Some(Tok::Punct('+')) => BinOp::Add,
177+ Some(Tok::Punct('-')) => BinOp::Sub,
178+ _ => return Ok(l),
179+ };
180+ self.pos += 1;
181+ l = Expr::Bin(op, Box::new(l), Box::new(self.mul()?));
182+ }
183+ }
184+
185+ fn mul(&mut self) -> PResult<Expr> {
186+ let mut l = self.unary()?;
187+ loop {
188+ let op = match self.peek() {
189+ Some(Tok::Punct('*')) => BinOp::Mul,
190+ Some(Tok::Punct('/')) => BinOp::Div,
191+ Some(Tok::Punct('%')) => BinOp::Mod,
192+ Some(Tok::Ident(s)) if s == "MOD" => BinOp::Mod,
193+ _ => return Ok(l),
194+ };
195+ self.pos += 1;
196+ l = Expr::Bin(op, Box::new(l), Box::new(self.unary()?));
197+ }
198+ }
199+
200+ fn unary(&mut self) -> PResult<Expr> {
201+ let wrap: fn(Box<Expr>) -> Expr = match self.peek() {
202+ Some(Tok::Punct('-')) => Expr::Neg,
203+ Some(Tok::Punct('+')) => {
204+ self.pos += 1;
205+ return self.unary();
206+ }
207+ Some(Tok::Punct('~')) => Expr::Not,
208+ Some(Tok::Ident(s)) if s == "NOT" => Expr::Not,
209+ Some(Tok::Ident(s)) if s == "HIGH" => Expr::High,
210+ Some(Tok::Ident(s)) if s == "LOW" => Expr::Low,
211+ _ => return self.dot(),
212+ };
213+ self.pos += 1;
214+ Ok(wrap(Box::new(self.unary()?)))
215+ }
216+
217+ fn dot(&mut self) -> PResult<Expr> {
218+ let l = self.primary()?;
219+ if self.peek() == Some(&Tok::Punct('.')) {
220+ self.pos += 1;
221+ let r = self.primary()?;
222+ return Ok(Expr::Bin(BinOp::Dot, Box::new(l), Box::new(r)));
223+ }
224+ Ok(l)
225+ }
226+
227+ fn primary(&mut self) -> PResult<Expr> {
228+ let col = self.col();
229+ let Some(tok) = self.peek().cloned() else {
230+ return Err((col, "expected expression".into()));
231+ };
232+ self.pos += 1;
233+ match tok {
234+ Tok::Num(n) => Ok(Expr::Num(n)),
235+ Tok::Punct('$') => Ok(Expr::Here),
236+ Tok::Ident(s) => Ok(Expr::Sym(s, col)),
237+ Tok::Str(b) if b.len() == 1 => Ok(Expr::Num(b[0] as i64)),
238+ Tok::Str(b) if b.len() == 2 => Ok(Expr::Num(((b[0] as i64) << 8) | b[1] as i64)),
239+ Tok::Str(_) => Err((col, "string too long for an expression".into())),
240+ Tok::Punct('(') => {
241+ let e = self.expr()?;
242+ if self.peek() != Some(&Tok::Punct(')')) {
243+ return Err((self.col(), "expected ')'".into()));
244+ }
245+ self.pos += 1;
246+ Ok(e)
247+ }
248+ _ => Err((col, "expected expression".into())),
249+ }
250+ }
251+}
A src/asm/hex.rs View
@@ -0,0 +1,117 @@
1+//! Intel HEX reading and writing.
2+
3+use std::collections::BTreeMap;
4+use std::fmt::Write;
5+
6+const RECORD_LEN: usize = 16;
7+
8+/// Writes data records for contiguous runs of bytes, then the EOF record.
9+pub fn write(bytes: impl Iterator<Item = (u16, u8)>) -> String {
10+ let image: BTreeMap<u16, u8> = bytes.collect();
11+ let mut out = String::new();
12+ let mut run: Vec<u8> = Vec::new();
13+ let mut run_start = 0u16;
14+ let mut flush = |start: u16, data: &mut Vec<u8>| {
15+ for (i, chunk) in data.chunks(RECORD_LEN).enumerate() {
16+ record(
17+ &mut out,
18+ start.wrapping_add((i * RECORD_LEN) as u16),
19+ 0x00,
20+ chunk,
21+ );
22+ }
23+ data.clear();
24+ };
25+ for (&addr, &b) in &image {
26+ if !run.is_empty() && run_start.wrapping_add(run.len() as u16) != addr {
27+ flush(run_start, &mut run);
28+ }
29+ if run.is_empty() {
30+ run_start = addr;
31+ }
32+ run.push(b);
33+ }
34+ flush(run_start, &mut run);
35+ record(&mut out, 0, 0x01, &[]);
36+ out
37+}
38+
39+fn record(out: &mut String, addr: u16, kind: u8, data: &[u8]) {
40+ let mut sum = data.len() as u8;
41+ sum = sum
42+ .wrapping_add((addr >> 8) as u8)
43+ .wrapping_add(addr as u8)
44+ .wrapping_add(kind);
45+ let _ = write!(out, ":{:02X}{addr:04X}{kind:02X}", data.len());
46+ for &b in data {
47+ sum = sum.wrapping_add(b);
48+ let _ = write!(out, "{b:02X}");
49+ }
50+ let _ = writeln!(out, "{:02X}", sum.wrapping_neg());
51+}
52+
53+/// Parses Intel HEX into (address, byte) pairs.
54+pub fn read(text: &str) -> Result<Vec<(u16, u8)>, String> {
55+ let mut out = Vec::new();
56+ for (n, line) in text.lines().enumerate() {
57+ let line = line.trim();
58+ if line.is_empty() {
59+ continue;
60+ }
61+ let fail = |m: &str| format!("line {}: {m}", n + 1);
62+ let hex = line
63+ .strip_prefix(':')
64+ .ok_or_else(|| fail("record must start with ':'"))?;
65+ if hex.len() % 2 != 0 || hex.len() < 10 {
66+ return Err(fail("malformed record"));
67+ }
68+ let raw = (0..hex.len())
69+ .step_by(2)
70+ .map(|i| u8::from_str_radix(&hex[i..i + 2], 16))
71+ .collect::<Result<Vec<u8>, _>>()
72+ .map_err(|_| fail("invalid hex digit"))?;
73+ let len = raw[0] as usize;
74+ if raw.len() != len + 5 {
75+ return Err(fail("record length mismatch"));
76+ }
77+ if raw.iter().fold(0u8, |a, &b| a.wrapping_add(b)) != 0 {
78+ return Err(fail("checksum mismatch"));
79+ }
80+ let addr = u16::from_be_bytes([raw[1], raw[2]]);
81+ match raw[3] {
82+ 0x00 => out.extend(
83+ raw[4..4 + len]
84+ .iter()
85+ .enumerate()
86+ .map(|(i, &b)| (addr.wrapping_add(i as u16), b)),
87+ ),
88+ 0x01 => break,
89+ // Extended address / start address records don't apply to a 64K 8051.
90+ _ => {}
91+ }
92+ }
93+ Ok(out)
94+}
95+
96+#[cfg(test)]
97+mod tests {
98+ use super::*;
99+
100+ #[test]
101+ fn round_trip() {
102+ let bytes: Vec<(u16, u8)> = (0..20u16)
103+ .map(|i| (i, i as u8))
104+ .chain([(0x100, 0xAA)])
105+ .collect();
106+ let text = write(bytes.iter().copied());
107+ assert!(text.ends_with(":00000001FF\n"));
108+ assert_eq!(read(&text).unwrap(), bytes);
109+ }
110+
111+ #[test]
112+ fn known_record() {
113+ // MOV A,#55h ; SJMP $
114+ let text = write([(0, 0x74), (1, 0x55), (2, 0x80), (3, 0xFE)].into_iter());
115+ assert_eq!(text.lines().next().unwrap(), ":04000000745580FEB5");
116+ }
117+}
A src/asm/lexer.rs View
@@ -0,0 +1,175 @@
1+#[derive(Debug, Clone, PartialEq)]
2+pub enum Tok {
3+ Ident(String),
4+ Num(i64),
5+ Str(Vec<u8>),
6+ Punct(char),
7+ /// `<<` and `>>`
8+ Shl,
9+ Shr,
10+}
11+
12+#[derive(Debug, Clone)]
13+pub struct Token {
14+ pub tok: Tok,
15+ pub col: usize,
16+}
17+
18+impl Token {
19+ pub fn ident(&self) -> Option<&str> {
20+ match &self.tok {
21+ Tok::Ident(s) => Some(s),
22+ _ => None,
23+ }
24+ }
25+
26+ pub fn is_punct(&self, c: char) -> bool {
27+ self.tok == Tok::Punct(c)
28+ }
29+}
30+
31+/// Splits one source line into tokens, stopping at a `;` comment.
32+/// Identifiers are upper-cased (the assembler is case-insensitive).
33+pub fn lex(line: &str) -> Result<Vec<Token>, (usize, String)> {
34+ let chars: Vec<char> = line.chars().collect();
35+ let mut out = Vec::new();
36+ let mut i = 0;
37+ while i < chars.len() {
38+ let c = chars[i];
39+ let col = i;
40+ if c.is_whitespace() {
41+ i += 1;
42+ } else if c == ';' {
43+ break;
44+ } else if c.is_ascii_alphabetic() || c == '_' || c == '?' {
45+ let start = i;
46+ while i < chars.len()
47+ && (chars[i].is_ascii_alphanumeric() || chars[i] == '_' || chars[i] == '?')
48+ {
49+ i += 1;
50+ }
51+ let s: String = chars[start..i].iter().collect();
52+ out.push(Token {
53+ tok: Tok::Ident(s.to_ascii_uppercase()),
54+ col,
55+ });
56+ } else if c.is_ascii_digit() {
57+ let start = i;
58+ while i < chars.len() && chars[i].is_ascii_alphanumeric() {
59+ i += 1;
60+ }
61+ let s: String = chars[start..i].iter().collect();
62+ let n = parse_number(&s).ok_or((col, format!("invalid number '{s}'")))?;
63+ out.push(Token {
64+ tok: Tok::Num(n),
65+ col,
66+ });
67+ } else if c == '\'' || c == '"' {
68+ i += 1;
69+ let mut bytes = Vec::new();
70+ loop {
71+ match chars.get(i) {
72+ None => return Err((col, "unterminated string".into())),
73+ // A doubled quote inside a string stands for the quote itself.
74+ Some(&q) if q == c && chars.get(i + 1) == Some(&c) => {
75+ bytes.push(c as u8);
76+ i += 2;
77+ }
78+ Some(&q) if q == c => {
79+ i += 1;
80+ break;
81+ }
82+ Some(&ch) => {
83+ let mut buf = [0u8; 4];
84+ bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
85+ i += 1;
86+ }
87+ }
88+ }
89+ out.push(Token {
90+ tok: Tok::Str(bytes),
91+ col,
92+ });
93+ } else if c == '<' && chars.get(i + 1) == Some(&'<') {
94+ out.push(Token { tok: Tok::Shl, col });
95+ i += 2;
96+ } else if c == '>' && chars.get(i + 1) == Some(&'>') {
97+ out.push(Token { tok: Tok::Shr, col });
98+ i += 2;
99+ } else if "+-*/%()#@.,:$~&|^".contains(c) {
100+ out.push(Token {
101+ tok: Tok::Punct(c),
102+ col,
103+ });
104+ i += 1;
105+ } else {
106+ return Err((col, format!("unexpected character '{c}'")));
107+ }
108+ }
109+ Ok(out)
110+}
111+
112+/// Number literals: `123`, `123d`, `0x1F`, `1Fh`, `0b101`, `101b`, `17o`, `17q`.
113+pub fn parse_number(s: &str) -> Option<i64> {
114+ let l = s.to_ascii_lowercase();
115+ let (digits, radix) = if let Some(r) = l.strip_prefix("0x") {
116+ (r, 16)
117+ } else if let Some(r) = l.strip_suffix('h') {
118+ (r, 16)
119+ } else if let Some(r) = l
120+ .strip_prefix("0b")
121+ .filter(|r| !r.is_empty() && r.chars().all(|c| c == '0' || c == '1'))
122+ {
123+ (r, 2)
124+ } else if let Some(r) = l
125+ .strip_suffix('b')
126+ .filter(|r| r.chars().all(|c| c == '0' || c == '1'))
127+ {
128+ (r, 2)
129+ } else if let Some(r) = l.strip_suffix('o').or_else(|| l.strip_suffix('q')) {
130+ (r, 8)
131+ } else if let Some(r) = l.strip_suffix('d') {
132+ (r, 10)
133+ } else {
134+ (l.as_str(), 10)
135+ };
136+ if digits.is_empty() {
137+ return None;
138+ }
139+ i64::from_str_radix(digits, radix).ok()
140+}
141+
142+#[cfg(test)]
143+mod tests {
144+ use super::*;
145+
146+ #[test]
147+ fn numbers() {
148+ assert_eq!(parse_number("0FFh"), Some(255));
149+ assert_eq!(parse_number("0bh"), Some(11));
150+ assert_eq!(parse_number("0b101"), Some(5));
151+ assert_eq!(parse_number("1010b"), Some(10));
152+ assert_eq!(parse_number("0x1f"), Some(31));
153+ assert_eq!(parse_number("17o"), Some(15));
154+ assert_eq!(parse_number("42"), Some(42));
155+ assert_eq!(parse_number("12x"), None);
156+ }
157+
158+ #[test]
159+ fn lexes_line() {
160+ let t = lex("loop: mov a, #'x' ; comment").unwrap();
161+ let toks: Vec<_> = t.into_iter().map(|t| t.tok).collect();
162+ assert_eq!(
163+ toks,
164+ vec![
165+ Tok::Ident("LOOP".into()),
166+ Tok::Punct(':'),
167+ Tok::Ident("MOV".into()),
168+ Tok::Ident("A".into()),
169+ Tok::Punct(','),
170+ Tok::Punct('#'),
171+ Tok::Str(vec![b'x']),
172+ ]
173+ );
174+ }
175+}
A src/asm/listing.rs View
@@ -0,0 +1,53 @@
1+use std::fmt::Write;
2+
3+use super::AsmOutput;
4+
5+const BYTES_PER_ROW: usize = 4;
6+
7+/// Keil A51-style listing: location, object bytes, line number, source,
8+/// with errors inline and a symbol table at the end.
9+pub fn render(out: &AsmOutput, source: &str) -> String {
10+ let mut s = String::new();
11+ let _ = writeln!(s, "LOC OBJ LINE SOURCE");
12+ let _ = writeln!(s);
13+ let mut diags = out.diagnostics.iter().peekable();
14+ for (i, text) in source.lines().enumerate() {
15+ let line = out.lines.get(i).cloned().unwrap_or_default();
16+ let loc = line.addr.map_or(" ".to_string(), |a| format!("{a:04X}"));
17+ let mut chunks = line.bytes.chunks(BYTES_PER_ROW);
18+ let obj = chunks.next().map(hex_bytes).unwrap_or_default();
19+ let _ = writeln!(s, "{loc} {obj:<12} {:>5} {}", i + 1, text.trim_end());
20+ let mut addr = line.addr.unwrap_or(0) as usize;
21+ for c in chunks {
22+ addr += BYTES_PER_ROW;
23+ let _ = writeln!(s, "{:04X} {}", addr & 0xFFFF, hex_bytes(c));
24+ }
25+ while let Some(d) = diags.next_if(|d| d.line == i) {
26+ let _ = writeln!(s, "*** ERROR at column {}: {}", d.col + 1, d.msg);
27+ }
28+ }
29+
30+ let _ = writeln!(s);
31+ let _ = writeln!(s, "SYMBOL TABLE");
32+ let _ = writeln!(s, "NAME TYPE VALUE");
33+ for (name, sym) in &out.symbols {
34+ let _ = writeln!(
35+ s,
36+ "{name:<19} {:<7} {:04X}h",
37+ sym.kind.name(),
38+ sym.value & 0xFFFF
39+ );
40+ }
41+ let _ = writeln!(s);
42+ let _ = writeln!(
43+ s,
44+ "ASSEMBLY COMPLETE: {} ERROR(S), {} BYTE(S) OF CODE",
45+ out.diagnostics.len(),
46+ out.code_size()
47+ );
48+ s
49+}
50+
51+fn hex_bytes(b: &[u8]) -> String {
52+ b.iter().map(|x| format!("{x:02X}")).collect()
53+}
A src/asm/mod.rs View
@@ -0,0 +1,940 @@
1+//! Two-pass Intel 8051 assembler.
2+//!
3+//! Pass one parses every line, picks the opcode for each instruction (which
4+//! fixes its size) and assigns addresses to labels. Pass two evaluates operand
5+//! expressions with the complete symbol table and emits bytes.
6+
7+mod expr;
8+pub mod hex;
9+mod lexer;
10+mod listing;
11+
12+use std::collections::{BTreeMap, HashMap};
13+
14+use crate::isa::{self, OpInfo, Operand};
15+use crate::sfr;
16+use expr::{Env, EvalError, Expr, Parser};
17+use lexer::{Tok, Token, lex};
18+
19+#[derive(Debug, Clone)]
20+pub struct Diagnostic {
21+ /// 0-based source line.
22+ pub line: usize,
23+ /// 0-based column.
24+ pub col: usize,
25+ pub msg: String,
26+}
27+
28+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29+pub enum SymKind {
30+ Code,
31+ Number,
32+ Bit,
33+ Data,
34+ Idata,
35+ Xdata,
36+}
37+
38+impl SymKind {
39+ pub fn name(self) -> &'static str {
40+ match self {
41+ SymKind::Code => "CODE",
42+ SymKind::Number => "NUMBER",
43+ SymKind::Bit => "BIT",
44+ SymKind::Data => "DATA",
45+ SymKind::Idata => "IDATA",
46+ SymKind::Xdata => "XDATA",
47+ }
48+ }
49+}
50+
51+#[derive(Debug, Clone)]
52+pub struct Symbol {
53+ pub value: i64,
54+ pub kind: SymKind,
55+ pub line: usize,
56+}
57+
58+/// What one source line produced.
59+#[derive(Debug, Clone, Default)]
60+pub struct LineOut {
61+ /// Location counter at the start of the line, if the line occupies or
62+ /// labels an address.
63+ pub addr: Option<u16>,
64+ pub bytes: Vec<u8>,
65+}
66+
67+#[derive(Debug, Clone, Default)]
68+pub struct AsmOutput {
69+ pub lines: Vec<LineOut>,
70+ pub symbols: BTreeMap<String, Symbol>,
71+ pub diagnostics: Vec<Diagnostic>,
72+}
73+
74+impl AsmOutput {
75+ pub fn ok(&self) -> bool {
76+ self.diagnostics.is_empty()
77+ }
78+
79+ /// All emitted code bytes as (address, byte), in source order.
80+ pub fn bytes(&self) -> impl Iterator<Item = (u16, u8)> + '_ {
81+ self.lines.iter().flat_map(|l| {
82+ let base = l.addr.unwrap_or(0);
83+ l.bytes
84+ .iter()
85+ .enumerate()
86+ .map(move |(i, &b)| (base.wrapping_add(i as u16), b))
87+ })
88+ }
89+
90+ /// Source line holding the instruction that starts at each address.
91+ pub fn addr_to_line(&self) -> HashMap<u16, usize> {
92+ let mut m = HashMap::new();
93+ for (i, l) in self.lines.iter().enumerate() {
94+ if let (Some(a), false) = (l.addr, l.bytes.is_empty()) {
95+ m.entry(a).or_insert(i);
96+ }
97+ }
98+ m
99+ }
100+
101+ pub fn code_size(&self) -> usize {
102+ self.lines.iter().map(|l| l.bytes.len()).sum()
103+ }
104+
105+ pub fn listing(&self, source: &str) -> String {
106+ listing::render(self, source)
107+ }
108+
109+ pub fn intel_hex(&self) -> String {
110+ hex::write(self.bytes())
111+ }
112+}
113+
114+/// Operand as written in the source, before matching it against the ISA.
115+#[derive(Debug, Clone)]
116+enum Syn {
117+ A,
118+ AB,
119+ C,
120+ Dptr,
121+ Reg(u8),
122+ AtReg(u8),
123+ AtDptr,
124+ AtAPlusDptr,
125+ AtAPlusPc,
126+ Imm(Expr),
127+ NotBit(Expr),
128+ Expr(Expr),
129+}
130+
131+#[derive(Debug, Clone)]
132+enum DbItem {
133+ Bytes(Vec<u8>),
134+ Expr(Expr, usize),
135+}
136+
137+#[derive(Debug, Clone)]
138+enum Stmt {
139+ Empty,
140+ Instr {
141+ info: &'static OpInfo,
142+ args: Vec<(Expr, usize)>,
143+ },
144+ Org(Expr, usize),
145+ Db(Vec<DbItem>),
146+ Dw(Vec<(Expr, usize)>),
147+ Ds(Expr, usize),
148+ Equ {
149+ name: String,
150+ kind: SymKind,
151+ expr: Expr,
152+ col: usize,
153+ redefinable: bool,
154+ },
155+ End,
156+}
157+
158+impl Stmt {
159+ fn size(&self) -> u16 {
160+ match self {
161+ Stmt::Instr { info, .. } => info.size as u16,
162+ Stmt::Db(items) => items
163+ .iter()
164+ .map(|i| match i {
165+ DbItem::Bytes(b) => b.len() as u16,
166+ DbItem::Expr(..) => 1,
167+ })
168+ .sum(),
169+ Stmt::Dw(items) => 2 * items.len() as u16,
170+ _ => 0,
171+ }
172+ }
173+}
174+
175+struct ParsedLine {
176+ label: Option<(String, usize)>,
177+ stmt: Stmt,
178+}
179+
180+type PResult<T> = Result<T, (usize, String)>;
181+
182+fn split_commas(toks: &[Token]) -> Vec<&[Token]> {
183+ if toks.is_empty() {
184+ return Vec::new();
185+ }
186+ let mut parts = Vec::new();
187+ let mut depth = 0;
188+ let mut start = 0;
189+ for (i, t) in toks.iter().enumerate() {
190+ match t.tok {
191+ Tok::Punct('(') => depth += 1,
192+ Tok::Punct(')') => depth -= 1,
193+ Tok::Punct(',') if depth == 0 => {
194+ parts.push(&toks[start..i]);
195+ start = i + 1;
196+ }
197+ _ => {}
198+ }
199+ }
200+ parts.push(&toks[start..]);
201+ parts
202+}
203+
204+fn parse_expr(toks: &[Token], fallback_col: usize) -> PResult<Expr> {
205+ if toks.is_empty() {
206+ return Err((fallback_col, "missing operand".into()));
207+ }
208+ Parser::parse_all(toks)
209+}
210+
211+fn reg_num(s: &str) -> Option<u8> {
212+ let n = s.strip_prefix('R')?;
213+ if n.len() == 1 {
214+ n.parse().ok().filter(|&n: &u8| n < 8)
215+ } else {
216+ None
217+ }
218+}
219+
220+fn parse_operand(toks: &[Token], col: usize) -> PResult<Syn> {
221+ let Some(first) = toks.first() else {
222+ return Err((col, "missing operand".into()));
223+ };
224+ let words: Vec<Option<&str>> = toks.iter().map(|t| t.ident()).collect();
225+ if toks.len() == 1
226+ && let Some(w) = first.ident()
227+ {
228+ match w {
229+ "A" => return Ok(Syn::A),
230+ "AB" => return Ok(Syn::AB),
231+ "C" => return Ok(Syn::C),
232+ "DPTR" => return Ok(Syn::Dptr),
233+ _ => {}
234+ }
235+ if let Some(n) = reg_num(w) {
236+ return Ok(Syn::Reg(n));
237+ }
238+ }
239+ match &first.tok {
240+ Tok::Punct('@') => match (&words[1..], toks.get(2)) {
241+ ([Some("R0")], _) => Ok(Syn::AtReg(0)),
242+ ([Some("R1")], _) => Ok(Syn::AtReg(1)),
243+ ([Some("DPTR")], _) => Ok(Syn::AtDptr),
244+ ([Some("A"), None, Some("DPTR")], Some(p)) if p.is_punct('+') => Ok(Syn::AtAPlusDptr),
245+ ([Some("A"), None, Some("PC")], Some(p)) if p.is_punct('+') => Ok(Syn::AtAPlusPc),
246+ _ => Err((
247+ first.col,
248+ "invalid indirect operand (expected @R0, @R1, @DPTR, @A+DPTR or @A+PC)".into(),
249+ )),
250+ },
251+ Tok::Punct('#') => Ok(Syn::Imm(parse_expr(&toks[1..], first.col + 1)?)),
252+ Tok::Punct('/') => Ok(Syn::NotBit(parse_expr(&toks[1..], first.col + 1)?)),
253+ _ => Ok(Syn::Expr(parse_expr(toks, col)?)),
254+ }
255+}
256+
257+fn operand_matches(op: Operand, syn: &Syn) -> bool {
258+ match (op, syn) {
259+ (Operand::A, Syn::A)
260+ | (Operand::AB, Syn::AB)
261+ | (Operand::C, Syn::C)
262+ | (Operand::Dptr, Syn::Dptr)
263+ | (Operand::AtDptr, Syn::AtDptr)
264+ | (Operand::AtAPlusDptr, Syn::AtAPlusDptr)
265+ | (Operand::AtAPlusPc, Syn::AtAPlusPc) => true,
266+ (Operand::Reg(a), Syn::Reg(b)) | (Operand::AtReg(a), Syn::AtReg(b)) => a == *b,
267+ (Operand::Imm8 | Operand::Imm16, Syn::Imm(_)) => true,
268+ (Operand::NotBit, Syn::NotBit(_)) => true,
269+ (
270+ Operand::Direct | Operand::Bit | Operand::Addr11 | Operand::Addr16 | Operand::Rel,
271+ Syn::Expr(_),
272+ ) => true,
273+ _ => false,
274+ }
275+}
276+
277+fn parse_instruction(mnemonic: &str, col: usize, rest: &[Token]) -> PResult<Stmt> {
278+ let mut syns = Vec::new();
279+ let mut cols = Vec::new();
280+ for part in split_commas(rest) {
281+ let c = part.first().map_or(col, |t| t.col);
282+ syns.push(parse_operand(part, c)?);
283+ cols.push(c);
284+ }
285+ // Generic JMP/CALL always assemble to the long forms, so sizes never
286+ // depend on label values.
287+ let lookup = match (mnemonic, syns.as_slice()) {
288+ ("JMP", [Syn::Expr(_)]) => "LJMP",
289+ ("CALL", _) => "LCALL",
290+ (m, _) => m,
291+ };
292+ let info = isa::by_mnemonic(lookup)
293+ .find(|i| {
294+ i.operands.len() == syns.len()
295+ && i.operands
296+ .iter()
297+ .zip(&syns)
298+ .all(|(&o, s)| operand_matches(o, s))
299+ })
300+ .ok_or_else(|| (col, format!("invalid operands for {mnemonic}")))?;
301+ let args = syns
302+ .into_iter()
303+ .zip(cols)
304+ .filter_map(|(s, c)| match s {
305+ Syn::Imm(e) | Syn::NotBit(e) | Syn::Expr(e) => Some((e, c)),
306+ _ => None,
307+ })
308+ .collect();
309+ Ok(Stmt::Instr { info, args })
310+}
311+
312+fn parse_line(toks: &[Token]) -> PResult<ParsedLine> {
313+ let mut toks = toks;
314+ let mut label = None;
315+
316+ // `name EQU expr` and friends.
317+ if let (Some(name), Some(kw)) = (
318+ toks.first().and_then(Token::ident),
319+ toks.get(1).and_then(Token::ident),
320+ ) {
321+ let kind = match kw {
322+ "EQU" | "SET" => Some(SymKind::Number),
323+ "BIT" => Some(SymKind::Bit),
324+ "DATA" => Some(SymKind::Data),
325+ "IDATA" => Some(SymKind::Idata),
326+ "XDATA" => Some(SymKind::Xdata),
327+ "CODE" => Some(SymKind::Code),
328+ _ => None,
329+ };
330+ if let Some(kind) = kind {
331+ let expr = parse_expr(&toks[2..], toks[1].col + kw.len())?;
332+ return Ok(ParsedLine {
333+ label: None,
334+ stmt: Stmt::Equ {
335+ name: name.to_string(),
336+ kind,
337+ expr,
338+ col: toks[0].col,
339+ redefinable: kw == "SET",
340+ },
341+ });
342+ }
343+ }
344+
345+ if let (Some(name), Some(colon)) = (toks.first().and_then(Token::ident), toks.get(1))
346+ && colon.is_punct(':')
347+ {
348+ label = Some((name.to_string(), toks[0].col));
349+ toks = &toks[2..];
350+ }
351+
352+ // Allow `.org`, `.db`, ... spellings.
353+ if toks.first().is_some_and(|t| t.is_punct('.')) {
354+ toks = &toks[1..];
355+ }
356+
357+ let Some(first) = toks.first() else {
358+ return Ok(ParsedLine {
359+ label,
360+ stmt: Stmt::Empty,
361+ });
362+ };
363+ let Some(word) = first.ident() else {
364+ return Err((first.col, "expected instruction or directive".into()));
365+ };
366+ let rest = &toks[1..];
367+ let end_col = first.col + word.len();
368+
369+ let stmt = match word {
370+ "ORG" => Stmt::Org(parse_expr(rest, end_col)?, first.col),
371+ "CSEG" | "XSEG" | "DSEG" if rest.first().and_then(Token::ident) == Some("AT") => {
372+ Stmt::Org(parse_expr(&rest[1..], end_col)?, first.col)
373+ }
374+ "DB" | "BYTE" => {
375+ let mut items = Vec::new();
376+ for part in split_commas(rest) {
377+ match part {
378+ [
379+ Token {
380+ tok: Tok::Str(b), ..
381+ },
382+ ] if b.len() != 1 => items.push(DbItem::Bytes(b.clone())),
383+ _ => {
384+ let c = part.first().map_or(end_col, |t| t.col);
385+ items.push(DbItem::Expr(parse_expr(part, c)?, c));
386+ }
387+ }
388+ }
389+ if items.is_empty() {
390+ return Err((end_col, "DB needs at least one value".into()));
391+ }
392+ Stmt::Db(items)
393+ }
394+ "DW" | "WORD" => {
395+ let mut items = Vec::new();
396+ for part in split_commas(rest) {
397+ let c = part.first().map_or(end_col, |t| t.col);
398+ items.push((parse_expr(part, c)?, c));
399+ }
400+ if items.is_empty() {
401+ return Err((end_col, "DW needs at least one value".into()));
402+ }
403+ Stmt::Dw(items)
404+ }
405+ "DS" => Stmt::Ds(parse_expr(rest, end_col)?, first.col),
406+ "END" => Stmt::End,
407+ // Accepted for compatibility, no effect in a single-module assembler.
408+ "CSEG" | "USING" | "NAME" => Stmt::Empty,
409+ m if isa::is_mnemonic(m) => parse_instruction(m, first.col, rest)?,
410+ _ if rest.first().is_some_and(|t| t.is_punct(':')) => {
411+ return Err((
412+ first.col,
413+ "a label must be the first thing on a line".into(),
414+ ));
415+ }
416+ _ => {
417+ return Err((
418+ first.col,
419+ format!("unknown instruction or directive '{word}'"),
420+ ));
421+ }
422+ };
423+ Ok(ParsedLine { label, stmt })
424+}
425+
426+struct Symbols {
427+ user: HashMap<String, Symbol>,
428+ here: i64,
429+}
430+
431+impl Env for Symbols {
432+ fn lookup(&self, name: &str) -> Option<i64> {
433+ if let Some(s) = self.user.get(name) {
434+ return Some(s.value);
435+ }
436+ sfr::SFRS
437+ .iter()
438+ .chain(sfr::BITS)
439+ .find(|(n, _)| *n == name)
440+ .map(|&(_, v)| v as i64)
441+ }
442+
443+ fn here(&self) -> i64 {
444+ self.here
445+ }
446+}
447+
448+fn is_predefined(name: &str) -> bool {
449+ sfr::SFRS.iter().chain(sfr::BITS).any(|(n, _)| *n == name)
450+}
451+
452+fn is_reserved(name: &str) -> bool {
453+ matches!(name, "A" | "AB" | "C" | "DPTR" | "PC")
454+ || reg_num(name).is_some()
455+ || isa::is_mnemonic(name)
456+}
457+
458+pub fn assemble(source: &str) -> AsmOutput {
459+ let mut diags: Vec<Diagnostic> = Vec::new();
460+ let mut syms = Symbols {
461+ user: HashMap::new(),
462+ here: 0,
463+ };
464+ let mut parsed: Vec<Option<ParsedLine>> = Vec::new();
465+ let mut addrs: Vec<i64> = Vec::new();
466+ let mut pending_equ: Vec<usize> = Vec::new();
467+
468+ let err = |diags: &mut Vec<Diagnostic>, line: usize, col: usize, msg: String| {
469+ diags.push(Diagnostic { line, col, msg });
470+ };
471+
472+ // ---- Pass 1: parse, size, assign label addresses. ----
473+ let mut loc: i64 = 0;
474+ let mut ended = false;
475+ for (ln, text) in source.lines().enumerate() {
476+ addrs.push(loc);
477+ if ended {
478+ parsed.push(None);
479+ continue;
480+ }
481+ let line = match lex(text).and_then(|t| parse_line(&t)) {
482+ Ok(l) => l,
483+ Err((col, msg)) => {
484+ err(&mut diags, ln, col, msg);
485+ parsed.push(None);
486+ continue;
487+ }
488+ };
489+
490+ if let Some((name, col)) = &line.label {
491+ define(
492+ &mut syms,
493+ &mut diags,
494+ name,
495+ *col,
496+ ln,
497+ loc,
498+ SymKind::Code,
499+ false,
500+ );
501+ }
502+
503+ syms.here = loc;
504+ match &line.stmt {
505+ Stmt::Org(e, col) | Stmt::Ds(e, col) => match e.eval(&syms) {
506+ Ok(v) if matches!(line.stmt, Stmt::Org(..)) => {
507+ if !(0..=0xFFFF).contains(&v) {
508+ err(
509+ &mut diags,
510+ ln,
511+ *col,
512+ format!("ORG address {v} out of range"),
513+ );
514+ } else {
515+ loc = v;
516+ addrs[ln] = loc;
517+ }
518+ }
519+ Ok(v) => {
520+ if v < 0 {
521+ err(&mut diags, ln, *col, "DS size must not be negative".into());
522+ } else {
523+ loc += v;
524+ }
525+ }
526+ Err(EvalError::Undefined(s, c)) => err(
527+ &mut diags,
528+ ln,
529+ c,
530+ format!("'{s}' must be defined before it is used here"),
531+ ),
532+ Err(EvalError::Other(m)) => err(&mut diags, ln, *col, m),
533+ },
534+ Stmt::Equ {
535+ name,
536+ kind,
537+ expr,
538+ col,
539+ redefinable,
540+ } => match expr.eval(&syms) {
541+ Ok(v) => define(
542+ &mut syms,
543+ &mut diags,
544+ name,
545+ *col,
546+ ln,
547+ v,
548+ *kind,
549+ *redefinable,
550+ ),
551+ Err(EvalError::Undefined(..)) => pending_equ.push(ln),
552+ Err(EvalError::Other(m)) => err(&mut diags, ln, *col, m),
553+ },
554+ Stmt::End => ended = true,
555+ s => loc += s.size() as i64,
556+ }
557+ if loc > 0x10000 {
558+ err(&mut diags, ln, 0, "code exceeds 64K address space".into());
559+ loc &= 0xFFFF;
560+ }
561+ parsed.push(Some(line));
562+ }
563+
564+ // Resolve EQUs that referred to symbols defined later.
565+ loop {
566+ let before = pending_equ.len();
567+ pending_equ.retain(|&ln| {
568+ let Some(ParsedLine {
569+ stmt:
570+ Stmt::Equ {
571+ name,
572+ kind,
573+ expr,
574+ col,
575+ redefinable,
576+ },
577+ ..
578+ }) = &parsed[ln]
579+ else {
580+ return false;
581+ };
582+ syms.here = addrs[ln];
583+ match expr.eval(&syms) {
584+ Ok(v) => {
585+ define(
586+ &mut syms,
587+ &mut diags,
588+ name,
589+ *col,
590+ ln,
591+ v,
592+ *kind,
593+ *redefinable,
594+ );
595+ false
596+ }
597+ Err(_) => true,
598+ }
599+ });
600+ if pending_equ.len() == before {
601+ break;
602+ }
603+ }
604+ for ln in pending_equ {
605+ if let Some(ParsedLine {
606+ stmt: Stmt::Equ { expr, col, .. },
607+ ..
608+ }) = &parsed[ln]
609+ {
610+ syms.here = addrs[ln];
611+ match expr.eval(&syms) {
612+ Err(EvalError::Undefined(s, c)) => {
613+ err(&mut diags, ln, c, format!("undefined symbol '{s}'"))
614+ }
615+ Err(EvalError::Other(m)) => err(&mut diags, ln, *col, m),
616+ Ok(_) => {}
617+ }
618+ }
619+ }
620+
621+ // ---- Pass 2: emit bytes. ----
622+ let mut lines = Vec::with_capacity(parsed.len());
623+ let mut used = vec![false; 0x10000];
624+ for (ln, p) in parsed.iter().enumerate() {
625+ let mut out = LineOut::default();
626+ let Some(p) = p else {
627+ lines.push(out);
628+ continue;
629+ };
630+ let pc = addrs[ln];
631+ syms.here = pc;
632+ let has_addr = p.label.is_some()
633+ || matches!(
634+ p.stmt,
635+ Stmt::Instr { .. } | Stmt::Db(_) | Stmt::Dw(_) | Stmt::Ds(..) | Stmt::Org(..)
636+ );
637+ if has_addr {
638+ out.addr = Some(pc as u16);
639+ }
640+
641+ let eval = |e: &Expr, col: usize, diags: &mut Vec<Diagnostic>| -> Option<i64> {
642+ match e.eval(&syms) {
643+ Ok(v) => Some(v),
644+ Err(EvalError::Undefined(s, c)) => {
645+ err(diags, ln, c, format!("undefined symbol '{s}'"));
646+ None
647+ }
648+ Err(EvalError::Other(m)) => {
649+ err(diags, ln, col, m);
650+ None
651+ }
652+ }
653+ };
654+
655+ match &p.stmt {
656+ Stmt::Instr { info, args } => {
657+ let mut bytes = vec![info.opcode];
658+ let kinds = info.operands.iter().filter(|o| o.len() > 0);
659+ let mut failed = false;
660+ for (&kind, (e, col)) in kinds.zip(args) {
661+ let Some(v) = eval(e, *col, &mut diags) else {
662+ failed = true;
663+ continue;
664+ };
665+ if let Err(m) = encode_operand(kind, v, pc, info.size, &mut bytes) {
666+ err(&mut diags, ln, *col, m);
667+ failed = true;
668+ }
669+ }
670+ if !failed {
671+ if info.opcode == 0x85 {
672+ // MOV dest,src is encoded with the source byte first.
673+ bytes.swap(1, 2);
674+ }
675+ out.bytes = bytes;
676+ }
677+ }
678+ Stmt::Db(items) => {
679+ for item in items {
680+ match item {
681+ DbItem::Bytes(b) => out.bytes.extend_from_slice(b),
682+ DbItem::Expr(e, col) => {
683+ let v = eval(e, *col, &mut diags).unwrap_or(0);
684+ if !(-128..=255).contains(&v) {
685+ err(
686+ &mut diags,
687+ ln,
688+ *col,
689+ format!("value {v} does not fit in a byte"),
690+ );
691+ }
692+ out.bytes.push(v as u8);
693+ }
694+ }
695+ }
696+ }
697+ Stmt::Dw(items) => {
698+ for (e, col) in items {
699+ let v = eval(e, *col, &mut diags).unwrap_or(0);
700+ if !(-32768..=0xFFFF).contains(&v) {
701+ err(
702+ &mut diags,
703+ ln,
704+ *col,
705+ format!("value {v} does not fit in a word"),
706+ );
707+ }
708+ out.bytes.push((v >> 8) as u8);
709+ out.bytes.push(v as u8);
710+ }
711+ }
712+ _ => {}
713+ }
714+
715+ for i in 0..out.bytes.len() {
716+ let a = (pc as usize + i) & 0xFFFF;
717+ if used[a] {
718+ err(
719+ &mut diags,
720+ ln,
721+ 0,
722+ format!("code overlaps previously emitted code at {a:04X}h"),
723+ );
724+ break;
725+ }
726+ used[a] = true;
727+ }
728+ lines.push(out);
729+ }
730+
731+ diags.sort_by_key(|d| (d.line, d.col));
732+ let symbols = syms.user.into_iter().collect();
733+ AsmOutput {
734+ lines,
735+ symbols,
736+ diagnostics: diags,
737+ }
738+}
739+
740+#[allow(clippy::too_many_arguments)]
741+fn define(
742+ syms: &mut Symbols,
743+ diags: &mut Vec<Diagnostic>,
744+ name: &str,
745+ col: usize,
746+ line: usize,
747+ value: i64,
748+ kind: SymKind,
749+ redefinable: bool,
750+) {
751+ let msg = if is_reserved(name) {
752+ Some(format!("'{name}' is a reserved word"))
753+ } else if is_predefined(name) {
754+ Some(format!("'{name}' is a predefined SFR or bit name"))
755+ } else {
756+ syms.user
757+ .get(name)
758+ .filter(|_| !redefinable)
759+ .map(|prev| format!("'{name}' already defined on line {}", prev.line + 1))
760+ };
761+ match msg {
762+ Some(msg) => diags.push(Diagnostic { line, col, msg }),
763+ None => {
764+ syms.user
765+ .insert(name.to_string(), Symbol { value, kind, line });
766+ }
767+ }
768+}
769+
770+fn encode_operand(
771+ kind: Operand,
772+ v: i64,
773+ pc: i64,
774+ size: u8,
775+ bytes: &mut Vec<u8>,
776+) -> Result<(), String> {
777+ let range = |lo: i64, hi: i64, what: &str| {
778+ if (lo..=hi).contains(&v) {
779+ Ok(())
780+ } else {
781+ Err(format!("{what} {v} out of range"))
782+ }
783+ };
784+ match kind {
785+ Operand::Imm8 => {
786+ range(-128, 255, "immediate value")?;
787+ bytes.push(v as u8);
788+ }
789+ Operand::Imm16 => {
790+ range(-32768, 0xFFFF, "immediate value")?;
791+ bytes.extend_from_slice(&(v as u16).to_be_bytes());
792+ }
793+ Operand::Direct => {
794+ range(0, 255, "direct address")?;
795+ bytes.push(v as u8);
796+ }
797+ Operand::Bit | Operand::NotBit => {
798+ range(0, 255, "bit address")?;
799+ bytes.push(v as u8);
800+ }
801+ Operand::Addr16 => {
802+ range(0, 0xFFFF, "address")?;
803+ bytes.extend_from_slice(&(v as u16).to_be_bytes());
804+ }
805+ Operand::Addr11 => {
806+ range(0, 0xFFFF, "address")?;
807+ let next = pc + size as i64;
808+ if (next & 0xF800) != (v & 0xF800) {
809+ return Err(format!("target {v:04X}h is outside the current 2K page"));
810+ }
811+ bytes[0] |= (((v >> 8) & 7) as u8) << 5;
812+ bytes.push(v as u8);
813+ }
814+ Operand::Rel => {
815+ let off = v - (pc + size as i64);
816+ if !(-128..=127).contains(&off) {
817+ return Err(format!(
818+ "jump target out of range (offset {off}, must be -128..127)"
819+ ));
820+ }
821+ bytes.push(off as u8);
822+ }
823+ _ => unreachable!("operand {kind:?} carries no value"),
824+ }
825+ Ok(())
826+}
827+
828+#[cfg(test)]
829+mod tests {
830+ use super::*;
831+
832+ fn bytes(src: &str) -> Vec<u8> {
833+ let out = assemble(src);
834+ assert!(out.ok(), "{:?}", out.diagnostics);
835+ out.bytes().map(|(_, b)| b).collect()
836+ }
837+
838+ fn one(line: &str) -> Vec<u8> {
839+ bytes(line)
840+ }
841+
842+ #[test]
843+ fn encodes_operand_forms() {
844+ assert_eq!(one("MOV A,#55h"), [0x74, 0x55]);
845+ assert_eq!(one("mov a, r3"), [0xEB]);
846+ assert_eq!(one("MOV A,@R1"), [0xE7]);
847+ assert_eq!(one("MOV A,30h"), [0xE5, 0x30]);
848+ assert_eq!(one("MOV A,B"), [0xE5, 0xF0]);
849+ assert_eq!(one("MOV 30h,#1"), [0x75, 0x30, 0x01]);
850+ assert_eq!(one("MOV 30h,40h"), [0x85, 0x40, 0x30]);
851+ assert_eq!(one("MOV R7,#0"), [0x7F, 0x00]);
852+ assert_eq!(one("MOV @R0,A"), [0xF6]);
853+ assert_eq!(one("MOV DPTR,#1234h"), [0x90, 0x12, 0x34]);
854+ assert_eq!(one("MOV C,P1.3"), [0xA2, 0x93]);
855+ assert_eq!(one("MOV P1.3,C"), [0x92, 0x93]);
856+ assert_eq!(one("SETB TR0"), [0xD2, 0x8C]);
857+ assert_eq!(one("CLR C"), [0xC3]);
858+ assert_eq!(one("ANL C,/ACC.7"), [0xB0, 0xE7]);
859+ assert_eq!(one("CPL 20h.0"), [0xB2, 0x00]);
860+ assert_eq!(one("MOVC A,@A+DPTR"), [0x93]);
861+ assert_eq!(one("MOVX @DPTR,A"), [0xF0]);
862+ assert_eq!(one("JMP @A+DPTR"), [0x73]);
863+ assert_eq!(one("MUL AB"), [0xA4]);
864+ assert_eq!(one("PUSH ACC"), [0xC0, 0xE0]);
865+ assert_eq!(one("ORL 20h,#0Fh"), [0x43, 0x20, 0x0F]);
866+ assert_eq!(one("XCHD A,@R0"), [0xD6]);
867+ assert_eq!(one("MOV A,#-1"), [0x74, 0xFF]);
868+ assert_eq!(one("MOV A,#HIGH(1234h)"), [0x74, 0x12]);
869+ }
870+
871+ #[test]
872+ fn jumps_and_labels() {
873+ let src = "
874+start: SJMP start
875+ DJNZ R2, start
876+ CJNE A,#3,fwd
877+ LJMP fwd
878+ CALL fwd
879+fwd: AJMP start
880+ SJMP $
881+";
882+ assert_eq!(
883+ bytes(src),
884+ [
885+ 0x80, 0xFE, // SJMP start
886+ 0xDA, 0xFC, // DJNZ R2,start
887+ 0xB4, 0x03, 0x06, // CJNE -> fwd at 0x0D
888+ 0x02, 0x00, 0x0D, // LJMP
889+ 0x12, 0x00, 0x0D, // LCALL
890+ 0x01, 0x00, // AJMP 0
891+ 0x80, 0xFE,
892+ ]
893+ );
894+ }
895+
896+ #[test]
897+ fn directives() {
898+ let src = "
899+COUNT EQU 3
900+LED BIT P1.0
901+ ORG 100h
902+ MOV R0,#COUNT
903+ SETB LED
904+tbl: DB 1, 'AB', COUNT*2
905+ DW tbl
906+ DS 2
907+after: NOP
908+ END
909+ garbage that is ignored
910+";
911+ let out = assemble(src);
912+ assert!(out.ok(), "{:?}", out.diagnostics);
913+ let v: Vec<_> = out.bytes().collect();
914+ assert_eq!(v[0], (0x100, 0x78));
915+ let b: Vec<u8> = v.iter().map(|x| x.1).collect();
916+ assert_eq!(b, [0x78, 3, 0xD2, 0x90, 1, b'A', b'B', 6, 0x01, 0x04, 0x00]);
917+ assert_eq!(out.symbols["AFTER"].value, 0x10C);
918+ }
919+
920+ #[test]
921+ fn forward_equ() {
922+ let out = assemble("X EQU Y+1\nY EQU 2\nMOV A,#X");
923+ assert!(out.ok(), "{:?}", out.diagnostics);
924+ assert_eq!(out.bytes().map(|b| b.1).collect::<Vec<_>>(), [0x74, 3]);
925+ }
926+
927+ #[test]
928+ fn reports_errors_with_lines() {
929+ let out = assemble("NOP\nMOV A,@R2\nSJMP nowhere\nFOO\nMOV A,#300\n");
930+ let lines: Vec<_> = out.diagnostics.iter().map(|d| d.line).collect();
931+ assert_eq!(lines, [1, 2, 3, 4]);
932+ }
933+
934+ #[test]
935+ fn relative_range_checked() {
936+ let out = assemble("SJMP far\nORG 200h\nfar: NOP");
937+ assert_eq!(out.diagnostics.len(), 1);
938+ assert!(out.diagnostics[0].msg.contains("out of range"));
939+ }
940+}
D src/assembler.rs View
@@ -1,201 +0,0 @@
1−use crate::instructions::Instruction;
2−use std::collections::HashMap;
3−
4−fn tokenize(line: &str) -> Vec<String> {
5− line.split([' ', ',', '\t'])
6− .filter(|s| !s.is_empty())
7− .map(|s| s.to_lowercase())
8− .collect()
9−}
10−
11−// Returns corrensponding u8 value of the register
12−fn parse_reg(s: &str) -> u8 {
13− match s {
14− "a" => 0,
15− "b" => 1,
16− "c" => 2,
17− "d" => 3,
18− _ => panic!("Unknown register {}", s),
19− }
20−}
21−
22−fn is_reg(s: &str) -> bool {
23− matches!(s, "a" | "b" | "c" | "d")
24−}
25−
26−fn instr_size(tokens: &[String]) -> u16 {
27− match tokens[0].as_str() {
28− "mov" | "add" | "sub" | "jmp" | "jz" | "jnz" | "cmp" | "mul" | "div" | "call" => 3,
29− "sys" => 2,
30− "hlt" | "ret" => 1,
31− _ => panic!("Unknown instruction {}", tokens[0]),
32− }
33−}
34−
35−fn first_pass(source: &str) -> HashMap<String, u16> {
36− let mut symbols = HashMap::new();
37− let mut addr: u16 = 0;
38−
39− for line in source.lines() {
40− let line = line.trim();
41−
42− // Ignoring comments and empty lines
43− if line.is_empty() || line.starts_with(";") {
44− continue;
45− }
46−
47− // Labels (ends with ":")
48− if line.ends_with(":") {
49− let label = line.trim_end_matches(":").to_string();
50− symbols.insert(label, addr);
51− continue;
52− }
53−
54− let tokens = tokenize(line);
55− addr += instr_size(&tokens);
56− }
57−
58− symbols
59−}
60−
61−pub fn assembler(source: &str) -> Vec<u8> {
62− let symbols = first_pass(source);
63− let mut bytes = Vec::new();
64−
65− for (line_no, line) in source.lines().enumerate() {
66− let line = line.trim();
67−
68− // Comments in assembly start with ";"
69− if line.is_empty() || line.starts_with(';') || line.ends_with(":") {
70− continue;
71− }
72−
73− let tokens = tokenize(line);
74−
75− match tokens[0].as_str() {
76− "mov" => {
77− // mov reg, imm
78− let r1 = parse_reg(&tokens[1]);
79− if is_reg(&tokens[2]) {
80− let r2 = parse_reg(&tokens[2]);
81− bytes.push(Instruction::MOV_RR as u8);
82− bytes.push(r1);
83− bytes.push(r2);
84− } else {
85− let imm: u8 = tokens[2].parse().unwrap();
86− bytes.push(Instruction::MOV_RI as u8);
87− bytes.push(r1);
88− bytes.push(imm);
89− }
90− }
91−
92− "add" => {
93− // add a, b
94− let r1 = parse_reg(&tokens[1]);
95−
96− if is_reg(&tokens[2]) {
97− let r2 = parse_reg(&tokens[2]);
98− bytes.push(Instruction::ADD_RR as u8);
99− bytes.push(r1);
100− bytes.push(r2);
101− } else {
102− let imm: u8 = tokens[2].parse().unwrap();
103− bytes.push(Instruction::ADD_RI as u8);
104− bytes.push(r1);
105− bytes.push(imm);
106− }
107− }
108−
109− "sub" => {
110− // sub a, b
111− let r1 = parse_reg(&tokens[1]);
112− if is_reg(&tokens[2]) {
113− let r2 = parse_reg(&tokens[2]);
114− bytes.push(Instruction::SUB_RR as u8);
115− bytes.push(r1);
116− bytes.push(r2);
117− } else {
118− let imm: u8 = tokens[2].parse().unwrap();
119− bytes.push(Instruction::SUB_RI as u8);
120− bytes.push(r1);
121− bytes.push(imm);
122− }
123− }
124−
125− "jmp" | "jz" | "jnz" => {
126− let opcode = match tokens[0].as_str() {
127− "jmp" => Instruction::JMP,
128− "jz" => Instruction::JZ,
129− "jnz" => Instruction::JNZ,
130− _ => unreachable!(),
131− };
132−
133− let label = &tokens[1];
134− let addr = *symbols.get(label).expect("Uknown label");
135−
136− bytes.push(opcode as u8);
137− bytes.push((addr & 0xFF) as u8); // low
138− bytes.push((addr >> 8) as u8); // high
139− }
140−
141− "cmp" => {
142− let r1 = parse_reg(&tokens[1]);
143− if tokens[2].chars().all(|c| c.is_ascii_digit()) {
144− let imm: u8 = tokens[2].parse().unwrap();
145− bytes.push(Instruction::CMP_RI as u8);
146− bytes.push(r1);
147− bytes.push(imm);
148− } else {
149− let r2 = parse_reg(&tokens[2]);
150− bytes.push(Instruction::CMP_RR as u8);
151− bytes.push(r1);
152− bytes.push(r2);
153− }
154− }
155−
156− "mul" => {
157− let r1 = parse_reg(&tokens[1]);
158− let r2 = parse_reg(&tokens[2]);
159−
160− bytes.push(Instruction::MUL as u8);
161− bytes.push(r1);
162− bytes.push(r2);
163− }
164−
165− "div" => {
166− let r1 = parse_reg(&tokens[1]);
167− let r2 = parse_reg(&tokens[2]);
168−
169− bytes.push(Instruction::DIV as u8);
170− bytes.push(r1);
171− bytes.push(r2);
172− }
173−
174− "call" => {
175− let addr = *symbols.get(&tokens[1]).expect("Unknown label");
176−
177− bytes.push(Instruction::CALL as u8);
178− bytes.push((addr & 0xFF) as u8); // low
179− bytes.push((addr >> 8) as u8); // high
180− }
181−
182− "ret" => {
183− bytes.push(Instruction::RET as u8);
184− }
185−
186− "hlt" => {
187− bytes.push(Instruction::HLT as u8);
188− }
189−
190− "sys" => {
191− let imm = tokens[1].parse().unwrap();
192− bytes.push(Instruction::SYS as u8);
193− bytes.push(imm);
194− }
195−
196− _ => panic!("Line {}: unknown instruction", line_no + 1),
197− }
198− }
199−
200− bytes
201−}
D src/cpu.rs View
@@ -1,424 +0,0 @@
1−use crate::instructions::Instruction;
2−use crate::memory::Memory;
3−
4−#[derive(Default, Debug)]
5−#[allow(dead_code)]
6−pub struct CPU {
7− pub a: u8,
8− pub b: u8,
9− pub c: u8,
10− pub d: u8,
11−
12− pub pc: u16,
13− pub sp: u16,
14−
15− pub zero: bool,
16− pub carry: bool,
17−
18− pub halted: bool,
19−}
20−
21−impl CPU {
22− fn push16(&mut self, mem: &mut Memory, val: u16) {
23− self.dec_sp();
24− mem.write(self.sp, (val >> 8) as u8); // high
25− self.dec_sp();
26− mem.write(self.sp, (val & 0xFF) as u8); // low
27− }
28−
29− fn pop16(&mut self, mem: &mut Memory) -> u16 {
30− let low = mem.read(self.sp) as u16;
31− self.inc_sp();
32− let high = mem.read(self.sp) as u16;
33− self.inc_sp();
34−
35− (high << 8) | low
36− }
37−
38− pub fn debug_instr(&self, mem: &Memory) {
39− let opcode = mem.read(self.pc);
40−
41− println!(
42− "PC={:04X} {:<3} | A={:02X} B={:02X} C={:02X} D={:02X} | Z={} C={}",
43− self.pc,
44− Instruction::opcode_name(opcode),
45− self.a,
46− self.b,
47− self.c,
48− self.d,
49− self.zero as u8,
50− self.carry as u8
51− );
52− }
53−
54− pub fn step(&mut self, mem: &mut Memory) {
55− let opcode = mem.read(self.pc);
56− self.inc_pc();
57−
58− match opcode {
59− x if x == Instruction::MOV_RI as u8 => self.mov_ri(mem),
60− x if x == Instruction::MOV_RR as u8 => self.mov_rr(mem),
61− x if x == Instruction::ADD_RR as u8 => self.add_rr(mem),
62− x if x == Instruction::ADD_RI as u8 => self.add_ri(mem),
63− x if x == Instruction::SUB_RR as u8 => self.sub_rr(mem),
64− x if x == Instruction::SUB_RI as u8 => self.sub_ri(mem),
65− x if x == Instruction::JMP as u8 => self.jmp(mem),
66− x if x == Instruction::JZ as u8 => self.jz(mem),
67− x if x == Instruction::JNZ as u8 => self.jnz(mem),
68− x if x == Instruction::CMP_RI as u8 => self.cmp_ri(mem),
69− x if x == Instruction::CMP_RR as u8 => self.cmp_rr(mem),
70− x if x == Instruction::MUL as u8 => self.mul(mem),
71− x if x == Instruction::DIV as u8 => self.div(mem),
72− x if x == Instruction::CALL as u8 => self.call(mem),
73− x if x == Instruction::RET as u8 => self.ret(mem),
74− x if x == Instruction::SYS as u8 => self.syscall(mem),
75− x if x == Instruction::HLT as u8 => self.halt(),
76− _ => panic!("Unknown opcode {:02X}", opcode),
77− }
78− }
79−
80− pub fn inc_pc(&mut self) {
81− self.pc += 1;
82− }
83−
84− pub fn inc_sp(&mut self) {
85− self.sp = self.sp.wrapping_add(1);
86− }
87−
88− pub fn dec_sp(&mut self) {
89− self.sp = self.sp.wrapping_sub(1);
90− }
91−
92− pub fn halt(&mut self) {
93− self.halted = true;
94− }
95−
96− pub fn mov_ri(&mut self, mem: &Memory) {
97− let reg = mem.read(self.pc);
98− self.inc_pc();
99− let val = mem.read(self.pc);
100− self.inc_pc();
101−
102− match reg {
103− 0 => self.a = val,
104− 1 => self.b = val,
105− 2 => self.c = val,
106− 3 => self.d = val,
107− _ => {}
108− }
109−
110− self.zero = val == 0;
111− }
112−
113− pub fn mov_rr(&mut self, mem: &Memory) {
114− let dest = mem.read(self.pc);
115− self.inc_pc();
116− let src = mem.read(self.pc);
117− self.inc_pc();
118−
119− let val = self.get_reg(src);
120− self.set_reg(dest, val);
121−
122− self.zero = val == 0;
123− }
124−
125− pub fn add_rr(&mut self, mem: &Memory) {
126− let dest = mem.read(self.pc);
127− self.pc += 1;
128− let src = mem.read(self.pc);
129− self.pc += 1;
130−
131− let (result, carry) = match (dest, src) {
132− (0, 0) => self.a.overflowing_add(self.a),
133− (0, 1) => self.a.overflowing_add(self.b),
134− (0, 2) => self.a.overflowing_add(self.c),
135− (0, 3) => self.a.overflowing_add(self.d),
136−
137− (1, 0) => self.b.overflowing_add(self.a),
138− (1, 1) => self.b.overflowing_add(self.b),
139− (1, 2) => self.b.overflowing_add(self.c),
140− (1, 3) => self.b.overflowing_add(self.d),
141−
142− (2, 0) => self.c.overflowing_add(self.a),
143− (2, 1) => self.c.overflowing_add(self.b),
144− (2, 2) => self.c.overflowing_add(self.c),
145− (2, 3) => self.c.overflowing_add(self.d),
146−
147− (3, 0) => self.d.overflowing_add(self.a),
148− (3, 1) => self.d.overflowing_add(self.b),
149− (3, 2) => self.d.overflowing_add(self.c),
150− (3, 3) => self.d.overflowing_add(self.d),
151−
152− _ => (0, false),
153− };
154−
155− match dest {
156− 0 => self.a = result,
157− 1 => self.b = result,
158− 2 => self.c = result,
159− 3 => self.d = result,
160− _ => {}
161− }
162−
163− self.zero = result == 0;
164− self.carry = carry;
165− }
166− pub fn add_ri(&mut self, mem: &Memory) {
167− let dest = mem.read(self.pc);
168− self.pc += 1;
169− let imm = mem.read(self.pc);
170− self.pc += 1;
171−
172− let (result, carry) = match dest {
173− 0 => self.a.overflowing_add(imm),
174− 1 => self.b.overflowing_add(imm),
175− 2 => self.c.overflowing_add(imm),
176− 3 => self.d.overflowing_add(imm),
177− _ => (0, false),
178− };
179−
180− match dest {
181− 0 => self.a = result,
182− 1 => self.b = result,
183− 2 => self.c = result,
184− 3 => self.d = result,
185− _ => {}
186− }
187−
188− self.zero = result == 0;
189− self.carry = carry;
190− }
191−
192− pub fn sub_rr(&mut self, mem: &Memory) {
193− let dest = mem.read(self.pc);
194− self.pc += 1;
195− let src = mem.read(self.pc);
196− self.pc += 1;
197−
198− let (result, borrow) = match (dest, src) {
199− (0, 0) => self.a.overflowing_sub(self.a),
200− (0, 1) => self.a.overflowing_sub(self.b),
201− (0, 2) => self.a.overflowing_sub(self.c),
202− (0, 3) => self.a.overflowing_sub(self.d),
203−
204− (1, 0) => self.b.overflowing_sub(self.a),
205− (1, 1) => self.b.overflowing_sub(self.b),
206− (1, 2) => self.b.overflowing_sub(self.c),
207− (1, 3) => self.b.overflowing_sub(self.d),
208−
209− (2, 0) => self.c.overflowing_sub(self.a),
210− (2, 1) => self.c.overflowing_sub(self.b),
211− (2, 2) => self.c.overflowing_sub(self.c),
212− (2, 3) => self.c.overflowing_sub(self.d),
213−
214− (3, 0) => self.d.overflowing_sub(self.a),
215− (3, 1) => self.d.overflowing_sub(self.b),
216− (3, 2) => self.d.overflowing_sub(self.c),
217− (3, 3) => self.d.overflowing_sub(self.d),
218−
219− _ => (0, false),
220− };
221−
222− match dest {
223− 0 => self.a = result,
224− 1 => self.b = result,
225− 2 => self.c = result,
226− 3 => self.d = result,
227− _ => {}
228− }
229−
230− self.zero = result == 0;
231− self.carry = borrow;
232− }
233−
234− pub fn sub_ri(&mut self, mem: &Memory) {
235− let dest = mem.read(self.pc);
236− self.pc += 1;
237− let imm = mem.read(self.pc);
238− self.pc += 1;
239−
240− let (result, borrow) = match dest {
241− 0 => self.a.overflowing_sub(imm),
242− 1 => self.b.overflowing_sub(imm),
243− 2 => self.c.overflowing_sub(imm),
244− 3 => self.d.overflowing_sub(imm),
245− _ => (0, false),
246− };
247−
248− match dest {
249− 0 => self.a = result,
250− 1 => self.b = result,
251− 2 => self.c = result,
252− 3 => self.d = result,
253− _ => {}
254− }
255−
256− self.zero = result == 0;
257− self.carry = borrow;
258− }
259−
260− pub fn jmp(&mut self, mem: &Memory) {
261− let low = mem.read(self.pc) as u16;
262− self.inc_pc();
263− let high = mem.read(self.pc) as u16;
264− self.inc_pc();
265−
266− let addrs = (high << 8) | low;
267−
268− self.pc = addrs;
269− }
270−
271− pub fn jz(&mut self, mem: &Memory) {
272− let low = mem.read(self.pc) as u16;
273− self.inc_pc();
274− let high = mem.read(self.pc) as u16;
275− self.inc_pc();
276−
277− let addrs = (high << 8) | low;
278−
279− if self.zero {
280− self.pc = addrs;
281− }
282− }
283−
284− pub fn jnz(&mut self, mem: &Memory) {
285− let low = mem.read(self.pc) as u16;
286− self.inc_pc();
287− let high = mem.read(self.pc) as u16;
288− self.inc_pc();
289−
290− let addrs = (high << 8) | low;
291−
292− if !self.zero {
293− self.pc = addrs;
294− }
295− }
296−
297− pub fn cmp_rr(&mut self, mem: &Memory) {
298− let r1 = mem.read(self.pc);
299− self.inc_pc();
300− let r2 = mem.read(self.pc);
301− self.inc_pc();
302−
303− let v1 = self.get_reg(r1);
304− let v2 = self.get_reg(r2);
305−
306− let (result, borrow) = v1.overflowing_sub(v2);
307−
308− self.zero = result == 0;
309− self.carry = borrow;
310− }
311−
312− pub fn cmp_ri(&mut self, mem: &Memory) {
313− let r1 = mem.read(self.pc);
314− self.inc_pc();
315− let r2 = mem.read(self.pc);
316− self.inc_pc();
317−
318− let v1 = self.get_reg(r1);
319−
320− let (result, borrow) = v1.overflowing_sub(r2);
321−
322− self.zero = result == 0;
323− self.carry = borrow;
324− }
325−
326− pub fn mul(&mut self, mem: &Memory) {
327− let dest = mem.read(self.pc);
328− self.inc_pc();
329− let src = mem.read(self.pc);
330− self.inc_pc();
331−
332− let lhs = self.get_reg(dest);
333− let rhs = self.get_reg(src);
334−
335− let result16 = (lhs as u16) * (rhs as u16);
336− let result8 = (result16 & 0xFF) as u8;
337−
338− self.set_reg(dest, result8);
339−
340− self.zero = result8 == 0;
341− self.carry = result16 > 0xFF;
342− }
343−
344− pub fn div(&mut self, mem: &Memory) {
345− let dest = mem.read(self.pc);
346− self.inc_pc();
347− let src = mem.read(self.pc);
348− self.inc_pc();
349−
350− let lhs = self.get_reg(dest);
351− let rhs = self.get_reg(src);
352−
353− if rhs == 0 {
354− panic!("Division by zero");
355− // OR: set halted = true
356− // OR: define a syscall/exception later
357− }
358−
359− let result = lhs / rhs;
360−
361− self.set_reg(dest, result);
362−
363− self.zero = result == 0;
364− // carry unchanged
365− }
366−
367− pub fn call(&mut self, mem: &mut Memory) {
368− let low = mem.read(self.pc) as u16;
369− self.inc_pc();
370− let high = mem.read(self.pc) as u16;
371− self.inc_pc();
372− let addr = (high << 8) | low;
373−
374− let return_addr = self.pc;
375−
376− self.push16(mem, return_addr);
377− self.pc = addr;
378− }
379−
380− pub fn ret(&mut self, mem: &mut Memory) {
381− let addr = self.pop16(mem);
382− self.pc = addr;
383− }
384−
385− pub fn syscall(&mut self, mem: &Memory) {
386− let num = mem.read(self.pc);
387− self.pc = self.pc.wrapping_add(1);
388− match num {
389− 0 => {
390− // exit
391− self.halted = true;
392− }
393− 1 => {
394− // print A as number
395− println!("{}", self.a);
396− }
397− 2 => {
398− // print A as ASCII char
399− print!("{}", self.a as char);
400− }
401− _ => panic!("Unknown syscall {}", num),
402− }
403− }
404−
405− fn get_reg(&self, r: u8) -> u8 {
406− match r {
407− 0 => self.a,
408− 1 => self.b,
409− 2 => self.c,
410− 3 => self.d,
411− _ => panic!("Invalid Register"),
412− }
413− }
414−
415− fn set_reg(&mut self, dest: u8, val: u8) {
416− match dest {
417− 0 => self.a = val,
418− 1 => self.b = val,
419− 2 => self.c = val,
420− 3 => self.d = val,
421− _ => panic!("Invalid register"),
422− }
423− }
424−}
D src/instructions.rs View
@@ -1,43 +0,0 @@
1−#[repr(u8)]
2−#[allow(non_camel_case_types)]
3−pub enum Instruction {
4− MOV_RI = 0x01,
5− MOV_RR = 0x08,
6− ADD_RR = 0x02,
7− ADD_RI = 0x0A,
8− SUB_RR = 0x03,
9− SUB_RI = 0x0B,
10− JMP = 0x04,
11− JZ = 0x05,
12− JNZ = 0x06,
13− CMP_RI = 0x07,
14− CMP_RR = 0x09,
15− MUL = 0x0C,
16− DIV = 0x0D,
17− CALL = 0x0E,
18− RET = 0x0F,
19− SYS = 0x10,
20− HLT = 0xFF,
21−}
22−
23−impl Instruction {
24− pub fn opcode_name(op: u8) -> &'static str {
25− match op {
26− 0x01 | 0x08 => "MOV",
27− 0x02 | 0x0A => "ADD",
28− 0x03 | 0x0B => "SUB",
29− 0x04 => "JMP",
30− 0x05 => "JZ",
31− 0x06 => "JNZ",
32− 0x07 | 0x09 => "CMP",
33− 0x0C => "MUL",
34− 0x0D => "DIV",
35− 0x0E => "CALL",
36− 0x0F => "RET",
37− 0x10 => "SYS",
38− 0xFF => "HLT",
39−
40− _ => "???",
41− }
42− }
43−}
A src/isa.rs View
@@ -0,0 +1,347 @@
1+//! The 8051 instruction set as a single opcode table, shared by the
2+//! assembler (pattern -> opcode), the disassembler and the simulator
3+//! (sizes and machine-cycle counts).
4+
5+use std::sync::OnceLock;
6+
7+/// Operand forms as they appear in an instruction encoding.
8+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9+pub enum Operand {
10+ A,
11+ AB,
12+ C,
13+ Dptr,
14+ Reg(u8),
15+ AtReg(u8),
16+ AtDptr,
17+ AtAPlusDptr,
18+ AtAPlusPc,
19+ Imm8,
20+ Imm16,
21+ Direct,
22+ Bit,
23+ NotBit,
24+ Addr11,
25+ Addr16,
26+ Rel,
27+}
28+
29+impl Operand {
30+ /// Bytes this operand occupies after the opcode byte.
31+ pub fn len(self) -> u8 {
32+ match self {
33+ Operand::Imm8 | Operand::Direct | Operand::Bit | Operand::NotBit => 1,
34+ Operand::Addr11 | Operand::Rel => 1,
35+ Operand::Imm16 | Operand::Addr16 => 2,
36+ _ => 0,
37+ }
38+ }
39+}
40+
41+#[derive(Debug, Clone, Copy)]
42+pub struct OpInfo {
43+ pub opcode: u8,
44+ pub mnemonic: &'static str,
45+ pub operands: &'static [Operand],
46+ pub size: u8,
47+ pub cycles: u8,
48+}
49+
50+use Operand::*;
51+
52+const RN: [[Operand; 1]; 8] = [
53+ [Reg(0)],
54+ [Reg(1)],
55+ [Reg(2)],
56+ [Reg(3)],
57+ [Reg(4)],
58+ [Reg(5)],
59+ [Reg(6)],
60+ [Reg(7)],
61+];
62+const A_RN: [[Operand; 2]; 8] = [
63+ [A, Reg(0)],
64+ [A, Reg(1)],
65+ [A, Reg(2)],
66+ [A, Reg(3)],
67+ [A, Reg(4)],
68+ [A, Reg(5)],
69+ [A, Reg(6)],
70+ [A, Reg(7)],
71+];
72+const RN_A: [[Operand; 2]; 8] = [
73+ [Reg(0), A],
74+ [Reg(1), A],
75+ [Reg(2), A],
76+ [Reg(3), A],
77+ [Reg(4), A],
78+ [Reg(5), A],
79+ [Reg(6), A],
80+ [Reg(7), A],
81+];
82+const RN_IMM: [[Operand; 2]; 8] = [
83+ [Reg(0), Imm8],
84+ [Reg(1), Imm8],
85+ [Reg(2), Imm8],
86+ [Reg(3), Imm8],
87+ [Reg(4), Imm8],
88+ [Reg(5), Imm8],
89+ [Reg(6), Imm8],
90+ [Reg(7), Imm8],
91+];
92+const DIR_RN: [[Operand; 2]; 8] = [
93+ [Direct, Reg(0)],
94+ [Direct, Reg(1)],
95+ [Direct, Reg(2)],
96+ [Direct, Reg(3)],
97+ [Direct, Reg(4)],
98+ [Direct, Reg(5)],
99+ [Direct, Reg(6)],
100+ [Direct, Reg(7)],
101+];
102+const RN_DIR: [[Operand; 2]; 8] = [
103+ [Reg(0), Direct],
104+ [Reg(1), Direct],
105+ [Reg(2), Direct],
106+ [Reg(3), Direct],
107+ [Reg(4), Direct],
108+ [Reg(5), Direct],
109+ [Reg(6), Direct],
110+ [Reg(7), Direct],
111+];
112+const RN_REL: [[Operand; 2]; 8] = [
113+ [Reg(0), Rel],
114+ [Reg(1), Rel],
115+ [Reg(2), Rel],
116+ [Reg(3), Rel],
117+ [Reg(4), Rel],
118+ [Reg(5), Rel],
119+ [Reg(6), Rel],
120+ [Reg(7), Rel],
121+];
122+const RN_IMM_REL: [[Operand; 3]; 8] = [
123+ [Reg(0), Imm8, Rel],
124+ [Reg(1), Imm8, Rel],
125+ [Reg(2), Imm8, Rel],
126+ [Reg(3), Imm8, Rel],
127+ [Reg(4), Imm8, Rel],
128+ [Reg(5), Imm8, Rel],
129+ [Reg(6), Imm8, Rel],
130+ [Reg(7), Imm8, Rel],
131+];
132+const RI: [[Operand; 1]; 2] = [[AtReg(0)], [AtReg(1)]];
133+const A_RI: [[Operand; 2]; 2] = [[A, AtReg(0)], [A, AtReg(1)]];
134+const RI_A: [[Operand; 2]; 2] = [[AtReg(0), A], [AtReg(1), A]];
135+const RI_IMM: [[Operand; 2]; 2] = [[AtReg(0), Imm8], [AtReg(1), Imm8]];
136+const DIR_RI: [[Operand; 2]; 2] = [[Direct, AtReg(0)], [Direct, AtReg(1)]];
137+const RI_DIR: [[Operand; 2]; 2] = [[AtReg(0), Direct], [AtReg(1), Direct]];
138+const RI_IMM_REL: [[Operand; 3]; 2] = [[AtReg(0), Imm8, Rel], [AtReg(1), Imm8, Rel]];
139+
140+/// Fixed (non register-indexed) opcodes.
141+const FIXED: &[(u8, &str, &[Operand], u8)] = &[
142+ (0x00, "NOP", &[], 1),
143+ (0x02, "LJMP", &[Addr16], 2),
144+ (0x03, "RR", &[A], 1),
145+ (0x04, "INC", &[A], 1),
146+ (0x05, "INC", &[Direct], 1),
147+ (0x10, "JBC", &[Bit, Rel], 2),
148+ (0x12, "LCALL", &[Addr16], 2),
149+ (0x13, "RRC", &[A], 1),
150+ (0x14, "DEC", &[A], 1),
151+ (0x15, "DEC", &[Direct], 1),
152+ (0x20, "JB", &[Bit, Rel], 2),
153+ (0x22, "RET", &[], 2),
154+ (0x23, "RL", &[A], 1),
155+ (0x24, "ADD", &[A, Imm8], 1),
156+ (0x25, "ADD", &[A, Direct], 1),
157+ (0x30, "JNB", &[Bit, Rel], 2),
158+ (0x32, "RETI", &[], 2),
159+ (0x33, "RLC", &[A], 1),
160+ (0x34, "ADDC", &[A, Imm8], 1),
161+ (0x35, "ADDC", &[A, Direct], 1),
162+ (0x40, "JC", &[Rel], 2),
163+ (0x42, "ORL", &[Direct, A], 1),
164+ (0x43, "ORL", &[Direct, Imm8], 2),
165+ (0x44, "ORL", &[A, Imm8], 1),
166+ (0x45, "ORL", &[A, Direct], 1),
167+ (0x50, "JNC", &[Rel], 2),
168+ (0x52, "ANL", &[Direct, A], 1),
169+ (0x53, "ANL", &[Direct, Imm8], 2),
170+ (0x54, "ANL", &[A, Imm8], 1),
171+ (0x55, "ANL", &[A, Direct], 1),
172+ (0x60, "JZ", &[Rel], 2),
173+ (0x62, "XRL", &[Direct, A], 1),
174+ (0x63, "XRL", &[Direct, Imm8], 2),
175+ (0x64, "XRL", &[A, Imm8], 1),
176+ (0x65, "XRL", &[A, Direct], 1),
177+ (0x70, "JNZ", &[Rel], 2),
178+ (0x72, "ORL", &[C, Bit], 2),
179+ (0x73, "JMP", &[AtAPlusDptr], 2),
180+ (0x74, "MOV", &[A, Imm8], 1),
181+ (0x75, "MOV", &[Direct, Imm8], 2),
182+ (0x80, "SJMP", &[Rel], 2),
183+ (0x82, "ANL", &[C, Bit], 2),
184+ (0x83, "MOVC", &[A, AtAPlusPc], 2),
185+ (0x84, "DIV", &[AB], 4),
186+ // Encoded as: opcode, src, dest (handled specially by the encoder).
187+ (0x85, "MOV", &[Direct, Direct], 2),
188+ (0x90, "MOV", &[Dptr, Imm16], 2),
189+ (0x92, "MOV", &[Bit, C], 2),
190+ (0x93, "MOVC", &[A, AtAPlusDptr], 2),
191+ (0x94, "SUBB", &[A, Imm8], 1),
192+ (0x95, "SUBB", &[A, Direct], 1),
193+ (0xA0, "ORL", &[C, NotBit], 2),
194+ (0xA2, "MOV", &[C, Bit], 1),
195+ (0xA3, "INC", &[Dptr], 2),
196+ (0xA4, "MUL", &[AB], 4),
197+ (0xB0, "ANL", &[C, NotBit], 2),
198+ (0xB2, "CPL", &[Bit], 1),
199+ (0xB3, "CPL", &[C], 1),
200+ (0xB4, "CJNE", &[A, Imm8, Rel], 2),
201+ (0xB5, "CJNE", &[A, Direct, Rel], 2),
202+ (0xC0, "PUSH", &[Direct], 2),
203+ (0xC2, "CLR", &[Bit], 1),
204+ (0xC3, "CLR", &[C], 1),
205+ (0xC4, "SWAP", &[A], 1),
206+ (0xC5, "XCH", &[A, Direct], 1),
207+ (0xD0, "POP", &[Direct], 2),
208+ (0xD2, "SETB", &[Bit], 1),
209+ (0xD3, "SETB", &[C], 1),
210+ (0xD4, "DA", &[A], 1),
211+ (0xD5, "DJNZ", &[Direct, Rel], 2),
212+ (0xE0, "MOVX", &[A, AtDptr], 2),
213+ (0xE4, "CLR", &[A], 1),
214+ (0xE5, "MOV", &[A, Direct], 1),
215+ (0xF0, "MOVX", &[AtDptr, A], 2),
216+ (0xF4, "CPL", &[A], 1),
217+ (0xF5, "MOV", &[Direct, A], 1),
218+];
219+
220+/// Register-indexed groups: (base opcode, mnemonic, cycles, @Ri forms, Rn forms).
221+type Group = (
222+ u8,
223+ &'static str,
224+ u8,
225+ Option<&'static [[Operand; 2]; 2]>,
226+ Option<&'static [[Operand; 2]; 8]>,
227+);
228+const GROUPS2: &[Group] = &[
229+ (0x20, "ADD", 1, Some(&A_RI), Some(&A_RN)),
230+ (0x30, "ADDC", 1, Some(&A_RI), Some(&A_RN)),
231+ (0x40, "ORL", 1, Some(&A_RI), Some(&A_RN)),
232+ (0x50, "ANL", 1, Some(&A_RI), Some(&A_RN)),
233+ (0x60, "XRL", 1, Some(&A_RI), Some(&A_RN)),
234+ (0x70, "MOV", 1, Some(&RI_IMM), Some(&RN_IMM)),
235+ (0x80, "MOV", 2, Some(&DIR_RI), Some(&DIR_RN)),
236+ (0x90, "SUBB", 1, Some(&A_RI), Some(&A_RN)),
237+ (0xA0, "MOV", 2, Some(&RI_DIR), Some(&RN_DIR)),
238+ (0xC0, "XCH", 1, Some(&A_RI), Some(&A_RN)),
239+ (0xD0, "XCHD", 1, Some(&A_RI), None),
240+ (0xD0, "DJNZ", 2, None, Some(&RN_REL)),
241+ (0xE0, "MOV", 1, Some(&A_RI), Some(&A_RN)),
242+ (0xF0, "MOV", 1, Some(&RI_A), Some(&RN_A)),
243+];
244+
245+fn size_of(ops: &[Operand]) -> u8 {
246+ 1 + ops.iter().map(|o| o.len()).sum::<u8>()
247+}
248+
249+fn build() -> Vec<Option<OpInfo>> {
250+ let mut t: Vec<Option<OpInfo>> = vec![None; 256];
251+ let mut put = |opcode: u8, mnemonic: &'static str, operands: &'static [Operand], cycles: u8| {
252+ assert!(
253+ t[opcode as usize].is_none(),
254+ "duplicate opcode {opcode:02X}"
255+ );
256+ t[opcode as usize] = Some(OpInfo {
257+ opcode,
258+ mnemonic,
259+ operands,
260+ size: size_of(operands),
261+ cycles,
262+ });
263+ };
264+
265+ for &(op, m, ops, cyc) in FIXED {
266+ put(op, m, ops, cyc);
267+ }
268+ // AJMP / ACALL: addr11 high bits live in the opcode's top three bits.
269+ for page in 0..8u8 {
270+ put((page << 5) | 0x01, "AJMP", &[Addr11], 2);
271+ put((page << 5) | 0x11, "ACALL", &[Addr11], 2);
272+ }
273+ for i in 0..2 {
274+ put(0x06 + i as u8, "INC", &RI[i], 1);
275+ put(0x16 + i as u8, "DEC", &RI[i], 1);
276+ put(0xB6 + i as u8, "CJNE", &RI_IMM_REL[i], 2);
277+ put(0xE2 + i as u8, "MOVX", &A_RI[i], 2);
278+ put(0xF2 + i as u8, "MOVX", &RI_A[i], 2);
279+ }
280+ for n in 0..8 {
281+ put(0x08 + n as u8, "INC", &RN[n], 1);
282+ put(0x18 + n as u8, "DEC", &RN[n], 1);
283+ put(0xB8 + n as u8, "CJNE", &RN_IMM_REL[n], 2);
284+ }
285+ for &(base, m, cyc, ri, rn) in GROUPS2 {
286+ if let Some(ri) = ri {
287+ for (i, ops) in ri.iter().enumerate() {
288+ put(base + 6 + i as u8, m, ops, cyc);
289+ }
290+ }
291+ if let Some(rn) = rn {
292+ for (n, ops) in rn.iter().enumerate() {
293+ put(base + 8 + n as u8, m, ops, cyc);
294+ }
295+ }
296+ }
297+ t
298+}
299+
300+fn table() -> &'static [Option<OpInfo>] {
301+ static TABLE: OnceLock<Vec<Option<OpInfo>>> = OnceLock::new();
302+ TABLE.get_or_init(build)
303+}
304+
305+/// Info for an opcode; `None` only for the reserved opcode 0xA5.
306+pub fn info(opcode: u8) -> Option<&'static OpInfo> {
307+ table()[opcode as usize].as_ref()
308+}
309+
310+/// All defined opcodes with the given mnemonic (upper case).
311+pub fn by_mnemonic(mnemonic: &str) -> impl Iterator<Item = &'static OpInfo> + '_ {
312+ table()
313+ .iter()
314+ .flatten()
315+ .filter(move |i| i.mnemonic == mnemonic)
316+}
317+
318+pub fn is_mnemonic(s: &str) -> bool {
319+ matches!(s, "JMP" | "CALL") || table().iter().flatten().any(|i| i.mnemonic == s)
320+}
321+
322+#[cfg(test)]
323+mod tests {
324+ use super::*;
325+
326+ #[test]
327+ fn only_a5_is_undefined() {
328+ let undefined: Vec<_> = (0..=255u8).filter(|&op| info(op).is_none()).collect();
329+ assert_eq!(undefined, vec![0xA5]);
330+ }
331+
332+ #[test]
333+ fn spot_check_sizes_and_cycles() {
334+ let i = info(0x85).unwrap();
335+ assert_eq!((i.mnemonic, i.size, i.cycles), ("MOV", 3, 2));
336+ let i = info(0xB8).unwrap();
337+ assert_eq!((i.mnemonic, i.size, i.cycles), ("CJNE", 3, 2));
338+ let i = info(0xD6).unwrap();
339+ assert_eq!((i.mnemonic, i.size), ("XCHD", 1));
340+ let i = info(0xA4).unwrap();
341+ assert_eq!((i.mnemonic, i.cycles), ("MUL", 4));
342+ let i = info(0xF1).unwrap();
343+ assert_eq!((i.mnemonic, i.size), ("ACALL", 2));
344+ let i = info(0x90).unwrap();
345+ assert_eq!((i.mnemonic, i.size), ("MOV", 3));
346+ }
347+}
M src/main.rs View
@@ -1,43 +1,95 @@
1−mod assembler;
2−mod cpu;
3−mod instructions;
4−mod memory;
1+mod asm;
2+mod isa;
3+mod sfr;
4+mod sim;
5+mod ui;
56
6−use std::io;
7+use std::path::PathBuf;
8+use std::process::ExitCode;
79
8−use crate::assembler::assembler;
910 use clap::Parser;
10−use cpu::CPU;
11−use memory::Memory;
1211
12+/// Intel 8051 assembler, simulator and TUI debugger.
1313 #[derive(Parser, Debug)]
14−#[command(version, about, long_about = None)]
14+#[command(version, about)]
1515 struct Args {
16− #[arg(short, long)]
17− filename: String,
18−}
16+ /// Assembly source (.asm) or Intel HEX (.hex) file to open.
17+ file: Option<PathBuf>,
1918
20−fn main() {
21− let args = Args::parse();
19+ /// Assemble FILE without starting the TUI, writing the Intel HEX file.
20+ #[arg(short, long, requires = "file")]
21+ assemble: bool,
2222
23− let mut cpu = CPU {
24− sp: 0xFFFE,
25− ..Default::default()
26− };
27− let mut mem = Memory::new();
23+ /// Output HEX path (default: FILE with a .hex extension).
24+ #[arg(short, long, requires = "assemble")]
25+ output: Option<PathBuf>,
2826
29− let asm = std::fs::read_to_string(args.filename).unwrap();
30− let program = assembler(&asm);
27+ /// Also write a listing to this path (default: FILE with a .lst extension).
28+ #[arg(long, requires = "assemble")]
29+ lst: Option<PathBuf>,
3130
32− for (i, byte) in program.iter().enumerate() {
33− mem.write(i as u16, *byte);
34− }
31+ /// Oscillator frequency in MHz, used to show elapsed time.
32+ #[arg(long, default_value_t = 11.0592)]
33+ clock: f64,
34+}
3535
36− while !cpu.halted {
37− cpu.debug_instr(&mem);
38− cpu.step(&mut mem);
36+fn assemble_file(args: &Args) -> ExitCode {
37+ let path = args.file.as_ref().unwrap();
38+ let source = match std::fs::read_to_string(path) {
39+ Ok(s) => s,
40+ Err(e) => {
41+ eprintln!("error: cannot read {}: {e}", path.display());
42+ return ExitCode::FAILURE;
43+ }
44+ };
45+ let out = asm::assemble(&source);
46+ let lst = args
47+ .lst
48+ .clone()
49+ .unwrap_or_else(|| path.with_extension("lst"));
50+ if let Err(e) = std::fs::write(&lst, out.listing(&source)) {
51+ eprintln!("error: cannot write {}: {e}", lst.display());
52+ }
53+ if !out.ok() {
54+ for d in &out.diagnostics {
55+ eprintln!(
56+ "{}:{}:{}: error: {}",
57+ path.display(),
58+ d.line + 1,
59+ d.col + 1,
60+ d.msg
61+ );
62+ }
63+ eprintln!("{} error(s); see {}", out.diagnostics.len(), lst.display());
64+ return ExitCode::FAILURE;
65+ }
66+ let hex = args
67+ .output
68+ .clone()
69+ .unwrap_or_else(|| path.with_extension("hex"));
70+ if let Err(e) = std::fs::write(&hex, out.intel_hex()) {
71+ eprintln!("error: cannot write {}: {e}", hex.display());
72+ return ExitCode::FAILURE;
73+ }
74+ println!(
75+ "{} bytes -> {}, listing -> {}",
76+ out.code_size(),
77+ hex.display(),
78+ lst.display()
79+ );
80+ ExitCode::SUCCESS
81+}
3982
40− let mut buf = String::new();
41− io::stdin().read_line(&mut buf).unwrap();
83+fn main() -> ExitCode {
84+ let args = Args::parse();
85+ if args.assemble {
86+ return assemble_file(&args);
87+ }
88+ match ui::run(args.file, args.clock) {
89+ Ok(()) => ExitCode::SUCCESS,
90+ Err(e) => {
91+ eprintln!("error: {e}");
92+ ExitCode::FAILURE
93+ }
4294 }
4395 }
D src/memory.rs View
@@ -1,17 +0,0 @@
1−pub struct Memory {
2− pub data: [u8; 65536],
3−}
4−
5−impl Memory {
6− pub fn new() -> Self {
7− Self { data: [0; 65536] }
8− }
9−
10− pub fn read(&self, addr: u16) -> u8 {
11− self.data[addr as usize]
12− }
13−
14− pub fn write(&mut self, addr: u16, value: u8) {
15− self.data[addr as usize] = value;
16− }
17−}
A src/sfr.rs View
@@ -0,0 +1,146 @@
1+//! Special function register and bit addresses of the standard 8051.
2+
3+pub const P0: u8 = 0x80;
4+pub const SP: u8 = 0x81;
5+pub const DPL: u8 = 0x82;
6+pub const DPH: u8 = 0x83;
7+pub const PCON: u8 = 0x87;
8+pub const TCON: u8 = 0x88;
9+pub const TMOD: u8 = 0x89;
10+pub const TL0: u8 = 0x8A;
11+pub const TL1: u8 = 0x8B;
12+pub const TH0: u8 = 0x8C;
13+pub const TH1: u8 = 0x8D;
14+pub const P1: u8 = 0x90;
15+pub const SCON: u8 = 0x98;
16+pub const SBUF: u8 = 0x99;
17+pub const P2: u8 = 0xA0;
18+pub const IE: u8 = 0xA8;
19+pub const P3: u8 = 0xB0;
20+pub const IP: u8 = 0xB8;
21+pub const PSW: u8 = 0xD0;
22+pub const ACC: u8 = 0xE0;
23+pub const B: u8 = 0xF0;
24+
25+pub const SFRS: &[(&str, u8)] = &[
26+ ("P0", P0),
27+ ("SP", SP),
28+ ("DPL", DPL),
29+ ("DPH", DPH),
30+ ("PCON", PCON),
31+ ("TCON", TCON),
32+ ("TMOD", TMOD),
33+ ("TL0", TL0),
34+ ("TL1", TL1),
35+ ("TH0", TH0),
36+ ("TH1", TH1),
37+ ("P1", P1),
38+ ("SCON", SCON),
39+ ("SBUF", SBUF),
40+ ("P2", P2),
41+ ("IE", IE),
42+ ("P3", P3),
43+ ("IP", IP),
44+ ("PSW", PSW),
45+ ("ACC", ACC),
46+ ("B", B),
47+];
48+
49+pub const BITS: &[(&str, u8)] = &[
50+ ("IT0", 0x88),
51+ ("IE0", 0x89),
52+ ("IT1", 0x8A),
53+ ("IE1", 0x8B),
54+ ("TR0", 0x8C),
55+ ("TF0", 0x8D),
56+ ("TR1", 0x8E),
57+ ("TF1", 0x8F),
58+ ("RI", 0x98),
59+ ("TI", 0x99),
60+ ("RB8", 0x9A),
61+ ("TB8", 0x9B),
62+ ("REN", 0x9C),
63+ ("SM2", 0x9D),
64+ ("SM1", 0x9E),
65+ ("SM0", 0x9F),
66+ ("EX0", 0xA8),
67+ ("ET0", 0xA9),
68+ ("EX1", 0xAA),
69+ ("ET1", 0xAB),
70+ ("ES", 0xAC),
71+ ("EA", 0xAF),
72+ ("RXD", 0xB0),
73+ ("TXD", 0xB1),
74+ ("INT0", 0xB2),
75+ ("INT1", 0xB3),
76+ ("T0", 0xB4),
77+ ("T1", 0xB5),
78+ ("WR", 0xB6),
79+ ("RD", 0xB7),
80+ ("PX0", 0xB8),
81+ ("PT0", 0xB9),
82+ ("PX1", 0xBA),
83+ ("PT1", 0xBB),
84+ ("PS", 0xBC),
85+ ("P", 0xD0),
86+ ("OV", 0xD2),
87+ ("RS0", 0xD3),
88+ ("RS1", 0xD4),
89+ ("F0", 0xD5),
90+ ("AC", 0xD6),
91+ ("CY", 0xD7),
92+];
93+
94+pub fn sfr_name(addr: u8) -> Option<&'static str> {
95+ SFRS.iter().find(|&&(_, a)| a == addr).map(|&(n, _)| n)
96+}
97+
98+pub fn bit_name(addr: u8) -> Option<&'static str> {
99+ BITS.iter().find(|&&(_, a)| a == addr).map(|&(n, _)| n)
100+}
101+
102+/// Byte address and bit index of a bit address.
103+pub fn bit_location(bit: u8) -> (u8, u8) {
104+ if bit < 0x80 {
105+ (0x20 + bit / 8, bit % 8)
106+ } else {
107+ (bit & 0xF8, bit & 7)
108+ }
109+}
110+
111+/// Human-readable bit operand, e.g. `TR0`, `P1.3` or `20h.5`.
112+pub fn format_bit(bit: u8) -> String {
113+ if let Some(n) = bit_name(bit) {
114+ return n.to_string();
115+ }
116+ let (byte, idx) = bit_location(bit);
117+ match sfr_name(byte) {
118+ Some(n) => format!("{n}.{idx}"),
119+ None => format!("{}.{idx}", hex8(byte)),
120+ }
121+}
122+
123+/// Human-readable direct address: SFR name or hex.
124+pub fn format_direct(addr: u8) -> String {
125+ match sfr_name(addr) {
126+ Some(n) => n.to_string(),
127+ None => hex8(addr),
128+ }
129+}
130+
131+/// Assembler-style hex literal, with a leading zero when needed (`0A0h`).
132+pub fn hex8(v: u8) -> String {
133+ if v >= 0xA0 {
134+ format!("0{v:02X}h")
135+ } else {
136+ format!("{v:02X}h")
137+ }
138+}
139+
140+pub fn hex16(v: u16) -> String {
141+ if v >= 0xA000 {
142+ format!("0{v:04X}h")
143+ } else {
144+ format!("{v:04X}h")
145+ }
146+}
A src/sim/cpu.rs View
@@ -0,0 +1,528 @@
1+//! Instruction execution.
2+
3+use super::{AC, CY, OV, Sim};
4+use crate::sfr::{B, P2};
5+
6+/// Operand location for the register-indexed columns (opcode low nibble 5..F).
7+#[derive(Clone, Copy)]
8+enum Loc {
9+ Direct(u8),
10+ Iram(u8),
11+}
12+
13+impl Sim {
14+ fn fetch(&mut self) -> u8 {
15+ let b = self.code[self.pc as usize];
16+ self.pc = self.pc.wrapping_add(1);
17+ b
18+ }
19+
20+ fn fetch16(&mut self) -> u16 {
21+ let h = self.fetch();
22+ let l = self.fetch();
23+ u16::from_be_bytes([h, l])
24+ }
25+
26+ fn rel_jump(&mut self, off: u8) {
27+ self.pc = self.pc.wrapping_add(off as i8 as u16);
28+ }
29+
30+ /// Decodes the operand selected by the low nibble: 5 = direct,
31+ /// 6/7 = @R0/@R1, 8..F = R0..R7.
32+ fn loc(&mut self, op: u8) -> Loc {
33+ match op & 0x0F {
34+ 0x05 => Loc::Direct(self.fetch()),
35+ 0x06 | 0x07 => Loc::Iram(self.reg(op & 1)),
36+ n => Loc::Iram(self.reg_addr(n & 7)),
37+ }
38+ }
39+
40+ fn read_loc(&self, l: Loc, rmw: bool) -> u8 {
41+ match l {
42+ Loc::Direct(a) => self.read_direct(a, rmw),
43+ Loc::Iram(a) => self.iram[a as usize],
44+ }
45+ }
46+
47+ fn write_loc(&mut self, l: Loc, v: u8) {
48+ match l {
49+ Loc::Direct(a) => self.write_direct(a, v),
50+ Loc::Iram(a) => self.iram[a as usize] = v,
51+ }
52+ }
53+
54+ /// Source operand for the arithmetic/logic rows: 4 = #imm, otherwise `loc`.
55+ fn src(&mut self, op: u8) -> u8 {
56+ if op & 0x0F == 0x04 {
57+ self.fetch()
58+ } else {
59+ let l = self.loc(op);
60+ self.read_loc(l, false)
61+ }
62+ }
63+
64+ fn add(&mut self, v: u8, carry: bool) {
65+ let a = self.acc();
66+ let c = carry as u16;
67+ let sum = a as u16 + v as u16 + c;
68+ let r = sum as u8;
69+ self.set_flag(CY, sum > 0xFF);
70+ self.set_flag(AC, (a & 0x0F) as u16 + (v & 0x0F) as u16 + c > 0x0F);
71+ self.set_flag(OV, (a ^ r) & (v ^ r) & 0x80 != 0);
72+ self.set_acc(r);
73+ }
74+
75+ fn subb(&mut self, v: u8) {
76+ let a = self.acc();
77+ let c = self.flag(CY) as i16;
78+ let diff = a as i16 - v as i16 - c;
79+ let r = diff as u8;
80+ self.set_flag(CY, diff < 0);
81+ self.set_flag(AC, ((a & 0x0F) as i16) < (v & 0x0F) as i16 + c);
82+ self.set_flag(OV, (a ^ v) & (a ^ r) & 0x80 != 0);
83+ self.set_acc(r);
84+ }
85+
86+ fn cjne(&mut self, a: u8, b: u8) {
87+ let off = self.fetch();
88+ self.set_flag(CY, a < b);
89+ if a != b {
90+ self.rel_jump(off);
91+ }
92+ }
93+
94+ fn movx_addr_ri(&self, op: u8) -> u16 {
95+ // The high address byte comes from the P2 latch.
96+ u16::from_be_bytes([self.sfr(P2), self.reg(op & 1)])
97+ }
98+
99+ pub(super) fn execute(&mut self, op: u8) {
100+ match op {
101+ 0x00 => {}
102+
103+ // AJMP / ACALL addr11
104+ _ if op & 0x1F == 0x01 || op & 0x1F == 0x11 => {
105+ let lo = self.fetch();
106+ let target = (self.pc & 0xF800) | ((op as u16 >> 5) << 8) | lo as u16;
107+ if op & 0x10 != 0 {
108+ self.push_pc();
109+ }
110+ self.pc = target;
111+ }
112+ 0x02 => self.pc = self.fetch16(),
113+ 0x12 => {
114+ let target = self.fetch16();
115+ self.push_pc();
116+ self.pc = target;
117+ }
118+ 0x22 => self.pop_pc(),
119+ 0x32 => {
120+ self.pop_pc();
121+ self.in_service.pop();
122+ self.irq_hold = true;
123+ }
124+ 0x73 => self.pc = self.dptr().wrapping_add(self.acc() as u16),
125+ 0x80 => {
126+ let off = self.fetch();
127+ self.rel_jump(off);
128+ }
129+
130+ // Rotates
131+ 0x03 => self.set_acc(self.acc().rotate_right(1)),
132+ 0x23 => self.set_acc(self.acc().rotate_left(1)),
133+ 0x13 => {
134+ let a = self.acc();
135+ self.set_acc((a >> 1) | ((self.flag(CY) as u8) << 7));
136+ self.set_flag(CY, a & 1 != 0);
137+ }
138+ 0x33 => {
139+ let a = self.acc();
140+ self.set_acc((a << 1) | self.flag(CY) as u8);
141+ self.set_flag(CY, a & 0x80 != 0);
142+ }
143+
144+ // INC / DEC
145+ 0x04 => self.set_acc(self.acc().wrapping_add(1)),
146+ 0x14 => self.set_acc(self.acc().wrapping_sub(1)),
147+ 0x05..=0x0F | 0x15..=0x1F => {
148+ let l = self.loc(op);
149+ let v = self.read_loc(l, true);
150+ self.write_loc(
151+ l,
152+ if op < 0x10 {
153+ v.wrapping_add(1)
154+ } else {
155+ v.wrapping_sub(1)
156+ },
157+ );
158+ }
159+ 0xA3 => self.set_dptr(self.dptr().wrapping_add(1)),
160+
161+ // Conditional jumps on bits
162+ 0x10 | 0x20 | 0x30 => {
163+ let bit = self.fetch();
164+ let off = self.fetch();
165+ let set = self.read_bit(bit, op == 0x10);
166+ if op == 0x10 && set {
167+ self.write_bit(bit, false);
168+ }
169+ if set == (op != 0x30) {
170+ self.rel_jump(off);
171+ }
172+ }
173+ 0x40 | 0x50 | 0x60 | 0x70 => {
174+ let off = self.fetch();
175+ let take = match op {
176+ 0x40 => self.flag(CY),
177+ 0x50 => !self.flag(CY),
178+ 0x60 => self.acc() == 0,
179+ _ => self.acc() != 0,
180+ };
181+ if take {
182+ self.rel_jump(off);
183+ }
184+ }
185+
186+ // Arithmetic
187+ 0x24..=0x2F => {
188+ let v = self.src(op);
189+ self.add(v, false);
190+ }
191+ 0x34..=0x3F => {
192+ let v = self.src(op);
193+ self.add(v, self.flag(CY));
194+ }
195+ 0x94..=0x9F => {
196+ let v = self.src(op);
197+ self.subb(v);
198+ }
199+ 0x84 => {
200+ let (a, b) = (self.acc(), self.b());
201+ self.set_flag(CY, false);
202+ // Division by zero sets OV and leaves A and B unchanged.
203+ match a.checked_div(b) {
204+ Some(q) => {
205+ self.set_acc(q);
206+ self.set_sfr(B, a % b);
207+ self.set_flag(OV, false);
208+ }
209+ None => self.set_flag(OV, true),
210+ }
211+ }
212+ 0xA4 => {
213+ let r = self.acc() as u16 * self.b() as u16;
214+ self.set_acc(r as u8);
215+ self.set_sfr(B, (r >> 8) as u8);
216+ self.set_flag(CY, false);
217+ self.set_flag(OV, r > 0xFF);
218+ }
219+ 0xD4 => {
220+ let mut a = self.acc() as u16;
221+ if a & 0x0F > 9 || self.flag(AC) {
222+ a += 0x06;
223+ }
224+ if a > 0xFF {
225+ self.set_flag(CY, true);
226+ }
227+ if (a >> 4) & 0x1F > 9 || self.flag(CY) {
228+ a += 0x60;
229+ }
230+ if a > 0xFF {
231+ self.set_flag(CY, true);
232+ }
233+ self.set_acc(a as u8);
234+ }
235+
236+ // Logic: ORL/ANL/XRL direct,A / direct,#imm / A,src
237+ 0x42 | 0x43 | 0x52 | 0x53 | 0x62 | 0x63 => {
238+ let addr = self.fetch();
239+ let v = if op & 1 == 1 {
240+ self.fetch()
241+ } else {
242+ self.acc()
243+ };
244+ let d = self.read_direct(addr, true);
245+ self.write_direct(addr, logic(op, d, v));
246+ }
247+ 0x44..=0x4F | 0x54..=0x5F | 0x64..=0x6F => {
248+ let v = self.src(op);
249+ self.set_acc(logic(op, self.acc(), v));
250+ }
251+ 0xC4 => self.set_acc(self.acc().rotate_left(4)),
252+ 0xE4 => self.set_acc(0),
253+ 0xF4 => self.set_acc(!self.acc()),
254+
255+ // Boolean (carry) operations
256+ 0x72 | 0x82 | 0xA0 | 0xB0 => {
257+ let bit = self.fetch();
258+ let v = self.read_bit(bit, false) != (op >= 0xA0);
259+ let c = self.flag(CY);
260+ self.set_flag(CY, if op & 0x10 == 0 { c || v } else { c && v });
261+ }
262+ 0x92 => {
263+ let bit = self.fetch();
264+ self.write_bit(bit, self.flag(CY));
265+ }
266+ 0xA2 => {
267+ let bit = self.fetch();
268+ self.set_flag(CY, self.read_bit(bit, false));
269+ }
270+ 0xB2 | 0xC2 | 0xD2 => {
271+ let bit = self.fetch();
272+ let v = match op {
273+ 0xB2 => !self.read_bit(bit, true),
274+ 0xC2 => false,
275+ _ => true,
276+ };
277+ self.write_bit(bit, v);
278+ }
279+ 0xB3 => self.set_flag(CY, !self.flag(CY)),
280+ 0xC3 => self.set_flag(CY, false),
281+ 0xD3 => self.set_flag(CY, true),
282+
283+ // Data transfer
284+ 0x74 => {
285+ let v = self.fetch();
286+ self.set_acc(v);
287+ }
288+ 0x75 => {
289+ let addr = self.fetch();
290+ let v = self.fetch();
291+ self.write_direct(addr, v);
292+ }
293+ 0x76..=0x7F => {
294+ let l = self.loc(op);
295+ let v = self.fetch();
296+ self.write_loc(l, v);
297+ }
298+ 0x85 => {
299+ let src = self.fetch();
300+ let dst = self.fetch();
301+ let v = self.read_direct(src, false);
302+ self.write_direct(dst, v);
303+ }
304+ 0x86..=0x8F => {
305+ let l = self.loc(op);
306+ let addr = self.fetch();
307+ let v = self.read_loc(l, false);
308+ self.write_direct(addr, v);
309+ }
310+ 0xA6..=0xAF => {
311+ let l = self.loc(op);
312+ let addr = self.fetch();
313+ let v = self.read_direct(addr, false);
314+ self.write_loc(l, v);
315+ }
316+ 0xE5..=0xEF => {
317+ let v = self.src(op);
318+ self.set_acc(v);
319+ }
320+ 0xF5..=0xFF => {
321+ let l = self.loc(op);
322+ self.write_loc(l, self.acc());
323+ }
324+ 0x90 => {
325+ let v = self.fetch16();
326+ self.set_dptr(v);
327+ }
328+ 0x83 => self.set_acc(self.code[self.pc.wrapping_add(self.acc() as u16) as usize]),
329+ 0x93 => self.set_acc(self.code[self.dptr().wrapping_add(self.acc() as u16) as usize]),
330+ 0xE0 => self.set_acc(self.xram[self.dptr() as usize]),
331+ 0xF0 => self.xram[self.dptr() as usize] = self.acc(),
332+ 0xE2 | 0xE3 => self.set_acc(self.xram[self.movx_addr_ri(op) as usize]),
333+ 0xF2 | 0xF3 => self.xram[self.movx_addr_ri(op) as usize] = self.acc(),
334+ 0xC0 => {
335+ let addr = self.fetch();
336+ let v = self.read_direct(addr, false);
337+ self.push(v);
338+ }
339+ 0xD0 => {
340+ let addr = self.fetch();
341+ let v = self.pop();
342+ self.write_direct(addr, v);
343+ }
344+ 0xC5..=0xCF => {
345+ let l = self.loc(op);
346+ let v = self.read_loc(l, true);
347+ self.write_loc(l, self.acc());
348+ self.set_acc(v);
349+ }
350+ 0xD6 | 0xD7 => {
351+ let addr = self.reg(op & 1) as usize;
352+ let (a, m) = (self.acc(), self.iram[addr]);
353+ self.set_acc((a & 0xF0) | (m & 0x0F));
354+ self.iram[addr] = (m & 0xF0) | (a & 0x0F);
355+ }
356+
357+ // Compare and loop
358+ 0xB4 => {
359+ let v = self.fetch();
360+ self.cjne(self.acc(), v);
361+ }
362+ 0xB5 => {
363+ let addr = self.fetch();
364+ let v = self.read_direct(addr, false);
365+ self.cjne(self.acc(), v);
366+ }
367+ 0xB6..=0xBF => {
368+ let l = self.loc(op);
369+ let a = self.read_loc(l, false);
370+ let v = self.fetch();
371+ self.cjne(a, v);
372+ }
373+ 0xD5 | 0xD8..=0xDF => {
374+ let l = self.loc(op);
375+ let v = self.read_loc(l, true).wrapping_sub(1);
376+ self.write_loc(l, v);
377+ let off = self.fetch();
378+ if v != 0 {
379+ self.rel_jump(off);
380+ }
381+ }
382+
383+ // 0xA5 is reserved and rejected before `execute` is called.
384+ _ => unreachable!("unhandled opcode {op:02X}"),
385+ }
386+ }
387+}
388+
389+fn logic(op: u8, a: u8, b: u8) -> u8 {
390+ match op & 0xF0 {
391+ 0x40 => a | b,
392+ 0x50 => a & b,
393+ _ => a ^ b,
394+ }
395+}
396+
397+#[cfg(test)]
398+mod tests {
399+ use crate::asm::assemble;
400+ use crate::sim::{AC, CY, OV, PARITY, Sim};
401+ use std::collections::HashSet;
402+
403+ /// Assembles `src`, then steps until the PC reaches the `done` label.
404+ fn run(src: &str) -> Sim {
405+ let src = format!("{src}\ndone: SJMP done\n");
406+ let out = assemble(&src);
407+ assert!(out.ok(), "{:?}", out.diagnostics);
408+ let done = out.symbols["DONE"].value as u16;
409+ let mut s = Sim::new();
410+ s.load_code(out.bytes());
411+ for _ in 0..100_000 {
412+ if s.pc == done {
413+ return s;
414+ }
415+ s.step().unwrap();
416+ }
417+ panic!("program did not reach 'done'");
418+ }
419+
420+ #[test]
421+ fn add_flags() {
422+ let s = run("MOV A,#0x7F\nADD A,#1");
423+ assert_eq!(s.acc(), 0x80);
424+ assert!(s.flag(OV) && s.flag(AC) && !s.flag(CY));
425+ assert!(s.flag(PARITY));
426+ let s = run("MOV A,#0xFF\nSETB C\nADDC A,#0");
427+ assert_eq!(s.acc(), 0);
428+ assert!(s.flag(CY) && s.flag(AC) && !s.flag(OV));
429+ }
430+
431+ #[test]
432+ fn subb_flags() {
433+ let s = run("CLR C\nMOV A,#0x80\nSUBB A,#1");
434+ assert_eq!(s.acc(), 0x7F);
435+ assert!(s.flag(OV) && s.flag(AC) && !s.flag(CY));
436+ let s = run("SETB C\nMOV A,#5\nSUBB A,#5");
437+ assert_eq!(s.acc(), 0xFF);
438+ assert!(s.flag(CY));
439+ }
440+
441+ #[test]
442+ fn decimal_adjust() {
443+ let s = run("MOV A,#0x56\nADD A,#0x67\nDA A");
444+ assert_eq!(s.acc(), 0x23);
445+ assert!(s.flag(CY));
446+ let s = run("MOV A,#0x19\nADD A,#0x28\nDA A");
447+ assert_eq!(s.acc(), 0x47);
448+ assert!(!s.flag(CY));
449+ }
450+
451+ #[test]
452+ fn mul_div() {
453+ let s = run("MOV A,#80\nMOV B,#160\nMUL AB");
454+ assert_eq!((s.acc(), s.b()), (0x00, 0x32));
455+ assert!(s.flag(OV));
456+ let s = run("MOV A,#251\nMOV B,#18\nDIV AB");
457+ assert_eq!((s.acc(), s.b()), (13, 17));
458+ let s = run("MOV B,#0\nDIV AB");
459+ assert!(s.flag(OV));
460+ }
461+
462+ #[test]
463+ fn banks_and_indirect() {
464+ let s = run("MOV R0,#1\nSETB RS0\nMOV R0,#2\nMOV R1,#40h\nMOV @R1,#99\nMOV A,40h");
465+ assert_eq!(s.iram[0], 1);
466+ assert_eq!(s.iram[8], 2);
467+ assert_eq!(s.acc(), 99);
468+ assert_eq!(s.bank(), 1);
469+ }
470+
471+ #[test]
472+ fn stack_and_calls() {
473+ let s = run("MOV A,#5\nPUSH ACC\nMOV A,#0\nPOP B\nCALL sub\nSJMP done\nsub: INC A\nRET");
474+ assert_eq!(s.b(), 5);
475+ assert_eq!(s.acc(), 1);
476+ assert_eq!(s.sp(), 7);
477+ }
478+
479+ #[test]
480+ fn loops_and_compares() {
481+ let s = run("MOV R2,#10\nCLR A\nl: ADD A,#2\nDJNZ R2,l");
482+ assert_eq!(s.acc(), 20);
483+ let s = run("MOV A,#3\nCJNE A,#5,ne\nne: NOP");
484+ assert!(s.flag(CY));
485+ }
486+
487+ #[test]
488+ fn bits() {
489+ let s = run("SETB 20h.3\nMOV C,20h.3\nCPL ACC.0\nANL C,/ACC.0\nMOV 21h.0,C");
490+ assert_eq!(s.iram[0x20], 0x08);
491+ assert_eq!(s.acc(), 1);
492+ assert_eq!(s.iram[0x21], 0);
493+ let s = run("SETB 20h.1\nJBC 20h.1,x\nx: NOP");
494+ assert_eq!(s.iram[0x20], 0);
495+ }
496+
497+ #[test]
498+ fn movc_movx_xchd() {
499+ let s = run(
500+ "MOV DPTR,#tbl\nMOV A,#2\nMOVC A,@A+DPTR\nSJMP go\ntbl: DB 10,20,30\ngo: MOV DPTR,#1234h\nMOVX @DPTR,A\nMOV R0,#30h\nMOV @R0,#0x5A\nMOV A,#0xC3\nXCHD A,@R0",
501+ );
502+ assert_eq!(s.xram[0x1234], 30);
503+ assert_eq!(s.acc(), 0xCA);
504+ assert_eq!(s.iram[0x30], 0x53);
505+ }
506+
507+ #[test]
508+ fn ports_read_pins_but_rmw_reads_latch() {
509+ let mut s = run("MOV P1,#0FFh");
510+ s.pins[1] = 0xF0;
511+ assert_eq!(s.read_direct(0x90, false), 0xF0);
512+ assert_eq!(s.read_direct(0x90, true), 0xFF);
513+ }
514+
515+ #[test]
516+ fn step_over_and_out() {
517+ let out = assemble("CALL f\nNOP\nSJMP $\nf: MOV A,#1\nMOV A,#2\nRET");
518+ let mut s = Sim::new();
519+ s.load_code(out.bytes());
520+ let none = HashSet::new();
521+ s.step_over(1000, &none);
522+ assert_eq!((s.pc, s.acc()), (3, 2));
523+ s.reset();
524+ s.step().unwrap(); // into f
525+ s.step_out(1000, &none);
526+ assert_eq!(s.pc, 3);
527+ }
528+}
A src/sim/disasm.rs View
@@ -0,0 +1,91 @@
1+use crate::isa::{self, Operand};
2+use crate::sfr::{format_bit, format_direct, hex8, hex16};
3+
4+pub struct Disasm {
5+ pub text: String,
6+ pub size: u8,
7+}
8+
9+/// Disassembles the instruction at `addr`.
10+pub fn disassemble(code: &[u8; 0x10000], addr: u16) -> Disasm {
11+ let byte = |i: u16| code[addr.wrapping_add(i) as usize];
12+ let op = byte(0);
13+ let Some(info) = isa::info(op) else {
14+ return Disasm {
15+ text: format!("DB {}", hex8(op)),
16+ size: 1,
17+ };
18+ };
19+ let next = addr.wrapping_add(info.size as u16);
20+ let mut pos = 1u16;
21+ let mut parts: Vec<String> = Vec::new();
22+ for &o in info.operands {
23+ let s = match o {
24+ Operand::A => "A".into(),
25+ Operand::AB => "AB".into(),
26+ Operand::C => "C".into(),
27+ Operand::Dptr => "DPTR".into(),
28+ Operand::Reg(n) => format!("R{n}"),
29+ Operand::AtReg(n) => format!("@R{n}"),
30+ Operand::AtDptr => "@DPTR".into(),
31+ Operand::AtAPlusDptr => "@A+DPTR".into(),
32+ Operand::AtAPlusPc => "@A+PC".into(),
33+ Operand::Imm8 => format!("#{}", hex8(byte(pos))),
34+ Operand::Imm16 => format!("#{}", hex16(u16::from_be_bytes([byte(pos), byte(pos + 1)]))),
35+ Operand::Direct => format_direct(byte(pos)),
36+ Operand::Bit => format_bit(byte(pos)),
37+ Operand::NotBit => format!("/{}", format_bit(byte(pos))),
38+ Operand::Addr11 => {
39+ hex16((next & 0xF800) | (((op >> 5) as u16) << 8) | byte(pos) as u16)
40+ }
41+ Operand::Addr16 => hex16(u16::from_be_bytes([byte(pos), byte(pos + 1)])),
42+ Operand::Rel => hex16(next.wrapping_add(byte(pos) as i8 as u16)),
43+ };
44+ pos += o.len() as u16;
45+ parts.push(s);
46+ }
47+ if op == 0x85 {
48+ // Encoded as source, destination.
49+ parts.swap(0, 1);
50+ }
51+ let text = if parts.is_empty() {
52+ info.mnemonic.to_string()
53+ } else {
54+ format!("{:<5} {}", info.mnemonic, parts.join(","))
55+ };
56+ Disasm {
57+ text,
58+ size: info.size,
59+ }
60+}
61+
62+#[cfg(test)]
63+mod tests {
64+ use super::*;
65+ use crate::asm::assemble;
66+
67+ /// Disassembling and re-assembling every opcode gives the same bytes.
68+ #[test]
69+ fn round_trips_every_opcode() {
70+ for op in 0..=255u8 {
71+ if op == 0xA5 {
72+ continue;
73+ }
74+ let mut code = Box::new([0u8; 0x10000]);
75+ code[0x100] = op;
76+ code[0x101] = 0x30;
77+ code[0x102] = 0x40;
78+ let d = disassemble(&code, 0x100);
79+ let src = format!("ORG 100h\n{}", d.text);
80+ let out = assemble(&src);
81+ assert!(out.ok(), "{op:02X} '{}': {:?}", d.text, out.diagnostics);
82+ let bytes: Vec<u8> = out.bytes().map(|b| b.1).collect();
83+ assert_eq!(
84+ bytes,
85+ code[0x100..0x100 + d.size as usize],
86+ "opcode {op:02X} '{}'",
87+ d.text
88+ );
89+ }
90+ }
91+}
A src/sim/mod.rs View
@@ -0,0 +1,449 @@
1+//! Intel 8051 simulator: CPU core, memories and on-chip peripherals.
2+
3+mod cpu;
4+pub mod disasm;
5+mod periph;
6+
7+use std::collections::{HashSet, VecDeque};
8+
9+use crate::isa;
10+use crate::sfr::{self, ACC, B, DPH, DPL, IE, IP, P0, P1, P2, P3, PCON, PSW, SBUF, SP};
11+pub use periph::Uart;
12+
13+pub const CY: u8 = 0x80;
14+pub const AC: u8 = 0x40;
15+pub const OV: u8 = 0x04;
16+pub const PARITY: u8 = 0x01;
17+
18+#[derive(Debug, Clone, PartialEq, Eq)]
19+pub enum Stop {
20+ /// Ran the requested number of instructions / cycle budget.
21+ Budget,
22+ Breakpoint(u16),
23+ /// A step-over / step-out target was reached.
24+ Target,
25+ ReservedOpcode {
26+ pc: u16,
27+ opcode: u8,
28+ },
29+ PowerDown,
30+}
31+
32+pub struct Sim {
33+ pub code: Box<[u8; 0x10000]>,
34+ /// Internal RAM. Indirect addressing reaches all 256 bytes (8052-style
35+ /// upper 128); direct addresses 80h-FFh go to the SFRs instead.
36+ pub iram: [u8; 256],
37+ /// SFR space 80h-FFh, indexed by `addr - 0x80`.
38+ pub sfr: [u8; 128],
39+ pub xram: Box<[u8; 0x10000]>,
40+ pub pc: u16,
41+ /// Machine cycles since reset.
42+ pub cycles: u64,
43+ /// Levels driven onto the port pins from outside (1 = released/high).
44+ /// A pin reads as `latch & external`.
45+ pub pins: [u8; 4],
46+ pub uart: Uart,
47+ pub idle: bool,
48+ pub power_down: bool,
49+ /// Priority levels of interrupts currently in service (innermost last).
50+ pub in_service: Vec<u8>,
51+ /// Vector of the last interrupt taken, for the UI log.
52+ pub last_irq: Option<u16>,
53+ /// RETI or a write to IE/IP delays interrupt acceptance by one instruction.
54+ irq_hold: bool,
55+ /// Pin levels of P3 in the previous cycle, for edge detection.
56+ prev_p3: u8,
57+}
58+
59+impl Default for Sim {
60+ fn default() -> Self {
61+ Self::new()
62+ }
63+}
64+
65+impl Sim {
66+ pub fn new() -> Self {
67+ let mut s = Self {
68+ code: Box::new([0; 0x10000]),
69+ iram: [0; 256],
70+ sfr: [0; 128],
71+ xram: Box::new([0; 0x10000]),
72+ pc: 0,
73+ cycles: 0,
74+ pins: [0xFF; 4],
75+ uart: Uart::default(),
76+ idle: false,
77+ power_down: false,
78+ in_service: Vec::new(),
79+ last_irq: None,
80+ irq_hold: false,
81+ prev_p3: 0xFF,
82+ };
83+ s.reset();
84+ s
85+ }
86+
87+ /// Hardware reset: SFRs to their reset values, PC to 0. Code and XRAM
88+ /// are kept; IRAM is cleared so runs are reproducible.
89+ pub fn reset(&mut self) {
90+ self.iram = [0; 256];
91+ self.sfr = [0; 128];
92+ for p in [P0, P1, P2, P3] {
93+ self.set_sfr(p, 0xFF);
94+ }
95+ self.set_sfr(SP, 0x07);
96+ self.pc = 0;
97+ self.cycles = 0;
98+ self.uart.reset();
99+ self.idle = false;
100+ self.power_down = false;
101+ self.in_service.clear();
102+ self.last_irq = None;
103+ self.irq_hold = false;
104+ self.prev_p3 = self.port_pins(3);
105+ }
106+
107+ pub fn load_code(&mut self, bytes: impl Iterator<Item = (u16, u8)>) {
108+ self.code.fill(0);
109+ for (a, b) in bytes {
110+ self.code[a as usize] = b;
111+ }
112+ }
113+
114+ // ---- Raw register access ----
115+
116+ pub fn sfr(&self, addr: u8) -> u8 {
117+ self.sfr[(addr & 0x7F) as usize]
118+ }
119+
120+ pub fn set_sfr(&mut self, addr: u8, v: u8) {
121+ self.sfr[(addr & 0x7F) as usize] = v;
122+ }
123+
124+ pub fn acc(&self) -> u8 {
125+ self.sfr(ACC)
126+ }
127+
128+ pub fn set_acc(&mut self, v: u8) {
129+ self.set_sfr(ACC, v);
130+ }
131+
132+ pub fn b(&self) -> u8 {
133+ self.sfr(B)
134+ }
135+
136+ pub fn psw(&self) -> u8 {
137+ self.sfr(PSW)
138+ }
139+
140+ pub fn sp(&self) -> u8 {
141+ self.sfr(SP)
142+ }
143+
144+ pub fn dptr(&self) -> u16 {
145+ u16::from_be_bytes([self.sfr(DPH), self.sfr(DPL)])
146+ }
147+
148+ pub fn set_dptr(&mut self, v: u16) {
149+ let [h, l] = v.to_be_bytes();
150+ self.set_sfr(DPH, h);
151+ self.set_sfr(DPL, l);
152+ }
153+
154+ pub fn flag(&self, mask: u8) -> bool {
155+ self.psw() & mask != 0
156+ }
157+
158+ pub fn set_flag(&mut self, mask: u8, on: bool) {
159+ let p = self.psw();
160+ self.set_sfr(PSW, if on { p | mask } else { p & !mask });
161+ }
162+
163+ pub fn bank(&self) -> u8 {
164+ (self.psw() >> 3) & 3
165+ }
166+
167+ /// IRAM address of register Rn in the active bank.
168+ pub fn reg_addr(&self, n: u8) -> u8 {
169+ self.bank() * 8 + n
170+ }
171+
172+ pub fn reg(&self, n: u8) -> u8 {
173+ self.iram[self.reg_addr(n) as usize]
174+ }
175+
176+ /// Pin levels of port `n`: latch AND externally driven level.
177+ pub fn port_pins(&self, n: usize) -> u8 {
178+ self.sfr(P0 + 0x10 * n as u8) & self.pins[n]
179+ }
180+
181+ // ---- Addressing modes ----
182+
183+ /// Reads a direct address. Ports read their pins, except for
184+ /// read-modify-write instructions (`rmw`) which read the latch.
185+ pub fn read_direct(&self, addr: u8, rmw: bool) -> u8 {
186+ match addr {
187+ 0x00..=0x7F => self.iram[addr as usize],
188+ P0 | P1 | P2 | P3 if !rmw => self.port_pins(((addr - P0) >> 4) as usize),
189+ SBUF => self.uart.sbuf_rx,
190+ _ => self.sfr(addr),
191+ }
192+ }
193+
194+ pub fn write_direct(&mut self, addr: u8, v: u8) {
195+ match addr {
196+ 0x00..=0x7F => self.iram[addr as usize] = v,
197+ SBUF => self.uart_transmit(v),
198+ IE | IP => {
199+ self.set_sfr(addr, v);
200+ self.irq_hold = true;
201+ }
202+ PCON => {
203+ // IDL (bit 0) and PD (bit 1) take effect immediately.
204+ self.set_sfr(addr, v);
205+ self.power_down |= v & 0x02 != 0;
206+ self.idle |= v & 0x01 != 0;
207+ }
208+ _ => self.set_sfr(addr, v),
209+ }
210+ }
211+
212+ pub fn read_bit(&self, bit: u8, rmw: bool) -> bool {
213+ let (byte, idx) = sfr::bit_location(bit);
214+ self.read_direct(byte, rmw) & (1 << idx) != 0
215+ }
216+
217+ pub fn write_bit(&mut self, bit: u8, on: bool) {
218+ let (byte, idx) = sfr::bit_location(bit);
219+ let v = self.read_direct(byte, true);
220+ self.write_direct(byte, if on { v | (1 << idx) } else { v & !(1 << idx) });
221+ }
222+
223+ pub fn push(&mut self, v: u8) {
224+ let sp = self.sp().wrapping_add(1);
225+ self.set_sfr(SP, sp);
226+ self.iram[sp as usize] = v;
227+ }
228+
229+ pub fn pop(&mut self) -> u8 {
230+ let sp = self.sp();
231+ self.set_sfr(SP, sp.wrapping_sub(1));
232+ self.iram[sp as usize]
233+ }
234+
235+ pub fn push_pc(&mut self) {
236+ let [h, l] = self.pc.to_be_bytes();
237+ self.push(l);
238+ self.push(h);
239+ }
240+
241+ pub fn pop_pc(&mut self) {
242+ let h = self.pop();
243+ let l = self.pop();
244+ self.pc = u16::from_be_bytes([h, l]);
245+ }
246+
247+ // ---- Execution control ----
248+
249+ /// Executes one instruction (or one idle cycle), then services interrupts.
250+ pub fn step(&mut self) -> Result<(), Stop> {
251+ if self.power_down {
252+ return Err(Stop::PowerDown);
253+ }
254+ if self.idle {
255+ self.tick(1);
256+ } else {
257+ let pc = self.pc;
258+ let opcode = self.code[pc as usize];
259+ let Some(info) = isa::info(opcode) else {
260+ return Err(Stop::ReservedOpcode { pc, opcode });
261+ };
262+ self.pc = pc.wrapping_add(1);
263+ self.execute(opcode);
264+ let p = self.acc().count_ones() % 2 == 1;
265+ self.set_flag(PARITY, p);
266+ self.tick(info.cycles as u32);
267+ }
268+ self.service_interrupts();
269+ Ok(())
270+ }
271+
272+ fn current_call_len(&self) -> Option<u16> {
273+ let op = self.code[self.pc as usize];
274+ match op {
275+ 0x12 => Some(3),
276+ _ if op & 0x1F == 0x11 => Some(2),
277+ _ => None,
278+ }
279+ }
280+
281+ /// Runs up to `max_steps` instructions, stopping at breakpoints.
282+ /// `resume` ignores a breakpoint at the starting PC, so execution can
283+ /// continue from the breakpoint it last stopped at.
284+ pub fn run(&mut self, max_steps: u32, breakpoints: &HashSet<u16>, resume: bool) -> Stop {
285+ for i in 0..max_steps {
286+ if (i > 0 || !resume) && !self.idle && breakpoints.contains(&self.pc) {
287+ return Stop::Breakpoint(self.pc);
288+ }
289+ if let Err(s) = self.step() {
290+ return s;
291+ }
292+ }
293+ Stop::Budget
294+ }
295+
296+ /// Like `step`, but executes a whole ACALL/LCALL subroutine.
297+ pub fn step_over(&mut self, max_steps: u32, breakpoints: &HashSet<u16>) -> Stop {
298+ match self.current_call_len() {
299+ Some(len) => {
300+ let target = self.pc.wrapping_add(len);
301+ let sp = self.sp();
302+ self.run_until(max_steps, breakpoints, |s| s.pc == target && s.sp() == sp)
303+ }
304+ None => match self.step() {
305+ Ok(()) => Stop::Target,
306+ Err(s) => s,
307+ },
308+ }
309+ }
310+
311+ /// Runs until the current subroutine (or interrupt handler) returns.
312+ pub fn step_out(&mut self, max_steps: u32, breakpoints: &HashSet<u16>) -> Stop {
313+ let sp = self.sp();
314+ let mut returning = false;
315+ self.run_until_with(max_steps, breakpoints, |s, before| {
316+ if before {
317+ let op = s.code[s.pc as usize];
318+ returning = (op == 0x22 || op == 0x32) && s.sp() <= sp && !s.idle;
319+ false
320+ } else {
321+ returning
322+ }
323+ })
324+ }
325+
326+ fn run_until(
327+ &mut self,
328+ max_steps: u32,
329+ bps: &HashSet<u16>,
330+ mut done: impl FnMut(&Sim) -> bool,
331+ ) -> Stop {
332+ self.run_until_with(max_steps, bps, |s, before| !before && done(s))
333+ }
334+
335+ /// `done(sim, before)` is called before (`true`) and after (`false`) each step.
336+ fn run_until_with(
337+ &mut self,
338+ max_steps: u32,
339+ bps: &HashSet<u16>,
340+ mut done: impl FnMut(&Sim, bool) -> bool,
341+ ) -> Stop {
342+ for i in 0..max_steps {
343+ if i > 0 && !self.idle && bps.contains(&self.pc) {
344+ return Stop::Breakpoint(self.pc);
345+ }
346+ done(self, true);
347+ if let Err(s) = self.step() {
348+ return s;
349+ }
350+ if done(self, false) {
351+ return Stop::Target;
352+ }
353+ }
354+ Stop::Budget
355+ }
356+
357+ /// Queue bytes for the UART receiver, as if typed on a serial terminal.
358+ pub fn uart_send(&mut self, bytes: &[u8]) {
359+ self.uart.rx_queue.extend(bytes.iter().copied());
360+ }
361+
362+ pub fn uart_rx_pending(&self) -> &VecDeque<u8> {
363+ &self.uart.rx_queue
364+ }
365+}
366+
367+#[cfg(test)]
368+mod example_tests {
369+ use super::Sim;
370+ use crate::asm::assemble;
371+
372+ fn load(src: &str) -> Sim {
373+ let out = assemble(src);
374+ assert!(out.ok(), "{:?}", out.diagnostics);
375+ let mut s = Sim::new();
376+ s.load_code(out.bytes());
377+ s
378+ }
379+
380+ fn steps(s: &mut Sim, n: usize) {
381+ for _ in 0..n {
382+ s.step().unwrap();
383+ }
384+ }
385+
386+ #[test]
387+ fn serial_hello_greets_and_echoes() {
388+ let mut s = load(include_str!("../../examples/serial_hello.asm"));
389+ steps(&mut s, 60_000);
390+ assert_eq!(
391+ String::from_utf8_lossy(&s.uart.tx_log),
392+ "Hello from the 8051!\r\nType something: "
393+ );
394+ s.uart_send(b"hi\r");
395+ steps(&mut s, 20_000);
396+ assert!(String::from_utf8_lossy(&s.uart.tx_log).ends_with("hi\r\n"));
397+ }
398+
399+ #[test]
400+ fn timer_irq_ticks() {
401+ let mut s = load(include_str!("../../examples/timer_irq.asm"));
402+ steps(&mut s, 5_000);
403+ // ~10000 cycles at one interrupt per 1000 cycles.
404+ assert!((3..=10).contains(&s.reg(7)), "R7 = {}", s.reg(7));
405+ }
406+
407+ #[test]
408+ fn button_counts_edges_while_idle() {
409+ let mut s = load(include_str!("../../examples/button_int0.asm"));
410+ steps(&mut s, 20);
411+ assert!(s.idle);
412+ for _ in 0..3 {
413+ s.pins[3] &= !0x04;
414+ steps(&mut s, 10);
415+ s.pins[3] |= 0x04;
416+ steps(&mut s, 10);
417+ }
418+ assert_eq!(s.sfr(crate::sfr::P1), 3);
419+ }
420+
421+ #[test]
422+ fn bcd_and_lookup() {
423+ let mut s = load(include_str!("../../examples/bcd_add.asm"));
424+ steps(&mut s, 20);
425+ assert_eq!((s.iram[0x31], s.iram[0x30]), (0x69, 0x12));
426+ let mut s = load(include_str!("../../examples/lookup_movc.asm"));
427+ steps(&mut s, 3 + 4 * 5 + 3); // setup, digits 0-3, then up to MOV P2,A
428+ assert_eq!(s.sfr(crate::sfr::P2), 0x66); // digit 4
429+ }
430+}
431+
432+#[cfg(test)]
433+mod run_tests {
434+ use super::{Sim, Stop};
435+ use crate::asm::assemble;
436+ use std::collections::HashSet;
437+
438+ #[test]
439+ fn breakpoint_at_chunk_boundary_is_hit() {
440+ let out = assemble("l: NOP\nNOP\nSJMP l");
441+ let mut s = Sim::new();
442+ s.load_code(out.bytes());
443+ let bps = HashSet::from([1u16]);
444+ assert_eq!(s.run(1, &bps, true), Stop::Budget); // stops at PC 1 by budget
445+ assert_eq!(s.run(10, &bps, false), Stop::Breakpoint(1)); // next chunk must not skip it
446+ assert_eq!(s.run(10, &bps, true), Stop::Breakpoint(1)); // resuming loops back to it
447+ assert_eq!(s.pc, 1);
448+ }
449+}
A src/sim/periph.rs View
@@ -0,0 +1,403 @@
1+//! Timers 0/1, the UART and interrupt dispatch, advanced per machine cycle.
2+
3+use std::collections::VecDeque;
4+
5+use super::Sim;
6+use crate::sfr::{IE, IP, PCON, SCON, TCON, TH0, TH1, TL0, TL1, TMOD};
7+
8+// TCON bits
9+const TF1: u8 = 0x80;
10+const TR1: u8 = 0x40;
11+const TF0: u8 = 0x20;
12+const TR0: u8 = 0x10;
13+const IE1: u8 = 0x08;
14+const IT1: u8 = 0x04;
15+const IE0: u8 = 0x02;
16+const IT0: u8 = 0x01;
17+// SCON bits
18+const SM2: u8 = 0x20;
19+const REN: u8 = 0x10;
20+const RB8: u8 = 0x04;
21+const TI: u8 = 0x02;
22+const RI: u8 = 0x01;
23+// P3 pins
24+const INT0_PIN: u8 = 0x04;
25+const INT1_PIN: u8 = 0x08;
26+const T0_PIN: u8 = 0x10;
27+const T1_PIN: u8 = 0x20;
28+
29+#[derive(Debug, Clone, Copy)]
30+struct Shift {
31+ byte: u8,
32+ /// Remaining time in the mode's unit (see `Sim::uart_units`).
33+ remaining: u32,
34+}
35+
36+#[derive(Debug, Default)]
37+pub struct Uart {
38+ /// Everything the 8051 has transmitted.
39+ pub tx_log: Vec<u8>,
40+ /// Bytes waiting to be received by the 8051.
41+ pub rx_queue: VecDeque<u8>,
42+ /// The receive half of SBUF.
43+ pub sbuf_rx: u8,
44+ tx: Option<Shift>,
45+ rx: Option<Shift>,
46+}
47+
48+impl Uart {
49+ /// Clears the shift registers and the TX log; queued input is kept.
50+ pub fn reset(&mut self) {
51+ self.tx_log.clear();
52+ self.sbuf_rx = 0;
53+ self.tx = None;
54+ self.rx = None;
55+ }
56+
57+ pub fn transmitting(&self) -> bool {
58+ self.tx.is_some()
59+ }
60+}
61+
62+/// Interrupt sources in hardware polling order: (flag test, IE/IP bit, vector).
63+const SOURCES: [(u8, u16); 5] = [
64+ (0x01, 0x03),
65+ (0x02, 0x0B),
66+ (0x04, 0x13),
67+ (0x08, 0x1B),
68+ (0x10, 0x23),
69+];
70+
71+impl Sim {
72+ pub(super) fn tick(&mut self, cycles: u32) {
73+ for _ in 0..cycles {
74+ self.cycles += 1;
75+ let p3 = self.port_pins(3);
76+ let fell = self.prev_p3 & !p3;
77+ self.prev_p3 = p3;
78+ self.external_interrupts(p3, fell);
79+ let t1_overflows = self.timers(p3, fell);
80+ self.uart_tick(t1_overflows);
81+ }
82+ }
83+
84+ fn external_interrupts(&mut self, p3: u8, fell: u8) {
85+ let mut tcon = self.sfr(TCON);
86+ for (pin, it, ie) in [(INT0_PIN, IT0, IE0), (INT1_PIN, IT1, IE1)] {
87+ if tcon & it != 0 {
88+ if fell & pin != 0 {
89+ tcon |= ie;
90+ }
91+ } else if p3 & pin == 0 {
92+ tcon |= ie;
93+ } else {
94+ tcon &= !ie;
95+ }
96+ }
97+ self.set_sfr(TCON, tcon);
98+ }
99+
100+ /// Advances both timers by one machine cycle. Returns the number of
101+ /// timer 1 overflows (the UART baud clock in modes 1 and 3).
102+ fn timers(&mut self, p3: u8, fell: u8) -> u32 {
103+ let tmod = self.sfr(TMOD);
104+ let mut tcon = self.sfr(TCON);
105+ let t0_mode = tmod & 0x03;
106+ let mut t1_overflows = 0;
107+
108+ for t in 0..2u8 {
109+ let ctl = (tmod >> (4 * t)) & 0x0F;
110+ let mode = ctl & 0x03;
111+ let (tr, int_pin, count_pin) = if t == 0 {
112+ (TR0, INT0_PIN, T0_PIN)
113+ } else {
114+ (TR1, INT1_PIN, T1_PIN)
115+ };
116+ let gate_ok = ctl & 0x08 == 0 || p3 & int_pin != 0;
117+ let running = tcon & tr != 0 && gate_ok;
118+ let event = if ctl & 0x04 != 0 {
119+ fell & count_pin != 0
120+ } else {
121+ true
122+ };
123+
124+ // Timer 1 is stopped in mode 3; timer 0 mode 3 also clocks TH0 from TR1.
125+ if t == 1 && mode == 3 {
126+ continue;
127+ }
128+ let (tl_addr, th_addr) = if t == 0 { (TL0, TH0) } else { (TL1, TH1) };
129+ if t == 0 && mode == 3 && tcon & TR1 != 0 {
130+ let th = self.sfr(TH0).wrapping_add(1);
131+ self.set_sfr(TH0, th);
132+ if th == 0 {
133+ tcon |= TF1;
134+ }
135+ }
136+ if !(running && event) {
137+ continue;
138+ }
139+ let (tl, th) = (self.sfr(tl_addr), self.sfr(th_addr));
140+ let (tl, th, overflow) = match mode {
141+ 0 => {
142+ let low = (tl.wrapping_add(1)) & 0x1F;
143+ let th2 = if low == 0 { th.wrapping_add(1) } else { th };
144+ ((tl & 0xE0) | low, th2, low == 0 && th2 == 0)
145+ }
146+ 1 => {
147+ let v = u16::from_be_bytes([th, tl]).wrapping_add(1);
148+ let [h, l] = v.to_be_bytes();
149+ (l, h, v == 0)
150+ }
151+ 2 => {
152+ let l = tl.wrapping_add(1);
153+ if l == 0 {
154+ (th, th, true)
155+ } else {
156+ (l, th, false)
157+ }
158+ }
159+ _ => {
160+ // Timer 0 mode 3: TL0 is an 8-bit timer.
161+ let l = tl.wrapping_add(1);
162+ (l, th, l == 0)
163+ }
164+ };
165+ self.set_sfr(tl_addr, tl);
166+ self.set_sfr(th_addr, th);
167+ if overflow {
168+ if t == 1 {
169+ t1_overflows += 1;
170+ // With timer 0 in mode 3, TF1 belongs to TH0.
171+ if t0_mode != 3 {
172+ tcon |= TF1;
173+ }
174+ } else {
175+ tcon |= TF0;
176+ }
177+ }
178+ }
179+ self.set_sfr(TCON, tcon);
180+ t1_overflows
181+ }
182+
183+ /// Frame length of the current serial mode, in the units that
184+ /// `uart_tick` counts: machine cycles (mode 0), oscillator periods
185+ /// (mode 2) or timer 1 overflows (modes 1 and 3).
186+ fn uart_frame(&self) -> u32 {
187+ let smod = self.sfr(PCON) & 0x80 != 0;
188+ let bit = |base: u32| if smod { base / 2 } else { base };
189+ match self.sfr(SCON) >> 6 {
190+ 0 => 8,
191+ 1 => 10 * bit(32),
192+ 2 => 11 * bit(64),
193+ _ => 11 * bit(32),
194+ }
195+ }
196+
197+ pub(super) fn uart_transmit(&mut self, byte: u8) {
198+ let remaining = self.uart_frame();
199+ self.uart.tx = Some(Shift { byte, remaining });
200+ }
201+
202+ fn uart_tick(&mut self, t1_overflows: u32) {
203+ let mode = self.sfr(SCON) >> 6;
204+ let units = match mode {
205+ 0 => 1,
206+ 2 => 12,
207+ _ => t1_overflows,
208+ };
209+ if units == 0 {
210+ return;
211+ }
212+ if let Some(tx) = &mut self.uart.tx {
213+ tx.remaining = tx.remaining.saturating_sub(units);
214+ if tx.remaining == 0 {
215+ self.uart.tx_log.push(tx.byte);
216+ self.uart.tx = None;
217+ self.set_sfr(SCON, self.sfr(SCON) | TI);
218+ }
219+ }
220+
221+ let scon = self.sfr(SCON);
222+ if self.uart.rx.is_none()
223+ && scon & REN != 0
224+ && scon & RI == 0
225+ && let Some(byte) = self.uart.rx_queue.pop_front()
226+ {
227+ self.uart.rx = Some(Shift {
228+ byte,
229+ remaining: self.uart_frame(),
230+ });
231+ }
232+ if let Some(rx) = &mut self.uart.rx {
233+ rx.remaining = rx.remaining.saturating_sub(units);
234+ if rx.remaining == 0 {
235+ let byte = rx.byte;
236+ self.uart.rx = None;
237+ // Terminal input has no 9th data bit; in modes 1-3 RB8 gets
238+ // the stop bit (1). With SM2 set in modes 2/3 that 9th bit
239+ // would be 0, so the byte is discarded.
240+ let mut scon = self.sfr(SCON);
241+ if !(mode >= 2 && scon & SM2 != 0) {
242+ self.uart.sbuf_rx = byte;
243+ scon |= RI;
244+ if mode == 1 {
245+ scon |= RB8;
246+ } else if mode >= 2 {
247+ scon &= !RB8;
248+ }
249+ }
250+ self.set_sfr(SCON, scon);
251+ }
252+ }
253+ }
254+
255+ pub(super) fn service_interrupts(&mut self) {
256+ if self.irq_hold {
257+ self.irq_hold = false;
258+ return;
259+ }
260+ let ie = self.sfr(IE);
261+ if ie & 0x80 == 0 {
262+ return;
263+ }
264+ let tcon = self.sfr(TCON);
265+ let scon = self.sfr(SCON);
266+ let pending = |mask: u8| match mask {
267+ 0x01 => tcon & IE0 != 0,
268+ 0x02 => tcon & TF0 != 0,
269+ 0x04 => tcon & IE1 != 0,
270+ 0x08 => tcon & TF1 != 0,
271+ _ => scon & (RI | TI) != 0,
272+ };
273+ let ip = self.sfr(IP);
274+ let current = self.in_service.last().copied();
275+ for level in [1u8, 0] {
276+ if current.is_some_and(|c| c >= level) {
277+ continue;
278+ }
279+ let found = SOURCES.iter().find(|&&(mask, _)| {
280+ ie & mask != 0 && (ip & mask != 0) == (level == 1) && pending(mask)
281+ });
282+ if let Some(&(mask, vector)) = found {
283+ // Hardware clears timer flags and edge-triggered external flags.
284+ let mut tcon = self.sfr(TCON);
285+ match mask {
286+ 0x01 if tcon & IT0 != 0 => tcon &= !IE0,
287+ 0x02 => tcon &= !TF0,
288+ 0x04 if tcon & IT1 != 0 => tcon &= !IE1,
289+ 0x08 => tcon &= !TF1,
290+ _ => {}
291+ }
292+ self.set_sfr(TCON, tcon);
293+ self.idle = false;
294+ self.set_sfr(PCON, self.sfr(PCON) & !0x01);
295+ self.push_pc();
296+ self.pc = vector;
297+ self.in_service.push(level);
298+ self.last_irq = Some(vector);
299+ self.tick(2);
300+ return;
301+ }
302+ }
303+ }
304+}
305+
306+#[cfg(test)]
307+mod tests {
308+ use crate::asm::assemble;
309+ use crate::sim::Sim;
310+
311+ fn sim(src: &str) -> Sim {
312+ let out = assemble(src);
313+ assert!(out.ok(), "{:?}", out.diagnostics);
314+ let mut s = Sim::new();
315+ s.load_code(out.bytes());
316+ s
317+ }
318+
319+ #[test]
320+ fn timer0_mode1_interrupt() {
321+ let mut s = sim("
322+ ORG 0
323+ LJMP main
324+ ORG 0Bh
325+ INC R7
326+ RETI
327+main: MOV TMOD,#01h
328+ MOV TH0,#0FFh
329+ MOV TL0,#0F0h
330+ SETB ET0
331+ SETB EA
332+ SETB TR0
333+ SJMP $
334+");
335+ let mut saw_vector = false;
336+ for _ in 0..200 {
337+ s.step().unwrap();
338+ saw_vector |= s.pc == 0x0B;
339+ }
340+ assert!(saw_vector);
341+ assert!(s.reg(7) >= 1);
342+ // TF0 is cleared by hardware on vectoring.
343+ assert_eq!(s.sfr(crate::sfr::TCON) & 0x20, 0);
344+ }
345+
346+ #[test]
347+ fn timer_mode2_auto_reload() {
348+ let mut s = sim("MOV TMOD,#02h\nMOV TH0,#0F0h\nMOV TL0,#0FEh\nSETB TR0\nSJMP $");
349+ for _ in 0..8 {
350+ s.step().unwrap();
351+ }
352+ assert!(s.sfr(crate::sfr::TCON) & 0x20 != 0, "TF0 set");
353+ assert!(s.sfr(crate::sfr::TL0) >= 0xF0);
354+ }
355+
356+ #[test]
357+ fn uart_tx_and_rx() {
358+ let mut s = sim("
359+ MOV TMOD,#20h ; timer 1 mode 2
360+ MOV TH1,#0FDh ; 9600 baud @ 11.0592 MHz
361+ SETB TR1
362+ MOV SCON,#50h ; mode 1, REN
363+ MOV SBUF,#'H'
364+w: JNB TI,w
365+ CLR TI
366+r: JNB RI,r
367+ MOV A,SBUF
368+ SJMP $
369+");
370+ s.uart_send(b"x");
371+ for _ in 0..10_000 {
372+ s.step().unwrap();
373+ }
374+ assert_eq!(s.uart.tx_log, b"H");
375+ assert_eq!(s.acc(), b'x');
376+ }
377+
378+ #[test]
379+ fn external_interrupt_edge() {
380+ let mut s = sim("
381+ LJMP main
382+ ORG 03h
383+ INC R0
384+ RETI
385+main: SETB IT0
386+ SETB EX0
387+ SETB EA
388+ SJMP $
389+");
390+ for _ in 0..10 {
391+ s.step().unwrap();
392+ }
393+ s.pins[3] &= !0x04; // pull INT0 low
394+ for _ in 0..10 {
395+ s.step().unwrap();
396+ }
397+ s.pins[3] |= 0x04;
398+ for _ in 0..10 {
399+ s.step().unwrap();
400+ }
401+ assert_eq!(s.reg(0), 1, "one falling edge, one interrupt");
402+ }
403+}
A src/ui/app.rs View
@@ -0,0 +1,903 @@
1+use std::collections::{HashMap, HashSet};
2+use std::path::{Path, PathBuf};
3+use std::process::Command;
4+
5+use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};
6+use ratatui_textarea::{CursorMove, TextArea};
7+
8+use crate::asm::{self, AsmOutput};
9+use crate::sim::{Sim, Stop, disasm};
10+
11+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12+pub enum Mode {
13+ Edit,
14+ Debug,
15+}
16+
17+/// Right-hand pane in edit mode.
18+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19+pub enum OutTab {
20+ Listing,
21+ Hex,
22+ Errors,
23+}
24+
25+/// Focusable panels in debug mode.
26+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27+pub enum Focus {
28+ Source,
29+ Memory,
30+ Ports,
31+ Serial,
32+}
33+
34+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35+pub enum MemTab {
36+ Iram,
37+ Sfr,
38+ Xram,
39+ Code,
40+}
41+
42+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43+pub enum PromptKind {
44+ Open,
45+ SaveAs,
46+ /// `addr value` poke into the memory shown in the memory panel.
47+ Poke,
48+}
49+
50+pub struct Prompt {
51+ pub kind: PromptKind,
52+ pub input: String,
53+}
54+
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+pub struct App {
77+ pub path: Option<PathBuf>,
78+ pub editor: TextArea<'static>,
79+ saved_text: String,
80+ pub mode: Mode,
81+ pub out_tab: OutTab,
82+ pub focus: Focus,
83+ pub mem_tab: MemTab,
84+ 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,
98+ /// Selected line in the debug source view.
99+ pub src_cursor: usize,
100+ pub src_scroll: usize,
101+ pub out_scroll: usize,
102+ pub port_sel: (usize, u8),
103+ pub serial_input: String,
104+ pub prev_iram: [u8; 256],
105+ pub prev_sfr: [u8; 128],
106+ pub show_help: bool,
107+ pub prompt: Option<Prompt>,
108+ pub quit: bool,
109+ quit_armed: bool,
110+ /// Set by Ctrl-G; the event loop suspends the TUI and calls
111+ /// `edit_externally`.
112+ pub external_edit: bool,
113+}
114+
115+impl App {
116+ pub fn new(path: Option<PathBuf>, clock_mhz: f64) -> Self {
117+ let mut app = Self {
118+ path: None,
119+ editor: TextArea::default(),
120+ saved_text: String::new(),
121+ mode: Mode::Edit,
122+ out_tab: OutTab::Listing,
123+ focus: Focus::Source,
124+ mem_tab: MemTab::Iram,
125+ 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(),
135+ src_cursor: 0,
136+ src_scroll: 0,
137+ out_scroll: 0,
138+ port_sel: (1, 0),
139+ serial_input: String::new(),
140+ prev_iram: [0; 256],
141+ prev_sfr: [0; 128],
142+ show_help: false,
143+ prompt: None,
144+ quit: false,
145+ quit_armed: false,
146+ external_edit: false,
147+ };
148+ match path {
149+ Some(p) => app.open(&p),
150+ None => app.set_text(TEMPLATE),
151+ }
152+ app
153+ }
154+
155+ fn set_text(&mut self, text: &str) {
156+ let lines: Vec<String> = text.lines().map(String::from).collect();
157+ self.editor = TextArea::new(if lines.is_empty() {
158+ vec![String::new()]
159+ } else {
160+ lines
161+ });
162+ crate::ui::draw::style_editor(&mut self.editor);
163+ self.saved_text = self.text();
164+ self.breakpoints.clear();
165+ self.build = None;
166+ self.last_asm = None;
167+ self.focus = Focus::Source;
168+ self.serial_input.clear();
169+ }
170+
171+ pub fn text(&self) -> String {
172+ self.editor.lines().join("\n")
173+ }
174+
175+ pub fn dirty(&self) -> bool {
176+ self.text() != self.saved_text
177+ }
178+
179+ /// Source differs from what the simulator is running.
180+ pub fn stale(&self) -> bool {
181+ self.build
182+ .as_ref()
183+ .is_none_or(|b| b.source != self.editor.lines())
184+ }
185+
186+ pub fn title(&self) -> String {
187+ let name = self
188+ .path
189+ .as_ref()
190+ .map_or("untitled.asm".to_string(), |p| p.display().to_string());
191+ format!("{name}{}", if self.dirty() { " [+]" } else { "" })
192+ }
193+
194+ // ---- Files ----
195+
196+ 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;
202+ }
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}"),
222+ }
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+ }
229+ }
230+
231+ fn save(&mut self, path: Option<PathBuf>) {
232+ let Some(path) = path.or_else(|| self.path.clone()) else {
233+ self.prompt = Some(Prompt {
234+ kind: PromptKind::SaveAs,
235+ input: String::new(),
236+ });
237+ return;
238+ };
239+ let mut text = self.text();
240+ text.push('\n');
241+ match std::fs::write(&path, text) {
242+ Ok(()) => {
243+ self.saved_text = self.text();
244+ self.status = format!("saved {}", path.display());
245+ self.path = Some(path);
246+ }
247+ Err(e) => self.status = format!("cannot save {}: {e}", path.display()),
248+ }
249+ }
250+
251+ /// Opens the source in `$EDITOR` and loads the result back. The
252+ /// caller must have suspended the TUI. A saved file is edited in place
253+ /// (unsaved changes are saved first); an untitled buffer goes through
254+ /// a temporary file.
255+ pub fn edit_externally(&mut self) {
256+ let (path, temp) = match self.path.clone() {
257+ Some(p) => {
258+ if self.dirty() {
259+ self.save(None);
260+ if self.dirty() {
261+ return; // save failed; `save` set the status
262+ }
263+ }
264+ (p, false)
265+ }
266+ None => {
267+ let p = std::env::temp_dir().join(format!("i8051-{}.asm", std::process::id()));
268+ if let Err(e) = std::fs::write(&p, self.text() + "\n") {
269+ self.status = format!("cannot write {}: {e}", p.display());
270+ return;
271+ }
272+ (p, true)
273+ }
274+ };
275+
276+ let line = self.editor.cursor().0 + 1;
277+ let result = editor_command(&path, line).status();
278+ let text = std::fs::read_to_string(&path);
279+ if temp {
280+ let _ = std::fs::remove_file(&path);
281+ }
282+ match (result, text) {
283+ (Err(e), _) => self.status = format!("cannot start editor: {e}"),
284+ (Ok(s), _) if !s.success() => {
285+ self.status = format!("editor exited with {s}; changes not loaded")
286+ }
287+ (Ok(_), Err(e)) => self.status = format!("cannot read {}: {e}", path.display()),
288+ (Ok(_), Ok(text)) => self.reload(&text, !temp),
289+ }
290+ }
291+
292+ /// Replaces the buffer with externally edited text, keeping the cursor
293+ /// and breakpoints, and reassembles if anything changed.
294+ fn reload(&mut self, text: &str, from_saved_file: bool) {
295+ let lines: Vec<String> = text.lines().map(String::from).collect();
296+ let lines = if lines.is_empty() {
297+ vec![String::new()]
298+ } else {
299+ lines
300+ };
301+ if lines == self.editor.lines() {
302+ self.status = "no changes from external editor".into();
303+ return;
304+ }
305+ let cursor = self.editor.cursor();
306+ let (row, col) = (cursor.0, cursor.1);
307+ self.editor = TextArea::new(lines);
308+ crate::ui::draw::style_editor(&mut self.editor);
309+ self.editor
310+ .move_cursor(CursorMove::Jump(row as u16, col as u16));
311+ if from_saved_file {
312+ self.saved_text = self.text();
313+ }
314+ let n = self.editor.lines().len();
315+ self.breakpoints.retain(|&l| l < n);
316+ if self.assemble() {
317+ self.status = format!("reloaded from external editor; {}", self.status);
318+ }
319+ }
320+
321+ // ---- Build & run ----
322+
323+ /// Assembles the editor contents. On success the program is loaded
324+ /// into a freshly reset simulator.
325+ pub fn assemble(&mut self) -> bool {
326+ let source = self.text();
327+ let out = asm::assemble(&source);
328+ let ok = out.ok();
329+ 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+ self.follow_pc();
342+ } else {
343+ 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+ self.editor
351+ .move_cursor(CursorMove::Jump(first.line as u16, first.col as u16));
352+ self.mode = Mode::Edit;
353+ self.out_tab = OutTab::Errors;
354+ }
355+ self.out_scroll = 0;
356+ self.last_asm = Some((source, out));
357+ ok
358+ }
359+
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+ /// Makes sure an up-to-date build is loaded; returns false on errors.
371+ fn ensure_built(&mut self) -> bool {
372+ if self.stale() && !self.assemble() {
373+ return false;
374+ }
375+ if self.mode == Mode::Edit {
376+ self.mode = Mode::Debug;
377+ self.focus = Focus::Source;
378+ }
379+ true
380+ }
381+
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.
402+ pub fn follow_pc(&mut self) {
403+ if let Some(l) = self.pc_line() {
404+ self.src_cursor = l;
405+ }
406+ }
407+
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) {
422+ if !self.ensure_built() {
423+ return;
424+ }
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+ }
437+ }
438+
439+ fn toggle_run(&mut self) {
440+ if self.running {
441+ self.running = false;
442+ self.status = format!("paused at {:04X}h", self.sim.pc);
443+ self.follow_pc();
444+ } else if self.ensure_built() {
445+ self.running = true;
446+ self.resume = true;
447+ self.status = "running…".into();
448+ }
449+ }
450+
451+ fn run_to_cursor(&mut self) {
452+ if !self.ensure_built() {
453+ return;
454+ }
455+ let line = if self.mode == Mode::Debug {
456+ self.src_cursor
457+ } else {
458+ 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();
462+ 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+ }
471+ });
472+ }
473+
474+ fn reset(&mut self) {
475+ self.sim.reset();
476+ self.running = false;
477+ self.snapshot();
478+ self.follow_pc();
479+ self.status = "reset".into();
480+ }
481+
482+ 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+ }
491+ }
492+
493+ /// Called once per frame.
494+ 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),
508+ }
509+ }
510+
511+ // ---- Input ----
512+
513+ pub fn on_mouse(&mut self, m: MouseEvent) {
514+ let delta: isize = match m.kind {
515+ MouseEventKind::ScrollDown => 3,
516+ MouseEventKind::ScrollUp => -3,
517+ _ => return,
518+ };
519+ match self.mode {
520+ Mode::Edit => self.out_scroll = self.out_scroll.saturating_add_signed(delta),
521+ Mode::Debug => self.src_cursor = self.src_cursor.saturating_add_signed(delta),
522+ }
523+ }
524+
525+ pub fn on_key(&mut self, key: KeyEvent) {
526+ let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
527+ if !(ctrl && key.code == KeyCode::Char('q')) {
528+ self.quit_armed = false;
529+ }
530+ if self.prompt.is_some() {
531+ self.on_prompt_key(key);
532+ return;
533+ }
534+ if self.show_help {
535+ self.show_help = false;
536+ return;
537+ }
538+
539+ // Global keys.
540+ match (key.code, ctrl) {
541+ (KeyCode::Char('q'), true) => {
542+ if self.dirty() && !self.quit_armed {
543+ self.quit_armed = true;
544+ self.status = "unsaved changes — press Ctrl-Q again to quit".into();
545+ } else {
546+ self.quit = true;
547+ }
548+ }
549+ (KeyCode::F(1), _) => self.show_help = true,
550+ (KeyCode::F(2), _) | (KeyCode::Char('s'), true) => self.save(None),
551+ (KeyCode::Char('g'), true) => self.external_edit = true,
552+ (KeyCode::F(3), _) | (KeyCode::Char('o'), true) => {
553+ self.prompt = Some(Prompt {
554+ kind: PromptKind::Open,
555+ input: String::new(),
556+ })
557+ }
558+ (KeyCode::F(4), _) => match self.mode {
559+ // ensure_built switches to debug mode on success.
560+ Mode::Edit => {
561+ self.ensure_built();
562+ }
563+ Mode::Debug => self.mode = Mode::Edit,
564+ },
565+ (KeyCode::F(5), _) => self.toggle_run(),
566+ (KeyCode::F(6), _) => self.reset(),
567+ (KeyCode::F(7), _) => {
568+ if self.assemble() && self.mode == Mode::Edit {
569+ self.out_tab = OutTab::Listing;
570+ }
571+ }
572+ (KeyCode::F(8), _) => self.run_to_cursor(),
573+ (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)),
580+ _ => match self.mode {
581+ Mode::Edit => self.on_edit_key(key),
582+ Mode::Debug => self.on_debug_key(key),
583+ },
584+ }
585+ }
586+
587+ fn on_edit_key(&mut self, key: KeyEvent) {
588+ let alt = key.modifiers.contains(KeyModifiers::ALT);
589+ match key.code {
590+ // Alt-1/2/3 switch the output tab; Alt-Up/Down scroll it.
591+ KeyCode::Char('1') if alt => self.out_tab = OutTab::Listing,
592+ KeyCode::Char('2') if alt => self.out_tab = OutTab::Hex,
593+ KeyCode::Char('3') if alt => self.out_tab = OutTab::Errors,
594+ KeyCode::Up if alt => self.out_scroll = self.out_scroll.saturating_sub(1),
595+ KeyCode::Down if alt => self.out_scroll += 1,
596+ KeyCode::PageUp if alt => self.out_scroll = self.out_scroll.saturating_sub(20),
597+ KeyCode::PageDown if alt => self.out_scroll += 20,
598+ KeyCode::Esc if self.running => self.toggle_run(),
599+ _ => {
600+ self.editor.input(key);
601+ }
602+ }
603+ }
604+
605+ fn on_debug_key(&mut self, key: KeyEvent) {
606+ if self.focus == Focus::Serial {
607+ match key.code {
608+ KeyCode::Enter => {
609+ let mut bytes = std::mem::take(&mut self.serial_input).into_bytes();
610+ bytes.push(b'\r');
611+ self.sim.uart_send(&bytes);
612+ return;
613+ }
614+ KeyCode::Backspace => {
615+ self.serial_input.pop();
616+ return;
617+ }
618+ KeyCode::Char(c) if !key.modifiers.contains(KeyModifiers::CONTROL) => {
619+ self.serial_input.push(c);
620+ return;
621+ }
622+ _ => {}
623+ }
624+ }
625+
626+ let page = 16usize;
627+ match key.code {
628+ KeyCode::Tab => self.cycle_focus(true),
629+ KeyCode::BackTab => self.cycle_focus(false),
630+ KeyCode::Esc => {
631+ if self.running {
632+ self.toggle_run();
633+ } else if self.focus == Focus::Serial {
634+ self.focus = Focus::Source;
635+ } else {
636+ self.mode = Mode::Edit;
637+ }
638+ }
639+ KeyCode::Char('+') | KeyCode::Char('=') => {
640+ self.speed = (self.speed + 1).min(SPEEDS.len() - 1)
641+ }
642+ KeyCode::Char('-') => self.speed = self.speed.saturating_sub(1),
643+ KeyCode::Char('?') => self.show_help = true,
644+ // Letter aliases for terminals that swallow function keys.
645+ KeyCode::Char('r') => self.toggle_run(),
646+ KeyCode::Char('s') => self.on_key(KeyEvent::from(KeyCode::F(11))),
647+ KeyCode::Char('n') => self.on_key(KeyEvent::from(KeyCode::F(10))),
648+ KeyCode::Char('o') => self.on_key(KeyEvent::from(KeyCode::F(12))),
649+ KeyCode::Char('g') => self.run_to_cursor(),
650+ KeyCode::Char('b') => self.toggle_breakpoint(),
651+ KeyCode::Char('x') => self.reset(),
652+ KeyCode::Char('e') => self.mode = Mode::Edit,
653+ KeyCode::Char('i') => self.set_mem_tab(MemTab::Iram),
654+ KeyCode::Char('f') => self.set_mem_tab(MemTab::Sfr),
655+ KeyCode::Char('d') => self.set_mem_tab(MemTab::Xram),
656+ KeyCode::Char('c') => self.set_mem_tab(MemTab::Code),
657+ KeyCode::Char('p') => {
658+ self.prompt = Some(Prompt {
659+ kind: PromptKind::Poke,
660+ input: String::new(),
661+ });
662+ }
663+ _ => match self.focus {
664+ Focus::Source => self.move_src(key.code, page),
665+ Focus::Memory => self.move_mem(key.code, page),
666+ Focus::Ports => self.move_ports(key.code),
667+ Focus::Serial => {}
668+ },
669+ }
670+ }
671+
672+ fn set_mem_tab(&mut self, tab: MemTab) {
673+ self.mem_tab = tab;
674+ self.mem_scroll = 0;
675+ self.focus = Focus::Memory;
676+ }
677+
678+ fn cycle_focus(&mut self, fwd: bool) {
679+ const ORDER: [Focus; 4] = [Focus::Source, Focus::Memory, Focus::Ports, Focus::Serial];
680+ let i = ORDER.iter().position(|&f| f == self.focus).unwrap_or(0);
681+ let n = ORDER.len();
682+ self.focus = ORDER[if fwd { (i + 1) % n } else { (i + n - 1) % n }];
683+ }
684+
685+ fn move_src(&mut self, code: KeyCode, page: usize) {
686+ let max = self
687+ .build
688+ .as_ref()
689+ .map_or(0, |b| b.source.len().saturating_sub(1));
690+ self.src_cursor = match code {
691+ KeyCode::Up => self.src_cursor.saturating_sub(1),
692+ KeyCode::Down => self.src_cursor + 1,
693+ KeyCode::PageUp => self.src_cursor.saturating_sub(page),
694+ KeyCode::PageDown => self.src_cursor + page,
695+ KeyCode::Home => 0,
696+ KeyCode::End => max,
697+ KeyCode::Char('.') => self.pc_line().unwrap_or(self.src_cursor),
698+ _ => self.src_cursor,
699+ }
700+ .min(max);
701+ }
702+
703+ fn move_mem(&mut self, code: KeyCode, page: usize) {
704+ let rows = match self.mem_tab {
705+ MemTab::Iram => 16,
706+ MemTab::Sfr => 8,
707+ MemTab::Xram | MemTab::Code => 4096,
708+ };
709+ self.mem_scroll = match code {
710+ KeyCode::Up => self.mem_scroll.saturating_sub(1),
711+ KeyCode::Down => self.mem_scroll + 1,
712+ KeyCode::PageUp => self.mem_scroll.saturating_sub(page),
713+ KeyCode::PageDown => self.mem_scroll + page,
714+ KeyCode::Home => 0,
715+ KeyCode::End => rows,
716+ KeyCode::Left => {
717+ self.mem_tab = match self.mem_tab {
718+ MemTab::Iram => MemTab::Code,
719+ MemTab::Sfr => MemTab::Iram,
720+ MemTab::Xram => MemTab::Sfr,
721+ MemTab::Code => MemTab::Xram,
722+ };
723+ 0
724+ }
725+ KeyCode::Right => {
726+ self.mem_tab = match self.mem_tab {
727+ MemTab::Iram => MemTab::Sfr,
728+ MemTab::Sfr => MemTab::Xram,
729+ MemTab::Xram => MemTab::Code,
730+ MemTab::Code => MemTab::Iram,
731+ };
732+ 0
733+ }
734+ _ => self.mem_scroll,
735+ }
736+ .min(rows.saturating_sub(1));
737+ }
738+
739+ fn move_ports(&mut self, code: KeyCode) {
740+ let (port, bit) = &mut self.port_sel;
741+ match code {
742+ KeyCode::Up => *port = port.saturating_sub(1),
743+ KeyCode::Down => *port = (*port + 1).min(3),
744+ // Bits are drawn MSB first, so Left moves towards bit 7.
745+ KeyCode::Left => *bit = (*bit + 1).min(7),
746+ KeyCode::Right => *bit = bit.saturating_sub(1),
747+ KeyCode::Char(' ') | KeyCode::Enter => {
748+ self.sim.pins[*port] ^= 1 << *bit;
749+ }
750+ _ => {}
751+ }
752+ }
753+
754+ fn on_prompt_key(&mut self, key: KeyEvent) {
755+ let Some(p) = &mut self.prompt else { return };
756+ match key.code {
757+ KeyCode::Esc => self.prompt = None,
758+ KeyCode::Backspace => {
759+ p.input.pop();
760+ }
761+ KeyCode::Char(c) => p.input.push(c),
762+ KeyCode::Enter => {
763+ let p = self.prompt.take().unwrap();
764+ let input = p.input.trim().to_string();
765+ if input.is_empty() {
766+ return;
767+ }
768+ match p.kind {
769+ PromptKind::Open => self.open(Path::new(&input)),
770+ PromptKind::SaveAs => self.save(Some(PathBuf::from(input))),
771+ PromptKind::Poke => self.poke(&input),
772+ }
773+ }
774+ _ => {}
775+ }
776+ }
777+
778+ /// `addr value` → write `value` at `addr` in the current memory tab.
779+ fn poke(&mut self, input: &str) {
780+ let parse = |s: &str| {
781+ let s = s.trim_start_matches("0x").trim_end_matches(['h', 'H']);
782+ u32::from_str_radix(s, 16).ok()
783+ };
784+ let parts: Vec<&str> = input.split_whitespace().collect();
785+ let (Some(addr), Some(val)) = (
786+ parts.first().and_then(|s| parse(s)),
787+ parts.get(1).and_then(|s| parse(s)),
788+ ) else {
789+ self.status = "usage: <addr> <value> (hex), e.g. 30 FF".into();
790+ return;
791+ };
792+ if val > 0xFF {
793+ self.status = "value must be a byte".into();
794+ return;
795+ }
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+ };
821+ }
822+}
823+
824+/// Command that opens `path` at `line` in the user's editor: `$EDITOR`,
825+/// then `$VISUAL`, then `vi`. Run through `sh` so values such as
826+/// `code --wait` work as they do for git.
827+fn editor_command(path: &Path, line: usize) -> Command {
828+ let editor = ["EDITOR", "VISUAL"]
829+ .iter()
830+ .filter_map(|v| std::env::var(v).ok())
831+ .find(|v| !v.trim().is_empty())
832+ .unwrap_or_else(|| "vi".into());
833+ // Editors known to accept `+LINE` before the file name.
834+ let program = editor.split_whitespace().next().unwrap_or_default();
835+ let name = Path::new(program)
836+ .file_name()
837+ .and_then(|n| n.to_str())
838+ .unwrap_or_default();
839+ let goto = matches!(
840+ name,
841+ "vi" | "vim" | "nvim" | "nano" | "emacs" | "micro" | "kak" | "ne"
842+ )
843+ .then(|| format!("+{line}"));
844+
845+ let mut cmd = Command::new("sh");
846+ cmd.arg("-c").arg(format!("{editor} \"$@\"")).arg("sh");
847+ cmd.args(goto).arg(path);
848+ cmd
849+}
850+
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+const TEMPLATE: &str = "\
892+; New 8051 program. F7 assembles, F5 runs, F11 steps, F1 shows all keys.
893+ ORG 0000h
894+ LJMP main
895+
896+ ORG 0030h
897+main: MOV A,#01h ; walking LED pattern on P1
898+loop: MOV P1,A
899+ RL A
900+ SJMP loop
901+
902+ END
903+";
A src/ui/draw.rs View
@@ -0,0 +1,882 @@
1+use ratatui::Frame;
2+use ratatui::layout::{Constraint, Layout, Rect};
3+use ratatui::style::{Color, Modifier, Style};
4+use ratatui::text::{Line, Span};
5+use ratatui::widgets::{Block, BorderType, Borders, Clear, Paragraph, Tabs, Wrap};
6+use ratatui_textarea::TextArea;
7+
8+use super::app::{App, Focus, MemTab, Mode, OutTab, PromptKind, SPEEDS};
9+use super::syntax::highlight;
10+use crate::sfr::{self, IE, IP, PCON, PSW, SCON, TCON, TH0, TH1, TL0, TL1, TMOD};
11+
12+const ACCENT: Color = Color::Cyan;
13+const CHANGED: Style = Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD);
14+const DIM: Style = Style::new().fg(Color::DarkGray);
15+const LABEL: Style = Style::new().fg(Color::Gray);
16+const ON: Style = Style::new()
17+ .fg(Color::LightGreen)
18+ .add_modifier(Modifier::BOLD);
19+const PC_BG: Color = Color::Indexed(58);
20+const CURSOR_BG: Color = Color::Indexed(236);
21+
22+pub fn style_editor(ed: &mut TextArea<'static>) {
23+ ed.set_line_number_style(DIM);
24+ ed.set_cursor_line_style(Style::new().bg(CURSOR_BG));
25+ ed.set_tab_length(8);
26+}
27+
28+fn panel(title: &str, focused: bool) -> Block<'_> {
29+ Block::default()
30+ .borders(Borders::ALL)
31+ .border_type(BorderType::Rounded)
32+ .border_style(if focused {
33+ Style::new().fg(ACCENT)
34+ } else {
35+ DIM
36+ })
37+ .title(Span::styled(
38+ format!(" {title} "),
39+ if focused {
40+ Style::new().fg(ACCENT).bold()
41+ } else {
42+ LABEL
43+ },
44+ ))
45+}
46+
47+pub fn draw(f: &mut Frame, app: &mut App) {
48+ let [header, body, footer] = Layout::vertical([
49+ Constraint::Length(1),
50+ Constraint::Min(5),
51+ Constraint::Length(1),
52+ ])
53+ .areas(f.area());
54+ draw_header(f, app, header);
55+ match app.mode {
56+ Mode::Edit => draw_edit(f, app, body),
57+ Mode::Debug => draw_debug(f, app, body),
58+ }
59+ draw_footer(f, app, footer);
60+ if app.show_help {
61+ draw_help(f);
62+ }
63+}
64+
65+fn draw_header(f: &mut Frame, app: &App, area: Rect) {
66+ let (mode, mode_style) = match app.mode {
67+ Mode::Edit => (
68+ " EDIT ",
69+ Style::new().bg(Color::Blue).fg(Color::White).bold(),
70+ ),
71+ Mode::Debug => (
72+ " DEBUG ",
73+ Style::new().bg(Color::Magenta).fg(Color::White).bold(),
74+ ),
75+ };
76+ let (state, state_style) = if app.running {
77+ (
78+ " RUNNING ",
79+ Style::new().bg(Color::Green).fg(Color::Black).bold(),
80+ )
81+ } else if app.build.is_none() {
82+ (
83+ " NOT BUILT ",
84+ Style::new().bg(Color::DarkGray).fg(Color::White),
85+ )
86+ } else {
87+ (
88+ " STOPPED ",
89+ Style::new().bg(Color::Yellow).fg(Color::Black).bold(),
90+ )
91+ };
92+ let mut spans = vec![
93+ Span::styled(" i8051 ", Style::new().bg(ACCENT).fg(Color::Black).bold()),
94+ Span::raw(" "),
95+ Span::styled(mode, mode_style),
96+ Span::styled(state, state_style),
97+ Span::raw(format!(" {}", app.title())),
98+ ];
99+ if app.build.is_some() && app.stale() {
100+ spans.push(Span::styled(
101+ " (source changed — F7 to rebuild)",
102+ Style::new().fg(Color::Yellow),
103+ ));
104+ }
105+ spans.push(Span::styled(
106+ format!(" speed {}/frame (+/-)", SPEEDS[app.speed]),
107+ DIM,
108+ ));
109+ f.render_widget(Line::from(spans), area);
110+}
111+
112+fn draw_footer(f: &mut Frame, app: &App, area: Rect) {
113+ if let Some(p) = &app.prompt {
114+ let label = match p.kind {
115+ PromptKind::Open => "Open file (.asm or .hex): ",
116+ PromptKind::SaveAs => "Save as: ",
117+ PromptKind::Poke => match app.mem_tab {
118+ MemTab::Iram => "Write IRAM — <addr> <value> (hex): ",
119+ MemTab::Sfr => "Write SFR — <addr> <value> (hex): ",
120+ MemTab::Xram => "Write XRAM — <addr> <value> (hex): ",
121+ MemTab::Code => "Write CODE — <addr> <value> (hex): ",
122+ },
123+ };
124+ let line = Line::from(vec![
125+ Span::styled(label, Style::new().fg(ACCENT).bold()),
126+ Span::raw(p.input.clone()),
127+ Span::styled("█", Style::new().fg(ACCENT)),
128+ Span::styled(" Enter confirm · Esc cancel", DIM),
129+ ]);
130+ f.render_widget(line, area);
131+ return;
132+ }
133+ let keys = match app.mode {
134+ Mode::Edit => "F7 build F5 run F11 step F9 bp F4 debug ^G $EDITOR F1 help",
135+ Mode::Debug => {
136+ "F5 run/pause F11 step F10 over F12 out F8 to cursor F9 bp F6 reset Tab focus F1 help"
137+ }
138+ };
139+ let width = area.width as usize;
140+ let status = format!(" {}", app.status);
141+ let pad = width.saturating_sub(status.chars().count() + keys.len() + 1);
142+ let line = Line::from(vec![
143+ Span::styled(status, Style::new().fg(Color::White)),
144+ Span::raw(" ".repeat(pad)),
145+ Span::styled(keys, DIM),
146+ ]);
147+ f.render_widget(line, area);
148+}
149+
150+// ---------------------------------------------------------------- Edit mode
151+
152+fn draw_edit(f: &mut Frame, app: &mut App, area: Rect) {
153+ let [left, right] =
154+ Layout::horizontal([Constraint::Percentage(58), Constraint::Percentage(42)]).areas(area);
155+
156+ let block = panel("Editor", true);
157+ let inner = block.inner(left);
158+ f.render_widget(block, left);
159+ let [gutter, text] =
160+ Layout::horizontal([Constraint::Length(2), Constraint::Min(1)]).areas(inner);
161+ f.render_widget(&app.editor, text);
162+
163+ // Breakpoint / error markers aligned with the editor's scroll position.
164+ let top = app.editor.scroll_offset().0 as usize;
165+ 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();
171+ let marks: Vec<Line> = (top..top + gutter.height as usize)
172+ .map(|l| {
173+ if errors.contains(&l) {
174+ Line::styled("✗", Style::new().fg(Color::Red).bold())
175+ } else if app.breakpoints.contains(&l) {
176+ Line::styled("●", Style::new().fg(Color::Red))
177+ } else {
178+ Line::raw("")
179+ }
180+ })
181+ .collect();
182+ f.render_widget(Paragraph::new(marks), gutter);
183+
184+ draw_output(f, app, right);
185+}
186+
187+fn draw_output(f: &mut Frame, app: &mut App, area: Rect) {
188+ let n_err = app
189+ .last_asm
190+ .as_ref()
191+ .map_or(0, |(_, o)| o.diagnostics.len());
192+ let block = panel("Output", false);
193+ let inner = block.inner(area);
194+ f.render_widget(block, area);
195+ let [tabs_area, content] =
196+ Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).areas(inner);
197+ let err_title = if n_err > 0 {
198+ format!("Errors ({n_err})")
199+ } else {
200+ "Errors".to_string()
201+ };
202+ let selected = match app.out_tab {
203+ OutTab::Listing => 0,
204+ OutTab::Hex => 1,
205+ OutTab::Errors => 2,
206+ };
207+ let tabs = Tabs::new(vec![
208+ "Listing [Alt-1]".to_string(),
209+ "HEX [Alt-2]".to_string(),
210+ format!("{err_title} [Alt-3]"),
211+ ])
212+ .select(selected)
213+ .style(DIM)
214+ .highlight_style(Style::new().fg(ACCENT).bold().underlined());
215+ f.render_widget(tabs, tabs_area);
216+
217+ let lines: Vec<Line> = match (app.out_tab, &app.last_asm) {
218+ (_, None) => vec![Line::styled("Press F7 to assemble.", DIM)],
219+ (OutTab::Listing, Some((src, out))) => out
220+ .listing(src)
221+ .lines()
222+ .map(|l| {
223+ if l.starts_with("***") {
224+ Line::styled(l.to_string(), Style::new().fg(Color::Red).bold())
225+ } else if l.len() > 25 && l.as_bytes()[0].is_ascii_hexdigit() {
226+ // LOC OBJ LINE | source
227+ let (head, src) = l.split_at(25);
228+ let mut spans =
229+ vec![Span::styled(head.to_string(), Style::new().fg(Color::Gray))];
230+ spans.extend(highlight(src, Style::new()));
231+ Line::from(spans)
232+ } else if l.len() > 25 && l.starts_with(' ') {
233+ let (head, src) = l.split_at(25);
234+ let mut spans = vec![Span::styled(head.to_string(), DIM)];
235+ spans.extend(highlight(src, Style::new()));
236+ Line::from(spans)
237+ } else {
238+ Line::styled(l.to_string(), LABEL)
239+ }
240+ })
241+ .collect(),
242+ (OutTab::Hex, Some((_, out))) if out.ok() => {
243+ out.intel_hex().lines().map(hex_line).collect()
244+ }
245+ (OutTab::Hex, Some(_)) => vec![Line::styled("Fix the errors to generate HEX output.", DIM)],
246+ (OutTab::Errors, Some((_, out))) if out.ok() => {
247+ vec![Line::styled("No errors.", Style::new().fg(Color::Green))]
248+ }
249+ (OutTab::Errors, Some((_, out))) => out
250+ .diagnostics
251+ .iter()
252+ .map(|d| {
253+ Line::from(vec![
254+ Span::styled(
255+ format!("line {:>4}:{:<3} ", d.line + 1, d.col + 1),
256+ Style::new().fg(Color::Red),
257+ ),
258+ Span::raw(d.msg.clone()),
259+ ])
260+ })
261+ .collect(),
262+ };
263+ app.out_scroll = app.out_scroll.min(lines.len().saturating_sub(1));
264+ f.render_widget(
265+ Paragraph::new(lines).scroll((app.out_scroll as u16, 0)),
266+ content,
267+ );
268+}
269+
270+/// Colors the fields of an Intel HEX record.
271+fn hex_line(l: &str) -> Line<'static> {
272+ if l.len() < 11 {
273+ return Line::raw(l.to_string());
274+ }
275+ let n = l.len();
276+ Line::from(vec![
277+ Span::styled(":", DIM),
278+ Span::styled(l[1..3].to_string(), Style::new().fg(Color::Magenta)),
279+ Span::styled(l[3..7].to_string(), Style::new().fg(Color::Yellow)),
280+ Span::styled(l[7..9].to_string(), Style::new().fg(Color::Blue)),
281+ Span::raw(l[9..n - 2].to_string()),
282+ Span::styled(l[n - 2..].to_string(), DIM),
283+ ])
284+}
285+
286+// --------------------------------------------------------------- Debug mode
287+
288+fn draw_debug(f: &mut Frame, app: &mut App, area: Rect) {
289+ let [left, right] =
290+ Layout::horizontal([Constraint::Percentage(45), Constraint::Percentage(55)]).areas(area);
291+ draw_source(f, app, left);
292+
293+ let [top, mid, bottom] = Layout::vertical([
294+ Constraint::Length(10),
295+ Constraint::Min(6),
296+ Constraint::Length(8),
297+ ])
298+ .areas(right);
299+ let [regs, periph] =
300+ Layout::horizontal([Constraint::Length(30), Constraint::Min(20)]).areas(top);
301+ let [ports, serial] =
302+ Layout::horizontal([Constraint::Length(26), Constraint::Min(20)]).areas(bottom);
303+ draw_registers(f, app, regs);
304+ draw_peripherals(f, app, periph);
305+ draw_memory(f, app, mid);
306+ draw_ports(f, app, ports);
307+ draw_serial(f, app, serial);
308+}
309+
310+fn draw_source(f: &mut Frame, app: &mut App, area: Rect) {
311+ let focused = app.focus == Focus::Source;
312+ let block = panel("Source", focused);
313+ let inner = block.inner(area);
314+ f.render_widget(block, area);
315+ let Some(build) = &app.build else {
316+ f.render_widget(
317+ Paragraph::new("Nothing assembled yet — press F7.").style(DIM),
318+ inner,
319+ );
320+ return;
321+ };
322+
323+ let h = inner.height as usize;
324+ if app.src_cursor < app.src_scroll {
325+ app.src_scroll = app.src_cursor;
326+ } else if app.src_cursor >= app.src_scroll + h {
327+ app.src_scroll = app.src_cursor + 1 - h;
328+ }
329+ let pc_line = build.addr_to_line.get(&app.sim.pc).copied();
330+ let lines: Vec<Line> = build
331+ .source
332+ .iter()
333+ .enumerate()
334+ .skip(app.src_scroll)
335+ .take(h)
336+ .map(|(i, text)| {
337+ let out = &build.out.lines[i];
338+ let is_pc = pc_line == Some(i);
339+ let bg = if is_pc {
340+ Style::new().bg(PC_BG)
341+ } else if i == app.src_cursor && focused {
342+ Style::new().bg(CURSOR_BG)
343+ } else {
344+ Style::new()
345+ };
346+ let bp = if app.breakpoints.contains(&i) {
347+ Span::styled("●", bg.fg(Color::Red))
348+ } else {
349+ Span::styled(" ", bg)
350+ };
351+ let arrow = if is_pc {
352+ Span::styled("▶", bg.fg(Color::Yellow).bold())
353+ } else {
354+ Span::styled(" ", bg)
355+ };
356+ let addr = match (out.addr, out.bytes.is_empty()) {
357+ (Some(a), false) => Span::styled(format!(" {a:04X} "), bg.fg(Color::Gray)),
358+ _ => Span::styled(" ", bg),
359+ };
360+ let mut spans = vec![bp, arrow, addr];
361+ spans.extend(highlight(text, bg));
362+ let used: usize = spans.iter().map(|s| s.content.chars().count()).sum();
363+ spans.push(Span::styled(
364+ " ".repeat((inner.width as usize).saturating_sub(used)),
365+ bg,
366+ ));
367+ Line::from(spans)
368+ })
369+ .collect();
370+ f.render_widget(Paragraph::new(lines), inner);
371+}
372+
373+fn hex2(v: u8, changed: bool) -> Span<'static> {
374+ Span::styled(
375+ format!("{v:02X}"),
376+ if changed {
377+ CHANGED
378+ } else {
379+ Style::new().fg(Color::White)
380+ },
381+ )
382+}
383+
384+fn flags(byte: u8, names: [&'static str; 8]) -> Vec<Span<'static>> {
385+ names
386+ .iter()
387+ .enumerate()
388+ .filter(|(_, n)| !n.is_empty())
389+ .flat_map(|(i, &n)| {
390+ let on = byte & (0x80 >> i) != 0;
391+ [Span::styled(n, if on { ON } else { DIM }), Span::raw(" ")]
392+ })
393+ .collect()
394+}
395+
396+fn draw_registers(f: &mut Frame, app: &App, area: Rect) {
397+ let s = &app.sim;
398+ let block = panel("Registers", false);
399+ let inner = block.inner(area);
400+ f.render_widget(block, area);
401+ let ch = |addr: u8| s.sfr(addr) != app.prev_sfr[(addr & 0x7F) as usize];
402+ let reg = |n: u8| {
403+ let a = s.reg_addr(n) as usize;
404+ vec![
405+ Span::styled(format!("R{n} "), LABEL),
406+ hex2(s.iram[a], s.iram[a] != app.prev_iram[a]),
407+ Span::raw(" "),
408+ ]
409+ };
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+ };
418+ let lines = vec![
419+ Line::from(vec![
420+ Span::styled("PC ", LABEL),
421+ Span::styled(
422+ format!("{:04X}", s.pc),
423+ Style::new().fg(Color::Yellow).bold(),
424+ ),
425+ Span::styled(" DPTR ", LABEL),
426+ Span::styled(
427+ format!("{:04X}", s.dptr()),
428+ if ch(sfr::DPL) || ch(sfr::DPH) {
429+ CHANGED
430+ } else {
431+ Style::new()
432+ },
433+ ),
434+ ]),
435+ Line::from(vec![
436+ Span::styled("A ", LABEL),
437+ hex2(s.acc(), ch(sfr::ACC)),
438+ Span::styled(" B ", LABEL),
439+ hex2(s.b(), ch(sfr::B)),
440+ Span::styled(" SP ", LABEL),
441+ hex2(s.sp(), ch(sfr::SP)),
442+ ]),
443+ Line::from([0, 1, 2, 3].into_iter().flat_map(reg).collect::<Vec<_>>()),
444+ Line::from([4, 5, 6, 7].into_iter().flat_map(reg).collect::<Vec<_>>()),
445+ Line::from(
446+ [
447+ Span::styled("PSW ", LABEL),
448+ hex2(s.psw(), ch(PSW)),
449+ Span::raw(" "),
450+ ]
451+ .into_iter()
452+ .chain(flags(s.psw(), ["C", "AC", "F0", "", "", "OV", "", "P"]))
453+ .collect::<Vec<_>>(),
454+ ),
455+ Line::from(vec![
456+ Span::styled("Bank ", LABEL),
457+ Span::raw(format!("{}", s.bank())),
458+ Span::styled(" Cycles ", LABEL),
459+ Span::raw(format!("{}", s.cycles)),
460+ ]),
461+ Line::from(vec![
462+ Span::styled("Time ", LABEL),
463+ Span::raw(time),
464+ Span::styled(format!(" @{}MHz", app.clock_mhz), DIM),
465+ ]),
466+ ];
467+ f.render_widget(Paragraph::new(lines), inner);
468+}
469+
470+fn draw_peripherals(f: &mut Frame, app: &App, area: Rect) {
471+ let s = &app.sim;
472+ let block = panel("Peripherals", false);
473+ let inner = block.inner(area);
474+ f.render_widget(block, area);
475+ let ch = |addr: u8| s.sfr(addr) != app.prev_sfr[(addr & 0x7F) as usize];
476+ let sfr_val = |name: &'static str, addr: u8| {
477+ vec![
478+ Span::styled(name, LABEL),
479+ hex2(s.sfr(addr), ch(addr)),
480+ Span::raw(" "),
481+ ]
482+ };
483+
484+ let tmod = s.sfr(TMOD);
485+ let tcon = s.sfr(TCON);
486+ let timer = |n: u8| {
487+ let ctl = (tmod >> (4 * n)) & 0x0F;
488+ let (th, tl, tr) = if n == 0 {
489+ (TH0, TL0, 0x10)
490+ } else {
491+ (TH1, TL1, 0x40)
492+ };
493+ let kind = if ctl & 4 != 0 { "ctr" } else { "tmr" };
494+ let gate = if ctl & 8 != 0 { " gate" } else { "" };
495+ vec![
496+ Span::styled(format!("T{n} "), LABEL),
497+ Span::styled(
498+ format!("{:02X}{:02X}", s.sfr(th), s.sfr(tl)),
499+ if ch(th) || ch(tl) {
500+ CHANGED
501+ } else {
502+ Style::new()
503+ },
504+ ),
505+ Span::styled(format!(" m{} {kind}{gate} ", ctl & 3), DIM),
506+ if tcon & tr != 0 {
507+ Span::styled("run", ON)
508+ } else {
509+ Span::styled("stop", DIM)
510+ },
511+ Span::raw(" "),
512+ ]
513+ };
514+
515+ let scon = s.sfr(SCON);
516+ let irq = if s.in_service.is_empty() {
517+ Span::styled("none", DIM)
518+ } else {
519+ Span::styled(
520+ s.in_service
521+ .iter()
522+ .map(|l| format!("L{l}"))
523+ .collect::<Vec<_>>()
524+ .join(">"),
525+ Style::new().fg(Color::Yellow),
526+ )
527+ };
528+ let last = s.last_irq.map_or("-".to_string(), |v| {
529+ format!("{v:04X}h ({})", vector_name(v))
530+ });
531+ let mut cpu_state = vec![];
532+ if s.idle {
533+ cpu_state.push(Span::styled(" IDLE", Style::new().fg(Color::Yellow).bold()));
534+ }
535+ if s.power_down {
536+ cpu_state.push(Span::styled(
537+ " POWER-DOWN",
538+ Style::new().fg(Color::Red).bold(),
539+ ));
540+ }
541+
542+ let lines = vec![
543+ Line::from([sfr_val("TMOD ", TMOD), timer(0), timer(1)].concat()),
544+ Line::from(
545+ [
546+ sfr_val("TCON ", TCON),
547+ flags(
548+ tcon,
549+ ["TF1", "TR1", "TF0", "TR0", "IE1", "IT1", "IE0", "IT0"],
550+ ),
551+ ]
552+ .concat(),
553+ ),
554+ Line::from(
555+ [
556+ sfr_val("SCON ", SCON),
557+ vec![Span::styled(format!("mode{} ", scon >> 6), DIM)],
558+ flags(scon, ["", "", "SM2", "REN", "TB8", "RB8", "TI", "RI"]),
559+ ]
560+ .concat(),
561+ ),
562+ Line::from(vec![
563+ Span::styled("SBUF ", LABEL),
564+ Span::styled("rx ", DIM),
565+ hex2(s.uart.sbuf_rx, false),
566+ Span::styled(
567+ if s.uart.transmitting() {
568+ " tx busy"
569+ } else {
570+ " tx idle"
571+ },
572+ DIM,
573+ ),
574+ Span::styled(
575+ format!(" {} byte(s) queued", s.uart_rx_pending().len()),
576+ DIM,
577+ ),
578+ ]),
579+ Line::from(
580+ [
581+ sfr_val("IE ", IE),
582+ flags(s.sfr(IE), ["EA", "", "", "ES", "ET1", "EX1", "ET0", "EX0"]),
583+ ]
584+ .concat(),
585+ ),
586+ Line::from(
587+ [
588+ sfr_val("IP ", IP),
589+ flags(s.sfr(IP), ["", "", "", "PS", "PT1", "PX1", "PT0", "PX0"]),
590+ sfr_val(" PCON ", PCON),
591+ flags(s.sfr(PCON), ["SMOD", "", "", "", "", "", "PD", "IDL"]),
592+ ]
593+ .concat(),
594+ ),
595+ Line::from(
596+ [
597+ vec![
598+ Span::styled("IRQ ", LABEL),
599+ Span::styled("active ", DIM),
600+ irq,
601+ Span::styled(" last ", DIM),
602+ ],
603+ vec![Span::raw(last)],
604+ cpu_state,
605+ ]
606+ .concat(),
607+ ),
608+ ];
609+ f.render_widget(Paragraph::new(lines), inner);
610+}
611+
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+fn draw_memory(f: &mut Frame, app: &mut App, area: Rect) {
624+ let focused = app.focus == Focus::Memory;
625+ let block = panel("Memory", focused);
626+ let inner = block.inner(area);
627+ f.render_widget(block, area);
628+ let [tabs_area, content] =
629+ Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).areas(inner);
630+ let selected = match app.mem_tab {
631+ MemTab::Iram => 0,
632+ MemTab::Sfr => 1,
633+ MemTab::Xram => 2,
634+ MemTab::Code => 3,
635+ };
636+ f.render_widget(
637+ Tabs::new(vec!["IRAM [i]", "SFR [f]", "XRAM [d]", "CODE [c]"])
638+ .select(selected)
639+ .style(DIM)
640+ .highlight_style(Style::new().fg(ACCENT).bold().underlined()),
641+ tabs_area,
642+ );
643+
644+ if app.mem_tab == MemTab::Sfr {
645+ draw_sfr_table(f, app, content);
646+ return;
647+ }
648+
649+ let s = &app.sim;
650+ let per_row: usize = if content.width >= 74 { 16 } else { 8 };
651+ let total = match app.mem_tab {
652+ MemTab::Iram => 256,
653+ _ => 0x10000,
654+ };
655+ let rows = total / per_row;
656+ let h = content.height as usize;
657+ app.mem_scroll = app.mem_scroll.min(rows.saturating_sub(h));
658+ let bps = app.bp_addrs();
659+ let pc = s.pc as usize;
660+ let pc_len = crate::isa::info(s.code[pc]).map_or(1, |i| i.size as usize);
661+ let bank = s.reg_addr(0) as usize;
662+
663+ let lines: Vec<Line> = (app.mem_scroll..rows.min(app.mem_scroll + h))
664+ .map(|row| {
665+ let base = row * per_row;
666+ let mut spans = vec![Span::styled(
667+ if total > 256 {
668+ format!("{base:04X} ")
669+ } else {
670+ format!("{base:02X} ")
671+ },
672+ LABEL,
673+ )];
674+ let mut ascii = String::new();
675+ for i in 0..per_row {
676+ let a = base + i;
677+ let (v, style) = match app.mem_tab {
678+ MemTab::Iram => {
679+ let v = s.iram[a];
680+ let st = if a == s.sp() as usize {
681+ Style::new().fg(Color::Black).bg(Color::LightMagenta)
682+ } else if v != app.prev_iram[a] {
683+ CHANGED
684+ } else if (bank..bank + 8).contains(&a) {
685+ Style::new().fg(Color::LightBlue)
686+ } else if v == 0 {
687+ DIM
688+ } else {
689+ Style::new()
690+ };
691+ (v, st)
692+ }
693+ MemTab::Xram => {
694+ let v = s.xram[a];
695+ (v, if v == 0 { DIM } else { Style::new() })
696+ }
697+ _ => {
698+ let v = s.code[a];
699+ let st = if (pc..pc + pc_len).contains(&a) {
700+ Style::new().fg(Color::Black).bg(Color::Yellow)
701+ } else if bps.contains(&(a as u16)) {
702+ Style::new().fg(Color::White).bg(Color::Red)
703+ } else if v == 0 {
704+ DIM
705+ } else {
706+ Style::new()
707+ };
708+ (v, st)
709+ }
710+ };
711+ spans.push(Span::styled(format!("{v:02X}"), style));
712+ spans.push(Span::raw(if i == 7 && per_row == 16 { " " } else { " " }));
713+ ascii.push(if v.is_ascii_graphic() || v == b' ' {
714+ v as char
715+ } else {
716+ '·'
717+ });
718+ }
719+ spans.push(Span::styled(ascii, DIM));
720+ Line::from(spans)
721+ })
722+ .collect();
723+ f.render_widget(Paragraph::new(lines), content);
724+}
725+
726+fn draw_sfr_table(f: &mut Frame, app: &App, area: Rect) {
727+ let s = &app.sim;
728+ let cols = if area.width >= 64 { 3 } else { 2 };
729+ let per_col = sfr::SFRS.len().div_ceil(cols);
730+ let lines: Vec<Line> = (0..per_col)
731+ .map(|r| {
732+ let mut spans = Vec::new();
733+ for c in 0..cols {
734+ if let Some(&(name, addr)) = sfr::SFRS.get(c * per_col + r) {
735+ let v = s.read_direct(addr, true);
736+ let changed = s.sfr(addr) != app.prev_sfr[(addr & 0x7F) as usize];
737+ spans.push(Span::styled(format!("{name:<5}"), LABEL));
738+ spans.push(Span::styled(format!("{addr:02X} "), DIM));
739+ spans.push(hex2(v, changed));
740+ spans.push(Span::styled(
741+ format!(" {v:08b} "),
742+ if changed { CHANGED } else { DIM },
743+ ));
744+ }
745+ }
746+ Line::from(spans)
747+ })
748+ .collect();
749+ f.render_widget(Paragraph::new(lines), area);
750+}
751+
752+fn draw_ports(f: &mut Frame, app: &App, area: Rect) {
753+ let focused = app.focus == Focus::Ports;
754+ let block = panel("Ports", focused);
755+ let inner = block.inner(area);
756+ f.render_widget(block, area);
757+ let s = &app.sim;
758+ let mut lines: Vec<Line> = (0..4)
759+ .map(|p| {
760+ let latch = s.sfr(sfr::P0 + 0x10 * p as u8);
761+ let pins = s.port_pins(p);
762+ let mut spans = vec![Span::styled(format!("P{p} "), LABEL)];
763+ for bit in (0..8u8).rev() {
764+ let m = 1 << bit;
765+ let (sym, mut st) = if pins & m != 0 {
766+ ("●", Style::new().fg(Color::LightGreen))
767+ } else if s.pins[p] & m == 0 {
768+ // Pulled low from outside (e.g. a pressed button).
769+ ("○", Style::new().fg(Color::LightRed))
770+ } else {
771+ ("○", Style::new().fg(Color::DarkGray))
772+ };
773+ if focused && app.port_sel == (p, bit) {
774+ st = st.add_modifier(Modifier::REVERSED);
775+ }
776+ spans.push(Span::styled(sym, st));
777+ spans.push(Span::raw(if bit == 4 { " " } else { " " }));
778+ }
779+ spans.push(Span::styled(format!("{latch:02X}"), DIM));
780+ Line::from(spans)
781+ })
782+ .collect();
783+ if focused {
784+ lines.push(Line::styled("←→↑↓ select · space: pull pin low", DIM));
785+ } else {
786+ lines.push(Line::styled("●=1 ○=0 red=pulled low", DIM));
787+ }
788+ f.render_widget(Paragraph::new(lines), inner);
789+}
790+
791+fn draw_serial(f: &mut Frame, app: &App, area: Rect) {
792+ let focused = app.focus == Focus::Serial;
793+ let block = panel("Serial (UART)", focused);
794+ let inner = block.inner(area);
795+ f.render_widget(block, area);
796+ let [out_area, in_area] =
797+ Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).areas(inner);
798+
799+ let mut text = String::new();
800+ for &b in &app.sim.uart.tx_log {
801+ match b {
802+ b'\n' => text.push('\n'),
803+ b'\r' => {}
804+ 0x20..=0x7E => text.push(b as char),
805+ _ => text.push('·'),
806+ }
807+ }
808+ let lines: Vec<&str> = text.split('\n').collect();
809+ let h = out_area.height as usize;
810+ let shown: Vec<Line> = lines[lines.len().saturating_sub(h)..]
811+ .iter()
812+ .map(|l| Line::raw(l.to_string()))
813+ .collect();
814+ f.render_widget(Paragraph::new(shown).wrap(Wrap { trim: false }), out_area);
815+
816+ let input = if focused {
817+ Line::from(vec![
818+ Span::styled("> ", Style::new().fg(ACCENT)),
819+ Span::raw(app.serial_input.clone()),
820+ Span::styled("█", Style::new().fg(ACCENT)),
821+ ])
822+ } else {
823+ Line::styled("Tab here to type; Enter sends text + CR", DIM)
824+ };
825+ f.render_widget(input, in_area);
826+}
827+
828+fn draw_help(f: &mut Frame) {
829+ let area = f.area();
830+ let w = 72.min(area.width);
831+ let h = 31.min(area.height);
832+ let rect = Rect::new((area.width - w) / 2, (area.height - h) / 2, w, h);
833+ f.render_widget(Clear, rect);
834+ let k = |key: &str, desc: &str| {
835+ Line::from(vec![
836+ Span::styled(format!(" {key:<16}"), Style::new().fg(ACCENT).bold()),
837+ Span::raw(desc.to_string()),
838+ ])
839+ };
840+ let head = |t: &str| Line::styled(t.to_string(), Style::new().fg(Color::Yellow).bold());
841+ let lines = vec![
842+ head("Global"),
843+ k("F1", "this help"),
844+ k(
845+ "F2 / Ctrl-S",
846+ "save (writes .asm; F7 also writes .lst + .hex)",
847+ ),
848+ k("F3 / Ctrl-O", "open .asm, or .hex (disassembled)"),
849+ k("Ctrl-G", "edit in $EDITOR; reloads + reassembles on exit"),
850+ k("F4", "switch Edit ⇄ Debug"),
851+ k("F7", "assemble (reset + load)"),
852+ k("F5", "run / pause (assembles if needed)"),
853+ k("F11 F10 F12", "step into · step over · step out"),
854+ k("F8", "run to cursor line"),
855+ k("F9", "toggle breakpoint on cursor line"),
856+ k("F6", "reset CPU"),
857+ k("Ctrl-Q", "quit"),
858+ head("Edit mode"),
859+ k("Alt-1/2/3", "Listing / HEX / Errors tab"),
860+ k("Alt-↑↓ PgUp/Dn", "scroll output"),
861+ k("Ctrl-U / Ctrl-R", "undo / redo (Emacs-style editing keys)"),
862+ head("Debug mode"),
863+ k("Tab / Shift-Tab", "focus Source → Memory → Ports → Serial"),
864+ k(
865+ "r s n o g b x",
866+ "run · step · next · out · to cursor · bp · reset",
867+ ),
868+ k("i f d c", "memory: IRAM · SFR · XRAM · CODE"),
869+ k("p", "poke a byte into the shown memory"),
870+ k("+ / -", "simulation speed"),
871+ k(".", "jump source cursor to PC"),
872+ k("Esc", "pause / leave serial / back to editor"),
873+ Line::raw(""),
874+ Line::styled(" press any key to close", DIM),
875+ ];
876+ f.render_widget(
877+ Paragraph::new(lines)
878+ .block(panel("Help", true))
879+ .style(Style::new().bg(Color::Black)),
880+ rect,
881+ );
882+}
A src/ui/mod.rs View
@@ -0,0 +1,68 @@
1+mod app;
2+pub mod draw;
3+mod syntax;
4+
5+use std::io;
6+use std::path::PathBuf;
7+use std::time::{Duration, Instant};
8+
9+use ratatui::crossterm::event::{
10+ self, DisableMouseCapture, EnableMouseCapture, Event, KeyEventKind,
11+};
12+use ratatui::crossterm::execute;
13+use ratatui::crossterm::terminal::{
14+ EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
15+};
16+
17+use app::App;
18+
19+const FRAME: Duration = Duration::from_millis(33);
20+
21+pub fn run(path: Option<PathBuf>, clock_mhz: f64) -> io::Result<()> {
22+ let mut terminal = ratatui::init();
23+ execute!(io::stdout(), EnableMouseCapture)?;
24+ let mut app = App::new(path, clock_mhz);
25+ let result = event_loop(&mut terminal, &mut app);
26+ let _ = execute!(io::stdout(), DisableMouseCapture);
27+ ratatui::restore();
28+ result
29+}
30+
31+fn event_loop(terminal: &mut ratatui::DefaultTerminal, app: &mut App) -> io::Result<()> {
32+ let mut next_frame = Instant::now();
33+ while !app.quit {
34+ terminal.draw(|f| draw::draw(f, app))?;
35+ // Wait for input until the next frame is due; while running, the
36+ // simulation advances once per frame.
37+ let timeout = next_frame.saturating_duration_since(Instant::now());
38+ if event::poll(timeout)? {
39+ match event::read()? {
40+ Event::Key(k) if k.kind != KeyEventKind::Release => app.on_key(k),
41+ Event::Mouse(m) => app.on_mouse(m),
42+ _ => {}
43+ }
44+ }
45+ if std::mem::take(&mut app.external_edit) {
46+ suspend()?;
47+ app.edit_externally();
48+ resume(terminal)?;
49+ }
50+ if Instant::now() >= next_frame {
51+ app.tick();
52+ next_frame = Instant::now() + FRAME;
53+ }
54+ }
55+ Ok(())
56+}
57+
58+/// Hands the terminal back to the shell (for `$EDITOR`).
59+fn suspend() -> io::Result<()> {
60+ execute!(io::stdout(), DisableMouseCapture, LeaveAlternateScreen)?;
61+ disable_raw_mode()
62+}
63+
64+fn resume(terminal: &mut ratatui::DefaultTerminal) -> io::Result<()> {
65+ enable_raw_mode()?;
66+ execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture)?;
67+ terminal.clear()
68+}
A src/ui/syntax.rs View
@@ -0,0 +1,122 @@
1+//! Minimal 8051 assembly syntax highlighting.
2+
3+use ratatui::style::{Color, Modifier, Style};
4+use ratatui::text::Span;
5+
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()
65+ }
66+}
67+
68+/// Splits a source line into styled spans, applying `base` underneath.
69+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
122+}