@henols/c64-re-tools 1.1.0 → 2.0.0-rc.11

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 (162) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -37
  3. package/distribution/package.ts +21 -0
  4. package/distribution/plugin.ts +122 -0
  5. package/package.json +34 -40
  6. package/skills/c64-assembler/SKILL.md +76 -0
  7. package/skills/c64-assembler/scripts/assemble.ts +110 -0
  8. package/skills/c64-assembler/scripts/compare.ts +84 -0
  9. package/skills/c64-basic/SKILL.md +66 -0
  10. package/skills/c64-basic/scripts/basic.ts +46 -0
  11. package/skills/c64-disk/SKILL.md +93 -0
  12. package/skills/c64-disk/scripts/containers.ts +124 -0
  13. package/skills/c64-disk/scripts/disk.ts +120 -0
  14. package/skills/c64-emulator/SKILL.md +134 -0
  15. package/skills/c64-knowledge/SKILL.md +192 -0
  16. package/skills/c64-knowledge/scripts/knowledge.ts +613 -0
  17. package/skills/c64-memory-map/SKILL.md +79 -0
  18. package/skills/c64-memory-map/scripts/memmap.ts +92 -0
  19. package/skills/c64-provenance/SKILL.md +110 -0
  20. package/skills/c64-provenance/scripts/compare.ts +152 -0
  21. package/skills/c64-provenance/scripts/provenance.ts +334 -0
  22. package/skills/c64-provenance/scripts/runtime.ts +191 -0
  23. package/skills/c64-provenance/scripts/separate.ts +140 -0
  24. package/skills/c64-reverse-engineering/SKILL.md +144 -0
  25. package/skills/c64-source/SKILL.md +133 -0
  26. package/skills/c64-source/scripts/generate.ts +503 -0
  27. package/skills/c64-source/scripts/loader.ts +589 -0
  28. package/skills/c64-source/scripts/opcodes.ts +61 -0
  29. package/skills/c64-source/scripts/source.ts +673 -0
  30. package/skills/c64-static-analysis/SKILL.md +120 -0
  31. package/skills/c64-static-analysis/scripts/analyze.ts +293 -0
  32. package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
  33. package/skills/c64-static-analysis/scripts/findings.ts +75 -0
  34. package/skills/c64-static-analysis/scripts/idioms.ts +149 -0
  35. package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
  36. package/skills/c64-static-analysis/scripts/pointers.ts +108 -0
  37. package/skills/c64-testing/SKILL.md +103 -0
  38. package/skills/c64-testing/references/scenario-format.md +116 -0
  39. package/skills/c64-testing/scripts/checklist.ts +51 -0
  40. package/skills/c64-testing/scripts/run.ts +189 -0
  41. package/skills/c64-testing/scripts/scenario.ts +493 -0
  42. package/skills/c64-testing/scripts/test.ts +65 -0
  43. package/skills/c64-unpacker/SKILL.md +135 -0
  44. package/skills/c64-unpacker/scripts/evidence.ts +207 -0
  45. package/skills/c64-unpacker/scripts/loads.ts +209 -0
  46. package/skills/c64-unpacker/scripts/machine.ts +80 -0
  47. package/skills/c64-unpacker/scripts/stages.ts +130 -0
  48. package/skills/c64-unpacker/scripts/unpack.ts +683 -0
  49. package/src/c64.ts +411 -0
  50. package/src/cli/ap-sdk.ts +44 -0
  51. package/src/cli/cleanup.ts +194 -0
  52. package/src/cli/main.ts +125 -0
  53. package/src/cli/status.ts +61 -0
  54. package/src/host/main.ts +134 -0
  55. package/src/host/server.ts +392 -0
  56. package/src/host/tools/c1541.ts +284 -0
  57. package/src/host/tools/index.ts +35 -0
  58. package/src/host/tools/petcat.ts +77 -0
  59. package/src/host/tools/status.ts +47 -0
  60. package/src/host/tools/version.ts +41 -0
  61. package/src/host/vice/adapter.ts +804 -0
  62. package/src/host/vice/binary-monitor.ts +389 -0
  63. package/src/host/vice/chips.ts +118 -0
  64. package/src/host/vice/connect.ts +34 -0
  65. package/src/host/vice/process.ts +413 -0
  66. package/src/host/vice/screen.ts +184 -0
  67. package/src/host/vice/session.ts +1426 -0
  68. package/src/host/vice/text-monitor.ts +223 -0
  69. package/src/host-client/autostart.ts +55 -0
  70. package/src/host-client/connect.ts +241 -0
  71. package/src/host-client/coverage.ts +63 -0
  72. package/src/host-client/image.ts +63 -0
  73. package/src/host-client/steps.ts +170 -0
  74. package/src/host-client/tools.ts +89 -0
  75. package/src/host-client/transfer.ts +126 -0
  76. package/src/host-client/vice-session.ts +316 -0
  77. package/src/knowledge/database.ts +132 -0
  78. package/src/knowledge/expression.ts +209 -0
  79. package/src/knowledge/history.ts +169 -0
  80. package/src/knowledge/import.ts +392 -0
  81. package/src/knowledge/read.ts +392 -0
  82. package/src/knowledge/schema.ts +250 -0
  83. package/src/knowledge/write.ts +837 -0
  84. package/src/mcp/main.ts +134 -0
  85. package/src/mcp/output.ts +32 -0
  86. package/src/mcp/server.ts +316 -0
  87. package/src/mcp/tools/debug.ts +327 -0
  88. package/src/mcp/tools/execution.ts +117 -0
  89. package/src/mcp/tools/input.ts +72 -0
  90. package/src/mcp/tools/machine.ts +100 -0
  91. package/src/mcp/tools/media.ts +129 -0
  92. package/src/mcp/tools/memory.ts +250 -0
  93. package/src/mcp/tools/video.ts +271 -0
  94. package/src/native/acme.ts +116 -0
  95. package/src/native/discover.ts +179 -0
  96. package/src/native/dxa.ts +166 -0
  97. package/src/native/ghidra/analyze.ts +86 -0
  98. package/src/native/ghidra/index.ts +173 -0
  99. package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
  100. package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
  101. package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
  102. package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
  103. package/src/native/ghidra/scripts/C64Export.java +274 -0
  104. package/src/native/ghidra/scripts/C64Prepare.java +60 -0
  105. package/src/native/local.ts +34 -0
  106. package/src/native/processes.ts +350 -0
  107. package/src/native/run.ts +172 -0
  108. package/src/native/staging.ts +83 -0
  109. package/src/native/status.ts +113 -0
  110. package/src/native/types.ts +257 -0
  111. package/src/native/watchdog.ts +78 -0
  112. package/src/package.ts +21 -0
  113. package/src/platform.ts +441 -0
  114. package/src/project.ts +39 -0
  115. package/src/protocol/framing.ts +132 -0
  116. package/src/protocol/messages.ts +259 -0
  117. package/src/protocol/tools.ts +266 -0
  118. package/src/protocol/vice.ts +1348 -0
  119. package/src/script.ts +60 -0
  120. package/src/time.ts +15 -0
  121. package/src/trace.ts +194 -0
  122. package/THIRD-PARTY-NOTICES.md +0 -26
  123. package/bin/cli.mjs +0 -283
  124. package/skills/acme-build/SKILL.md +0 -260
  125. package/skills/acme-build/scripts/acme.mjs +0 -341
  126. package/skills/acme-build/template.a +0 -53
  127. package/skills/c64-disk-access/SKILL.md +0 -159
  128. package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
  129. package/skills/c64-memory-mapping/SKILL.md +0 -631
  130. package/skills/c64-memory-mapping/memmap.json +0 -8800
  131. package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
  132. package/skills/c64-petcat/SKILL.md +0 -87
  133. package/skills/c64-petcat/scripts/petcat.mjs +0 -221
  134. package/skills/c64-program-recon/SKILL.md +0 -706
  135. package/skills/c64-program-recon/references/control-flow.md +0 -180
  136. package/skills/c64-program-recon/references/graphics.md +0 -79
  137. package/skills/c64-program-recon/references/observation-hazards.md +0 -143
  138. package/skills/c64-program-recon/references/reconstruction.md +0 -133
  139. package/skills/c64-program-recon/references/sound-and-input.md +0 -74
  140. package/skills/c64-program-recon/references/tool-selection.md +0 -78
  141. package/skills/c64-program-recon/scripts/derive.mjs +0 -364
  142. package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
  143. package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
  144. package/skills/c64-provenance-diff/SKILL.md +0 -292
  145. package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
  146. package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
  147. package/skills/c64-ram-capture/RELEASES.json.example +0 -17
  148. package/skills/c64-ram-capture/SKILL.md +0 -474
  149. package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
  150. package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
  151. package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
  152. package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
  153. package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
  154. package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
  155. package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
  156. package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
  157. package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
  158. package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
  159. package/skills/c64-ram-capture/transients/README.md +0 -137
  160. package/skills/routine-queue-walker/SKILL.md +0 -394
  161. package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
  162. package/skills/vice-wedge-triage/SKILL.md +0 -269
@@ -0,0 +1,503 @@
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 { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
9
+ import { join, posix } from "node:path";
10
+ import { parseArgs } from "node:util";
11
+
12
+ import { formatC64Address, parseC64Address, prgRange, textToPetscii } from "#src/c64.ts";
13
+ import { autostartLoaded, RUN_LIMIT_FRAMES } from "#src/host-client/autostart.ts";
14
+ import { readCoverageFiles } from "#src/host-client/coverage.ts";
15
+ import { ImageError, readImage as readSharedImage, type Image } from "#src/host-client/image.ts";
16
+ import { MAX_MEMORY_READ, WireFailure } from "#src/host-client/tools.ts";
17
+ import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
18
+ import { ViceSessionClient } from "#src/host-client/vice-session.ts";
19
+ import { KnowledgeError, openForRead } from "#src/knowledge/database.ts";
20
+ import { currentRevision, listComments, listModules, listOperands, listReferences, listRegions, listSymbols } from "#src/knowledge/read.ts";
21
+ import { CODE_REGION_TYPES, DEFAULT_SPACE, SPACE, type Space } from "#src/knowledge/schema.ts";
22
+ import { assemble } from "#src/native/acme.ts";
23
+ import { analyze } from "#src/native/ghidra/analyze.ts";
24
+ import { localToolContext } from "#src/native/local.ts";
25
+ import { GHIDRA_LIMITS, type AssembledSymbol, type Diagnostic } from "#src/native/types.ts";
26
+ import { platformNames } from "#src/platform.ts";
27
+ import { resolveProjectPath } from "#src/project.ts";
28
+ import { runScript, UsageError } from "#src/script.ts";
29
+ import { LOADER_JUMP, planLoader, rasterLine, readEntryState, simulate, VIC_DISPLAY_REGISTERS, type EntryState, type LoaderPlan } from "./loader.ts";
30
+ import { OPCODES } from "./opcodes.ts";
31
+ import { entriesInData, entryOutsideImage, instructionText, layout, literalOperand, probeForms, render, type Layout, type Range, type Rendered } from "./source.ts";
32
+
33
+ const USAGE = `generate.ts <image> --out <dir> [options]
34
+
35
+ <image> an image.json of the c64-unpacker skill, or a PRG, relative to
36
+ the project directory
37
+ --out <dir> the directory for the source files, relative to the project
38
+ --entry <address> an entry point for Ghidra, for example '$080d' (repeatable)
39
+ --coverage <file> a coverage map of 64 KiB from c64_memmap (repeatable): code
40
+ that no run executed is marked and listed; <dir>/not_executed.json
41
+ has all of its ranges
42
+ --space <space> the knowledge space of the image: c64 (the default), drive8, or
43
+ bank0 to bank255; --coverage and --loader are for c64 only
44
+ --loader also write loader.asm and loader.prg: the loader copies the
45
+ segments into place, sets the entry state from entry_state.json
46
+ next to image.json, and jumps to the entry; it is checked in
47
+ the emulator
48
+
49
+ The result is one JSON object. Addresses are $ and four hex digits.`;
50
+
51
+ const MAX_LISTED = 20;
52
+ /** Rounds of assembly that move operand expressions to numbers (D65). */
53
+ const OPERAND_ROUNDS = 3;
54
+ const PROBE_BASE = 0x1000;
55
+ const PROBE_SLOT = 4;
56
+ /** The copies of a full 64 KiB take about 140 frames. */
57
+ const LOADER_FRAMES = 1500;
58
+
59
+ function readImage(path: string): Image {
60
+ try {
61
+ return readSharedImage(path);
62
+ } catch (error) {
63
+ if (error instanceof ImageError) throw new UsageError(error.message);
64
+ throw error;
65
+ }
66
+ }
67
+
68
+ /** The knowledge of one space that the source uses, read once. */
69
+ function readKnowledge(space: Space) {
70
+ const db = openForRead();
71
+ try {
72
+ return {
73
+ revision: currentRevision(db),
74
+ symbols: listSymbols(db, undefined, undefined, undefined, space),
75
+ regions: listRegions(db, undefined, undefined, undefined, space),
76
+ comments: listComments(db, undefined, undefined, space),
77
+ modules: listModules(db, undefined, undefined, space),
78
+ references: listReferences(db, undefined, undefined, undefined, space),
79
+ operands: listOperands(db, undefined, undefined, space),
80
+ };
81
+ } finally {
82
+ db?.close();
83
+ }
84
+ }
85
+
86
+ /** Ghidra's instruction starts and lengths inside the image, seeded from the entry and from knowledge of any origin. */
87
+ async function decode(image: Image, knowledge: ReturnType<typeof readKnowledge>, entries: number[]): Promise<Map<number, number>> {
88
+ const inImage = (address: number) => image.segments.some((segment) => address >= segment.start && address <= segment.end);
89
+ const seeds = new Set<number>([...(image.entry === undefined ? [] : [image.entry]), ...entries]);
90
+ for (const symbol of knowledge.symbols) if (symbol.kind === "routine") seeds.add(symbol.address);
91
+ for (const region of knowledge.regions) {
92
+ if (CODE_REGION_TYPES.includes(region.type)) seeds.add(region.start);
93
+ if (region.type === "jumps") for (let at = region.start; at + 2 <= region.end; at += 3) seeds.add(at);
94
+ }
95
+ const entryPoints = [...seeds].filter(inImage).sort((a, b) => a - b);
96
+ if (entryPoints.length === 0) {
97
+ // An entry in no segment is a capture fault: the image is data only, and the result says entryOutside (R3-01).
98
+ if (image.entry !== undefined && !inImage(image.entry)) return new Map();
99
+ 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");
100
+ }
101
+ const dataRanges = knowledge.regions
102
+ .filter((region) => !CODE_REGION_TYPES.includes(region.type) && region.type !== "dual-use")
103
+ .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) })));
104
+ const { result } = await analyze(
105
+ { imageKind: image.analyzed.kind, entryPoints: entryPoints.slice(0, GHIDRA_LIMITS.entryPoints), dataRanges: dataRanges.slice(0, GHIDRA_LIMITS.dataRanges), labels: [], decompile: [] },
106
+ image.analyzed.bytes,
107
+ localToolContext(),
108
+ );
109
+ const instructions = new Map<number, number>();
110
+ for (const instruction of result.instructions) {
111
+ const bytes = Buffer.from(instruction.bytes, "hex");
112
+ const inside = Array.from({ length: bytes.length }, (_, offset) => instruction.address + offset).every((address, offset) => inImage(address) && image.byteAt(address) === bytes[offset]);
113
+ if (inside) instructions.set(instruction.address, bytes.length);
114
+ }
115
+ return instructions;
116
+ }
117
+
118
+ /** Assembles files given as text with ACME; the program or the diagnostics. */
119
+ async function assembleFiles(files: Array<{ name: string; text: string }>, entry: string): Promise<{ program?: Buffer; start?: number; symbols?: AssembledSymbol[]; diagnostics: Diagnostic[] }> {
120
+ const contents = files.map((file) => Buffer.from(file.text, "utf8"));
121
+ const { result, attachments } = await assemble(
122
+ { files: files.map((file, index) => ({ path: file.name, size: contents[index]!.length })), entrySource: entry, includeDirs: [], defines: {} },
123
+ contents,
124
+ localToolContext(),
125
+ );
126
+ if (!result.assembled) return { diagnostics: result.diagnostics };
127
+ return { program: attachments![0]!, start: result.loadRange!.start, ...(result.symbols === undefined ? {} : { symbols: result.symbols }), diagnostics: result.diagnostics };
128
+ }
129
+
130
+ /**
131
+ * Assembles one instruction of each opcode form that the source uses, each
132
+ * in its own slot. A form that ACME refuses, or writes with other bytes, is
133
+ * written as bytes in the source.
134
+ */
135
+ async function probe(laid: Layout): Promise<Set<string>> {
136
+ const asBytes = new Set<string>();
137
+ for (let round = 0; round < 3; round++) {
138
+ const forms = probeForms(laid).filter((item) => !asBytes.has(item.form.key));
139
+ if (forms.length === 0) return asBytes;
140
+ const lines = ["!cpu 6510"];
141
+ const lineOf = new Map<number, string>();
142
+ forms.forEach((item, index) => {
143
+ const slot = PROBE_BASE + PROBE_SLOT * index;
144
+ const { mode } = OPCODES[item.form.opcode]!;
145
+ const operand =
146
+ mode === "imp"
147
+ ? ""
148
+ : mode === "rel"
149
+ ? formatC64Address((slot + 2 + (item.bytes[1]! < 0x80 ? item.bytes[1]! : item.bytes[1]! - 0x100)) & 0xffff)
150
+ : literalOperand(item.form.opcode, item.bytes.length === 3 ? item.bytes[1]! | (item.bytes[2]! << 8) : item.bytes[1]!);
151
+ lines.push(`* = ${formatC64Address(slot)}`);
152
+ lines.push(` ${instructionText(item.form.opcode, item.form.suffix, operand)}`);
153
+ lineOf.set(lines.length, item.form.key);
154
+ });
155
+ const built = await assembleFiles([{ name: "probe.asm", text: `${lines.join("\n")}\n` }], "probe.asm");
156
+ if (built.program === undefined) {
157
+ const refused = built.diagnostics.filter((diagnostic) => diagnostic.severity === "error" && diagnostic.line !== undefined && lineOf.has(diagnostic.line));
158
+ if (refused.length === 0) throw new WireFailure("operation-failed", `ACME refused the opcode probe: ${built.diagnostics.map((diagnostic) => diagnostic.message).join("; ")}`);
159
+ for (const diagnostic of refused) asBytes.add(lineOf.get(diagnostic.line!)!);
160
+ continue;
161
+ }
162
+ forms.forEach((item, index) => {
163
+ const offset = 2 + PROBE_BASE + PROBE_SLOT * index - built.start!;
164
+ const same = item.bytes.every((byte, at) => built.program![offset + at] === byte);
165
+ if (!same) asBytes.add(item.form.key);
166
+ });
167
+ return asBytes;
168
+ }
169
+ throw new WireFailure("operation-failed", "ACME refused the opcode probe three times.");
170
+ }
171
+
172
+ /**
173
+ * The operand expressions that made the assembly fail or that ACME assembled
174
+ * to other bytes than the image, with the reason (D65). `markLines` is the
175
+ * number of lines added before the rendered text of each file.
176
+ */
177
+ function blame(rendered: Rendered, built: Awaited<ReturnType<typeof assembleFiles>>, image: Image, markLines: number): Map<number, string> {
178
+ const blamed = new Map<number, string>();
179
+ if (built.program === undefined) {
180
+ const lines = new Map(rendered.expressions.map((item) => [`${item.file}:${item.line + markLines}`, item.address]));
181
+ for (const diagnostic of built.diagnostics) {
182
+ if (diagnostic.severity !== "error" || diagnostic.file === undefined || diagnostic.line === undefined) continue;
183
+ const address = lines.get(`${diagnostic.file}:${diagnostic.line}`);
184
+ if (address !== undefined) blamed.set(address, `ACME refuses it: ${diagnostic.message}`);
185
+ }
186
+ return blamed;
187
+ }
188
+ const end = built.start! + built.program.length - 3;
189
+ const word = (at: (address: number) => number, item: Rendered["expressions"][number]) => (item.length === 3 ? at(item.address + 1) | (at(item.address + 2) << 8) : at(item.address + 1));
190
+ const show = (value: number, length: number) => `$${value.toString(16).padStart(length === 3 ? 4 : 2, "0")}`;
191
+ for (const item of rendered.expressions) {
192
+ if (item.address + item.length - 1 > end || item.address < built.start!) continue;
193
+ const assembled = word((address) => built.program![2 + address - built.start!]!, item);
194
+ const original = word(image.byteAt, item);
195
+ if (assembled !== original) blamed.set(item.address, `ACME assembles it to ${show(assembled, item.length)}, and the image has ${show(original, item.length)}.`);
196
+ }
197
+ return blamed;
198
+ }
199
+
200
+ interface SegmentCheck {
201
+ start: string;
202
+ end: string;
203
+ result: "identical" | "different" | "not covered";
204
+ differences?: number;
205
+ firstDifference?: { address: string; built: number; image: number };
206
+ }
207
+
208
+ /** Compares the assembled program with the image, segment by segment. */
209
+ function check(program: Buffer, start: number, image: Image): { identical: boolean; segments: SegmentCheck[] } {
210
+ const end = start + program.length - 3;
211
+ const segments = image.segments.map((segment): SegmentCheck => {
212
+ const shown = { start: formatC64Address(segment.start), end: formatC64Address(segment.end) };
213
+ if (segment.start < start || segment.end > end) return { ...shown, result: "not covered" };
214
+ let differences = 0;
215
+ let first: SegmentCheck["firstDifference"];
216
+ for (let address = segment.start; address <= segment.end; address++) {
217
+ const built = program[2 + address - start]!;
218
+ if (built === image.byteAt(address)) continue;
219
+ differences++;
220
+ first ??= { address: formatC64Address(address), built, image: image.byteAt(address) };
221
+ }
222
+ return differences === 0 ? { ...shown, result: "identical" } : { ...shown, result: "different", differences, firstDifference: first! };
223
+ });
224
+ return { identical: segments.every((segment) => segment.result === "identical"), segments };
225
+ }
226
+
227
+ /** Reads RAM in parts. */
228
+ async function readRam(client: ViceSessionClient, start: number, size: number): Promise<Buffer> {
229
+ const parts: Buffer[] = [];
230
+ for (let at = start; at < start + size; at += MAX_MEMORY_READ) {
231
+ parts.push(Buffer.from((await client.memoryRead({ address: at, size: Math.min(MAX_MEMORY_READ, start + size - at), space: "c64", view: "ram" })).data, "hex"));
232
+ }
233
+ return Buffer.concat(parts);
234
+ }
235
+
236
+ /** Runs the loader in the emulator to the entry and compares memory, $00/$01 and the registers with the entry state. */
237
+ /**
238
+ * Runs the loader in the emulator to its jump to the entry and compares the
239
+ * machine with the entry state there. At the jump, every byte and register is
240
+ * set and no interrupt has run since the PLP: the 6502 takes one only after
241
+ * the JMP, as the original program can take one right at its entry (F-32).
242
+ */
243
+ async function runLoader(path: string, program: Buffer, plan: LoaderPlan, image: Image, state: EntryState, jump: number): Promise<{ verified: boolean; differences: string[] }> {
244
+ const client = await ViceSessionClient.open({ videoStandard: "pal" });
245
+ try {
246
+ await client.reset({ mode: "hard", run: false });
247
+ if (!(await autostartLoaded(client, path, program))) return { verified: false, differences: ["The loader was not in memory after autostart."] };
248
+ await client.keyboard(textToPetscii("RUN\n"));
249
+ const copier = await client.runUntil({ target: { kind: "address", address: plan.copier, space: "c64" }, timeoutFrames: RUN_LIMIT_FRAMES });
250
+ 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)}.`] };
251
+ const atJump = await client.runUntil({ target: { kind: "address", address: jump, space: "c64" }, timeoutFrames: LOADER_FRAMES });
252
+ if (!atJump.reached) return { verified: false, differences: [`The loader did not get to its jump to the entry at ${formatC64Address(jump)}; it stopped at ${formatC64Address(atJump.pc)}.`] };
253
+ const memory = await readRam(client, 0x0000, 0x10000);
254
+ if (memory[jump] !== 0x4c || (memory[jump + 1]! | (memory[jump + 2]! << 8)) !== state.pc) {
255
+ return { verified: false, differences: [`The instruction at ${formatC64Address(jump)} is not JMP ${formatC64Address(state.pc)}.`] };
256
+ }
257
+ const port = Buffer.from((await client.memoryRead({ address: 0x0000, size: 2, space: "c64", view: "cpu" })).data, "hex");
258
+ const registers = await client.registersGet("c64");
259
+ const differences: string[] = [];
260
+ const byte = (value: number) => `$${value.toString(16).padStart(2, "0")}`;
261
+ const differ = (what: string, got: number, wanted: number) => got !== wanted && differences.push(`${what} is ${byte(got)}, not ${byte(wanted)}`);
262
+ for (const segment of image.segments) {
263
+ for (let address = Math.max(segment.start, 2); address <= segment.end; address++) differ(formatC64Address(address), memory[address]!, image.byteAt(address));
264
+ }
265
+ for (let address = 2; address < 0x400; address++) if (address !== plan.stackByte) differ(formatC64Address(address), memory[address]!, state.pages[address]!);
266
+ differ("$00", port[0]!, state.port[0]);
267
+ differ("$01", port[1]!, state.port[1]);
268
+ differ("a", registers.a, state.a);
269
+ differ("x", registers.x, state.x);
270
+ differ("y", registers.y, state.y);
271
+ differ("sp", registers.sp, state.sp);
272
+ 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);
273
+ differ("flags", flags, state.flags & 0xcf);
274
+ if (state.vic !== undefined) {
275
+ // The display registers and the raster line (D60): the loader waited for the line, so the jump comes in it or the next one.
276
+ const vic = Buffer.from((await client.ioRead(0xd000, 0x2f)).data, "hex");
277
+ for (const { register, mask } of VIC_DISPLAY_REGISTERS) differ(formatC64Address(0xd000 + register), vic[register]! & mask, state.vic.registers[register]! & mask);
278
+ differ("the VIC bank", Buffer.from((await client.ioRead(0xdd00, 1)).data, "hex")[0]! & 0x03, state.vic.bank);
279
+ const line = rasterLine(vic);
280
+ const wanted = rasterLine(state.vic.registers);
281
+ if (line !== wanted && line !== wanted + 1) differences.push(`the raster line is ${line}, not ${wanted}`);
282
+ }
283
+ if (plan.cia !== undefined) {
284
+ // The CIA timers (D63): at the jump a running timer is 3 cycles, the JMP, before its value at the entry.
285
+ const chips = (await client.cia("both")).chips;
286
+ const word = (value: number) => `$${value.toString(16).padStart(4, "0")}`;
287
+ const differWord = (what: string, got: number, wanted: number) => got !== wanted && differences.push(`${what} is ${word(got)}, not ${word(wanted)}`);
288
+ for (const timer of plan.cia.timers) {
289
+ const chip = chips[timer.cia - 1]!;
290
+ const name = `CIA ${timer.cia} timer ${timer.timer}`;
291
+ const [counter, latch, control] = timer.timer === "A" ? [chip.timerA, chip.latchA, chip.controlA] : [chip.timerB, chip.latchB, chip.controlB];
292
+ differ(`${name} control`, control, timer.control);
293
+ differWord(`${name} latch`, latch, timer.latch);
294
+ if (timer.counterAtJump !== undefined) differWord(`${name} counter`, counter, timer.counterAtJump);
295
+ }
296
+ plan.cia.masks.forEach((mask, index) => differ(`CIA ${index + 1} interrupt mask`, chips[index]!.interruptMask, mask));
297
+ }
298
+ return { verified: differences.length === 0, differences: differences.slice(0, MAX_LISTED) };
299
+ } finally {
300
+ await client.close();
301
+ }
302
+ }
303
+
304
+ /** The first words of every file that the script writes. */
305
+ const GENERATED_MARK = "; Generated by c64-source";
306
+ /** The file with every range of instructions that no run executed (D49). */
307
+ const NOT_EXECUTED_FILE = "not_executed.json";
308
+
309
+ /** Removes the .asm files of an earlier run that this run does not write, and an earlier loader.prg and not_executed.json; other files stay. */
310
+ function removeOldFiles(out: string, keep: ReadonlySet<string>): void {
311
+ const directory = resolveProjectPath(out);
312
+ if (!existsSync(directory)) return;
313
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
314
+ if (!entry.isFile()) continue;
315
+ const name = entry.name;
316
+ const path = join(directory, name);
317
+ if (name === "loader.prg" || name === NOT_EXECUTED_FILE) rmSync(path, { force: true });
318
+ else if (name.endsWith(".asm") && !keep.has(name) && readFileSync(path, "utf8").startsWith(GENERATED_MARK)) rmSync(path, { force: true });
319
+ }
320
+ }
321
+
322
+ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; failed: boolean }> {
323
+ const { values, positionals } = parseArgs({
324
+ args: argv,
325
+ strict: true,
326
+ allowPositionals: true,
327
+ options: { out: { type: "string" }, entry: { type: "string", multiple: true }, coverage: { type: "string", multiple: true }, space: { type: "string" }, loader: { type: "boolean" }, help: { type: "boolean" } },
328
+ });
329
+ if (values.help) throw new UsageError("");
330
+ if (positionals.length !== 1) throw new UsageError("give one image");
331
+ if (values.out === undefined) throw new UsageError("generate needs --out <dir>");
332
+ try {
333
+ resolveProjectPath(values.out);
334
+ } catch {
335
+ throw new UsageError(`--out must be a path relative to the project directory that stays inside it, not ${values.out}`);
336
+ }
337
+ const entries = (values.entry ?? []).map((value) => {
338
+ try {
339
+ return parseC64Address(value);
340
+ } catch {
341
+ throw new UsageError(`--entry must be $ followed by four hex digits, not ${value}`);
342
+ }
343
+ });
344
+ const space = values.space ?? DEFAULT_SPACE;
345
+ if (!SPACE.test(space)) throw new UsageError("--space must be c64, drive8, or bank0 to bank255");
346
+ if (space !== DEFAULT_SPACE && (values.coverage !== undefined || values.loader)) throw new UsageError("--coverage and --loader work only in space c64");
347
+ const image = readImage(positionals[0]!);
348
+ const coverage = readCoverageFiles(values.coverage ?? [], (message) => {
349
+ throw new UsageError(message);
350
+ });
351
+ 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");
352
+ // Read the entry state before the long steps, so a missing file stops the run at once.
353
+ const state = values.loader ? readEntryState(readProjectFile(image.statePath!).bytes.toString("utf8"), image.statePath!) : undefined;
354
+ const knowledge = readKnowledge(space);
355
+ const instructions = await decode(image, knowledge, entries);
356
+ const laid = layout({
357
+ segments: image.segments,
358
+ byteAt: image.byteAt,
359
+ ...(image.entry === undefined ? {} : { entry: image.entry }),
360
+ instructions,
361
+ symbols: knowledge.symbols,
362
+ regions: knowledge.regions,
363
+ comments: knowledge.comments,
364
+ modules: knowledge.modules,
365
+ referenceTargets: knowledge.references.map((reference) => reference.to),
366
+ operands: knowledge.operands,
367
+ ...(coverage === undefined ? {} : { coverage }),
368
+ // The platform names are C64 names: drive code and banks get none.
369
+ ...(space === DEFAULT_SPACE ? { platform: platformNames() } : {}),
370
+ });
371
+ const asBytes = await probe(laid);
372
+ 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."];
373
+ let loader: LoaderPlan | undefined;
374
+ // An image without its entry has no program: the source is still written and checked, but the run fails (R2-02).
375
+ const missingEntry = entryOutsideImage(image.entry, image.segments);
376
+ // A loader that cannot be planned does not stop the source: the source is written and checked, and the result says why.
377
+ let loaderRefused: string | undefined = state === undefined ? undefined : missingEntry;
378
+ if (state !== undefined && missingEntry === undefined) {
379
+ const segments = image.segments.map((segment) => ({ ...segment, files: laid.pieces.filter((piece) => piece.start >= segment.start && piece.start <= segment.end).map((piece) => piece.file) }));
380
+ try {
381
+ const entryName = laid.names.get(image.entry!);
382
+ loader = planLoader({ segments, byteAt: image.byteAt, state, entry: image.entry!, ...(entryName === undefined ? {} : { entryName }), header: [`Loader generated by c64-source from ${image.path}.`, "It copies the segments into place, sets the entry state and jumps to the entry."] });
383
+ } catch (error) {
384
+ if (!(error instanceof WireFailure)) throw error;
385
+ loaderRefused = error.message;
386
+ }
387
+ }
388
+ // Every generated file starts with the mark, so a later run knows its own files and removes the ones it does not write again (F-25).
389
+ const marked = (list: Array<{ name: string; text: string }>) => list.map((file) => ({ ...file, text: `${GENERATED_MARK}. Do not edit: change the knowledge and generate again.\n${file.text}` }));
390
+ const loaderFiles = loader === undefined ? [] : [{ name: "loader.asm", text: loader.text }];
391
+ // An operand expression that ACME refuses, or assembles to other bytes, is written as its number, and the source is assembled again (D65).
392
+ const numbers = new Map<number, string>();
393
+ let rendered = render(laid, asBytes, header, numbers);
394
+ let files = marked([...rendered.files, ...loaderFiles]);
395
+ let built = await assembleFiles(files, "main.asm");
396
+ for (let round = 0; round < OPERAND_ROUNDS && rendered.expressions.length > 0; round++) {
397
+ const blamed = blame(rendered, built, image, 1);
398
+ if (blamed.size === 0) break;
399
+ for (const [address, reason] of blamed) numbers.set(address, reason);
400
+ rendered = render(laid, asBytes, header, numbers);
401
+ files = marked([...rendered.files, ...loaderFiles]);
402
+ built = await assembleFiles(files, "main.asm");
403
+ }
404
+ removeOldFiles(values.out, new Set(files.map((file) => file.name)));
405
+ for (const file of files) writeProjectFile(`${values.out}/${file.name}`, Buffer.from(file.text, "utf8"), { what: "--out" });
406
+ const showRange = (range: Range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) });
407
+ const inData = entriesInData(entries, knowledge.regions);
408
+ if (rendered.notExecuted !== undefined) {
409
+ const all = { image: image.path, revision: knowledge.revision, instructions: rendered.notExecuted.instructions, ranges: rendered.notExecuted.ranges.map(showRange) };
410
+ writeProjectFile(`${values.out}/${NOT_EXECUTED_FILE}`, Buffer.from(`${JSON.stringify(all, null, 2)}\n`, "utf8"), { what: "--out" });
411
+ }
412
+
413
+ const output: Record<string, unknown> = {
414
+ out: values.out,
415
+ files: files.map((file) => file.name),
416
+ revision: knowledge.revision,
417
+ instructions: rendered.instructions,
418
+ dataBytes: rendered.dataBytes,
419
+ ...(rendered.undecodedBytes === 0 ? {} : { undecodedBytes: rendered.undecodedBytes }),
420
+ ...(missingEntry === undefined ? {} : { entryOutside: { entry: formatC64Address(image.entry!), reason: missingEntry } }),
421
+ labels: { named: laid.namedLabels, generated: laid.generatedLabels },
422
+ // An --entry in a data region decodes nothing: the result says so, and a code region in the knowledge changes it (F-27).
423
+ ...(inData.length === 0
424
+ ? {}
425
+ : { entriesInData: inData.map(({ entry, region }) => ({ entry: formatC64Address(entry), region: { ...showRange(region), type: region.type }, origin: region.origin })) }),
426
+ ...(rendered.asBytes.length === 0
427
+ ? {}
428
+ : { asBytes: { count: rendered.asBytes.length, listed: rendered.asBytes.slice(0, MAX_LISTED).map((item) => ({ address: formatC64Address(item.address), instruction: item.text })) } }),
429
+ ...(rendered.notExecuted === undefined
430
+ ? {}
431
+ : {
432
+ notExecuted: {
433
+ instructions: rendered.notExecuted.instructions,
434
+ ranges: rendered.notExecuted.ranges.length,
435
+ listed: rendered.notExecuted.ranges.slice(0, MAX_LISTED).map(showRange),
436
+ all: `${values.out}/${NOT_EXECUTED_FILE}`,
437
+ },
438
+ }),
439
+ ...(laid.unusedModules.length === 0 ? {} : { unusedModules: laid.unusedModules.map((module) => ({ start: formatC64Address(module.start), file: module.file })) }),
440
+ ...(rendered.expressions.length === 0 && rendered.operandsAsNumbers.length === 0 ? {} : { operandExpressions: rendered.expressions.length }),
441
+ ...(rendered.operandsAsNumbers.length === 0
442
+ ? {}
443
+ : {
444
+ operandsAsNumbers: {
445
+ count: rendered.operandsAsNumbers.length,
446
+ listed: rendered.operandsAsNumbers.slice(0, MAX_LISTED).map((item) => ({ address: formatC64Address(item.address), expression: item.expression, reason: item.reason })),
447
+ },
448
+ }),
449
+ };
450
+ // The loader includes the source, so it is built only from a source that is identical to the image.
451
+ const notBuilt = (reason: string) => (state === undefined ? {} : { loader: { verified: false, reason: loaderRefused ?? reason } });
452
+ if (built.program === undefined) {
453
+ return { output: { ...output, check: { identical: false, diagnostics: built.diagnostics.slice(0, MAX_LISTED) }, ...notBuilt("The source does not assemble, and the loader includes it.") }, failed: true };
454
+ }
455
+ const checked = check(built.program, built.start!, image);
456
+ output.check = checked;
457
+ if (!checked.identical) return { output: { ...output, ...notBuilt("The source does not assemble to the image, and the loader includes it.") }, failed: true };
458
+ if (loaderRefused !== undefined) return { output: { ...output, loader: { verified: false, reason: loaderRefused } }, failed: true };
459
+ if (loader === undefined || state === undefined) return { output, failed: missingEntry !== undefined };
460
+
461
+ const loaderBuilt = await assembleFiles(files, "loader.asm");
462
+ if (loaderBuilt.program === undefined) return { output: { ...output, loader: { verified: false, reason: "loader.asm does not assemble.", diagnostics: loaderBuilt.diagnostics.slice(0, MAX_LISTED) } }, failed: true };
463
+ const program = loaderBuilt.program;
464
+ // The simulation: the loader in memory as it loads, then every copy in its order, byte by byte.
465
+ const memory = new Int16Array(0x10000).fill(-1);
466
+ program.subarray(2).forEach((byte, offset) => (memory[loaderBuilt.start! + offset] = byte));
467
+ const expected = (address: number) => (image.segments.some((segment) => address >= segment.start && address <= segment.end) ? image.byteAt(address) : state.pages[address]!);
468
+ const wrongPacking = loader.blocks.find((block) => {
469
+ for (let address = block.range.start; address <= block.range.end; address++) if (memory[block.packedAt + address - block.range.start] !== block.bytes(address)) return true;
470
+ return false;
471
+ });
472
+ if (loaderBuilt.start !== 0x0801 || loaderBuilt.start + program.length - 3 !== loader.end || wrongPacking !== undefined) {
473
+ 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.`);
474
+ }
475
+ const jumpSymbol = loaderBuilt.symbols?.find((symbol) => symbol.name === LOADER_JUMP);
476
+ if (jumpSymbol === undefined) throw new WireFailure("operation-failed", `The assembled loader has no ${LOADER_JUMP} label. This is a defect of the c64-source script.`);
477
+ const jump = jumpSymbol.kind === "address" ? jumpSymbol.address : jumpSymbol.value;
478
+ const wrong = simulate(memory, loader.copies, expected);
479
+ 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.`);
480
+ const loaderPath = `${values.out}/loader.prg`;
481
+ writeProjectFile(loaderPath, program, { what: "--out" });
482
+ (output.files as string[]).push("loader.prg");
483
+ const ran = await runLoader(loaderPath, program, loader, image, state, jump);
484
+ output.loader = {
485
+ program: loaderPath,
486
+ copies: loader.copies.map((copy) => ({ what: copy.what, to: `${formatC64Address(copy.target.start)}-${formatC64Address(copy.target.end)}` })),
487
+ copier: formatC64Address(loader.copier),
488
+ stackByte: formatC64Address(loader.stackByte),
489
+ ...ran,
490
+ // What the loader cannot set as it was at the entry (D63).
491
+ ...(loader.cia === undefined || loader.cia.limits.length === 0 ? {} : { limits: loader.cia.limits }),
492
+ };
493
+ return { output, failed: !ran.verified };
494
+ }
495
+
496
+ await runScript(
497
+ async () => {
498
+ const { output, failed } = await run(process.argv.slice(2));
499
+ if (failed) process.exitCode = 1;
500
+ return output;
501
+ },
502
+ { usage: USAGE, refusals: [KnowledgeError] },
503
+ );