c64bridge 1.0.4 → 1.0.6

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 (63) hide show
  1. package/README.md +2 -2
  2. package/data/embeddings_basic.json +1 -1
  3. package/data/embeddings_hardware.json +1 -1
  4. package/data/embeddings_mixed.json +1 -1
  5. package/data/embeddings_other.json +1 -1
  6. package/data/graphics/character-set.csv +64 -64
  7. package/data/sound/sidwave.md +113 -36
  8. package/data/sound/sidwave.schema.json +138 -28
  9. package/dist/.tsbuildinfo +1 -1
  10. package/dist/c64Client.js +144 -150
  11. package/dist/config.js +93 -39
  12. package/dist/device.js +252 -130
  13. package/dist/diagnostics.js +10 -2
  14. package/dist/knowledge.js +2 -1
  15. package/dist/mcp-server.js +12 -4
  16. package/dist/petscii.js +18 -15
  17. package/dist/petsciiArt.js +40 -14
  18. package/dist/rag/indexer.js +2 -1
  19. package/dist/sidwave.js +453 -61
  20. package/dist/sidwaveCompiler.js +1424 -275
  21. package/dist/tools/audio.js +97 -18
  22. package/dist/tools/debug.js +9 -9
  23. package/dist/tools/developer.js +7 -7
  24. package/dist/tools/errors.js +19 -1
  25. package/dist/tools/graphics.js +4 -4
  26. package/dist/tools/hex.js +9 -0
  27. package/dist/tools/input.js +21 -18
  28. package/dist/tools/machineControl.js +6 -6
  29. package/dist/tools/memory.js +11 -10
  30. package/dist/tools/meta/audio.js +8 -2
  31. package/dist/tools/meta/background.js +6 -1
  32. package/dist/tools/meta/filesystem.js +37 -10
  33. package/dist/tools/meta/graphics.js +11 -16
  34. package/dist/tools/meta/memory.js +16 -22
  35. package/dist/tools/meta/program.js +28 -9
  36. package/dist/tools/pollValidator.js +30 -2
  37. package/dist/tools/printer.js +4 -4
  38. package/dist/tools/programRunners.js +10 -9
  39. package/dist/tools/registry/disk.js +7 -5
  40. package/dist/tools/storage.js +21 -17
  41. package/dist/tools/streaming.js +2 -2
  42. package/dist/tools/translation/assembler.js +192 -57
  43. package/dist/tools/translation/basicTokenizer.js +44 -10
  44. package/dist/tools/translation/opcodeInventory.js +1 -1
  45. package/dist/tools/types.js +11 -4
  46. package/dist/tools/vice.js +1 -1
  47. package/dist/vicBitmap.js +16 -0
  48. package/dist/vice/mockServer.js +99 -8
  49. package/dist/vice/readiness.js +4 -9
  50. package/dist/vice/viceClient.js +34 -6
  51. package/mcp/schemas/c64_disk.schema.json +2 -2
  52. package/mcp/schemas/c64_input.schema.json +3 -3
  53. package/mcp/schemas/c64_program.schema.json +2 -2
  54. package/mcp/schemas/c64_system.schema.json +6 -4
  55. package/mcp/schemas/c64_vice.schema.json +3 -3
  56. package/mcp/server.json +1 -1
  57. package/mcp/tools.json +20 -14
  58. package/mcp.json +1 -1
  59. package/package.json +1 -1
  60. package/scripts/mockC64Server.mjs +113 -20
  61. package/scripts/run-coverage.mjs +31 -1
  62. package/scripts/update-readme.ts +3 -2
  63. package/server.json +2 -2
@@ -97,10 +97,10 @@ const createD64ArgsSchema = objectSchema({
97
97
  properties: {
98
98
  path: diskImagePathSchema,
99
99
  tracks: optionalSchema(numberSchema({
100
- description: "Track count (35 for standard, 40 for extended).",
100
+ description: "Track count: 35 for a standard disk, up to 41 for the extended area (firmware limit).",
101
101
  integer: true,
102
102
  minimum: 35,
103
- maximum: 40,
103
+ maximum: 41,
104
104
  })),
105
105
  diskname: diskNameSchema,
106
106
  },
@@ -175,7 +175,7 @@ export const storageModule = defineToolModule({
175
175
  drivesListArgsSchema.parse(args ?? {});
176
176
  ctx.logger.info("Listing Ultimate drives");
177
177
  const details = await ctx.client.drivesList();
178
- return textResult("Retrieved Ultimate drive status.", {
178
+ return textResult("Retrieved drive status.", {
179
179
  success: true,
180
180
  drives: details,
181
181
  });
@@ -221,14 +221,16 @@ export const storageModule = defineToolModule({
221
221
  mode: parsed.mode,
222
222
  });
223
223
  if (!result.success) {
224
- throw new ToolExecutionError("C64 firmware reported failure while mounting disk image", {
224
+ throw new ToolExecutionError("C64 backend reported failure while mounting disk image", {
225
225
  details: normaliseFailure(result.details),
226
226
  });
227
227
  }
228
228
  const details = toRecord(result.details) ?? {};
229
229
  const finalMode = typeof details.mode === "string" ? details.mode : parsed.mode ?? null;
230
230
  const finalType = typeof details.type === "string" ? details.type : parsed.type ?? null;
231
- return textResult(`Mounted ${parsed.image} on ${parsed.drive}.`, {
231
+ // Backend notes (for example VICE's autoload resetting the machine) belong in the visible text.
232
+ const note = typeof details.note === "string" ? ` ${details.note}` : "";
233
+ return textResult(`Mounted ${parsed.image} on ${parsed.drive}.${note}`, {
232
234
  success: true,
233
235
  drive: parsed.drive,
234
236
  image: parsed.image,
@@ -269,7 +271,7 @@ export const storageModule = defineToolModule({
269
271
  ctx.logger.info("Removing disk image", { drive: parsed.drive });
270
272
  const result = await ctx.client.driveRemove(parsed.drive);
271
273
  if (!result.success) {
272
- throw new ToolExecutionError("C64 firmware reported failure while removing disk image", {
274
+ throw new ToolExecutionError("C64 backend reported failure while removing disk image", {
273
275
  details: normaliseFailure(result.details),
274
276
  });
275
277
  }
@@ -311,7 +313,7 @@ export const storageModule = defineToolModule({
311
313
  ctx.logger.info("Resetting drive", { drive: parsed.drive });
312
314
  const result = await ctx.client.driveReset(parsed.drive);
313
315
  if (!result.success) {
314
- throw new ToolExecutionError("C64 firmware reported failure while resetting drive", {
316
+ throw new ToolExecutionError("C64 backend reported failure while resetting drive", {
315
317
  details: normaliseFailure(result.details),
316
318
  });
317
319
  }
@@ -353,7 +355,7 @@ export const storageModule = defineToolModule({
353
355
  ctx.logger.info("Powering on drive", { drive: parsed.drive });
354
356
  const result = await ctx.client.driveOn(parsed.drive);
355
357
  if (!result.success) {
356
- throw new ToolExecutionError("C64 firmware reported failure while powering on drive", {
358
+ throw new ToolExecutionError("C64 backend reported failure while powering on drive", {
357
359
  details: normaliseFailure(result.details),
358
360
  });
359
361
  }
@@ -396,7 +398,7 @@ export const storageModule = defineToolModule({
396
398
  ctx.logger.info("Powering off drive", { drive: parsed.drive });
397
399
  const result = await ctx.client.driveOff(parsed.drive);
398
400
  if (!result.success) {
399
- throw new ToolExecutionError("C64 firmware reported failure while powering off drive", {
401
+ throw new ToolExecutionError("C64 backend reported failure while powering off drive", {
400
402
  details: normaliseFailure(result.details),
401
403
  });
402
404
  }
@@ -439,7 +441,7 @@ export const storageModule = defineToolModule({
439
441
  ctx.logger.info("Loading drive ROM", { drive: parsed.drive, path: parsed.path });
440
442
  const result = await ctx.client.driveLoadRom(parsed.drive, parsed.path);
441
443
  if (!result.success) {
442
- throw new ToolExecutionError("C64 firmware reported failure while loading drive ROM", {
444
+ throw new ToolExecutionError("C64 backend reported failure while loading drive ROM", {
443
445
  details: normaliseFailure(result.details),
444
446
  });
445
447
  }
@@ -482,7 +484,7 @@ export const storageModule = defineToolModule({
482
484
  ctx.logger.info("Setting drive mode", { drive: parsed.drive, mode: parsed.mode });
483
485
  const result = await ctx.client.driveSetMode(parsed.drive, parsed.mode);
484
486
  if (!result.success) {
485
- throw new ToolExecutionError("C64 firmware reported failure while setting drive mode", {
487
+ throw new ToolExecutionError("C64 backend reported failure while setting drive mode", {
486
488
  details: normaliseFailure(result.details),
487
489
  });
488
490
  }
@@ -541,7 +543,7 @@ export const storageModule = defineToolModule({
541
543
  {
542
544
  name: "create_d64",
543
545
  description: "Create a blank D64 disk image on the Ultimate filesystem.",
544
- summary: "Allocates a 35 or 40 track D64 image with an optional disk name.",
546
+ summary: "Allocates a 35 to 41 track D64 image with an optional disk name.",
545
547
  inputSchema: createD64ArgsSchema.jsonSchema,
546
548
  relatedResources: ["c64://guide/bootstrap"],
547
549
  tags: ["disk", "create"],
@@ -569,14 +571,16 @@ export const storageModule = defineToolModule({
569
571
  diskname: parsed.diskname,
570
572
  });
571
573
  if (!result.success) {
572
- throw new ToolExecutionError("C64 firmware reported failure while creating D64 image", {
574
+ throw new ToolExecutionError("C64 backend reported failure while creating D64 image", {
573
575
  details: normaliseFailure(result.details),
574
576
  });
575
577
  }
578
+ const firmwareTracks = toRecord(result.details)?.tracks;
576
579
  return textResult(`Created D64 image at ${parsed.path}.`, {
577
580
  success: true,
578
581
  path: parsed.path,
579
- tracks: parsed.tracks ?? null,
582
+ // The firmware reports the track count it used, including its default of 35.
583
+ tracks: typeof firmwareTracks === "number" ? firmwareTracks : parsed.tracks ?? 35,
580
584
  diskname: parsed.diskname ?? null,
581
585
  details: toRecord(result.details) ?? {},
582
586
  });
@@ -618,7 +622,7 @@ export const storageModule = defineToolModule({
618
622
  diskname: parsed.diskname,
619
623
  });
620
624
  if (!result.success) {
621
- throw new ToolExecutionError("C64 firmware reported failure while creating D71 image", {
625
+ throw new ToolExecutionError("C64 backend reported failure while creating D71 image", {
622
626
  details: normaliseFailure(result.details),
623
627
  });
624
628
  }
@@ -666,7 +670,7 @@ export const storageModule = defineToolModule({
666
670
  diskname: parsed.diskname,
667
671
  });
668
672
  if (!result.success) {
669
- throw new ToolExecutionError("C64 firmware reported failure while creating D81 image", {
673
+ throw new ToolExecutionError("C64 backend reported failure while creating D81 image", {
670
674
  details: normaliseFailure(result.details),
671
675
  });
672
676
  }
@@ -717,7 +721,7 @@ export const storageModule = defineToolModule({
717
721
  diskname: parsed.diskname,
718
722
  });
719
723
  if (!result.success) {
720
- throw new ToolExecutionError("C64 firmware reported failure while creating DNP image", {
724
+ throw new ToolExecutionError("C64 backend reported failure while creating DNP image", {
721
725
  details: normaliseFailure(result.details),
722
726
  });
723
727
  }
@@ -80,7 +80,7 @@ export const streamingModule = defineToolModule({
80
80
  });
81
81
  const result = await ctx.client.streamStart(parsed.stream, parsed.target);
82
82
  if (!result.success) {
83
- throw new ToolExecutionError(withFailureReason("C64 firmware reported failure while starting stream", result.details), {
83
+ throw new ToolExecutionError(withFailureReason("C64 backend reported failure while starting stream", result.details), {
84
84
  details: toRecord(result.details),
85
85
  });
86
86
  }
@@ -120,7 +120,7 @@ export const streamingModule = defineToolModule({
120
120
  });
121
121
  const result = await ctx.client.streamStop(parsed.stream);
122
122
  if (!result.success) {
123
- throw new ToolExecutionError(withFailureReason("C64 firmware reported failure while stopping stream", result.details), {
123
+ throw new ToolExecutionError(withFailureReason("C64 backend reported failure while stopping stream", result.details), {
124
124
  details: toRecord(result.details),
125
125
  });
126
126
  }
@@ -31,6 +31,41 @@ var AddressingMode;
31
31
  AddressingMode["Relative"] = "relative";
32
32
  })(AddressingMode || (AddressingMode = {}));
33
33
  const DEFAULT_LOAD_ADDRESS = 0x0801;
34
+ const BASIC_START = 0x0801;
35
+ // "10 SYS0nnnn": link $080C, line 10, SYS token, five digits, end of line, end
36
+ // of program. The digits are the first instruction's address; a fixed width
37
+ // keeps the stub, and so every address after it, the same in both passes.
38
+ function basicSysStub(target) {
39
+ const digits = String(target & 0xffff).padStart(5, "0");
40
+ return [0x0c, 0x08, 0x0a, 0x00, 0x9e, ...[...digits].map((digit) => digit.charCodeAt(0)), 0x00, 0x00, 0x00];
41
+ }
42
+ /**
43
+ * Address to SYS to start a PRG: the target of a leading "SYS <n>" BASIC line
44
+ * when the PRG loads at $0801, otherwise the load address.
45
+ */
46
+ export function prgEntryAddress(prg) {
47
+ const loadAddress = (prg[0] ?? 0) | ((prg[1] ?? 0) << 8);
48
+ if (loadAddress !== BASIC_START || prg.length < 8) {
49
+ return loadAddress;
50
+ }
51
+ // Skip the link pointer and line number, then expect the SYS token and digits.
52
+ let index = 6;
53
+ while (prg[index] === 0x20)
54
+ index += 1;
55
+ if (prg[index] !== 0x9e) {
56
+ return loadAddress;
57
+ }
58
+ index += 1;
59
+ while (prg[index] === 0x20 || prg[index] === 0x28)
60
+ index += 1;
61
+ let digits = "";
62
+ while (index < prg.length && prg[index] >= 0x30 && prg[index] <= 0x39) {
63
+ digits += String.fromCharCode(prg[index]);
64
+ index += 1;
65
+ }
66
+ const target = Number.parseInt(digits, 10);
67
+ return digits.length > 0 && target <= 0xffff ? target : loadAddress;
68
+ }
34
69
  const MAX_ADDRESS = 0xffff;
35
70
  const SINGLE_CHAR_TOKENS = new Set([",", ":", "#", "(", ")", "+", "-", "*", "/", "%", "&", "|", "^", "<", ">", "=", "!", "~"]);
36
71
  const MULTI_CHAR_TOKENS = new Set([
@@ -91,7 +126,13 @@ export function assemblyToPrgDetailed(source, options) {
91
126
  const assembler = new Assembler(statements, loadAddress);
92
127
  assembler.runPass(0);
93
128
  assembler.runPass(1);
94
- return { prg: assembler.buildPrg(), symbols: assembler.getSymbols() };
129
+ const prg = assembler.buildPrg();
130
+ const loadAddressOfPrg = prg.length >= 2 ? prg.readUInt16LE(0) : loadAddress;
131
+ const stubTarget = prg.length >= 2 ? prgEntryAddress(prg) : loadAddressOfPrg;
132
+ const entryAddress = stubTarget !== loadAddressOfPrg
133
+ ? stubTarget
134
+ : assembler.getFirstInstructionAddress() ?? loadAddressOfPrg;
135
+ return { prg, symbols: assembler.getSymbols(), entryAddress };
95
136
  }
96
137
  function normalizeSource(input) {
97
138
  return input.replace(/\r\n?/g, "\n");
@@ -280,6 +321,13 @@ function tokenize(line) {
280
321
  }
281
322
  if (isDigit(char) || char === "$") {
282
323
  const { value, nextIndex } = parseNumberToken(line, index);
324
+ const digits = line.slice(index, nextIndex);
325
+ const afterPercent = tokens[tokens.length - 1]?.kind === "punct" && tokens[tokens.length - 1]?.value === "%" && line[index - 1] === "%";
326
+ if (afterPercent && /^[01]+$/.test(digits)) {
327
+ tokens.push({ kind: "number", value, binaryValue: parseInt(digits, 2) });
328
+ index = nextIndex;
329
+ continue;
330
+ }
283
331
  tokens.push({ kind: "number", value });
284
332
  index = nextIndex;
285
333
  continue;
@@ -466,10 +514,29 @@ function parseUnaryExpression(stream, terminators) {
466
514
  if (!token) {
467
515
  throw new Error("Unexpected end of expression");
468
516
  }
469
- if (token.kind === "punct" && (token.value === "+" || token.value === "-" || token.value === "!" || token.value === "~")) {
517
+ // In operand position, "<" and ">" are byte selectors and "*" is the current
518
+ // PC; parseBinaryExpression consumes them as binary operators elsewhere.
519
+ // As in ACME, 64tass, DASM and Kick Assembler, "<" and ">" apply to the
520
+ // whole rest of the expression: "#>msg+1" is the high byte of msg+1.
521
+ if (token.kind === "punct" && (token.value === "<" || token.value === ">")) {
522
+ const operand = parseBinaryExpression(stream, terminators, 0);
523
+ return { type: "unary", op: token.value, operand };
524
+ }
525
+ if (token.kind === "punct" &&
526
+ (token.value === "+" || token.value === "-" || token.value === "!" || token.value === "~")) {
470
527
  const operand = parseUnaryExpression(stream, terminators);
471
528
  return { type: "unary", op: token.value, operand };
472
529
  }
530
+ if (token.kind === "punct" && token.value === "*") {
531
+ return { type: "symbol", name: "*" };
532
+ }
533
+ if (token.kind === "punct" && token.value === "%") {
534
+ const digits = stream.peek();
535
+ if (digits?.kind === "number" && digits.binaryValue !== undefined) {
536
+ stream.consume();
537
+ return { type: "number", value: digits.binaryValue };
538
+ }
539
+ }
473
540
  if (token.kind === "punct" && token.value === "(") {
474
541
  const expr = parseExpression(stream, new Set([")"]));
475
542
  const closing = stream.consume();
@@ -591,17 +658,16 @@ class SymbolTable {
591
658
  }
592
659
  }
593
660
  ensureLabel(label, expected) {
594
- const existing = this.get(label);
595
- if (existing === undefined) {
596
- throw new Error(`Symbol "${label}" was not defined in the first pass`);
597
- }
598
- if (existing !== expected) {
599
- throw new Error(`Symbol "${label}" changed value between passes (expected ${formatHex(existing)}, got ${formatHex(expected)})`);
600
- }
661
+ this.ensureUnchanged(label, expected);
601
662
  if (!label.startsWith(".")) {
602
663
  this.currentScope = label;
603
664
  }
604
665
  }
666
+ // Constant assignments never open a local-label scope (as in ACME and
667
+ // 64tass), matching setAssignment in the first pass.
668
+ ensureAssignment(label, expected) {
669
+ this.ensureUnchanged(label, expected);
670
+ }
605
671
  setAssignment(label, value) {
606
672
  if (label.startsWith(".")) {
607
673
  if (!this.currentScope) {
@@ -628,6 +694,15 @@ class SymbolTable {
628
694
  allGlobals() {
629
695
  return this.globals;
630
696
  }
697
+ ensureUnchanged(label, expected) {
698
+ const existing = this.get(label);
699
+ if (existing === undefined) {
700
+ throw new Error(`Symbol "${label}" was not defined in the first pass`);
701
+ }
702
+ if (existing !== expected) {
703
+ throw new Error(`Symbol "${label}" changed value between passes (expected ${formatHex(existing)}, got ${formatHex(expected)})`);
704
+ }
705
+ }
631
706
  ensureLocalScope(scope) {
632
707
  if (!this.locals.has(scope)) {
633
708
  this.locals.set(scope, new Map());
@@ -638,21 +713,36 @@ class SymbolTable {
638
713
  class Assembler {
639
714
  constructor(lines, defaultLoadAddress) {
640
715
  this.lines = lines;
716
+ this.defaultLoadAddress = defaultLoadAddress;
641
717
  this.symbolTable = new SymbolTable();
642
718
  this.memory = new Uint8Array(0x10000);
643
719
  this.written = new Uint8Array(0x10000);
644
720
  this.pc = 0;
645
721
  this.lowestWritten = MAX_ADDRESS;
646
722
  this.highestWritten = 0;
647
- this.loadAddress = defaultLoadAddress;
723
+ /** Reserved (.res) ranges from the last pass, as [start, end] inclusive. */
724
+ this.reserved = [];
648
725
  this.symbolTable.reset();
649
726
  }
650
727
  runPass(pass) {
651
- this.pc = 0;
728
+ // Code before the first org directive is assembled at the load address.
729
+ this.pc = this.defaultLoadAddress & 0xffff;
652
730
  if (pass === 0) {
653
731
  this.symbolTable.reset();
654
732
  this.lowestWritten = MAX_ADDRESS;
655
733
  this.highestWritten = 0;
734
+ this.firstInstructionAddress = undefined;
735
+ }
736
+ this.reserved = [];
737
+ // Loading a PRG at $0801 makes BASIC relink the bytes there as program
738
+ // lines, which corrupts raw machine code. Code that starts at $0801
739
+ // without its own org therefore gets a "10 SYS" stub that jumps to the
740
+ // first instruction, even when data comes before it.
741
+ if (this.pc === BASIC_START && this.emitsBeforeFirstOrg()) {
742
+ // Pass 0 does not know the first instruction's address yet; pass 1 uses the address pass 0 recorded.
743
+ for (const byte of basicSysStub(pass === 1 ? this.firstInstructionAddress ?? 0 : 0)) {
744
+ this.writeByte(byte, this.lines[0].location);
745
+ }
656
746
  }
657
747
  for (const line of this.lines) {
658
748
  try {
@@ -667,17 +757,57 @@ class Assembler {
667
757
  }
668
758
  }
669
759
  }
760
+ emitsBeforeFirstOrg() {
761
+ for (const line of this.lines) {
762
+ const statement = line.statement;
763
+ if (!statement)
764
+ continue;
765
+ if (statement.kind === "instruction")
766
+ return true;
767
+ if (statement.kind === "assignment" && statement.isLocationCounter)
768
+ return false;
769
+ if (statement.kind === "directive") {
770
+ if (statement.name === "org")
771
+ return false;
772
+ if (statement.name !== "include")
773
+ return true;
774
+ }
775
+ }
776
+ return false;
777
+ }
670
778
  buildPrg() {
671
- if (this.lowestWritten === MAX_ADDRESS || this.highestWritten < this.loadAddress) {
779
+ // The PRG spans the bytes actually emitted, so org blocks that only define
780
+ // labels neither move the load address nor add padding.
781
+ if (this.lowestWritten > this.highestWritten) {
672
782
  return Buffer.alloc(0);
673
783
  }
674
- const start = Math.min(this.lowestWritten, this.loadAddress);
675
- const end = this.highestWritten;
784
+ let start = this.lowestWritten;
785
+ let end = this.highestWritten;
786
+ // Reserved space that adjoins the emitted bytes (a trailing "counter: .res 1")
787
+ // is part of the image and zero-filled, as in ca65 and DASM; detached blocks
788
+ // such as zero-page variables are not, so they do not stretch the PRG.
789
+ for (let changed = true; changed;) {
790
+ changed = false;
791
+ for (const [resStart, resEnd] of this.reserved) {
792
+ // Adjoining or overlapping blocks extend the image.
793
+ if (resStart <= end + 1 && resEnd > end) {
794
+ end = resEnd;
795
+ changed = true;
796
+ }
797
+ if (resEnd >= start - 1 && resStart < start) {
798
+ start = resStart;
799
+ changed = true;
800
+ }
801
+ }
802
+ }
676
803
  const body = this.memory.subarray(start, end + 1);
677
804
  const header = Buffer.alloc(2);
678
805
  header.writeUInt16LE(start, 0);
679
806
  return Buffer.concat([header, Buffer.from(body)]);
680
807
  }
808
+ getFirstInstructionAddress() {
809
+ return this.firstInstructionAddress;
810
+ }
681
811
  getSymbols() {
682
812
  return this.symbolTable.allGlobals();
683
813
  }
@@ -714,15 +844,12 @@ class Assembler {
714
844
  }
715
845
  if (statement.isLocationCounter) {
716
846
  this.pc = value & 0xffff;
717
- if (pass === 0) {
718
- this.loadAddress = this.pc;
719
- }
720
847
  }
721
848
  else if (pass === 0) {
722
849
  this.symbolTable.setAssignment(statement.target, value);
723
850
  }
724
851
  else {
725
- this.symbolTable.ensureLabel(statement.target, value);
852
+ this.symbolTable.ensureAssignment(statement.target, value);
726
853
  }
727
854
  }
728
855
  handleDirective(statement, pass, location) {
@@ -736,9 +863,6 @@ class Assembler {
736
863
  throw new AssemblyError("org directive requires a defined value", location);
737
864
  }
738
865
  this.pc = value & 0xffff;
739
- if (pass === 0) {
740
- this.loadAddress = this.pc;
741
- }
742
866
  break;
743
867
  }
744
868
  case "byte": {
@@ -809,14 +933,11 @@ class Assembler {
809
933
  if (amount < 0) {
810
934
  throw new AssemblyError("reserve directive requires a non-negative size", location);
811
935
  }
812
- if (pass === 1) {
813
- for (let i = 0; i < amount; i += 1) {
814
- this.writeByte(0, location);
815
- }
816
- }
817
- else {
818
- this.pc += amount;
819
- }
936
+ // Reserved space advances the PC without emitting; buildPrg includes it
937
+ // (zero-filled) only where it adjoins emitted bytes.
938
+ if (amount > 0)
939
+ this.reserved.push([this.pc, Math.min(0xffff, this.pc + amount - 1)]);
940
+ this.pc = (this.pc + amount) & 0xffff;
820
941
  break;
821
942
  }
822
943
  case "include": {
@@ -831,6 +952,7 @@ class Assembler {
831
952
  const info = determineInstruction(statement, pass, this.evaluate.bind(this), this.pc, location);
832
953
  statement.resolvedMode = info.mode;
833
954
  if (pass === 0) {
955
+ this.firstInstructionAddress ?? (this.firstInstructionAddress = this.pc);
834
956
  this.pc += info.size;
835
957
  return;
836
958
  }
@@ -889,6 +1011,10 @@ class Assembler {
889
1011
  return value ? 0 : 1;
890
1012
  case "~":
891
1013
  return ~value;
1014
+ case "<":
1015
+ return value & 0xff;
1016
+ case ">":
1017
+ return (value >> 8) & 0xff;
892
1018
  default:
893
1019
  throw new AssemblyError("Unknown unary operator", location);
894
1020
  }
@@ -961,6 +1087,7 @@ class Assembler {
961
1087
  function determineInstruction(statement, pass, evaluator, locationCounter, location) {
962
1088
  if (statement.resolvedMode && pass === 1) {
963
1089
  const value = statement.operand.kind === "none" ? undefined : resolveOperandValue(statement.operand, evaluator, pass, location);
1090
+ assertZeroPageOperand(statement.mnemonic, statement.resolvedMode, value, location);
964
1091
  const size = instructionSize(statement.resolvedMode);
965
1092
  return {
966
1093
  mode: statement.resolvedMode,
@@ -1040,43 +1167,51 @@ function chooseAddressingMode(mnemonic, operand, value, pass, location) {
1040
1167
  return AddressingMode.Relative;
1041
1168
  }
1042
1169
  if (register === "X") {
1043
- if (available.has(AddressingMode.ZeroPageX) && value !== undefined && fitsByte(value)) {
1044
- return AddressingMode.ZeroPageX;
1045
- }
1046
- if (available.has(AddressingMode.AbsoluteX)) {
1047
- return AddressingMode.AbsoluteX;
1048
- }
1049
- }
1050
- else if (register === "Y") {
1051
- if (available.has(AddressingMode.ZeroPageY) && value !== undefined && fitsByte(value)) {
1052
- return AddressingMode.ZeroPageY;
1053
- }
1054
- if (available.has(AddressingMode.AbsoluteY)) {
1055
- return AddressingMode.AbsoluteY;
1056
- }
1170
+ return chooseZeroPageOrAbsolute(mnemonic, available, AddressingMode.ZeroPageX, AddressingMode.AbsoluteX, value, location);
1057
1171
  }
1058
- else if (register === "S") {
1059
- if (available.has(AddressingMode.ZeroPageS)) {
1060
- return AddressingMode.ZeroPageS;
1061
- }
1172
+ if (register === "Y") {
1173
+ return chooseZeroPageOrAbsolute(mnemonic, available, AddressingMode.ZeroPageY, AddressingMode.AbsoluteY, value, location);
1062
1174
  }
1063
- else {
1064
- if (available.has(AddressingMode.ZeroPage) && value !== undefined && fitsByte(value)) {
1065
- return AddressingMode.ZeroPage;
1066
- }
1067
- if (available.has(AddressingMode.Absolute)) {
1068
- return AddressingMode.Absolute;
1175
+ if (register === "S") {
1176
+ if (!available.has(AddressingMode.ZeroPageS)) {
1177
+ throw new AssemblyError(`Instruction ${mnemonic} does not support expr,S addressing`, location);
1069
1178
  }
1179
+ return AddressingMode.ZeroPageS;
1070
1180
  }
1071
- if (available.has(AddressingMode.Absolute)) {
1072
- return AddressingMode.Absolute;
1073
- }
1074
- throw new AssemblyError(`Instruction ${mnemonic} cannot resolve addressing mode`, location);
1181
+ return chooseZeroPageOrAbsolute(mnemonic, available, AddressingMode.ZeroPage, AddressingMode.Absolute, value, location);
1075
1182
  }
1076
1183
  default:
1077
1184
  throw new AssemblyError("Unsupported operand kind", location);
1078
1185
  }
1079
1186
  }
1187
+ function chooseZeroPageOrAbsolute(mnemonic, available, zeroPageMode, absoluteMode, value, location) {
1188
+ const hasZeroPage = available.has(zeroPageMode);
1189
+ const hasAbsolute = available.has(absoluteMode);
1190
+ if (hasZeroPage && value !== undefined && fitsByte(value)) {
1191
+ return zeroPageMode;
1192
+ }
1193
+ if (hasAbsolute) {
1194
+ return absoluteMode;
1195
+ }
1196
+ // Without an absolute form (e.g. STX zp,Y), a forward reference must be
1197
+ // sized as zero page; the second pass rejects values outside zero page.
1198
+ if (hasZeroPage && value === undefined) {
1199
+ return zeroPageMode;
1200
+ }
1201
+ throw new AssemblyError(`Instruction ${mnemonic} does not support ${absoluteMode} addressing`, location);
1202
+ }
1203
+ function assertZeroPageOperand(mnemonic, mode, value, location) {
1204
+ const absoluteMode = ZERO_PAGE_TO_ABSOLUTE_MODE.get(mode);
1205
+ if (absoluteMode === undefined || value === undefined || fitsByte(value)) {
1206
+ return;
1207
+ }
1208
+ throw new AssemblyError(`Instruction ${mnemonic} does not support ${absoluteMode} addressing (operand ${formatHex(value)} is outside zero page)`, location);
1209
+ }
1210
+ const ZERO_PAGE_TO_ABSOLUTE_MODE = new Map([
1211
+ [AddressingMode.ZeroPage, AddressingMode.Absolute],
1212
+ [AddressingMode.ZeroPageX, AddressingMode.AbsoluteX],
1213
+ [AddressingMode.ZeroPageY, AddressingMode.AbsoluteY],
1214
+ ]);
1080
1215
  function instructionSize(mode) {
1081
1216
  switch (mode) {
1082
1217
  case AddressingMode.Implied: