c64bridge 0.9.0 → 0.9.2

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 (61) hide show
  1. package/README.md +161 -135
  2. package/data/assembly/assembly-spec.md +1 -1
  3. package/data/context/bootstrap.md +3 -3
  4. package/data/context/fast-paths.md +3 -1
  5. package/data/graphics/petscii-style-guide.md +1 -1
  6. package/data/graphics/sprite-charset-best-practices.md +14 -13
  7. package/data/sound/sid-file-structure.md +5 -5
  8. package/data/sound/sid-programming-best-practices.md +5 -5
  9. package/data/vice/vice-binary-monitor-spec.md +26 -10
  10. package/dist/.tsbuildinfo +1 -1
  11. package/dist/c64Client.js +107 -8
  12. package/dist/device.js +43 -3
  13. package/dist/rag/indexer.js +18 -8
  14. package/dist/sidwaveCompiler.js +1 -1
  15. package/dist/tools/debug.js +158 -5
  16. package/dist/tools/input.js +336 -0
  17. package/dist/tools/memory.js +326 -1
  18. package/dist/tools/programRunners.js +45 -6
  19. package/dist/tools/registry/batch.js +167 -0
  20. package/dist/tools/registry/graphics.js +101 -1
  21. package/dist/tools/registry/index.js +4 -0
  22. package/dist/tools/registry/input.js +2 -0
  23. package/dist/tools/registry/memory.js +61 -1
  24. package/dist/tools/responses.js +14 -1
  25. package/dist/tools/schema.js +4 -5
  26. package/dist/{assemblyConverter.js → tools/translation/assembler.js} +66 -163
  27. package/dist/{basicConverter.js → tools/translation/basicTokenizer.js} +1 -1
  28. package/dist/tools/translation/disassembler.js +150 -0
  29. package/dist/tools/translation/opcodeInventory.js +126 -0
  30. package/dist/tools/translation/symbolRegistry.js +49 -0
  31. package/dist/tools/types.js +19 -4
  32. package/dist/vice/process.js +43 -2
  33. package/dist/vice/viceClient.js +5 -1
  34. package/doc/reviews/audit.md +143 -0
  35. package/mcp/schemas/c64_batch.schema.json +53 -0
  36. package/mcp/schemas/c64_config.schema.json +461 -2
  37. package/mcp/schemas/c64_debug.schema.json +641 -3
  38. package/mcp/schemas/c64_disk.schema.json +340 -2
  39. package/mcp/schemas/c64_drive.schema.json +157 -2
  40. package/mcp/schemas/c64_extract.schema.json +359 -2
  41. package/mcp/schemas/c64_graphics.schema.json +415 -3
  42. package/mcp/schemas/c64_input.schema.json +254 -0
  43. package/mcp/schemas/c64_memory.schema.json +562 -3
  44. package/mcp/schemas/c64_printer.schema.json +253 -2
  45. package/mcp/schemas/c64_program.schema.json +499 -2
  46. package/mcp/schemas/c64_rag.schema.json +82 -2
  47. package/mcp/schemas/c64_select_backend.schema.json +35 -2
  48. package/mcp/schemas/c64_sound.schema.json +793 -2
  49. package/mcp/schemas/c64_stream.schema.json +73 -2
  50. package/mcp/schemas/c64_system.schema.json +301 -2
  51. package/mcp/schemas/c64_vice.schema.json +71 -2
  52. package/mcp/server.json +1 -1
  53. package/mcp/tools.json +7509 -1929
  54. package/mcp.json +1 -1
  55. package/package.json +2 -2
  56. package/scripts/c64-cli.mjs +2 -2
  57. package/scripts/run-coverage.mjs +61 -3
  58. package/scripts/run-tests.mjs +1 -0
  59. package/scripts/run-tests.ts +3 -0
  60. package/scripts/update-readme.ts +79 -2
  61. package/server.json +2 -2
@@ -3,8 +3,8 @@ import { createOperationDispatcher, defineToolModule, discriminatedUnionSchema,
3
3
  import { graphicsModule, graphicsOperationHandlers as groupedGraphicsHandlers } from "../graphics.js";
4
4
  import { booleanSchema, literalSchema, numberSchema, objectSchema, optionalSchema, stringSchema, } from "../schema.js";
5
5
  import { buildDescriptorIndex, ensureDescriptor, extendSchemaWithOp, createOperationHandlers, } from "./utils.js";
6
- import { ToolError, toolErrorResult, unknownErrorResult, } from "../errors.js";
7
6
  import { jsonResult } from "../responses.js";
7
+ import { ToolError, toolErrorResult, unknownErrorResult, } from "../errors.js";
8
8
  const graphicsDescriptorIndex = buildDescriptorIndex(graphicsModule);
9
9
  const captureFrameArgsSchema = objectSchema({
10
10
  description: "Capture one or more complete video frames from the active backend.",
@@ -30,6 +30,72 @@ const captureFrameArgsSchema = objectSchema({
30
30
  required: ["op"],
31
31
  additionalProperties: false,
32
32
  });
33
+ // ---------------------------------------------------------------------------
34
+ // get_display_state helpers — VIC-II register decoding
35
+ // ---------------------------------------------------------------------------
36
+ function parseVicState(d011, d016, d018, dd00, d020, d021) {
37
+ const bank = (~dd00) & 0x03; // CIA2 Port A bits 0-1 select VIC bank (inverted)
38
+ const bankBase = bank * 0x4000;
39
+ const bitmapMode = (d011 & 0x20) !== 0;
40
+ const extendedColor = (d011 & 0x40) !== 0;
41
+ const multicolor = (d016 & 0x10) !== 0;
42
+ const screenVisible = (d011 & 0x10) !== 0;
43
+ const raster8 = (d011 & 0x80) !== 0;
44
+ const vertScroll = d011 & 0x07;
45
+ const horizScroll = d016 & 0x07;
46
+ const columns40 = (d016 & 0x08) !== 0;
47
+ const rows25 = (d011 & 0x08) !== 0;
48
+ // D018 bits 4-7 = screen RAM offset (× $0400); bits 1-3 = char/bitmap pointer (× $0800)
49
+ const screenRamOffset = ((d018 >> 4) & 0x0f) * 0x0400;
50
+ const screenRamAddress = bankBase + screenRamOffset;
51
+ const charOrBitmapPointer = ((d018 >> 1) & 0x07) * 0x0800;
52
+ const charOrBitmapAddress = bankBase + charOrBitmapPointer;
53
+ let mode;
54
+ if (bitmapMode && multicolor)
55
+ mode = "multicolor_bitmap";
56
+ else if (bitmapMode)
57
+ mode = "hires_bitmap";
58
+ else if (extendedColor)
59
+ mode = "extended_color_text";
60
+ else if (multicolor)
61
+ mode = "multicolor_text";
62
+ else
63
+ mode = "standard_text";
64
+ return {
65
+ mode,
66
+ bank,
67
+ bankBase: `$${bankBase.toString(16).toUpperCase().padStart(4, "0")}`,
68
+ screenRamAddress: `$${screenRamAddress.toString(16).toUpperCase().padStart(4, "0")}`,
69
+ charOrBitmapAddress: `$${charOrBitmapAddress.toString(16).toUpperCase().padStart(4, "0")}`,
70
+ screenVisible,
71
+ bitmapMode,
72
+ extendedColor,
73
+ multicolor,
74
+ rows25,
75
+ columns40,
76
+ vertScroll,
77
+ horizScroll,
78
+ raster8,
79
+ borderColor: d020 & 0x0f,
80
+ backgroundColor: d021 & 0x0f,
81
+ registers: {
82
+ d011: `$${d011.toString(16).toUpperCase().padStart(2, "0")}`,
83
+ d016: `$${d016.toString(16).toUpperCase().padStart(2, "0")}`,
84
+ d018: `$${d018.toString(16).toUpperCase().padStart(2, "0")}`,
85
+ d020: `$${d020.toString(16).toUpperCase().padStart(2, "0")}`,
86
+ d021: `$${d021.toString(16).toUpperCase().padStart(2, "0")}`,
87
+ dd00: `$${dd00.toString(16).toUpperCase().padStart(2, "0")}`,
88
+ },
89
+ };
90
+ }
91
+ const getDisplayStateArgsSchema = objectSchema({
92
+ description: "Read VIC-II and CIA2 registers to determine the current graphics mode and memory layout. Works on C64U and VICE; both backends are read through shared memory primitives so the response shape is identical.",
93
+ properties: {
94
+ op: literalSchema("get_display_state"),
95
+ },
96
+ required: ["op"],
97
+ additionalProperties: false,
98
+ });
33
99
  const graphicsOperations = [
34
100
  {
35
101
  op: "capture_frame",
@@ -105,6 +171,35 @@ const graphicsOperations = [
105
171
  schema: extendSchemaWithOp("render_bitmap", ensureDescriptor(graphicsDescriptorIndex, "render_bitmap").inputSchema, { description: "Import an image file, convert it to VIC-II bitmap memory, write it into RAM, and display it." }),
106
172
  handler: groupedGraphicsHandlers.render_bitmap,
107
173
  },
174
+ {
175
+ op: "get_display_state",
176
+ schema: getDisplayStateArgsSchema.jsonSchema,
177
+ handler: async (_rawArgs, ctx) => {
178
+ try {
179
+ // Use cross-platform memory reads so the same handler works on both
180
+ // C64U and VICE. The VIC-II and CIA2 registers live in machine memory
181
+ // either way; the underlying facade hides the transport difference.
182
+ const vicRegs = await ctx.client.readMemoryRaw(0xD011, 8);
183
+ const colorRegs = await ctx.client.readMemoryRaw(0xD020, 2);
184
+ const cia2 = await ctx.client.readMemoryRaw(0xDD00, 1);
185
+ const d011 = vicRegs[0] ?? 0;
186
+ const d016 = vicRegs[5] ?? 0; // $D016 = $D011 + 5
187
+ const d018 = vicRegs[7] ?? 0; // $D018 = $D011 + 7
188
+ const d020 = colorRegs[0] ?? 0;
189
+ const d021 = colorRegs[1] ?? 0;
190
+ const dd00 = cia2[0] ?? 0x03;
191
+ const backend = await ctx.client.getActiveBackendType();
192
+ const state = { ...parseVicState(d011, d016, d018, dd00, d020, d021), backend };
193
+ ctx.logger.info("Read VIC-II display state", { mode: state.mode, bank: state.bank, backend });
194
+ return jsonResult(state, { success: true, mode: state.mode, backend });
195
+ }
196
+ catch (error) {
197
+ if (error instanceof ToolError)
198
+ return toolErrorResult(error);
199
+ return unknownErrorResult(error);
200
+ }
201
+ },
202
+ },
108
203
  ];
109
204
  const graphicsOperationHandlers = createOperationHandlers(graphicsOperations);
110
205
  export const graphicsModuleGroup = defineToolModule({
@@ -154,6 +249,11 @@ export const graphicsModuleGroup = defineToolModule({
154
249
  description: "Convert a PNG into VIC bitmap memory and display it",
155
250
  arguments: { op: "render_bitmap", imagePath: "./artifacts/sample.png", format: "hires" },
156
251
  },
252
+ {
253
+ name: "Read VIC-II state",
254
+ description: "Decode graphics mode, memory layout, and colours from VIC-II registers (cross-platform)",
255
+ arguments: { op: "get_display_state" },
256
+ },
157
257
  ],
158
258
  execute: createOperationDispatcher("c64_graphics", graphicsOperationHandlers),
159
259
  },
@@ -14,6 +14,8 @@ import { extractModule } from "./extract.js";
14
14
  import { streamModule } from "./stream.js";
15
15
  import { debugModuleGroup as debugModule } from "../debug.js";
16
16
  import { viceModuleGroup as viceModule } from "../vice.js";
17
+ import { inputModuleGroup as inputModule } from "./input.js";
18
+ import { batchModuleGroup as batchModule } from "./batch.js";
17
19
  const modules = [
18
20
  programModule,
19
21
  memoryModule,
@@ -30,6 +32,8 @@ const modules = [
30
32
  streamModule,
31
33
  debugModule,
32
34
  viceModule,
35
+ inputModule,
36
+ batchModule,
33
37
  ];
34
38
  const toolMap = new Map();
35
39
  for (const module of modules) {
@@ -0,0 +1,2 @@
1
+ import { inputModule } from "../input.js";
2
+ export { inputModule as inputModuleGroup };
@@ -1,5 +1,5 @@
1
1
  import { createOperationDispatcher, defineToolModule, discriminatedUnionSchema, } from "../types.js";
2
- import { memoryModule, memoryOperationHandlers as groupedMemoryHandlers } from "../memory.js";
2
+ import { memoryModule, memoryOperationHandlers as groupedMemoryHandlers, disassembleArgsSchema, copyMemoryArgsSchema, fillMemoryArgsSchema, searchMemoryArgsSchema, compareMemoryArgsSchema, saveMemoryArgsSchema, } from "../memory.js";
3
3
  import { metaModule } from "../meta/index.js";
4
4
  import { buildDescriptorIndex, ensureDescriptor, extendSchemaWithOp, createOperationHandlers, invokeModuleTool, } from "./utils.js";
5
5
  const memoryDescriptorIndex = buildDescriptorIndex(memoryModule);
@@ -25,6 +25,36 @@ const memoryOperations = [
25
25
  schema: extendSchemaWithOp("wait_for_text", ensureDescriptor(metaDescriptorIndex, "wait_for_screen_text").inputSchema, { description: "Poll the screen until a substring or regex appears, or timeout elapses." }),
26
26
  handler: async (rawArgs, ctx) => invokeModuleTool(metaModule, "wait_for_screen_text", rawArgs, ctx),
27
27
  },
28
+ {
29
+ op: "disassemble",
30
+ schema: extendSchemaWithOp("disassemble", disassembleArgsSchema.jsonSchema, { description: "Disassemble a memory region into annotated 6502/6510 instructions, including undocumented opcodes with canonical names. Symbol annotations from `.vs` files are applied when available. Works on both C64U and VICE." }),
31
+ handler: groupedMemoryHandlers.disassemble,
32
+ },
33
+ {
34
+ op: "copy_memory",
35
+ schema: extendSchemaWithOp("copy_memory", copyMemoryArgsSchema.jsonSchema, { description: "Copy a RAM region to another address." }),
36
+ handler: groupedMemoryHandlers.copy_memory,
37
+ },
38
+ {
39
+ op: "fill_memory",
40
+ schema: extendSchemaWithOp("fill_memory", fillMemoryArgsSchema.jsonSchema, { description: "Fill a memory range with a repeating byte pattern." }),
41
+ handler: groupedMemoryHandlers.fill_memory,
42
+ },
43
+ {
44
+ op: "search_memory",
45
+ schema: extendSchemaWithOp("search_memory", searchMemoryArgsSchema.jsonSchema, { description: "Search for a byte pattern within a memory range and return matching addresses." }),
46
+ handler: groupedMemoryHandlers.search_memory,
47
+ },
48
+ {
49
+ op: "compare_memory",
50
+ schema: extendSchemaWithOp("compare_memory", compareMemoryArgsSchema.jsonSchema, { description: "Compare two memory regions byte-by-byte and report differences." }),
51
+ handler: groupedMemoryHandlers.compare_memory,
52
+ },
53
+ {
54
+ op: "save_memory",
55
+ schema: extendSchemaWithOp("save_memory", saveMemoryArgsSchema.jsonSchema, { description: "Dump a memory range to a local file, with an optional PRG load-address header." }),
56
+ handler: groupedMemoryHandlers.save_memory,
57
+ },
28
58
  ];
29
59
  const memoryOperationHandlers = createOperationHandlers(memoryOperations);
30
60
  export const memoryModuleGroup = defineToolModule({
@@ -59,6 +89,36 @@ export const memoryModuleGroup = defineToolModule({
59
89
  description: "Poll until the READY. prompt appears",
60
90
  arguments: { op: "wait_for_text", pattern: "READY." },
61
91
  },
92
+ {
93
+ name: "Disassemble entry point",
94
+ description: "Disassemble 20 instructions starting at $0810",
95
+ arguments: { op: "disassemble", address: "$0810", instructionCount: 20 },
96
+ },
97
+ {
98
+ name: "Copy screen RAM",
99
+ description: "Copy 1000 bytes of screen RAM to a scratch area",
100
+ arguments: { op: "copy_memory", source: "$0400", dest: "$C000", length: 1000 },
101
+ },
102
+ {
103
+ name: "Clear screen RAM",
104
+ description: "Fill screen RAM with space character ($20)",
105
+ arguments: { op: "fill_memory", address: "$0400", length: 1000, pattern: "20" },
106
+ },
107
+ {
108
+ name: "Search for RTS",
109
+ description: "Find RTS ($60) instructions in BASIC ROM",
110
+ arguments: { op: "search_memory", startAddress: "$A000", endAddress: "$BFFF", pattern: "60" },
111
+ },
112
+ {
113
+ name: "Compare two buffers",
114
+ description: "Check whether two 256-byte regions differ",
115
+ arguments: { op: "compare_memory", address1: "$C000", address2: "$C100", length: 256 },
116
+ },
117
+ {
118
+ name: "Save PRG dump",
119
+ description: "Save $0801-$CFFF as a PRG file",
120
+ arguments: { op: "save_memory", startAddress: "$0801", endAddress: "$CFFF", filePath: "/tmp/dump.prg" },
121
+ },
62
122
  ],
63
123
  execute: createOperationDispatcher("c64_memory", memoryOperationHandlers),
64
124
  },
@@ -1,5 +1,5 @@
1
1
  export function textResult(text, metadata) {
2
- return {
2
+ const result = {
3
3
  content: [
4
4
  {
5
5
  type: "text",
@@ -8,6 +8,19 @@ export function textResult(text, metadata) {
8
8
  ],
9
9
  metadata,
10
10
  };
11
+ if (metadata !== undefined) {
12
+ return {
13
+ ...result,
14
+ structuredContent: {
15
+ type: "json",
16
+ data: {
17
+ message: text,
18
+ ...metadata,
19
+ },
20
+ },
21
+ };
22
+ }
23
+ return result;
11
24
  }
12
25
  export function jsonResult(data, metadata) {
13
26
  const text = typeof data === "string"
@@ -195,15 +195,14 @@ function isPlainObject(value) {
195
195
  }
196
196
  export function objectSchema(options) {
197
197
  const required = options.required ?? [];
198
+ const additionalProperties = options.additionalProperties ?? false;
198
199
  const propertyEntries = Object.entries(options.properties).map(([key, schema]) => [key, schema.jsonSchema]);
199
200
  const jsonSchema = {
200
201
  type: "object",
201
202
  ...(options.description ? { description: options.description } : {}),
202
203
  properties: Object.fromEntries(propertyEntries),
203
204
  ...(required.length > 0 ? { required: required.map((key) => key) } : {}),
204
- ...(options.additionalProperties !== undefined
205
- ? { additionalProperties: options.additionalProperties }
206
- : { additionalProperties: false }),
205
+ additionalProperties,
207
206
  };
208
207
  return createSchema(jsonSchema, (value, path) => {
209
208
  ensureType(isPlainObject(value), "Expected an object", path, {
@@ -234,7 +233,7 @@ export function objectSchema(options) {
234
233
  }
235
234
  }
236
235
  }
237
- if (options.additionalProperties === false) {
236
+ if (!additionalProperties) {
238
237
  for (const key of Object.keys(input)) {
239
238
  if (!hasOwn.call(options.properties, key)) {
240
239
  throw new ToolValidationError("Unexpected property", {
@@ -243,7 +242,7 @@ export function objectSchema(options) {
243
242
  }
244
243
  }
245
244
  }
246
- else if (options.additionalProperties === true) {
245
+ else {
247
246
  for (const key of Object.keys(input)) {
248
247
  if (!hasOwn.call(options.properties, key)) {
249
248
  result[key] = input[key];
@@ -6,6 +6,7 @@ Licensed under the GNU General Public License v2.0 or later.
6
6
  See <https://www.gnu.org/licenses/> for details.
7
7
  */
8
8
  import { Buffer } from "node:buffer";
9
+ import { OPCODE_INVENTORY } from "./opcodeInventory.js";
9
10
  export class AssemblyError extends Error {
10
11
  constructor(message, location) {
11
12
  super(`${location.file}:${location.line}: ${message}`);
@@ -20,6 +21,7 @@ var AddressingMode;
20
21
  AddressingMode["ZeroPage"] = "zeroPage";
21
22
  AddressingMode["ZeroPageX"] = "zeroPageX";
22
23
  AddressingMode["ZeroPageY"] = "zeroPageY";
24
+ AddressingMode["ZeroPageS"] = "zeroPageS";
23
25
  AddressingMode["Absolute"] = "absolute";
24
26
  AddressingMode["AbsoluteX"] = "absoluteX";
25
27
  AddressingMode["AbsoluteY"] = "absoluteY";
@@ -77,6 +79,9 @@ const DIRECTIVE_ALIASES = {
77
79
  };
78
80
  const RELATIVE_ONLY = new Set(["BCC", "BCS", "BEQ", "BMI", "BNE", "BPL", "BVC", "BVS"]);
79
81
  export function assemblyToPrg(source, options) {
82
+ return assemblyToPrgDetailed(source, options).prg;
83
+ }
84
+ export function assemblyToPrgDetailed(source, options) {
80
85
  const normalized = normalizeSource(source);
81
86
  const loadAddress = options?.loadAddress ?? DEFAULT_LOAD_ADDRESS;
82
87
  const rootFile = options?.fileName ?? "(input)";
@@ -86,7 +91,7 @@ export function assemblyToPrg(source, options) {
86
91
  const assembler = new Assembler(statements, loadAddress);
87
92
  assembler.runPass(0);
88
93
  assembler.runPass(1);
89
- return assembler.buildPrg();
94
+ return { prg: assembler.buildPrg(), symbols: assembler.getSymbols() };
90
95
  }
91
96
  function normalizeSource(input) {
92
97
  return input.replace(/\r\n?/g, "\n");
@@ -516,7 +521,19 @@ function parseOperand(stream) {
516
521
  }
517
522
  if (first.kind === "punct" && first.value === "(") {
518
523
  stream.consumePunct("(");
519
- const expr = parseExpression(stream, new Set([")"]));
524
+ const expr = parseExpression(stream, new Set([",", ")"]));
525
+ if (stream.peek()?.kind === "punct" && stream.peek()?.value === ",") {
526
+ stream.consumePunct(",");
527
+ const register = stream.consumeSymbol("Expected X before ')'").toUpperCase();
528
+ if (register !== "X") {
529
+ throw new Error("Only X is allowed inside pre-indexed indirect operands");
530
+ }
531
+ stream.consumePunct(")", "Expected ')' to close indirect operand");
532
+ if (!stream.isAtEnd()) {
533
+ throw new Error("Unexpected tokens after indirect operand");
534
+ }
535
+ return { kind: "indirect", expr, register: "X" };
536
+ }
520
537
  stream.consumePunct(")", "Expected ')' to close indirect operand");
521
538
  if (stream.isAtEnd()) {
522
539
  return { kind: "indirect", expr };
@@ -542,9 +559,9 @@ function parseOperand(stream) {
542
559
  const next = stream.peek();
543
560
  if (next && next.kind === "punct" && next.value === ",") {
544
561
  stream.consumePunct(",");
545
- const register = stream.consumeSymbol("Expected X or Y after comma").toUpperCase();
546
- if (register !== "X" && register !== "Y") {
547
- throw new Error("Only X or Y are allowed after comma");
562
+ const register = stream.consumeSymbol("Expected X, Y, or S after comma").toUpperCase();
563
+ if (register !== "X" && register !== "Y" && register !== "S") {
564
+ throw new Error("Only X, Y, or S are allowed after comma");
548
565
  }
549
566
  return { kind: "expression", expr, register };
550
567
  }
@@ -608,6 +625,9 @@ class SymbolTable {
608
625
  }
609
626
  return this.globals.get(label);
610
627
  }
628
+ allGlobals() {
629
+ return this.globals;
630
+ }
611
631
  ensureLocalScope(scope) {
612
632
  if (!this.locals.has(scope)) {
613
633
  this.locals.set(scope, new Map());
@@ -658,6 +678,9 @@ class Assembler {
658
678
  header.writeUInt16LE(start, 0);
659
679
  return Buffer.concat([header, Buffer.from(body)]);
660
680
  }
681
+ getSymbols() {
682
+ return this.symbolTable.allGlobals();
683
+ }
661
684
  processLine(line, pass) {
662
685
  for (const label of line.labels) {
663
686
  if (pass === 0) {
@@ -821,6 +844,7 @@ class Assembler {
821
844
  case AddressingMode.ZeroPage:
822
845
  case AddressingMode.ZeroPageX:
823
846
  case AddressingMode.ZeroPageY:
847
+ case AddressingMode.ZeroPageS:
824
848
  case AddressingMode.IndirectX:
825
849
  case AddressingMode.IndirectY:
826
850
  this.writeByte(info.value ?? 0, location);
@@ -1031,6 +1055,11 @@ function chooseAddressingMode(mnemonic, operand, value, pass, location) {
1031
1055
  return AddressingMode.AbsoluteY;
1032
1056
  }
1033
1057
  }
1058
+ else if (register === "S") {
1059
+ if (available.has(AddressingMode.ZeroPageS)) {
1060
+ return AddressingMode.ZeroPageS;
1061
+ }
1062
+ }
1034
1063
  else {
1035
1064
  if (available.has(AddressingMode.ZeroPage) && value !== undefined && fitsByte(value)) {
1036
1065
  return AddressingMode.ZeroPage;
@@ -1057,6 +1086,7 @@ function instructionSize(mode) {
1057
1086
  case AddressingMode.ZeroPage:
1058
1087
  case AddressingMode.ZeroPageX:
1059
1088
  case AddressingMode.ZeroPageY:
1089
+ case AddressingMode.ZeroPageS:
1060
1090
  case AddressingMode.IndirectX:
1061
1091
  case AddressingMode.IndirectY:
1062
1092
  case AddressingMode.Relative:
@@ -1086,6 +1116,7 @@ function normalizeOperandValue(mode, value, locationCounterOrLocation, locationM
1086
1116
  case AddressingMode.ZeroPage:
1087
1117
  case AddressingMode.ZeroPageX:
1088
1118
  case AddressingMode.ZeroPageY:
1119
+ case AddressingMode.ZeroPageS:
1089
1120
  case AddressingMode.IndirectX:
1090
1121
  case AddressingMode.IndirectY:
1091
1122
  if (value === undefined)
@@ -1132,168 +1163,40 @@ function fitsByte(value) {
1132
1163
  function formatHex(value) {
1133
1164
  return `$${value.toString(16).toUpperCase().padStart(4, "0")}`;
1134
1165
  }
1166
+ const OPCODE_MODE_TO_ADDRESSING_MODE = {
1167
+ implied: AddressingMode.Implied,
1168
+ accumulator: AddressingMode.Accumulator,
1169
+ immediate: AddressingMode.Immediate,
1170
+ zeroPage: AddressingMode.ZeroPage,
1171
+ zeroPageX: AddressingMode.ZeroPageX,
1172
+ zeroPageY: AddressingMode.ZeroPageY,
1173
+ zeroPageS: AddressingMode.ZeroPageS,
1174
+ absolute: AddressingMode.Absolute,
1175
+ absoluteX: AddressingMode.AbsoluteX,
1176
+ absoluteY: AddressingMode.AbsoluteY,
1177
+ indirect: AddressingMode.Indirect,
1178
+ indirectX: AddressingMode.IndirectX,
1179
+ indirectY: AddressingMode.IndirectY,
1180
+ relative: AddressingMode.Relative,
1181
+ };
1135
1182
  function buildInstructionTable() {
1136
1183
  const table = new Map();
1137
- for (const entry of INSTRUCTION_DATA) {
1138
- const mnemonic = entry.mnemonic;
1139
- if (!table.has(mnemonic)) {
1140
- table.set(mnemonic, new Map());
1184
+ const entryByMnemonicAndMode = new Map();
1185
+ for (const entry of OPCODE_INVENTORY) {
1186
+ const mode = OPCODE_MODE_TO_ADDRESSING_MODE[entry.mode];
1187
+ const key = `${entry.mnemonic}\u0000${mode}`;
1188
+ const existing = entryByMnemonicAndMode.get(key);
1189
+ // The inventory is sorted by opcode, but assembly should prefer official
1190
+ // encodings over undocumented aliases for duplicate mnemonic/mode pairs.
1191
+ if (existing && (!existing.illegal || entry.illegal)) {
1192
+ continue;
1193
+ }
1194
+ entryByMnemonicAndMode.set(key, { opcode: entry.opcode, illegal: entry.illegal });
1195
+ if (!table.has(entry.mnemonic)) {
1196
+ table.set(entry.mnemonic, new Map());
1141
1197
  }
1142
- table.get(mnemonic).set(entry.mode, entry.opcode);
1198
+ table.get(entry.mnemonic).set(mode, entry.opcode);
1143
1199
  }
1144
1200
  return table;
1145
1201
  }
1146
- const INSTRUCTION_DATA = [
1147
- { mnemonic: "ADC", mode: AddressingMode.Immediate, opcode: 0x69 },
1148
- { mnemonic: "ADC", mode: AddressingMode.ZeroPage, opcode: 0x65 },
1149
- { mnemonic: "ADC", mode: AddressingMode.ZeroPageX, opcode: 0x75 },
1150
- { mnemonic: "ADC", mode: AddressingMode.Absolute, opcode: 0x6d },
1151
- { mnemonic: "ADC", mode: AddressingMode.AbsoluteX, opcode: 0x7d },
1152
- { mnemonic: "ADC", mode: AddressingMode.AbsoluteY, opcode: 0x79 },
1153
- { mnemonic: "ADC", mode: AddressingMode.IndirectX, opcode: 0x61 },
1154
- { mnemonic: "ADC", mode: AddressingMode.IndirectY, opcode: 0x71 },
1155
- { mnemonic: "AND", mode: AddressingMode.Immediate, opcode: 0x29 },
1156
- { mnemonic: "AND", mode: AddressingMode.ZeroPage, opcode: 0x25 },
1157
- { mnemonic: "AND", mode: AddressingMode.ZeroPageX, opcode: 0x35 },
1158
- { mnemonic: "AND", mode: AddressingMode.Absolute, opcode: 0x2d },
1159
- { mnemonic: "AND", mode: AddressingMode.AbsoluteX, opcode: 0x3d },
1160
- { mnemonic: "AND", mode: AddressingMode.AbsoluteY, opcode: 0x39 },
1161
- { mnemonic: "AND", mode: AddressingMode.IndirectX, opcode: 0x21 },
1162
- { mnemonic: "AND", mode: AddressingMode.IndirectY, opcode: 0x31 },
1163
- { mnemonic: "ASL", mode: AddressingMode.Accumulator, opcode: 0x0a },
1164
- { mnemonic: "ASL", mode: AddressingMode.ZeroPage, opcode: 0x06 },
1165
- { mnemonic: "ASL", mode: AddressingMode.ZeroPageX, opcode: 0x16 },
1166
- { mnemonic: "ASL", mode: AddressingMode.Absolute, opcode: 0x0e },
1167
- { mnemonic: "ASL", mode: AddressingMode.AbsoluteX, opcode: 0x1e },
1168
- { mnemonic: "BIT", mode: AddressingMode.ZeroPage, opcode: 0x24 },
1169
- { mnemonic: "BIT", mode: AddressingMode.Absolute, opcode: 0x2c },
1170
- { mnemonic: "BPL", mode: AddressingMode.Relative, opcode: 0x10 },
1171
- { mnemonic: "BMI", mode: AddressingMode.Relative, opcode: 0x30 },
1172
- { mnemonic: "BVC", mode: AddressingMode.Relative, opcode: 0x50 },
1173
- { mnemonic: "BVS", mode: AddressingMode.Relative, opcode: 0x70 },
1174
- { mnemonic: "BCC", mode: AddressingMode.Relative, opcode: 0x90 },
1175
- { mnemonic: "BCS", mode: AddressingMode.Relative, opcode: 0xb0 },
1176
- { mnemonic: "BNE", mode: AddressingMode.Relative, opcode: 0xd0 },
1177
- { mnemonic: "BEQ", mode: AddressingMode.Relative, opcode: 0xf0 },
1178
- { mnemonic: "BRK", mode: AddressingMode.Implied, opcode: 0x00 },
1179
- { mnemonic: "CLC", mode: AddressingMode.Implied, opcode: 0x18 },
1180
- { mnemonic: "CLD", mode: AddressingMode.Implied, opcode: 0xd8 },
1181
- { mnemonic: "CLI", mode: AddressingMode.Implied, opcode: 0x58 },
1182
- { mnemonic: "CLV", mode: AddressingMode.Implied, opcode: 0xb8 },
1183
- { mnemonic: "CMP", mode: AddressingMode.Immediate, opcode: 0xc9 },
1184
- { mnemonic: "CMP", mode: AddressingMode.ZeroPage, opcode: 0xc5 },
1185
- { mnemonic: "CMP", mode: AddressingMode.ZeroPageX, opcode: 0xd5 },
1186
- { mnemonic: "CMP", mode: AddressingMode.Absolute, opcode: 0xcd },
1187
- { mnemonic: "CMP", mode: AddressingMode.AbsoluteX, opcode: 0xdd },
1188
- { mnemonic: "CMP", mode: AddressingMode.AbsoluteY, opcode: 0xd9 },
1189
- { mnemonic: "CMP", mode: AddressingMode.IndirectX, opcode: 0xc1 },
1190
- { mnemonic: "CMP", mode: AddressingMode.IndirectY, opcode: 0xd1 },
1191
- { mnemonic: "CPX", mode: AddressingMode.Immediate, opcode: 0xe0 },
1192
- { mnemonic: "CPX", mode: AddressingMode.ZeroPage, opcode: 0xe4 },
1193
- { mnemonic: "CPX", mode: AddressingMode.Absolute, opcode: 0xec },
1194
- { mnemonic: "CPY", mode: AddressingMode.Immediate, opcode: 0xc0 },
1195
- { mnemonic: "CPY", mode: AddressingMode.ZeroPage, opcode: 0xc4 },
1196
- { mnemonic: "CPY", mode: AddressingMode.Absolute, opcode: 0xcc },
1197
- { mnemonic: "DEC", mode: AddressingMode.ZeroPage, opcode: 0xc6 },
1198
- { mnemonic: "DEC", mode: AddressingMode.ZeroPageX, opcode: 0xd6 },
1199
- { mnemonic: "DEC", mode: AddressingMode.Absolute, opcode: 0xce },
1200
- { mnemonic: "DEC", mode: AddressingMode.AbsoluteX, opcode: 0xde },
1201
- { mnemonic: "DEX", mode: AddressingMode.Implied, opcode: 0xca },
1202
- { mnemonic: "DEY", mode: AddressingMode.Implied, opcode: 0x88 },
1203
- { mnemonic: "EOR", mode: AddressingMode.Immediate, opcode: 0x49 },
1204
- { mnemonic: "EOR", mode: AddressingMode.ZeroPage, opcode: 0x45 },
1205
- { mnemonic: "EOR", mode: AddressingMode.ZeroPageX, opcode: 0x55 },
1206
- { mnemonic: "EOR", mode: AddressingMode.Absolute, opcode: 0x4d },
1207
- { mnemonic: "EOR", mode: AddressingMode.AbsoluteX, opcode: 0x5d },
1208
- { mnemonic: "EOR", mode: AddressingMode.AbsoluteY, opcode: 0x59 },
1209
- { mnemonic: "EOR", mode: AddressingMode.IndirectX, opcode: 0x41 },
1210
- { mnemonic: "EOR", mode: AddressingMode.IndirectY, opcode: 0x51 },
1211
- { mnemonic: "INC", mode: AddressingMode.ZeroPage, opcode: 0xe6 },
1212
- { mnemonic: "INC", mode: AddressingMode.ZeroPageX, opcode: 0xf6 },
1213
- { mnemonic: "INC", mode: AddressingMode.Absolute, opcode: 0xee },
1214
- { mnemonic: "INC", mode: AddressingMode.AbsoluteX, opcode: 0xfe },
1215
- { mnemonic: "INX", mode: AddressingMode.Implied, opcode: 0xe8 },
1216
- { mnemonic: "INY", mode: AddressingMode.Implied, opcode: 0xc8 },
1217
- { mnemonic: "JMP", mode: AddressingMode.Absolute, opcode: 0x4c },
1218
- { mnemonic: "JMP", mode: AddressingMode.Indirect, opcode: 0x6c },
1219
- { mnemonic: "JSR", mode: AddressingMode.Absolute, opcode: 0x20 },
1220
- { mnemonic: "LDA", mode: AddressingMode.Immediate, opcode: 0xa9 },
1221
- { mnemonic: "LDA", mode: AddressingMode.ZeroPage, opcode: 0xa5 },
1222
- { mnemonic: "LDA", mode: AddressingMode.ZeroPageX, opcode: 0xb5 },
1223
- { mnemonic: "LDA", mode: AddressingMode.Absolute, opcode: 0xad },
1224
- { mnemonic: "LDA", mode: AddressingMode.AbsoluteX, opcode: 0xbd },
1225
- { mnemonic: "LDA", mode: AddressingMode.AbsoluteY, opcode: 0xb9 },
1226
- { mnemonic: "LDA", mode: AddressingMode.IndirectX, opcode: 0xa1 },
1227
- { mnemonic: "LDA", mode: AddressingMode.IndirectY, opcode: 0xb1 },
1228
- { mnemonic: "LDX", mode: AddressingMode.Immediate, opcode: 0xa2 },
1229
- { mnemonic: "LDX", mode: AddressingMode.ZeroPage, opcode: 0xa6 },
1230
- { mnemonic: "LDX", mode: AddressingMode.ZeroPageY, opcode: 0xb6 },
1231
- { mnemonic: "LDX", mode: AddressingMode.Absolute, opcode: 0xae },
1232
- { mnemonic: "LDX", mode: AddressingMode.AbsoluteY, opcode: 0xbe },
1233
- { mnemonic: "LDY", mode: AddressingMode.Immediate, opcode: 0xa0 },
1234
- { mnemonic: "LDY", mode: AddressingMode.ZeroPage, opcode: 0xa4 },
1235
- { mnemonic: "LDY", mode: AddressingMode.ZeroPageX, opcode: 0xb4 },
1236
- { mnemonic: "LDY", mode: AddressingMode.Absolute, opcode: 0xac },
1237
- { mnemonic: "LDY", mode: AddressingMode.AbsoluteX, opcode: 0xbc },
1238
- { mnemonic: "LSR", mode: AddressingMode.Accumulator, opcode: 0x4a },
1239
- { mnemonic: "LSR", mode: AddressingMode.ZeroPage, opcode: 0x46 },
1240
- { mnemonic: "LSR", mode: AddressingMode.ZeroPageX, opcode: 0x56 },
1241
- { mnemonic: "LSR", mode: AddressingMode.Absolute, opcode: 0x4e },
1242
- { mnemonic: "LSR", mode: AddressingMode.AbsoluteX, opcode: 0x5e },
1243
- { mnemonic: "NOP", mode: AddressingMode.Implied, opcode: 0xea },
1244
- { mnemonic: "ORA", mode: AddressingMode.Immediate, opcode: 0x09 },
1245
- { mnemonic: "ORA", mode: AddressingMode.ZeroPage, opcode: 0x05 },
1246
- { mnemonic: "ORA", mode: AddressingMode.ZeroPageX, opcode: 0x15 },
1247
- { mnemonic: "ORA", mode: AddressingMode.Absolute, opcode: 0x0d },
1248
- { mnemonic: "ORA", mode: AddressingMode.AbsoluteX, opcode: 0x1d },
1249
- { mnemonic: "ORA", mode: AddressingMode.AbsoluteY, opcode: 0x19 },
1250
- { mnemonic: "ORA", mode: AddressingMode.IndirectX, opcode: 0x01 },
1251
- { mnemonic: "ORA", mode: AddressingMode.IndirectY, opcode: 0x11 },
1252
- { mnemonic: "PHA", mode: AddressingMode.Implied, opcode: 0x48 },
1253
- { mnemonic: "PHP", mode: AddressingMode.Implied, opcode: 0x08 },
1254
- { mnemonic: "PLA", mode: AddressingMode.Implied, opcode: 0x68 },
1255
- { mnemonic: "PLP", mode: AddressingMode.Implied, opcode: 0x28 },
1256
- { mnemonic: "ROL", mode: AddressingMode.Accumulator, opcode: 0x2a },
1257
- { mnemonic: "ROL", mode: AddressingMode.ZeroPage, opcode: 0x26 },
1258
- { mnemonic: "ROL", mode: AddressingMode.ZeroPageX, opcode: 0x36 },
1259
- { mnemonic: "ROL", mode: AddressingMode.Absolute, opcode: 0x2e },
1260
- { mnemonic: "ROL", mode: AddressingMode.AbsoluteX, opcode: 0x3e },
1261
- { mnemonic: "ROR", mode: AddressingMode.Accumulator, opcode: 0x6a },
1262
- { mnemonic: "ROR", mode: AddressingMode.ZeroPage, opcode: 0x66 },
1263
- { mnemonic: "ROR", mode: AddressingMode.ZeroPageX, opcode: 0x76 },
1264
- { mnemonic: "ROR", mode: AddressingMode.Absolute, opcode: 0x6e },
1265
- { mnemonic: "ROR", mode: AddressingMode.AbsoluteX, opcode: 0x7e },
1266
- { mnemonic: "RTI", mode: AddressingMode.Implied, opcode: 0x40 },
1267
- { mnemonic: "RTS", mode: AddressingMode.Implied, opcode: 0x60 },
1268
- { mnemonic: "SBC", mode: AddressingMode.Immediate, opcode: 0xe9 },
1269
- { mnemonic: "SBC", mode: AddressingMode.ZeroPage, opcode: 0xe5 },
1270
- { mnemonic: "SBC", mode: AddressingMode.ZeroPageX, opcode: 0xf5 },
1271
- { mnemonic: "SBC", mode: AddressingMode.Absolute, opcode: 0xed },
1272
- { mnemonic: "SBC", mode: AddressingMode.AbsoluteX, opcode: 0xfd },
1273
- { mnemonic: "SBC", mode: AddressingMode.AbsoluteY, opcode: 0xf9 },
1274
- { mnemonic: "SBC", mode: AddressingMode.IndirectX, opcode: 0xe1 },
1275
- { mnemonic: "SBC", mode: AddressingMode.IndirectY, opcode: 0xf1 },
1276
- { mnemonic: "SEC", mode: AddressingMode.Implied, opcode: 0x38 },
1277
- { mnemonic: "SED", mode: AddressingMode.Implied, opcode: 0xf8 },
1278
- { mnemonic: "SEI", mode: AddressingMode.Implied, opcode: 0x78 },
1279
- { mnemonic: "STA", mode: AddressingMode.ZeroPage, opcode: 0x85 },
1280
- { mnemonic: "STA", mode: AddressingMode.ZeroPageX, opcode: 0x95 },
1281
- { mnemonic: "STA", mode: AddressingMode.Absolute, opcode: 0x8d },
1282
- { mnemonic: "STA", mode: AddressingMode.AbsoluteX, opcode: 0x9d },
1283
- { mnemonic: "STA", mode: AddressingMode.AbsoluteY, opcode: 0x99 },
1284
- { mnemonic: "STA", mode: AddressingMode.IndirectX, opcode: 0x81 },
1285
- { mnemonic: "STA", mode: AddressingMode.IndirectY, opcode: 0x91 },
1286
- { mnemonic: "STX", mode: AddressingMode.ZeroPage, opcode: 0x86 },
1287
- { mnemonic: "STX", mode: AddressingMode.ZeroPageY, opcode: 0x96 },
1288
- { mnemonic: "STX", mode: AddressingMode.Absolute, opcode: 0x8e },
1289
- { mnemonic: "STY", mode: AddressingMode.ZeroPage, opcode: 0x84 },
1290
- { mnemonic: "STY", mode: AddressingMode.ZeroPageX, opcode: 0x94 },
1291
- { mnemonic: "STY", mode: AddressingMode.Absolute, opcode: 0x8c },
1292
- { mnemonic: "TAX", mode: AddressingMode.Implied, opcode: 0xaa },
1293
- { mnemonic: "TAY", mode: AddressingMode.Implied, opcode: 0xa8 },
1294
- { mnemonic: "TSX", mode: AddressingMode.Implied, opcode: 0xba },
1295
- { mnemonic: "TXA", mode: AddressingMode.Implied, opcode: 0x8a },
1296
- { mnemonic: "TXS", mode: AddressingMode.Implied, opcode: 0x9a },
1297
- { mnemonic: "TYA", mode: AddressingMode.Implied, opcode: 0x98 },
1298
- ];
1299
1202
  const INSTRUCTION_TABLE = buildInstructionTable();
@@ -6,7 +6,7 @@ Licensed under the GNU General Public License v2.0 or later.
6
6
  See <https://www.gnu.org/licenses/> for details.
7
7
  */
8
8
  import { Buffer } from "node:buffer";
9
- import { asciiToPetscii, encodeStringWithNames } from "./petscii.js";
9
+ import { asciiToPetscii, encodeStringWithNames } from "../../petscii.js";
10
10
  const DEFAULT_START_ADDRESS = 0x0801;
11
11
  const REM_TOKEN = 0x8f;
12
12
  const TOKEN_TABLE = buildTokenTable();