web-a2e 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/.clangd +5 -0
  2. package/.mcp.json +12 -0
  3. package/CLAUDE.md +362 -0
  4. package/CMakeLists.txt +774 -0
  5. package/LICENSE +21 -0
  6. package/README.md +392 -0
  7. package/build-wasm/generated/roms.cpp +2447 -0
  8. package/docker-compose.staging.yml +9 -0
  9. package/docs/basic-rom-disassembly.md +6663 -0
  10. package/docs/softswitch-comparison.md +273 -0
  11. package/docs/thunderclock-debug.md +89 -0
  12. package/examples/cube.bas +72 -0
  13. package/examples/hello.s +55 -0
  14. package/examples/scroll.s +140 -0
  15. package/package.json +18 -0
  16. package/public/assets/apple-logo-old.png +0 -0
  17. package/public/assets/apple-logo.png +0 -0
  18. package/public/assets/drive-closed-light-on.png +0 -0
  19. package/public/assets/drive-closed.png +0 -0
  20. package/public/assets/drive-open-light-on.png +0 -0
  21. package/public/assets/drive-open.png +0 -0
  22. package/public/audio-worklet.js +82 -0
  23. package/public/disks/Apple DOS 3.3 January 1983.dsk +0 -0
  24. package/public/disks/ProDOS 2.4.3.po +0 -0
  25. package/public/disks/h32mb.2mg +0 -0
  26. package/public/disks/library.json +26 -0
  27. package/public/docs/llms/llm-assembler.txt +90 -0
  28. package/public/docs/llms/llm-basic-program.txt +256 -0
  29. package/public/docs/llms/llm-disk-drives.txt +72 -0
  30. package/public/docs/llms/llm-file-explorer.txt +50 -0
  31. package/public/docs/llms/llm-hard-drives.txt +80 -0
  32. package/public/docs/llms/llm-main.txt +51 -0
  33. package/public/docs/llms/llm-slot-configuration.txt +66 -0
  34. package/public/icons/icon-192.svg +4 -0
  35. package/public/icons/icon-512.svg +4 -0
  36. package/public/index.html +661 -0
  37. package/public/llms.txt +49 -0
  38. package/public/manifest.json +29 -0
  39. package/public/shaders/burnin.glsl +22 -0
  40. package/public/shaders/crt.glsl +706 -0
  41. package/public/shaders/edge.glsl +109 -0
  42. package/public/shaders/vertex.glsl +8 -0
  43. package/public/sw.js +186 -0
  44. package/roms/341-0027.bin +0 -0
  45. package/roms/341-0160-A-US-UK.bin +0 -0
  46. package/roms/341-0160-A.bin +0 -0
  47. package/roms/342-0273-A-US-UK.bin +0 -0
  48. package/roms/342-0349-B-C0-FF.bin +0 -0
  49. package/roms/Apple Mouse Interface Card ROM - 342-0270-C.bin +0 -0
  50. package/roms/Thunderclock Plus ROM.bin +0 -0
  51. package/scripts/generate_roms.sh +69 -0
  52. package/src/bindings/wasm_interface.cpp +1940 -0
  53. package/src/core/assembler/assembler.cpp +1239 -0
  54. package/src/core/assembler/assembler.hpp +115 -0
  55. package/src/core/audio/audio.cpp +160 -0
  56. package/src/core/audio/audio.hpp +81 -0
  57. package/src/core/basic/basic_detokenizer.cpp +436 -0
  58. package/src/core/basic/basic_detokenizer.hpp +41 -0
  59. package/src/core/basic/basic_tokenizer.cpp +286 -0
  60. package/src/core/basic/basic_tokenizer.hpp +26 -0
  61. package/src/core/basic/basic_tokens.hpp +295 -0
  62. package/src/core/cards/disk2_card.cpp +568 -0
  63. package/src/core/cards/disk2_card.hpp +316 -0
  64. package/src/core/cards/expansion_card.hpp +185 -0
  65. package/src/core/cards/mockingboard/ay8910.cpp +616 -0
  66. package/src/core/cards/mockingboard/ay8910.hpp +159 -0
  67. package/src/core/cards/mockingboard/via6522.cpp +530 -0
  68. package/src/core/cards/mockingboard/via6522.hpp +163 -0
  69. package/src/core/cards/mockingboard_card.cpp +312 -0
  70. package/src/core/cards/mockingboard_card.hpp +159 -0
  71. package/src/core/cards/mouse_card.cpp +654 -0
  72. package/src/core/cards/mouse_card.hpp +190 -0
  73. package/src/core/cards/smartport/block_device.cpp +202 -0
  74. package/src/core/cards/smartport/block_device.hpp +60 -0
  75. package/src/core/cards/smartport/smartport_card.cpp +603 -0
  76. package/src/core/cards/smartport/smartport_card.hpp +120 -0
  77. package/src/core/cards/thunderclock_card.cpp +237 -0
  78. package/src/core/cards/thunderclock_card.hpp +122 -0
  79. package/src/core/cpu/cpu6502.cpp +1609 -0
  80. package/src/core/cpu/cpu6502.hpp +203 -0
  81. package/src/core/debug/condition_evaluator.cpp +470 -0
  82. package/src/core/debug/condition_evaluator.hpp +87 -0
  83. package/src/core/disassembler/disassembler.cpp +552 -0
  84. package/src/core/disassembler/disassembler.hpp +171 -0
  85. package/src/core/disk-image/disk_image.hpp +267 -0
  86. package/src/core/disk-image/dsk_disk_image.cpp +827 -0
  87. package/src/core/disk-image/dsk_disk_image.hpp +204 -0
  88. package/src/core/disk-image/gcr_encoding.cpp +147 -0
  89. package/src/core/disk-image/gcr_encoding.hpp +78 -0
  90. package/src/core/disk-image/woz_disk_image.cpp +1049 -0
  91. package/src/core/disk-image/woz_disk_image.hpp +343 -0
  92. package/src/core/emulator.cpp +2126 -0
  93. package/src/core/emulator.hpp +434 -0
  94. package/src/core/filesystem/dos33.cpp +178 -0
  95. package/src/core/filesystem/dos33.hpp +66 -0
  96. package/src/core/filesystem/pascal.cpp +262 -0
  97. package/src/core/filesystem/pascal.hpp +87 -0
  98. package/src/core/filesystem/prodos.cpp +369 -0
  99. package/src/core/filesystem/prodos.hpp +119 -0
  100. package/src/core/input/keyboard.cpp +227 -0
  101. package/src/core/input/keyboard.hpp +111 -0
  102. package/src/core/mmu/mmu.cpp +1387 -0
  103. package/src/core/mmu/mmu.hpp +236 -0
  104. package/src/core/types.hpp +196 -0
  105. package/src/core/video/video.cpp +680 -0
  106. package/src/core/video/video.hpp +156 -0
  107. package/src/css/assembler-editor.css +1617 -0
  108. package/src/css/base.css +470 -0
  109. package/src/css/basic-debugger.css +791 -0
  110. package/src/css/basic-editor.css +792 -0
  111. package/src/css/controls.css +783 -0
  112. package/src/css/cpu-debugger.css +1413 -0
  113. package/src/css/debug-base.css +160 -0
  114. package/src/css/debug-windows.css +6455 -0
  115. package/src/css/disk-drives.css +406 -0
  116. package/src/css/documentation.css +392 -0
  117. package/src/css/file-explorer.css +867 -0
  118. package/src/css/hard-drive.css +180 -0
  119. package/src/css/layout.css +217 -0
  120. package/src/css/memory-windows.css +798 -0
  121. package/src/css/modals.css +510 -0
  122. package/src/css/monitor.css +425 -0
  123. package/src/css/release-notes.css +101 -0
  124. package/src/css/responsive.css +400 -0
  125. package/src/css/rule-builder.css +340 -0
  126. package/src/css/save-states.css +201 -0
  127. package/src/css/settings-windows.css +1231 -0
  128. package/src/css/window-switcher.css +150 -0
  129. package/src/js/agent/agent-manager.js +643 -0
  130. package/src/js/agent/agent-tools.js +293 -0
  131. package/src/js/agent/agent-version-tools.js +131 -0
  132. package/src/js/agent/assembler-tools.js +357 -0
  133. package/src/js/agent/basic-program-tools.js +894 -0
  134. package/src/js/agent/disk-tools.js +417 -0
  135. package/src/js/agent/file-explorer-tools.js +269 -0
  136. package/src/js/agent/index.js +13 -0
  137. package/src/js/agent/main-tools.js +222 -0
  138. package/src/js/agent/slot-tools.js +303 -0
  139. package/src/js/agent/smartport-tools.js +257 -0
  140. package/src/js/agent/window-tools.js +80 -0
  141. package/src/js/audio/audio-driver.js +417 -0
  142. package/src/js/audio/audio-worklet.js +85 -0
  143. package/src/js/audio/index.js +8 -0
  144. package/src/js/config/default-layout.js +34 -0
  145. package/src/js/config/version.js +8 -0
  146. package/src/js/data/apple2-rom-routines.js +577 -0
  147. package/src/js/debug/assembler-editor-window.js +2993 -0
  148. package/src/js/debug/basic-breakpoint-manager.js +529 -0
  149. package/src/js/debug/basic-program-parser.js +436 -0
  150. package/src/js/debug/basic-program-window.js +2594 -0
  151. package/src/js/debug/basic-variable-inspector.js +447 -0
  152. package/src/js/debug/breakpoint-manager.js +472 -0
  153. package/src/js/debug/cpu-debugger-window.js +2396 -0
  154. package/src/js/debug/index.js +22 -0
  155. package/src/js/debug/label-manager.js +238 -0
  156. package/src/js/debug/memory-browser-window.js +416 -0
  157. package/src/js/debug/memory-heat-map-window.js +481 -0
  158. package/src/js/debug/memory-map-window.js +206 -0
  159. package/src/js/debug/mockingboard-window.js +882 -0
  160. package/src/js/debug/mouse-card-window.js +355 -0
  161. package/src/js/debug/rule-builder-window.js +648 -0
  162. package/src/js/debug/soft-switch-window.js +458 -0
  163. package/src/js/debug/stack-viewer-window.js +221 -0
  164. package/src/js/debug/symbols.js +416 -0
  165. package/src/js/debug/trace-panel.js +291 -0
  166. package/src/js/debug/zero-page-watch-window.js +297 -0
  167. package/src/js/disk-manager/disk-drives-window.js +212 -0
  168. package/src/js/disk-manager/disk-operations.js +284 -0
  169. package/src/js/disk-manager/disk-persistence.js +301 -0
  170. package/src/js/disk-manager/disk-surface-renderer.js +388 -0
  171. package/src/js/disk-manager/drive-sounds.js +139 -0
  172. package/src/js/disk-manager/hard-drive-manager.js +481 -0
  173. package/src/js/disk-manager/hard-drive-persistence.js +187 -0
  174. package/src/js/disk-manager/hard-drive-window.js +57 -0
  175. package/src/js/disk-manager/index.js +890 -0
  176. package/src/js/display/display-settings-window.js +383 -0
  177. package/src/js/display/index.js +10 -0
  178. package/src/js/display/screen-window.js +342 -0
  179. package/src/js/display/webgl-renderer.js +705 -0
  180. package/src/js/file-explorer/disassembler.js +574 -0
  181. package/src/js/file-explorer/dos33.js +266 -0
  182. package/src/js/file-explorer/file-viewer.js +359 -0
  183. package/src/js/file-explorer/index.js +1261 -0
  184. package/src/js/file-explorer/prodos.js +549 -0
  185. package/src/js/file-explorer/utils.js +67 -0
  186. package/src/js/help/documentation-window.js +1096 -0
  187. package/src/js/help/index.js +10 -0
  188. package/src/js/help/release-notes-window.js +85 -0
  189. package/src/js/help/release-notes.js +612 -0
  190. package/src/js/input/gamepad-handler.js +176 -0
  191. package/src/js/input/index.js +12 -0
  192. package/src/js/input/input-handler.js +396 -0
  193. package/src/js/input/joystick-window.js +404 -0
  194. package/src/js/input/mouse-handler.js +99 -0
  195. package/src/js/input/text-selection.js +462 -0
  196. package/src/js/main.js +653 -0
  197. package/src/js/state/index.js +15 -0
  198. package/src/js/state/save-states-window.js +393 -0
  199. package/src/js/state/state-manager.js +409 -0
  200. package/src/js/state/state-persistence.js +218 -0
  201. package/src/js/ui/confirm.js +43 -0
  202. package/src/js/ui/disk-drive-positioner.js +347 -0
  203. package/src/js/ui/reminder-controller.js +129 -0
  204. package/src/js/ui/slot-configuration-window.js +560 -0
  205. package/src/js/ui/theme-manager.js +61 -0
  206. package/src/js/ui/toast.js +44 -0
  207. package/src/js/ui/ui-controller.js +897 -0
  208. package/src/js/ui/window-switcher.js +275 -0
  209. package/src/js/utils/basic-autocomplete.js +832 -0
  210. package/src/js/utils/basic-highlighting.js +473 -0
  211. package/src/js/utils/basic-tokenizer.js +153 -0
  212. package/src/js/utils/basic-tokens.js +117 -0
  213. package/src/js/utils/constants.js +28 -0
  214. package/src/js/utils/indexeddb-helper.js +225 -0
  215. package/src/js/utils/merlin-editor-support.js +905 -0
  216. package/src/js/utils/merlin-highlighting.js +551 -0
  217. package/src/js/utils/storage.js +125 -0
  218. package/src/js/utils/string-utils.js +19 -0
  219. package/src/js/utils/wasm-memory.js +54 -0
  220. package/src/js/windows/base-window.js +690 -0
  221. package/src/js/windows/index.js +9 -0
  222. package/src/js/windows/window-manager.js +375 -0
  223. package/tests/catch2/catch.hpp +17976 -0
  224. package/tests/common/basic_program_builder.cpp +119 -0
  225. package/tests/common/basic_program_builder.hpp +209 -0
  226. package/tests/common/disk_image_builder.cpp +444 -0
  227. package/tests/common/disk_image_builder.hpp +141 -0
  228. package/tests/common/test_helpers.hpp +118 -0
  229. package/tests/gcr/gcr-test.cpp +142 -0
  230. package/tests/integration/check-rom.js +70 -0
  231. package/tests/integration/compare-boot.js +239 -0
  232. package/tests/integration/crash-trace.js +102 -0
  233. package/tests/integration/disk-boot-test.js +264 -0
  234. package/tests/integration/memory-crash.js +108 -0
  235. package/tests/integration/nibble-read-test.js +249 -0
  236. package/tests/integration/phase-test.js +159 -0
  237. package/tests/integration/test_emulator.cpp +291 -0
  238. package/tests/integration/test_emulator_basic.cpp +91 -0
  239. package/tests/integration/test_emulator_debug.cpp +344 -0
  240. package/tests/integration/test_emulator_disk.cpp +153 -0
  241. package/tests/integration/test_emulator_state.cpp +163 -0
  242. package/tests/klaus/6502_functional_test.bin +0 -0
  243. package/tests/klaus/65C02_extended_opcodes_test.bin +0 -0
  244. package/tests/klaus/klaus_6502_test.cpp +184 -0
  245. package/tests/klaus/klaus_65c02_test.cpp +197 -0
  246. package/tests/thunderclock/thunderclock_mmu_test.cpp +304 -0
  247. package/tests/thunderclock/thunderclock_test.cpp +550 -0
  248. package/tests/unit/test_assembler.cpp +521 -0
  249. package/tests/unit/test_audio.cpp +196 -0
  250. package/tests/unit/test_ay8910.cpp +311 -0
  251. package/tests/unit/test_basic_detokenizer.cpp +265 -0
  252. package/tests/unit/test_basic_tokenizer.cpp +382 -0
  253. package/tests/unit/test_block_device.cpp +259 -0
  254. package/tests/unit/test_condition_evaluator.cpp +219 -0
  255. package/tests/unit/test_cpu6502.cpp +1301 -0
  256. package/tests/unit/test_cpu_addressing.cpp +361 -0
  257. package/tests/unit/test_cpu_cycle_counts.cpp +409 -0
  258. package/tests/unit/test_cpu_decimal.cpp +166 -0
  259. package/tests/unit/test_cpu_interrupts.cpp +285 -0
  260. package/tests/unit/test_disassembler.cpp +323 -0
  261. package/tests/unit/test_disk2_card.cpp +330 -0
  262. package/tests/unit/test_dos33.cpp +273 -0
  263. package/tests/unit/test_dsk_disk_image.cpp +315 -0
  264. package/tests/unit/test_expansion_card.cpp +178 -0
  265. package/tests/unit/test_gcr_encoding.cpp +232 -0
  266. package/tests/unit/test_keyboard.cpp +262 -0
  267. package/tests/unit/test_mmu.cpp +555 -0
  268. package/tests/unit/test_mmu_slots.cpp +323 -0
  269. package/tests/unit/test_mockingboard.cpp +352 -0
  270. package/tests/unit/test_mouse_card.cpp +386 -0
  271. package/tests/unit/test_pascal.cpp +248 -0
  272. package/tests/unit/test_prodos.cpp +259 -0
  273. package/tests/unit/test_smartport_card.cpp +321 -0
  274. package/tests/unit/test_thunderclock.cpp +354 -0
  275. package/tests/unit/test_via6522.cpp +323 -0
  276. package/tests/unit/test_video.cpp +319 -0
  277. package/tests/unit/test_woz_disk_image.cpp +257 -0
  278. package/vite.config.js +96 -0
  279. package/wiki/AI-Agent.md +372 -0
  280. package/wiki/Architecture-Overview.md +303 -0
  281. package/wiki/Audio-System.md +449 -0
  282. package/wiki/CPU-Emulation.md +477 -0
  283. package/wiki/Debugger.md +516 -0
  284. package/wiki/Disk-Drives.md +161 -0
  285. package/wiki/Disk-System-Internals.md +547 -0
  286. package/wiki/Display-Settings.md +88 -0
  287. package/wiki/Expansion-Slots.md +187 -0
  288. package/wiki/File-Explorer.md +259 -0
  289. package/wiki/Getting-Started.md +156 -0
  290. package/wiki/Home.md +69 -0
  291. package/wiki/Input-Devices.md +183 -0
  292. package/wiki/Keyboard-Shortcuts.md +158 -0
  293. package/wiki/Memory-System.md +364 -0
  294. package/wiki/Save-States.md +172 -0
  295. package/wiki/Video-Rendering.md +658 -0
@@ -0,0 +1,265 @@
1
+ /*
2
+ * test_basic_detokenizer.cpp - Unit tests for Applesoft and Integer BASIC detokenizer
3
+ */
4
+
5
+ #define CATCH_CONFIG_MAIN
6
+ #include "catch.hpp"
7
+
8
+ #include "basic_detokenizer.hpp"
9
+ #include "basic_program_builder.hpp"
10
+
11
+ #include <cstring>
12
+ #include <string>
13
+ #include <vector>
14
+
15
+ using namespace a2e;
16
+ using test::ApplesoftProgramBuilder;
17
+ using test::IntegerBasicProgramBuilder;
18
+
19
+ // ============================================================================
20
+ // Applesoft: simple PRINT line
21
+ // ============================================================================
22
+
23
+ TEST_CASE("detokenizeApplesoft simple PRINT line", "[basic][applesoft][print]") {
24
+ ApplesoftProgramBuilder builder;
25
+
26
+ // 10 PRINT "HELLO"
27
+ // Token stream: PRINT(0xBA) '"' 'H' 'E' 'L' 'L' 'O' '"'
28
+ builder.addLine(10, std::vector<uint8_t>{0xBA, 0x22, 'H', 'E', 'L', 'L', 'O', 0x22});
29
+
30
+ auto data = builder.build();
31
+ const char* result = BasicDetokenizer::detokenizeApplesoft(data.data(),
32
+ static_cast<int>(data.size()),
33
+ false);
34
+
35
+ std::string output(result);
36
+ // Should contain line number 10
37
+ CHECK(output.find("10") != std::string::npos);
38
+ // Should contain PRINT keyword
39
+ CHECK(output.find("PRINT") != std::string::npos);
40
+ // Should contain the string
41
+ CHECK(output.find("HELLO") != std::string::npos);
42
+ }
43
+
44
+ // ============================================================================
45
+ // Applesoft: GOTO line
46
+ // ============================================================================
47
+
48
+ TEST_CASE("detokenizeApplesoft GOTO line", "[basic][applesoft][goto]") {
49
+ ApplesoftProgramBuilder builder;
50
+
51
+ // 20 GOTO 100
52
+ // Token stream: GOTO(0xAB) '1' '0' '0'
53
+ builder.addLine(20, std::vector<uint8_t>{0xAB, '1', '0', '0'});
54
+
55
+ auto data = builder.build();
56
+ const char* result = BasicDetokenizer::detokenizeApplesoft(data.data(),
57
+ static_cast<int>(data.size()),
58
+ false);
59
+
60
+ std::string output(result);
61
+ CHECK(output.find("20") != std::string::npos);
62
+ CHECK(output.find("GOTO") != std::string::npos);
63
+ CHECK(output.find("100") != std::string::npos);
64
+ }
65
+
66
+ // ============================================================================
67
+ // Applesoft: multiple lines
68
+ // ============================================================================
69
+
70
+ TEST_CASE("detokenizeApplesoft multiple lines", "[basic][applesoft][multi]") {
71
+ ApplesoftProgramBuilder builder;
72
+
73
+ // 10 PRINT "HI"
74
+ builder.addLine(10, std::vector<uint8_t>{0xBA, 0x22, 'H', 'I', 0x22});
75
+
76
+ // 20 GOTO 10
77
+ builder.addLine(20, std::vector<uint8_t>{0xAB, '1', '0'});
78
+
79
+ auto data = builder.build();
80
+ const char* result = BasicDetokenizer::detokenizeApplesoft(data.data(),
81
+ static_cast<int>(data.size()),
82
+ false);
83
+
84
+ std::string output(result);
85
+ // Both line numbers should appear
86
+ CHECK(output.find("10") != std::string::npos);
87
+ CHECK(output.find("20") != std::string::npos);
88
+ CHECK(output.find("PRINT") != std::string::npos);
89
+ CHECK(output.find("GOTO") != std::string::npos);
90
+
91
+ // Should have a newline separating the lines
92
+ CHECK(output.find('\n') != std::string::npos);
93
+ }
94
+
95
+ // ============================================================================
96
+ // Applesoft: with length header (hasLengthHeader=true)
97
+ // ============================================================================
98
+
99
+ TEST_CASE("detokenizeApplesoft with length header", "[basic][applesoft][header]") {
100
+ ApplesoftProgramBuilder builder;
101
+
102
+ // 10 END
103
+ builder.addLine(10, std::vector<uint8_t>{0x80}); // END token
104
+
105
+ auto data = builder.buildWithHeader();
106
+ const char* result = BasicDetokenizer::detokenizeApplesoft(data.data(),
107
+ static_cast<int>(data.size()),
108
+ true);
109
+
110
+ std::string output(result);
111
+ CHECK(output.find("10") != std::string::npos);
112
+ CHECK(output.find("END") != std::string::npos);
113
+ }
114
+
115
+ // ============================================================================
116
+ // Applesoft: verify output contains line numbers
117
+ // ============================================================================
118
+
119
+ TEST_CASE("detokenizeApplesoft output contains padded line numbers", "[basic][applesoft][linenum]") {
120
+ ApplesoftProgramBuilder builder;
121
+ builder.addLine(100, std::vector<uint8_t>{0x80}); // END
122
+
123
+ auto data = builder.build();
124
+ const char* result = BasicDetokenizer::detokenizeApplesoft(data.data(),
125
+ static_cast<int>(data.size()),
126
+ false);
127
+
128
+ std::string output(result);
129
+ // Line number 100 should appear in the output, padded to 5 chars
130
+ CHECK(output.find("100") != std::string::npos);
131
+ }
132
+
133
+ // ============================================================================
134
+ // Applesoft: HOME keyword (0x97)
135
+ // ============================================================================
136
+
137
+ TEST_CASE("detokenizeApplesoft HOME keyword", "[basic][applesoft][home]") {
138
+ ApplesoftProgramBuilder builder;
139
+
140
+ // 5 HOME
141
+ builder.addLine(5, std::vector<uint8_t>{0x97});
142
+
143
+ auto data = builder.build();
144
+ const char* result = BasicDetokenizer::detokenizeApplesoft(data.data(),
145
+ static_cast<int>(data.size()),
146
+ false);
147
+
148
+ std::string output(result);
149
+ CHECK(output.find("HOME") != std::string::npos);
150
+ }
151
+
152
+ // ============================================================================
153
+ // Applesoft: FOR/NEXT
154
+ // ============================================================================
155
+
156
+ TEST_CASE("detokenizeApplesoft FOR/NEXT", "[basic][applesoft][for]") {
157
+ ApplesoftProgramBuilder builder;
158
+
159
+ // 10 FOR I = 1 TO 10
160
+ // FOR(0x81) 'I' '=' '1' TO(0xC1) '1' '0'
161
+ builder.addLine(10, std::vector<uint8_t>{0x81, 'I', 0xD0, '1', 0xC1, '1', '0'});
162
+
163
+ // 20 NEXT I
164
+ // NEXT(0x82) 'I'
165
+ builder.addLine(20, std::vector<uint8_t>{0x82, 'I'});
166
+
167
+ auto data = builder.build();
168
+ const char* result = BasicDetokenizer::detokenizeApplesoft(data.data(),
169
+ static_cast<int>(data.size()),
170
+ false);
171
+
172
+ std::string output(result);
173
+ CHECK(output.find("FOR") != std::string::npos);
174
+ CHECK(output.find("TO") != std::string::npos);
175
+ CHECK(output.find("NEXT") != std::string::npos);
176
+ }
177
+
178
+ // ============================================================================
179
+ // Integer BASIC: simple program
180
+ // ============================================================================
181
+
182
+ TEST_CASE("detokenizeIntegerBasic simple PRINT program", "[basic][integer]") {
183
+ IntegerBasicProgramBuilder builder;
184
+
185
+ // Integer BASIC: 10 PRINT "HI"
186
+ // PRINT token = 0x61 in integer BASIC token table
187
+ // String: 0x28 = start quote, characters, 0x29 = end quote
188
+ builder.addLine(10, std::vector<uint8_t>{0x61, 0x28, 'H', 'I', 0x29});
189
+
190
+ auto data = builder.build();
191
+ const char* result = BasicDetokenizer::detokenizeIntegerBasic(data.data(),
192
+ static_cast<int>(data.size()),
193
+ false);
194
+
195
+ std::string output(result);
196
+ CHECK(output.find("10") != std::string::npos);
197
+ CHECK(output.find("PRINT") != std::string::npos);
198
+ CHECK(output.find("HI") != std::string::npos);
199
+ }
200
+
201
+ TEST_CASE("detokenizeIntegerBasic with numeric constant", "[basic][integer][numeric]") {
202
+ IntegerBasicProgramBuilder builder;
203
+
204
+ // Integer BASIC: 10 PRINT 42
205
+ // PRINT token = 0x61
206
+ // Numeric: 0xB0 prefix + 2-byte LE value (42 = 0x002A)
207
+ builder.addLine(10, std::vector<uint8_t>{0x61, 0xB0, 0x2A, 0x00});
208
+
209
+ auto data = builder.build();
210
+ const char* result = BasicDetokenizer::detokenizeIntegerBasic(data.data(),
211
+ static_cast<int>(data.size()),
212
+ false);
213
+
214
+ std::string output(result);
215
+ CHECK(output.find("10") != std::string::npos);
216
+ CHECK(output.find("42") != std::string::npos);
217
+ }
218
+
219
+ TEST_CASE("detokenizeIntegerBasic with length header", "[basic][integer][header]") {
220
+ IntegerBasicProgramBuilder builder;
221
+
222
+ // 10 END
223
+ builder.addLine(10, std::vector<uint8_t>{0x51}); // END token
224
+
225
+ auto data = builder.buildWithHeader();
226
+ const char* result = BasicDetokenizer::detokenizeIntegerBasic(data.data(),
227
+ static_cast<int>(data.size()),
228
+ true);
229
+
230
+ std::string output(result);
231
+ CHECK(output.find("10") != std::string::npos);
232
+ CHECK(output.find("END") != std::string::npos);
233
+ }
234
+
235
+ // ============================================================================
236
+ // Applesoft: REM preserves text
237
+ // ============================================================================
238
+
239
+ TEST_CASE("detokenizeApplesoft REM preserves text", "[basic][applesoft][rem]") {
240
+ ApplesoftProgramBuilder builder;
241
+
242
+ // 10 REM THIS IS A COMMENT
243
+ builder.addLine(10, std::vector<uint8_t>{0xB2, 'T', 'H', 'I', 'S', ' ', 'I', 'S',
244
+ ' ', 'A', ' ', 'C', 'O', 'M', 'M', 'E', 'N', 'T'});
245
+
246
+ auto data = builder.build();
247
+ const char* result = BasicDetokenizer::detokenizeApplesoft(data.data(),
248
+ static_cast<int>(data.size()),
249
+ false);
250
+
251
+ std::string output(result);
252
+ CHECK(output.find("REM") != std::string::npos);
253
+ CHECK(output.find("THIS IS A COMMENT") != std::string::npos);
254
+ }
255
+
256
+ // ============================================================================
257
+ // Empty program
258
+ // ============================================================================
259
+
260
+ TEST_CASE("detokenizeApplesoft empty program returns empty string", "[basic][applesoft][empty]") {
261
+ // Just the program terminator (0x00, 0x00)
262
+ uint8_t data[] = {0x00, 0x00};
263
+ const char* result = BasicDetokenizer::detokenizeApplesoft(data, sizeof(data), false);
264
+ CHECK(strlen(result) == 0);
265
+ }
@@ -0,0 +1,382 @@
1
+ /*
2
+ * test_basic_tokenizer.cpp - Unit tests for Applesoft BASIC tokenizer
3
+ *
4
+ * Tests the BASIC tokenizer including:
5
+ * - Single-line tokenization
6
+ * - Multi-line programs
7
+ * - Memory layout (next-addr, line-num, tokens, terminator)
8
+ * - Keyword token values (PRINT, GOTO, etc.)
9
+ * - Empty input handling
10
+ * - Round-trip tokenize/detokenize recovery
11
+ */
12
+
13
+ #define CATCH_CONFIG_MAIN
14
+ #include "catch.hpp"
15
+
16
+ #include "basic_tokenizer.hpp"
17
+ #include "basic_detokenizer.hpp"
18
+
19
+ #include <array>
20
+ #include <cstring>
21
+ #include <string>
22
+ #include <functional>
23
+
24
+ using namespace a2e;
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Test fixture: 64KB memory with zero-page pointers set up
28
+ // ---------------------------------------------------------------------------
29
+
30
+ struct BasicMemory {
31
+ std::array<uint8_t, 65536> mem{};
32
+ MemReadFn readMem;
33
+ MemWriteFn writeMem;
34
+
35
+ BasicMemory() {
36
+ mem.fill(0);
37
+ // Set up TXTTAB pointer at $67/$68 = $0801
38
+ mem[0x67] = 0x01;
39
+ mem[0x68] = 0x08;
40
+ // Set up HIMEM/MEMSIZE at $73/$74 = $9600
41
+ mem[0x73] = 0x00;
42
+ mem[0x74] = 0x96;
43
+
44
+ readMem = [this](uint16_t a) -> uint8_t { return mem[a]; };
45
+ writeMem = [this](uint16_t a, uint8_t v) { mem[a] = v; };
46
+ }
47
+
48
+ // Read a 16-bit little-endian value from memory
49
+ uint16_t read16(uint16_t addr) const {
50
+ return mem[addr] | (mem[addr + 1] << 8);
51
+ }
52
+ };
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Simple PRINT tokenization
56
+ // ---------------------------------------------------------------------------
57
+
58
+ TEST_CASE("loadBasicProgram tokenizes simple PRINT line", "[basic][tokenizer]") {
59
+ BasicMemory m;
60
+ int count = loadBasicProgram("10 PRINT \"HELLO\"", m.readMem, m.writeMem);
61
+ REQUIRE(count == 1);
62
+ }
63
+
64
+ TEST_CASE("Tokenized memory has correct structure at $0801", "[basic][tokenizer]") {
65
+ BasicMemory m;
66
+ int count = loadBasicProgram("10 PRINT \"HELLO\"", m.readMem, m.writeMem);
67
+ REQUIRE(count == 1);
68
+
69
+ // At $0801: [next-addr:2][line-num:2][tokens...][0x00]
70
+ uint16_t nextAddr = m.read16(0x0801);
71
+ uint16_t lineNum = m.read16(0x0803);
72
+
73
+ CHECK(lineNum == 10);
74
+ CHECK(nextAddr > 0x0805); // Must point past this line
75
+
76
+ // The first token byte should be the PRINT token (0xBA)
77
+ CHECK(m.mem[0x0805] == 0xBA);
78
+
79
+ // The line should end with 0x00
80
+ uint16_t termPos = nextAddr - 1;
81
+ CHECK(m.mem[termPos] == 0x00);
82
+ }
83
+
84
+ TEST_CASE("Program ends with double zero bytes", "[basic][tokenizer]") {
85
+ BasicMemory m;
86
+ int count = loadBasicProgram("10 PRINT \"HI\"", m.readMem, m.writeMem);
87
+ REQUIRE(count == 1);
88
+
89
+ uint16_t nextAddr = m.read16(0x0801);
90
+ // After the last line, there should be 0x00, 0x00
91
+ CHECK(m.mem[nextAddr] == 0x00);
92
+ CHECK(m.mem[nextAddr + 1] == 0x00);
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Multiple lines
97
+ // ---------------------------------------------------------------------------
98
+
99
+ TEST_CASE("loadBasicProgram returns correct count for multiple lines", "[basic][tokenizer]") {
100
+ BasicMemory m;
101
+ int count = loadBasicProgram(
102
+ "10 PRINT \"A\"\n"
103
+ "20 PRINT \"B\"\n"
104
+ "30 PRINT \"C\"",
105
+ m.readMem, m.writeMem
106
+ );
107
+ REQUIRE(count == 3);
108
+ }
109
+
110
+ TEST_CASE("Multiple lines are chained in memory", "[basic][tokenizer]") {
111
+ BasicMemory m;
112
+ int count = loadBasicProgram(
113
+ "10 PRINT \"A\"\n"
114
+ "20 GOTO 10",
115
+ m.readMem, m.writeMem
116
+ );
117
+ REQUIRE(count == 2);
118
+
119
+ // Line 1 starts at $0801
120
+ uint16_t line1Next = m.read16(0x0801);
121
+ uint16_t line1Num = m.read16(0x0803);
122
+ CHECK(line1Num == 10);
123
+
124
+ // Line 2 starts at line1Next
125
+ uint16_t line2Next = m.read16(line1Next);
126
+ uint16_t line2Num = m.read16(line1Next + 2);
127
+ CHECK(line2Num == 20);
128
+
129
+ // End of program
130
+ CHECK(m.mem[line2Next] == 0x00);
131
+ CHECK(m.mem[line2Next + 1] == 0x00);
132
+ }
133
+
134
+ // ---------------------------------------------------------------------------
135
+ // Empty / null input
136
+ // ---------------------------------------------------------------------------
137
+
138
+ TEST_CASE("loadBasicProgram with empty string returns 0", "[basic][tokenizer]") {
139
+ BasicMemory m;
140
+ int count = loadBasicProgram("", m.readMem, m.writeMem);
141
+ CHECK(count == 0);
142
+ }
143
+
144
+ TEST_CASE("loadBasicProgram with null pointer returns -1", "[basic][tokenizer]") {
145
+ BasicMemory m;
146
+ int count = loadBasicProgram(nullptr, m.readMem, m.writeMem);
147
+ CHECK(count == -1);
148
+ }
149
+
150
+ TEST_CASE("loadBasicProgram with whitespace-only lines returns 0", "[basic][tokenizer]") {
151
+ BasicMemory m;
152
+ int count = loadBasicProgram(" \n \n", m.readMem, m.writeMem);
153
+ CHECK(count == 0);
154
+ }
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // Specific token values
158
+ // ---------------------------------------------------------------------------
159
+
160
+ TEST_CASE("GOTO keyword is tokenized as 0xAB", "[basic][tokenizer]") {
161
+ BasicMemory m;
162
+ int count = loadBasicProgram("10 GOTO 100", m.readMem, m.writeMem);
163
+ REQUIRE(count == 1);
164
+
165
+ // Scan token bytes at $0805+ for the GOTO token (0xAB)
166
+ uint16_t nextAddr = m.read16(0x0801);
167
+ bool foundGoto = false;
168
+ for (uint16_t addr = 0x0805; addr < nextAddr; addr++) {
169
+ if (m.mem[addr] == 0xAB) {
170
+ foundGoto = true;
171
+ break;
172
+ }
173
+ }
174
+ CHECK(foundGoto);
175
+ }
176
+
177
+ TEST_CASE("PRINT keyword is tokenized as 0xBA", "[basic][tokenizer]") {
178
+ BasicMemory m;
179
+ int count = loadBasicProgram("10 PRINT", m.readMem, m.writeMem);
180
+ REQUIRE(count == 1);
181
+
182
+ CHECK(m.mem[0x0805] == 0xBA);
183
+ }
184
+
185
+ TEST_CASE("HOME keyword is tokenized as 0x97", "[basic][tokenizer]") {
186
+ BasicMemory m;
187
+ int count = loadBasicProgram("10 HOME", m.readMem, m.writeMem);
188
+ REQUIRE(count == 1);
189
+
190
+ CHECK(m.mem[0x0805] == 0x97);
191
+ }
192
+
193
+ TEST_CASE("FOR keyword is tokenized as 0x81", "[basic][tokenizer]") {
194
+ BasicMemory m;
195
+ int count = loadBasicProgram("10 FOR I=1 TO 10", m.readMem, m.writeMem);
196
+ REQUIRE(count == 1);
197
+
198
+ CHECK(m.mem[0x0805] == 0x81); // FOR token
199
+ }
200
+
201
+ TEST_CASE("REM keyword is tokenized as 0xB2", "[basic][tokenizer]") {
202
+ BasicMemory m;
203
+ int count = loadBasicProgram("10 REM THIS IS A COMMENT", m.readMem, m.writeMem);
204
+ REQUIRE(count == 1);
205
+
206
+ CHECK(m.mem[0x0805] == 0xB2); // REM token
207
+ }
208
+
209
+ // ---------------------------------------------------------------------------
210
+ // String content in quotes preserved
211
+ // ---------------------------------------------------------------------------
212
+
213
+ TEST_CASE("Quoted string content is preserved verbatim", "[basic][tokenizer]") {
214
+ BasicMemory m;
215
+ int count = loadBasicProgram("10 PRINT \"AB\"", m.readMem, m.writeMem);
216
+ REQUIRE(count == 1);
217
+
218
+ // After PRINT token (0xBA) there should be: " A B "
219
+ // Find the quote in the token stream
220
+ uint16_t nextAddr = m.read16(0x0801);
221
+ bool foundQuotedAB = false;
222
+ for (uint16_t addr = 0x0805; addr + 3 < nextAddr; addr++) {
223
+ if (m.mem[addr] == '"' && m.mem[addr + 1] == 'A' &&
224
+ m.mem[addr + 2] == 'B' && m.mem[addr + 3] == '"') {
225
+ foundQuotedAB = true;
226
+ break;
227
+ }
228
+ }
229
+ CHECK(foundQuotedAB);
230
+ }
231
+
232
+ // ---------------------------------------------------------------------------
233
+ // Zero-page pointers are set correctly
234
+ // ---------------------------------------------------------------------------
235
+
236
+ TEST_CASE("TXTTAB pointer is set to $0801", "[basic][tokenizer][zeropage]") {
237
+ BasicMemory m;
238
+ loadBasicProgram("10 PRINT", m.readMem, m.writeMem);
239
+
240
+ CHECK(m.read16(0x67) == 0x0801);
241
+ }
242
+
243
+ TEST_CASE("VARTAB and ARYTAB point past end of program", "[basic][tokenizer][zeropage]") {
244
+ BasicMemory m;
245
+ int count = loadBasicProgram("10 PRINT \"HI\"", m.readMem, m.writeMem);
246
+ REQUIRE(count == 1);
247
+
248
+ uint16_t vartab = m.read16(0x69); // VARTAB
249
+ uint16_t arytab = m.read16(0x6B); // ARYTAB
250
+ CHECK(vartab > 0x0801);
251
+ CHECK(arytab == vartab);
252
+ }
253
+
254
+ // ---------------------------------------------------------------------------
255
+ // Round-trip: tokenize then detokenize recovers keywords
256
+ // ---------------------------------------------------------------------------
257
+
258
+ TEST_CASE("Round-trip tokenize/detokenize recovers PRINT keyword", "[basic][roundtrip]") {
259
+ BasicMemory m;
260
+ int count = loadBasicProgram("10 PRINT \"HELLO\"", m.readMem, m.writeMem);
261
+ REQUIRE(count == 1);
262
+
263
+ // Build a data buffer from memory starting at $0801
264
+ // Find program end (double zero)
265
+ uint16_t addr = 0x0801;
266
+ while (addr < 0x9600) {
267
+ uint16_t nextAddr = m.read16(addr);
268
+ if (nextAddr == 0x0000) break;
269
+ addr = nextAddr;
270
+ }
271
+ uint16_t progEnd = addr + 2; // Include the final 0x00, 0x00
272
+
273
+ int dataSize = progEnd - 0x0801;
274
+ REQUIRE(dataSize > 0);
275
+ REQUIRE(dataSize < 65536);
276
+
277
+ const char* listing = BasicDetokenizer::detokenizeApplesoft(
278
+ &m.mem[0x0801], dataSize, false
279
+ );
280
+ REQUIRE(listing != nullptr);
281
+
282
+ std::string result(listing);
283
+ // The listing should contain "PRINT" and "HELLO"
284
+ CHECK(result.find("PRINT") != std::string::npos);
285
+ CHECK(result.find("HELLO") != std::string::npos);
286
+ }
287
+
288
+ TEST_CASE("Round-trip tokenize/detokenize recovers GOTO keyword", "[basic][roundtrip]") {
289
+ BasicMemory m;
290
+ int count = loadBasicProgram("10 GOTO 100", m.readMem, m.writeMem);
291
+ REQUIRE(count == 1);
292
+
293
+ uint16_t addr = 0x0801;
294
+ while (addr < 0x9600) {
295
+ uint16_t nextAddr = m.read16(addr);
296
+ if (nextAddr == 0x0000) break;
297
+ addr = nextAddr;
298
+ }
299
+ uint16_t progEnd = addr + 2;
300
+ int dataSize = progEnd - 0x0801;
301
+
302
+ const char* listing = BasicDetokenizer::detokenizeApplesoft(
303
+ &m.mem[0x0801], dataSize, false
304
+ );
305
+ REQUIRE(listing != nullptr);
306
+
307
+ std::string result(listing);
308
+ CHECK(result.find("GOTO") != std::string::npos);
309
+ CHECK(result.find("100") != std::string::npos);
310
+ }
311
+
312
+ TEST_CASE("Round-trip multi-line program", "[basic][roundtrip]") {
313
+ BasicMemory m;
314
+ int count = loadBasicProgram(
315
+ "10 HOME\n"
316
+ "20 PRINT \"HELLO\"\n"
317
+ "30 GOTO 20",
318
+ m.readMem, m.writeMem
319
+ );
320
+ REQUIRE(count == 3);
321
+
322
+ uint16_t addr = 0x0801;
323
+ while (addr < 0x9600) {
324
+ uint16_t nextAddr = m.read16(addr);
325
+ if (nextAddr == 0x0000) break;
326
+ addr = nextAddr;
327
+ }
328
+ uint16_t progEnd = addr + 2;
329
+ int dataSize = progEnd - 0x0801;
330
+
331
+ const char* listing = BasicDetokenizer::detokenizeApplesoft(
332
+ &m.mem[0x0801], dataSize, false
333
+ );
334
+ REQUIRE(listing != nullptr);
335
+
336
+ std::string result(listing);
337
+ CHECK(result.find("HOME") != std::string::npos);
338
+ CHECK(result.find("PRINT") != std::string::npos);
339
+ CHECK(result.find("GOTO") != std::string::npos);
340
+ }
341
+
342
+ // ---------------------------------------------------------------------------
343
+ // Question mark shorthand for PRINT
344
+ // ---------------------------------------------------------------------------
345
+
346
+ TEST_CASE("Question mark is tokenized as PRINT", "[basic][tokenizer]") {
347
+ BasicMemory m;
348
+ int count = loadBasicProgram("10 ?\"HI\"", m.readMem, m.writeMem);
349
+ REQUIRE(count == 1);
350
+
351
+ // Should find PRINT token (0xBA)
352
+ CHECK(m.mem[0x0805] == 0xBA);
353
+ }
354
+
355
+ // ---------------------------------------------------------------------------
356
+ // Lines are sorted by line number
357
+ // ---------------------------------------------------------------------------
358
+
359
+ TEST_CASE("Lines provided out of order are sorted by line number", "[basic][tokenizer]") {
360
+ BasicMemory m;
361
+ int count = loadBasicProgram(
362
+ "30 END\n"
363
+ "10 PRINT\n"
364
+ "20 GOTO 10",
365
+ m.readMem, m.writeMem
366
+ );
367
+ REQUIRE(count == 3);
368
+
369
+ // Line 1 at $0801 should be line 10
370
+ uint16_t line1Num = m.read16(0x0803);
371
+ CHECK(line1Num == 10);
372
+
373
+ // Line 2 should be line 20
374
+ uint16_t line1Next = m.read16(0x0801);
375
+ uint16_t line2Num = m.read16(line1Next + 2);
376
+ CHECK(line2Num == 20);
377
+
378
+ // Line 3 should be line 30
379
+ uint16_t line2Next = m.read16(line1Next);
380
+ uint16_t line3Num = m.read16(line2Next + 2);
381
+ CHECK(line3Num == 30);
382
+ }