@henols/c64-re-tools 1.1.0 → 2.0.0-rc.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -37
  3. package/distribution/package.ts +21 -0
  4. package/distribution/plugin.ts +122 -0
  5. package/package.json +34 -40
  6. package/skills/c64-assembler/SKILL.md +76 -0
  7. package/skills/c64-assembler/scripts/assemble.ts +110 -0
  8. package/skills/c64-assembler/scripts/compare.ts +84 -0
  9. package/skills/c64-basic/SKILL.md +66 -0
  10. package/skills/c64-basic/scripts/basic.ts +46 -0
  11. package/skills/c64-disk/SKILL.md +93 -0
  12. package/skills/c64-disk/scripts/containers.ts +124 -0
  13. package/skills/c64-disk/scripts/disk.ts +120 -0
  14. package/skills/c64-emulator/SKILL.md +134 -0
  15. package/skills/c64-knowledge/SKILL.md +192 -0
  16. package/skills/c64-knowledge/scripts/knowledge.ts +613 -0
  17. package/skills/c64-memory-map/SKILL.md +79 -0
  18. package/skills/c64-memory-map/scripts/memmap.ts +92 -0
  19. package/skills/c64-provenance/SKILL.md +110 -0
  20. package/skills/c64-provenance/scripts/compare.ts +152 -0
  21. package/skills/c64-provenance/scripts/provenance.ts +334 -0
  22. package/skills/c64-provenance/scripts/runtime.ts +191 -0
  23. package/skills/c64-provenance/scripts/separate.ts +140 -0
  24. package/skills/c64-reverse-engineering/SKILL.md +144 -0
  25. package/skills/c64-source/SKILL.md +133 -0
  26. package/skills/c64-source/scripts/generate.ts +503 -0
  27. package/skills/c64-source/scripts/loader.ts +589 -0
  28. package/skills/c64-source/scripts/opcodes.ts +61 -0
  29. package/skills/c64-source/scripts/source.ts +673 -0
  30. package/skills/c64-static-analysis/SKILL.md +120 -0
  31. package/skills/c64-static-analysis/scripts/analyze.ts +293 -0
  32. package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
  33. package/skills/c64-static-analysis/scripts/findings.ts +75 -0
  34. package/skills/c64-static-analysis/scripts/idioms.ts +149 -0
  35. package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
  36. package/skills/c64-static-analysis/scripts/pointers.ts +108 -0
  37. package/skills/c64-testing/SKILL.md +103 -0
  38. package/skills/c64-testing/references/scenario-format.md +116 -0
  39. package/skills/c64-testing/scripts/checklist.ts +51 -0
  40. package/skills/c64-testing/scripts/run.ts +189 -0
  41. package/skills/c64-testing/scripts/scenario.ts +493 -0
  42. package/skills/c64-testing/scripts/test.ts +65 -0
  43. package/skills/c64-unpacker/SKILL.md +135 -0
  44. package/skills/c64-unpacker/scripts/evidence.ts +207 -0
  45. package/skills/c64-unpacker/scripts/loads.ts +209 -0
  46. package/skills/c64-unpacker/scripts/machine.ts +80 -0
  47. package/skills/c64-unpacker/scripts/stages.ts +130 -0
  48. package/skills/c64-unpacker/scripts/unpack.ts +683 -0
  49. package/src/c64.ts +411 -0
  50. package/src/cli/ap-sdk.ts +44 -0
  51. package/src/cli/cleanup.ts +194 -0
  52. package/src/cli/main.ts +125 -0
  53. package/src/cli/status.ts +61 -0
  54. package/src/host/main.ts +134 -0
  55. package/src/host/server.ts +392 -0
  56. package/src/host/tools/c1541.ts +284 -0
  57. package/src/host/tools/index.ts +35 -0
  58. package/src/host/tools/petcat.ts +77 -0
  59. package/src/host/tools/status.ts +47 -0
  60. package/src/host/tools/version.ts +41 -0
  61. package/src/host/vice/adapter.ts +804 -0
  62. package/src/host/vice/binary-monitor.ts +389 -0
  63. package/src/host/vice/chips.ts +118 -0
  64. package/src/host/vice/connect.ts +34 -0
  65. package/src/host/vice/process.ts +413 -0
  66. package/src/host/vice/screen.ts +184 -0
  67. package/src/host/vice/session.ts +1426 -0
  68. package/src/host/vice/text-monitor.ts +223 -0
  69. package/src/host-client/autostart.ts +55 -0
  70. package/src/host-client/connect.ts +241 -0
  71. package/src/host-client/coverage.ts +63 -0
  72. package/src/host-client/image.ts +63 -0
  73. package/src/host-client/steps.ts +170 -0
  74. package/src/host-client/tools.ts +89 -0
  75. package/src/host-client/transfer.ts +126 -0
  76. package/src/host-client/vice-session.ts +316 -0
  77. package/src/knowledge/database.ts +132 -0
  78. package/src/knowledge/expression.ts +209 -0
  79. package/src/knowledge/history.ts +169 -0
  80. package/src/knowledge/import.ts +392 -0
  81. package/src/knowledge/read.ts +392 -0
  82. package/src/knowledge/schema.ts +250 -0
  83. package/src/knowledge/write.ts +837 -0
  84. package/src/mcp/main.ts +134 -0
  85. package/src/mcp/output.ts +32 -0
  86. package/src/mcp/server.ts +316 -0
  87. package/src/mcp/tools/debug.ts +327 -0
  88. package/src/mcp/tools/execution.ts +117 -0
  89. package/src/mcp/tools/input.ts +72 -0
  90. package/src/mcp/tools/machine.ts +100 -0
  91. package/src/mcp/tools/media.ts +129 -0
  92. package/src/mcp/tools/memory.ts +250 -0
  93. package/src/mcp/tools/video.ts +271 -0
  94. package/src/native/acme.ts +116 -0
  95. package/src/native/discover.ts +179 -0
  96. package/src/native/dxa.ts +166 -0
  97. package/src/native/ghidra/analyze.ts +86 -0
  98. package/src/native/ghidra/index.ts +173 -0
  99. package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
  100. package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
  101. package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
  102. package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
  103. package/src/native/ghidra/scripts/C64Export.java +274 -0
  104. package/src/native/ghidra/scripts/C64Prepare.java +60 -0
  105. package/src/native/local.ts +34 -0
  106. package/src/native/processes.ts +350 -0
  107. package/src/native/run.ts +172 -0
  108. package/src/native/staging.ts +83 -0
  109. package/src/native/status.ts +113 -0
  110. package/src/native/types.ts +257 -0
  111. package/src/native/watchdog.ts +78 -0
  112. package/src/package.ts +21 -0
  113. package/src/platform.ts +441 -0
  114. package/src/project.ts +39 -0
  115. package/src/protocol/framing.ts +132 -0
  116. package/src/protocol/messages.ts +259 -0
  117. package/src/protocol/tools.ts +266 -0
  118. package/src/protocol/vice.ts +1348 -0
  119. package/src/script.ts +60 -0
  120. package/src/time.ts +15 -0
  121. package/src/trace.ts +194 -0
  122. package/THIRD-PARTY-NOTICES.md +0 -26
  123. package/bin/cli.mjs +0 -283
  124. package/skills/acme-build/SKILL.md +0 -260
  125. package/skills/acme-build/scripts/acme.mjs +0 -341
  126. package/skills/acme-build/template.a +0 -53
  127. package/skills/c64-disk-access/SKILL.md +0 -159
  128. package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
  129. package/skills/c64-memory-mapping/SKILL.md +0 -631
  130. package/skills/c64-memory-mapping/memmap.json +0 -8800
  131. package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
  132. package/skills/c64-petcat/SKILL.md +0 -87
  133. package/skills/c64-petcat/scripts/petcat.mjs +0 -221
  134. package/skills/c64-program-recon/SKILL.md +0 -706
  135. package/skills/c64-program-recon/references/control-flow.md +0 -180
  136. package/skills/c64-program-recon/references/graphics.md +0 -79
  137. package/skills/c64-program-recon/references/observation-hazards.md +0 -143
  138. package/skills/c64-program-recon/references/reconstruction.md +0 -133
  139. package/skills/c64-program-recon/references/sound-and-input.md +0 -74
  140. package/skills/c64-program-recon/references/tool-selection.md +0 -78
  141. package/skills/c64-program-recon/scripts/derive.mjs +0 -364
  142. package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
  143. package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
  144. package/skills/c64-provenance-diff/SKILL.md +0 -292
  145. package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
  146. package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
  147. package/skills/c64-ram-capture/RELEASES.json.example +0 -17
  148. package/skills/c64-ram-capture/SKILL.md +0 -474
  149. package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
  150. package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
  151. package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
  152. package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
  153. package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
  154. package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
  155. package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
  156. package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
  157. package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
  158. package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
  159. package/skills/c64-ram-capture/transients/README.md +0 -137
  160. package/skills/routine-queue-walker/SKILL.md +0 -394
  161. package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
  162. package/skills/vice-wedge-triage/SKILL.md +0 -269
@@ -0,0 +1,589 @@
1
+ // The loader of a generated source (D48): a PRG that copies each segment and
2
+ // the entry state's pages $0002-$03ff into place, sets the display registers
3
+ // of the VIC-II and the VIC bank, waits for the entry's raster line (D60),
4
+ // sets the CIA timers and interrupt masks (D63), sets $00/$01, the stack
5
+ // pointer, A, X, Y and the flags, and jumps to the entry. The copies run
6
+ // from a free page with SEI and all RAM visible, in an order where no copy
7
+ // overwrites the packed bytes of a copy that has not run yet. The plan is
8
+ // simulated here before the emulator runs it.
9
+
10
+ import { formatC64Address } from "#src/c64.ts";
11
+ import { WireFailure } from "#src/host-client/tools.ts";
12
+ import type { Range } from "./source.ts";
13
+
14
+ export interface EntryState {
15
+ pc: number;
16
+ a: number;
17
+ x: number;
18
+ y: number;
19
+ sp: number;
20
+ /** The status register as PLP reads it. */
21
+ flags: number;
22
+ port: [number, number];
23
+ /** $0000-$03ff at the entry. */
24
+ pages: Uint8Array;
25
+ /** The VIC-II at the entry (D60): $d000-$d02e, and the VIC bank (bits 0-1 of $dd00). */
26
+ vic?: { registers: Uint8Array; bank: number };
27
+ /** CIA 1 and CIA 2 at the entry (D63). */
28
+ cia?: [EntryCia, EntryCia];
29
+ }
30
+
31
+ /** A CIA at the entry: $xx00-$xx0f as a read without side effects gives them, and what no read gives. */
32
+ export interface EntryCia {
33
+ registers: Uint8Array;
34
+ latchA: number;
35
+ latchB: number;
36
+ interruptMask: number;
37
+ }
38
+
39
+ /**
40
+ * The VIC-II registers that the loader sets (D60), each with the bits that a
41
+ * read gives back: not the raster, the light pen, the interrupt registers or
42
+ * the collisions. The raster line comes from a wait instead.
43
+ */
44
+ export const VIC_DISPLAY_REGISTERS: ReadonlyArray<{ register: number; mask: number }> = [
45
+ ...Array.from({ length: 0x11 }, (_, register) => ({ register, mask: 0xff })),
46
+ { register: 0x11, mask: 0x7f },
47
+ { register: 0x15, mask: 0xff },
48
+ { register: 0x16, mask: 0x3f },
49
+ { register: 0x17, mask: 0xff },
50
+ { register: 0x18, mask: 0xfe },
51
+ { register: 0x1b, mask: 0xff },
52
+ { register: 0x1c, mask: 0xff },
53
+ { register: 0x1d, mask: 0xff },
54
+ ...Array.from({ length: 0x0f }, (_, index) => ({ register: 0x20 + index, mask: 0x0f })),
55
+ ];
56
+
57
+ /** The raster line of the entry, from $d011 bit 7 and $d012 as the capture read them. */
58
+ export function rasterLine(registers: Uint8Array): number {
59
+ return ((registers[0x11]! & 0x80) << 1) | registers[0x12]!;
60
+ }
61
+
62
+ const FLAG_BITS: Record<string, number> = { n: 0x80, v: 0x40, b: 0x10, d: 0x08, i: 0x04, z: 0x02, c: 0x01 };
63
+
64
+ /** Reads entry_state.json of the c64-unpacker skill. */
65
+ export function readEntryState(text: string, path: string): EntryState {
66
+ const fail = (what: string): never => {
67
+ throw new WireFailure("invalid-input", `${path} is not an entry_state.json of the c64-unpacker skill: ${what}.`);
68
+ };
69
+ let value: Record<string, any>;
70
+ try {
71
+ value = JSON.parse(text) as Record<string, any>;
72
+ } catch (error) {
73
+ return fail((error as Error).message);
74
+ }
75
+ const registers = value.registers;
76
+ const byte = (item: unknown, what: string) => (Number.isInteger(item) && (item as number) >= 0 && (item as number) <= 0xff ? (item as number) : fail(`${what} is not a byte`));
77
+ if (typeof registers !== "object" || registers === null) return fail("it has no registers");
78
+ const pc = typeof registers.pc === "string" && /^\$[0-9a-f]{4}$/i.test(registers.pc) ? Number.parseInt(registers.pc.slice(1), 16) : fail("registers.pc is not a $ address");
79
+ if (typeof registers.flags !== "object" || registers.flags === null) fail("registers.flags is missing");
80
+ let flags = 0x20;
81
+ for (const [name, bit] of Object.entries(FLAG_BITS)) if (registers.flags[name] === true) flags |= bit;
82
+ const pages = new Uint8Array(0x400);
83
+ for (let page = 0; page < 4; page++) {
84
+ const hex = value.pages?.[formatC64Address(page << 8)];
85
+ if (typeof hex !== "string" || !/^[0-9a-f]{512}$/i.test(hex)) fail(`pages.${formatC64Address(page << 8)} is not 256 bytes of hex`);
86
+ pages.set(Buffer.from(hex as string, "hex"), page << 8);
87
+ }
88
+ return {
89
+ pc,
90
+ a: byte(registers.a, "registers.a"),
91
+ x: byte(registers.x, "registers.x"),
92
+ y: byte(registers.y, "registers.y"),
93
+ sp: byte(registers.sp, "registers.sp"),
94
+ flags,
95
+ port: [byte(value.cpuPort?.$00, "cpuPort.$00"), byte(value.cpuPort?.$01, "cpuPort.$01")],
96
+ pages,
97
+ ...readVic(value.io, fail),
98
+ ...readCia(value, fail),
99
+ };
100
+ }
101
+
102
+ export interface Copy {
103
+ /** What the copy writes: a segment or a part of pages 0-3. */
104
+ what: string;
105
+ target: Range;
106
+ source: Range;
107
+ /** True when the copy runs from the last byte down: its target is above an overlapping source. */
108
+ backward: boolean;
109
+ }
110
+
111
+ /** What the loader holds, in its order: a segment or a part of pages 0-3, packed at an address of the loader. */
112
+ export interface PackedBlock {
113
+ what: string;
114
+ range: Range;
115
+ packedAt: number;
116
+ bytes: (address: number) => number;
117
+ }
118
+
119
+ export interface LoaderPlan {
120
+ blocks: PackedBlock[];
121
+ copies: Copy[];
122
+ /** The first of the two free pages where the copier runs. */
123
+ copier: number;
124
+ /** The address of the free stack byte below the stack pointer, which the flags pass through. */
125
+ stackByte: number;
126
+ end: number;
127
+ text: string;
128
+ /** The CIA timers and masks that the loader sets, when the entry state has them (D63). */
129
+ cia?: CiaPlan;
130
+ }
131
+
132
+ /** The label of the loader's jump to the entry, in ACME's symbol list. */
133
+ export const LOADER_JUMP = "c64_source_loader_jump";
134
+
135
+ const LOAD = 0x0801;
136
+ const STUB = 0x080d;
137
+ /** sei, ldx, the 2-page move loop and jmp. */
138
+ const STUB_SIZE = 21;
139
+ /** The copier code and its three variables, without the table. */
140
+ const COPIER_CODE_SIZE = 167;
141
+ /** The VIC-II code of the copier (D60), without its two tables of one byte per register. */
142
+ const VIC_CODE_SIZE = 44;
143
+ const VIC_SIZE = VIC_CODE_SIZE + 2 * VIC_DISPLAY_REGISTERS.length;
144
+ /** The copier and the stub's move of it: two pages. */
145
+ const COPIER_LIMIT = 0x200;
146
+ const COPY_ENTRY = 7;
147
+ /** X indexes the table, so the table fits in 256 bytes, and the copier with the VIC-II code fits in two pages. */
148
+ export const MAX_COPIES = Math.min(Math.floor((256 - 2) / COPY_ENTRY), Math.floor((COPIER_LIMIT - COPIER_CODE_SIZE - VIC_SIZE - 2) / COPY_ENTRY));
149
+ /** A loader that loads into $d000 or higher would write the I/O area. */
150
+ const LOAD_LIMIT = 0xcfff;
151
+ const COPIER_PAGES = [...Array.from({ length: 15 }, (_, index) => 0xc000 + index * 0x100), ...Array.from({ length: 0x9b }, (_, index) => 0x9e00 - index * 0x100)];
152
+
153
+ const hex2 = (value: number) => `$${value.toString(16).padStart(2, "0")}`;
154
+ const hex4 = formatC64Address;
155
+ const overlaps = (a: Range, b: Range) => a.start <= b.end && b.start <= a.end;
156
+ const show = (range: Range) => `${hex4(range.start)}-${hex4(range.end)}`;
157
+
158
+ /** Parts of `range` that no range in `holes` covers. */
159
+ function subtract(range: Range, holes: Range[]): Range[] {
160
+ let pieces = [range];
161
+ for (const hole of holes) {
162
+ pieces = pieces.flatMap((piece) => {
163
+ if (!overlaps(piece, hole)) return [piece];
164
+ return [...(hole.start > piece.start ? [{ start: piece.start, end: hole.start - 1 }] : []), ...(hole.end < piece.end ? [{ start: hole.end + 1, end: piece.end }] : [])];
165
+ });
166
+ }
167
+ return pieces;
168
+ }
169
+
170
+ /** Orders the copies so that no copy writes over the source of a later one; undefined when they form a cycle. */
171
+ export function copyOrder(copies: Array<Omit<Copy, "backward">>): number[] | undefined {
172
+ const order: number[] = [];
173
+ const done = new Set<number>();
174
+ while (order.length < copies.length) {
175
+ const next = copies.findIndex((copy, index) => !done.has(index) && copies.every((other, at) => at === index || done.has(at) || !overlaps(copy.target, other.source)));
176
+ if (next < 0) return undefined;
177
+ order.push(next);
178
+ done.add(next);
179
+ }
180
+ return order;
181
+ }
182
+
183
+ /** Runs the copies on a memory model as the copier does, byte by byte; returns the first wrong target byte. */
184
+ export function simulate(memory: Int16Array, copies: Copy[], expected: (address: number) => number): number | undefined {
185
+ for (const copy of copies) {
186
+ const length = copy.target.end - copy.target.start + 1;
187
+ for (let step = 0; step < length; step++) {
188
+ const offset = copy.backward ? length - 1 - step : step;
189
+ memory[copy.target.start + offset] = memory[copy.source.start + offset]!;
190
+ }
191
+ }
192
+ for (const copy of copies) for (let address = copy.target.start; address <= copy.target.end; address++) if (memory[address] !== expected(address)) return address;
193
+ return undefined;
194
+ }
195
+
196
+ /** The VIC-II and its bank from the io part of an entry state, when it has them. */
197
+ function readVic(io: Record<string, any> | undefined, fail: (what: string) => never): { vic: NonNullable<EntryState["vic"]> } | Record<string, never> {
198
+ if (io?.vic === undefined) return {};
199
+ const vic = io.vic.bytes;
200
+ const cia2 = io.cia2?.bytes;
201
+ if (typeof vic !== "string" || !/^[0-9a-f]{94}$/i.test(vic)) return fail("io.vic.bytes is not 47 bytes of hex");
202
+ if (typeof cia2 !== "string" || !/^[0-9a-f]{2}/i.test(cia2)) return fail("io.cia2.bytes is missing");
203
+ return { vic: { registers: Buffer.from(vic, "hex"), bank: Number.parseInt(cia2.slice(0, 2), 16) & 0x03 } };
204
+ }
205
+
206
+ function readCia(value: Record<string, any>, fail: (what: string) => never): { cia: [EntryCia, EntryCia] } | Record<string, never> {
207
+ if (value.cia === undefined) return {};
208
+ const chip = (name: "cia1" | "cia2"): EntryCia => {
209
+ const bytes = value.io?.[name]?.bytes;
210
+ if (typeof bytes !== "string" || !/^[0-9a-f]{32}$/i.test(bytes)) return fail(`io.${name}.bytes is not 16 bytes of hex`);
211
+ const hidden = value.cia[name];
212
+ const number = (item: unknown, what: string, max: number) => (Number.isInteger(item) && (item as number) >= 0 && (item as number) <= max ? (item as number) : fail(`cia.${name}.${what} is not a number from 0 to ${max}`));
213
+ return { registers: Buffer.from(bytes, "hex"), latchA: number(hidden?.latchA, "latchA", 0xffff), latchB: number(hidden?.latchB, "latchB", 0xffff), interruptMask: number(hidden?.interruptMask, "interruptMask", 0x1f) };
214
+ };
215
+ return { cia: [chip("cia1"), chip("cia2")] };
216
+ }
217
+
218
+ /** One CIA timer that the loader sets, and what the check of the loader expects of it at the jump. */
219
+ export interface CiaTimerPlan {
220
+ cia: 1 | 2;
221
+ timer: "A" | "B";
222
+ /** The control register as a read gives it: without bit 4, the force load. */
223
+ control: number;
224
+ latch: number;
225
+ /** The counter at the jump, 3 cycles before the entry; undefined when the loader cannot set it. */
226
+ counterAtJump?: number;
227
+ }
228
+
229
+ export interface CiaPlan {
230
+ timers: CiaTimerPlan[];
231
+ masks: [number, number];
232
+ /** What the loader cannot set as it was at the entry. */
233
+ limits: string[];
234
+ }
235
+
236
+ /**
237
+ * The CIA timers as the KERNAL leaves them when RUN starts the loader, on a
238
+ * PAL C64: only CIA 1 timer A runs, for the 60 Hz interrupt. A timer that the
239
+ * entry has in this state is not written. The loader check reads every timer.
240
+ */
241
+ const KERNAL_TIMERS: ReadonlyArray<{ control: number; latch: number; counter?: number }> = [
242
+ { control: 0x01, latch: 0x4025 },
243
+ { control: 0x08, latch: 0xffff, counter: 0xffff },
244
+ { control: 0x08, latch: 0xffff, counter: 0xffff },
245
+ { control: 0x08, latch: 0xffff, counter: 0xffff },
246
+ ];
247
+ const KERNAL_MASKS: readonly [number, number] = [0x01, 0x00];
248
+ /** Cycles from the end of the CIA part to the entry: $00, $01, the stack pointer, the flags, A, X, Y, PLP and JMP. */
249
+ const TAIL_CYCLES = 33;
250
+ /** Cycles after the write that starts a CIA timer in which it does not count yet, measured in VICE. */
251
+ const CIA_START_DELAY = 1;
252
+ /** Cycles of one raster line of a PAL C64. */
253
+ const LINE_CYCLES = 63;
254
+ const PAL_LINES = 312;
255
+
256
+ interface Op {
257
+ text: string;
258
+ bytes: number;
259
+ cycles: number;
260
+ }
261
+
262
+ /**
263
+ * The copier's CIA part (D63), after the raster wait: the timers and
264
+ * interrupt masks of the entry state. A running timer is stopped, loaded
265
+ * with its counter at the entry plus the cycles to the entry, started, and
266
+ * given its latch, so it counts to the entry's value just at the entry.
267
+ * Then the interrupt flags that the copy left are read away.
268
+ */
269
+ export function ciaSection(state: EntryState): { ops: Op[]; plan: CiaPlan } | undefined {
270
+ if (state.cia === undefined) return undefined;
271
+ const lda = (value: number): Op => ({ text: `lda #${hex2(value)}`, bytes: 2, cycles: 2 });
272
+ const sta = (address: number): Op => ({ text: `sta ${hex4(address)}`, bytes: 3, cycles: 4 });
273
+ const limits: string[] = [];
274
+ const timers: CiaTimerPlan[] = [];
275
+ const disable: Op[] = [];
276
+ const loads: Array<Op | ((cycles: number[]) => Op)> = [];
277
+ const starts: Op[] = [];
278
+ const latches: Op[] = [];
279
+ const acks: Op[] = [];
280
+ const masks: Op[] = [];
281
+ /** For each running timer: its index in starts, and how its load value follows from the cycles after its start. */
282
+ const running: Array<{ start: number; counter: number; latch: number; system: boolean; name: string; plan: CiaTimerPlan }> = [];
283
+ state.cia.forEach((chip, index) => {
284
+ const id = (index + 1) as 1 | 2;
285
+ const base = index === 0 ? 0xdc00 : 0xdd00;
286
+ let touched = false;
287
+ for (const timer of ["A", "B"] as const) {
288
+ const at = timer === "A" ? 0x04 : 0x06;
289
+ const controlAt = base + (timer === "A" ? 0x0e : 0x0f);
290
+ const control = chip.registers[timer === "A" ? 0x0e : 0x0f]! & 0xef;
291
+ const counter = chip.registers[at]! | (chip.registers[at + 1]! << 8);
292
+ const latch = timer === "A" ? chip.latchA : chip.latchB;
293
+ const name = `CIA ${id} timer ${timer}`;
294
+ const plan: CiaTimerPlan = { cia: id, timer, control, latch, counterAtJump: counter };
295
+ timers.push(plan);
296
+ const kernal = KERNAL_TIMERS[index * 2 + (timer === "A" ? 0 : 1)]!;
297
+ const runs = (control & 0x01) !== 0;
298
+ if (!runs && control === kernal.control && latch === kernal.latch && counter === kernal.counter) continue;
299
+ touched = true;
300
+ // Stopped, a write of the high latch byte also loads the counter.
301
+ loads.push(lda(control & 0xee), sta(controlAt));
302
+ if (runs) {
303
+ // Timer A counts system cycles without bit 5, timer B without bits 5 and 6; other inputs do not count in the loader.
304
+ const system = timer === "A" ? (control & 0x20) === 0 : (control & 0x60) === 0;
305
+ const slot = running.length;
306
+ running.push({ start: starts.length, counter, latch, system, name, plan });
307
+ loads.push((values) => lda(values[slot]! & 0xff), sta(base + at), (values) => lda(values[slot]! >> 8), sta(base + at + 1));
308
+ starts.push(lda(control), sta(controlAt));
309
+ latches.push(lda(latch & 0xff), sta(base + at), lda(latch >> 8), sta(base + at + 1));
310
+ if (system) plan.counterAtJump = counter + 3;
311
+ continue;
312
+ }
313
+ loads.push(lda(counter & 0xff), sta(base + at), lda(counter >> 8), sta(base + at + 1));
314
+ if (latch === counter) continue;
315
+ if (latch >> 8 === counter >> 8) {
316
+ loads.push(lda(latch & 0xff), sta(base + at));
317
+ continue;
318
+ }
319
+ // The high byte loads the counter of a stopped timer too.
320
+ loads.push(lda(latch & 0xff), sta(base + at), lda(latch >> 8), sta(base + at + 1));
321
+ plan.counterAtJump = latch;
322
+ limits.push(`${name} is stopped with the counter $${counter.toString(16).padStart(4, "0")} and the latch $${latch.toString(16).padStart(4, "0")}: a stopped CIA timer loads its latch, so the counter is the latch.`);
323
+ }
324
+ const mask = chip.interruptMask;
325
+ if (mask !== KERNAL_MASKS[index]) {
326
+ disable.push(lda(0x7f), sta(base + 0x0d));
327
+ if (mask !== 0) masks.push(lda(0x80 | mask), sta(base + 0x0d));
328
+ touched = true;
329
+ }
330
+ if (touched) acks.push({ text: `lda ${hex4(base + 0x0d)}`, bytes: 3, cycles: 4 });
331
+ const flags = chip.registers[0x0d]! & 0x1f;
332
+ if (flags !== 0) limits.push(`CIA ${id} has the interrupt flags $${flags.toString(16).padStart(2, "0")} at the entry: only an event sets a flag, so the loader cannot.`);
333
+ });
334
+ if (loads.length === 0 && disable.length === 0) return { ops: [], plan: { timers, masks: [state.cia[0].interruptMask, state.cia[1].interruptMask], limits } };
335
+ // The cycles from each start write to the entry: what follows it here, and the tail.
336
+ const after = [...starts, ...latches, ...acks, ...masks];
337
+ const values = running.map((item) => {
338
+ const followers = after.slice(item.start + 2).reduce((sum, op) => sum + op.cycles, 0);
339
+ if (!item.system) return item.counter;
340
+ const load = item.counter + followers + TAIL_CYCLES - CIA_START_DELAY;
341
+ if (load <= 0xffff) return load;
342
+ limits.push(`${item.name} counts from $${item.counter.toString(16).padStart(4, "0")} at the entry: the loader cannot start it so near $ffff without an underflow, so the counter differs.`);
343
+ delete item.plan.counterAtJump;
344
+ return item.counter;
345
+ });
346
+ const pre: Op[] = state.vic === undefined ? [lda(0x35), { text: "sta $01", bytes: 2, cycles: 3 }] : [];
347
+ const ops = [...pre, ...disable, ...loads.map((op) => (typeof op === "function" ? op(values) : op)), ...after];
348
+ return { ops, plan: { timers, masks: [state.cia[0].interruptMask, state.cia[1].interruptMask], limits } };
349
+ }
350
+
351
+ export interface LoaderInput {
352
+ /** The segments with the files that hold them, in address order. */
353
+ segments: Array<Range & { files: string[] }>;
354
+ byteAt: (address: number) => number;
355
+ state: EntryState;
356
+ entry: number;
357
+ /** The label of the entry in the source: the loader jumps to it, so the jump follows the code when it moves (D66). */
358
+ entryName?: string;
359
+ header: string[];
360
+ }
361
+
362
+ /** The label of a block (D66): its first byte and the byte after it in the loader file, and its first address where it runs. */
363
+ const blockLabel = (start: number, part: "from" | "end" | "to") => `loader_${part}_${start.toString(16).padStart(4, "0")}`;
364
+ const plus = (label: string, offset: number) => (offset === 0 ? label : `${label}+${offset}`);
365
+ /** The label of the copier in the loader file, which the stub moves to its two pages. */
366
+ export const COPIER_SOURCE = "loader_copier_src";
367
+
368
+ /** Plans the loader: the packed layout, the copy order and the copier page, and writes loader.asm. Refuses by name when no plan exists. */
369
+ export function planLoader(input: LoaderInput): LoaderPlan {
370
+ const { state } = input;
371
+ // What the copies write: each segment without $00/$01, which the loader sets last, and pages $0002-$03ff where no segment is.
372
+ const blocks: Array<{ what: string; range: Range; files?: string[]; pseudo: number; bytes: (address: number) => number }> = [];
373
+ for (const segment of input.segments) blocks.push({ what: `segment ${show(segment)}`, range: segment, files: segment.files, pseudo: segment.start, bytes: input.byteAt });
374
+ for (const part of subtract({ start: 0x0002, end: 0x03ff }, input.segments)) {
375
+ blocks.push({ what: `entry state ${show(part)}`, range: part, pseudo: part.start, bytes: (address) => state.pages[address]! });
376
+ }
377
+ blocks.sort((a, b) => a.range.start - b.range.start);
378
+
379
+ let at = STUB + STUB_SIZE;
380
+ const plain: Array<Omit<Copy, "backward">> = [];
381
+ const packedAt: number[] = [];
382
+ /** The block start of each copy in `plain`, which names its labels. */
383
+ const blockOf: number[] = [];
384
+ for (const block of blocks) {
385
+ packedAt.push(at);
386
+ const target = { start: Math.max(block.range.start, 0x0002), end: block.range.end };
387
+ if (target.start <= target.end) {
388
+ const source = { start: at + (target.start - block.range.start), end: at + (target.end - block.range.start) };
389
+ plain.push({ what: block.what, target, source });
390
+ blockOf.push(block.range.start);
391
+ }
392
+ at += block.range.end - block.range.start + 1;
393
+ }
394
+ const cia = ciaSection(state);
395
+ const ciaSize = cia === undefined ? 0 : cia.ops.reduce((sum, op) => sum + op.bytes, 0);
396
+ // The CIA part takes room from the copy table in the copier's two pages.
397
+ const maxCopies = Math.min(MAX_COPIES, Math.floor((COPIER_LIMIT - COPIER_CODE_SIZE - (state.vic === undefined ? 0 : VIC_SIZE) - ciaSize - 2) / COPY_ENTRY));
398
+ if (plain.length > maxCopies) throw new WireFailure("invalid-input", `The loader needs ${plain.length} copies; it can do at most ${maxCopies}. Join segments in the unpacker capture (--range) or write the loader by hand.`);
399
+ const copierImage = at;
400
+ const copierSize = COPIER_CODE_SIZE + (state.vic === undefined ? 0 : VIC_SIZE) + ciaSize + COPY_ENTRY * plain.length + 2;
401
+ const end = copierImage + copierSize - 1;
402
+ if (end > LOAD_LIMIT) throw new WireFailure("invalid-input", `The loader would end at ${hex4(end)}, after ${hex4(LOAD_LIMIT)}: loading it would write the I/O area. The image is too large for one loader file.`);
403
+
404
+ const order = copyOrder(plain);
405
+ if (order === undefined) {
406
+ const clashing = plain.filter((copy) => plain.some((other) => other !== copy && overlaps(copy.target, other.source))).map((copy) => `${copy.what} (packed at ${show(copy.source)})`);
407
+ throw new WireFailure("invalid-input", `No copy order exists: each of these copies writes over the packed bytes of another one: ${clashing.join(", ")}. Write the loader by hand.`);
408
+ }
409
+ const loaded = { start: LOAD, end };
410
+ const copier = COPIER_PAGES.find((page) => {
411
+ const pages = { start: page, end: page + 0x1ff };
412
+ return !overlaps(pages, loaded) && plain.every((copy) => !overlaps(pages, copy.target));
413
+ });
414
+ if (copier === undefined) throw new WireFailure("invalid-input", `No two free pages exist for the copier in $0400-$9fff or $c000-$cfff outside the loader (${show(loaded)}) and the targets: ${plain.map((copy) => show(copy.target)).join(", ")}.`);
415
+ const copies: Copy[] = order.map((index) => {
416
+ const copy = plain[index]!;
417
+ return { ...copy, backward: copy.target.start > copy.source.start && overlaps(copy.target, copy.source) };
418
+ });
419
+ const copyBlocks = order.map((index) => blockOf[index]!);
420
+
421
+ const stackByte = 0x0100 + state.sp;
422
+ const lines: string[] = [...input.header.map((line) => `; ${line}`), "", "!cpu 6510", '!source "constants.asm"', "", `* = ${hex4(LOAD)}`];
423
+ lines.push(` !byte $0b,$08,$0a,$00,$9e,${[...String(STUB)].map((digit) => hex2(digit.charCodeAt(0))).join(",")},$00,$00,$00 ; 10 SYS ${STUB}`);
424
+ lines.push(
425
+ "!zone loader_stub {",
426
+ " sei",
427
+ " ldx #0",
428
+ `- lda ${COPIER_SOURCE},x`,
429
+ ` sta ${hex4(copier)},x`,
430
+ ` lda ${COPIER_SOURCE}+$100,x`,
431
+ ` sta ${hex4(copier + 0x100)},x`,
432
+ " inx",
433
+ " bne -",
434
+ ` jmp ${hex4(copier)}`,
435
+ "}",
436
+ );
437
+ // The labels around each block give the copy table its numbers, so a block can change its length (D66).
438
+ for (const block of blocks) {
439
+ lines.push("", `; ${block.what}`, blockLabel(block.range.start, "from"), `!pseudopc ${hex4(block.pseudo)} {`, blockLabel(block.range.start, "to"));
440
+ if (block.files !== undefined) for (const file of block.files) lines.push(`!source "${file}"`);
441
+ else {
442
+ for (let address = block.range.start; address <= block.range.end; address += 16) {
443
+ const last = Math.min(block.range.end, address + 15);
444
+ lines.push(` !byte ${Array.from({ length: last - address + 1 }, (_, offset) => hex2(block.bytes(address + offset))).join(",")}`);
445
+ }
446
+ }
447
+ lines.push("}", blockLabel(block.range.start, "end"));
448
+ }
449
+ const [port0, port1] = state.port;
450
+ lines.push(
451
+ "",
452
+ `; The copier runs at ${hex4(copier)}. The flags pass through ${hex4(stackByte)}, the free stack byte below the stack pointer.`,
453
+ COPIER_SOURCE,
454
+ `!pseudopc ${hex4(copier)} {`,
455
+ "!zone loader_copier {",
456
+ " lda #$34",
457
+ " sta $01",
458
+ " ldx #0",
459
+ ".next lda .table,x",
460
+ " sta .count",
461
+ " lda .table+1,x",
462
+ " sta .count+1",
463
+ " ora .count",
464
+ " beq .done",
465
+ " lda .table+2,x",
466
+ " sta .read+1",
467
+ " lda .table+3,x",
468
+ " sta .read+2",
469
+ " lda .table+4,x",
470
+ " sta .write+1",
471
+ " lda .table+5,x",
472
+ " sta .write+2",
473
+ " lda .table+6,x",
474
+ " sta .back",
475
+ " stx .index",
476
+ ".read lda $ffff",
477
+ ".write sta $ffff",
478
+ " lda .back",
479
+ " bne .down",
480
+ " inc .read+1",
481
+ " bne +",
482
+ " inc .read+2",
483
+ "+ inc .write+1",
484
+ " bne +",
485
+ " inc .write+2",
486
+ "+ jmp .counted",
487
+ ".down lda .read+1",
488
+ " bne +",
489
+ " dec .read+2",
490
+ "+ dec .read+1",
491
+ " lda .write+1",
492
+ " bne +",
493
+ " dec .write+2",
494
+ "+ dec .write+1",
495
+ ".counted",
496
+ " lda .count",
497
+ " bne +",
498
+ " dec .count+1",
499
+ "+ dec .count",
500
+ " lda .count",
501
+ " ora .count+1",
502
+ " bne .read",
503
+ " lda .index",
504
+ " clc",
505
+ " adc #7",
506
+ " tax",
507
+ " jmp .next",
508
+ ".done",
509
+ // The CIA part runs after the raster wait: the wait ends its whole lines earlier, so the entry comes in its line.
510
+ ...vicLines(state, Math.floor((cia?.ops.reduce((sum, op) => sum + op.cycles, 0) ?? 0) / LINE_CYCLES)),
511
+ ...(cia === undefined ? [] : cia.ops.map((op) => ` ${op.text}`)),
512
+ ` lda #${hex2(port0)}`,
513
+ " sta $00",
514
+ ` lda #${hex2(port1)}`,
515
+ " sta $01",
516
+ ` ldx #${hex2((state.sp - 1) & 0xff)}`,
517
+ " txs",
518
+ ` lda #${hex2(state.flags)}`,
519
+ ` sta ${hex4(stackByte)}`,
520
+ ` lda #${hex2(state.a)}`,
521
+ ` ldx #${hex2(state.x)}`,
522
+ ` ldy #${hex2(state.y)}`,
523
+ " plp",
524
+ // A global label: the check of the loader stops here, before an interrupt can run (F-32).
525
+ `${LOADER_JUMP} jmp ${input.entryName ?? hex4(input.entry)}`,
526
+ ".count !word 0",
527
+ ".index !byte 0",
528
+ ".back !byte 0",
529
+ ...(state.vic === undefined
530
+ ? []
531
+ : [
532
+ `.vicreg !byte ${VIC_DISPLAY_REGISTERS.map((item) => hex2(item.register)).join(",")}`,
533
+ `.vicval !byte ${VIC_DISPLAY_REGISTERS.map((item) => hex2(state.vic!.registers[item.register]!)).join(",")}`,
534
+ ]),
535
+ ".table",
536
+ );
537
+ copies.forEach((copy, index) => {
538
+ // A block that starts at $0000 skips $00/$01, which the loader sets last.
539
+ const start = copyBlocks[index]!;
540
+ const skip = copy.target.start - start;
541
+ const [from, end, to] = [blockLabel(start, "from"), blockLabel(start, "end"), blockLabel(start, "to")];
542
+ const length = skip === 0 ? `${end} - ${from}` : `${end} - ${from} - ${skip}`;
543
+ const read = copy.backward ? `${end} - 1` : plus(from, skip);
544
+ const write = copy.backward ? `${to} + ${end} - ${from} - 1` : plus(to, skip);
545
+ lines.push(` !word ${length}, ${read}, ${write} : !byte ${copy.backward ? 1 : 0} ; ${copy.what}`);
546
+ });
547
+ lines.push(" !word 0", "}", "}");
548
+
549
+ return {
550
+ blocks: blocks.map((block, index) => ({ what: block.what, range: block.range, packedAt: packedAt[index]!, bytes: block.bytes })),
551
+ copies,
552
+ copier,
553
+ stackByte,
554
+ end,
555
+ text: `${lines.join("\n")}\n`,
556
+ ...(cia === undefined ? {} : { cia: cia.plan }),
557
+ };
558
+ }
559
+
560
+ /**
561
+ * The copier's VIC-II part (D60): with the I/O area visible it sets the
562
+ * display registers and the VIC bank of the entry state, then waits for the
563
+ * entry's raster line, so the program starts in the same phase of the frame.
564
+ */
565
+ function vicLines(state: EntryState, earlier: number): string[] {
566
+ if (state.vic === undefined) return [];
567
+ const line = (rasterLine(state.vic.registers) - earlier + PAL_LINES) % PAL_LINES;
568
+ return [
569
+ " lda #$35",
570
+ " sta $01",
571
+ ` ldx #${hex2(VIC_DISPLAY_REGISTERS.length - 1)}`,
572
+ ".vic ldy .vicreg,x",
573
+ " lda .vicval,x",
574
+ " sta $d000,y",
575
+ " dex",
576
+ " bpl .vic",
577
+ " lda $dd00",
578
+ " and #$fc",
579
+ ` ora #${hex2(state.vic.bank)}`,
580
+ " sta $dd00",
581
+ `.raster lda $d012`,
582
+ ` cmp #${hex2(line & 0xff)}`,
583
+ " bne .raster",
584
+ " lda $d011",
585
+ " and #$80",
586
+ ` cmp #${hex2((line >> 1) & 0x80)}`,
587
+ " bne .raster",
588
+ ];
589
+ }
@@ -0,0 +1,61 @@
1
+ // The ACME spelling of each 6510 opcode (D48): the mnemonic and the addressing
2
+ // mode, undocumented opcodes included. Ghidra gives the instruction
3
+ // boundaries; this table gives the text. Where several opcodes share one
4
+ // spelling, ACME writes only one of them: the generator finds those with a
5
+ // probe and writes the others as bytes.
6
+
7
+ export type Mode = "imp" | "imm" | "zp" | "zpx" | "zpy" | "abs" | "abx" | "aby" | "ind" | "izx" | "izy" | "rel";
8
+
9
+ export interface Opcode {
10
+ mnemonic: string;
11
+ mode: Mode;
12
+ }
13
+
14
+ const ROWS = `
15
+ 00 brk imp|01 ora izx|02 jam imp|03 slo izx|04 nop zp|05 ora zp|06 asl zp|07 slo zp|08 php imp|09 ora imm|0a asl imp|0b anc imm|0c nop abs|0d ora abs|0e asl abs|0f slo abs
16
+ 10 bpl rel|11 ora izy|12 jam imp|13 slo izy|14 nop zpx|15 ora zpx|16 asl zpx|17 slo zpx|18 clc imp|19 ora aby|1a nop imp|1b slo aby|1c nop abx|1d ora abx|1e asl abx|1f slo abx
17
+ 20 jsr abs|21 and izx|22 jam imp|23 rla izx|24 bit zp|25 and zp|26 rol zp|27 rla zp|28 plp imp|29 and imm|2a rol imp|2b anc imm|2c bit abs|2d and abs|2e rol abs|2f rla abs
18
+ 30 bmi rel|31 and izy|32 jam imp|33 rla izy|34 nop zpx|35 and zpx|36 rol zpx|37 rla zpx|38 sec imp|39 and aby|3a nop imp|3b rla aby|3c nop abx|3d and abx|3e rol abx|3f rla abx
19
+ 40 rti imp|41 eor izx|42 jam imp|43 sre izx|44 nop zp|45 eor zp|46 lsr zp|47 sre zp|48 pha imp|49 eor imm|4a lsr imp|4b asr imm|4c jmp abs|4d eor abs|4e lsr abs|4f sre abs
20
+ 50 bvc rel|51 eor izy|52 jam imp|53 sre izy|54 nop zpx|55 eor zpx|56 lsr zpx|57 sre zpx|58 cli imp|59 eor aby|5a nop imp|5b sre aby|5c nop abx|5d eor abx|5e lsr abx|5f sre abx
21
+ 60 rts imp|61 adc izx|62 jam imp|63 rra izx|64 nop zp|65 adc zp|66 ror zp|67 rra zp|68 pla imp|69 adc imm|6a ror imp|6b arr imm|6c jmp ind|6d adc abs|6e ror abs|6f rra abs
22
+ 70 bvs rel|71 adc izy|72 jam imp|73 rra izy|74 nop zpx|75 adc zpx|76 ror zpx|77 rra zpx|78 sei imp|79 adc aby|7a nop imp|7b rra aby|7c nop abx|7d adc abx|7e ror abx|7f rra abx
23
+ 80 nop imm|81 sta izx|82 nop imm|83 sax izx|84 sty zp|85 sta zp|86 stx zp|87 sax zp|88 dey imp|89 nop imm|8a txa imp|8b ane imm|8c sty abs|8d sta abs|8e stx abs|8f sax abs
24
+ 90 bcc rel|91 sta izy|92 jam imp|93 sha izy|94 sty zpx|95 sta zpx|96 stx zpy|97 sax zpy|98 tya imp|99 sta aby|9a txs imp|9b tas aby|9c shy abx|9d sta abx|9e shx aby|9f sha aby
25
+ a0 ldy imm|a1 lda izx|a2 ldx imm|a3 lax izx|a4 ldy zp|a5 lda zp|a6 ldx zp|a7 lax zp|a8 tay imp|a9 lda imm|aa tax imp|ab lxa imm|ac ldy abs|ad lda abs|ae ldx abs|af lax abs
26
+ b0 bcs rel|b1 lda izy|b2 jam imp|b3 lax izy|b4 ldy zpx|b5 lda zpx|b6 ldx zpy|b7 lax zpy|b8 clv imp|b9 lda aby|ba tsx imp|bb las aby|bc ldy abx|bd lda abx|be ldx aby|bf lax aby
27
+ c0 cpy imm|c1 cmp izx|c2 nop imm|c3 dcp izx|c4 cpy zp|c5 cmp zp|c6 dec zp|c7 dcp zp|c8 iny imp|c9 cmp imm|ca dex imp|cb sbx imm|cc cpy abs|cd cmp abs|ce dec abs|cf dcp abs
28
+ d0 bne rel|d1 cmp izy|d2 jam imp|d3 dcp izy|d4 nop zpx|d5 cmp zpx|d6 dec zpx|d7 dcp zpx|d8 cld imp|d9 cmp aby|da nop imp|db dcp aby|dc nop abx|dd cmp abx|de dec abx|df dcp abx
29
+ e0 cpx imm|e1 sbc izx|e2 nop imm|e3 isc izx|e4 cpx zp|e5 sbc zp|e6 inc zp|e7 isc zp|e8 inx imp|e9 sbc imm|ea nop imp|eb sbc imm|ec cpx abs|ed sbc abs|ee inc abs|ef isc abs
30
+ f0 beq rel|f1 sbc izy|f2 jam imp|f3 isc izy|f4 nop zpx|f5 sbc zpx|f6 inc zpx|f7 isc zpx|f8 sed imp|f9 sbc aby|fa nop imp|fb isc aby|fc nop abx|fd sbc abx|fe inc abx|ff isc abx`;
31
+
32
+ export const OPCODES: readonly Opcode[] = (() => {
33
+ const table: Opcode[] = [];
34
+ for (const cell of ROWS.trim().split(/[|\n]/)) {
35
+ const [code, mnemonic, mode] = cell.trim().split(" ");
36
+ table[Number.parseInt(code!, 16)] = { mnemonic: mnemonic!, mode: mode as Mode };
37
+ }
38
+ return table;
39
+ })();
40
+
41
+ export const MODE_LENGTH: Record<Mode, 1 | 2 | 3> = { imp: 1, imm: 2, zp: 2, zpx: 2, zpy: 2, izx: 2, izy: 2, rel: 2, abs: 3, abx: 3, aby: 3, ind: 3 };
42
+
43
+ /** Every mnemonic in the table: a symbol with one of these names would read as an instruction. */
44
+ export const MNEMONICS: ReadonlySet<string> = new Set(OPCODES.map((opcode) => opcode.mnemonic));
45
+
46
+ const ZERO_PAGE_OF: Partial<Record<Mode, Mode>> = { abs: "zp", abx: "zpx", aby: "zpy" };
47
+
48
+ /** True when the mnemonic also has the zero-page form of an absolute mode: ACME would pick it for a small operand. */
49
+ export function hasZeroPageForm(mnemonic: string, mode: Mode): boolean {
50
+ const zeroPage = ZERO_PAGE_OF[mode];
51
+ return zeroPage !== undefined && OPCODES.some((opcode) => opcode.mnemonic === mnemonic && opcode.mode === zeroPage);
52
+ }
53
+
54
+ /** True for a mode whose operand is an address (or a branch target), so a name can stand for it. */
55
+ export function takesAddress(mode: Mode): boolean {
56
+ return mode !== "imp" && mode !== "imm";
57
+ }
58
+
59
+ export function isZeroPageMode(mode: Mode): boolean {
60
+ return mode === "zp" || mode === "zpx" || mode === "zpy" || mode === "izx" || mode === "izy";
61
+ }