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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +19 -59
  2. package/package.json +1 -1
  3. package/skills/c64-assembler/SKILL.md +5 -2
  4. package/skills/c64-assembler/scripts/assemble.ts +8 -1
  5. package/skills/c64-assembler/scripts/compare.ts +84 -0
  6. package/skills/c64-basic/SKILL.md +1 -2
  7. package/skills/c64-disk/SKILL.md +20 -6
  8. package/skills/c64-disk/scripts/containers.ts +124 -0
  9. package/skills/c64-disk/scripts/disk.ts +59 -5
  10. package/skills/c64-emulator/SKILL.md +11 -4
  11. package/skills/c64-knowledge/SKILL.md +53 -3
  12. package/skills/c64-knowledge/scripts/knowledge.ts +258 -29
  13. package/skills/c64-memory-map/SKILL.md +4 -1
  14. package/skills/c64-memory-map/scripts/memmap.ts +14 -3
  15. package/skills/c64-provenance/SKILL.md +37 -9
  16. package/skills/c64-provenance/scripts/compare.ts +60 -29
  17. package/skills/c64-provenance/scripts/provenance.ts +248 -19
  18. package/skills/c64-provenance/scripts/runtime.ts +179 -0
  19. package/skills/c64-provenance/scripts/separate.ts +132 -0
  20. package/skills/c64-reverse-engineering/SKILL.md +65 -11
  21. package/skills/c64-source/SKILL.md +119 -0
  22. package/skills/c64-source/scripts/generate.ts +402 -0
  23. package/skills/c64-source/scripts/loader.ts +320 -0
  24. package/skills/c64-source/scripts/opcodes.ts +61 -0
  25. package/skills/c64-source/scripts/source.ts +558 -0
  26. package/skills/c64-static-analysis/SKILL.md +27 -4
  27. package/skills/c64-static-analysis/scripts/analyze.ts +102 -13
  28. package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
  29. package/skills/c64-static-analysis/scripts/findings.ts +6 -4
  30. package/skills/c64-static-analysis/scripts/idioms.ts +144 -0
  31. package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
  32. package/skills/c64-testing/SKILL.md +27 -3
  33. package/skills/c64-testing/references/scenario-format.md +43 -3
  34. package/skills/c64-testing/scripts/run.ts +189 -0
  35. package/skills/c64-testing/scripts/scenario.ts +153 -135
  36. package/skills/c64-testing/scripts/test.ts +16 -6
  37. package/skills/c64-unpacker/SKILL.md +65 -14
  38. package/skills/c64-unpacker/scripts/evidence.ts +86 -21
  39. package/skills/c64-unpacker/scripts/loads.ts +174 -0
  40. package/skills/c64-unpacker/scripts/machine.ts +68 -0
  41. package/skills/c64-unpacker/scripts/unpack.ts +302 -48
  42. package/src/c64.ts +91 -0
  43. package/src/cli/status.ts +3 -1
  44. package/src/host/main.ts +10 -25
  45. package/src/host/server.ts +8 -40
  46. package/src/host/vice/adapter.ts +25 -10
  47. package/src/host/vice/session.ts +64 -20
  48. package/src/host-client/autostart.ts +45 -0
  49. package/src/host-client/connect.ts +4 -5
  50. package/src/host-client/coverage.ts +59 -0
  51. package/src/host-client/steps.ts +170 -0
  52. package/src/host-client/transfer.ts +29 -3
  53. package/src/host-client/vice-session.ts +42 -13
  54. package/src/knowledge/history.ts +40 -13
  55. package/src/knowledge/import.ts +72 -56
  56. package/src/knowledge/read.ts +203 -52
  57. package/src/knowledge/schema.ts +94 -12
  58. package/src/knowledge/write.ts +432 -123
  59. package/src/mcp/main.ts +1 -4
  60. package/src/mcp/output.ts +32 -0
  61. package/src/mcp/server.ts +4 -2
  62. package/src/mcp/tools/debug.ts +110 -24
  63. package/src/mcp/tools/input.ts +1 -0
  64. package/src/mcp/tools/media.ts +22 -6
  65. package/src/mcp/tools/memory.ts +57 -10
  66. package/src/mcp/tools/video.ts +9 -3
  67. package/src/native/discover.ts +5 -2
  68. package/src/native/ghidra/index.ts +19 -3
  69. package/src/native/ghidra/scripts/C64Export.java +33 -1
  70. package/src/native/status.ts +15 -3
  71. package/src/native/types.ts +30 -1
  72. package/{skills/c64-memory-map/scripts → src}/platform.ts +18 -2
  73. package/src/protocol/messages.ts +17 -10
  74. package/src/protocol/vice.ts +72 -18
  75. package/src/script.ts +3 -2
@@ -0,0 +1,402 @@
1
+ // The c64-source script (D48): generates ACME source from an image, current
2
+ // knowledge and Ghidra's instructions, which it runs itself, and checks with
3
+ // ACME that the source assembles to the image byte by byte. With --loader it
4
+ // also writes a loader that recreates the entry state, simulates it, and
5
+ // checks it in the emulator. Exit status: 0 when every check passes, 1 when
6
+ // one fails, 2 for a bad call.
7
+
8
+ import { posix } from "node:path";
9
+ import { parseArgs } from "node:util";
10
+
11
+ import { formatC64Address, parseC64Address, prgRange, textToPetscii } from "#src/c64.ts";
12
+ import { autostartLoaded, RUN_LIMIT_FRAMES } from "#src/host-client/autostart.ts";
13
+ import { readCoverageFiles } from "#src/host-client/coverage.ts";
14
+ import { MAX_MEMORY_READ, WireFailure } from "#src/host-client/tools.ts";
15
+ import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
16
+ import { ViceSessionClient } from "#src/host-client/vice-session.ts";
17
+ import { KnowledgeError, openForRead } from "#src/knowledge/database.ts";
18
+ import { currentRevision, listComments, listModules, listReferences, listRegions, listSymbols } from "#src/knowledge/read.ts";
19
+ import { CODE_REGION_TYPES, DEFAULT_SPACE, SPACE, type Space } from "#src/knowledge/schema.ts";
20
+ import { assemble } from "#src/native/acme.ts";
21
+ import { analyze } from "#src/native/ghidra/analyze.ts";
22
+ import { localToolContext } from "#src/native/local.ts";
23
+ import { GHIDRA_LIMITS, type Diagnostic } from "#src/native/types.ts";
24
+ import { platformNames } from "#src/platform.ts";
25
+ import { resolveProjectPath } from "#src/project.ts";
26
+ import { runScript, UsageError } from "#src/script.ts";
27
+ import { planLoader, readEntryState, simulate, type EntryState, type LoaderPlan } from "./loader.ts";
28
+ import { OPCODES } from "./opcodes.ts";
29
+ import { instructionText, layout, literalOperand, probeForms, render, type Layout, type Range } from "./source.ts";
30
+
31
+ const USAGE = `generate.ts <image> --out <dir> [options]
32
+
33
+ <image> an image.json of the c64-unpacker skill, or a PRG, relative to
34
+ the project directory
35
+ --out <dir> the directory for the source files, relative to the project
36
+ --entry <address> an entry point for Ghidra, for example '$080d' (repeatable)
37
+ --coverage <file> a coverage map of 64 KiB from c64_memmap (repeatable): code
38
+ that no run executed is marked and listed
39
+ --space <space> the knowledge space of the image: c64 (the default), drive8, or
40
+ bank0 to bank255; --coverage and --loader are for c64 only
41
+ --loader also write loader.asm and loader.prg: the loader copies the
42
+ segments into place, sets the entry state from entry_state.json
43
+ next to image.json, and jumps to the entry; it is checked in
44
+ the emulator
45
+
46
+ The result is one JSON object. Addresses are $ and four hex digits.`;
47
+
48
+ const MAX_LISTED = 20;
49
+ const PROBE_BASE = 0x1000;
50
+ const PROBE_SLOT = 4;
51
+ /** The copies of a full 64 KiB take about 140 frames. */
52
+ const LOADER_FRAMES = 1500;
53
+
54
+ interface Image {
55
+ path: string;
56
+ segments: Range[];
57
+ byteAt: (address: number) => number;
58
+ /** The bytes that Ghidra analyzes, and how. */
59
+ analyzed: { bytes: Buffer; kind: "prg" | "flat64k" };
60
+ entry?: number;
61
+ /** entry_state.json next to image.json, when it exists. */
62
+ statePath?: string;
63
+ }
64
+
65
+ function readImage(path: string): Image {
66
+ const { bytes } = readProjectFile(path);
67
+ if (!path.endsWith(".json")) {
68
+ let range: ReturnType<typeof prgRange>;
69
+ try {
70
+ range = prgRange(bytes);
71
+ } catch (error) {
72
+ throw new UsageError(`${path}: ${(error as Error).message}`);
73
+ }
74
+ return { path, segments: [{ start: range.start, end: range.end }], byteAt: (address) => range.body[address - range.start]!, analyzed: { bytes, kind: "prg" } };
75
+ }
76
+ const fail = (what: string): never => {
77
+ throw new UsageError(`${path} is not an image.json of the c64-unpacker skill: ${what}`);
78
+ };
79
+ let manifest: Record<string, unknown>;
80
+ try {
81
+ manifest = JSON.parse(bytes.toString("utf8")) as Record<string, unknown>;
82
+ } catch (error) {
83
+ return fail((error as Error).message);
84
+ }
85
+ const address = (value: unknown, what: string) => (typeof value === "string" && /^\$[0-9a-f]{4}$/i.test(value) ? Number.parseInt(value.slice(1), 16) : fail(`${what} is not a $ address`));
86
+ if (typeof manifest.image !== "string") fail("it has no image");
87
+ if (!Array.isArray(manifest.segments) || manifest.segments.length === 0) fail("it has no segments");
88
+ const segments = (manifest.segments as Array<Record<string, unknown>>)
89
+ .map((segment, index) => ({ start: address(segment.start, `segment ${index + 1} start`), end: address(segment.end, `segment ${index + 1} end`) }))
90
+ .sort((a, b) => a.start - b.start);
91
+ segments.forEach((segment, index) => {
92
+ if (segment.end < segment.start) fail(`the segment at ${formatC64Address(segment.start)} ends before it starts`);
93
+ if (index > 0 && segment.start <= segments[index - 1]!.end) fail(`the segments at ${formatC64Address(segments[index - 1]!.start)} and ${formatC64Address(segment.start)} overlap`);
94
+ });
95
+ const directory = posix.dirname(path);
96
+ const memory = readProjectFile(posix.join(directory, manifest.image as string)).bytes;
97
+ if (memory.length !== 0x10000) fail(`its image has ${memory.length} bytes, not 65536`);
98
+ const image: Image = { path, segments, byteAt: (at) => memory[at]!, analyzed: { bytes: memory, kind: "flat64k" }, statePath: posix.join(directory, "entry_state.json") };
99
+ if (manifest.entry !== undefined) image.entry = address(manifest.entry, "entry");
100
+ return image;
101
+ }
102
+
103
+ /** The knowledge of one space that the source uses, read once. */
104
+ function readKnowledge(space: Space) {
105
+ const db = openForRead();
106
+ try {
107
+ return {
108
+ revision: currentRevision(db),
109
+ symbols: listSymbols(db, undefined, undefined, undefined, space),
110
+ regions: listRegions(db, undefined, undefined, undefined, space),
111
+ comments: listComments(db, undefined, undefined, space),
112
+ modules: listModules(db, undefined, undefined, space),
113
+ references: listReferences(db, undefined, undefined, undefined, space),
114
+ };
115
+ } finally {
116
+ db?.close();
117
+ }
118
+ }
119
+
120
+ /** Ghidra's instruction starts and lengths inside the image, seeded from the entry and from knowledge of any origin. */
121
+ async function decode(image: Image, knowledge: ReturnType<typeof readKnowledge>, entries: number[]): Promise<Map<number, number>> {
122
+ const inImage = (address: number) => image.segments.some((segment) => address >= segment.start && address <= segment.end);
123
+ const seeds = new Set<number>([...(image.entry === undefined ? [] : [image.entry]), ...entries]);
124
+ for (const symbol of knowledge.symbols) if (symbol.kind === "routine") seeds.add(symbol.address);
125
+ for (const region of knowledge.regions) {
126
+ if (CODE_REGION_TYPES.includes(region.type)) seeds.add(region.start);
127
+ if (region.type === "jumps") for (let at = region.start; at + 2 <= region.end; at += 3) seeds.add(at);
128
+ }
129
+ const entryPoints = [...seeds].filter(inImage).sort((a, b) => a - b);
130
+ if (entryPoints.length === 0) {
131
+ throw new UsageError("Ghidra needs an entry point: give --entry, or record a routine or a code region in knowledge with the c64-knowledge skill");
132
+ }
133
+ const dataRanges = knowledge.regions
134
+ .filter((region) => !CODE_REGION_TYPES.includes(region.type) && region.type !== "dual-use")
135
+ .flatMap((region) => image.segments.filter((segment) => region.start <= segment.end && segment.start <= region.end).map((segment) => ({ start: Math.max(region.start, segment.start), end: Math.min(region.end, segment.end) })));
136
+ const { result } = await analyze(
137
+ { imageKind: image.analyzed.kind, entryPoints: entryPoints.slice(0, GHIDRA_LIMITS.entryPoints), dataRanges: dataRanges.slice(0, GHIDRA_LIMITS.dataRanges), labels: [], decompile: [] },
138
+ image.analyzed.bytes,
139
+ localToolContext(),
140
+ );
141
+ const instructions = new Map<number, number>();
142
+ for (const instruction of result.instructions) {
143
+ const bytes = Buffer.from(instruction.bytes, "hex");
144
+ const inside = Array.from({ length: bytes.length }, (_, offset) => instruction.address + offset).every((address, offset) => inImage(address) && image.byteAt(address) === bytes[offset]);
145
+ if (inside) instructions.set(instruction.address, bytes.length);
146
+ }
147
+ return instructions;
148
+ }
149
+
150
+ /** Assembles files given as text with ACME; the program or the diagnostics. */
151
+ async function assembleFiles(files: Array<{ name: string; text: string }>, entry: string): Promise<{ program?: Buffer; start?: number; diagnostics: Diagnostic[] }> {
152
+ const contents = files.map((file) => Buffer.from(file.text, "utf8"));
153
+ const { result, attachments } = await assemble(
154
+ { files: files.map((file, index) => ({ path: file.name, size: contents[index]!.length })), entrySource: entry, includeDirs: [], defines: {} },
155
+ contents,
156
+ localToolContext(),
157
+ );
158
+ if (!result.assembled) return { diagnostics: result.diagnostics };
159
+ return { program: attachments![0]!, start: result.loadRange!.start, diagnostics: result.diagnostics };
160
+ }
161
+
162
+ /**
163
+ * Assembles one instruction of each opcode form that the source uses, each
164
+ * in its own slot. A form that ACME refuses, or writes with other bytes, is
165
+ * written as bytes in the source.
166
+ */
167
+ async function probe(laid: Layout): Promise<Set<string>> {
168
+ const asBytes = new Set<string>();
169
+ for (let round = 0; round < 3; round++) {
170
+ const forms = probeForms(laid).filter((item) => !asBytes.has(item.form.key));
171
+ if (forms.length === 0) return asBytes;
172
+ const lines = ["!cpu 6510"];
173
+ const lineOf = new Map<number, string>();
174
+ forms.forEach((item, index) => {
175
+ const slot = PROBE_BASE + PROBE_SLOT * index;
176
+ const { mode } = OPCODES[item.form.opcode]!;
177
+ const operand =
178
+ mode === "imp"
179
+ ? ""
180
+ : mode === "rel"
181
+ ? formatC64Address((slot + 2 + (item.bytes[1]! < 0x80 ? item.bytes[1]! : item.bytes[1]! - 0x100)) & 0xffff)
182
+ : literalOperand(item.form.opcode, item.bytes.length === 3 ? item.bytes[1]! | (item.bytes[2]! << 8) : item.bytes[1]!);
183
+ lines.push(`* = ${formatC64Address(slot)}`);
184
+ lines.push(` ${instructionText(item.form.opcode, item.form.suffix, operand)}`);
185
+ lineOf.set(lines.length, item.form.key);
186
+ });
187
+ const built = await assembleFiles([{ name: "probe.asm", text: `${lines.join("\n")}\n` }], "probe.asm");
188
+ if (built.program === undefined) {
189
+ const refused = built.diagnostics.filter((diagnostic) => diagnostic.severity === "error" && diagnostic.line !== undefined && lineOf.has(diagnostic.line));
190
+ if (refused.length === 0) throw new WireFailure("operation-failed", `ACME refused the opcode probe: ${built.diagnostics.map((diagnostic) => diagnostic.message).join("; ")}`);
191
+ for (const diagnostic of refused) asBytes.add(lineOf.get(diagnostic.line!)!);
192
+ continue;
193
+ }
194
+ forms.forEach((item, index) => {
195
+ const offset = 2 + PROBE_BASE + PROBE_SLOT * index - built.start!;
196
+ const same = item.bytes.every((byte, at) => built.program![offset + at] === byte);
197
+ if (!same) asBytes.add(item.form.key);
198
+ });
199
+ return asBytes;
200
+ }
201
+ throw new WireFailure("operation-failed", "ACME refused the opcode probe three times.");
202
+ }
203
+
204
+ interface SegmentCheck {
205
+ start: string;
206
+ end: string;
207
+ result: "identical" | "different" | "not covered";
208
+ differences?: number;
209
+ firstDifference?: { address: string; built: number; image: number };
210
+ }
211
+
212
+ /** Compares the assembled program with the image, segment by segment. */
213
+ function check(program: Buffer, start: number, image: Image): { identical: boolean; segments: SegmentCheck[] } {
214
+ const end = start + program.length - 3;
215
+ const segments = image.segments.map((segment): SegmentCheck => {
216
+ const shown = { start: formatC64Address(segment.start), end: formatC64Address(segment.end) };
217
+ if (segment.start < start || segment.end > end) return { ...shown, result: "not covered" };
218
+ let differences = 0;
219
+ let first: SegmentCheck["firstDifference"];
220
+ for (let address = segment.start; address <= segment.end; address++) {
221
+ const built = program[2 + address - start]!;
222
+ if (built === image.byteAt(address)) continue;
223
+ differences++;
224
+ first ??= { address: formatC64Address(address), built, image: image.byteAt(address) };
225
+ }
226
+ return differences === 0 ? { ...shown, result: "identical" } : { ...shown, result: "different", differences, firstDifference: first! };
227
+ });
228
+ return { identical: segments.every((segment) => segment.result === "identical"), segments };
229
+ }
230
+
231
+ /** Reads RAM in parts. */
232
+ async function readRam(client: ViceSessionClient, start: number, size: number): Promise<Buffer> {
233
+ const parts: Buffer[] = [];
234
+ for (let at = start; at < start + size; at += MAX_MEMORY_READ) {
235
+ parts.push(Buffer.from((await client.memoryRead({ address: at, size: Math.min(MAX_MEMORY_READ, start + size - at), space: "c64", view: "ram" })).data, "hex"));
236
+ }
237
+ return Buffer.concat(parts);
238
+ }
239
+
240
+ /** Runs the loader in the emulator to the entry and compares memory, $00/$01 and the registers with the entry state. */
241
+ async function runLoader(path: string, program: Buffer, plan: LoaderPlan, image: Image, state: EntryState): Promise<{ verified: boolean; differences: string[] }> {
242
+ const client = await ViceSessionClient.open({ videoStandard: "pal" });
243
+ try {
244
+ await client.reset({ mode: "hard", run: false });
245
+ if (!(await autostartLoaded(client, path, program))) return { verified: false, differences: ["The loader was not in memory after autostart."] };
246
+ await client.keyboard(textToPetscii("RUN\n"));
247
+ const copier = await client.runUntil({ target: { kind: "address", address: plan.copier, space: "c64" }, timeoutFrames: RUN_LIMIT_FRAMES });
248
+ if (!copier.reached) return { verified: false, differences: [`The loader did not get to its copier at ${formatC64Address(plan.copier)}; it stopped at ${formatC64Address(copier.pc)}.`] };
249
+ const entry = await client.runUntil({ target: { kind: "address", address: state.pc, space: "c64" }, timeoutFrames: LOADER_FRAMES });
250
+ if (!entry.reached) return { verified: false, differences: [`The loader did not get to the entry ${formatC64Address(state.pc)}; it stopped at ${formatC64Address(entry.pc)}.`] };
251
+ const memory = await readRam(client, 0x0000, 0x10000);
252
+ const port = Buffer.from((await client.memoryRead({ address: 0x0000, size: 2, space: "c64", view: "cpu" })).data, "hex");
253
+ const registers = await client.registersGet("c64");
254
+ const differences: string[] = [];
255
+ const byte = (value: number) => `$${value.toString(16).padStart(2, "0")}`;
256
+ const differ = (what: string, got: number, wanted: number) => got !== wanted && differences.push(`${what} is ${byte(got)}, not ${byte(wanted)}`);
257
+ for (const segment of image.segments) {
258
+ for (let address = Math.max(segment.start, 2); address <= segment.end; address++) differ(formatC64Address(address), memory[address]!, image.byteAt(address));
259
+ }
260
+ for (let address = 2; address < 0x400; address++) if (address !== plan.stackByte) differ(formatC64Address(address), memory[address]!, state.pages[address]!);
261
+ differ("$00", port[0]!, state.port[0]);
262
+ differ("$01", port[1]!, state.port[1]);
263
+ differ("a", registers.a, state.a);
264
+ differ("x", registers.x, state.x);
265
+ differ("y", registers.y, state.y);
266
+ differ("sp", registers.sp, state.sp);
267
+ const flags = (["n", "v", "d", "i", "z", "c"] as const).reduce((value, name, index) => value | (registers.flags[name] ? [0x80, 0x40, 0x08, 0x04, 0x02, 0x01][index]! : 0), 0);
268
+ differ("flags", flags, state.flags & 0xcf);
269
+ return { verified: differences.length === 0, differences: differences.slice(0, MAX_LISTED) };
270
+ } finally {
271
+ await client.close();
272
+ }
273
+ }
274
+
275
+ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; failed: boolean }> {
276
+ const { values, positionals } = parseArgs({
277
+ args: argv,
278
+ strict: true,
279
+ allowPositionals: true,
280
+ options: { out: { type: "string" }, entry: { type: "string", multiple: true }, coverage: { type: "string", multiple: true }, space: { type: "string" }, loader: { type: "boolean" }, help: { type: "boolean" } },
281
+ });
282
+ if (values.help) throw new UsageError("");
283
+ if (positionals.length !== 1) throw new UsageError("give one image");
284
+ if (values.out === undefined) throw new UsageError("generate needs --out <dir>");
285
+ try {
286
+ resolveProjectPath(values.out);
287
+ } catch {
288
+ throw new UsageError(`--out must be a path relative to the project directory that stays inside it, not ${values.out}`);
289
+ }
290
+ const entries = (values.entry ?? []).map((value) => {
291
+ try {
292
+ return parseC64Address(value);
293
+ } catch {
294
+ throw new UsageError(`--entry must be $ followed by four hex digits, not ${value}`);
295
+ }
296
+ });
297
+ const space = values.space ?? DEFAULT_SPACE;
298
+ if (!SPACE.test(space)) throw new UsageError("--space must be c64, drive8, or bank0 to bank255");
299
+ if (space !== DEFAULT_SPACE && (values.coverage !== undefined || values.loader)) throw new UsageError("--coverage and --loader work only in space c64");
300
+ const image = readImage(positionals[0]!);
301
+ const coverage = readCoverageFiles(values.coverage ?? [], (message) => {
302
+ throw new UsageError(message);
303
+ });
304
+ if (values.loader && (image.statePath === undefined || image.entry === undefined)) throw new UsageError("--loader needs an image.json with an entry and an entry_state.json next to it, from c64-unpacker capture");
305
+ // Read the entry state before the long steps, so a missing file stops the run at once.
306
+ const state = values.loader ? readEntryState(readProjectFile(image.statePath!).bytes.toString("utf8"), image.statePath!) : undefined;
307
+ const knowledge = readKnowledge(space);
308
+ const instructions = await decode(image, knowledge, entries);
309
+ const laid = layout({
310
+ segments: image.segments,
311
+ byteAt: image.byteAt,
312
+ ...(image.entry === undefined ? {} : { entry: image.entry }),
313
+ instructions,
314
+ symbols: knowledge.symbols,
315
+ regions: knowledge.regions,
316
+ comments: knowledge.comments,
317
+ modules: knowledge.modules,
318
+ referenceTargets: knowledge.references.map((reference) => reference.to),
319
+ ...(coverage === undefined ? {} : { coverage }),
320
+ // The platform names are C64 names: drive code and banks get none.
321
+ ...(space === DEFAULT_SPACE ? { platform: new Map(platformNames().map((item) => [item.address, item.name])) } : {}),
322
+ });
323
+ const asBytes = await probe(laid);
324
+ const header = [`Generated by c64-source from ${image.path}${space === DEFAULT_SPACE ? "" : ` in space ${space}`} and knowledge revision ${knowledge.revision}.`, "Change names, comments, regions and modules in knowledge, then generate again."];
325
+ const rendered = render(laid, asBytes, header);
326
+ const files = [...rendered.files];
327
+ let loader: LoaderPlan | undefined;
328
+ if (state !== undefined) {
329
+ const segments = image.segments.map((segment) => ({ ...segment, files: laid.pieces.filter((piece) => piece.start >= segment.start && piece.start <= segment.end).map((piece) => piece.file) }));
330
+ loader = planLoader({ segments, byteAt: image.byteAt, state, entry: image.entry!, header: [`Loader generated by c64-source from ${image.path}.`, "It copies the segments into place, sets the entry state and jumps to the entry."] });
331
+ files.push({ name: "loader.asm", text: loader.text });
332
+ }
333
+ for (const file of files) writeProjectFile(`${values.out}/${file.name}`, Buffer.from(file.text, "utf8"), { what: "--out" });
334
+
335
+ const output: Record<string, unknown> = {
336
+ out: values.out,
337
+ files: files.map((file) => file.name),
338
+ revision: knowledge.revision,
339
+ instructions: rendered.instructions,
340
+ dataBytes: rendered.dataBytes,
341
+ ...(rendered.undecodedBytes === 0 ? {} : { undecodedBytes: rendered.undecodedBytes }),
342
+ labels: { named: laid.namedLabels, generated: laid.generatedLabels },
343
+ ...(rendered.asBytes.length === 0
344
+ ? {}
345
+ : { asBytes: { count: rendered.asBytes.length, listed: rendered.asBytes.slice(0, MAX_LISTED).map((item) => ({ address: formatC64Address(item.address), instruction: item.text })) } }),
346
+ ...(rendered.notExecuted === undefined
347
+ ? {}
348
+ : {
349
+ notExecuted: {
350
+ instructions: rendered.notExecuted.instructions,
351
+ ranges: rendered.notExecuted.ranges.length,
352
+ listed: rendered.notExecuted.ranges.slice(0, MAX_LISTED).map((range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) })),
353
+ },
354
+ }),
355
+ ...(laid.unusedModules.length === 0 ? {} : { unusedModules: laid.unusedModules.map((module) => ({ start: formatC64Address(module.start), file: module.file })) }),
356
+ };
357
+ const built = await assembleFiles(files, "main.asm");
358
+ if (built.program === undefined) {
359
+ return { output: { ...output, check: { identical: false, diagnostics: built.diagnostics.slice(0, MAX_LISTED) } }, failed: true };
360
+ }
361
+ const checked = check(built.program, built.start!, image);
362
+ output.check = checked;
363
+ if (!checked.identical || loader === undefined || state === undefined) return { output, failed: !checked.identical };
364
+
365
+ const loaderBuilt = await assembleFiles(files, "loader.asm");
366
+ if (loaderBuilt.program === undefined) return { output: { ...output, loader: { verified: false, diagnostics: loaderBuilt.diagnostics.slice(0, MAX_LISTED) } }, failed: true };
367
+ const program = loaderBuilt.program;
368
+ // The simulation: the loader in memory as it loads, then every copy in its order, byte by byte.
369
+ const memory = new Int16Array(0x10000).fill(-1);
370
+ program.subarray(2).forEach((byte, offset) => (memory[loaderBuilt.start! + offset] = byte));
371
+ const expected = (address: number) => (image.segments.some((segment) => address >= segment.start && address <= segment.end) ? image.byteAt(address) : state.pages[address]!);
372
+ const wrongPacking = loader.blocks.find((block) => {
373
+ for (let address = block.range.start; address <= block.range.end; address++) if (memory[block.packedAt + address - block.range.start] !== block.bytes(address)) return true;
374
+ return false;
375
+ });
376
+ if (loaderBuilt.start !== 0x0801 || loaderBuilt.start + program.length - 3 !== loader.end || wrongPacking !== undefined) {
377
+ throw new WireFailure("operation-failed", `The assembled loader does not have the planned layout${wrongPacking === undefined ? "" : ` (${wrongPacking.what})`}. This is a defect of the c64-source script.`);
378
+ }
379
+ const wrong = simulate(memory, loader.copies, expected);
380
+ if (wrong !== undefined) throw new WireFailure("operation-failed", `The simulated loader writes a wrong byte at ${formatC64Address(wrong)}. This is a defect of the c64-source script.`);
381
+ const loaderPath = `${values.out}/loader.prg`;
382
+ writeProjectFile(loaderPath, program, { what: "--out" });
383
+ (output.files as string[]).push("loader.prg");
384
+ const ran = await runLoader(loaderPath, program, loader, image, state);
385
+ output.loader = {
386
+ program: loaderPath,
387
+ copies: loader.copies.map((copy) => ({ what: copy.what, to: `${formatC64Address(copy.target.start)}-${formatC64Address(copy.target.end)}` })),
388
+ copier: formatC64Address(loader.copier),
389
+ stackByte: formatC64Address(loader.stackByte),
390
+ ...ran,
391
+ };
392
+ return { output, failed: !ran.verified };
393
+ }
394
+
395
+ await runScript(
396
+ async () => {
397
+ const { output, failed } = await run(process.argv.slice(2));
398
+ if (failed) process.exitCode = 1;
399
+ return output;
400
+ },
401
+ { usage: USAGE, refusals: [KnowledgeError] },
402
+ );