@henols/c64-re-tools 2.0.0-rc.3 → 2.0.0-rc.4

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 (73) hide show
  1. package/README.md +15 -3
  2. package/package.json +1 -1
  3. package/skills/c64-assembler/SKILL.md +5 -2
  4. package/skills/c64-assembler/scripts/assemble.ts +8 -1
  5. package/skills/c64-assembler/scripts/compare.ts +84 -0
  6. package/skills/c64-basic/SKILL.md +1 -2
  7. package/skills/c64-disk/SKILL.md +20 -6
  8. package/skills/c64-disk/scripts/containers.ts +124 -0
  9. package/skills/c64-disk/scripts/disk.ts +59 -5
  10. package/skills/c64-emulator/SKILL.md +11 -4
  11. package/skills/c64-knowledge/SKILL.md +53 -3
  12. package/skills/c64-knowledge/scripts/knowledge.ts +258 -29
  13. package/skills/c64-memory-map/SKILL.md +4 -1
  14. package/skills/c64-memory-map/scripts/memmap.ts +14 -3
  15. package/skills/c64-provenance/SKILL.md +37 -9
  16. package/skills/c64-provenance/scripts/compare.ts +60 -29
  17. package/skills/c64-provenance/scripts/provenance.ts +248 -19
  18. package/skills/c64-provenance/scripts/runtime.ts +179 -0
  19. package/skills/c64-provenance/scripts/separate.ts +132 -0
  20. package/skills/c64-reverse-engineering/SKILL.md +65 -11
  21. package/skills/c64-source/SKILL.md +119 -0
  22. package/skills/c64-source/scripts/generate.ts +402 -0
  23. package/skills/c64-source/scripts/loader.ts +320 -0
  24. package/skills/c64-source/scripts/opcodes.ts +61 -0
  25. package/skills/c64-source/scripts/source.ts +558 -0
  26. package/skills/c64-static-analysis/SKILL.md +27 -4
  27. package/skills/c64-static-analysis/scripts/analyze.ts +102 -13
  28. package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
  29. package/skills/c64-static-analysis/scripts/findings.ts +6 -4
  30. package/skills/c64-static-analysis/scripts/idioms.ts +144 -0
  31. package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
  32. package/skills/c64-testing/SKILL.md +27 -3
  33. package/skills/c64-testing/references/scenario-format.md +43 -3
  34. package/skills/c64-testing/scripts/run.ts +189 -0
  35. package/skills/c64-testing/scripts/scenario.ts +153 -135
  36. package/skills/c64-testing/scripts/test.ts +16 -6
  37. package/skills/c64-unpacker/SKILL.md +65 -14
  38. package/skills/c64-unpacker/scripts/evidence.ts +86 -21
  39. package/skills/c64-unpacker/scripts/loads.ts +174 -0
  40. package/skills/c64-unpacker/scripts/machine.ts +68 -0
  41. package/skills/c64-unpacker/scripts/unpack.ts +302 -48
  42. package/src/c64.ts +91 -0
  43. package/src/cli/status.ts +3 -1
  44. package/src/host/server.ts +6 -5
  45. package/src/host/vice/adapter.ts +25 -10
  46. package/src/host/vice/session.ts +64 -20
  47. package/src/host-client/autostart.ts +45 -0
  48. package/src/host-client/connect.ts +4 -2
  49. package/src/host-client/coverage.ts +59 -0
  50. package/src/host-client/steps.ts +170 -0
  51. package/src/host-client/transfer.ts +29 -3
  52. package/src/host-client/vice-session.ts +42 -13
  53. package/src/knowledge/history.ts +40 -13
  54. package/src/knowledge/import.ts +72 -56
  55. package/src/knowledge/read.ts +203 -52
  56. package/src/knowledge/schema.ts +94 -12
  57. package/src/knowledge/write.ts +432 -123
  58. package/src/mcp/output.ts +32 -0
  59. package/src/mcp/server.ts +4 -2
  60. package/src/mcp/tools/debug.ts +110 -24
  61. package/src/mcp/tools/input.ts +1 -0
  62. package/src/mcp/tools/media.ts +22 -6
  63. package/src/mcp/tools/memory.ts +57 -10
  64. package/src/mcp/tools/video.ts +9 -3
  65. package/src/native/discover.ts +5 -2
  66. package/src/native/ghidra/index.ts +19 -3
  67. package/src/native/ghidra/scripts/C64Export.java +33 -1
  68. package/src/native/status.ts +15 -3
  69. package/src/native/types.ts +30 -1
  70. package/{skills/c64-memory-map/scripts → src}/platform.ts +18 -2
  71. package/src/protocol/messages.ts +17 -1
  72. package/src/protocol/vice.ts +72 -18
  73. package/src/script.ts +3 -2
@@ -0,0 +1,320 @@
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 $00/$01, the stack
3
+ // pointer, A, X, Y and the flags, and jumps to the entry. The copies run
4
+ // from a free page with SEI and all RAM visible, in an order where no copy
5
+ // overwrites the packed bytes of a copy that has not run yet. The plan is
6
+ // simulated here before the emulator runs it.
7
+
8
+ import { formatC64Address } from "#src/c64.ts";
9
+ import { WireFailure } from "#src/host-client/tools.ts";
10
+ import type { Range } from "./source.ts";
11
+
12
+ export interface EntryState {
13
+ pc: number;
14
+ a: number;
15
+ x: number;
16
+ y: number;
17
+ sp: number;
18
+ /** The status register as PLP reads it. */
19
+ flags: number;
20
+ port: [number, number];
21
+ /** $0000-$03ff at the entry. */
22
+ pages: Uint8Array;
23
+ }
24
+
25
+ const FLAG_BITS: Record<string, number> = { n: 0x80, v: 0x40, b: 0x10, d: 0x08, i: 0x04, z: 0x02, c: 0x01 };
26
+
27
+ /** Reads entry_state.json of the c64-unpacker skill. */
28
+ export function readEntryState(text: string, path: string): EntryState {
29
+ const fail = (what: string): never => {
30
+ throw new WireFailure("invalid-input", `${path} is not an entry_state.json of the c64-unpacker skill: ${what}.`);
31
+ };
32
+ let value: Record<string, any>;
33
+ try {
34
+ value = JSON.parse(text) as Record<string, any>;
35
+ } catch (error) {
36
+ return fail((error as Error).message);
37
+ }
38
+ const registers = value.registers;
39
+ 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`));
40
+ if (typeof registers !== "object" || registers === null) return fail("it has no registers");
41
+ 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");
42
+ if (typeof registers.flags !== "object" || registers.flags === null) fail("registers.flags is missing");
43
+ let flags = 0x20;
44
+ for (const [name, bit] of Object.entries(FLAG_BITS)) if (registers.flags[name] === true) flags |= bit;
45
+ const pages = new Uint8Array(0x400);
46
+ for (let page = 0; page < 4; page++) {
47
+ const hex = value.pages?.[formatC64Address(page << 8)];
48
+ if (typeof hex !== "string" || !/^[0-9a-f]{512}$/i.test(hex)) fail(`pages.${formatC64Address(page << 8)} is not 256 bytes of hex`);
49
+ pages.set(Buffer.from(hex as string, "hex"), page << 8);
50
+ }
51
+ return {
52
+ pc,
53
+ a: byte(registers.a, "registers.a"),
54
+ x: byte(registers.x, "registers.x"),
55
+ y: byte(registers.y, "registers.y"),
56
+ sp: byte(registers.sp, "registers.sp"),
57
+ flags,
58
+ port: [byte(value.cpuPort?.$00, "cpuPort.$00"), byte(value.cpuPort?.$01, "cpuPort.$01")],
59
+ pages,
60
+ };
61
+ }
62
+
63
+ export interface Copy {
64
+ /** What the copy writes: a segment or a part of pages 0-3. */
65
+ what: string;
66
+ target: Range;
67
+ source: Range;
68
+ /** True when the copy runs from the last byte down: its target is above an overlapping source. */
69
+ backward: boolean;
70
+ }
71
+
72
+ /** What the loader holds, in its order: a segment or a part of pages 0-3, packed at an address of the loader. */
73
+ export interface PackedBlock {
74
+ what: string;
75
+ range: Range;
76
+ packedAt: number;
77
+ bytes: (address: number) => number;
78
+ }
79
+
80
+ export interface LoaderPlan {
81
+ blocks: PackedBlock[];
82
+ copies: Copy[];
83
+ /** The first of the two free pages where the copier runs. */
84
+ copier: number;
85
+ /** The address of the free stack byte below the stack pointer, which the flags pass through. */
86
+ stackByte: number;
87
+ end: number;
88
+ text: string;
89
+ }
90
+
91
+ const LOAD = 0x0801;
92
+ const STUB = 0x080d;
93
+ /** sei, ldx, the 2-page move loop and jmp. */
94
+ const STUB_SIZE = 21;
95
+ /** The copier code and its three variables, without the table. */
96
+ const COPIER_CODE_SIZE = 167;
97
+ const COPY_ENTRY = 7;
98
+ /** X indexes the table, so the table fits in 256 bytes. */
99
+ export const MAX_COPIES = Math.floor((256 - 2) / COPY_ENTRY);
100
+ /** A loader that loads into $d000 or higher would write the I/O area. */
101
+ const LOAD_LIMIT = 0xcfff;
102
+ const COPIER_PAGES = [...Array.from({ length: 15 }, (_, index) => 0xc000 + index * 0x100), ...Array.from({ length: 0x9b }, (_, index) => 0x9e00 - index * 0x100)];
103
+
104
+ const hex2 = (value: number) => `$${value.toString(16).padStart(2, "0")}`;
105
+ const hex4 = formatC64Address;
106
+ const overlaps = (a: Range, b: Range) => a.start <= b.end && b.start <= a.end;
107
+ const show = (range: Range) => `${hex4(range.start)}-${hex4(range.end)}`;
108
+
109
+ /** Parts of `range` that no range in `holes` covers. */
110
+ function subtract(range: Range, holes: Range[]): Range[] {
111
+ let pieces = [range];
112
+ for (const hole of holes) {
113
+ pieces = pieces.flatMap((piece) => {
114
+ if (!overlaps(piece, hole)) return [piece];
115
+ return [...(hole.start > piece.start ? [{ start: piece.start, end: hole.start - 1 }] : []), ...(hole.end < piece.end ? [{ start: hole.end + 1, end: piece.end }] : [])];
116
+ });
117
+ }
118
+ return pieces;
119
+ }
120
+
121
+ /** Orders the copies so that no copy writes over the source of a later one; undefined when they form a cycle. */
122
+ export function copyOrder(copies: Array<Omit<Copy, "backward">>): number[] | undefined {
123
+ const order: number[] = [];
124
+ const done = new Set<number>();
125
+ while (order.length < copies.length) {
126
+ const next = copies.findIndex((copy, index) => !done.has(index) && copies.every((other, at) => at === index || done.has(at) || !overlaps(copy.target, other.source)));
127
+ if (next < 0) return undefined;
128
+ order.push(next);
129
+ done.add(next);
130
+ }
131
+ return order;
132
+ }
133
+
134
+ /** Runs the copies on a memory model as the copier does, byte by byte; returns the first wrong target byte. */
135
+ export function simulate(memory: Int16Array, copies: Copy[], expected: (address: number) => number): number | undefined {
136
+ for (const copy of copies) {
137
+ const length = copy.target.end - copy.target.start + 1;
138
+ for (let step = 0; step < length; step++) {
139
+ const offset = copy.backward ? length - 1 - step : step;
140
+ memory[copy.target.start + offset] = memory[copy.source.start + offset]!;
141
+ }
142
+ }
143
+ for (const copy of copies) for (let address = copy.target.start; address <= copy.target.end; address++) if (memory[address] !== expected(address)) return address;
144
+ return undefined;
145
+ }
146
+
147
+ export interface LoaderInput {
148
+ /** The segments with the files that hold them, in address order. */
149
+ segments: Array<Range & { files: string[] }>;
150
+ byteAt: (address: number) => number;
151
+ state: EntryState;
152
+ entry: number;
153
+ header: string[];
154
+ }
155
+
156
+ /** Plans the loader: the packed layout, the copy order and the copier page, and writes loader.asm. Refuses by name when no plan exists. */
157
+ export function planLoader(input: LoaderInput): LoaderPlan {
158
+ const { state } = input;
159
+ // What the copies write: each segment without $00/$01, which the loader sets last, and pages $0002-$03ff where no segment is.
160
+ const blocks: Array<{ what: string; range: Range; files?: string[]; pseudo: number; bytes: (address: number) => number }> = [];
161
+ for (const segment of input.segments) blocks.push({ what: `segment ${show(segment)}`, range: segment, files: segment.files, pseudo: segment.start, bytes: input.byteAt });
162
+ for (const part of subtract({ start: 0x0002, end: 0x03ff }, input.segments)) {
163
+ blocks.push({ what: `entry state ${show(part)}`, range: part, pseudo: part.start, bytes: (address) => state.pages[address]! });
164
+ }
165
+ blocks.sort((a, b) => a.range.start - b.range.start);
166
+
167
+ let at = STUB + STUB_SIZE;
168
+ const plain: Array<Omit<Copy, "backward">> = [];
169
+ const packedAt: number[] = [];
170
+ for (const block of blocks) {
171
+ packedAt.push(at);
172
+ const target = { start: Math.max(block.range.start, 0x0002), end: block.range.end };
173
+ if (target.start <= target.end) {
174
+ const source = { start: at + (target.start - block.range.start), end: at + (target.end - block.range.start) };
175
+ plain.push({ what: block.what, target, source });
176
+ }
177
+ at += block.range.end - block.range.start + 1;
178
+ }
179
+ if (plain.length > MAX_COPIES) throw new WireFailure("invalid-input", `The loader needs ${plain.length} copies; it can do at most ${MAX_COPIES}. Join segments in the unpacker capture (--range) or write the loader by hand.`);
180
+ const copierImage = at;
181
+ const copierSize = COPIER_CODE_SIZE + COPY_ENTRY * plain.length + 2;
182
+ const end = copierImage + copierSize - 1;
183
+ 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.`);
184
+
185
+ const order = copyOrder(plain);
186
+ if (order === undefined) {
187
+ const clashing = plain.filter((copy) => plain.some((other) => other !== copy && overlaps(copy.target, other.source))).map((copy) => `${copy.what} (packed at ${show(copy.source)})`);
188
+ 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.`);
189
+ }
190
+ const loaded = { start: LOAD, end };
191
+ const copier = COPIER_PAGES.find((page) => {
192
+ const pages = { start: page, end: page + 0x1ff };
193
+ return !overlaps(pages, loaded) && plain.every((copy) => !overlaps(pages, copy.target));
194
+ });
195
+ 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(", ")}.`);
196
+ const copies: Copy[] = order.map((index) => {
197
+ const copy = plain[index]!;
198
+ return { ...copy, backward: copy.target.start > copy.source.start && overlaps(copy.target, copy.source) };
199
+ });
200
+
201
+ const stackByte = 0x0100 + state.sp;
202
+ const lines: string[] = [...input.header.map((line) => `; ${line}`), "", "!cpu 6510", '!source "constants.asm"', "", `* = ${hex4(LOAD)}`];
203
+ lines.push(` !byte $0b,$08,$0a,$00,$9e,${[...String(STUB)].map((digit) => hex2(digit.charCodeAt(0))).join(",")},$00,$00,$00 ; 10 SYS ${STUB}`);
204
+ lines.push(
205
+ "!zone loader_stub {",
206
+ " sei",
207
+ " ldx #0",
208
+ `- lda ${hex4(copierImage)},x`,
209
+ ` sta ${hex4(copier)},x`,
210
+ ` lda ${hex4(copierImage + 0x100)},x`,
211
+ ` sta ${hex4(copier + 0x100)},x`,
212
+ " inx",
213
+ " bne -",
214
+ ` jmp ${hex4(copier)}`,
215
+ "}",
216
+ );
217
+ for (const block of blocks) {
218
+ lines.push("", `; ${block.what}`, `!pseudopc ${hex4(block.pseudo)} {`);
219
+ if (block.files !== undefined) for (const file of block.files) lines.push(`!source "${file}"`);
220
+ else {
221
+ for (let address = block.range.start; address <= block.range.end; address += 16) {
222
+ const last = Math.min(block.range.end, address + 15);
223
+ lines.push(` !byte ${Array.from({ length: last - address + 1 }, (_, offset) => hex2(block.bytes(address + offset))).join(",")}`);
224
+ }
225
+ }
226
+ lines.push("}");
227
+ }
228
+ const [port0, port1] = state.port;
229
+ lines.push(
230
+ "",
231
+ `; The copier runs at ${hex4(copier)}. The flags pass through ${hex4(stackByte)}, the free stack byte below the stack pointer.`,
232
+ `!pseudopc ${hex4(copier)} {`,
233
+ "!zone loader_copier {",
234
+ " lda #$34",
235
+ " sta $01",
236
+ " ldx #0",
237
+ ".next lda .table,x",
238
+ " sta .count",
239
+ " lda .table+1,x",
240
+ " sta .count+1",
241
+ " ora .count",
242
+ " beq .done",
243
+ " lda .table+2,x",
244
+ " sta .read+1",
245
+ " lda .table+3,x",
246
+ " sta .read+2",
247
+ " lda .table+4,x",
248
+ " sta .write+1",
249
+ " lda .table+5,x",
250
+ " sta .write+2",
251
+ " lda .table+6,x",
252
+ " sta .back",
253
+ " stx .index",
254
+ ".read lda $ffff",
255
+ ".write sta $ffff",
256
+ " lda .back",
257
+ " bne .down",
258
+ " inc .read+1",
259
+ " bne +",
260
+ " inc .read+2",
261
+ "+ inc .write+1",
262
+ " bne +",
263
+ " inc .write+2",
264
+ "+ jmp .counted",
265
+ ".down lda .read+1",
266
+ " bne +",
267
+ " dec .read+2",
268
+ "+ dec .read+1",
269
+ " lda .write+1",
270
+ " bne +",
271
+ " dec .write+2",
272
+ "+ dec .write+1",
273
+ ".counted",
274
+ " lda .count",
275
+ " bne +",
276
+ " dec .count+1",
277
+ "+ dec .count",
278
+ " lda .count",
279
+ " ora .count+1",
280
+ " bne .read",
281
+ " lda .index",
282
+ " clc",
283
+ " adc #7",
284
+ " tax",
285
+ " jmp .next",
286
+ `.done lda #${hex2(port0)}`,
287
+ " sta $00",
288
+ ` lda #${hex2(port1)}`,
289
+ " sta $01",
290
+ ` ldx #${hex2((state.sp - 1) & 0xff)}`,
291
+ " txs",
292
+ ` lda #${hex2(state.flags)}`,
293
+ ` sta ${hex4(stackByte)}`,
294
+ ` lda #${hex2(state.a)}`,
295
+ ` ldx #${hex2(state.x)}`,
296
+ ` ldy #${hex2(state.y)}`,
297
+ " plp",
298
+ ` jmp ${hex4(input.entry)}`,
299
+ ".count !word 0",
300
+ ".index !byte 0",
301
+ ".back !byte 0",
302
+ ".table",
303
+ );
304
+ for (const copy of copies) {
305
+ const length = copy.target.end - copy.target.start + 1;
306
+ const read = copy.backward ? copy.source.end : copy.source.start;
307
+ const write = copy.backward ? copy.target.end : copy.target.start;
308
+ lines.push(` !word ${hex4(length)},${hex4(read)},${hex4(write)} : !byte ${copy.backward ? 1 : 0} ; ${copy.what}`);
309
+ }
310
+ lines.push(" !word 0", "}", "}");
311
+
312
+ return {
313
+ blocks: blocks.map((block, index) => ({ what: block.what, range: block.range, packedAt: packedAt[index]!, bytes: block.bytes })),
314
+ copies,
315
+ copier,
316
+ stackByte,
317
+ end,
318
+ text: `${lines.join("\n")}\n`,
319
+ };
320
+ }
@@ -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
+ }