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,436 @@
1
+ /*
2
+ * basic-program-parser.js - Parse BASIC program from memory for debugger display
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ /**
9
+ * Applesoft BASIC Program Memory Layout:
10
+ * - TXTTAB ($67-$68): Start of BASIC program text
11
+ * - Program lines: [next-ptr:2][line-num:2][tokenized-text...][00]
12
+ * - End marker: [00][00] (null next pointer)
13
+ *
14
+ * Execution State:
15
+ * - CURLIN ($75-$76): Current line number being executed (CURLIN+1=$FF = direct mode)
16
+ * - TXTPTR ($7A-$7B): Pointer to current position in program text
17
+ */
18
+
19
+ import { APPLESOFT_TOKENS } from "../utils/basic-tokens.js";
20
+ import { peek, readWord } from "../utils/wasm-memory.js";
21
+
22
+ export class BasicProgramParser {
23
+ constructor(wasmModule) {
24
+ this.wasmModule = wasmModule;
25
+ this.lineCache = null;
26
+ this.lastTxttab = 0;
27
+ this.lastVartab = 0;
28
+ }
29
+
30
+ /**
31
+ * Get all program lines
32
+ * @returns {Array<{lineNumber: number, address: number, text: string}>}
33
+ */
34
+ getLines() {
35
+ const txttab = readWord(this.wasmModule,0x67);
36
+ const vartab = readWord(this.wasmModule,0x69);
37
+
38
+ // Check cache validity
39
+ if (
40
+ this.lineCache &&
41
+ txttab === this.lastTxttab &&
42
+ vartab === this.lastVartab
43
+ ) {
44
+ return this.lineCache;
45
+ }
46
+
47
+ const lines = [];
48
+
49
+ if (txttab === 0 || vartab === 0 || txttab >= vartab) {
50
+ this.lineCache = lines;
51
+ this.lastTxttab = txttab;
52
+ this.lastVartab = vartab;
53
+ return lines;
54
+ }
55
+
56
+ let addr = txttab;
57
+ const endAddr = vartab;
58
+ let safetyCount = 0;
59
+ const maxLines = 10000;
60
+
61
+ while (addr < endAddr && safetyCount < maxLines) {
62
+ const nextPtr = readWord(this.wasmModule,addr);
63
+
64
+ // End of program
65
+ if (nextPtr === 0) break;
66
+
67
+ const lineNumber = readWord(this.wasmModule,addr + 2);
68
+ const textStart = addr + 4;
69
+
70
+ // Find end of line (null terminator)
71
+ let textEnd = textStart;
72
+ while (peek(this.wasmModule,textEnd) !== 0 && textEnd < nextPtr) {
73
+ textEnd++;
74
+ }
75
+
76
+ // Read tokenized bytes
77
+ const tokenBytes = new Uint8Array(textEnd - textStart);
78
+ for (let i = 0; i < tokenBytes.length; i++) {
79
+ tokenBytes[i] = peek(this.wasmModule,textStart + i);
80
+ }
81
+
82
+ // Detokenize
83
+ const text = this._detokenize(tokenBytes);
84
+
85
+ lines.push({
86
+ lineNumber,
87
+ address: addr,
88
+ text,
89
+ tokenAddress: textStart,
90
+ });
91
+
92
+ addr = nextPtr;
93
+ safetyCount++;
94
+ }
95
+
96
+ this.lineCache = lines;
97
+ this.lastTxttab = txttab;
98
+ this.lastVartab = vartab;
99
+
100
+ return lines;
101
+ }
102
+
103
+ /**
104
+ * Invalidate the line cache (call when program may have changed)
105
+ */
106
+ invalidateCache() {
107
+ this.lineCache = null;
108
+ }
109
+
110
+ /**
111
+ * Get a map of line numbers to addresses for breakpoint setting
112
+ * @returns {Map<number, number>}
113
+ */
114
+ getLineAddressMap() {
115
+ const lines = this.getLines();
116
+ const map = new Map();
117
+ for (const line of lines) {
118
+ map.set(line.lineNumber, line.address);
119
+ }
120
+ return map;
121
+ }
122
+
123
+ /**
124
+ * Get execution state
125
+ * @returns {{running: boolean, currentLine: number, txtptr: number}}
126
+ */
127
+ getExecutionState() {
128
+ const curlin = readWord(this.wasmModule,0x75);
129
+ const txtptr = readWord(this.wasmModule,0xb8);
130
+
131
+ // Use the C++ emulator's ROM-based tracking: PC at $D912 (RUN) = started,
132
+ // PC at $D43C (RESTART/] prompt) = stopped. This is definitive because it
133
+ // hooks directly into Applesoft's own execution flow.
134
+ const running = this.wasmModule._isBasicProgramRunning
135
+ ? this.wasmModule._isBasicProgramRunning()
136
+ : false;
137
+ // Direct mode check: only CURLIN+1 ($76) matters (ROM checks INX on $76)
138
+ const curlinHi = peek(this.wasmModule, 0x76);
139
+ const directMode = curlinHi === 0xff;
140
+ return {
141
+ running,
142
+ currentLine: !directMode ? curlin : null,
143
+ txtptr,
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Check if BASIC is running (CURLIN+1 != $FF, matching ROM check)
149
+ */
150
+ isRunning() {
151
+ return peek(this.wasmModule, 0x76) !== 0xff;
152
+ }
153
+
154
+ /**
155
+ * Get current line number being executed
156
+ * Returns null if in direct mode or not running
157
+ */
158
+ getCurrentLine() {
159
+ // Direct mode check: only CURLIN+1 ($76) matters (ROM checks INX on $76)
160
+ if (peek(this.wasmModule, 0x76) === 0xff) return null;
161
+ return readWord(this.wasmModule, 0x75);
162
+ }
163
+
164
+ /**
165
+ * Get current text pointer position
166
+ */
167
+ getTxtptr() {
168
+ return readWord(this.wasmModule,0x7a);
169
+ }
170
+
171
+ /**
172
+ * Find line info by line number
173
+ */
174
+ findLine(lineNumber) {
175
+ const lines = this.getLines();
176
+ return lines.find((l) => l.lineNumber === lineNumber) || null;
177
+ }
178
+
179
+ /**
180
+ * Find the line containing the given address
181
+ */
182
+ findLineByAddress(addr) {
183
+ const lines = this.getLines();
184
+ for (let i = 0; i < lines.length; i++) {
185
+ const line = lines[i];
186
+ const nextLine = lines[i + 1];
187
+ const endAddr = nextLine ? nextLine.address : readWord(this.wasmModule,0x69);
188
+
189
+ if (addr >= line.address && addr < endAddr) {
190
+ return line;
191
+ }
192
+ }
193
+ return null;
194
+ }
195
+
196
+ /**
197
+ * Get current statement info for the given line and TXTPTR
198
+ * Returns {statementIndex, statementCount, statementStart, statementEnd}
199
+ * where statementStart/End are character offsets in the detokenized text
200
+ */
201
+ getCurrentStatementInfo(lineNumber, txtptr) {
202
+ const line = this.findLine(lineNumber);
203
+ if (!line) return null;
204
+
205
+ // Find end of this line's tokens
206
+ const nextPtr = readWord(this.wasmModule,line.address);
207
+ const tokenStart = line.tokenAddress;
208
+ const tokenEnd = nextPtr - 1; // -1 for null terminator
209
+
210
+ // If TXTPTR is outside this line, return null
211
+ if (txtptr < tokenStart || txtptr > tokenEnd) {
212
+ return null;
213
+ }
214
+
215
+ // Parse tokenized bytes to find statement boundaries (colons not in quotes/REM/DATA)
216
+ const statementBoundaries = [0]; // Start positions in detokenized text
217
+ let inQuote = false;
218
+ let inRem = false;
219
+ let inData = false;
220
+ let detokenizedPos = 0;
221
+ let currentStatementIndex = 0;
222
+
223
+ for (let addr = tokenStart; addr < tokenEnd; addr++) {
224
+ const byte = peek(this.wasmModule,addr);
225
+
226
+ // Track if we've passed TXTPTR
227
+ if (addr === txtptr) {
228
+ currentStatementIndex = statementBoundaries.length - 1;
229
+ }
230
+
231
+ // Handle quotes
232
+ if (byte === 0x22) { // Quote
233
+ inQuote = !inQuote;
234
+ detokenizedPos++;
235
+ continue;
236
+ }
237
+
238
+ // Inside quote or REM - just count characters
239
+ if (inQuote || inRem) {
240
+ detokenizedPos++;
241
+ continue;
242
+ }
243
+
244
+ // Token range: $80-$EA
245
+ if (byte >= 0x80 && byte <= 0xea) {
246
+ const token = this._getTokenString(byte);
247
+ if (token) {
248
+ detokenizedPos += token.length;
249
+ if (byte === 0xb2) inRem = true; // REM
250
+ if (byte === 0x83) inData = true; // DATA
251
+ continue;
252
+ }
253
+ }
254
+
255
+ // Colon outside quotes/REM marks statement boundary
256
+ if (byte === 0x3a && !inData) { // Colon
257
+ detokenizedPos++;
258
+ statementBoundaries.push(detokenizedPos);
259
+ continue;
260
+ }
261
+
262
+ // Inside DATA - colons end DATA mode
263
+ if (inData && byte === 0x3a) {
264
+ inData = false;
265
+ }
266
+
267
+ detokenizedPos++;
268
+ }
269
+
270
+ // Add end position
271
+ statementBoundaries.push(line.text.length);
272
+
273
+ // Handle case where TXTPTR is at or past the last checked position
274
+ if (txtptr >= tokenEnd) {
275
+ currentStatementIndex = statementBoundaries.length - 2;
276
+ }
277
+
278
+ return {
279
+ statementIndex: currentStatementIndex,
280
+ statementCount: statementBoundaries.length - 1,
281
+ statementStart: statementBoundaries[currentStatementIndex] || 0,
282
+ statementEnd: statementBoundaries[currentStatementIndex + 1] || line.text.length,
283
+ };
284
+ }
285
+
286
+ /**
287
+ * Get the number of statements in a given line (colons + 1, respecting quotes/REM/DATA)
288
+ * @param {number} lineNumber
289
+ * @returns {number} statement count (1 if no colons)
290
+ */
291
+ getStatementCount(lineNumber) {
292
+ const line = this.findLine(lineNumber);
293
+ if (!line) return 1;
294
+
295
+ const nextPtr = readWord(this.wasmModule, line.address);
296
+ const tokenStart = line.tokenAddress;
297
+ const tokenEnd = nextPtr - 1;
298
+
299
+ let colonCount = 0;
300
+ let inQuote = false;
301
+ let inRem = false;
302
+
303
+ for (let addr = tokenStart; addr < tokenEnd; addr++) {
304
+ const byte = peek(this.wasmModule, addr);
305
+ if (byte === 0) break;
306
+ if (inRem) continue;
307
+ if (byte === 0x22) { inQuote = !inQuote; continue; }
308
+ if (inQuote) continue;
309
+ if (byte === 0xB2) { inRem = true; continue; }
310
+ if (byte === 0x3A) colonCount++;
311
+ }
312
+
313
+ return colonCount + 1;
314
+ }
315
+
316
+ /**
317
+ * Get token string for a token byte
318
+ */
319
+ _getTokenString(byte) {
320
+ if (byte >= 0x80 && byte <= 0xea) {
321
+ return APPLESOFT_TOKENS[byte - 0x80] || null;
322
+ }
323
+ return null;
324
+ }
325
+
326
+ /**
327
+ * Detokenize Applesoft BASIC tokens with proper spacing
328
+ */
329
+ _detokenize(bytes) {
330
+ let result = "";
331
+ let inQuote = false;
332
+ let inRem = false;
333
+ let inData = false;
334
+
335
+ // Helper to check if character is alphanumeric
336
+ const isAlphaNum = (c) => /[A-Za-z0-9]/.test(c);
337
+
338
+ for (let i = 0; i < bytes.length; i++) {
339
+ const byte = bytes[i];
340
+
341
+ // Inside quote or REM/DATA - output as character (no spacing changes)
342
+ if (inQuote || inRem) {
343
+ if (byte === 0x22) inQuote = false; // End quote
344
+ result += String.fromCharCode(byte & 0x7f);
345
+ continue;
346
+ }
347
+
348
+ // Check for quote start
349
+ if (byte === 0x22) {
350
+ inQuote = true;
351
+ result += '"';
352
+ continue;
353
+ }
354
+
355
+ // Token range: $80-$EA
356
+ if (byte >= 0x80 && byte <= 0xea) {
357
+ const token = APPLESOFT_TOKENS[byte - 0x80];
358
+ if (token) {
359
+ // Add space before token if last char is alphanumeric
360
+ const lastChar = result.length > 0 ? result[result.length - 1] : "";
361
+ if (isAlphaNum(lastChar)) {
362
+ result += " ";
363
+ }
364
+
365
+ result += token;
366
+
367
+ // Check for REM or DATA
368
+ if (byte === 0xb2) inRem = true; // REM
369
+ if (byte === 0x83) inData = true; // DATA
370
+
371
+ // Add space after token if next byte isn't a space and token ends with a letter
372
+ const nextByte = i + 1 < bytes.length ? bytes[i + 1] : 0;
373
+ if (nextByte !== 0x20 && nextByte !== 0 && isAlphaNum(token[token.length - 1])) {
374
+ result += " ";
375
+ }
376
+ continue;
377
+ }
378
+ }
379
+
380
+ // Inside DATA - colons end DATA mode
381
+ if (inData && byte === 0x3a) {
382
+ inData = false;
383
+ }
384
+
385
+ // Regular character
386
+ const char = String.fromCharCode(byte & 0x7f);
387
+
388
+ // Consume contiguous number (digits + optional decimal point)
389
+ if (byte >= 0x30 && byte <= 0x39) {
390
+ const lastChar = result.length > 0 ? result[result.length - 1] : "";
391
+ if (isAlphaNum(lastChar)) {
392
+ result += " ";
393
+ }
394
+ result += char;
395
+ while (i + 1 < bytes.length) {
396
+ const next = bytes[i + 1];
397
+ if ((next >= 0x30 && next <= 0x39) || next === 0x2e) {
398
+ result += String.fromCharCode(next);
399
+ i++;
400
+ } else {
401
+ break;
402
+ }
403
+ }
404
+ continue;
405
+ }
406
+
407
+ // Consume contiguous variable name (letters + digits + $ + %)
408
+ const isLetter = (byte >= 0x41 && byte <= 0x5a) || (byte >= 0x61 && byte <= 0x7a);
409
+ if (isLetter) {
410
+ const lastChar = result.length > 0 ? result[result.length - 1] : "";
411
+ if (isAlphaNum(lastChar)) {
412
+ result += " ";
413
+ }
414
+ result += char;
415
+ while (i + 1 < bytes.length) {
416
+ const next = bytes[i + 1];
417
+ const nextIsLetter =
418
+ (next >= 0x41 && next <= 0x5a) || (next >= 0x61 && next <= 0x7a);
419
+ const nextIsDigit = next >= 0x30 && next <= 0x39;
420
+ if (nextIsLetter || nextIsDigit || next === 0x24 || next === 0x25) {
421
+ result += String.fromCharCode(next);
422
+ i++;
423
+ } else {
424
+ break;
425
+ }
426
+ }
427
+ continue;
428
+ }
429
+
430
+ result += char;
431
+ }
432
+
433
+ return result;
434
+ }
435
+
436
+ }