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.
- package/.clangd +5 -0
- package/.mcp.json +12 -0
- package/CLAUDE.md +362 -0
- package/CMakeLists.txt +774 -0
- package/LICENSE +21 -0
- package/README.md +392 -0
- package/build-wasm/generated/roms.cpp +2447 -0
- package/docker-compose.staging.yml +9 -0
- package/docs/basic-rom-disassembly.md +6663 -0
- package/docs/softswitch-comparison.md +273 -0
- package/docs/thunderclock-debug.md +89 -0
- package/examples/cube.bas +72 -0
- package/examples/hello.s +55 -0
- package/examples/scroll.s +140 -0
- package/package.json +18 -0
- package/public/assets/apple-logo-old.png +0 -0
- package/public/assets/apple-logo.png +0 -0
- package/public/assets/drive-closed-light-on.png +0 -0
- package/public/assets/drive-closed.png +0 -0
- package/public/assets/drive-open-light-on.png +0 -0
- package/public/assets/drive-open.png +0 -0
- package/public/audio-worklet.js +82 -0
- package/public/disks/Apple DOS 3.3 January 1983.dsk +0 -0
- package/public/disks/ProDOS 2.4.3.po +0 -0
- package/public/disks/h32mb.2mg +0 -0
- package/public/disks/library.json +26 -0
- package/public/docs/llms/llm-assembler.txt +90 -0
- package/public/docs/llms/llm-basic-program.txt +256 -0
- package/public/docs/llms/llm-disk-drives.txt +72 -0
- package/public/docs/llms/llm-file-explorer.txt +50 -0
- package/public/docs/llms/llm-hard-drives.txt +80 -0
- package/public/docs/llms/llm-main.txt +51 -0
- package/public/docs/llms/llm-slot-configuration.txt +66 -0
- package/public/icons/icon-192.svg +4 -0
- package/public/icons/icon-512.svg +4 -0
- package/public/index.html +661 -0
- package/public/llms.txt +49 -0
- package/public/manifest.json +29 -0
- package/public/shaders/burnin.glsl +22 -0
- package/public/shaders/crt.glsl +706 -0
- package/public/shaders/edge.glsl +109 -0
- package/public/shaders/vertex.glsl +8 -0
- package/public/sw.js +186 -0
- package/roms/341-0027.bin +0 -0
- package/roms/341-0160-A-US-UK.bin +0 -0
- package/roms/341-0160-A.bin +0 -0
- package/roms/342-0273-A-US-UK.bin +0 -0
- package/roms/342-0349-B-C0-FF.bin +0 -0
- package/roms/Apple Mouse Interface Card ROM - 342-0270-C.bin +0 -0
- package/roms/Thunderclock Plus ROM.bin +0 -0
- package/scripts/generate_roms.sh +69 -0
- package/src/bindings/wasm_interface.cpp +1940 -0
- package/src/core/assembler/assembler.cpp +1239 -0
- package/src/core/assembler/assembler.hpp +115 -0
- package/src/core/audio/audio.cpp +160 -0
- package/src/core/audio/audio.hpp +81 -0
- package/src/core/basic/basic_detokenizer.cpp +436 -0
- package/src/core/basic/basic_detokenizer.hpp +41 -0
- package/src/core/basic/basic_tokenizer.cpp +286 -0
- package/src/core/basic/basic_tokenizer.hpp +26 -0
- package/src/core/basic/basic_tokens.hpp +295 -0
- package/src/core/cards/disk2_card.cpp +568 -0
- package/src/core/cards/disk2_card.hpp +316 -0
- package/src/core/cards/expansion_card.hpp +185 -0
- package/src/core/cards/mockingboard/ay8910.cpp +616 -0
- package/src/core/cards/mockingboard/ay8910.hpp +159 -0
- package/src/core/cards/mockingboard/via6522.cpp +530 -0
- package/src/core/cards/mockingboard/via6522.hpp +163 -0
- package/src/core/cards/mockingboard_card.cpp +312 -0
- package/src/core/cards/mockingboard_card.hpp +159 -0
- package/src/core/cards/mouse_card.cpp +654 -0
- package/src/core/cards/mouse_card.hpp +190 -0
- package/src/core/cards/smartport/block_device.cpp +202 -0
- package/src/core/cards/smartport/block_device.hpp +60 -0
- package/src/core/cards/smartport/smartport_card.cpp +603 -0
- package/src/core/cards/smartport/smartport_card.hpp +120 -0
- package/src/core/cards/thunderclock_card.cpp +237 -0
- package/src/core/cards/thunderclock_card.hpp +122 -0
- package/src/core/cpu/cpu6502.cpp +1609 -0
- package/src/core/cpu/cpu6502.hpp +203 -0
- package/src/core/debug/condition_evaluator.cpp +470 -0
- package/src/core/debug/condition_evaluator.hpp +87 -0
- package/src/core/disassembler/disassembler.cpp +552 -0
- package/src/core/disassembler/disassembler.hpp +171 -0
- package/src/core/disk-image/disk_image.hpp +267 -0
- package/src/core/disk-image/dsk_disk_image.cpp +827 -0
- package/src/core/disk-image/dsk_disk_image.hpp +204 -0
- package/src/core/disk-image/gcr_encoding.cpp +147 -0
- package/src/core/disk-image/gcr_encoding.hpp +78 -0
- package/src/core/disk-image/woz_disk_image.cpp +1049 -0
- package/src/core/disk-image/woz_disk_image.hpp +343 -0
- package/src/core/emulator.cpp +2126 -0
- package/src/core/emulator.hpp +434 -0
- package/src/core/filesystem/dos33.cpp +178 -0
- package/src/core/filesystem/dos33.hpp +66 -0
- package/src/core/filesystem/pascal.cpp +262 -0
- package/src/core/filesystem/pascal.hpp +87 -0
- package/src/core/filesystem/prodos.cpp +369 -0
- package/src/core/filesystem/prodos.hpp +119 -0
- package/src/core/input/keyboard.cpp +227 -0
- package/src/core/input/keyboard.hpp +111 -0
- package/src/core/mmu/mmu.cpp +1387 -0
- package/src/core/mmu/mmu.hpp +236 -0
- package/src/core/types.hpp +196 -0
- package/src/core/video/video.cpp +680 -0
- package/src/core/video/video.hpp +156 -0
- package/src/css/assembler-editor.css +1617 -0
- package/src/css/base.css +470 -0
- package/src/css/basic-debugger.css +791 -0
- package/src/css/basic-editor.css +792 -0
- package/src/css/controls.css +783 -0
- package/src/css/cpu-debugger.css +1413 -0
- package/src/css/debug-base.css +160 -0
- package/src/css/debug-windows.css +6455 -0
- package/src/css/disk-drives.css +406 -0
- package/src/css/documentation.css +392 -0
- package/src/css/file-explorer.css +867 -0
- package/src/css/hard-drive.css +180 -0
- package/src/css/layout.css +217 -0
- package/src/css/memory-windows.css +798 -0
- package/src/css/modals.css +510 -0
- package/src/css/monitor.css +425 -0
- package/src/css/release-notes.css +101 -0
- package/src/css/responsive.css +400 -0
- package/src/css/rule-builder.css +340 -0
- package/src/css/save-states.css +201 -0
- package/src/css/settings-windows.css +1231 -0
- package/src/css/window-switcher.css +150 -0
- package/src/js/agent/agent-manager.js +643 -0
- package/src/js/agent/agent-tools.js +293 -0
- package/src/js/agent/agent-version-tools.js +131 -0
- package/src/js/agent/assembler-tools.js +357 -0
- package/src/js/agent/basic-program-tools.js +894 -0
- package/src/js/agent/disk-tools.js +417 -0
- package/src/js/agent/file-explorer-tools.js +269 -0
- package/src/js/agent/index.js +13 -0
- package/src/js/agent/main-tools.js +222 -0
- package/src/js/agent/slot-tools.js +303 -0
- package/src/js/agent/smartport-tools.js +257 -0
- package/src/js/agent/window-tools.js +80 -0
- package/src/js/audio/audio-driver.js +417 -0
- package/src/js/audio/audio-worklet.js +85 -0
- package/src/js/audio/index.js +8 -0
- package/src/js/config/default-layout.js +34 -0
- package/src/js/config/version.js +8 -0
- package/src/js/data/apple2-rom-routines.js +577 -0
- package/src/js/debug/assembler-editor-window.js +2993 -0
- package/src/js/debug/basic-breakpoint-manager.js +529 -0
- package/src/js/debug/basic-program-parser.js +436 -0
- package/src/js/debug/basic-program-window.js +2594 -0
- package/src/js/debug/basic-variable-inspector.js +447 -0
- package/src/js/debug/breakpoint-manager.js +472 -0
- package/src/js/debug/cpu-debugger-window.js +2396 -0
- package/src/js/debug/index.js +22 -0
- package/src/js/debug/label-manager.js +238 -0
- package/src/js/debug/memory-browser-window.js +416 -0
- package/src/js/debug/memory-heat-map-window.js +481 -0
- package/src/js/debug/memory-map-window.js +206 -0
- package/src/js/debug/mockingboard-window.js +882 -0
- package/src/js/debug/mouse-card-window.js +355 -0
- package/src/js/debug/rule-builder-window.js +648 -0
- package/src/js/debug/soft-switch-window.js +458 -0
- package/src/js/debug/stack-viewer-window.js +221 -0
- package/src/js/debug/symbols.js +416 -0
- package/src/js/debug/trace-panel.js +291 -0
- package/src/js/debug/zero-page-watch-window.js +297 -0
- package/src/js/disk-manager/disk-drives-window.js +212 -0
- package/src/js/disk-manager/disk-operations.js +284 -0
- package/src/js/disk-manager/disk-persistence.js +301 -0
- package/src/js/disk-manager/disk-surface-renderer.js +388 -0
- package/src/js/disk-manager/drive-sounds.js +139 -0
- package/src/js/disk-manager/hard-drive-manager.js +481 -0
- package/src/js/disk-manager/hard-drive-persistence.js +187 -0
- package/src/js/disk-manager/hard-drive-window.js +57 -0
- package/src/js/disk-manager/index.js +890 -0
- package/src/js/display/display-settings-window.js +383 -0
- package/src/js/display/index.js +10 -0
- package/src/js/display/screen-window.js +342 -0
- package/src/js/display/webgl-renderer.js +705 -0
- package/src/js/file-explorer/disassembler.js +574 -0
- package/src/js/file-explorer/dos33.js +266 -0
- package/src/js/file-explorer/file-viewer.js +359 -0
- package/src/js/file-explorer/index.js +1261 -0
- package/src/js/file-explorer/prodos.js +549 -0
- package/src/js/file-explorer/utils.js +67 -0
- package/src/js/help/documentation-window.js +1096 -0
- package/src/js/help/index.js +10 -0
- package/src/js/help/release-notes-window.js +85 -0
- package/src/js/help/release-notes.js +612 -0
- package/src/js/input/gamepad-handler.js +176 -0
- package/src/js/input/index.js +12 -0
- package/src/js/input/input-handler.js +396 -0
- package/src/js/input/joystick-window.js +404 -0
- package/src/js/input/mouse-handler.js +99 -0
- package/src/js/input/text-selection.js +462 -0
- package/src/js/main.js +653 -0
- package/src/js/state/index.js +15 -0
- package/src/js/state/save-states-window.js +393 -0
- package/src/js/state/state-manager.js +409 -0
- package/src/js/state/state-persistence.js +218 -0
- package/src/js/ui/confirm.js +43 -0
- package/src/js/ui/disk-drive-positioner.js +347 -0
- package/src/js/ui/reminder-controller.js +129 -0
- package/src/js/ui/slot-configuration-window.js +560 -0
- package/src/js/ui/theme-manager.js +61 -0
- package/src/js/ui/toast.js +44 -0
- package/src/js/ui/ui-controller.js +897 -0
- package/src/js/ui/window-switcher.js +275 -0
- package/src/js/utils/basic-autocomplete.js +832 -0
- package/src/js/utils/basic-highlighting.js +473 -0
- package/src/js/utils/basic-tokenizer.js +153 -0
- package/src/js/utils/basic-tokens.js +117 -0
- package/src/js/utils/constants.js +28 -0
- package/src/js/utils/indexeddb-helper.js +225 -0
- package/src/js/utils/merlin-editor-support.js +905 -0
- package/src/js/utils/merlin-highlighting.js +551 -0
- package/src/js/utils/storage.js +125 -0
- package/src/js/utils/string-utils.js +19 -0
- package/src/js/utils/wasm-memory.js +54 -0
- package/src/js/windows/base-window.js +690 -0
- package/src/js/windows/index.js +9 -0
- package/src/js/windows/window-manager.js +375 -0
- package/tests/catch2/catch.hpp +17976 -0
- package/tests/common/basic_program_builder.cpp +119 -0
- package/tests/common/basic_program_builder.hpp +209 -0
- package/tests/common/disk_image_builder.cpp +444 -0
- package/tests/common/disk_image_builder.hpp +141 -0
- package/tests/common/test_helpers.hpp +118 -0
- package/tests/gcr/gcr-test.cpp +142 -0
- package/tests/integration/check-rom.js +70 -0
- package/tests/integration/compare-boot.js +239 -0
- package/tests/integration/crash-trace.js +102 -0
- package/tests/integration/disk-boot-test.js +264 -0
- package/tests/integration/memory-crash.js +108 -0
- package/tests/integration/nibble-read-test.js +249 -0
- package/tests/integration/phase-test.js +159 -0
- package/tests/integration/test_emulator.cpp +291 -0
- package/tests/integration/test_emulator_basic.cpp +91 -0
- package/tests/integration/test_emulator_debug.cpp +344 -0
- package/tests/integration/test_emulator_disk.cpp +153 -0
- package/tests/integration/test_emulator_state.cpp +163 -0
- package/tests/klaus/6502_functional_test.bin +0 -0
- package/tests/klaus/65C02_extended_opcodes_test.bin +0 -0
- package/tests/klaus/klaus_6502_test.cpp +184 -0
- package/tests/klaus/klaus_65c02_test.cpp +197 -0
- package/tests/thunderclock/thunderclock_mmu_test.cpp +304 -0
- package/tests/thunderclock/thunderclock_test.cpp +550 -0
- package/tests/unit/test_assembler.cpp +521 -0
- package/tests/unit/test_audio.cpp +196 -0
- package/tests/unit/test_ay8910.cpp +311 -0
- package/tests/unit/test_basic_detokenizer.cpp +265 -0
- package/tests/unit/test_basic_tokenizer.cpp +382 -0
- package/tests/unit/test_block_device.cpp +259 -0
- package/tests/unit/test_condition_evaluator.cpp +219 -0
- package/tests/unit/test_cpu6502.cpp +1301 -0
- package/tests/unit/test_cpu_addressing.cpp +361 -0
- package/tests/unit/test_cpu_cycle_counts.cpp +409 -0
- package/tests/unit/test_cpu_decimal.cpp +166 -0
- package/tests/unit/test_cpu_interrupts.cpp +285 -0
- package/tests/unit/test_disassembler.cpp +323 -0
- package/tests/unit/test_disk2_card.cpp +330 -0
- package/tests/unit/test_dos33.cpp +273 -0
- package/tests/unit/test_dsk_disk_image.cpp +315 -0
- package/tests/unit/test_expansion_card.cpp +178 -0
- package/tests/unit/test_gcr_encoding.cpp +232 -0
- package/tests/unit/test_keyboard.cpp +262 -0
- package/tests/unit/test_mmu.cpp +555 -0
- package/tests/unit/test_mmu_slots.cpp +323 -0
- package/tests/unit/test_mockingboard.cpp +352 -0
- package/tests/unit/test_mouse_card.cpp +386 -0
- package/tests/unit/test_pascal.cpp +248 -0
- package/tests/unit/test_prodos.cpp +259 -0
- package/tests/unit/test_smartport_card.cpp +321 -0
- package/tests/unit/test_thunderclock.cpp +354 -0
- package/tests/unit/test_via6522.cpp +323 -0
- package/tests/unit/test_video.cpp +319 -0
- package/tests/unit/test_woz_disk_image.cpp +257 -0
- package/vite.config.js +96 -0
- package/wiki/AI-Agent.md +372 -0
- package/wiki/Architecture-Overview.md +303 -0
- package/wiki/Audio-System.md +449 -0
- package/wiki/CPU-Emulation.md +477 -0
- package/wiki/Debugger.md +516 -0
- package/wiki/Disk-Drives.md +161 -0
- package/wiki/Disk-System-Internals.md +547 -0
- package/wiki/Display-Settings.md +88 -0
- package/wiki/Expansion-Slots.md +187 -0
- package/wiki/File-Explorer.md +259 -0
- package/wiki/Getting-Started.md +156 -0
- package/wiki/Home.md +69 -0
- package/wiki/Input-Devices.md +183 -0
- package/wiki/Keyboard-Shortcuts.md +158 -0
- package/wiki/Memory-System.md +364 -0
- package/wiki/Save-States.md +172 -0
- package/wiki/Video-Rendering.md +658 -0
|
@@ -0,0 +1,2993 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* assembler-editor-window.js - 65C02 assembler editor window
|
|
3
|
+
*
|
|
4
|
+
* Written by
|
|
5
|
+
* Mike Daley <michael_daley@icloud.com>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { BaseWindow } from "../windows/base-window.js";
|
|
9
|
+
import { highlightMerlinSourceInline } from "../utils/merlin-highlighting.js";
|
|
10
|
+
import {
|
|
11
|
+
MerlinEditorSupport,
|
|
12
|
+
COL_OPCODE,
|
|
13
|
+
COL_OPERAND,
|
|
14
|
+
COL_COMMENT,
|
|
15
|
+
OPCODE_WIDTH,
|
|
16
|
+
} from "../utils/merlin-editor-support.js";
|
|
17
|
+
import {
|
|
18
|
+
ROM_ROUTINES,
|
|
19
|
+
ROM_CATEGORIES,
|
|
20
|
+
searchRoutines,
|
|
21
|
+
getRoutinesByCategory,
|
|
22
|
+
} from "../data/apple2-rom-routines.js";
|
|
23
|
+
import { showConfirm } from "../ui/confirm.js";
|
|
24
|
+
|
|
25
|
+
export class AssemblerEditorWindow extends BaseWindow {
|
|
26
|
+
constructor(wasmModule, breakpointManager, isRunningCallback, cpuDebuggerWindow) {
|
|
27
|
+
super({
|
|
28
|
+
id: "assembler-editor",
|
|
29
|
+
title: "Assembler",
|
|
30
|
+
defaultWidth: 640,
|
|
31
|
+
defaultHeight: 600,
|
|
32
|
+
minWidth: 480,
|
|
33
|
+
minHeight: 400,
|
|
34
|
+
});
|
|
35
|
+
this.wasmModule = wasmModule;
|
|
36
|
+
this.bpManager = breakpointManager;
|
|
37
|
+
this.isRunningCallback = isRunningCallback || (() => false);
|
|
38
|
+
this.cpuDebugger = cpuDebuggerWindow;
|
|
39
|
+
this.lastAssembledSize = 0;
|
|
40
|
+
this.lastOrigin = 0;
|
|
41
|
+
this.errors = new Map(); // line number -> error message (from assembler)
|
|
42
|
+
this.syntaxErrors = new Map(); // line number -> error message (from live validation)
|
|
43
|
+
this.currentLine = -1; // Track current line for auto-assemble
|
|
44
|
+
this.lineBytes = new Map(); // line number -> hex bytes string
|
|
45
|
+
this.linePCs = new Map(); // line number -> PC address
|
|
46
|
+
this.symbols = new Map(); // symbol name -> value (from last assembly)
|
|
47
|
+
this.currentPC = undefined; // current PC for expression evaluation
|
|
48
|
+
this.lineBreakpoints = new Map(); // line number -> breakpoint address
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
renderContent() {
|
|
52
|
+
return `
|
|
53
|
+
<div class="asm-editor-content">
|
|
54
|
+
<div class="asm-toolbar">
|
|
55
|
+
<div class="asm-toolbar-group asm-toolbar-actions">
|
|
56
|
+
<button class="asm-btn asm-assemble-btn" title="Assemble (⌘/Ctrl+Enter)">
|
|
57
|
+
<span class="asm-btn-icon">▶</span> Assemble
|
|
58
|
+
</button>
|
|
59
|
+
<button class="asm-btn asm-load-btn" disabled title="Write assembled code into memory">
|
|
60
|
+
Write
|
|
61
|
+
</button>
|
|
62
|
+
<button class="asm-btn asm-debug-btn" disabled title="Open CPU debugger at ORG address">
|
|
63
|
+
Debug
|
|
64
|
+
</button>
|
|
65
|
+
<button class="asm-btn asm-example-btn" title="Load example program">
|
|
66
|
+
<span class="asm-btn-icon">Example</span>
|
|
67
|
+
</button>
|
|
68
|
+
<button class="asm-btn asm-rom-btn" title="ROM Routines Reference (F2)">
|
|
69
|
+
ROM Routines
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="asm-toolbar-separator"></div>
|
|
73
|
+
<div class="asm-toolbar-group asm-toolbar-file">
|
|
74
|
+
<button class="asm-btn asm-new-btn" title="New (⌘/Ctrl+N)">New</button>
|
|
75
|
+
<button class="asm-btn asm-open-btn" title="Open File (⌘/Ctrl+O)">Open</button>
|
|
76
|
+
<button class="asm-btn asm-save-btn" title="Save File (⌘/Ctrl+S)">Save</button>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="asm-status-bar">
|
|
80
|
+
<span class="asm-status"></span>
|
|
81
|
+
<div style="display:flex;align-items:center;gap:8px;margin-left:auto;">
|
|
82
|
+
<span class="asm-cursor-position">Ln 1, Col 0</span>
|
|
83
|
+
<span class="asm-column-indicator"></span>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
<div class="asm-split-container">
|
|
87
|
+
<div class="asm-editor-pane">
|
|
88
|
+
<div class="asm-editor-wrapper">
|
|
89
|
+
<div class="asm-gutter-column">
|
|
90
|
+
<div class="asm-gutter-header">
|
|
91
|
+
<span class="asm-gutter-header-bp" title="Breakpoints (F9 to toggle)"></span>
|
|
92
|
+
<span class="asm-gutter-header-ln">#</span>
|
|
93
|
+
<span class="asm-gutter-header-cyc">Cyc</span>
|
|
94
|
+
<span class="asm-gutter-header-bytes">Bytes</span>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="asm-gutter-content"></div>
|
|
97
|
+
</div>
|
|
98
|
+
<div class="asm-editor-container">
|
|
99
|
+
<div class="asm-editor-header">
|
|
100
|
+
<span class="asm-editor-header-label">Label</span>
|
|
101
|
+
<span class="asm-editor-header-opcode">Opcode</span>
|
|
102
|
+
<span class="asm-editor-header-operand">Operand</span>
|
|
103
|
+
<span class="asm-editor-header-comment">Comment</span>
|
|
104
|
+
</div>
|
|
105
|
+
<div class="asm-editor-scroll-area">
|
|
106
|
+
<div class="asm-column-guides">
|
|
107
|
+
<div class="asm-column-guide" data-col="${COL_OPCODE}" title="Opcode column"></div>
|
|
108
|
+
<div class="asm-column-guide" data-col="${COL_OPERAND}" title="Operand column"></div>
|
|
109
|
+
<div class="asm-column-guide" data-col="${COL_COMMENT}" title="Comment column"></div>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="asm-line-highlight"></div>
|
|
112
|
+
<pre class="asm-highlight" aria-hidden="true"></pre>
|
|
113
|
+
<div class="asm-errors-overlay"></div>
|
|
114
|
+
<textarea class="asm-textarea" spellcheck="false"></textarea>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
<div class="asm-splitter asm-splitter-h" data-direction="horizontal">
|
|
120
|
+
<div class="asm-splitter-handle"></div>
|
|
121
|
+
</div>
|
|
122
|
+
<div class="asm-output-pane">
|
|
123
|
+
<div class="asm-output-panels">
|
|
124
|
+
<div class="asm-panel asm-symbols-panel">
|
|
125
|
+
<div class="asm-panel-header">
|
|
126
|
+
<span class="asm-panel-title">Symbols</span>
|
|
127
|
+
<span class="asm-panel-count"></span>
|
|
128
|
+
</div>
|
|
129
|
+
<div class="asm-panel-content asm-symbols-content">
|
|
130
|
+
<div class="asm-panel-empty">
|
|
131
|
+
<div class="asm-empty-icon">{ }</div>
|
|
132
|
+
<div class="asm-empty-text">Symbols will appear here</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
<div class="asm-splitter asm-splitter-v" data-direction="vertical">
|
|
137
|
+
<div class="asm-splitter-handle"></div>
|
|
138
|
+
</div>
|
|
139
|
+
<div class="asm-panel asm-hex-panel">
|
|
140
|
+
<div class="asm-panel-header">
|
|
141
|
+
<span class="asm-panel-title">Hex Output</span>
|
|
142
|
+
<span class="asm-panel-count"></span>
|
|
143
|
+
</div>
|
|
144
|
+
<div class="asm-panel-content asm-hex-content">
|
|
145
|
+
<div class="asm-panel-empty">
|
|
146
|
+
<div class="asm-empty-icon">[ ]</div>
|
|
147
|
+
<div class="asm-empty-text">Machine code will appear here</div>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
<div class="asm-shortcuts-bar">
|
|
155
|
+
<span class="asm-shortcut"><kbd>Tab</kbd> Next column</span>
|
|
156
|
+
<span class="asm-shortcut"><kbd>⌘/</kbd> Comment</span>
|
|
157
|
+
<span class="asm-shortcut"><kbd>⌘D</kbd> Duplicate</span>
|
|
158
|
+
<span class="asm-shortcut"><kbd>⌘↵</kbd> Assemble</span>
|
|
159
|
+
<span class="asm-shortcut"><kbd>F9</kbd> Breakpoint</span>
|
|
160
|
+
<span class="asm-shortcut"><kbd>F2</kbd> ROM Reference</span>
|
|
161
|
+
</div>
|
|
162
|
+
<div class="asm-rom-panel hidden">
|
|
163
|
+
<div class="asm-rom-header">
|
|
164
|
+
<span class="asm-rom-title">ROM Routines</span>
|
|
165
|
+
<button class="asm-rom-close" title="Close (Esc)">×</button>
|
|
166
|
+
</div>
|
|
167
|
+
<div class="asm-rom-search-bar">
|
|
168
|
+
<input type="text" class="asm-rom-search" placeholder="Search routines..." spellcheck="false" />
|
|
169
|
+
<select class="asm-rom-category">
|
|
170
|
+
<option value="All">All Categories</option>
|
|
171
|
+
</select>
|
|
172
|
+
</div>
|
|
173
|
+
<div class="asm-rom-list"></div>
|
|
174
|
+
<div class="asm-rom-detail hidden">
|
|
175
|
+
<div class="asm-rom-detail-header">
|
|
176
|
+
<button class="asm-rom-back" title="Back to list">← Back</button>
|
|
177
|
+
<span class="asm-rom-detail-name"></span>
|
|
178
|
+
</div>
|
|
179
|
+
<div class="asm-rom-detail-content"></div>
|
|
180
|
+
<div class="asm-rom-detail-actions">
|
|
181
|
+
<button class="asm-btn asm-rom-insert-equ">Insert EQU</button>
|
|
182
|
+
<button class="asm-btn asm-rom-insert-jsr">Insert JSR</button>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
onContentRendered() {
|
|
191
|
+
this.textarea = this.contentElement.querySelector(".asm-textarea");
|
|
192
|
+
this.highlight = this.contentElement.querySelector(".asm-highlight");
|
|
193
|
+
this.lineHighlight = this.contentElement.querySelector(
|
|
194
|
+
".asm-line-highlight",
|
|
195
|
+
);
|
|
196
|
+
this.errorsOverlay = this.contentElement.querySelector(
|
|
197
|
+
".asm-errors-overlay",
|
|
198
|
+
);
|
|
199
|
+
this.gutterContent = this.contentElement.querySelector(
|
|
200
|
+
".asm-gutter-content",
|
|
201
|
+
);
|
|
202
|
+
this.assembleBtn = this.contentElement.querySelector(".asm-assemble-btn");
|
|
203
|
+
this.loadBtn = this.contentElement.querySelector(".asm-load-btn");
|
|
204
|
+
this.debugBtn = this.contentElement.querySelector(".asm-debug-btn");
|
|
205
|
+
this.newBtn = this.contentElement.querySelector(".asm-new-btn");
|
|
206
|
+
this.openBtn = this.contentElement.querySelector(".asm-open-btn");
|
|
207
|
+
this.saveBtn = this.contentElement.querySelector(".asm-save-btn");
|
|
208
|
+
this.statusSpan = this.contentElement.querySelector(".asm-status");
|
|
209
|
+
this.currentFileName = null;
|
|
210
|
+
this._fileHandle = null;
|
|
211
|
+
this.columnIndicator = this.contentElement.querySelector(
|
|
212
|
+
".asm-column-indicator",
|
|
213
|
+
);
|
|
214
|
+
this.cursorPosition = this.contentElement.querySelector(
|
|
215
|
+
".asm-cursor-position",
|
|
216
|
+
);
|
|
217
|
+
this.symbolsContent = this.contentElement.querySelector(
|
|
218
|
+
".asm-symbols-content",
|
|
219
|
+
);
|
|
220
|
+
this.symbolsCount = this.contentElement.querySelector(
|
|
221
|
+
".asm-symbols-panel .asm-panel-count",
|
|
222
|
+
);
|
|
223
|
+
this.hexContent = this.contentElement.querySelector(".asm-hex-content");
|
|
224
|
+
this.hexCount = this.contentElement.querySelector(
|
|
225
|
+
".asm-hex-panel .asm-panel-count",
|
|
226
|
+
);
|
|
227
|
+
this.columnGuides =
|
|
228
|
+
this.contentElement.querySelectorAll(".asm-column-guide");
|
|
229
|
+
this.editorHeader = this.contentElement.querySelector(".asm-editor-header");
|
|
230
|
+
|
|
231
|
+
// ROM panel elements
|
|
232
|
+
this.romBtn = this.contentElement.querySelector(".asm-rom-btn");
|
|
233
|
+
this.romPanel = this.contentElement.querySelector(".asm-rom-panel");
|
|
234
|
+
this.romSearch = this.contentElement.querySelector(".asm-rom-search");
|
|
235
|
+
this.romCategory = this.contentElement.querySelector(".asm-rom-category");
|
|
236
|
+
this.romList = this.contentElement.querySelector(".asm-rom-list");
|
|
237
|
+
this.romDetail = this.contentElement.querySelector(".asm-rom-detail");
|
|
238
|
+
this.romDetailName = this.contentElement.querySelector(
|
|
239
|
+
".asm-rom-detail-name",
|
|
240
|
+
);
|
|
241
|
+
this.romDetailContent = this.contentElement.querySelector(
|
|
242
|
+
".asm-rom-detail-content",
|
|
243
|
+
);
|
|
244
|
+
this.selectedRoutine = null;
|
|
245
|
+
|
|
246
|
+
// Initialize ROM panel
|
|
247
|
+
this.initRomPanel();
|
|
248
|
+
|
|
249
|
+
const editorContainer = this.contentElement.querySelector(
|
|
250
|
+
".asm-editor-scroll-area",
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
// Position column guides and header labels based on character width
|
|
254
|
+
this.positionColumnGuides();
|
|
255
|
+
|
|
256
|
+
// Set placeholder with proper Merlin formatting
|
|
257
|
+
this.setPlaceholder();
|
|
258
|
+
|
|
259
|
+
// Sync highlighting on input
|
|
260
|
+
this.textarea.addEventListener("input", () => {
|
|
261
|
+
this.updateHighlighting();
|
|
262
|
+
this.updateCurrentLineHighlight();
|
|
263
|
+
this.updateGutter();
|
|
264
|
+
this.updateCursorPosition();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Sync scroll position
|
|
268
|
+
this.textarea.addEventListener("scroll", () => {
|
|
269
|
+
this.highlight.scrollTop = this.textarea.scrollTop;
|
|
270
|
+
this.highlight.scrollLeft = this.textarea.scrollLeft;
|
|
271
|
+
this.gutterContent.scrollTop = this.textarea.scrollTop;
|
|
272
|
+
this.errorsOverlay.style.top = `-${this.textarea.scrollTop}px`;
|
|
273
|
+
this.updateCurrentLineHighlight();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Track cursor for line highlight and auto-format on line change
|
|
277
|
+
this.textarea.addEventListener("click", () => {
|
|
278
|
+
this.updateCurrentLineHighlight();
|
|
279
|
+
this.checkLineChangeAndFormat();
|
|
280
|
+
this.updateCursorPosition();
|
|
281
|
+
});
|
|
282
|
+
this.textarea.addEventListener("keyup", (e) => {
|
|
283
|
+
// Check for navigation keys that might change lines
|
|
284
|
+
if (
|
|
285
|
+
[
|
|
286
|
+
"ArrowUp",
|
|
287
|
+
"ArrowDown",
|
|
288
|
+
"Enter",
|
|
289
|
+
"PageUp",
|
|
290
|
+
"PageDown",
|
|
291
|
+
"Home",
|
|
292
|
+
"End",
|
|
293
|
+
].includes(e.key)
|
|
294
|
+
) {
|
|
295
|
+
this.checkLineChangeAndFormat();
|
|
296
|
+
this.scrollCursorIntoView();
|
|
297
|
+
}
|
|
298
|
+
this.updateCursorPosition();
|
|
299
|
+
});
|
|
300
|
+
this.textarea.addEventListener("keydown", (e) => {
|
|
301
|
+
requestAnimationFrame(() => {
|
|
302
|
+
this.updateCurrentLineHighlight();
|
|
303
|
+
this.updateCursorPosition();
|
|
304
|
+
// Scroll for navigation keys
|
|
305
|
+
if (
|
|
306
|
+
[
|
|
307
|
+
"ArrowUp",
|
|
308
|
+
"ArrowDown",
|
|
309
|
+
"Enter",
|
|
310
|
+
"PageUp",
|
|
311
|
+
"PageDown",
|
|
312
|
+
"Home",
|
|
313
|
+
"End",
|
|
314
|
+
].includes(e.key)
|
|
315
|
+
) {
|
|
316
|
+
this.scrollCursorIntoView();
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
this.textarea.addEventListener("focus", () => {
|
|
321
|
+
this.lineHighlight.classList.add("visible");
|
|
322
|
+
this.updateCurrentLineHighlight();
|
|
323
|
+
this.currentLine = this.getCurrentLineNumber();
|
|
324
|
+
this.updateCursorPosition();
|
|
325
|
+
});
|
|
326
|
+
this.textarea.addEventListener("blur", () => {
|
|
327
|
+
this.lineHighlight.classList.remove("visible");
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// Assemble button
|
|
331
|
+
this.assembleBtn.addEventListener("click", () => this.doAssemble());
|
|
332
|
+
|
|
333
|
+
// Load button
|
|
334
|
+
this.loadBtn.addEventListener("click", () => this.doLoad());
|
|
335
|
+
|
|
336
|
+
// Debug button
|
|
337
|
+
this.debugBtn.addEventListener("click", () => {
|
|
338
|
+
if (this.cpuDebugger) {
|
|
339
|
+
this.cpuDebugger.show();
|
|
340
|
+
this.cpuDebugger.disasmViewAddress = this.lastOrigin;
|
|
341
|
+
this.cpuDebugger.updateDisassembly();
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Clear button
|
|
346
|
+
|
|
347
|
+
// Example button
|
|
348
|
+
this.contentElement
|
|
349
|
+
.querySelector(".asm-example-btn")
|
|
350
|
+
.addEventListener("click", () => this.loadExample());
|
|
351
|
+
|
|
352
|
+
// File management buttons
|
|
353
|
+
this.newBtn.addEventListener("click", () => this.newFile());
|
|
354
|
+
this.openBtn.addEventListener("click", () => this.openFile());
|
|
355
|
+
this.saveBtn.addEventListener("click", () => this.saveFile());
|
|
356
|
+
|
|
357
|
+
// Editor support (Tab nav, smart enter, autocomplete, etc.)
|
|
358
|
+
this.editorSupport = new MerlinEditorSupport(
|
|
359
|
+
this.textarea,
|
|
360
|
+
editorContainer,
|
|
361
|
+
{
|
|
362
|
+
onColumnChange: (name, col) => this.updateColumnIndicator(name, col),
|
|
363
|
+
onAssemble: () => this.doAssemble(),
|
|
364
|
+
},
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
// Splitters
|
|
368
|
+
this.initSplitters();
|
|
369
|
+
|
|
370
|
+
// Reposition column guides on window resize
|
|
371
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
372
|
+
this.positionColumnGuides();
|
|
373
|
+
});
|
|
374
|
+
resizeObserver.observe(editorContainer);
|
|
375
|
+
|
|
376
|
+
// Gutter click handler for breakpoints
|
|
377
|
+
this.gutterContent.addEventListener("click", (e) => {
|
|
378
|
+
const gutterLine = e.target.closest(".asm-gutter-line");
|
|
379
|
+
if (gutterLine) {
|
|
380
|
+
const lineNumber = parseInt(gutterLine.dataset.line, 10);
|
|
381
|
+
if (lineNumber) {
|
|
382
|
+
this.toggleBreakpoint(lineNumber);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Keyboard shortcuts
|
|
388
|
+
this.textarea.addEventListener("keydown", (e) => {
|
|
389
|
+
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
|
|
390
|
+
const modKey = isMac ? e.metaKey : e.ctrlKey;
|
|
391
|
+
|
|
392
|
+
if (e.key === "F9") {
|
|
393
|
+
e.preventDefault();
|
|
394
|
+
const lineNumber = this.getCurrentLineNumber();
|
|
395
|
+
this.toggleBreakpoint(lineNumber);
|
|
396
|
+
} else if (e.key === "F2") {
|
|
397
|
+
e.preventDefault();
|
|
398
|
+
this.toggleRomPanel();
|
|
399
|
+
} else if (modKey && e.key.toLowerCase() === "n") {
|
|
400
|
+
e.preventDefault();
|
|
401
|
+
this.newFile();
|
|
402
|
+
} else if (modKey && e.key.toLowerCase() === "o") {
|
|
403
|
+
e.preventDefault();
|
|
404
|
+
this.openFile();
|
|
405
|
+
} else if (modKey && e.key.toLowerCase() === "s") {
|
|
406
|
+
e.preventDefault();
|
|
407
|
+
this.saveFile();
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Listen for breakpoint changes from the manager
|
|
412
|
+
if (this.bpManager) {
|
|
413
|
+
this.bpManager.onChange(() => this.syncBreakpointsFromManager());
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
this.updateHighlighting();
|
|
417
|
+
this.validateAllLines();
|
|
418
|
+
this.encodeAllLineBytes();
|
|
419
|
+
this.updateGutter();
|
|
420
|
+
this.updateCursorPosition();
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
setPlaceholder() {
|
|
424
|
+
// Set a well-formatted Hello World example as placeholder
|
|
425
|
+
const example = `**********************************
|
|
426
|
+
* *
|
|
427
|
+
* HELLO WORLD FOR 6502 *
|
|
428
|
+
* APPLE ][, MERLIN ASSEMBLER *
|
|
429
|
+
* *
|
|
430
|
+
**********************************
|
|
431
|
+
|
|
432
|
+
STROUT EQU $DB3A ;Outputs AY-pointed null-terminated string
|
|
433
|
+
|
|
434
|
+
ORG $0800 ;Standard BASIC program area
|
|
435
|
+
|
|
436
|
+
START LDY #>HELLO
|
|
437
|
+
LDA #<HELLO
|
|
438
|
+
JMP STROUT
|
|
439
|
+
|
|
440
|
+
HELLO ASC "HELLO WORLD!!!!!!",00`;
|
|
441
|
+
this.textarea.placeholder = example;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
positionColumnGuides() {
|
|
445
|
+
// Calculate character width based on font metrics
|
|
446
|
+
const style = getComputedStyle(this.textarea);
|
|
447
|
+
const fontSize = parseFloat(style.fontSize) || 12;
|
|
448
|
+
const charWidth = fontSize * 0.6; // Approximate monospace character width
|
|
449
|
+
const paddingLeft = parseFloat(style.paddingLeft) || 8;
|
|
450
|
+
|
|
451
|
+
// Position column guide lines
|
|
452
|
+
this.columnGuides.forEach((guide) => {
|
|
453
|
+
const col = parseInt(guide.dataset.col, 10);
|
|
454
|
+
guide.style.left = `${paddingLeft + col * charWidth}px`;
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// Position header labels at Merlin column positions
|
|
458
|
+
if (this.editorHeader) {
|
|
459
|
+
const labelEl = this.editorHeader.querySelector(
|
|
460
|
+
".asm-editor-header-label",
|
|
461
|
+
);
|
|
462
|
+
const opcodeEl = this.editorHeader.querySelector(
|
|
463
|
+
".asm-editor-header-opcode",
|
|
464
|
+
);
|
|
465
|
+
const operandEl = this.editorHeader.querySelector(
|
|
466
|
+
".asm-editor-header-operand",
|
|
467
|
+
);
|
|
468
|
+
const commentEl = this.editorHeader.querySelector(
|
|
469
|
+
".asm-editor-header-comment",
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
if (labelEl) labelEl.style.left = `${paddingLeft}px`;
|
|
473
|
+
if (opcodeEl)
|
|
474
|
+
opcodeEl.style.left = `${paddingLeft + COL_OPCODE * charWidth}px`;
|
|
475
|
+
if (operandEl)
|
|
476
|
+
operandEl.style.left = `${paddingLeft + COL_OPERAND * charWidth}px`;
|
|
477
|
+
if (commentEl)
|
|
478
|
+
commentEl.style.left = `${paddingLeft + COL_COMMENT * charWidth}px`;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
initSplitters() {
|
|
483
|
+
const splitters = this.contentElement.querySelectorAll(".asm-splitter");
|
|
484
|
+
for (const splitter of splitters) {
|
|
485
|
+
splitter.addEventListener("mousedown", (e) =>
|
|
486
|
+
this.onSplitterMouseDown(e, splitter),
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
onSplitterMouseDown(e, splitter) {
|
|
492
|
+
e.preventDefault();
|
|
493
|
+
const direction = splitter.dataset.direction;
|
|
494
|
+
const isHorizontal = direction === "horizontal";
|
|
495
|
+
|
|
496
|
+
const container = splitter.parentElement;
|
|
497
|
+
const prevEl = splitter.previousElementSibling;
|
|
498
|
+
const nextEl = splitter.nextElementSibling;
|
|
499
|
+
|
|
500
|
+
const containerRect = container.getBoundingClientRect();
|
|
501
|
+
const startPos = isHorizontal ? e.clientY : e.clientX;
|
|
502
|
+
const prevSize = isHorizontal
|
|
503
|
+
? prevEl.getBoundingClientRect().height
|
|
504
|
+
: prevEl.getBoundingClientRect().width;
|
|
505
|
+
|
|
506
|
+
const totalSize = isHorizontal
|
|
507
|
+
? containerRect.height - splitter.offsetHeight
|
|
508
|
+
: containerRect.width - splitter.offsetWidth;
|
|
509
|
+
|
|
510
|
+
const minSize = 60;
|
|
511
|
+
|
|
512
|
+
const onMouseMove = (e) => {
|
|
513
|
+
const currentPos = isHorizontal ? e.clientY : e.clientX;
|
|
514
|
+
const delta = currentPos - startPos;
|
|
515
|
+
let newPrevSize = prevSize + delta;
|
|
516
|
+
|
|
517
|
+
// Clamp
|
|
518
|
+
newPrevSize = Math.max(
|
|
519
|
+
minSize,
|
|
520
|
+
Math.min(totalSize - minSize, newPrevSize),
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
const prevPercent = (newPrevSize / totalSize) * 100;
|
|
524
|
+
const nextPercent = 100 - prevPercent;
|
|
525
|
+
|
|
526
|
+
prevEl.style.flex = `0 0 ${prevPercent}%`;
|
|
527
|
+
nextEl.style.flex = `0 0 ${nextPercent}%`;
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
const onMouseUp = () => {
|
|
531
|
+
document.removeEventListener("mousemove", onMouseMove);
|
|
532
|
+
document.removeEventListener("mouseup", onMouseUp);
|
|
533
|
+
document.body.style.cursor = "";
|
|
534
|
+
document.body.style.userSelect = "";
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
document.addEventListener("mousemove", onMouseMove);
|
|
538
|
+
document.addEventListener("mouseup", onMouseUp);
|
|
539
|
+
document.body.style.cursor = isHorizontal ? "row-resize" : "col-resize";
|
|
540
|
+
document.body.style.userSelect = "none";
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
updateColumnIndicator(name, col) {
|
|
544
|
+
if (!this.columnIndicator) return;
|
|
545
|
+
const displayNames = {
|
|
546
|
+
label: "Label",
|
|
547
|
+
opcode: "Opcode",
|
|
548
|
+
operand: "Operand",
|
|
549
|
+
comment: "Comment",
|
|
550
|
+
};
|
|
551
|
+
const display = displayNames[name] || name;
|
|
552
|
+
this.columnIndicator.textContent = display;
|
|
553
|
+
this.columnIndicator.className = `asm-column-indicator asm-col-${name}`;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
updateCursorPosition() {
|
|
557
|
+
if (!this.cursorPosition || !this.textarea) return;
|
|
558
|
+
const text = this.textarea.value.substring(0, this.textarea.selectionStart);
|
|
559
|
+
const lines = text.split("\n");
|
|
560
|
+
const lineNum = lines.length;
|
|
561
|
+
const col = lines[lines.length - 1].length;
|
|
562
|
+
this.cursorPosition.textContent = `Ln ${lineNum}, Col ${col}`;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Scroll the textarea to keep the cursor line visible
|
|
567
|
+
*/
|
|
568
|
+
scrollCursorIntoView() {
|
|
569
|
+
if (!this.textarea) return;
|
|
570
|
+
|
|
571
|
+
const text = this.textarea.value.substring(0, this.textarea.selectionStart);
|
|
572
|
+
const lineIndex = text.split("\n").length - 1;
|
|
573
|
+
|
|
574
|
+
const style = getComputedStyle(this.textarea);
|
|
575
|
+
const lineHeight =
|
|
576
|
+
parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.4;
|
|
577
|
+
const paddingTop = parseFloat(style.paddingTop) || 8;
|
|
578
|
+
const paddingBottom = parseFloat(style.paddingBottom) || 8;
|
|
579
|
+
|
|
580
|
+
const cursorTop = paddingTop + lineIndex * lineHeight;
|
|
581
|
+
const cursorBottom = cursorTop + lineHeight;
|
|
582
|
+
|
|
583
|
+
const viewportTop = this.textarea.scrollTop;
|
|
584
|
+
const viewportBottom =
|
|
585
|
+
viewportTop + this.textarea.clientHeight - paddingBottom;
|
|
586
|
+
|
|
587
|
+
// Scroll up if cursor is above viewport
|
|
588
|
+
if (cursorTop < viewportTop + paddingTop) {
|
|
589
|
+
this.textarea.scrollTop = cursorTop - paddingTop;
|
|
590
|
+
}
|
|
591
|
+
// Scroll down if cursor is below viewport
|
|
592
|
+
else if (cursorBottom > viewportBottom) {
|
|
593
|
+
this.textarea.scrollTop =
|
|
594
|
+
cursorBottom - this.textarea.clientHeight + paddingBottom + paddingTop;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
updateCurrentLineHighlight() {
|
|
599
|
+
if (!this.lineHighlight || !this.textarea) return;
|
|
600
|
+
const text = this.textarea.value.substring(0, this.textarea.selectionStart);
|
|
601
|
+
const lineIndex = text.split("\n").length - 1;
|
|
602
|
+
const style = getComputedStyle(this.textarea);
|
|
603
|
+
const lineHeight =
|
|
604
|
+
parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.4;
|
|
605
|
+
const paddingTop = parseFloat(style.paddingTop) || 0;
|
|
606
|
+
const top = paddingTop + lineIndex * lineHeight - this.textarea.scrollTop;
|
|
607
|
+
this.lineHighlight.style.top = `${top}px`;
|
|
608
|
+
this.lineHighlight.style.height = `${lineHeight}px`;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
getCurrentLineNumber() {
|
|
612
|
+
if (!this.textarea) return 0;
|
|
613
|
+
const text = this.textarea.value.substring(0, this.textarea.selectionStart);
|
|
614
|
+
return text.split("\n").length;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
checkLineChangeAndFormat() {
|
|
618
|
+
const newLine = this.getCurrentLineNumber();
|
|
619
|
+
if (newLine !== this.currentLine && this.currentLine !== -1) {
|
|
620
|
+
// Format, validate, and encode the line we're leaving
|
|
621
|
+
this.formatLine(this.currentLine);
|
|
622
|
+
this.validateLine(this.currentLine);
|
|
623
|
+
this.encodeLineBytes(this.currentLine);
|
|
624
|
+
this.updateGutter();
|
|
625
|
+
this.updateErrorsOverlay();
|
|
626
|
+
this.currentLine = newLine;
|
|
627
|
+
} else {
|
|
628
|
+
this.currentLine = newLine;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
formatLine(lineNumber) {
|
|
633
|
+
if (!this.textarea) return;
|
|
634
|
+
|
|
635
|
+
const lines = this.textarea.value.split("\n");
|
|
636
|
+
if (lineNumber < 1 || lineNumber > lines.length) return;
|
|
637
|
+
|
|
638
|
+
const lineIndex = lineNumber - 1;
|
|
639
|
+
const line = lines[lineIndex];
|
|
640
|
+
|
|
641
|
+
// Skip empty lines or comment-only lines
|
|
642
|
+
if (
|
|
643
|
+
!line.trim() ||
|
|
644
|
+
line.trim().startsWith(";") ||
|
|
645
|
+
line.trim().startsWith("*")
|
|
646
|
+
) {
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Parse the line into components
|
|
651
|
+
const parsed = this.parseLine(line);
|
|
652
|
+
if (!parsed) return;
|
|
653
|
+
|
|
654
|
+
// Rebuild with proper column alignment
|
|
655
|
+
const formatted = this.buildFormattedLine(parsed);
|
|
656
|
+
|
|
657
|
+
// Only update if different
|
|
658
|
+
if (formatted !== line) {
|
|
659
|
+
lines[lineIndex] = formatted;
|
|
660
|
+
const cursorPos = this.textarea.selectionStart;
|
|
661
|
+
this.textarea.value = lines.join("\n");
|
|
662
|
+
this.textarea.selectionStart = this.textarea.selectionEnd = cursorPos;
|
|
663
|
+
this.updateHighlighting();
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
formatAllLines() {
|
|
668
|
+
if (!this.textarea) return;
|
|
669
|
+
|
|
670
|
+
const lines = this.textarea.value.split("\n");
|
|
671
|
+
let changed = false;
|
|
672
|
+
|
|
673
|
+
for (let i = 0; i < lines.length; i++) {
|
|
674
|
+
const line = lines[i];
|
|
675
|
+
|
|
676
|
+
// Skip empty lines or comment-only lines
|
|
677
|
+
if (
|
|
678
|
+
!line.trim() ||
|
|
679
|
+
line.trim().startsWith(";") ||
|
|
680
|
+
line.trim().startsWith("*")
|
|
681
|
+
) {
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Parse the line into components
|
|
686
|
+
const parsed = this.parseLine(line);
|
|
687
|
+
if (!parsed) continue;
|
|
688
|
+
|
|
689
|
+
// Rebuild with proper column alignment
|
|
690
|
+
const formatted = this.buildFormattedLine(parsed);
|
|
691
|
+
|
|
692
|
+
if (formatted !== line) {
|
|
693
|
+
lines[i] = formatted;
|
|
694
|
+
changed = true;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (changed) {
|
|
699
|
+
const cursorPos = this.textarea.selectionStart;
|
|
700
|
+
this.textarea.value = lines.join("\n");
|
|
701
|
+
this.textarea.selectionStart = this.textarea.selectionEnd = cursorPos;
|
|
702
|
+
this.updateHighlighting();
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
parseLine(line) {
|
|
707
|
+
// Merlin column layout: Label, Opcode, Operand, Comment (see COL_* constants)
|
|
708
|
+
let label = "";
|
|
709
|
+
let opcode = "";
|
|
710
|
+
let operand = "";
|
|
711
|
+
let comment = "";
|
|
712
|
+
|
|
713
|
+
// Check for comment at start of line (full-line comment)
|
|
714
|
+
if (line.trim().startsWith(";") || line.trim().startsWith("*")) {
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Find comment (starts with ;)
|
|
719
|
+
let commentIdx = -1;
|
|
720
|
+
let inQuote = false;
|
|
721
|
+
for (let i = 0; i < line.length; i++) {
|
|
722
|
+
const ch = line[i];
|
|
723
|
+
if (ch === '"' || ch === "'") inQuote = !inQuote;
|
|
724
|
+
if (ch === ";" && !inQuote) {
|
|
725
|
+
commentIdx = i;
|
|
726
|
+
break;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
let mainPart = commentIdx >= 0 ? line.substring(0, commentIdx) : line;
|
|
731
|
+
comment = commentIdx >= 0 ? line.substring(commentIdx) : "";
|
|
732
|
+
|
|
733
|
+
// Check if line starts with whitespace (no label)
|
|
734
|
+
const startsWithSpace =
|
|
735
|
+
mainPart.length > 0 && (mainPart[0] === " " || mainPart[0] === "\t");
|
|
736
|
+
|
|
737
|
+
// Split main part by whitespace
|
|
738
|
+
const tokens = mainPart.trim().split(/\s+/);
|
|
739
|
+
|
|
740
|
+
if (tokens.length === 0 || (tokens.length === 1 && tokens[0] === "")) {
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if (startsWithSpace) {
|
|
745
|
+
// No label - first token is opcode
|
|
746
|
+
opcode = tokens[0] || "";
|
|
747
|
+
operand = tokens.slice(1).join(" ");
|
|
748
|
+
} else {
|
|
749
|
+
// First token is label
|
|
750
|
+
label = tokens[0] || "";
|
|
751
|
+
opcode = tokens[1] || "";
|
|
752
|
+
operand = tokens.slice(2).join(" ");
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
return { label, opcode, operand, comment };
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
buildFormattedLine(parsed) {
|
|
759
|
+
const { label, opcode, operand, comment } = parsed;
|
|
760
|
+
|
|
761
|
+
let result = "";
|
|
762
|
+
|
|
763
|
+
// Label column
|
|
764
|
+
result = label.padEnd(COL_OPCODE, " ");
|
|
765
|
+
|
|
766
|
+
// Opcode column
|
|
767
|
+
if (opcode) {
|
|
768
|
+
result =
|
|
769
|
+
result.substring(0, COL_OPCODE) +
|
|
770
|
+
opcode.toUpperCase().padEnd(OPCODE_WIDTH, " ");
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Operand column
|
|
774
|
+
if (operand) {
|
|
775
|
+
result = result.substring(0, COL_OPERAND) + operand;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Comment column
|
|
779
|
+
if (comment) {
|
|
780
|
+
const currentLen = result.trimEnd().length;
|
|
781
|
+
if (currentLen < COL_COMMENT) {
|
|
782
|
+
result = result.trimEnd().padEnd(COL_COMMENT, " ") + comment;
|
|
783
|
+
} else {
|
|
784
|
+
result = result.trimEnd() + " " + comment;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return result.trimEnd();
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// 65C02 instruction info: { cycles, bytes } by mnemonic
|
|
792
|
+
// Cycles shown are base cycles (some modes add +1 for page crossing)
|
|
793
|
+
getInstructionInfo() {
|
|
794
|
+
return {
|
|
795
|
+
// Branch / Flow
|
|
796
|
+
JMP: { cycles: 3, bytes: 3 },
|
|
797
|
+
JSR: { cycles: 6, bytes: 3 },
|
|
798
|
+
BCC: { cycles: 2, bytes: 2 },
|
|
799
|
+
BCS: { cycles: 2, bytes: 2 },
|
|
800
|
+
BEQ: { cycles: 2, bytes: 2 },
|
|
801
|
+
BMI: { cycles: 2, bytes: 2 },
|
|
802
|
+
BNE: { cycles: 2, bytes: 2 },
|
|
803
|
+
BPL: { cycles: 2, bytes: 2 },
|
|
804
|
+
BRA: { cycles: 3, bytes: 2 },
|
|
805
|
+
BVC: { cycles: 2, bytes: 2 },
|
|
806
|
+
BVS: { cycles: 2, bytes: 2 },
|
|
807
|
+
RTS: { cycles: 6, bytes: 1 },
|
|
808
|
+
RTI: { cycles: 6, bytes: 1 },
|
|
809
|
+
BRK: { cycles: 7, bytes: 1 },
|
|
810
|
+
// Load / Store
|
|
811
|
+
LDA: { cycles: 2, bytes: 2 },
|
|
812
|
+
LDX: { cycles: 2, bytes: 2 },
|
|
813
|
+
LDY: { cycles: 2, bytes: 2 },
|
|
814
|
+
STA: { cycles: 3, bytes: 2 },
|
|
815
|
+
STX: { cycles: 3, bytes: 2 },
|
|
816
|
+
STY: { cycles: 3, bytes: 2 },
|
|
817
|
+
STZ: { cycles: 3, bytes: 2 },
|
|
818
|
+
// Math / Logic
|
|
819
|
+
ADC: { cycles: 2, bytes: 2 },
|
|
820
|
+
SBC: { cycles: 2, bytes: 2 },
|
|
821
|
+
AND: { cycles: 2, bytes: 2 },
|
|
822
|
+
ORA: { cycles: 2, bytes: 2 },
|
|
823
|
+
EOR: { cycles: 2, bytes: 2 },
|
|
824
|
+
ASL: { cycles: 2, bytes: 1 },
|
|
825
|
+
LSR: { cycles: 2, bytes: 1 },
|
|
826
|
+
ROL: { cycles: 2, bytes: 1 },
|
|
827
|
+
ROR: { cycles: 2, bytes: 1 },
|
|
828
|
+
INC: { cycles: 2, bytes: 1 },
|
|
829
|
+
DEC: { cycles: 2, bytes: 1 },
|
|
830
|
+
INA: { cycles: 2, bytes: 1 },
|
|
831
|
+
DEA: { cycles: 2, bytes: 1 },
|
|
832
|
+
INX: { cycles: 2, bytes: 1 },
|
|
833
|
+
DEX: { cycles: 2, bytes: 1 },
|
|
834
|
+
INY: { cycles: 2, bytes: 1 },
|
|
835
|
+
DEY: { cycles: 2, bytes: 1 },
|
|
836
|
+
CMP: { cycles: 2, bytes: 2 },
|
|
837
|
+
CPX: { cycles: 2, bytes: 2 },
|
|
838
|
+
CPY: { cycles: 2, bytes: 2 },
|
|
839
|
+
BIT: { cycles: 2, bytes: 2 },
|
|
840
|
+
TRB: { cycles: 5, bytes: 2 },
|
|
841
|
+
TSB: { cycles: 5, bytes: 2 },
|
|
842
|
+
// Stack / Transfer
|
|
843
|
+
PHA: { cycles: 3, bytes: 1 },
|
|
844
|
+
PHP: { cycles: 3, bytes: 1 },
|
|
845
|
+
PHX: { cycles: 3, bytes: 1 },
|
|
846
|
+
PHY: { cycles: 3, bytes: 1 },
|
|
847
|
+
PLA: { cycles: 4, bytes: 1 },
|
|
848
|
+
PLP: { cycles: 4, bytes: 1 },
|
|
849
|
+
PLX: { cycles: 4, bytes: 1 },
|
|
850
|
+
PLY: { cycles: 4, bytes: 1 },
|
|
851
|
+
TAX: { cycles: 2, bytes: 1 },
|
|
852
|
+
TAY: { cycles: 2, bytes: 1 },
|
|
853
|
+
TSX: { cycles: 2, bytes: 1 },
|
|
854
|
+
TXA: { cycles: 2, bytes: 1 },
|
|
855
|
+
TXS: { cycles: 2, bytes: 1 },
|
|
856
|
+
TYA: { cycles: 2, bytes: 1 },
|
|
857
|
+
// Flags
|
|
858
|
+
CLC: { cycles: 2, bytes: 1 },
|
|
859
|
+
CLD: { cycles: 2, bytes: 1 },
|
|
860
|
+
CLI: { cycles: 2, bytes: 1 },
|
|
861
|
+
CLV: { cycles: 2, bytes: 1 },
|
|
862
|
+
SEC: { cycles: 2, bytes: 1 },
|
|
863
|
+
SED: { cycles: 2, bytes: 1 },
|
|
864
|
+
SEI: { cycles: 2, bytes: 1 },
|
|
865
|
+
NOP: { cycles: 2, bytes: 1 },
|
|
866
|
+
WAI: { cycles: 3, bytes: 1 },
|
|
867
|
+
STP: { cycles: 3, bytes: 1 },
|
|
868
|
+
// 65C02 BBR/BBS (3 bytes: opcode, zp address, relative offset)
|
|
869
|
+
BBR0: { cycles: 5, bytes: 3 },
|
|
870
|
+
BBR1: { cycles: 5, bytes: 3 },
|
|
871
|
+
BBR2: { cycles: 5, bytes: 3 },
|
|
872
|
+
BBR3: { cycles: 5, bytes: 3 },
|
|
873
|
+
BBR4: { cycles: 5, bytes: 3 },
|
|
874
|
+
BBR5: { cycles: 5, bytes: 3 },
|
|
875
|
+
BBR6: { cycles: 5, bytes: 3 },
|
|
876
|
+
BBR7: { cycles: 5, bytes: 3 },
|
|
877
|
+
BBS0: { cycles: 5, bytes: 3 },
|
|
878
|
+
BBS1: { cycles: 5, bytes: 3 },
|
|
879
|
+
BBS2: { cycles: 5, bytes: 3 },
|
|
880
|
+
BBS3: { cycles: 5, bytes: 3 },
|
|
881
|
+
BBS4: { cycles: 5, bytes: 3 },
|
|
882
|
+
BBS5: { cycles: 5, bytes: 3 },
|
|
883
|
+
BBS6: { cycles: 5, bytes: 3 },
|
|
884
|
+
BBS7: { cycles: 5, bytes: 3 },
|
|
885
|
+
// 65C02 RMB/SMB (2 bytes: opcode, zp address)
|
|
886
|
+
RMB0: { cycles: 5, bytes: 2 },
|
|
887
|
+
RMB1: { cycles: 5, bytes: 2 },
|
|
888
|
+
RMB2: { cycles: 5, bytes: 2 },
|
|
889
|
+
RMB3: { cycles: 5, bytes: 2 },
|
|
890
|
+
RMB4: { cycles: 5, bytes: 2 },
|
|
891
|
+
RMB5: { cycles: 5, bytes: 2 },
|
|
892
|
+
RMB6: { cycles: 5, bytes: 2 },
|
|
893
|
+
RMB7: { cycles: 5, bytes: 2 },
|
|
894
|
+
SMB0: { cycles: 5, bytes: 2 },
|
|
895
|
+
SMB1: { cycles: 5, bytes: 2 },
|
|
896
|
+
SMB2: { cycles: 5, bytes: 2 },
|
|
897
|
+
SMB3: { cycles: 5, bytes: 2 },
|
|
898
|
+
SMB4: { cycles: 5, bytes: 2 },
|
|
899
|
+
SMB5: { cycles: 5, bytes: 2 },
|
|
900
|
+
SMB6: { cycles: 5, bytes: 2 },
|
|
901
|
+
SMB7: { cycles: 5, bytes: 2 },
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
updateGutter() {
|
|
906
|
+
if (!this.gutterContent || !this.textarea) return;
|
|
907
|
+
|
|
908
|
+
const lines = this.textarea.value.split("\n");
|
|
909
|
+
const instrInfo = this.getInstructionInfo();
|
|
910
|
+
const opcodeTable = this.getOpcodeTable();
|
|
911
|
+
const gutterLines = [];
|
|
912
|
+
const numWidth = Math.max(2, String(lines.length).length);
|
|
913
|
+
|
|
914
|
+
for (let i = 0; i < lines.length; i++) {
|
|
915
|
+
const lineNum = String(i + 1).padStart(numWidth, " ");
|
|
916
|
+
const lineNumber = i + 1;
|
|
917
|
+
const parsed = this.parseLine(lines[i]);
|
|
918
|
+
let cycles = "";
|
|
919
|
+
let bytesHex = this.lineBytes.get(lineNumber) || "";
|
|
920
|
+
const hasError =
|
|
921
|
+
this.errors.has(lineNumber) || this.syntaxErrors.has(lineNumber);
|
|
922
|
+
const errorClass = hasError ? " asm-gutter-error" : "";
|
|
923
|
+
|
|
924
|
+
// Check if this line has an actual instruction (not a directive, comment, or label-only)
|
|
925
|
+
const isInstruction =
|
|
926
|
+
parsed && parsed.opcode && opcodeTable[parsed.opcode.toUpperCase()];
|
|
927
|
+
|
|
928
|
+
// Check for breakpoint at this line's address (only for instruction lines)
|
|
929
|
+
const lineAddr = this.linePCs.get(lineNumber);
|
|
930
|
+
const hasBreakpoint =
|
|
931
|
+
isInstruction &&
|
|
932
|
+
lineAddr !== undefined &&
|
|
933
|
+
this.bpManager?.has(lineAddr);
|
|
934
|
+
const bpClass = hasBreakpoint ? " asm-gutter-bp" : "";
|
|
935
|
+
|
|
936
|
+
// Breakpoint indicator: red dot for breakpoint, clickable space for instruction lines, nothing for non-instructions
|
|
937
|
+
let bpIndicator;
|
|
938
|
+
if (hasBreakpoint) {
|
|
939
|
+
bpIndicator = '<span class="asm-gutter-bp-dot"></span>';
|
|
940
|
+
} else if (isInstruction) {
|
|
941
|
+
bpIndicator =
|
|
942
|
+
'<span class="asm-gutter-bp-space asm-gutter-bp-clickable"></span>';
|
|
943
|
+
} else {
|
|
944
|
+
bpIndicator = '<span class="asm-gutter-bp-space"></span>';
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
if (parsed && parsed.opcode) {
|
|
948
|
+
const mnem = parsed.opcode.toUpperCase();
|
|
949
|
+
const info = instrInfo[mnem];
|
|
950
|
+
if (info) {
|
|
951
|
+
cycles = String(info.cycles);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
gutterLines.push(
|
|
956
|
+
`<div class="asm-gutter-line${errorClass}${bpClass}" data-line="${lineNumber}">` +
|
|
957
|
+
`${bpIndicator}` +
|
|
958
|
+
`<span class="asm-gutter-ln">${lineNum}</span>` +
|
|
959
|
+
`<span class="asm-gutter-cyc">${cycles || ""}</span>` +
|
|
960
|
+
`<span class="asm-gutter-bytes">${bytesHex || ""}</span>` +
|
|
961
|
+
`</div>`,
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
this.gutterContent.innerHTML = gutterLines.join("");
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// Compatibility alias
|
|
969
|
+
updateCyclesGutter() {
|
|
970
|
+
this.updateGutter();
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// 65C02 opcode encoding table: mnemonic -> { mode: opcode }
|
|
974
|
+
// Modes: IMP, ACC, IMM, ZP, ZPX, ZPY, ABS, ABX, ABY, IND, IZX, IZY, ZPI, REL
|
|
975
|
+
getOpcodeTable() {
|
|
976
|
+
return {
|
|
977
|
+
ADC: {
|
|
978
|
+
IMM: 0x69,
|
|
979
|
+
ZP: 0x65,
|
|
980
|
+
ZPX: 0x75,
|
|
981
|
+
ABS: 0x6d,
|
|
982
|
+
ABX: 0x7d,
|
|
983
|
+
ABY: 0x79,
|
|
984
|
+
IZX: 0x61,
|
|
985
|
+
IZY: 0x71,
|
|
986
|
+
ZPI: 0x72,
|
|
987
|
+
},
|
|
988
|
+
AND: {
|
|
989
|
+
IMM: 0x29,
|
|
990
|
+
ZP: 0x25,
|
|
991
|
+
ZPX: 0x35,
|
|
992
|
+
ABS: 0x2d,
|
|
993
|
+
ABX: 0x3d,
|
|
994
|
+
ABY: 0x39,
|
|
995
|
+
IZX: 0x21,
|
|
996
|
+
IZY: 0x31,
|
|
997
|
+
ZPI: 0x32,
|
|
998
|
+
},
|
|
999
|
+
ASL: { IMP: 0x0a, ACC: 0x0a, ZP: 0x06, ZPX: 0x16, ABS: 0x0e, ABX: 0x1e },
|
|
1000
|
+
BCC: { REL: 0x90 },
|
|
1001
|
+
BCS: { REL: 0xb0 },
|
|
1002
|
+
BEQ: { REL: 0xf0 },
|
|
1003
|
+
BIT: { IMM: 0x89, ZP: 0x24, ZPX: 0x34, ABS: 0x2c, ABX: 0x3c },
|
|
1004
|
+
BMI: { REL: 0x30 },
|
|
1005
|
+
BNE: { REL: 0xd0 },
|
|
1006
|
+
BPL: { REL: 0x10 },
|
|
1007
|
+
BRA: { REL: 0x80 },
|
|
1008
|
+
BRK: { IMP: 0x00 },
|
|
1009
|
+
BVC: { REL: 0x50 },
|
|
1010
|
+
BVS: { REL: 0x70 },
|
|
1011
|
+
CLC: { IMP: 0x18 },
|
|
1012
|
+
CLD: { IMP: 0xd8 },
|
|
1013
|
+
CLI: { IMP: 0x58 },
|
|
1014
|
+
CLV: { IMP: 0xb8 },
|
|
1015
|
+
CMP: {
|
|
1016
|
+
IMM: 0xc9,
|
|
1017
|
+
ZP: 0xc5,
|
|
1018
|
+
ZPX: 0xd5,
|
|
1019
|
+
ABS: 0xcd,
|
|
1020
|
+
ABX: 0xdd,
|
|
1021
|
+
ABY: 0xd9,
|
|
1022
|
+
IZX: 0xc1,
|
|
1023
|
+
IZY: 0xd1,
|
|
1024
|
+
ZPI: 0xd2,
|
|
1025
|
+
},
|
|
1026
|
+
CPX: { IMM: 0xe0, ZP: 0xe4, ABS: 0xec },
|
|
1027
|
+
CPY: { IMM: 0xc0, ZP: 0xc4, ABS: 0xcc },
|
|
1028
|
+
DEC: { IMP: 0x3a, ACC: 0x3a, ZP: 0xc6, ZPX: 0xd6, ABS: 0xce, ABX: 0xde },
|
|
1029
|
+
DEA: { IMP: 0x3a },
|
|
1030
|
+
DEX: { IMP: 0xca },
|
|
1031
|
+
DEY: { IMP: 0x88 },
|
|
1032
|
+
EOR: {
|
|
1033
|
+
IMM: 0x49,
|
|
1034
|
+
ZP: 0x45,
|
|
1035
|
+
ZPX: 0x55,
|
|
1036
|
+
ABS: 0x4d,
|
|
1037
|
+
ABX: 0x5d,
|
|
1038
|
+
ABY: 0x59,
|
|
1039
|
+
IZX: 0x41,
|
|
1040
|
+
IZY: 0x51,
|
|
1041
|
+
ZPI: 0x52,
|
|
1042
|
+
},
|
|
1043
|
+
INC: { IMP: 0x1a, ACC: 0x1a, ZP: 0xe6, ZPX: 0xf6, ABS: 0xee, ABX: 0xfe },
|
|
1044
|
+
INA: { IMP: 0x1a },
|
|
1045
|
+
INX: { IMP: 0xe8 },
|
|
1046
|
+
INY: { IMP: 0xc8 },
|
|
1047
|
+
JMP: { ABS: 0x4c, IND: 0x6c, IAX: 0x7c },
|
|
1048
|
+
JSR: { ABS: 0x20 },
|
|
1049
|
+
LDA: {
|
|
1050
|
+
IMM: 0xa9,
|
|
1051
|
+
ZP: 0xa5,
|
|
1052
|
+
ZPX: 0xb5,
|
|
1053
|
+
ABS: 0xad,
|
|
1054
|
+
ABX: 0xbd,
|
|
1055
|
+
ABY: 0xb9,
|
|
1056
|
+
IZX: 0xa1,
|
|
1057
|
+
IZY: 0xb1,
|
|
1058
|
+
ZPI: 0xb2,
|
|
1059
|
+
},
|
|
1060
|
+
LDX: { IMM: 0xa2, ZP: 0xa6, ZPY: 0xb6, ABS: 0xae, ABY: 0xbe },
|
|
1061
|
+
LDY: { IMM: 0xa0, ZP: 0xa4, ZPX: 0xb4, ABS: 0xac, ABX: 0xbc },
|
|
1062
|
+
LSR: { IMP: 0x4a, ACC: 0x4a, ZP: 0x46, ZPX: 0x56, ABS: 0x4e, ABX: 0x5e },
|
|
1063
|
+
NOP: { IMP: 0xea },
|
|
1064
|
+
ORA: {
|
|
1065
|
+
IMM: 0x09,
|
|
1066
|
+
ZP: 0x05,
|
|
1067
|
+
ZPX: 0x15,
|
|
1068
|
+
ABS: 0x0d,
|
|
1069
|
+
ABX: 0x1d,
|
|
1070
|
+
ABY: 0x19,
|
|
1071
|
+
IZX: 0x01,
|
|
1072
|
+
IZY: 0x11,
|
|
1073
|
+
ZPI: 0x12,
|
|
1074
|
+
},
|
|
1075
|
+
PHA: { IMP: 0x48 },
|
|
1076
|
+
PHP: { IMP: 0x08 },
|
|
1077
|
+
PHX: { IMP: 0xda },
|
|
1078
|
+
PHY: { IMP: 0x5a },
|
|
1079
|
+
PLA: { IMP: 0x68 },
|
|
1080
|
+
PLP: { IMP: 0x28 },
|
|
1081
|
+
PLX: { IMP: 0xfa },
|
|
1082
|
+
PLY: { IMP: 0x7a },
|
|
1083
|
+
ROL: { IMP: 0x2a, ACC: 0x2a, ZP: 0x26, ZPX: 0x36, ABS: 0x2e, ABX: 0x3e },
|
|
1084
|
+
ROR: { IMP: 0x6a, ACC: 0x6a, ZP: 0x66, ZPX: 0x76, ABS: 0x6e, ABX: 0x7e },
|
|
1085
|
+
RTI: { IMP: 0x40 },
|
|
1086
|
+
RTS: { IMP: 0x60 },
|
|
1087
|
+
SBC: {
|
|
1088
|
+
IMM: 0xe9,
|
|
1089
|
+
ZP: 0xe5,
|
|
1090
|
+
ZPX: 0xf5,
|
|
1091
|
+
ABS: 0xed,
|
|
1092
|
+
ABX: 0xfd,
|
|
1093
|
+
ABY: 0xf9,
|
|
1094
|
+
IZX: 0xe1,
|
|
1095
|
+
IZY: 0xf1,
|
|
1096
|
+
ZPI: 0xf2,
|
|
1097
|
+
},
|
|
1098
|
+
SEC: { IMP: 0x38 },
|
|
1099
|
+
SED: { IMP: 0xf8 },
|
|
1100
|
+
SEI: { IMP: 0x78 },
|
|
1101
|
+
STA: {
|
|
1102
|
+
ZP: 0x85,
|
|
1103
|
+
ZPX: 0x95,
|
|
1104
|
+
ABS: 0x8d,
|
|
1105
|
+
ABX: 0x9d,
|
|
1106
|
+
ABY: 0x99,
|
|
1107
|
+
IZX: 0x81,
|
|
1108
|
+
IZY: 0x91,
|
|
1109
|
+
ZPI: 0x92,
|
|
1110
|
+
},
|
|
1111
|
+
STX: { ZP: 0x86, ZPY: 0x96, ABS: 0x8e },
|
|
1112
|
+
STY: { ZP: 0x84, ZPX: 0x94, ABS: 0x8c },
|
|
1113
|
+
STZ: { ZP: 0x64, ZPX: 0x74, ABS: 0x9c, ABX: 0x9e },
|
|
1114
|
+
TAX: { IMP: 0xaa },
|
|
1115
|
+
TAY: { IMP: 0xa8 },
|
|
1116
|
+
TRB: { ZP: 0x14, ABS: 0x1c },
|
|
1117
|
+
TSB: { ZP: 0x04, ABS: 0x0c },
|
|
1118
|
+
TSX: { IMP: 0xba },
|
|
1119
|
+
TXA: { IMP: 0x8a },
|
|
1120
|
+
TXS: { IMP: 0x9a },
|
|
1121
|
+
TYA: { IMP: 0x98 },
|
|
1122
|
+
WAI: { IMP: 0xcb },
|
|
1123
|
+
STP: { IMP: 0xdb },
|
|
1124
|
+
// BBR/BBS (zero page relative - 3 bytes)
|
|
1125
|
+
BBR0: { ZPR: 0x0f },
|
|
1126
|
+
BBR1: { ZPR: 0x1f },
|
|
1127
|
+
BBR2: { ZPR: 0x2f },
|
|
1128
|
+
BBR3: { ZPR: 0x3f },
|
|
1129
|
+
BBR4: { ZPR: 0x4f },
|
|
1130
|
+
BBR5: { ZPR: 0x5f },
|
|
1131
|
+
BBR6: { ZPR: 0x6f },
|
|
1132
|
+
BBR7: { ZPR: 0x7f },
|
|
1133
|
+
BBS0: { ZPR: 0x8f },
|
|
1134
|
+
BBS1: { ZPR: 0x9f },
|
|
1135
|
+
BBS2: { ZPR: 0xaf },
|
|
1136
|
+
BBS3: { ZPR: 0xbf },
|
|
1137
|
+
BBS4: { ZPR: 0xcf },
|
|
1138
|
+
BBS5: { ZPR: 0xdf },
|
|
1139
|
+
BBS6: { ZPR: 0xef },
|
|
1140
|
+
BBS7: { ZPR: 0xff },
|
|
1141
|
+
// RMB/SMB (zero page - 2 bytes)
|
|
1142
|
+
RMB0: { ZP: 0x07 },
|
|
1143
|
+
RMB1: { ZP: 0x17 },
|
|
1144
|
+
RMB2: { ZP: 0x27 },
|
|
1145
|
+
RMB3: { ZP: 0x37 },
|
|
1146
|
+
RMB4: { ZP: 0x47 },
|
|
1147
|
+
RMB5: { ZP: 0x57 },
|
|
1148
|
+
RMB6: { ZP: 0x67 },
|
|
1149
|
+
RMB7: { ZP: 0x77 },
|
|
1150
|
+
SMB0: { ZP: 0x87 },
|
|
1151
|
+
SMB1: { ZP: 0x97 },
|
|
1152
|
+
SMB2: { ZP: 0xa7 },
|
|
1153
|
+
SMB3: { ZP: 0xb7 },
|
|
1154
|
+
SMB4: { ZP: 0xc7 },
|
|
1155
|
+
SMB5: { ZP: 0xd7 },
|
|
1156
|
+
SMB6: { ZP: 0xe7 },
|
|
1157
|
+
SMB7: { ZP: 0xf7 },
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
/**
|
|
1162
|
+
* Parse an operand and determine addressing mode + value
|
|
1163
|
+
* Returns { mode, value, value2 } or null if unparseable
|
|
1164
|
+
*/
|
|
1165
|
+
parseOperand(operand, mnemonic) {
|
|
1166
|
+
if (!operand || operand.trim() === "") {
|
|
1167
|
+
return { mode: "IMP", value: null };
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
operand = operand.trim();
|
|
1171
|
+
const opcodes = this.getOpcodeTable()[mnemonic];
|
|
1172
|
+
if (!opcodes) return null;
|
|
1173
|
+
|
|
1174
|
+
// Immediate: #$xx or #value
|
|
1175
|
+
if (operand.startsWith("#")) {
|
|
1176
|
+
const val = this.parseValue(operand.substring(1));
|
|
1177
|
+
if (val !== null) {
|
|
1178
|
+
return { mode: "IMM", value: val & 0xff };
|
|
1179
|
+
}
|
|
1180
|
+
return null; // Unresolved symbol
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// Indirect modes
|
|
1184
|
+
if (operand.startsWith("(")) {
|
|
1185
|
+
// (addr,X) - Indexed indirect
|
|
1186
|
+
if (operand.match(/^\([^)]+,\s*X\)$/i)) {
|
|
1187
|
+
const inner = operand.match(/^\(([^,]+),/i)[1];
|
|
1188
|
+
const val = this.parseValue(inner);
|
|
1189
|
+
if (val !== null) return { mode: "IZX", value: val & 0xff };
|
|
1190
|
+
return null;
|
|
1191
|
+
}
|
|
1192
|
+
// (addr),Y - Indirect indexed
|
|
1193
|
+
if (operand.match(/^\([^)]+\)\s*,\s*Y$/i)) {
|
|
1194
|
+
const inner = operand.match(/^\(([^)]+)\)/i)[1];
|
|
1195
|
+
const val = this.parseValue(inner);
|
|
1196
|
+
if (val !== null) return { mode: "IZY", value: val & 0xff };
|
|
1197
|
+
return null;
|
|
1198
|
+
}
|
|
1199
|
+
// (addr,X) for JMP
|
|
1200
|
+
if (operand.match(/^\([^)]+,\s*X\)$/i) && opcodes.IAX) {
|
|
1201
|
+
const inner = operand.match(/^\(([^,]+),/i)[1];
|
|
1202
|
+
const val = this.parseValue(inner);
|
|
1203
|
+
if (val !== null) return { mode: "IAX", value: val & 0xffff };
|
|
1204
|
+
return null;
|
|
1205
|
+
}
|
|
1206
|
+
// (addr) - Indirect (JMP) or Zero Page Indirect (65C02)
|
|
1207
|
+
if (operand.match(/^\([^)]+\)$/)) {
|
|
1208
|
+
const inner = operand.match(/^\(([^)]+)\)$/)[1];
|
|
1209
|
+
const val = this.parseValue(inner);
|
|
1210
|
+
if (val !== null) {
|
|
1211
|
+
if (opcodes.IND && val > 0xff)
|
|
1212
|
+
return { mode: "IND", value: val & 0xffff };
|
|
1213
|
+
if (opcodes.ZPI) return { mode: "ZPI", value: val & 0xff };
|
|
1214
|
+
if (opcodes.IND) return { mode: "IND", value: val & 0xffff };
|
|
1215
|
+
}
|
|
1216
|
+
return null;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// addr,X or addr,Y
|
|
1221
|
+
if (operand.match(/,\s*X$/i)) {
|
|
1222
|
+
const addrPart = operand.replace(/,\s*X$/i, "").trim();
|
|
1223
|
+
const val = this.parseValue(addrPart);
|
|
1224
|
+
if (val !== null) {
|
|
1225
|
+
if (val <= 0xff && opcodes.ZPX) return { mode: "ZPX", value: val };
|
|
1226
|
+
if (opcodes.ABX) return { mode: "ABX", value: val & 0xffff };
|
|
1227
|
+
}
|
|
1228
|
+
return null;
|
|
1229
|
+
}
|
|
1230
|
+
if (operand.match(/,\s*Y$/i)) {
|
|
1231
|
+
const addrPart = operand.replace(/,\s*Y$/i, "").trim();
|
|
1232
|
+
const val = this.parseValue(addrPart);
|
|
1233
|
+
if (val !== null) {
|
|
1234
|
+
if (val <= 0xff && opcodes.ZPY) return { mode: "ZPY", value: val };
|
|
1235
|
+
if (opcodes.ABY) return { mode: "ABY", value: val & 0xffff };
|
|
1236
|
+
}
|
|
1237
|
+
return null;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// Accumulator mode (A or empty for shift/rotate)
|
|
1241
|
+
if (operand.toUpperCase() === "A" && opcodes.ACC) {
|
|
1242
|
+
return { mode: "ACC", value: null };
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// Branch relative - just parse the target, we'll show ?? for offset
|
|
1246
|
+
if (opcodes.REL) {
|
|
1247
|
+
const val = this.parseValue(operand);
|
|
1248
|
+
// For branches, we can't calculate offset without knowing current PC
|
|
1249
|
+
// Just return the mode with the target value
|
|
1250
|
+
return { mode: "REL", value: val };
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// Plain address - zero page or absolute
|
|
1254
|
+
const val = this.parseValue(operand);
|
|
1255
|
+
if (val !== null) {
|
|
1256
|
+
if (val <= 0xff && opcodes.ZP) return { mode: "ZP", value: val };
|
|
1257
|
+
if (opcodes.ABS) return { mode: "ABS", value: val & 0xffff };
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
return null; // Unresolved
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* Parse a value or expression. Supports arithmetic (+, -, *, /),
|
|
1265
|
+
* byte selectors (< >), current PC (*), and nested parentheses.
|
|
1266
|
+
*/
|
|
1267
|
+
parseValue(str) {
|
|
1268
|
+
if (!str) return null;
|
|
1269
|
+
str = str.trim();
|
|
1270
|
+
if (!str) return null;
|
|
1271
|
+
this._exprPos = 0;
|
|
1272
|
+
this._exprStr = str;
|
|
1273
|
+
const val = this._exprAddSub();
|
|
1274
|
+
return val;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
_exprSkipSpaces() {
|
|
1278
|
+
while (
|
|
1279
|
+
this._exprPos < this._exprStr.length &&
|
|
1280
|
+
this._exprStr[this._exprPos] === " "
|
|
1281
|
+
) {
|
|
1282
|
+
this._exprPos++;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
_exprPeek() {
|
|
1287
|
+
this._exprSkipSpaces();
|
|
1288
|
+
return this._exprPos < this._exprStr.length
|
|
1289
|
+
? this._exprStr[this._exprPos]
|
|
1290
|
+
: null;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
_exprAddSub() {
|
|
1294
|
+
let val = this._exprMulDiv();
|
|
1295
|
+
if (val === null) return null;
|
|
1296
|
+
while (true) {
|
|
1297
|
+
const ch = this._exprPeek();
|
|
1298
|
+
if (ch === "+") {
|
|
1299
|
+
this._exprPos++;
|
|
1300
|
+
const right = this._exprMulDiv();
|
|
1301
|
+
if (right === null) return null;
|
|
1302
|
+
val = val + right;
|
|
1303
|
+
} else if (ch === "-") {
|
|
1304
|
+
this._exprPos++;
|
|
1305
|
+
const right = this._exprMulDiv();
|
|
1306
|
+
if (right === null) return null;
|
|
1307
|
+
val = val - right;
|
|
1308
|
+
} else {
|
|
1309
|
+
break;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
return val;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
_exprMulDiv() {
|
|
1316
|
+
let val = this._exprUnary();
|
|
1317
|
+
if (val === null) return null;
|
|
1318
|
+
while (true) {
|
|
1319
|
+
const ch = this._exprPeek();
|
|
1320
|
+
// * here is always multiply — PC reference is handled in _exprPrimary
|
|
1321
|
+
if (ch === "*") {
|
|
1322
|
+
this._exprPos++;
|
|
1323
|
+
const right = this._exprUnary();
|
|
1324
|
+
if (right === null) return null;
|
|
1325
|
+
val = val * right;
|
|
1326
|
+
} else if (ch === "/") {
|
|
1327
|
+
this._exprPos++;
|
|
1328
|
+
const right = this._exprUnary();
|
|
1329
|
+
if (right === null) return null;
|
|
1330
|
+
val = right !== 0 ? Math.trunc(val / right) : 0;
|
|
1331
|
+
} else {
|
|
1332
|
+
break;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
return val;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
_exprUnary() {
|
|
1339
|
+
const ch = this._exprPeek();
|
|
1340
|
+
if (ch === "<") {
|
|
1341
|
+
this._exprPos++;
|
|
1342
|
+
const val = this._exprUnary();
|
|
1343
|
+
return val !== null ? val & 0xff : null;
|
|
1344
|
+
}
|
|
1345
|
+
if (ch === ">") {
|
|
1346
|
+
this._exprPos++;
|
|
1347
|
+
const val = this._exprUnary();
|
|
1348
|
+
return val !== null ? (val >> 8) & 0xff : null;
|
|
1349
|
+
}
|
|
1350
|
+
if (ch === "-") {
|
|
1351
|
+
this._exprPos++;
|
|
1352
|
+
const val = this._exprUnary();
|
|
1353
|
+
return val !== null ? -val : null;
|
|
1354
|
+
}
|
|
1355
|
+
return this._exprPrimary();
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
_exprPrimary() {
|
|
1359
|
+
this._exprSkipSpaces();
|
|
1360
|
+
if (this._exprPos >= this._exprStr.length) return null;
|
|
1361
|
+
const ch = this._exprStr[this._exprPos];
|
|
1362
|
+
|
|
1363
|
+
// Parenthesized sub-expression
|
|
1364
|
+
if (ch === "(") {
|
|
1365
|
+
this._exprPos++;
|
|
1366
|
+
const val = this._exprAddSub();
|
|
1367
|
+
this._exprSkipSpaces();
|
|
1368
|
+
if (
|
|
1369
|
+
this._exprPos < this._exprStr.length &&
|
|
1370
|
+
this._exprStr[this._exprPos] === ")"
|
|
1371
|
+
) {
|
|
1372
|
+
this._exprPos++;
|
|
1373
|
+
}
|
|
1374
|
+
return val;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// Current PC: *
|
|
1378
|
+
if (ch === "*") {
|
|
1379
|
+
this._exprPos++;
|
|
1380
|
+
return this.currentPC !== undefined ? this.currentPC : null;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// Hex: $xxxx
|
|
1384
|
+
if (ch === "$") {
|
|
1385
|
+
this._exprPos++;
|
|
1386
|
+
let start = this._exprPos;
|
|
1387
|
+
while (
|
|
1388
|
+
this._exprPos < this._exprStr.length &&
|
|
1389
|
+
/[0-9A-Fa-f]/.test(this._exprStr[this._exprPos])
|
|
1390
|
+
) {
|
|
1391
|
+
this._exprPos++;
|
|
1392
|
+
}
|
|
1393
|
+
if (this._exprPos === start) return null;
|
|
1394
|
+
return parseInt(this._exprStr.substring(start, this._exprPos), 16);
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// Binary: %01010101
|
|
1398
|
+
if (ch === "%") {
|
|
1399
|
+
this._exprPos++;
|
|
1400
|
+
let start = this._exprPos;
|
|
1401
|
+
while (
|
|
1402
|
+
this._exprPos < this._exprStr.length &&
|
|
1403
|
+
/[01]/.test(this._exprStr[this._exprPos])
|
|
1404
|
+
) {
|
|
1405
|
+
this._exprPos++;
|
|
1406
|
+
}
|
|
1407
|
+
if (this._exprPos === start) return null;
|
|
1408
|
+
return parseInt(this._exprStr.substring(start, this._exprPos), 2);
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
// Character literal: 'A'
|
|
1412
|
+
if (ch === "'") {
|
|
1413
|
+
this._exprPos++;
|
|
1414
|
+
if (this._exprPos >= this._exprStr.length) return null;
|
|
1415
|
+
const val = this._exprStr.charCodeAt(this._exprPos);
|
|
1416
|
+
this._exprPos++;
|
|
1417
|
+
if (
|
|
1418
|
+
this._exprPos < this._exprStr.length &&
|
|
1419
|
+
this._exprStr[this._exprPos] === "'"
|
|
1420
|
+
) {
|
|
1421
|
+
this._exprPos++;
|
|
1422
|
+
}
|
|
1423
|
+
return val;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// Decimal number
|
|
1427
|
+
if (/[0-9]/.test(ch)) {
|
|
1428
|
+
let start = this._exprPos;
|
|
1429
|
+
while (
|
|
1430
|
+
this._exprPos < this._exprStr.length &&
|
|
1431
|
+
/[0-9]/.test(this._exprStr[this._exprPos])
|
|
1432
|
+
) {
|
|
1433
|
+
this._exprPos++;
|
|
1434
|
+
}
|
|
1435
|
+
return parseInt(this._exprStr.substring(start, this._exprPos), 10);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
// Symbol / label
|
|
1439
|
+
if (/[A-Za-z_:\]]/.test(ch)) {
|
|
1440
|
+
let start = this._exprPos;
|
|
1441
|
+
while (
|
|
1442
|
+
this._exprPos < this._exprStr.length &&
|
|
1443
|
+
/[A-Za-z0-9_:\]]/.test(this._exprStr[this._exprPos])
|
|
1444
|
+
) {
|
|
1445
|
+
this._exprPos++;
|
|
1446
|
+
}
|
|
1447
|
+
const name = this._exprStr.substring(start, this._exprPos).toUpperCase();
|
|
1448
|
+
if (this.symbols.has(name)) {
|
|
1449
|
+
return this.symbols.get(name);
|
|
1450
|
+
}
|
|
1451
|
+
return null; // Unresolved symbol
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
return null;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* Encode a line and store the bytes
|
|
1459
|
+
*/
|
|
1460
|
+
encodeLineBytes(lineNumber) {
|
|
1461
|
+
if (!this.textarea) return;
|
|
1462
|
+
|
|
1463
|
+
const lines = this.textarea.value.split("\n");
|
|
1464
|
+
if (lineNumber < 1 || lineNumber > lines.length) return;
|
|
1465
|
+
|
|
1466
|
+
const line = lines[lineNumber - 1];
|
|
1467
|
+
const parsed = this.parseLine(line);
|
|
1468
|
+
|
|
1469
|
+
if (!parsed || !parsed.opcode) {
|
|
1470
|
+
this.lineBytes.delete(lineNumber);
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
const mnemonic = parsed.opcode.toUpperCase();
|
|
1475
|
+
const opcodes = this.getOpcodeTable()[mnemonic];
|
|
1476
|
+
|
|
1477
|
+
// Skip directives - they don't have opcodes in our table
|
|
1478
|
+
if (!opcodes) {
|
|
1479
|
+
this.lineBytes.delete(lineNumber);
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
const operandInfo = this.parseOperand(parsed.operand, mnemonic);
|
|
1484
|
+
if (!operandInfo) {
|
|
1485
|
+
this.lineBytes.delete(lineNumber);
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
const opcode = opcodes[operandInfo.mode];
|
|
1490
|
+
if (opcode === undefined) {
|
|
1491
|
+
this.lineBytes.delete(lineNumber);
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
// Build the byte string
|
|
1496
|
+
let bytes = opcode.toString(16).toUpperCase().padStart(2, "0");
|
|
1497
|
+
|
|
1498
|
+
if (operandInfo.mode === "IMP" || operandInfo.mode === "ACC") {
|
|
1499
|
+
// 1 byte - just the opcode
|
|
1500
|
+
} else if (operandInfo.mode === "REL") {
|
|
1501
|
+
// Branch - calculate relative offset if we know target and current PC
|
|
1502
|
+
const targetAddr = operandInfo.value;
|
|
1503
|
+
const currentPC = this.linePCs?.get(lineNumber);
|
|
1504
|
+
|
|
1505
|
+
if (targetAddr !== null && currentPC !== undefined) {
|
|
1506
|
+
// Branch offset is relative to PC after the instruction (PC + 2)
|
|
1507
|
+
const nextPC = currentPC + 2;
|
|
1508
|
+
const offset = targetAddr - nextPC;
|
|
1509
|
+
|
|
1510
|
+
// Check if offset is in valid range (-128 to +127)
|
|
1511
|
+
if (offset >= -128 && offset <= 127) {
|
|
1512
|
+
const signedByte = offset < 0 ? 256 + offset : offset;
|
|
1513
|
+
bytes += " " + signedByte.toString(16).toUpperCase().padStart(2, "0");
|
|
1514
|
+
} else {
|
|
1515
|
+
bytes += " ??"; // Out of range
|
|
1516
|
+
}
|
|
1517
|
+
} else {
|
|
1518
|
+
bytes += " ??"; // Unknown target
|
|
1519
|
+
}
|
|
1520
|
+
} else if (
|
|
1521
|
+
["IMM", "ZP", "ZPX", "ZPY", "IZX", "IZY", "ZPI"].includes(
|
|
1522
|
+
operandInfo.mode,
|
|
1523
|
+
)
|
|
1524
|
+
) {
|
|
1525
|
+
// 2 bytes
|
|
1526
|
+
if (operandInfo.value !== null) {
|
|
1527
|
+
bytes +=
|
|
1528
|
+
" " +
|
|
1529
|
+
(operandInfo.value & 0xff)
|
|
1530
|
+
.toString(16)
|
|
1531
|
+
.toUpperCase()
|
|
1532
|
+
.padStart(2, "0");
|
|
1533
|
+
} else {
|
|
1534
|
+
bytes += " ??";
|
|
1535
|
+
}
|
|
1536
|
+
} else if (["ABS", "ABX", "ABY", "IND", "IAX"].includes(operandInfo.mode)) {
|
|
1537
|
+
// 3 bytes (little-endian)
|
|
1538
|
+
if (operandInfo.value !== null) {
|
|
1539
|
+
const lo = operandInfo.value & 0xff;
|
|
1540
|
+
const hi = (operandInfo.value >> 8) & 0xff;
|
|
1541
|
+
bytes += " " + lo.toString(16).toUpperCase().padStart(2, "0");
|
|
1542
|
+
bytes += " " + hi.toString(16).toUpperCase().padStart(2, "0");
|
|
1543
|
+
} else {
|
|
1544
|
+
bytes += " ?? ??";
|
|
1545
|
+
}
|
|
1546
|
+
} else if (operandInfo.mode === "ZPR") {
|
|
1547
|
+
// 3 bytes: opcode, zp addr, relative offset
|
|
1548
|
+
bytes += " ?? ??";
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
this.lineBytes.set(lineNumber, bytes);
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
/**
|
|
1555
|
+
* Encode all lines (called after successful assembly)
|
|
1556
|
+
*/
|
|
1557
|
+
encodeAllLineBytes() {
|
|
1558
|
+
this.lineBytes.clear();
|
|
1559
|
+
this.linePCs = new Map(); // Track PC for each line
|
|
1560
|
+
|
|
1561
|
+
const lines = this.textarea.value.split("\n");
|
|
1562
|
+
|
|
1563
|
+
// Find ORG from source code, default to $0800 if not found yet
|
|
1564
|
+
let pc = 0x0800;
|
|
1565
|
+
|
|
1566
|
+
// First pass: calculate PC and collect labels/EQU values
|
|
1567
|
+
const localSymbols = new Map();
|
|
1568
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1569
|
+
const lineNumber = i + 1;
|
|
1570
|
+
const parsed = this.parseLine(lines[i]);
|
|
1571
|
+
|
|
1572
|
+
if (parsed && parsed.opcode) {
|
|
1573
|
+
const mnem = parsed.opcode.toUpperCase();
|
|
1574
|
+
|
|
1575
|
+
// Check for ORG directive and update PC
|
|
1576
|
+
if (mnem === "ORG") {
|
|
1577
|
+
this.currentPC = pc;
|
|
1578
|
+
const orgValue = this.parseValue(parsed.operand);
|
|
1579
|
+
if (orgValue !== null) {
|
|
1580
|
+
pc = orgValue;
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// Collect label addresses and EQU values
|
|
1585
|
+
if (parsed.label) {
|
|
1586
|
+
const labelUpper = parsed.label.toUpperCase();
|
|
1587
|
+
if (mnem === "EQU") {
|
|
1588
|
+
this.currentPC = pc;
|
|
1589
|
+
const val = this.parseValue(parsed.operand);
|
|
1590
|
+
if (val !== null) {
|
|
1591
|
+
localSymbols.set(labelUpper, val);
|
|
1592
|
+
this.symbols.set(labelUpper, val);
|
|
1593
|
+
}
|
|
1594
|
+
} else {
|
|
1595
|
+
localSymbols.set(labelUpper, pc);
|
|
1596
|
+
this.symbols.set(labelUpper, pc);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
} else if (parsed && parsed.label) {
|
|
1600
|
+
// Label-only line (no opcode)
|
|
1601
|
+
const labelUpper = parsed.label.toUpperCase();
|
|
1602
|
+
localSymbols.set(labelUpper, pc);
|
|
1603
|
+
this.symbols.set(labelUpper, pc);
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
this.linePCs.set(lineNumber, pc);
|
|
1607
|
+
|
|
1608
|
+
if (parsed && parsed.opcode) {
|
|
1609
|
+
const size = this.getInstructionSize(
|
|
1610
|
+
parsed.opcode.toUpperCase(),
|
|
1611
|
+
parsed.operand,
|
|
1612
|
+
);
|
|
1613
|
+
pc += size;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// Second pass: re-evaluate EQU values now that all labels are known
|
|
1618
|
+
pc = 0x0800;
|
|
1619
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1620
|
+
const parsed = this.parseLine(lines[i]);
|
|
1621
|
+
if (parsed && parsed.opcode) {
|
|
1622
|
+
const mnem = parsed.opcode.toUpperCase();
|
|
1623
|
+
if (mnem === "ORG") {
|
|
1624
|
+
this.currentPC = pc;
|
|
1625
|
+
const orgValue = this.parseValue(parsed.operand);
|
|
1626
|
+
if (orgValue !== null) pc = orgValue;
|
|
1627
|
+
}
|
|
1628
|
+
if (parsed.label && mnem === "EQU") {
|
|
1629
|
+
this.currentPC = pc;
|
|
1630
|
+
const val = this.parseValue(parsed.operand);
|
|
1631
|
+
if (val !== null) {
|
|
1632
|
+
this.symbols.set(parsed.label.toUpperCase(), val);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
this.currentPC = this.linePCs.get(i + 1);
|
|
1637
|
+
if (parsed && parsed.opcode) {
|
|
1638
|
+
const size = this.getInstructionSize(
|
|
1639
|
+
parsed.opcode.toUpperCase(),
|
|
1640
|
+
parsed.operand,
|
|
1641
|
+
);
|
|
1642
|
+
pc += size;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
// Third pass: encode with known PCs and symbols
|
|
1647
|
+
for (let i = 1; i <= lines.length; i++) {
|
|
1648
|
+
this.currentPC = this.linePCs.get(i);
|
|
1649
|
+
this.encodeLineBytes(i);
|
|
1650
|
+
}
|
|
1651
|
+
this.currentPC = undefined;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
/**
|
|
1655
|
+
* Get the size of an instruction in bytes
|
|
1656
|
+
*/
|
|
1657
|
+
getInstructionSize(mnemonic, operand) {
|
|
1658
|
+
const opcodes = this.getOpcodeTable()[mnemonic];
|
|
1659
|
+
if (!opcodes) {
|
|
1660
|
+
// Check if it's a directive
|
|
1661
|
+
const upper = mnemonic.toUpperCase();
|
|
1662
|
+
if (upper === "ORG" || upper === "EQU") return 0;
|
|
1663
|
+
if (upper === "DFB" || upper === "DB") {
|
|
1664
|
+
// Count comma-separated values
|
|
1665
|
+
if (!operand) return 1;
|
|
1666
|
+
return operand.split(",").length;
|
|
1667
|
+
}
|
|
1668
|
+
if (upper === "DW" || upper === "DA") {
|
|
1669
|
+
if (!operand) return 2;
|
|
1670
|
+
return operand.split(",").length * 2;
|
|
1671
|
+
}
|
|
1672
|
+
if (upper === "ASC" || upper === "DCI") {
|
|
1673
|
+
// String length (rough estimate)
|
|
1674
|
+
const match = operand?.match(/["']([^"']*)["']/);
|
|
1675
|
+
if (match) return match[1].length;
|
|
1676
|
+
return 0;
|
|
1677
|
+
}
|
|
1678
|
+
if (upper === "DS") {
|
|
1679
|
+
const val = this.parseValue(operand);
|
|
1680
|
+
return val || 0;
|
|
1681
|
+
}
|
|
1682
|
+
if (upper === "HEX") {
|
|
1683
|
+
// Count hex digits / 2
|
|
1684
|
+
const hex = operand?.replace(/[^0-9A-Fa-f]/g, "") || "";
|
|
1685
|
+
return Math.floor(hex.length / 2);
|
|
1686
|
+
}
|
|
1687
|
+
return 0;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
const operandInfo = this.parseOperand(operand, mnemonic);
|
|
1691
|
+
if (!operandInfo) return 0;
|
|
1692
|
+
|
|
1693
|
+
// Size based on addressing mode
|
|
1694
|
+
switch (operandInfo.mode) {
|
|
1695
|
+
case "IMP":
|
|
1696
|
+
case "ACC":
|
|
1697
|
+
return 1;
|
|
1698
|
+
case "IMM":
|
|
1699
|
+
case "ZP":
|
|
1700
|
+
case "ZPX":
|
|
1701
|
+
case "ZPY":
|
|
1702
|
+
case "IZX":
|
|
1703
|
+
case "IZY":
|
|
1704
|
+
case "ZPI":
|
|
1705
|
+
case "REL":
|
|
1706
|
+
return 2;
|
|
1707
|
+
case "ABS":
|
|
1708
|
+
case "ABX":
|
|
1709
|
+
case "ABY":
|
|
1710
|
+
case "IND":
|
|
1711
|
+
case "IAX":
|
|
1712
|
+
case "ZPR":
|
|
1713
|
+
return 3;
|
|
1714
|
+
default:
|
|
1715
|
+
return 1;
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
/**
|
|
1720
|
+
* Validate a line for syntax errors (stray characters, malformed syntax)
|
|
1721
|
+
*/
|
|
1722
|
+
validateLine(lineNumber) {
|
|
1723
|
+
if (!this.textarea) return;
|
|
1724
|
+
|
|
1725
|
+
const lines = this.textarea.value.split("\n");
|
|
1726
|
+
if (lineNumber < 1 || lineNumber > lines.length) return;
|
|
1727
|
+
|
|
1728
|
+
const line = lines[lineNumber - 1];
|
|
1729
|
+
|
|
1730
|
+
// Clear any previous syntax error for this line
|
|
1731
|
+
this.syntaxErrors.delete(lineNumber);
|
|
1732
|
+
|
|
1733
|
+
// Empty lines are valid
|
|
1734
|
+
if (!line.trim()) return;
|
|
1735
|
+
|
|
1736
|
+
// Full-line comments are valid
|
|
1737
|
+
const trimmed = line.trim();
|
|
1738
|
+
if (trimmed.startsWith(";") || trimmed.startsWith("*")) return;
|
|
1739
|
+
|
|
1740
|
+
// Parse the line to validate structure
|
|
1741
|
+
const error = this.checkLineSyntax(line);
|
|
1742
|
+
if (error) {
|
|
1743
|
+
this.syntaxErrors.set(lineNumber, error);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
/**
|
|
1748
|
+
* Check line syntax and return error message or null if valid
|
|
1749
|
+
*/
|
|
1750
|
+
checkLineSyntax(line) {
|
|
1751
|
+
// Extract comment first (respecting quotes)
|
|
1752
|
+
let commentIdx = -1;
|
|
1753
|
+
let inQuote = false;
|
|
1754
|
+
let quoteChar = null;
|
|
1755
|
+
for (let i = 0; i < line.length; i++) {
|
|
1756
|
+
const ch = line[i];
|
|
1757
|
+
if ((ch === '"' || ch === "'") && !inQuote) {
|
|
1758
|
+
inQuote = true;
|
|
1759
|
+
quoteChar = ch;
|
|
1760
|
+
} else if (ch === quoteChar && inQuote) {
|
|
1761
|
+
inQuote = false;
|
|
1762
|
+
quoteChar = null;
|
|
1763
|
+
} else if (ch === ";" && !inQuote) {
|
|
1764
|
+
commentIdx = i;
|
|
1765
|
+
break;
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
const mainPart = commentIdx >= 0 ? line.substring(0, commentIdx) : line;
|
|
1770
|
+
|
|
1771
|
+
// If line is just whitespace before comment, that's valid
|
|
1772
|
+
if (!mainPart.trim()) return null;
|
|
1773
|
+
|
|
1774
|
+
// Check if line starts with whitespace (no label)
|
|
1775
|
+
const hasLabel =
|
|
1776
|
+
mainPart.length > 0 && mainPart[0] !== " " && mainPart[0] !== "\t";
|
|
1777
|
+
|
|
1778
|
+
// Tokenize the main part
|
|
1779
|
+
const tokens = [];
|
|
1780
|
+
let current = "";
|
|
1781
|
+
let inStr = false;
|
|
1782
|
+
let strChar = null;
|
|
1783
|
+
|
|
1784
|
+
for (let i = 0; i < mainPart.length; i++) {
|
|
1785
|
+
const ch = mainPart[i];
|
|
1786
|
+
|
|
1787
|
+
if ((ch === '"' || ch === "'") && !inStr) {
|
|
1788
|
+
inStr = true;
|
|
1789
|
+
strChar = ch;
|
|
1790
|
+
current += ch;
|
|
1791
|
+
} else if (ch === strChar && inStr) {
|
|
1792
|
+
inStr = false;
|
|
1793
|
+
strChar = null;
|
|
1794
|
+
current += ch;
|
|
1795
|
+
} else if ((ch === " " || ch === "\t") && !inStr) {
|
|
1796
|
+
if (current) {
|
|
1797
|
+
tokens.push({ text: current, pos: i - current.length });
|
|
1798
|
+
current = "";
|
|
1799
|
+
}
|
|
1800
|
+
} else {
|
|
1801
|
+
current += ch;
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
if (current) {
|
|
1805
|
+
tokens.push({ text: current, pos: mainPart.length - current.length });
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
if (tokens.length === 0) return null;
|
|
1809
|
+
|
|
1810
|
+
// Validate token count based on whether there's a label
|
|
1811
|
+
// Valid patterns:
|
|
1812
|
+
// - Label only: 1 token at col 0
|
|
1813
|
+
// - Opcode only: 1 token not at col 0
|
|
1814
|
+
// - Label + Opcode: 2 tokens, first at col 0
|
|
1815
|
+
// - Opcode + Operand: 2 tokens, first not at col 0
|
|
1816
|
+
// - Label + Opcode + Operand: 3 tokens, first at col 0
|
|
1817
|
+
|
|
1818
|
+
const firstAtCol0 = tokens[0].pos === 0;
|
|
1819
|
+
|
|
1820
|
+
if (hasLabel) {
|
|
1821
|
+
// First token is label
|
|
1822
|
+
if (tokens.length === 1) {
|
|
1823
|
+
// Just a label - valid
|
|
1824
|
+
return null;
|
|
1825
|
+
} else if (tokens.length === 2) {
|
|
1826
|
+
// Label + opcode OR label + something invalid
|
|
1827
|
+
if (!this.isValidOpcode(tokens[1].text)) {
|
|
1828
|
+
return `Unknown mnemonic or directive: ${tokens[1].text}`;
|
|
1829
|
+
}
|
|
1830
|
+
return null;
|
|
1831
|
+
} else if (tokens.length === 3) {
|
|
1832
|
+
// Label + opcode + operand
|
|
1833
|
+
if (!this.isValidOpcode(tokens[1].text)) {
|
|
1834
|
+
return `Unknown mnemonic or directive: ${tokens[1].text}`;
|
|
1835
|
+
}
|
|
1836
|
+
// Validate operand for invalid numbers
|
|
1837
|
+
const operandError = this.validateOperandNumbers(
|
|
1838
|
+
tokens[1].text,
|
|
1839
|
+
tokens[2].text,
|
|
1840
|
+
);
|
|
1841
|
+
if (operandError) return operandError;
|
|
1842
|
+
return null;
|
|
1843
|
+
} else if (tokens.length > 3) {
|
|
1844
|
+
// Too many tokens - find the extra one
|
|
1845
|
+
const extra = tokens
|
|
1846
|
+
.slice(3)
|
|
1847
|
+
.map((t) => t.text)
|
|
1848
|
+
.join(" ");
|
|
1849
|
+
return `Unexpected: ${extra}`;
|
|
1850
|
+
}
|
|
1851
|
+
} else {
|
|
1852
|
+
// No label - first token should be opcode
|
|
1853
|
+
if (tokens.length === 1) {
|
|
1854
|
+
// Just opcode
|
|
1855
|
+
if (!this.isValidOpcode(tokens[0].text)) {
|
|
1856
|
+
return `Unknown mnemonic or directive: ${tokens[0].text}`;
|
|
1857
|
+
}
|
|
1858
|
+
return null;
|
|
1859
|
+
} else if (tokens.length === 2) {
|
|
1860
|
+
// Opcode + operand
|
|
1861
|
+
if (!this.isValidOpcode(tokens[0].text)) {
|
|
1862
|
+
return `Unknown mnemonic or directive: ${tokens[0].text}`;
|
|
1863
|
+
}
|
|
1864
|
+
// Validate operand for invalid numbers
|
|
1865
|
+
const operandError = this.validateOperandNumbers(
|
|
1866
|
+
tokens[0].text,
|
|
1867
|
+
tokens[1].text,
|
|
1868
|
+
);
|
|
1869
|
+
if (operandError) return operandError;
|
|
1870
|
+
return null;
|
|
1871
|
+
} else if (tokens.length > 2) {
|
|
1872
|
+
// Too many tokens
|
|
1873
|
+
const extra = tokens
|
|
1874
|
+
.slice(2)
|
|
1875
|
+
.map((t) => t.text)
|
|
1876
|
+
.join(" ");
|
|
1877
|
+
return `Unexpected: ${extra}`;
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
return null;
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
/**
|
|
1885
|
+
* Validate numeric literals in an operand
|
|
1886
|
+
* Returns error message or null if valid
|
|
1887
|
+
*/
|
|
1888
|
+
validateOperandNumbers(opcode, operand) {
|
|
1889
|
+
// Skip string literals
|
|
1890
|
+
if (operand.startsWith('"') || operand.startsWith("'")) {
|
|
1891
|
+
return null;
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
const upperOpcode = opcode.toUpperCase();
|
|
1895
|
+
|
|
1896
|
+
// Directives that expect 8-bit values
|
|
1897
|
+
const byteDirectives = new Set(["DFB", "DB"]);
|
|
1898
|
+
|
|
1899
|
+
// Check if this is immediate mode (starts with #)
|
|
1900
|
+
const isImmediate = operand.startsWith("#");
|
|
1901
|
+
|
|
1902
|
+
// Determine max value based on context
|
|
1903
|
+
// Immediate mode and byte directives expect 8-bit values
|
|
1904
|
+
const expects8Bit = isImmediate || byteDirectives.has(upperOpcode);
|
|
1905
|
+
|
|
1906
|
+
// Find all hex numbers ($xxxx) and validate them
|
|
1907
|
+
const hexPattern = /\$([A-Za-z0-9]+)/g;
|
|
1908
|
+
let match;
|
|
1909
|
+
while ((match = hexPattern.exec(operand)) !== null) {
|
|
1910
|
+
const hexDigits = match[1];
|
|
1911
|
+
if (!/^[0-9A-Fa-f]+$/.test(hexDigits)) {
|
|
1912
|
+
return `Invalid hex number: $${hexDigits}`;
|
|
1913
|
+
}
|
|
1914
|
+
// Check value range
|
|
1915
|
+
const value = parseInt(hexDigits, 16);
|
|
1916
|
+
if (value > 0xffff) {
|
|
1917
|
+
return `Value exceeds 16-bit maximum: $${hexDigits}`;
|
|
1918
|
+
}
|
|
1919
|
+
if (expects8Bit && value > 0xff) {
|
|
1920
|
+
return `Value exceeds 8-bit maximum: $${hexDigits}`;
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
// Find all binary numbers (%xxxx) and validate them
|
|
1925
|
+
const binPattern = /%([A-Za-z0-9]+)/g;
|
|
1926
|
+
while ((match = binPattern.exec(operand)) !== null) {
|
|
1927
|
+
const binDigits = match[1];
|
|
1928
|
+
if (!/^[01]+$/.test(binDigits)) {
|
|
1929
|
+
return `Invalid binary number: %${binDigits}`;
|
|
1930
|
+
}
|
|
1931
|
+
// Check value range
|
|
1932
|
+
const value = parseInt(binDigits, 2);
|
|
1933
|
+
if (value > 0xffff) {
|
|
1934
|
+
return `Value exceeds 16-bit maximum: %${binDigits}`;
|
|
1935
|
+
}
|
|
1936
|
+
if (expects8Bit && value > 0xff) {
|
|
1937
|
+
return `Value exceeds 8-bit maximum: %${binDigits}`;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
// Find decimal numbers (digits not preceded by $ or %)
|
|
1942
|
+
const decPattern = /(?<![A-Za-z0-9$%])(\d+)(?![A-Za-z])/g;
|
|
1943
|
+
while ((match = decPattern.exec(operand)) !== null) {
|
|
1944
|
+
const decDigits = match[1];
|
|
1945
|
+
const value = parseInt(decDigits, 10);
|
|
1946
|
+
if (value > 0xffff) {
|
|
1947
|
+
return `Value exceeds 16-bit maximum: ${decDigits}`;
|
|
1948
|
+
}
|
|
1949
|
+
if (expects8Bit && value > 0xff) {
|
|
1950
|
+
return `Value exceeds 8-bit maximum: ${decDigits}`;
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
return null;
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
/**
|
|
1958
|
+
* Check if a token is a valid opcode/mnemonic or directive
|
|
1959
|
+
*/
|
|
1960
|
+
isValidOpcode(token) {
|
|
1961
|
+
const upper = token.toUpperCase();
|
|
1962
|
+
|
|
1963
|
+
// Check against opcode table
|
|
1964
|
+
if (this.getOpcodeTable()[upper]) return true;
|
|
1965
|
+
|
|
1966
|
+
// Check common directives
|
|
1967
|
+
const directives = new Set([
|
|
1968
|
+
"ORG",
|
|
1969
|
+
"EQU",
|
|
1970
|
+
"DS",
|
|
1971
|
+
"DFB",
|
|
1972
|
+
"DB",
|
|
1973
|
+
"DW",
|
|
1974
|
+
"DA",
|
|
1975
|
+
"DDB",
|
|
1976
|
+
"ASC",
|
|
1977
|
+
"DCI",
|
|
1978
|
+
"HEX",
|
|
1979
|
+
"PUT",
|
|
1980
|
+
"USE",
|
|
1981
|
+
"OBJ",
|
|
1982
|
+
"LST",
|
|
1983
|
+
"DO",
|
|
1984
|
+
"ELSE",
|
|
1985
|
+
"FIN",
|
|
1986
|
+
"LUP",
|
|
1987
|
+
"--^",
|
|
1988
|
+
"REL",
|
|
1989
|
+
"TYP",
|
|
1990
|
+
"SAV",
|
|
1991
|
+
"DSK",
|
|
1992
|
+
"CHN",
|
|
1993
|
+
"ENT",
|
|
1994
|
+
"EXT",
|
|
1995
|
+
"DUM",
|
|
1996
|
+
"DEND",
|
|
1997
|
+
"ERR",
|
|
1998
|
+
"CYC",
|
|
1999
|
+
"DAT",
|
|
2000
|
+
"EXP",
|
|
2001
|
+
"PAU",
|
|
2002
|
+
"SW",
|
|
2003
|
+
"USR",
|
|
2004
|
+
"XC",
|
|
2005
|
+
"MX",
|
|
2006
|
+
"TR",
|
|
2007
|
+
"KBD",
|
|
2008
|
+
"PMC",
|
|
2009
|
+
"PAG",
|
|
2010
|
+
"TTL",
|
|
2011
|
+
"SKP",
|
|
2012
|
+
"CHK",
|
|
2013
|
+
"IF",
|
|
2014
|
+
"ELUP",
|
|
2015
|
+
"END",
|
|
2016
|
+
"MAC",
|
|
2017
|
+
"EOM",
|
|
2018
|
+
"<<<",
|
|
2019
|
+
"ADR",
|
|
2020
|
+
"ADRL",
|
|
2021
|
+
"LNK",
|
|
2022
|
+
"STR",
|
|
2023
|
+
"STRL",
|
|
2024
|
+
"REV",
|
|
2025
|
+
]);
|
|
2026
|
+
|
|
2027
|
+
return directives.has(upper);
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
/**
|
|
2031
|
+
* Validate all lines
|
|
2032
|
+
*/
|
|
2033
|
+
validateAllLines() {
|
|
2034
|
+
this.syntaxErrors.clear();
|
|
2035
|
+
const lines = this.textarea.value.split("\n");
|
|
2036
|
+
|
|
2037
|
+
// First pass: collect all symbol definitions to detect duplicates
|
|
2038
|
+
const symbolDefs = new Map(); // symbol name -> first line number
|
|
2039
|
+
|
|
2040
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2041
|
+
const lineNumber = i + 1;
|
|
2042
|
+
const label = this.extractLabel(lines[i]);
|
|
2043
|
+
if (label) {
|
|
2044
|
+
const upperLabel = label.toUpperCase();
|
|
2045
|
+
if (symbolDefs.has(upperLabel)) {
|
|
2046
|
+
const firstLine = symbolDefs.get(upperLabel);
|
|
2047
|
+
this.syntaxErrors.set(
|
|
2048
|
+
lineNumber,
|
|
2049
|
+
`Duplicate symbol: ${label} (first defined on line ${firstLine})`,
|
|
2050
|
+
);
|
|
2051
|
+
} else {
|
|
2052
|
+
symbolDefs.set(upperLabel, lineNumber);
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
// Second pass: validate each line for other syntax errors
|
|
2058
|
+
for (let i = 1; i <= lines.length; i++) {
|
|
2059
|
+
// Skip lines that already have duplicate symbol errors
|
|
2060
|
+
if (!this.syntaxErrors.has(i)) {
|
|
2061
|
+
this.validateLine(i);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
/**
|
|
2067
|
+
* Extract label from a line (if present)
|
|
2068
|
+
* Returns null if no label
|
|
2069
|
+
*/
|
|
2070
|
+
extractLabel(line) {
|
|
2071
|
+
// No label if line starts with whitespace or is empty
|
|
2072
|
+
if (!line || line[0] === " " || line[0] === "\t") {
|
|
2073
|
+
return null;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
// Full-line comments have no label
|
|
2077
|
+
const trimmed = line.trim();
|
|
2078
|
+
if (trimmed.startsWith(";") || trimmed.startsWith("*")) {
|
|
2079
|
+
return null;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
// Extract first token (the label)
|
|
2083
|
+
let label = "";
|
|
2084
|
+
for (let i = 0; i < line.length; i++) {
|
|
2085
|
+
const ch = line[i];
|
|
2086
|
+
if (ch === " " || ch === "\t") {
|
|
2087
|
+
break;
|
|
2088
|
+
}
|
|
2089
|
+
label += ch;
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
return label || null;
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
updateHighlighting() {
|
|
2096
|
+
const text = this.textarea.value;
|
|
2097
|
+
const highlighted = highlightMerlinSourceInline(text);
|
|
2098
|
+
|
|
2099
|
+
// Just render the syntax highlighting - errors are shown via overlay
|
|
2100
|
+
this.highlight.innerHTML = highlighted + "\n";
|
|
2101
|
+
|
|
2102
|
+
// Update error highlights and messages overlay
|
|
2103
|
+
this.updateErrorsOverlay();
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
updateErrorsOverlay() {
|
|
2107
|
+
if (!this.errorsOverlay) return;
|
|
2108
|
+
|
|
2109
|
+
// Combine assembler errors and syntax errors
|
|
2110
|
+
const allErrors = new Map();
|
|
2111
|
+
for (const [lineNum, msg] of this.syntaxErrors) {
|
|
2112
|
+
allErrors.set(lineNum, msg);
|
|
2113
|
+
}
|
|
2114
|
+
// Assembler errors override syntax errors
|
|
2115
|
+
for (const [lineNum, msg] of this.errors) {
|
|
2116
|
+
allErrors.set(lineNum, msg);
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
if (allErrors.size === 0) {
|
|
2120
|
+
this.errorsOverlay.innerHTML = "";
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
const style = getComputedStyle(this.textarea);
|
|
2125
|
+
const lineHeight =
|
|
2126
|
+
parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.4;
|
|
2127
|
+
const paddingTop = parseFloat(style.paddingTop) || 8;
|
|
2128
|
+
|
|
2129
|
+
let html = "";
|
|
2130
|
+
for (const [lineNum, msg] of allErrors) {
|
|
2131
|
+
// Position at the top of the error line
|
|
2132
|
+
const top = paddingTop + (lineNum - 1) * lineHeight;
|
|
2133
|
+
// Center of the line for the message
|
|
2134
|
+
const centerY = top + lineHeight / 2;
|
|
2135
|
+
// Error highlight bar (background)
|
|
2136
|
+
html += `<div class="asm-error-highlight" style="top: ${top}px; height: ${lineHeight}px"></div>`;
|
|
2137
|
+
// Error message (right side, vertically centered)
|
|
2138
|
+
html += `<div class="asm-error-msg" style="top: ${centerY}px">${this.escapeHtml(msg)}</div>`;
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
this.errorsOverlay.innerHTML = html;
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
doAssemble() {
|
|
2145
|
+
// Format all lines before assembling
|
|
2146
|
+
this.formatAllLines();
|
|
2147
|
+
|
|
2148
|
+
const text = this.textarea.value;
|
|
2149
|
+
if (!text.trim()) {
|
|
2150
|
+
this.setStatus("Nothing to assemble", false);
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
// Check if source has ORG directive before any code
|
|
2155
|
+
const hasOrg = text.match(/^\s*ORG\b/im);
|
|
2156
|
+
if (!hasOrg) {
|
|
2157
|
+
this.setStatus("ORG directive required before code", false);
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
// Clear previous errors
|
|
2162
|
+
this.errors.clear();
|
|
2163
|
+
this.syntaxErrors.clear();
|
|
2164
|
+
|
|
2165
|
+
// Validate all lines for syntax errors before assembly
|
|
2166
|
+
this.validateAllLines();
|
|
2167
|
+
|
|
2168
|
+
// If there are syntax errors, don't proceed with assembly
|
|
2169
|
+
if (this.syntaxErrors.size > 0) {
|
|
2170
|
+
const count = this.syntaxErrors.size;
|
|
2171
|
+
this.setStatus(`${count} syntax error${count !== 1 ? "s" : ""}`, false);
|
|
2172
|
+
this.loadBtn.disabled = true;
|
|
2173
|
+
this.debugBtn.disabled = true;
|
|
2174
|
+
this.clearOutputPanels();
|
|
2175
|
+
this.updateHighlighting();
|
|
2176
|
+
this.updateCyclesGutter();
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
// Allocate source string in WASM heap
|
|
2181
|
+
const wasm = this.wasmModule;
|
|
2182
|
+
const sourceLen = text.length + 1;
|
|
2183
|
+
const sourcePtr = wasm._malloc(sourceLen);
|
|
2184
|
+
wasm.stringToUTF8(text, sourcePtr, sourceLen);
|
|
2185
|
+
|
|
2186
|
+
const success = wasm._assembleSource(sourcePtr);
|
|
2187
|
+
wasm._free(sourcePtr);
|
|
2188
|
+
|
|
2189
|
+
if (success) {
|
|
2190
|
+
const size = wasm._getAsmOutputSize();
|
|
2191
|
+
const origin = wasm._getAsmOrigin();
|
|
2192
|
+
this.lastAssembledSize = size;
|
|
2193
|
+
this.lastOrigin = origin;
|
|
2194
|
+
this.setStatus(
|
|
2195
|
+
`OK: ${size} bytes at $${origin.toString(16).toUpperCase().padStart(4, "0")}`,
|
|
2196
|
+
true,
|
|
2197
|
+
);
|
|
2198
|
+
this.loadBtn.disabled = !this.isRunningCallback();
|
|
2199
|
+
this.debugBtn.disabled = !this.isRunningCallback();
|
|
2200
|
+
|
|
2201
|
+
// Store symbols for byte encoding
|
|
2202
|
+
this.symbols.clear();
|
|
2203
|
+
const symbolCount = wasm._getAsmSymbolCount();
|
|
2204
|
+
for (let i = 0; i < symbolCount; i++) {
|
|
2205
|
+
const namePtr = wasm._getAsmSymbolName(i);
|
|
2206
|
+
const name = wasm.UTF8ToString(namePtr);
|
|
2207
|
+
const value = wasm._getAsmSymbolValue(i);
|
|
2208
|
+
this.symbols.set(name.toUpperCase(), value);
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
// Export symbols to CPU debugger label manager
|
|
2212
|
+
if (this.cpuDebugger) {
|
|
2213
|
+
const lm = this.cpuDebugger.labelManager;
|
|
2214
|
+
lm.clearImportedBySource("assembler");
|
|
2215
|
+
for (const [name, value] of this.symbols) {
|
|
2216
|
+
if (value >= 0 && value <= 0xffff) {
|
|
2217
|
+
lm.importedLabels.set(value, { name, source: "assembler" });
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
lm._notify();
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
// Re-encode all lines with resolved symbols
|
|
2224
|
+
this.encodeAllLineBytes();
|
|
2225
|
+
|
|
2226
|
+
this.updateSymbolTable(wasm);
|
|
2227
|
+
this.updateHexOutput(wasm, origin, size);
|
|
2228
|
+
} else {
|
|
2229
|
+
const errorCount = wasm._getAsmErrorCount();
|
|
2230
|
+
this.setStatus(
|
|
2231
|
+
`${errorCount} error${errorCount !== 1 ? "s" : ""}`,
|
|
2232
|
+
false,
|
|
2233
|
+
);
|
|
2234
|
+
this.loadBtn.disabled = true;
|
|
2235
|
+
this.debugBtn.disabled = true;
|
|
2236
|
+
|
|
2237
|
+
// Collect errors
|
|
2238
|
+
for (let i = 0; i < errorCount; i++) {
|
|
2239
|
+
const line = wasm._getAsmErrorLine(i);
|
|
2240
|
+
const msgPtr = wasm._getAsmErrorMessage(i);
|
|
2241
|
+
const msg = wasm.UTF8ToString(msgPtr);
|
|
2242
|
+
|
|
2243
|
+
if (line >= 1) {
|
|
2244
|
+
this.errors.set(line, msg);
|
|
2245
|
+
// Clear bytes for error lines
|
|
2246
|
+
this.lineBytes.delete(line);
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
this.clearOutputPanels();
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
// Re-render highlighting with error markers
|
|
2254
|
+
this.updateHighlighting();
|
|
2255
|
+
this.updateCyclesGutter();
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
updateSymbolTable(wasm) {
|
|
2259
|
+
const count = wasm._getAsmSymbolCount();
|
|
2260
|
+
|
|
2261
|
+
// Update count badge
|
|
2262
|
+
if (this.symbolsCount) {
|
|
2263
|
+
this.symbolsCount.textContent = count > 0 ? count : "";
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
if (count === 0) {
|
|
2267
|
+
this.symbolsContent.innerHTML = `
|
|
2268
|
+
<div class="asm-panel-empty">
|
|
2269
|
+
<div class="asm-empty-icon">{ }</div>
|
|
2270
|
+
<div class="asm-empty-text">No symbols defined</div>
|
|
2271
|
+
</div>`;
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
// Separate symbols into labels and equates
|
|
2276
|
+
const labels = [];
|
|
2277
|
+
const equates = [];
|
|
2278
|
+
|
|
2279
|
+
for (let i = 0; i < count; i++) {
|
|
2280
|
+
const namePtr = wasm._getAsmSymbolName(i);
|
|
2281
|
+
const name = wasm.UTF8ToString(namePtr);
|
|
2282
|
+
const value = wasm._getAsmSymbolValue(i);
|
|
2283
|
+
const hex =
|
|
2284
|
+
"$" + (value & 0xffff).toString(16).toUpperCase().padStart(4, "0");
|
|
2285
|
+
const isLocal = name.startsWith(":") || name.startsWith("]");
|
|
2286
|
+
const item = { name, hex, isLocal };
|
|
2287
|
+
|
|
2288
|
+
// Heuristic: values in ROM range ($F800+) or under $0100 are likely equates
|
|
2289
|
+
if (value >= 0xf800 || value < 0x0100) {
|
|
2290
|
+
equates.push(item);
|
|
2291
|
+
} else {
|
|
2292
|
+
labels.push(item);
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
let html = '<div class="asm-symbol-list">';
|
|
2297
|
+
|
|
2298
|
+
if (labels.length > 0) {
|
|
2299
|
+
html += '<div class="asm-symbol-group">';
|
|
2300
|
+
html += '<div class="asm-symbol-group-header">Labels</div>';
|
|
2301
|
+
for (const item of labels) {
|
|
2302
|
+
const cls = item.isLocal ? "asm-sym-local" : "asm-sym-global";
|
|
2303
|
+
html += `<div class="asm-symbol-row">
|
|
2304
|
+
<span class="asm-sym-name ${cls}">${this.escapeHtml(item.name)}</span>
|
|
2305
|
+
<span class="asm-sym-value">${item.hex}</span>
|
|
2306
|
+
</div>`;
|
|
2307
|
+
}
|
|
2308
|
+
html += "</div>";
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
if (equates.length > 0) {
|
|
2312
|
+
html += '<div class="asm-symbol-group">';
|
|
2313
|
+
html += '<div class="asm-symbol-group-header">Equates</div>';
|
|
2314
|
+
for (const item of equates) {
|
|
2315
|
+
html += `<div class="asm-symbol-row">
|
|
2316
|
+
<span class="asm-sym-name asm-sym-equ">${this.escapeHtml(item.name)}</span>
|
|
2317
|
+
<span class="asm-sym-value">${item.hex}</span>
|
|
2318
|
+
</div>`;
|
|
2319
|
+
}
|
|
2320
|
+
html += "</div>";
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
html += "</div>";
|
|
2324
|
+
this.symbolsContent.innerHTML = html;
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
updateHexOutput(wasm, origin, size) {
|
|
2328
|
+
// Update count badge
|
|
2329
|
+
if (this.hexCount) {
|
|
2330
|
+
this.hexCount.textContent = size > 0 ? `${size} bytes` : "";
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
if (size === 0) {
|
|
2334
|
+
this.hexContent.innerHTML = `
|
|
2335
|
+
<div class="asm-panel-empty">
|
|
2336
|
+
<div class="asm-empty-icon">[ ]</div>
|
|
2337
|
+
<div class="asm-empty-text">No output</div>
|
|
2338
|
+
</div>`;
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
const bufPtr = wasm._getAsmOutputBuffer();
|
|
2343
|
+
const data = new Uint8Array(wasm.HEAPU8.buffer, bufPtr, size);
|
|
2344
|
+
|
|
2345
|
+
// Header showing range
|
|
2346
|
+
const endAddr = origin + size - 1;
|
|
2347
|
+
const rangeStr = `$${origin.toString(16).toUpperCase().padStart(4, "0")} - $${(endAddr & 0xffff).toString(16).toUpperCase().padStart(4, "0")}`;
|
|
2348
|
+
|
|
2349
|
+
let html = `<div class="asm-hex-header">
|
|
2350
|
+
<span class="asm-hex-range">${rangeStr}</span>
|
|
2351
|
+
<span class="asm-hex-size">${size} bytes</span>
|
|
2352
|
+
</div>`;
|
|
2353
|
+
|
|
2354
|
+
html += '<div class="asm-hex-dump">';
|
|
2355
|
+
const bytesPerRow = 8; // Use 8 bytes for cleaner display
|
|
2356
|
+
|
|
2357
|
+
for (let offset = 0; offset < size; offset += bytesPerRow) {
|
|
2358
|
+
const addr = origin + offset;
|
|
2359
|
+
const addrStr =
|
|
2360
|
+
"$" + (addr & 0xffff).toString(16).toUpperCase().padStart(4, "0");
|
|
2361
|
+
|
|
2362
|
+
let hexPart = "";
|
|
2363
|
+
let asciiPart = "";
|
|
2364
|
+
|
|
2365
|
+
for (let i = 0; i < bytesPerRow; i++) {
|
|
2366
|
+
if (offset + i < size) {
|
|
2367
|
+
const byte = data[offset + i];
|
|
2368
|
+
hexPart += byte.toString(16).toUpperCase().padStart(2, "0") + " ";
|
|
2369
|
+
asciiPart +=
|
|
2370
|
+
byte >= 0x20 && byte <= 0x7e ? String.fromCharCode(byte) : "·";
|
|
2371
|
+
} else {
|
|
2372
|
+
hexPart += " ";
|
|
2373
|
+
asciiPart += " ";
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
html +=
|
|
2378
|
+
`<div class="asm-hex-row">` +
|
|
2379
|
+
`<span class="asm-hex-addr">${addrStr}</span>` +
|
|
2380
|
+
`<span class="asm-hex-sep">│</span>` +
|
|
2381
|
+
`<span class="asm-hex-bytes">${hexPart}</span>` +
|
|
2382
|
+
`<span class="asm-hex-sep">│</span>` +
|
|
2383
|
+
`<span class="asm-hex-ascii">${this.escapeHtml(asciiPart)}</span>` +
|
|
2384
|
+
`</div>`;
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
html += "</div>";
|
|
2388
|
+
this.hexContent.innerHTML = html;
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
clearOutputPanels() {
|
|
2392
|
+
// Clear count badges
|
|
2393
|
+
if (this.symbolsCount) this.symbolsCount.textContent = "";
|
|
2394
|
+
if (this.hexCount) this.hexCount.textContent = "";
|
|
2395
|
+
|
|
2396
|
+
this.symbolsContent.innerHTML = `
|
|
2397
|
+
<div class="asm-panel-empty asm-panel-error">
|
|
2398
|
+
<div class="asm-empty-icon">⚠</div>
|
|
2399
|
+
<div class="asm-empty-text">Fix errors to see symbols</div>
|
|
2400
|
+
</div>`;
|
|
2401
|
+
this.hexContent.innerHTML = `
|
|
2402
|
+
<div class="asm-panel-empty asm-panel-error">
|
|
2403
|
+
<div class="asm-empty-icon">⚠</div>
|
|
2404
|
+
<div class="asm-empty-text">Fix errors to see output</div>
|
|
2405
|
+
</div>`;
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
escapeHtml(text) {
|
|
2409
|
+
const div = document.createElement("div");
|
|
2410
|
+
div.textContent = text;
|
|
2411
|
+
return div.innerHTML;
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
loadExample() {
|
|
2415
|
+
const example = `; Hello World - prints a message and returns to the monitor
|
|
2416
|
+
;
|
|
2417
|
+
; Assemble, Load, then type CALL 2048 from within BASIC.
|
|
2418
|
+
|
|
2419
|
+
ORG $0800
|
|
2420
|
+
|
|
2421
|
+
COUT EQU $FDED ;ROM character output routine
|
|
2422
|
+
CROUT EQU $FD8E ;ROM carriage return
|
|
2423
|
+
|
|
2424
|
+
START LDX #0 ;Start at first character
|
|
2425
|
+
LOOP LDA MSG,X ;Load next character
|
|
2426
|
+
BEQ DONE ;Zero byte = end of string
|
|
2427
|
+
JSR COUT ;Print character
|
|
2428
|
+
INX ;Next character
|
|
2429
|
+
BNE LOOP ;Continue (max 256 chars)
|
|
2430
|
+
DONE JSR CROUT ;Print carriage return
|
|
2431
|
+
RTS ;Return to monitor
|
|
2432
|
+
|
|
2433
|
+
MSG ASC "HELLO FROM THE APPLE //E EMULATOR!"
|
|
2434
|
+
DFB $00 ;Null terminator`;
|
|
2435
|
+
|
|
2436
|
+
this.textarea.value = example;
|
|
2437
|
+
this.textarea.dispatchEvent(new Event("input", { bubbles: true }));
|
|
2438
|
+
this.doClear();
|
|
2439
|
+
this.currentFileName = null;
|
|
2440
|
+
this.validateAllLines();
|
|
2441
|
+
this.encodeAllLineBytes();
|
|
2442
|
+
this.updateGutter();
|
|
2443
|
+
this.updateCursorPosition();
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
doClear() {
|
|
2447
|
+
this.symbols.clear();
|
|
2448
|
+
this.lineBytes.clear();
|
|
2449
|
+
this.linePCs = new Map();
|
|
2450
|
+
this.errors.clear();
|
|
2451
|
+
this.syntaxErrors.clear();
|
|
2452
|
+
this.loadBtn.disabled = true;
|
|
2453
|
+
this.debugBtn.disabled = true;
|
|
2454
|
+
this.setStatus("", false);
|
|
2455
|
+
|
|
2456
|
+
// Clear output panels to empty state
|
|
2457
|
+
if (this.symbolsCount) this.symbolsCount.textContent = "";
|
|
2458
|
+
if (this.hexCount) this.hexCount.textContent = "";
|
|
2459
|
+
this.symbolsContent.innerHTML = `
|
|
2460
|
+
<div class="asm-panel-empty">
|
|
2461
|
+
<div class="asm-empty-text">Assemble to see symbols</div>
|
|
2462
|
+
</div>`;
|
|
2463
|
+
this.hexContent.innerHTML = `
|
|
2464
|
+
<div class="asm-panel-empty">
|
|
2465
|
+
<div class="asm-empty-text">Assemble to see output</div>
|
|
2466
|
+
</div>`;
|
|
2467
|
+
|
|
2468
|
+
this.updateGutter();
|
|
2469
|
+
this.updateHighlighting();
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
doLoad() {
|
|
2473
|
+
this.wasmModule._loadAsmIntoMemory();
|
|
2474
|
+
this.showLoadedFeedback();
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
showLoadedFeedback() {
|
|
2478
|
+
const originalHtml = this.loadBtn.innerHTML;
|
|
2479
|
+
this.loadBtn.innerHTML = `<span class="asm-btn-icon">✓</span> Loaded!`;
|
|
2480
|
+
this.loadBtn.classList.add("asm-btn-success");
|
|
2481
|
+
|
|
2482
|
+
setTimeout(() => {
|
|
2483
|
+
this.loadBtn.innerHTML = originalHtml;
|
|
2484
|
+
this.loadBtn.classList.remove("asm-btn-success");
|
|
2485
|
+
}, 1500);
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
/**
|
|
2489
|
+
* Create a new empty file
|
|
2490
|
+
*/
|
|
2491
|
+
async newFile() {
|
|
2492
|
+
if (this.textarea.value.trim()) {
|
|
2493
|
+
const confirmed = await showConfirm(
|
|
2494
|
+
"Clear current source and start new file?",
|
|
2495
|
+
);
|
|
2496
|
+
if (!confirmed) return;
|
|
2497
|
+
}
|
|
2498
|
+
this.textarea.value = "";
|
|
2499
|
+
this.currentFileName = null;
|
|
2500
|
+
this._fileHandle = null;
|
|
2501
|
+
this.updateTitle("Assembler");
|
|
2502
|
+
this.updateHighlighting();
|
|
2503
|
+
this.updateGutter();
|
|
2504
|
+
this.errors.clear();
|
|
2505
|
+
this.syntaxErrors.clear();
|
|
2506
|
+
this.clearOutputPanels();
|
|
2507
|
+
this.setStatus("", true);
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
/**
|
|
2511
|
+
* Open a file from the local filesystem using the host file picker
|
|
2512
|
+
*/
|
|
2513
|
+
async openFile() {
|
|
2514
|
+
try {
|
|
2515
|
+
const [handle] = await window.showOpenFilePicker({
|
|
2516
|
+
types: [
|
|
2517
|
+
{
|
|
2518
|
+
description: "Assembly source files",
|
|
2519
|
+
accept: { "text/plain": [".s", ".asm", ".a65", ".txt"] },
|
|
2520
|
+
},
|
|
2521
|
+
],
|
|
2522
|
+
multiple: false,
|
|
2523
|
+
});
|
|
2524
|
+
const file = await handle.getFile();
|
|
2525
|
+
const text = await file.text();
|
|
2526
|
+
this.textarea.value = text;
|
|
2527
|
+
this.currentFileName = file.name;
|
|
2528
|
+
this._fileHandle = handle;
|
|
2529
|
+
this.updateTitle(`Assembler - ${file.name}`);
|
|
2530
|
+
this.updateHighlighting();
|
|
2531
|
+
this.validateAllLines();
|
|
2532
|
+
this.encodeAllLineBytes();
|
|
2533
|
+
this.updateGutter();
|
|
2534
|
+
this.setStatus(`Opened: ${file.name}`, true);
|
|
2535
|
+
} catch (err) {
|
|
2536
|
+
if (err.name !== "AbortError") {
|
|
2537
|
+
this.setStatus("Failed to open file", false);
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
/**
|
|
2543
|
+
* Save the current source to a file using the host save dialog
|
|
2544
|
+
*/
|
|
2545
|
+
async saveFile() {
|
|
2546
|
+
const content = this.textarea.value;
|
|
2547
|
+
if (!content.trim()) {
|
|
2548
|
+
this.setStatus("Nothing to save", false);
|
|
2549
|
+
return;
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
try {
|
|
2553
|
+
// Reuse existing handle if we have one, otherwise prompt
|
|
2554
|
+
if (!this._fileHandle) {
|
|
2555
|
+
this._fileHandle = await window.showSaveFilePicker({
|
|
2556
|
+
suggestedName: this.currentFileName || "untitled.s",
|
|
2557
|
+
types: [
|
|
2558
|
+
{
|
|
2559
|
+
description: "Assembly source files",
|
|
2560
|
+
accept: { "text/plain": [".s", ".asm", ".a65", ".txt"] },
|
|
2561
|
+
},
|
|
2562
|
+
],
|
|
2563
|
+
});
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
const writable = await this._fileHandle.createWritable();
|
|
2567
|
+
await writable.write(content);
|
|
2568
|
+
await writable.close();
|
|
2569
|
+
|
|
2570
|
+
const filename = this._fileHandle.name;
|
|
2571
|
+
this.currentFileName = filename;
|
|
2572
|
+
this.updateTitle(`Assembler - ${filename}`);
|
|
2573
|
+
this.setStatus(`Saved: ${filename}`, true);
|
|
2574
|
+
} catch (err) {
|
|
2575
|
+
if (err.name !== "AbortError") {
|
|
2576
|
+
this.setStatus("Failed to save file", false);
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
/**
|
|
2582
|
+
* Update the window title
|
|
2583
|
+
*/
|
|
2584
|
+
updateTitle(title) {
|
|
2585
|
+
const titleEl = this.windowElement?.querySelector(".window-title");
|
|
2586
|
+
if (titleEl) {
|
|
2587
|
+
titleEl.textContent = title;
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
setStatus(text, ok) {
|
|
2592
|
+
this.statusSpan.textContent = text;
|
|
2593
|
+
this.statusSpan.className =
|
|
2594
|
+
"asm-status" + (ok ? " asm-status-ok" : " asm-status-error");
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
goToLine(lineNumber) {
|
|
2598
|
+
if (!this.textarea) return;
|
|
2599
|
+
const lines = this.textarea.value.split("\n");
|
|
2600
|
+
let pos = 0;
|
|
2601
|
+
for (let i = 0; i < lineNumber - 1 && i < lines.length; i++) {
|
|
2602
|
+
pos += lines[i].length + 1;
|
|
2603
|
+
}
|
|
2604
|
+
this.textarea.focus();
|
|
2605
|
+
this.textarea.setSelectionRange(pos, pos);
|
|
2606
|
+
this.updateCurrentLineHighlight();
|
|
2607
|
+
|
|
2608
|
+
// Scroll line into view
|
|
2609
|
+
const style = getComputedStyle(this.textarea);
|
|
2610
|
+
const lineHeight =
|
|
2611
|
+
parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.4;
|
|
2612
|
+
const targetScroll =
|
|
2613
|
+
(lineNumber - 1) * lineHeight - this.textarea.clientHeight / 2;
|
|
2614
|
+
this.textarea.scrollTop = Math.max(0, targetScroll);
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
/**
|
|
2618
|
+
* Toggle a breakpoint for the given line number
|
|
2619
|
+
*/
|
|
2620
|
+
toggleBreakpoint(lineNumber) {
|
|
2621
|
+
if (!this.bpManager) return;
|
|
2622
|
+
|
|
2623
|
+
// Get the PC address for this line
|
|
2624
|
+
const address = this.linePCs.get(lineNumber);
|
|
2625
|
+
if (address === undefined) {
|
|
2626
|
+
// No instruction on this line (empty, comment, or directive)
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
// Check if line has an actual instruction (not just a label or directive)
|
|
2631
|
+
const lines = this.textarea.value.split("\n");
|
|
2632
|
+
if (lineNumber < 1 || lineNumber > lines.length) return;
|
|
2633
|
+
|
|
2634
|
+
const parsed = this.parseLine(lines[lineNumber - 1]);
|
|
2635
|
+
if (!parsed || !parsed.opcode) return;
|
|
2636
|
+
|
|
2637
|
+
// Skip directives - they don't generate code
|
|
2638
|
+
const opcodes = this.getOpcodeTable();
|
|
2639
|
+
if (!opcodes[parsed.opcode.toUpperCase()]) return;
|
|
2640
|
+
|
|
2641
|
+
// Toggle the breakpoint
|
|
2642
|
+
this.bpManager.toggle(address);
|
|
2643
|
+
|
|
2644
|
+
// Update our local tracking
|
|
2645
|
+
if (this.bpManager.has(address)) {
|
|
2646
|
+
this.lineBreakpoints.set(lineNumber, address);
|
|
2647
|
+
} else {
|
|
2648
|
+
this.lineBreakpoints.delete(lineNumber);
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
this.updateGutter();
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
/**
|
|
2655
|
+
* Sync breakpoint state from the manager (called when breakpoints change externally)
|
|
2656
|
+
*/
|
|
2657
|
+
syncBreakpointsFromManager() {
|
|
2658
|
+
if (!this.bpManager) return;
|
|
2659
|
+
|
|
2660
|
+
// Clear our local tracking and rebuild from manager state
|
|
2661
|
+
this.lineBreakpoints.clear();
|
|
2662
|
+
|
|
2663
|
+
// Check each line's PC against the manager's breakpoints
|
|
2664
|
+
for (const [lineNumber, address] of this.linePCs) {
|
|
2665
|
+
if (this.bpManager.has(address)) {
|
|
2666
|
+
this.lineBreakpoints.set(lineNumber, address);
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
this.updateGutter();
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
// ============================================================================
|
|
2674
|
+
// ROM Routines Panel
|
|
2675
|
+
// ============================================================================
|
|
2676
|
+
|
|
2677
|
+
initRomPanel() {
|
|
2678
|
+
// Populate category dropdown
|
|
2679
|
+
ROM_CATEGORIES.forEach((cat) => {
|
|
2680
|
+
const opt = document.createElement("option");
|
|
2681
|
+
opt.value = cat;
|
|
2682
|
+
opt.textContent = cat === "All" ? "All Categories" : cat;
|
|
2683
|
+
this.romCategory.appendChild(opt);
|
|
2684
|
+
});
|
|
2685
|
+
|
|
2686
|
+
// ROM button click
|
|
2687
|
+
this.romBtn.addEventListener("click", () => this.toggleRomPanel());
|
|
2688
|
+
|
|
2689
|
+
// Close button
|
|
2690
|
+
this.romPanel
|
|
2691
|
+
.querySelector(".asm-rom-close")
|
|
2692
|
+
.addEventListener("click", () => {
|
|
2693
|
+
this.hideRomPanel();
|
|
2694
|
+
});
|
|
2695
|
+
|
|
2696
|
+
// Search input
|
|
2697
|
+
this.romSearch.addEventListener("input", () => this.filterRomRoutines());
|
|
2698
|
+
|
|
2699
|
+
// Category filter
|
|
2700
|
+
this.romCategory.addEventListener("change", () => this.filterRomRoutines());
|
|
2701
|
+
|
|
2702
|
+
// Back button in detail view
|
|
2703
|
+
this.romPanel
|
|
2704
|
+
.querySelector(".asm-rom-back")
|
|
2705
|
+
.addEventListener("click", () => {
|
|
2706
|
+
this.romDetail.classList.add("hidden");
|
|
2707
|
+
this.romList.classList.remove("hidden");
|
|
2708
|
+
});
|
|
2709
|
+
|
|
2710
|
+
// Insert buttons
|
|
2711
|
+
this.romPanel
|
|
2712
|
+
.querySelector(".asm-rom-insert-equ")
|
|
2713
|
+
.addEventListener("click", () => {
|
|
2714
|
+
if (this.selectedRoutine) this.insertRoutineEqu(this.selectedRoutine);
|
|
2715
|
+
});
|
|
2716
|
+
this.romPanel
|
|
2717
|
+
.querySelector(".asm-rom-insert-jsr")
|
|
2718
|
+
.addEventListener("click", () => {
|
|
2719
|
+
if (this.selectedRoutine) this.insertRoutineJsr(this.selectedRoutine);
|
|
2720
|
+
});
|
|
2721
|
+
|
|
2722
|
+
// Escape key to close panel
|
|
2723
|
+
this.romPanel.addEventListener("keydown", (e) => {
|
|
2724
|
+
if (e.key === "Escape") {
|
|
2725
|
+
this.hideRomPanel();
|
|
2726
|
+
}
|
|
2727
|
+
});
|
|
2728
|
+
|
|
2729
|
+
// Populate initial list
|
|
2730
|
+
this.renderRomList(ROM_ROUTINES);
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
toggleRomPanel() {
|
|
2734
|
+
if (this.romPanel.classList.contains("hidden")) {
|
|
2735
|
+
this.showRomPanel();
|
|
2736
|
+
} else {
|
|
2737
|
+
this.hideRomPanel();
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
showRomPanel() {
|
|
2742
|
+
this.romPanel.classList.remove("hidden");
|
|
2743
|
+
this.romSearch.focus();
|
|
2744
|
+
this.romSearch.select();
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
hideRomPanel() {
|
|
2748
|
+
this.romPanel.classList.add("hidden");
|
|
2749
|
+
this.romDetail.classList.add("hidden");
|
|
2750
|
+
this.romList.classList.remove("hidden");
|
|
2751
|
+
this.textarea.focus();
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
filterRomRoutines() {
|
|
2755
|
+
const query = this.romSearch.value.trim();
|
|
2756
|
+
const category = this.romCategory.value;
|
|
2757
|
+
|
|
2758
|
+
let routines;
|
|
2759
|
+
if (query) {
|
|
2760
|
+
routines = searchRoutines(query);
|
|
2761
|
+
if (category !== "All") {
|
|
2762
|
+
routines = routines.filter((r) => r.category === category);
|
|
2763
|
+
}
|
|
2764
|
+
} else {
|
|
2765
|
+
routines = getRoutinesByCategory(category);
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
this.renderRomList(routines);
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
renderRomList(routines) {
|
|
2772
|
+
if (routines.length === 0) {
|
|
2773
|
+
this.romList.innerHTML = `
|
|
2774
|
+
<div class="asm-rom-empty">
|
|
2775
|
+
<div class="asm-rom-empty-icon">🔍</div>
|
|
2776
|
+
<div class="asm-rom-empty-text">No routines found</div>
|
|
2777
|
+
</div>`;
|
|
2778
|
+
return;
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
const html = routines
|
|
2782
|
+
.map(
|
|
2783
|
+
(r) => `
|
|
2784
|
+
<div class="asm-rom-item" data-name="${r.name}">
|
|
2785
|
+
<div class="asm-rom-item-header">
|
|
2786
|
+
<span class="asm-rom-item-name">${r.name}</span>
|
|
2787
|
+
<span class="asm-rom-item-addr">$${r.address.toString(16).toUpperCase().padStart(4, "0")}</span>
|
|
2788
|
+
</div>
|
|
2789
|
+
<div class="asm-rom-item-desc">${r.description}</div>
|
|
2790
|
+
<div class="asm-rom-item-cat">${r.category}</div>
|
|
2791
|
+
</div>
|
|
2792
|
+
`,
|
|
2793
|
+
)
|
|
2794
|
+
.join("");
|
|
2795
|
+
|
|
2796
|
+
this.romList.innerHTML = html;
|
|
2797
|
+
|
|
2798
|
+
// Add click handlers
|
|
2799
|
+
this.romList.querySelectorAll(".asm-rom-item").forEach((item) => {
|
|
2800
|
+
item.addEventListener("click", () => {
|
|
2801
|
+
const name = item.dataset.name;
|
|
2802
|
+
const routine = ROM_ROUTINES.find((r) => r.name === name);
|
|
2803
|
+
if (routine) this.showRoutineDetail(routine);
|
|
2804
|
+
});
|
|
2805
|
+
});
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
showRoutineDetail(routine) {
|
|
2809
|
+
this.selectedRoutine = routine;
|
|
2810
|
+
this.romDetailName.textContent = `${routine.name} ($${routine.address.toString(16).toUpperCase().padStart(4, "0")})`;
|
|
2811
|
+
|
|
2812
|
+
let html = `
|
|
2813
|
+
<div class="asm-rom-section">
|
|
2814
|
+
<div class="asm-rom-section-title">Description</div>
|
|
2815
|
+
<div class="asm-rom-section-content">${routine.description}</div>
|
|
2816
|
+
</div>
|
|
2817
|
+
`;
|
|
2818
|
+
|
|
2819
|
+
if (routine.inputs && routine.inputs.length > 0) {
|
|
2820
|
+
html += `
|
|
2821
|
+
<div class="asm-rom-section">
|
|
2822
|
+
<div class="asm-rom-section-title">Inputs</div>
|
|
2823
|
+
<div class="asm-rom-section-content">
|
|
2824
|
+
${routine.inputs
|
|
2825
|
+
.map(
|
|
2826
|
+
(i) => `
|
|
2827
|
+
<div class="asm-rom-param">
|
|
2828
|
+
<span class="asm-rom-param-reg">${i.register}</span>
|
|
2829
|
+
<span class="asm-rom-param-desc">${i.description}</span>
|
|
2830
|
+
</div>
|
|
2831
|
+
`,
|
|
2832
|
+
)
|
|
2833
|
+
.join("")}
|
|
2834
|
+
</div>
|
|
2835
|
+
</div>
|
|
2836
|
+
`;
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
if (routine.outputs && routine.outputs.length > 0) {
|
|
2840
|
+
html += `
|
|
2841
|
+
<div class="asm-rom-section">
|
|
2842
|
+
<div class="asm-rom-section-title">Outputs</div>
|
|
2843
|
+
<div class="asm-rom-section-content">
|
|
2844
|
+
${routine.outputs
|
|
2845
|
+
.map(
|
|
2846
|
+
(o) => `
|
|
2847
|
+
<div class="asm-rom-param">
|
|
2848
|
+
<span class="asm-rom-param-reg">${o.register}</span>
|
|
2849
|
+
<span class="asm-rom-param-desc">${o.description}</span>
|
|
2850
|
+
</div>
|
|
2851
|
+
`,
|
|
2852
|
+
)
|
|
2853
|
+
.join("")}
|
|
2854
|
+
</div>
|
|
2855
|
+
</div>
|
|
2856
|
+
`;
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
if (routine.preserves && routine.preserves.length > 0) {
|
|
2860
|
+
html += `
|
|
2861
|
+
<div class="asm-rom-section asm-rom-section-inline">
|
|
2862
|
+
<span class="asm-rom-section-label">Preserves:</span>
|
|
2863
|
+
<span class="asm-rom-regs">${routine.preserves.join(", ")}</span>
|
|
2864
|
+
</div>
|
|
2865
|
+
`;
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
if (routine.clobbers && routine.clobbers.length > 0) {
|
|
2869
|
+
html += `
|
|
2870
|
+
<div class="asm-rom-section asm-rom-section-inline">
|
|
2871
|
+
<span class="asm-rom-section-label">Clobbers:</span>
|
|
2872
|
+
<span class="asm-rom-regs asm-rom-regs-warn">${routine.clobbers.join(", ")}</span>
|
|
2873
|
+
</div>
|
|
2874
|
+
`;
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
if (routine.notes) {
|
|
2878
|
+
html += `
|
|
2879
|
+
<div class="asm-rom-section">
|
|
2880
|
+
<div class="asm-rom-section-title">Notes</div>
|
|
2881
|
+
<div class="asm-rom-section-content asm-rom-notes">${routine.notes}</div>
|
|
2882
|
+
</div>
|
|
2883
|
+
`;
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
if (routine.example) {
|
|
2887
|
+
html += `
|
|
2888
|
+
<div class="asm-rom-section">
|
|
2889
|
+
<div class="asm-rom-section-title">Example</div>
|
|
2890
|
+
<pre class="asm-rom-example">${this.escapeHtml(routine.example)}</pre>
|
|
2891
|
+
</div>
|
|
2892
|
+
`;
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
this.romDetailContent.innerHTML = html;
|
|
2896
|
+
this.romList.classList.add("hidden");
|
|
2897
|
+
this.romDetail.classList.remove("hidden");
|
|
2898
|
+
}
|
|
2899
|
+
|
|
2900
|
+
insertRoutineEqu(routine) {
|
|
2901
|
+
const equ = `${routine.name.padEnd(8)} EQU $${routine.address.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
2902
|
+
this.insertAtCursor(equ + "\n");
|
|
2903
|
+
this.hideRomPanel();
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
insertRoutineJsr(routine) {
|
|
2907
|
+
// Check if EQU already exists
|
|
2908
|
+
const hasEqu = this.textarea.value
|
|
2909
|
+
.toUpperCase()
|
|
2910
|
+
.includes(`${routine.name.toUpperCase()} `);
|
|
2911
|
+
|
|
2912
|
+
this.textarea.focus();
|
|
2913
|
+
|
|
2914
|
+
if (!hasEqu) {
|
|
2915
|
+
// Need to insert EQU at top (after header comments) AND JSR at cursor
|
|
2916
|
+
// Save current cursor position
|
|
2917
|
+
const savedCursor = this.textarea.selectionStart;
|
|
2918
|
+
const lines = this.textarea.value.split("\n");
|
|
2919
|
+
|
|
2920
|
+
// Find insert point for EQU (after header comments)
|
|
2921
|
+
let insertIdx = 0;
|
|
2922
|
+
let charPos = 0;
|
|
2923
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2924
|
+
const trimmed = lines[i].trim();
|
|
2925
|
+
if (trimmed && !trimmed.startsWith("*") && !trimmed.startsWith(";")) {
|
|
2926
|
+
insertIdx = i;
|
|
2927
|
+
break;
|
|
2928
|
+
}
|
|
2929
|
+
charPos += lines[i].length + 1; // +1 for newline
|
|
2930
|
+
insertIdx = i + 1;
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
const equ = `${routine.name.padEnd(8)} EQU $${routine.address.toString(16).toUpperCase().padStart(4, "0")}\n`;
|
|
2934
|
+
|
|
2935
|
+
// Move cursor to EQU insert position and insert
|
|
2936
|
+
this.textarea.selectionStart = this.textarea.selectionEnd = charPos;
|
|
2937
|
+
document.execCommand("insertText", false, equ);
|
|
2938
|
+
|
|
2939
|
+
// Move cursor back to original position (adjusted for inserted EQU line)
|
|
2940
|
+
const newCursorPos = savedCursor + equ.length;
|
|
2941
|
+
this.textarea.selectionStart = this.textarea.selectionEnd = newCursorPos;
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
// Insert JSR at cursor
|
|
2945
|
+
const jsr = ` JSR ${routine.name}`;
|
|
2946
|
+
this.insertAtCursor(jsr);
|
|
2947
|
+
this.hideRomPanel();
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
insertAtCursor(text) {
|
|
2951
|
+
this.textarea.focus();
|
|
2952
|
+
|
|
2953
|
+
const start = this.textarea.selectionStart;
|
|
2954
|
+
const value = this.textarea.value;
|
|
2955
|
+
|
|
2956
|
+
// If not at start of line, add newline first
|
|
2957
|
+
let insertText = text;
|
|
2958
|
+
if (start > 0 && value[start - 1] !== "\n") {
|
|
2959
|
+
insertText = "\n" + text;
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
// Use execCommand to preserve undo history
|
|
2963
|
+
// This inserts text at the current selection and is undoable with Cmd+Z
|
|
2964
|
+
document.execCommand("insertText", false, insertText);
|
|
2965
|
+
|
|
2966
|
+
// Update display
|
|
2967
|
+
this.updateHighlighting();
|
|
2968
|
+
this.updateGutter();
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
update() {
|
|
2972
|
+
// No periodic update needed
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
getState() {
|
|
2976
|
+
const baseState = super.getState();
|
|
2977
|
+
return {
|
|
2978
|
+
...baseState,
|
|
2979
|
+
content: this.textarea ? this.textarea.value : "",
|
|
2980
|
+
};
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
restoreState(state) {
|
|
2984
|
+
super.restoreState(state);
|
|
2985
|
+
if (state.content !== undefined && this.textarea) {
|
|
2986
|
+
this.textarea.value = state.content;
|
|
2987
|
+
this.updateHighlighting();
|
|
2988
|
+
this.validateAllLines();
|
|
2989
|
+
this.encodeAllLineBytes();
|
|
2990
|
+
this.updateGutter();
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
}
|