@henols/c64-re-tools 2.0.0-rc.3 → 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 (73) hide show
  1. package/README.md +15 -3
  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/server.ts +6 -5
  45. package/src/host/vice/adapter.ts +25 -10
  46. package/src/host/vice/session.ts +64 -20
  47. package/src/host-client/autostart.ts +45 -0
  48. package/src/host-client/connect.ts +4 -2
  49. package/src/host-client/coverage.ts +59 -0
  50. package/src/host-client/steps.ts +170 -0
  51. package/src/host-client/transfer.ts +29 -3
  52. package/src/host-client/vice-session.ts +42 -13
  53. package/src/knowledge/history.ts +40 -13
  54. package/src/knowledge/import.ts +72 -56
  55. package/src/knowledge/read.ts +203 -52
  56. package/src/knowledge/schema.ts +94 -12
  57. package/src/knowledge/write.ts +432 -123
  58. package/src/mcp/output.ts +32 -0
  59. package/src/mcp/server.ts +4 -2
  60. package/src/mcp/tools/debug.ts +110 -24
  61. package/src/mcp/tools/input.ts +1 -0
  62. package/src/mcp/tools/media.ts +22 -6
  63. package/src/mcp/tools/memory.ts +57 -10
  64. package/src/mcp/tools/video.ts +9 -3
  65. package/src/native/discover.ts +5 -2
  66. package/src/native/ghidra/index.ts +19 -3
  67. package/src/native/ghidra/scripts/C64Export.java +33 -1
  68. package/src/native/status.ts +15 -3
  69. package/src/native/types.ts +30 -1
  70. package/{skills/c64-memory-map/scripts → src}/platform.ts +18 -2
  71. package/src/protocol/messages.ts +17 -1
  72. package/src/protocol/vice.ts +72 -18
  73. package/src/script.ts +3 -2
@@ -0,0 +1,558 @@
1
+ // ACME source from an image, current knowledge and Ghidra's instruction
2
+ // boundaries (D48). The same input gives the same text. Names come from
3
+ // knowledge; a target that knowledge does not name gets l_xxxx. Data is
4
+ // written by its region type. The source assembles to the same bytes: the
5
+ // generator forces operand sizes, and writes an opcode form that ACME
6
+ // encodes differently as bytes (see the probe in generate.ts).
7
+
8
+ import { COVERAGE, formatC64Address } from "#src/c64.ts";
9
+ import { WireFailure } from "#src/host-client/tools.ts";
10
+ import type { CommentPlacement, RegionType } from "#src/knowledge/schema.ts";
11
+ import { hasZeroPageForm, MNEMONICS, MODE_LENGTH, OPCODES, takesAddress, type Mode } from "./opcodes.ts";
12
+
13
+ export interface Range {
14
+ start: number;
15
+ end: number;
16
+ }
17
+
18
+ export interface SourceInput {
19
+ /** In address order, without overlap. */
20
+ segments: Range[];
21
+ /** The image byte at an address inside a segment. */
22
+ byteAt: (address: number) => number;
23
+ entry?: number;
24
+ /** Where Ghidra decoded an instruction, with its length. */
25
+ instructions: ReadonlyMap<number, number>;
26
+ symbols: ReadonlyArray<{ address: number; name: string }>;
27
+ regions: ReadonlyArray<{ start: number; end: number; type: RegionType; element?: number }>;
28
+ comments: ReadonlyArray<{ address: number; placement: CommentPlacement; text: string }>;
29
+ modules: ReadonlyArray<{ start: number; file: string; title?: string }>;
30
+ /** Targets of knowledge references: each one inside the image gets a label. */
31
+ referenceTargets: readonly number[];
32
+ /** Joined coverage files (D40): an instruction that no run executed is marked (D49). */
33
+ coverage?: Uint8Array;
34
+ /** Platform names (D52): the name of an address that knowledge does not name. */
35
+ platform?: ReadonlyMap<number, string>;
36
+ }
37
+
38
+ /** Files that the generator writes itself; a module cannot have these names. */
39
+ export const GENERATED_FILES = ["main.asm", "loader.asm", "constants.asm"] as const;
40
+
41
+ const CODE_TYPES: readonly RegionType[] = ["code", "jumps", "dual-use"];
42
+ const BYTES_PER_LINE = 8;
43
+ const WORDS_PER_LINE = 4;
44
+ const CHARS_PER_LINE = 32;
45
+ const MIN_FILL = 16;
46
+ const SIDE_COLUMN = 40;
47
+ const INDENT = " ";
48
+
49
+ type Region = SourceInput["regions"][number];
50
+
51
+ interface InstructionItem {
52
+ kind: "instruction";
53
+ address: number;
54
+ length: number;
55
+ opcode: number;
56
+ }
57
+
58
+ interface DataItem {
59
+ kind: "data";
60
+ start: number;
61
+ end: number;
62
+ /** The region type, or unclassified. In a code region, bytes that Ghidra did not decode. */
63
+ type: RegionType | "unclassified" | "undecoded";
64
+ region?: Region;
65
+ }
66
+
67
+ type Item = InstructionItem | DataItem;
68
+
69
+ export interface Piece {
70
+ start: number;
71
+ end: number;
72
+ file: string;
73
+ title: string;
74
+ /** True when the piece starts a segment: the source sets the address there. */
75
+ segmentStart: boolean;
76
+ items: Item[];
77
+ }
78
+
79
+ export interface Layout {
80
+ pieces: Piece[];
81
+ /** Names of addresses: inside the image a label, outside it a constant. */
82
+ names: Map<number, string>;
83
+ /** Addresses outside the image that the source names, as constants. */
84
+ constants: number[];
85
+ inImage: (address: number) => boolean;
86
+ byteAt: (address: number) => number;
87
+ comments: SourceInput["comments"];
88
+ coverage?: Uint8Array;
89
+ generatedLabels: number;
90
+ namedLabels: number;
91
+ unusedModules: Array<{ start: number; file: string }>;
92
+ }
93
+
94
+ const hex2 = (value: number) => `$${value.toString(16).padStart(2, "0")}`;
95
+ const hex4 = (value: number) => formatC64Address(value);
96
+ const signed = (byte: number) => (byte < 0x80 ? byte : byte - 0x100);
97
+
98
+ function operandValue(address: number, opcode: number, byteAt: (address: number) => number): number | undefined {
99
+ const { mode } = OPCODES[opcode]!;
100
+ const low = MODE_LENGTH[mode] >= 2 ? byteAt(address + 1) : 0;
101
+ if (mode === "imp") return undefined;
102
+ if (mode === "rel") return (address + 2 + signed(low)) & 0xffff;
103
+ if (MODE_LENGTH[mode] === 3) return low | (byteAt(address + 2) << 8);
104
+ return low;
105
+ }
106
+
107
+ /** Splits the segments into pieces at module starts and lays out instructions and data. */
108
+ export function layout(input: SourceInput): Layout {
109
+ const inImage = (address: number) => input.segments.some((segment) => address >= segment.start && address <= segment.end);
110
+ const regionAt = (address: number) => input.regions.find((region) => address >= region.start && address <= region.end);
111
+ const isData = (address: number) => {
112
+ const region = regionAt(address);
113
+ return region !== undefined && !CODE_TYPES.includes(region.type);
114
+ };
115
+
116
+ for (const module of input.modules) {
117
+ if ((GENERATED_FILES as readonly string[]).includes(module.file)) throw new WireFailure("invalid-input", `The module at ${hex4(module.start)} has the file ${module.file}, which the generator writes itself. Give the module another file.`);
118
+ }
119
+ const modules = [...input.modules].sort((a, b) => a.start - b.start);
120
+ const unusedModules = modules.filter((module) => !inImage(module.start)).map(({ start, file }) => ({ start, file }));
121
+
122
+ const pieces: Piece[] = [];
123
+ for (const segment of input.segments) {
124
+ const starts = modules.filter((module) => module.start > segment.start && module.start <= segment.end).map((module) => module.start);
125
+ const bounds = [segment.start, ...starts, segment.end + 1];
126
+ for (let index = 0; index + 1 < bounds.length; index++) {
127
+ const start = bounds[index]!;
128
+ const end = bounds[index + 1]! - 1;
129
+ const module = modules.find((row) => row.start === start);
130
+ const file = module?.file ?? `segment_${start.toString(16).padStart(4, "0")}.asm`;
131
+ const title = module?.title?.replace(/\s*\r?\n\s*/g, " ") ?? `Segment ${hex4(segment.start)}-${hex4(segment.end)}`;
132
+ pieces.push({ start, end, file, title, segmentStart: index === 0, items: [] });
133
+ }
134
+ }
135
+ const files = new Map<string, number>();
136
+ for (const piece of pieces) {
137
+ const other = files.get(piece.file);
138
+ if (other !== undefined) throw new WireFailure("invalid-input", `The file ${piece.file} is used at ${hex4(other)} and at ${hex4(piece.start)}. Give the module another file.`);
139
+ files.set(piece.file, piece.start);
140
+ }
141
+
142
+ for (const piece of pieces) {
143
+ let address = piece.start;
144
+ while (address <= piece.end) {
145
+ const region = regionAt(address);
146
+ const codeRegion = region !== undefined && CODE_TYPES.includes(region.type);
147
+ const length = input.instructions.get(address);
148
+ const opcode = input.byteAt(address);
149
+ const fits =
150
+ length !== undefined &&
151
+ (codeRegion || region === undefined) &&
152
+ MODE_LENGTH[OPCODES[opcode]!.mode] === length &&
153
+ address + length - 1 <= piece.end &&
154
+ Array.from({ length }, (_, offset) => address + offset).every((at) => !isData(at));
155
+ if (fits) {
156
+ piece.items.push({ kind: "instruction", address, length: length!, opcode });
157
+ address += length!;
158
+ continue;
159
+ }
160
+ // A data run: one region, or unclassified bytes up to the next decoded instruction.
161
+ const type: DataItem["type"] = codeRegion ? "undecoded" : (region?.type ?? "unclassified");
162
+ let end = address;
163
+ const same = (at: number) => {
164
+ if (at > piece.end) return false;
165
+ const next = regionAt(at);
166
+ if (region !== undefined && !codeRegion) return next === region;
167
+ if (codeRegion) return next === region && !input.instructions.has(at);
168
+ return next === undefined && !input.instructions.has(at);
169
+ };
170
+ while (same(end + 1)) end++;
171
+ const last = piece.items.at(-1);
172
+ if (last?.kind === "data" && last.type === type && last.region === region && last.end + 1 === address) last.end = end;
173
+ else piece.items.push({ kind: "data", start: address, end, type, ...(region === undefined ? {} : { region }) });
174
+ address = end + 1;
175
+ }
176
+ }
177
+
178
+ // Labels: every address inside the image that an operand, a table, a symbol, a reference or the entry names.
179
+ const wanted = new Set<number>();
180
+ const want = (address: number) => inImage(address) && wanted.add(address);
181
+ const outside = new Set<number>();
182
+ const symbols = new Map(input.symbols.map((symbol) => [symbol.address, symbol.name]));
183
+ // A platform name that knowledge gives to another address, or that reads as an instruction, is not used.
184
+ const knowledgeNames = new Set(input.symbols.map((symbol) => symbol.name));
185
+ const platform = new Map([...(input.platform ?? [])].filter(([, name]) => !knowledgeNames.has(name) && !MNEMONICS.has(name.toLowerCase())));
186
+ const use = (address: number) => {
187
+ if (inImage(address)) wanted.add(address);
188
+ else if (symbols.has(address) || platform.has(address)) outside.add(address);
189
+ };
190
+ for (const piece of pieces) {
191
+ for (const item of piece.items) {
192
+ if (item.kind === "instruction") {
193
+ if (takesAddress(OPCODES[item.opcode]!.mode)) use(operandValue(item.address, item.opcode, input.byteAt)!);
194
+ continue;
195
+ }
196
+ for (const pointer of pointersOf(item, input.byteAt)) use(pointer);
197
+ }
198
+ }
199
+ for (const symbol of input.symbols) want(symbol.address);
200
+ for (const target of input.referenceTargets) want(target);
201
+ if (input.entry !== undefined) want(input.entry);
202
+
203
+ const names = new Map<number, string>();
204
+ const taken = new Set([...knowledgeNames, ...platform.values()]);
205
+ let generatedLabels = 0;
206
+ let namedLabels = 0;
207
+ for (const address of [...wanted, ...outside].sort((a, b) => a - b)) {
208
+ const known = symbols.get(address);
209
+ if (known !== undefined) {
210
+ if (MNEMONICS.has(known.toLowerCase())) throw new WireFailure("invalid-input", `The symbol ${known} at ${hex4(address)} has the name of an instruction, which ACME cannot read as a label. Rename the symbol with c64-knowledge.`);
211
+ names.set(address, known);
212
+ if (inImage(address)) namedLabels++;
213
+ continue;
214
+ }
215
+ const named = platform.get(address);
216
+ if (named !== undefined) {
217
+ names.set(address, named);
218
+ if (inImage(address)) namedLabels++;
219
+ continue;
220
+ }
221
+ let name = `l_${address.toString(16).padStart(4, "0")}`;
222
+ while (taken.has(name)) name = `${name}_`;
223
+ taken.add(name);
224
+ names.set(address, name);
225
+ generatedLabels++;
226
+ }
227
+ return {
228
+ pieces,
229
+ names,
230
+ constants: [...outside].sort((a, b) => a - b),
231
+ inImage,
232
+ byteAt: input.byteAt,
233
+ comments: input.comments,
234
+ ...(input.coverage === undefined ? {} : { coverage: input.coverage }),
235
+ generatedLabels,
236
+ namedLabels,
237
+ unusedModules,
238
+ };
239
+ }
240
+
241
+ /** The pointer region types: a table of addresses, or of addresses minus 1 for RTS. */
242
+ const POINTER_TYPES: ReadonlyArray<DataItem["type"]> = ["addresses", "addresses-split", "rts-addresses", "rts-addresses-split"];
243
+
244
+ /** The pairs of a pointer table: the address of each low and high byte, and the target it names. */
245
+ function pairsOf(region: Region, byteAt: (address: number) => number): Array<{ low: number; high: number; target: number }> {
246
+ const length = region.end - region.start + 1;
247
+ const split = region.type === "addresses-split" || region.type === "rts-addresses-split";
248
+ const minus = region.type === "rts-addresses" || region.type === "rts-addresses-split" ? 1 : 0;
249
+ const count = Math.floor(length / 2);
250
+ return Array.from({ length: count }, (_, index) => {
251
+ const low = split ? region.start + index : region.start + 2 * index;
252
+ const high = split ? region.start + count + index : low + 1;
253
+ return { low, high, target: ((byteAt(low) | (byteAt(high) << 8)) + minus) & 0xffff };
254
+ });
255
+ }
256
+
257
+ function pointersOf(item: DataItem, byteAt: (address: number) => number): number[] {
258
+ if (item.region === undefined || !POINTER_TYPES.includes(item.type)) return [];
259
+ return pairsOf(item.region, byteAt)
260
+ .filter((pair) => (pair.low >= item.start && pair.low <= item.end) || (pair.high >= item.start && pair.high <= item.end))
261
+ .map((pair) => pair.target);
262
+ }
263
+
264
+ /** How an instruction is written: the text with a given operand expression, and the key of its opcode form for the probe. */
265
+ export interface InstructionForm {
266
+ key: string;
267
+ opcode: number;
268
+ suffix: string;
269
+ }
270
+
271
+ /** The operand size suffix: +2 forces an absolute operand below $0100, +1 a zero-page operand that is a label of the image. */
272
+ export function formOf(layout: Pick<Layout, "inImage" | "byteAt">, item: InstructionItem): InstructionForm {
273
+ const { mnemonic, mode } = OPCODES[item.opcode]!;
274
+ const value = takesAddress(mode) ? operandValue(item.address, item.opcode, layout.byteAt)! : 0;
275
+ let suffix = "";
276
+ if ((mode === "abs" || mode === "abx" || mode === "aby") && value < 0x100 && hasZeroPageForm(mnemonic, mode)) suffix = "+2";
277
+ if ((mode === "zp" || mode === "zpx" || mode === "zpy") && layout.inImage(value)) suffix = "+1";
278
+ return { key: `${item.opcode.toString(16).padStart(2, "0")}${suffix}`, opcode: item.opcode, suffix };
279
+ }
280
+
281
+ /** The instruction text with an operand expression; the expression is a name or a $ number. */
282
+ export function instructionText(opcode: number, suffix: string, expression: string): string {
283
+ const { mnemonic, mode } = OPCODES[opcode]!;
284
+ const operand: Record<Mode, string> = {
285
+ imp: "",
286
+ imm: `#${expression}`,
287
+ zp: expression,
288
+ zpx: `${expression},x`,
289
+ zpy: `${expression},y`,
290
+ abs: expression,
291
+ abx: `${expression},x`,
292
+ aby: `${expression},y`,
293
+ ind: `(${expression})`,
294
+ izx: `(${expression},x)`,
295
+ izy: `(${expression}),y`,
296
+ rel: expression,
297
+ };
298
+ const text = operand[mode];
299
+ return text === "" ? `${mnemonic}${suffix}` : `${mnemonic}${suffix} ${text}`;
300
+ }
301
+
302
+ /** The plain number of an operand: what the probe assembles, and what a comment shows. */
303
+ export function literalOperand(opcode: number, value: number): string {
304
+ const { mode } = OPCODES[opcode]!;
305
+ return MODE_LENGTH[mode] === 3 || mode === "rel" ? hex4(value) : hex2(value);
306
+ }
307
+
308
+ /** One instruction of each form, for the probe: the form, and the instruction bytes. */
309
+ export function probeForms(layout: Layout): Array<{ form: InstructionForm; bytes: number[] }> {
310
+ const forms = new Map<string, { form: InstructionForm; bytes: number[] }>();
311
+ for (const piece of layout.pieces) {
312
+ for (const item of piece.items) {
313
+ if (item.kind !== "instruction") continue;
314
+ const form = formOf(layout, item);
315
+ if (!forms.has(form.key)) forms.set(form.key, { form, bytes: Array.from({ length: item.length }, (_, offset) => layout.byteAt(item.address + offset)) });
316
+ }
317
+ }
318
+ return [...forms.values()].sort((a, b) => (a.form.key < b.form.key ? -1 : 1));
319
+ }
320
+
321
+ // --- Data cells -------------------------------------------------------------
322
+
323
+ type Directive = "!byte" | "!word" | "!fill" | "!pet" | "!scr";
324
+
325
+ interface Cell {
326
+ address: number;
327
+ length: number;
328
+ directive: Directive;
329
+ /** The value text: an expression, a fill, or the characters of a string. */
330
+ text: string;
331
+ }
332
+
333
+ /** PETSCII bytes that ACME's !pet writes from one source character. */
334
+ function petsciiChar(byte: number): string | undefined {
335
+ if (byte >= 0x41 && byte <= 0x5a) return String.fromCharCode(byte + 0x20);
336
+ if (byte >= 0xc1 && byte <= 0xda) return String.fromCharCode(byte - 0x80);
337
+ if (byte >= 0x20 && byte <= 0x40 && byte !== 0x22) return String.fromCharCode(byte);
338
+ if (byte === 0x5b || byte === 0x5d) return String.fromCharCode(byte);
339
+ return undefined;
340
+ }
341
+
342
+ /** Screen codes that ACME's !scr writes from one source character. */
343
+ function screenChar(byte: number): string | undefined {
344
+ if (byte === 0x00) return "@";
345
+ if (byte >= 0x01 && byte <= 0x1a) return String.fromCharCode(byte + 0x60);
346
+ if (byte >= 0x41 && byte <= 0x5a) return String.fromCharCode(byte);
347
+ if (byte >= 0x20 && byte <= 0x3f && byte !== 0x22) return String.fromCharCode(byte);
348
+ return undefined;
349
+ }
350
+
351
+ const FILL_TYPES: ReadonlyArray<DataItem["type"]> = ["bytes", "workspace", "unused", "unclassified", "bitmap"];
352
+
353
+ function cellsOf(item: DataItem, layout: Layout, breaks: Set<number>): Cell[] {
354
+ const { byteAt } = layout;
355
+ const cells: Cell[] = [];
356
+ const name = (address: number) => layout.names.get(address);
357
+ const expression = (target: number) => name(target) ?? hex4(target);
358
+ const rtsExpression = (target: number) => (name(target) === undefined ? hex4((target - 1) & 0xffff) : `${name(target)}-1`);
359
+ const byteCell = (address: number, text = hex2(byteAt(address))): Cell => ({ address, length: 1, directive: "!byte", text });
360
+
361
+ if (item.region !== undefined && POINTER_TYPES.includes(item.type)) {
362
+ const rts = item.type === "rts-addresses" || item.type === "rts-addresses-split";
363
+ const split = item.type === "addresses-split" || item.type === "rts-addresses-split";
364
+ const pairs = pairsOf(item.region, byteAt);
365
+ const partOf = new Map<number, { pair: (typeof pairs)[number]; high: boolean }>();
366
+ for (const pair of pairs) {
367
+ partOf.set(pair.low, { pair, high: false });
368
+ partOf.set(pair.high, { pair, high: true });
369
+ }
370
+ for (let address = item.start; address <= item.end; address++) {
371
+ const part = partOf.get(address);
372
+ if (part === undefined) {
373
+ cells.push(byteCell(address));
374
+ continue;
375
+ }
376
+ const text = rts ? rtsExpression(part.pair.target) : expression(part.pair.target);
377
+ const whole = !split && !part.high && address + 1 <= item.end && !breaks.has(address + 1);
378
+ if (whole) {
379
+ cells.push({ address, length: 2, directive: "!word", text });
380
+ address++;
381
+ continue;
382
+ }
383
+ cells.push(byteCell(address, `${part.high ? ">" : "<"}${/^[$A-Za-z_.0-9]+$/.test(text) ? text : `(${text})`}`));
384
+ }
385
+ return cells;
386
+ }
387
+
388
+ if (item.type === "words") {
389
+ const odd = (item.region === undefined ? 0 : (item.region.end - item.region.start + 1) % 2) === 1;
390
+ for (let address = item.start; address <= item.end; address++) {
391
+ const offset = address - (item.region?.start ?? item.start);
392
+ const last = odd && address === item.region!.end;
393
+ if (offset % 2 === 0 && !last && address + 1 <= item.end && !breaks.has(address + 1)) {
394
+ cells.push({ address, length: 2, directive: "!word", text: hex4(byteAt(address) | (byteAt(address + 1) << 8)) });
395
+ address++;
396
+ } else cells.push(byteCell(address));
397
+ }
398
+ return cells;
399
+ }
400
+
401
+ if (item.type === "petscii" || item.type === "screen") {
402
+ const toChar = item.type === "petscii" ? petsciiChar : screenChar;
403
+ const directive = item.type === "petscii" ? "!pet" : "!scr";
404
+ for (let address = item.start; address <= item.end; address++) {
405
+ const char = toChar(byteAt(address));
406
+ cells.push(char === undefined ? byteCell(address) : { address, length: 1, directive, text: char });
407
+ }
408
+ return cells;
409
+ }
410
+
411
+ const binary = item.type === "sprite";
412
+ const fill = FILL_TYPES.includes(item.type) && item.region?.element === undefined;
413
+ for (let address = item.start; address <= item.end; address++) {
414
+ const value = byteAt(address);
415
+ if (fill) {
416
+ let end = address;
417
+ while (end + 1 <= item.end && byteAt(end + 1) === value && !breaks.has(end + 1)) end++;
418
+ if (end - address + 1 >= MIN_FILL) {
419
+ cells.push({ address, length: end - address + 1, directive: "!fill", text: `${end - address + 1}, ${hex2(value)}` });
420
+ address = end;
421
+ continue;
422
+ }
423
+ }
424
+ cells.push(byteCell(address, binary ? `%${value.toString(2).padStart(8, "0")}` : hex2(value)));
425
+ }
426
+ return cells;
427
+ }
428
+
429
+ /** The cells of one data item as lines: one directive per line, broken at labels, comments and elements. */
430
+ function dataLines(item: DataItem, layout: Layout, breaks: Set<number>): Array<{ address: number; text: string }> {
431
+ const cells = cellsOf(item, layout, breaks);
432
+ const element = item.region?.element;
433
+ const perLine = (directive: Directive) =>
434
+ directive === "!word" ? WORDS_PER_LINE : directive === "!pet" || directive === "!scr" ? CHARS_PER_LINE : item.type === "sprite" ? 3 : element !== undefined && element <= 16 ? element : BYTES_PER_LINE;
435
+ const lines: Array<{ address: number; text: string }> = [];
436
+ let group: Cell[] = [];
437
+ const flush = () => {
438
+ if (group.length === 0) return;
439
+ const [first] = group;
440
+ const directive = first!.directive;
441
+ const text =
442
+ directive === "!pet" || directive === "!scr"
443
+ ? `${directive} "${group.map((cell) => cell.text).join("")}"`
444
+ : `${directive} ${group.map((cell) => cell.text).join(directive === "!fill" ? "" : ",")}`;
445
+ lines.push({ address: first!.address, text });
446
+ group = [];
447
+ };
448
+ for (const cell of cells) {
449
+ const first = group[0];
450
+ const elementStart = element !== undefined && item.region !== undefined && (cell.address - item.region.start) % element === 0;
451
+ if (first !== undefined && (first.directive !== cell.directive || cell.directive === "!fill" || group.length >= perLine(first.directive) || breaks.has(cell.address) || elementStart)) flush();
452
+ group.push(cell);
453
+ }
454
+ flush();
455
+ return lines;
456
+ }
457
+
458
+ // --- Rendering --------------------------------------------------------------
459
+
460
+ export interface Rendered {
461
+ files: Array<{ name: string; text: string }>;
462
+ instructions: number;
463
+ dataBytes: number;
464
+ undecodedBytes: number;
465
+ /** Instructions written as bytes, because ACME writes their form with other bytes. */
466
+ asBytes: Array<{ address: number; text: string }>;
467
+ /** With coverage: runs of instructions that no run executed. */
468
+ notExecuted?: { instructions: number; ranges: Range[] };
469
+ }
470
+
471
+ const withSide = (text: string, side: string | undefined) => (side === undefined ? text : `${text.padEnd(SIDE_COLUMN - 1)} ; ${side}`);
472
+
473
+ /** The source text of each piece, the main file and the constants. `asBytes` holds the opcode form keys that the probe refused. */
474
+ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: string[]): Rendered {
475
+ const lineComments = new Map<number, string[]>();
476
+ const sideComments = new Map<number, string>();
477
+ for (const comment of layout.comments) {
478
+ if (!layout.inImage(comment.address)) continue;
479
+ if (comment.placement === "line") lineComments.set(comment.address, comment.text.split(/\r?\n/));
480
+ else sideComments.set(comment.address, comment.text.replace(/\s*\r?\n\s*/g, " "));
481
+ }
482
+ const breaks = new Set<number>([...layout.names.keys(), ...lineComments.keys(), ...sideComments.keys()]);
483
+ const result: Rendered = { files: [], instructions: 0, dataBytes: 0, undecodedBytes: 0, asBytes: [] };
484
+ const { coverage } = layout;
485
+ const notExecuted = { instructions: 0, ranges: [] as Range[] };
486
+
487
+ for (const piece of layout.pieces) {
488
+ const out: string[] = [`; ${piece.title}`, `; ${hex4(piece.start)}-${hex4(piece.end)}`, ""];
489
+ /** True while the instructions before have not executed: they form one range. */
490
+ let running = false;
491
+ const head = (address: number) => {
492
+ for (const text of lineComments.get(address) ?? []) out.push(`; ${text}`.trimEnd());
493
+ const label = layout.names.get(address);
494
+ if (label !== undefined) out.push(label);
495
+ };
496
+ for (const item of piece.items) {
497
+ if (item.kind === "instruction") {
498
+ head(item.address);
499
+ const executed = coverage === undefined || (coverage[item.address]! & COVERAGE.ramExecute) !== 0;
500
+ if (!executed) {
501
+ if (running) notExecuted.ranges.at(-1)!.end = item.address + item.length - 1;
502
+ else {
503
+ notExecuted.ranges.push({ start: item.address, end: item.address + item.length - 1 });
504
+ out.push("; not executed");
505
+ }
506
+ notExecuted.instructions++;
507
+ }
508
+ running = !executed;
509
+ const sides: string[] = [];
510
+ if (sideComments.has(item.address)) sides.push(sideComments.get(item.address)!);
511
+ for (let offset = 1; offset < item.length; offset++) {
512
+ const inside = item.address + offset;
513
+ const label = layout.names.get(inside);
514
+ if (label !== undefined) out.push(`${label} = * + ${offset}`);
515
+ for (const text of lineComments.get(inside) ?? []) sides.push(text);
516
+ if (sideComments.has(inside)) sides.push(sideComments.get(inside)!);
517
+ }
518
+ const form = formOf(layout, item);
519
+ const { mode } = OPCODES[item.opcode]!;
520
+ const value = mode === "imp" ? 0 : operandValue(item.address, item.opcode, layout.byteAt)!;
521
+ const literal = instructionText(item.opcode, "", literalOperand(item.opcode, value));
522
+ if (asBytes.has(form.key)) {
523
+ const bytes = Array.from({ length: item.length }, (_, offset) => hex2(layout.byteAt(item.address + offset)));
524
+ out.push(withSide(`${INDENT}!byte ${bytes.join(",")}`, [literal, ...sides].join(" ")));
525
+ result.asBytes.push({ address: item.address, text: literal });
526
+ } else {
527
+ const expression = takesAddress(mode) ? (layout.names.get(value) ?? literalOperand(item.opcode, value)) : literalOperand(item.opcode, value);
528
+ out.push(withSide(`${INDENT}${instructionText(item.opcode, form.suffix, expression)}`, sides.length === 0 ? undefined : sides.join(" ")));
529
+ }
530
+ result.instructions++;
531
+ continue;
532
+ }
533
+ running = false;
534
+ const lines = dataLines(item, layout, breaks);
535
+ lines.forEach((line, index) => {
536
+ head(line.address);
537
+ const note = item.type === "undecoded" && index === 0 ? "not decoded" : undefined;
538
+ const side = [note, sideComments.get(line.address)].filter((text) => text !== undefined).join(" ");
539
+ out.push(withSide(`${INDENT}${line.text}`, side === "" ? undefined : side));
540
+ });
541
+ const bytes = item.end - item.start + 1;
542
+ if (item.type === "undecoded") result.undecodedBytes += bytes;
543
+ else result.dataBytes += bytes;
544
+ }
545
+ result.files.push({ name: piece.file, text: `${out.join("\n")}\n` });
546
+ }
547
+
548
+ const constants = ["; Names outside the image.", ...layout.constants.map((address) => `${layout.names.get(address)} = ${hex4(address)}`)];
549
+ result.files.push({ name: "constants.asm", text: `${constants.join("\n")}\n` });
550
+ const main = [...header.map((line) => `; ${line}`), "", "!cpu 6510", '!source "constants.asm"'];
551
+ for (const piece of layout.pieces) {
552
+ if (piece.segmentStart) main.push("", `* = ${hex4(piece.start)}`);
553
+ main.push(`!source "${piece.file}"`);
554
+ }
555
+ result.files.push({ name: "main.asm", text: `${main.join("\n")}\n` });
556
+ if (coverage !== undefined) result.notExecuted = notExecuted;
557
+ return result;
558
+ }
@@ -31,6 +31,10 @@ node <skill>/scripts/analyze.ts extracted/game.prg --entry '$080d' --decompile '
31
31
  | `--listing analysis/game.lst` | DXA only: write the disassembly listing to this project file. |
32
32
  | `--decompile '$0818'` | Ghidra only: decompile the routine at this address. Use it again for more routines, at most 32. |
33
33
  | `--flat64k` | The file is 64 KiB of memory from `$0000`, not a PRG. |
34
+ | `--max-listed 20` | List at most this number of labels or functions. The default is 50. Use 0 to list none. |
35
+ | `--instructions analysis/instructions.json` | Ghidra only: write every instruction to this project file as JSON. Each has `address`, `bytes`, `mnemonic`, `mode` and `operand` in 6502 syntax. A branch, jump or call also has its `target`. With `--coverage`, `executed` tells if it ran. |
36
+ | `--space drive8` | The knowledge space of the image: `drive8` for code from the 1541 drive, or `bank0` to `bank255` for a cartridge bank. The default is `c64`. The seeds come from that space, and the findings go into it. |
37
+ | `--coverage coverage/attract.bin` | A coverage file of 64 KiB from `c64_memmap` (action `read` with `out`). Use it again for more runs. The executed code becomes entry points. |
34
38
 
35
39
  The script prints one JSON object. Addresses are `$` and four hex digits.
36
40
 
@@ -42,6 +46,13 @@ Both analyzers use the knowledge from the user and the LLM as seeds:
42
46
 
43
47
  Facts that an analyzer found are not seeds. A new analysis can then correct or remove them.
44
48
 
49
+ More seeds come from evidence:
50
+
51
+ - The start of each `code` region and each entry of a `jumps` table from the user or the LLM is an entry point.
52
+ - With `--coverage`, each executed instruction that starts a flow is an entry point. A flow starts after a gap, or after RTS, RTI, JMP or BRK. Then the analyzers find code that only a vector, an RTS table or an interrupt reaches.
53
+
54
+ The analyzers take at most 1024 entry points. Knowledge routines and `--entry` come first, then code regions and tables, then coverage. `coverageSeeds` gives how many entry points the coverage gave and how many the analysis used.
55
+
45
56
  ## Workflow
46
57
 
47
58
  1. Read the knowledge for the program with the c64-knowledge skill.
@@ -49,10 +60,13 @@ Facts that an analyzer found are not seeds. A new analysis can then correct or r
49
60
  3. Find the important routines. Give them names with the c64-knowledge skill.
50
61
  4. Run a deeper pass with Ghidra. Give the entry points with `--entry`. Ask for decompiled code of the routines that you must understand.
51
62
  5. Read the result:
52
- - `changes` counts the symbols, regions and references that the analysis `added`, `changed`, `retired` or kept `unchanged`.
53
- - DXA: `regions` lists the code and data regions, and `labels` lists the addresses that DXA named.
54
- - Ghidra: `functions` lists the routines that Ghidra found, and `decompilations` gives C-like code for the requested routines.
63
+ - `changes` counts the symbols, regions and references that the analysis `added`, `changed`, `retired` or kept `unchanged`. `suppressed` counts the findings that the analysis did not write, because a removal blocks that place.
64
+ - DXA: `regions` lists the code and data regions. `labels` gives the number of names that DXA found (`count`) and lists the first names (`listed`).
65
+ - Ghidra: `functions` gives the number of routines that Ghidra found (`count`) and lists the first routines (`listed`). `decompilations` gives C-like code for the requested routines.
66
+ - If a list is not complete, `rest` gives the c64-knowledge command that lists all of them.
55
67
  - `conflicts` lists the places where the analyzer and the current knowledge disagree.
68
+ - `idioms` tells what the script found by pattern: RTS dispatch tables (`rtsTables`), JMP tables (`jumpTables`) and BIT skips (`bitSkips`). The script writes them as knowledge with the origin `heuristic`, and seeds their targets into the same analysis. `candidates` gives DOP padding and opcodes that the program wrote and then executed. The script does not write those. Look at them and decide.
69
+ - With `--coverage`, `coverageConflicts` lists the analyzer data regions where the coverage shows executed code. Classify them as code with the c64-knowledge skill, or as `dual-use` when the bytes are also data.
56
70
  6. Give each important routine a name with the c64-knowledge skill. Then run the analysis again. The analyzers use the new names.
57
71
 
58
72
  The script records only structure: generated names, code and data regions, and references from Ghidra. The listing and the decompiled code are only for the current task. The script does not record them in the knowledge.
@@ -66,8 +80,12 @@ Automatic write:
66
80
 
67
81
  The script never replaces a name or a region from the user or the LLM. A name that an analyzer only shows back stays with its owner. DXA and Ghidra do not replace the facts of each other.
68
82
 
83
+ A `heuristic` region replaces the part of a DXA or Ghidra region where it lies, because an idiom is specific evidence about those bytes. DXA and Ghidra never replace a `heuristic` region. If they find code there, the result shows a conflict. A heuristic table can be too long or too short, because the script cannot know its length. Correct it with the c64-knowledge skill. Your region replaces it, and a removal holds.
84
+
69
85
  A new analysis replaces the earlier facts of the same analyzer in the analyzed range. The history keeps the earlier facts. DXA labels stay until a person or the LLM removes them, because DXA names only the addresses that it sees used.
70
86
 
87
+ A fact that the user or the LLM removes does not come back. The next analysis does not write a fact at that place, and counts it as `suppressed`. To remove a wrong fact, use the c64-knowledge skill.
88
+
71
89
  Do not store:
72
90
  - the listing or decompiled code
73
91
  - the function list as comments.
@@ -82,10 +100,15 @@ The exit status is 0 for a result, also with conflicts. The exit status is 1 for
82
100
 
83
101
  - A conflict: the analyzer finds code where the knowledge has data, or data where the knowledge has code. The knowledge stays unchanged. Find the correct answer with the c64-emulator skill, then correct the knowledge with the c64-knowledge skill.
84
102
  - A conflict with the problem "another address has this name": the knowledge has the generated name at a different address. The knowledge stays unchanged.
103
+ - A false reference through a vector: the code jumps through a vector, for example `JMP ($0408)`, and the program writes the vector at run time. The analyzer follows the bytes that the image has there. Do these steps:
104
+ 1. Find the real targets with the c64-emulator skill, or in a coverage file.
105
+ 2. If the vector bytes are also code at another time, classify them as `dual-use`.
106
+ 3. Remove the false reference with `unreference`. It does not come back.
107
+ 4. Add a `jump` reference from the JMP to each real target, and name each target as a routine.
85
108
  - `invalid-input` with a message about an entry point: no entry point is inside the program for Ghidra. Give one with `--entry`.
86
109
  - `stale-revision`: the knowledge changed during the analysis. The script recorded nothing. Run the analysis again.
87
110
  - `operation-failed` with a message about an incomplete result or listing: the analyzer did not give a complete result. The script recorded nothing. Tell the user.
88
- - `installation-incomplete`: DXA or Ghidra is not installed on this machine. Tell the user the message.
111
+ - `installation-incomplete`, or a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own disk parser, disassembler or emulator script.
89
112
  - `notDecompiled`: Ghidra did not decompile these routines. Each entry has the reason. If the reason says that the decompiler did not start, tell the user the remedy in the reason. On macOS, Ghidra has no native decompiler until the user builds it: `./gradlew buildNatives` in the `support/gradle` directory of Ghidra.
90
113
 
91
114
  ## Handoffs