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,529 @@
1
+ /*
2
+ * basic-breakpoint-manager.js - BASIC line and statement breakpoint management
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ import { peek, readWord } from "../utils/wasm-memory.js";
9
+
10
+ /**
11
+ * BasicBreakpointManager - Manages BASIC line and statement breakpoints
12
+ * Syncs breakpoints with C++ via WASM interface and persists to localStorage
13
+ *
14
+ * Breakpoints are keyed by "lineNumber:statementIndex" strings.
15
+ * statementIndex -1 means whole-line breakpoint, 0+ means a specific statement.
16
+ */
17
+ export class BasicBreakpointManager {
18
+ constructor(wasmModule) {
19
+ this.wasmModule = wasmModule;
20
+ // Key: "line:stmt" string, Value: { lineNumber, statementIndex, enabled, hitCount }
21
+ this.breakpoints = new Map();
22
+ this.listeners = [];
23
+ this.steppingMode = null; // null | 'line' | 'statement'
24
+ this.lastCurlin = null;
25
+ this.lastTxtptr = null;
26
+
27
+ this.load();
28
+ }
29
+
30
+ static STORAGE_KEY = "a2e-basic-breakpoints";
31
+
32
+ static _key(lineNumber, statementIndex) {
33
+ return `${lineNumber}:${statementIndex}`;
34
+ }
35
+
36
+ /**
37
+ * Add a change listener
38
+ */
39
+ onChange(fn) {
40
+ this.listeners.push(fn);
41
+ }
42
+
43
+ _notify() {
44
+ for (const fn of this.listeners) fn();
45
+ }
46
+
47
+ /**
48
+ * Add a breakpoint on a BASIC line/statement
49
+ * @param {number} lineNumber - use -1 for condition-only rules (no line)
50
+ * @param {number} statementIndex - -1 for whole line, 0+ for specific statement
51
+ */
52
+ add(lineNumber, statementIndex = -1) {
53
+ const key = BasicBreakpointManager._key(lineNumber, statementIndex);
54
+ if (this.breakpoints.has(key)) return;
55
+
56
+ this.breakpoints.set(key, {
57
+ lineNumber,
58
+ statementIndex,
59
+ enabled: true,
60
+ hitCount: 0,
61
+ condition: null,
62
+ conditionRules: null,
63
+ });
64
+
65
+ // Only sync line breakpoints to WASM; condition-only rules use the conditional break flag
66
+ if (lineNumber >= 0) {
67
+ this._syncWasmAdd(lineNumber, statementIndex);
68
+ }
69
+ this._syncConditionRulesToWasm();
70
+ this.save();
71
+ this._notify();
72
+ }
73
+
74
+ /**
75
+ * Add a condition-only rule (not tied to a line).
76
+ * Uses a unique auto-incrementing ID as the statementIndex for keying.
77
+ */
78
+ addConditionRule(condition, conditionRules) {
79
+ // Find a unique statementIndex for condition-only rules
80
+ let id = 0;
81
+ for (const entry of this.breakpoints.values()) {
82
+ if (entry.lineNumber === -1 && entry.statementIndex >= id) {
83
+ id = entry.statementIndex + 1;
84
+ }
85
+ }
86
+ const key = BasicBreakpointManager._key(-1, id);
87
+ this.breakpoints.set(key, {
88
+ lineNumber: -1,
89
+ statementIndex: id,
90
+ enabled: true,
91
+ hitCount: 0,
92
+ condition: condition || null,
93
+ conditionRules: conditionRules || null,
94
+ });
95
+ this._syncConditionRulesToWasm();
96
+ this.save();
97
+ this._notify();
98
+ }
99
+
100
+ /**
101
+ * Remove a breakpoint
102
+ */
103
+ remove(lineNumber, statementIndex = -1) {
104
+ const key = BasicBreakpointManager._key(lineNumber, statementIndex);
105
+ if (!this.breakpoints.has(key)) return;
106
+
107
+ this.breakpoints.delete(key);
108
+ if (lineNumber >= 0) {
109
+ this._syncWasmRemove(lineNumber, statementIndex);
110
+ }
111
+ this._syncConditionRulesToWasm();
112
+ this.save();
113
+ this._notify();
114
+ }
115
+
116
+ /**
117
+ * Remove all breakpoints for a given line (whole-line and all statement BPs)
118
+ */
119
+ removeAllForLine(lineNumber) {
120
+ const toRemove = [];
121
+ for (const [key, entry] of this.breakpoints) {
122
+ if (entry.lineNumber === lineNumber) {
123
+ toRemove.push(entry);
124
+ }
125
+ }
126
+ for (const entry of toRemove) {
127
+ const key = BasicBreakpointManager._key(entry.lineNumber, entry.statementIndex);
128
+ this.breakpoints.delete(key);
129
+ this._syncWasmRemove(entry.lineNumber, entry.statementIndex);
130
+ }
131
+ if (toRemove.length > 0) {
132
+ this.save();
133
+ this._notify();
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Toggle a breakpoint on a line/statement
139
+ */
140
+ toggle(lineNumber, statementIndex = -1) {
141
+ const key = BasicBreakpointManager._key(lineNumber, statementIndex);
142
+ if (this.breakpoints.has(key)) {
143
+ this.remove(lineNumber, statementIndex);
144
+ } else {
145
+ this.add(lineNumber, statementIndex);
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Enable/disable a breakpoint
151
+ */
152
+ setEnabled(lineNumber, statementIndex, enabled) {
153
+ const key = BasicBreakpointManager._key(lineNumber, statementIndex);
154
+ const entry = this.breakpoints.get(key);
155
+ if (!entry) return;
156
+
157
+ entry.enabled = enabled;
158
+ if (lineNumber >= 0) {
159
+ if (enabled) {
160
+ this._syncWasmAdd(lineNumber, statementIndex);
161
+ } else {
162
+ this._syncWasmRemove(lineNumber, statementIndex);
163
+ }
164
+ }
165
+ this._syncConditionRulesToWasm();
166
+ this.save();
167
+ this._notify();
168
+ }
169
+
170
+ /**
171
+ * Check if a specific breakpoint exists
172
+ */
173
+ has(lineNumber, statementIndex = -1) {
174
+ return this.breakpoints.has(BasicBreakpointManager._key(lineNumber, statementIndex));
175
+ }
176
+
177
+ /**
178
+ * Check if any breakpoint exists for a given line (whole-line or any statement)
179
+ */
180
+ hasAnyForLine(lineNumber) {
181
+ for (const entry of this.breakpoints.values()) {
182
+ if (entry.lineNumber === lineNumber) return true;
183
+ }
184
+ return false;
185
+ }
186
+
187
+ /**
188
+ * Get all breakpoints for a specific line
189
+ * @returns {Array<{statementIndex, enabled}>}
190
+ */
191
+ getForLine(lineNumber) {
192
+ const result = [];
193
+ for (const entry of this.breakpoints.values()) {
194
+ if (entry.lineNumber === lineNumber) {
195
+ result.push({ statementIndex: entry.statementIndex, enabled: entry.enabled });
196
+ }
197
+ }
198
+ return result;
199
+ }
200
+
201
+ /**
202
+ * Get breakpoint entry by key
203
+ */
204
+ get(lineNumber, statementIndex = -1) {
205
+ return this.breakpoints.get(BasicBreakpointManager._key(lineNumber, statementIndex)) || null;
206
+ }
207
+
208
+ /**
209
+ * Get all breakpoints
210
+ */
211
+ getAll() {
212
+ return this.breakpoints;
213
+ }
214
+
215
+ /**
216
+ * Get sorted list of unique breakpoint line numbers
217
+ */
218
+ getLineNumbers() {
219
+ const lineSet = new Set();
220
+ for (const entry of this.breakpoints.values()) {
221
+ lineSet.add(entry.lineNumber);
222
+ }
223
+ return [...lineSet].sort((a, b) => a - b);
224
+ }
225
+
226
+ /**
227
+ * Set condition on a breakpoint
228
+ */
229
+ setCondition(lineNumber, statementIndex, condition) {
230
+ const key = BasicBreakpointManager._key(lineNumber, statementIndex);
231
+ const entry = this.breakpoints.get(key);
232
+ if (!entry) return;
233
+ entry.condition = condition || null;
234
+ // Re-sync condition-only rules to C++ when their expression changes
235
+ if (lineNumber === -1) {
236
+ this._syncConditionRulesToWasm();
237
+ }
238
+ this.save();
239
+ this._notify();
240
+ }
241
+
242
+ /**
243
+ * Set the structured rule tree on a breakpoint (for Rule Builder persistence)
244
+ */
245
+ setConditionRules(lineNumber, statementIndex, rules) {
246
+ const key = BasicBreakpointManager._key(lineNumber, statementIndex);
247
+ const entry = this.breakpoints.get(key);
248
+ if (!entry) return;
249
+ entry.conditionRules = rules || null;
250
+ this.save();
251
+ this._notify();
252
+ }
253
+
254
+ /**
255
+ * Evaluate whether a breakpoint should actually fire.
256
+ * Returns true if we should stay paused, false if we should resume.
257
+ */
258
+ shouldBreak(lineNumber, statementIndex) {
259
+ const key = BasicBreakpointManager._key(lineNumber, statementIndex);
260
+ const entry = this.breakpoints.get(key);
261
+ if (!entry || !entry.enabled) return false;
262
+
263
+ entry.hitCount++;
264
+
265
+ if (entry.condition) {
266
+ try {
267
+ const result = this.evaluateCondition(entry.condition);
268
+ if (!result) return false;
269
+ } catch (e) {
270
+ console.warn("BASIC breakpoint condition error:", e.message);
271
+ }
272
+ }
273
+
274
+ return true;
275
+ }
276
+
277
+ /**
278
+ * Check if any enabled condition-only rules exist
279
+ */
280
+ hasConditionOnlyRules() {
281
+ for (const entry of this.breakpoints.values()) {
282
+ if (entry.lineNumber === -1 && entry.enabled && entry.condition) return true;
283
+ }
284
+ return false;
285
+ }
286
+
287
+ /**
288
+ * Sync all condition-only rules to C++ for native evaluation at $D820
289
+ */
290
+ _syncConditionRulesToWasm() {
291
+ try {
292
+ this.wasmModule._clearBasicConditionRules();
293
+ for (const entry of this.breakpoints.values()) {
294
+ if (entry.lineNumber !== -1 || !entry.enabled || !entry.condition) continue;
295
+ const exprPtr = this.wasmModule._malloc(entry.condition.length + 1);
296
+ this.wasmModule.stringToUTF8(entry.condition, exprPtr, entry.condition.length + 1);
297
+ this.wasmModule._addBasicConditionRule(entry.statementIndex, exprPtr);
298
+ this.wasmModule._free(exprPtr);
299
+ }
300
+ } catch (e) { /* ignore */ }
301
+ }
302
+
303
+ /**
304
+ * Evaluate a condition expression via C++ evaluator.
305
+ */
306
+ evaluateCondition(expr) {
307
+ const exprPtr = this.wasmModule._malloc(expr.length + 1);
308
+ this.wasmModule.stringToUTF8(expr, exprPtr, expr.length + 1);
309
+ const result = this.wasmModule._evaluateCondition(exprPtr);
310
+ this.wasmModule._free(exprPtr);
311
+ return result;
312
+ }
313
+
314
+ /**
315
+ * Get all breakpoint entries sorted by line then statement
316
+ */
317
+ getAllEntries() {
318
+ const entries = [...this.breakpoints.values()];
319
+ entries.sort((a, b) => {
320
+ if (a.lineNumber !== b.lineNumber) return a.lineNumber - b.lineNumber;
321
+ return a.statementIndex - b.statementIndex;
322
+ });
323
+ return entries;
324
+ }
325
+
326
+ /**
327
+ * Clear all breakpoints
328
+ */
329
+ clear() {
330
+ this.breakpoints.clear();
331
+ try {
332
+ this.wasmModule._clearBasicBreakpoints();
333
+ } catch (e) {
334
+ /* ignore */
335
+ }
336
+ this._syncConditionRulesToWasm();
337
+ this.save();
338
+ this._notify();
339
+ }
340
+
341
+ /**
342
+ * Start line stepping mode
343
+ */
344
+ startLineStep() {
345
+ this.steppingMode = "line";
346
+ this.lastCurlin = this._getCurlin();
347
+ }
348
+
349
+ /**
350
+ * Start statement stepping mode
351
+ */
352
+ startStatementStep() {
353
+ this.steppingMode = "statement";
354
+ this.lastTxtptr = this._getTxtptr();
355
+ }
356
+
357
+ /**
358
+ * Stop stepping
359
+ */
360
+ stopStepping() {
361
+ this.steppingMode = null;
362
+ this.lastCurlin = null;
363
+ this.lastTxtptr = null;
364
+ }
365
+
366
+ /**
367
+ * Check if we should break due to stepping
368
+ */
369
+ checkStepBreak() {
370
+ if (!this.steppingMode) return false;
371
+
372
+ const curlin = this._getCurlin();
373
+ const isRunning = !this._isDirectMode();
374
+
375
+ if (this.steppingMode === "line") {
376
+ if (curlin !== this.lastCurlin && isRunning) {
377
+ this.lastCurlin = curlin;
378
+ this.steppingMode = null;
379
+ return true;
380
+ }
381
+ if (!isRunning && this.lastCurlin !== null) {
382
+ this.steppingMode = null;
383
+ return true;
384
+ }
385
+ } else if (this.steppingMode === "statement") {
386
+ const txtptr = this._getTxtptr();
387
+ if (txtptr !== this.lastTxtptr) {
388
+ this.lastTxtptr = txtptr;
389
+ this.steppingMode = null;
390
+ return true;
391
+ }
392
+ if (!isRunning) {
393
+ this.steppingMode = null;
394
+ return true;
395
+ }
396
+ }
397
+
398
+ return false;
399
+ }
400
+
401
+ /**
402
+ * Check if a BASIC breakpoint was hit
403
+ */
404
+ isBreakpointHit() {
405
+ try {
406
+ return this.wasmModule._isBasicBreakpointHit();
407
+ } catch (e) {
408
+ return false;
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Get the line number where breakpoint was hit
414
+ */
415
+ getBreakLine() {
416
+ try {
417
+ return this.wasmModule._getBasicBreakLine();
418
+ } catch (e) {
419
+ return 0;
420
+ }
421
+ }
422
+
423
+ _getCurlin() {
424
+ return readWord(this.wasmModule, 0x75);
425
+ }
426
+
427
+ _isDirectMode() {
428
+ return peek(this.wasmModule, 0x76) === 0xff;
429
+ }
430
+
431
+ _getTxtptr() {
432
+ return readWord(this.wasmModule, 0x7a);
433
+ }
434
+
435
+ // ---- WASM sync helpers ----
436
+
437
+ _syncWasmAdd(lineNumber, statementIndex) {
438
+ try {
439
+ this.wasmModule._addBasicBreakpoint(lineNumber, statementIndex);
440
+ } catch (e) {
441
+ /* ignore */
442
+ }
443
+ }
444
+
445
+ _syncWasmRemove(lineNumber, statementIndex) {
446
+ try {
447
+ this.wasmModule._removeBasicBreakpoint(lineNumber, statementIndex);
448
+ } catch (e) {
449
+ /* ignore */
450
+ }
451
+ }
452
+
453
+ /**
454
+ * Re-sync all breakpoints to WASM after state import
455
+ */
456
+ resyncToWasm() {
457
+ this.clearFromWasm();
458
+ this.syncToWasm();
459
+ this._syncConditionRulesToWasm();
460
+ }
461
+
462
+ clearFromWasm() {
463
+ try {
464
+ this.wasmModule._clearBasicBreakpoints();
465
+ } catch (e) {
466
+ /* ignore */
467
+ }
468
+ }
469
+
470
+ syncToWasm() {
471
+ for (const entry of this.breakpoints.values()) {
472
+ if (entry.enabled) {
473
+ this._syncWasmAdd(entry.lineNumber, entry.statementIndex);
474
+ }
475
+ }
476
+ }
477
+
478
+ // ---- Persistence ----
479
+
480
+ save() {
481
+ try {
482
+ const data = [];
483
+ for (const entry of this.breakpoints.values()) {
484
+ data.push({
485
+ lineNumber: entry.lineNumber,
486
+ statementIndex: entry.statementIndex,
487
+ enabled: entry.enabled,
488
+ condition: entry.condition,
489
+ conditionRules: entry.conditionRules,
490
+ });
491
+ }
492
+ localStorage.setItem(
493
+ BasicBreakpointManager.STORAGE_KEY,
494
+ JSON.stringify(data),
495
+ );
496
+ } catch (e) {
497
+ console.warn("Failed to save BASIC breakpoints:", e);
498
+ }
499
+ }
500
+
501
+ load() {
502
+ try {
503
+ const saved = localStorage.getItem(BasicBreakpointManager.STORAGE_KEY);
504
+ if (saved) {
505
+ const data = JSON.parse(saved);
506
+ for (const entry of data) {
507
+ // Backward compat: missing statementIndex defaults to -1 (whole line)
508
+ const stmtIdx = entry.statementIndex !== undefined ? entry.statementIndex : -1;
509
+ const key = BasicBreakpointManager._key(entry.lineNumber, stmtIdx);
510
+ this.breakpoints.set(key, {
511
+ lineNumber: entry.lineNumber,
512
+ statementIndex: stmtIdx,
513
+ enabled: entry.enabled,
514
+ hitCount: 0,
515
+ condition: entry.condition || null,
516
+ conditionRules: entry.conditionRules || null,
517
+ });
518
+ if (entry.enabled && entry.lineNumber >= 0) {
519
+ this._syncWasmAdd(entry.lineNumber, stmtIdx);
520
+ }
521
+ }
522
+ // Sync condition-only rules to C++ after loading all entries
523
+ this._syncConditionRulesToWasm();
524
+ }
525
+ } catch (e) {
526
+ console.warn("Failed to load BASIC breakpoints:", e);
527
+ }
528
+ }
529
+ }