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.
- package/README.md +2 -2
- package/data/embeddings_basic.json +1 -1
- package/data/embeddings_hardware.json +1 -1
- package/data/embeddings_mixed.json +1 -1
- package/data/embeddings_other.json +1 -1
- package/data/graphics/character-set.csv +64 -64
- package/data/sound/sidwave.md +113 -36
- package/data/sound/sidwave.schema.json +138 -28
- package/dist/.tsbuildinfo +1 -1
- package/dist/c64Client.js +144 -150
- package/dist/config.js +93 -39
- package/dist/device.js +252 -130
- package/dist/diagnostics.js +10 -2
- package/dist/knowledge.js +2 -1
- package/dist/mcp-server.js +12 -4
- package/dist/petscii.js +18 -15
- package/dist/petsciiArt.js +40 -14
- package/dist/rag/indexer.js +2 -1
- package/dist/sidwave.js +453 -61
- package/dist/sidwaveCompiler.js +1424 -275
- package/dist/tools/audio.js +97 -18
- package/dist/tools/debug.js +9 -9
- package/dist/tools/developer.js +7 -7
- package/dist/tools/errors.js +19 -1
- package/dist/tools/graphics.js +4 -4
- package/dist/tools/hex.js +9 -0
- package/dist/tools/input.js +21 -18
- package/dist/tools/machineControl.js +6 -6
- package/dist/tools/memory.js +11 -10
- package/dist/tools/meta/audio.js +8 -2
- package/dist/tools/meta/background.js +6 -1
- package/dist/tools/meta/filesystem.js +37 -10
- package/dist/tools/meta/graphics.js +11 -16
- package/dist/tools/meta/memory.js +16 -22
- package/dist/tools/meta/program.js +28 -9
- package/dist/tools/pollValidator.js +30 -2
- package/dist/tools/printer.js +4 -4
- package/dist/tools/programRunners.js +10 -9
- package/dist/tools/registry/disk.js +7 -5
- package/dist/tools/storage.js +21 -17
- package/dist/tools/streaming.js +2 -2
- package/dist/tools/translation/assembler.js +192 -57
- package/dist/tools/translation/basicTokenizer.js +44 -10
- package/dist/tools/translation/opcodeInventory.js +1 -1
- package/dist/tools/types.js +11 -4
- package/dist/tools/vice.js +1 -1
- package/dist/vicBitmap.js +16 -0
- package/dist/vice/mockServer.js +99 -8
- package/dist/vice/readiness.js +4 -9
- package/dist/vice/viceClient.js +34 -6
- package/mcp/schemas/c64_disk.schema.json +2 -2
- package/mcp/schemas/c64_input.schema.json +3 -3
- package/mcp/schemas/c64_program.schema.json +2 -2
- package/mcp/schemas/c64_system.schema.json +6 -4
- package/mcp/schemas/c64_vice.schema.json +3 -3
- package/mcp/server.json +1 -1
- package/mcp/tools.json +20 -14
- package/mcp.json +1 -1
- package/package.json +1 -1
- package/scripts/mockC64Server.mjs +113 -20
- package/scripts/run-coverage.mjs +31 -1
- package/scripts/update-readme.ts +3 -2
- package/server.json +2 -2
package/dist/tools/storage.js
CHANGED
|
@@ -97,10 +97,10 @@ const createD64ArgsSchema = objectSchema({
|
|
|
97
97
|
properties: {
|
|
98
98
|
path: diskImagePathSchema,
|
|
99
99
|
tracks: optionalSchema(numberSchema({
|
|
100
|
-
description: "Track count
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
724
|
+
throw new ToolExecutionError("C64 backend reported failure while creating DNP image", {
|
|
721
725
|
details: normaliseFailure(result.details),
|
|
722
726
|
});
|
|
723
727
|
}
|
package/dist/tools/streaming.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
675
|
-
|
|
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.
|
|
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
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1059
|
-
|
|
1060
|
-
return AddressingMode.ZeroPageS;
|
|
1061
|
-
}
|
|
1172
|
+
if (register === "Y") {
|
|
1173
|
+
return chooseZeroPageOrAbsolute(mnemonic, available, AddressingMode.ZeroPageY, AddressingMode.AbsoluteY, value, location);
|
|
1062
1174
|
}
|
|
1063
|
-
|
|
1064
|
-
if (available.has(AddressingMode.
|
|
1065
|
-
|
|
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
|
-
|
|
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:
|