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

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 (54) hide show
  1. package/README.md +7 -5
  2. package/package.json +1 -1
  3. package/skills/c64-assembler/SKILL.md +11 -2
  4. package/skills/c64-assembler/scripts/assemble.ts +4 -4
  5. package/skills/c64-basic/SKILL.md +8 -1
  6. package/skills/c64-basic/scripts/basic.ts +3 -3
  7. package/skills/c64-disk/SKILL.md +10 -3
  8. package/skills/c64-disk/scripts/disk.ts +4 -4
  9. package/skills/c64-emulator/SKILL.md +4 -0
  10. package/skills/c64-knowledge/SKILL.md +62 -12
  11. package/skills/c64-knowledge/scripts/knowledge.ts +352 -102
  12. package/skills/c64-memory-map/SKILL.md +7 -0
  13. package/skills/c64-memory-map/scripts/memmap.ts +1 -1
  14. package/skills/c64-provenance/SKILL.md +56 -3
  15. package/skills/c64-provenance/scripts/library.ts +584 -0
  16. package/skills/c64-provenance/scripts/provenance.ts +201 -9
  17. package/skills/c64-reverse-engineering/SKILL.md +16 -2
  18. package/skills/c64-source/SKILL.md +38 -7
  19. package/skills/c64-source/scripts/generate.ts +213 -29
  20. package/skills/c64-source/scripts/loader.ts +3 -1
  21. package/skills/c64-source/scripts/opcodes.ts +19 -0
  22. package/skills/c64-source/scripts/source.ts +387 -35
  23. package/skills/c64-static-analysis/SKILL.md +11 -2
  24. package/skills/c64-static-analysis/scripts/analyze.ts +95 -8
  25. package/skills/c64-static-analysis/scripts/macros.ts +233 -0
  26. package/skills/c64-testing/SKILL.md +8 -1
  27. package/skills/c64-testing/references/scenario-format.md +1 -1
  28. package/skills/c64-testing/scripts/checklist.ts +2 -2
  29. package/skills/c64-testing/scripts/run.ts +4 -4
  30. package/skills/c64-testing/scripts/scenario.ts +1 -1
  31. package/skills/c64-testing/scripts/test.ts +4 -4
  32. package/skills/c64-unpacker/SKILL.md +11 -4
  33. package/skills/c64-unpacker/scripts/unpack.ts +4 -4
  34. package/src/cli/main.ts +5 -3
  35. package/src/host-client/image.ts +1 -1
  36. package/src/host-client/transfer.ts +34 -10
  37. package/src/host-client/vice-session.ts +9 -9
  38. package/src/knowledge/conflicts.ts +157 -0
  39. package/src/knowledge/database.ts +150 -38
  40. package/src/knowledge/history.ts +54 -20
  41. package/src/knowledge/import.ts +3 -2
  42. package/src/knowledge/read.ts +169 -20
  43. package/src/knowledge/schema.ts +115 -72
  44. package/src/knowledge/text.ts +391 -0
  45. package/src/knowledge/write.ts +508 -51
  46. package/src/mcp/output.ts +14 -7
  47. package/src/mcp/tools/debug.ts +8 -6
  48. package/src/mcp/tools/media.ts +17 -13
  49. package/src/mcp/tools/memory.ts +9 -5
  50. package/src/mcp/tools/video.ts +6 -5
  51. package/src/platform.ts +28 -5
  52. package/src/project.ts +166 -3
  53. package/src/protocol/tools.ts +1 -1
  54. package/src/script.ts +46 -7
@@ -4,14 +4,17 @@
4
4
  // written by its region type. The source assembles to the same bytes: the
5
5
  // generator forces operand sizes, and writes an opcode form that ACME
6
6
  // encodes differently as bytes (see the probe in generate.ts). An operand
7
- // expression of the knowledge is written in place of its number (D65).
7
+ // expression of the knowledge is written in place of its number (D65). A
8
+ // patch is written as !if SWITCH { code } else { original } (D72), and a
9
+ // macro use as one +name args line (D73).
8
10
 
9
11
  import { COVERAGE, formatC64Address } from "#src/c64.ts";
10
12
  import { WireFailure } from "#src/host-client/tools.ts";
11
13
  import { identifiers, operandText, replaceIdentifiers, type OperandExpression } from "#src/knowledge/expression.ts";
12
14
  import { ANALYZERS } from "#src/knowledge/import.ts";
13
15
  import { isGeneratedFile, type CommentPlacement, type Origin, type RegionType } from "#src/knowledge/schema.ts";
14
- import { hasZeroPageForm, MNEMONICS, MODE_LENGTH, OPCODES, takesAddress, type Mode } from "./opcodes.ts";
16
+ import type { Area } from "#src/platform.ts";
17
+ import { hasZeroPageForm, MNEMONICS, MODE_LENGTH, OPCODES, takesAddress, USE_KINDS, useKind, type Mode, type UseKind } from "./opcodes.ts";
15
18
 
16
19
  export interface Range {
17
20
  start: number;
@@ -35,10 +38,53 @@ export interface SourceInput {
35
38
  referenceTargets: readonly number[];
36
39
  /** Joined coverage files (D40): an instruction that no run executed is marked (D49). */
37
40
  coverage?: Uint8Array;
38
- /** Platform names (D52): the name of an address that knowledge does not name, and the last address of its entry. */
39
- platform?: ReadonlyArray<{ address: number; end: number; name: string }>;
41
+ /** Platform names (D52): the name of an address that knowledge does not name, the last address of its entry, and what it is. */
42
+ platform?: ReadonlyArray<{ address: number; end: number; name: string; meaning?: string }>;
43
+ /** The platform area of an address, and the register that an I/O mirror repeats: constants.asm groups and describes its names with them. */
44
+ describe?: (address: number) => { area: Area; mirrorOf?: number };
40
45
  /** Operand expressions of the knowledge (D65), by instruction address. */
41
46
  operands?: ReadonlyArray<OperandExpression & { address: number }>;
47
+ /** Patches of the knowledge (D72): code that replaces a range when the build defines the switch. */
48
+ patches?: ReadonlyArray<{ start: number; end: number; switch: string; code: string; comment?: string }>;
49
+ /** Macros of the knowledge (D73), written to macros.asm. */
50
+ macros?: ReadonlyArray<{ name: string; params: readonly string[]; body: string; comment?: string }>;
51
+ /** Macro uses of the knowledge (D73): the instructions of a range, written as one use of a macro. */
52
+ macroUses?: ReadonlyArray<{ start: number; end: number; macro: string; args: readonly string[] }>;
53
+ }
54
+
55
+ /** A macro use that the source writes (D73), with its args in the names of this source. */
56
+ export interface MacroUseBlock {
57
+ start: number;
58
+ end: number;
59
+ macro: string;
60
+ args: string[];
61
+ }
62
+
63
+ /** A macro use that the source writes as its instructions, and why. */
64
+ export interface MacroAsInstructions {
65
+ start: number;
66
+ end: number;
67
+ macro: string;
68
+ reason: string;
69
+ }
70
+
71
+ /** A patch that the source writes (D72), with the addresses inside it that have a name. */
72
+ export interface PatchBlock {
73
+ start: number;
74
+ end: number;
75
+ switch: string;
76
+ code: string;
77
+ comment?: string;
78
+ /** Named addresses after the start of the range: the source defines them before the patch, so both builds have them. */
79
+ inner: number[];
80
+ }
81
+
82
+ /** A patch of the knowledge that the source does not write, and why. */
83
+ export interface PatchNotWritten {
84
+ start: number;
85
+ end: number;
86
+ switch: string;
87
+ reason: string;
42
88
  }
43
89
 
44
90
  /** An operand expression that the source writes as a number, with its text and why. */
@@ -92,6 +138,10 @@ export interface Layout {
92
138
  names: Map<number, string>;
93
139
  /** Addresses outside the image that the source names, as constants. */
94
140
  constants: number[];
141
+ /** How the source uses each constant: instructions by kind, and pointer table entries. */
142
+ uses: Map<number, Map<UseKind, number>>;
143
+ platform: NonNullable<SourceInput["platform"]>;
144
+ describe?: SourceInput["describe"];
95
145
  inImage: (address: number) => boolean;
96
146
  byteAt: (address: number) => number;
97
147
  comments: SourceInput["comments"];
@@ -103,6 +153,14 @@ export interface Layout {
103
153
  operands: Map<number, string>;
104
154
  /** Operand expressions that cannot be written. */
105
155
  operandsAsNumbers: OperandAsNumber[];
156
+ /** The patches that the source writes, by start (D72). */
157
+ patches: Map<number, PatchBlock>;
158
+ patchesNotWritten: PatchNotWritten[];
159
+ /** The macros, by name (D73). */
160
+ macros: NonNullable<SourceInput["macros"]>;
161
+ /** The macro uses that the source can write, by start. */
162
+ macroUses: Map<number, MacroUseBlock>;
163
+ macrosAsInstructions: MacroAsInstructions[];
106
164
  }
107
165
 
108
166
  const hex2 = (value: number) => `$${value.toString(16).padStart(2, "0")}`;
@@ -153,6 +211,11 @@ export function layout(input: SourceInput): Layout {
153
211
  const modules = [...input.modules].sort((a, b) => a.start - b.start);
154
212
  const unusedModules = modules.filter((module) => !inImage(module.start)).map(({ start, file }) => ({ start, file }));
155
213
 
214
+ // A patch starts and ends at an item boundary, so a data run stops there (D72).
215
+ const patches = [...(input.patches ?? [])].sort((a, b) => a.start - b.start);
216
+ const macroUses = [...(input.macroUses ?? [])].sort((a, b) => a.start - b.start);
217
+ const cuts = new Set([...patches, ...macroUses].flatMap((range) => [range.start, range.end + 1]));
218
+
156
219
  const pieces: Piece[] = [];
157
220
  for (const segment of input.segments) {
158
221
  const starts = modules.filter((module) => module.start > segment.start && module.start <= segment.end).map((module) => module.start);
@@ -195,7 +258,7 @@ export function layout(input: SourceInput): Layout {
195
258
  const type: DataItem["type"] = codeRegion ? "undecoded" : (region?.type ?? "unclassified");
196
259
  let end = address;
197
260
  const same = (at: number) => {
198
- if (at > piece.end) return false;
261
+ if (at > piece.end || cuts.has(at)) return false;
199
262
  const next = regionAt(at);
200
263
  if (region !== undefined && !codeRegion) return next === region;
201
264
  if (codeRegion) return next === region && !input.instructions.has(at);
@@ -203,7 +266,7 @@ export function layout(input: SourceInput): Layout {
203
266
  };
204
267
  while (same(end + 1)) end++;
205
268
  const last = piece.items.at(-1);
206
- if (last?.kind === "data" && last.type === type && last.region === region && last.end + 1 === address) last.end = end;
269
+ if (last?.kind === "data" && last.type === type && last.region === region && last.end + 1 === address && !cuts.has(address)) last.end = end;
207
270
  else piece.items.push({ kind: "data", start: address, end, type, ...(region === undefined ? {} : { region }) });
208
271
  address = end + 1;
209
272
  }
@@ -221,13 +284,20 @@ export function layout(input: SourceInput): Layout {
221
284
  const symbols = new Map(input.symbols.filter((symbol) => !(isAnalyzer(symbol.origin) && platform.has(symbol.address))).map((symbol) => [symbol.address, symbol.name]));
222
285
  /** Addresses outside the image without a name of their own, inside a named platform entry of more bytes: the start of that entry (F-20). */
223
286
  const insideEntry = new Map<number, number>();
224
- const use = (address: number) => {
287
+ const uses = new Map<number, Map<UseKind, number>>();
288
+ const count = (constant: number, kind: UseKind) => {
289
+ outside.add(constant);
290
+ const kinds = uses.get(constant) ?? new Map<UseKind, number>();
291
+ kinds.set(kind, (kinds.get(kind) ?? 0) + 1);
292
+ uses.set(constant, kinds);
293
+ };
294
+ const use = (address: number, kind: UseKind) => {
225
295
  if (inImage(address)) wanted.add(address);
226
- else if (symbols.has(address) || platform.has(address)) outside.add(address);
296
+ else if (symbols.has(address) || platform.has(address)) count(address, kind);
227
297
  else {
228
298
  const entry = input.platform?.find((row) => address > row.address && address <= row.end && !inImage(row.address) && (symbols.has(row.address) || platform.has(row.address)));
229
299
  if (entry === undefined) return;
230
- outside.add(entry.address);
300
+ count(entry.address, kind);
231
301
  insideEntry.set(address, entry.address);
232
302
  }
233
303
  };
@@ -236,7 +306,43 @@ export function layout(input: SourceInput): Layout {
236
306
  for (const row of input.platform ?? []) if (!byName.has(row.name)) byName.set(row.name, row.address);
237
307
  for (const symbol of input.symbols) byName.set(symbol.name, symbol.address);
238
308
  const instructionAt = new Map(pieces.flatMap((piece) => piece.items.flatMap((item) => (item.kind === "instruction" ? [[item.address, item] as const] : []))));
239
- const expressions: Array<{ address: number; text: string }> = [];
309
+ // A macro use is a run of whole instructions of one file, of a macro with one param for each arg, whose args name known names (D73).
310
+ const macrosByName = new Map((input.macros ?? []).map((macro) => [macro.name, macro]));
311
+ const macrosAsInstructions: MacroAsInstructions[] = [];
312
+ const candidates: Array<{ use: (typeof macroUses)[number]; items: InstructionItem[] }> = [];
313
+ for (const use of macroUses) {
314
+ const fallBack = (reason: string) => macrosAsInstructions.push({ start: use.start, end: use.end, macro: use.macro, reason });
315
+ const macro = macrosByName.get(use.macro);
316
+ if (macro === undefined) {
317
+ fallBack(`There is no macro ${use.macro}.`);
318
+ continue;
319
+ }
320
+ if (macro.params.length !== use.args.length) {
321
+ fallBack(`The macro has ${macro.params.length} params, and the use gives ${use.args.length} args.`);
322
+ continue;
323
+ }
324
+ if (patches.some((patch) => patch.start <= use.end && use.start <= patch.end)) {
325
+ fallBack("The use lies in a patch.");
326
+ continue;
327
+ }
328
+ const piece = pieces.find((row) => use.start >= row.start && use.start <= row.end);
329
+ const run = piece?.items.filter((item) => (item.kind === "instruction" ? item.address : item.start) >= use.start && (item.kind === "instruction" ? item.address : item.start) <= use.end) ?? [];
330
+ const last = run.at(-1);
331
+ const whole = last?.kind === "instruction" && run.every((item) => item.kind === "instruction") && (run[0] as InstructionItem).address === use.start && last.address + last.length - 1 === use.end;
332
+ if (!whole) {
333
+ fallBack("The range is not a run of whole instructions of one file.");
334
+ continue;
335
+ }
336
+ const missing = [...new Set(use.args.flatMap((arg) => identifiers(arg)))].filter((name) => !byName.has(name));
337
+ if (missing.length > 0) {
338
+ fallBack(`No symbol and no platform name is ${missing.join(", ")}.`);
339
+ continue;
340
+ }
341
+ candidates.push({ use, items: run as InstructionItem[] });
342
+ }
343
+ /** The instructions of a written use name nothing: the macro writes them. */
344
+ const inUse = new Set(candidates.flatMap((candidate) => candidate.items.map((item) => item.address)));
345
+ const expressions: Array<{ address: number; text: string; kind: UseKind }> = [];
240
346
  const operandsAsNumbers: OperandAsNumber[] = [];
241
347
  for (const operand of [...(input.operands ?? [])].sort((a, b) => a.address - b.address)) {
242
348
  const expression = operandText(operand);
@@ -251,23 +357,41 @@ export function layout(input: SourceInput): Layout {
251
357
  operandsAsNumbers.push({ address: operand.address, expression, reason: `No symbol and no platform name is ${missing.join(", ")}.` });
252
358
  continue;
253
359
  }
254
- expressions.push({ address: operand.address, text: expression });
360
+ // An immediate operand builds the address of a name; any other operand uses the address as the instruction does.
361
+ expressions.push({ address: operand.address, text: expression, kind: mode === "imm" ? "address" : useKind(item!.opcode) });
255
362
  }
256
- for (const { text: expression } of expressions) for (const name of identifiers(expression)) use(byName.get(name)!);
363
+ for (const { text: expression, kind } of expressions) for (const name of identifiers(expression)) use(byName.get(name)!, kind);
257
364
  // An instruction with an expression names what the expression names, not its plain target (D65).
258
365
  const written = new Set(expressions.map((expression) => expression.address));
259
366
  for (const piece of pieces) {
260
367
  for (const item of piece.items) {
261
368
  if (item.kind === "instruction") {
262
- if (takesAddress(OPCODES[item.opcode]!.mode) && !written.has(item.address)) use(operandValue(item.address, item.opcode, input.byteAt)!);
369
+ if (takesAddress(OPCODES[item.opcode]!.mode) && !written.has(item.address) && !inUse.has(item.address)) use(operandValue(item.address, item.opcode, input.byteAt)!, useKind(item.opcode));
263
370
  continue;
264
371
  }
265
- for (const pointer of pointersOf(item, input.byteAt)) use(pointer);
372
+ for (const pointer of pointersOf(item, input.byteAt)) use(pointer, "table");
266
373
  }
267
374
  }
375
+ for (const candidate of candidates) for (const arg of candidate.use.args) for (const name of identifiers(arg)) use(byName.get(name)!, "macro argument");
268
376
  for (const symbol of input.symbols) want(symbol.address);
269
377
  for (const target of input.referenceTargets) want(target);
270
378
  if (input.entry !== undefined) want(input.entry);
379
+ // A use with a label inside it is written as its instructions, which then name their targets: that can put a label inside another use.
380
+ const accepted = new Set(candidates);
381
+ for (let changed = true; changed; ) {
382
+ changed = false;
383
+ for (const candidate of [...accepted]) {
384
+ const inside = [...wanted].filter((address) => address > candidate.use.start && address <= candidate.use.end).sort((a, b) => a - b)[0];
385
+ if (inside === undefined) continue;
386
+ accepted.delete(candidate);
387
+ changed = true;
388
+ macrosAsInstructions.push({ start: candidate.use.start, end: candidate.use.end, macro: candidate.use.macro, reason: `The source has a label at ${hex4(inside)}, inside the use.` });
389
+ for (const item of candidate.items) {
390
+ inUse.delete(item.address);
391
+ if (takesAddress(OPCODES[item.opcode]!.mode) && !written.has(item.address)) use(operandValue(item.address, item.opcode, input.byteAt)!, useKind(item.opcode));
392
+ }
393
+ }
394
+ }
271
395
 
272
396
  const names = new Map<number, string>();
273
397
  const taken = new Set([...knowledgeNames, ...platform.values()]);
@@ -293,8 +417,44 @@ export function layout(input: SourceInput): Layout {
293
417
  names.set(address, name);
294
418
  generatedLabels++;
295
419
  }
420
+ // A patch is written when its range is whole items of one file and its switch is no name of the source (D72).
421
+ const blocks = new Map<number, PatchBlock>();
422
+ const patchesNotWritten: PatchNotWritten[] = [];
423
+ const sourceNames = new Set([...names.values(), ...taken]);
424
+ for (const patch of patches) {
425
+ const refuse = (reason: string) => patchesNotWritten.push({ start: patch.start, end: patch.end, switch: patch.switch, reason });
426
+ const piece = pieces.find((row) => patch.start >= row.start && patch.start <= row.end);
427
+ if (piece === undefined) {
428
+ refuse("The range is not in the image.");
429
+ continue;
430
+ }
431
+ if (patch.end > piece.end) {
432
+ refuse(`The range runs past the end of the file ${piece.file} at ${hex4(piece.end)}: a patch is in one file.`);
433
+ continue;
434
+ }
435
+ const starts = new Set(piece.items.map((item) => (item.kind === "instruction" ? item.address : item.start)));
436
+ const ends = new Set(piece.items.map((item) => (item.kind === "instruction" ? item.address + item.length - 1 : item.end)));
437
+ if (!starts.has(patch.start) || !ends.has(patch.end)) {
438
+ const cut = piece.items.find((item) => item.kind === "instruction" && ((patch.start > item.address && patch.start < item.address + item.length) || (patch.end >= item.address && patch.end < item.address + item.length - 1)));
439
+ refuse(cut === undefined ? "The range does not start and end at an item of the source." : `The range cuts the instruction at ${hex4((cut as InstructionItem).address)}.`);
440
+ continue;
441
+ }
442
+ if (sourceNames.has(patch.switch)) {
443
+ refuse(`The switch name ${patch.switch} is also a name of the source.`);
444
+ continue;
445
+ }
446
+ const inner = [...names.keys()].filter((address) => address > patch.start && address <= patch.end).sort((a, b) => a - b);
447
+ blocks.set(patch.start, { start: patch.start, end: patch.end, switch: patch.switch, code: patch.code, ...(patch.comment === undefined ? {} : { comment: patch.comment }), inner });
448
+ }
296
449
  // The high byte of a vector is NMINV+1. A zero-page entry keeps zero-page instructions: its constant has two digits.
297
450
  for (const [address, start] of insideEntry) names.set(address, `${names.get(start)!}+${address - start}`);
451
+ /** An expression with the names of this source: a name with an offset in brackets, so an operator after it keeps its meaning. */
452
+ const sourceText = (text: string) => replaceIdentifiers(text, (name) => {
453
+ const label = names.get(byName.get(name)!)!;
454
+ return label.includes("+") ? `(${label})` : label;
455
+ });
456
+ const usesWritten = new Map<number, MacroUseBlock>();
457
+ for (const { use: row } of [...accepted].sort((a, b) => a.use.start - b.use.start)) usesWritten.set(row.start, { start: row.start, end: row.end, macro: row.macro, args: row.args.map(sourceText) });
298
458
  const operands = new Map<number, string>();
299
459
  for (const { address, text } of expressions) {
300
460
  const unnamed: string[] = [];
@@ -310,6 +470,9 @@ export function layout(input: SourceInput): Layout {
310
470
  pieces,
311
471
  names,
312
472
  constants: [...outside].sort((a, b) => a - b),
473
+ uses,
474
+ platform: input.platform ?? [],
475
+ ...(input.describe === undefined ? {} : { describe: input.describe }),
313
476
  inImage,
314
477
  byteAt: input.byteAt,
315
478
  comments: input.comments,
@@ -319,6 +482,11 @@ export function layout(input: SourceInput): Layout {
319
482
  unusedModules,
320
483
  operands,
321
484
  operandsAsNumbers: operandsAsNumbers.sort((a, b) => a.address - b.address),
485
+ patches: blocks,
486
+ patchesNotWritten,
487
+ macros: [...(input.macros ?? [])].sort((a, b) => (a.name < b.name ? -1 : 1)),
488
+ macroUses: usesWritten,
489
+ macrosAsInstructions: macrosAsInstructions.sort((a, b) => a.start - b.start),
322
490
  };
323
491
  }
324
492
 
@@ -539,6 +707,83 @@ function dataLines(item: DataItem, layout: Layout, breaks: Set<number>): Array<{
539
707
  return lines;
540
708
  }
541
709
 
710
+ // --- Constants --------------------------------------------------------------
711
+
712
+ /** A longer name moves only its own comment, not the columns of the file. */
713
+ const MAX_NAME_COLUMN = 24;
714
+
715
+ /** What an address outside the image is: the comment of the knowledge, else the platform meaning, else the register that a mirror repeats. */
716
+ function describeConstant(layout: Layout, address: number): string | undefined {
717
+ const comments = layout.comments.filter((comment) => comment.address === address);
718
+ // Line comments come before the side comment, as they do in the code.
719
+ const own = [...comments.filter((comment) => comment.placement === "line"), ...comments.filter((comment) => comment.placement === "side")];
720
+ if (own.length > 0) return own.map((comment) => comment.text.replace(/\s*\r?\n\s*/g, " ").trim()).join(" ");
721
+ const entry = layout.platform.find((row) => address >= row.address && address <= row.end);
722
+ if (entry?.meaning !== undefined) {
723
+ const platformName = address === entry.address ? entry.name : `${entry.name}+${address - entry.address}`;
724
+ return platformName === layout.names.get(address) ? entry.meaning : `${platformName}: ${entry.meaning}`;
725
+ }
726
+ const mirrorOf = layout.describe?.(address).mirrorOf;
727
+ if (mirrorOf === undefined) return undefined;
728
+ const register = layout.platform.find((row) => row.address === mirrorOf);
729
+ return `Mirror of ${hex4(mirrorOf)}${register === undefined ? "" : ` (${register.name})`}.`;
730
+ }
731
+
732
+ /** How the source uses a constant, for example "Used: read 4, written 1." */
733
+ function usageText(kinds: ReadonlyMap<UseKind, number> | undefined): string | undefined {
734
+ const parts = USE_KINDS.filter((kind) => kinds?.has(kind)).map((kind) => `${kind} ${kinds!.get(kind)!}`);
735
+ return parts.length === 0 ? undefined : `Used: ${parts.join(", ")}.`;
736
+ }
737
+
738
+ /** The switches of the patches (D72), each 0 unless the build defines it, with the ranges of its patches. */
739
+ export function switchesOf(layout: Pick<Layout, "patches">): Map<string, PatchBlock[]> {
740
+ const switches = new Map<string, PatchBlock[]>();
741
+ for (const patch of [...layout.patches.values()].sort((a, b) => a.start - b.start)) switches.set(patch.switch, [...(switches.get(patch.switch) ?? []), patch]);
742
+ return new Map([...switches].sort(([a], [b]) => (a < b ? -1 : 1)));
743
+ }
744
+
745
+ function switchLines(layout: Layout): string[] {
746
+ const switches = switchesOf(layout);
747
+ if (switches.size === 0) return [];
748
+ const width = Math.max(...[...switches.keys()].map((name) => name.length));
749
+ const lines = ["; Switches: each is 0 unless the build defines it. Assemble with -DNAME=1", "; (c64-assembler --define NAME=1) to put in the patches of NAME."];
750
+ for (const [name, patches] of switches) {
751
+ const ranges = patches.map((patch) => `${hex4(patch.start)}-${hex4(patch.end)}`).join(", ");
752
+ lines.push(`!ifndef ${name} { ${name} = 0 }${" ".repeat(width - name.length)} ; ${patches.length} patch${patches.length === 1 ? "" : "es"}: ${ranges}`);
753
+ }
754
+ return [...lines, ""];
755
+ }
756
+
757
+ /** constants.asm: the names outside the image by platform area, each with what it is and how the source uses it. */
758
+ function constantsText(layout: Layout): string {
759
+ const { describe } = layout;
760
+ const rows = layout.constants.map((address) => {
761
+ const comment = [describeConstant(layout, address), usageText(layout.uses.get(address))].filter((text) => text !== undefined).join(" ");
762
+ return { address, name: layout.names.get(address)!, value: constantValue(address), comment };
763
+ });
764
+ const nameColumn = Math.min(MAX_NAME_COLUMN, Math.max(0, ...rows.map((row) => row.name.length)));
765
+ const valueColumn = Math.max(0, ...rows.map((row) => row.value.length));
766
+ const out = [...switchLines(layout)];
767
+ out.push(
768
+ describe === undefined ? "; Names of addresses outside the image." : "; Names of addresses outside the image, by memory area.",
769
+ '; "Used" counts the instructions and table entries of this source that use the name.',
770
+ );
771
+ // Without platform areas the names form one group.
772
+ if (describe === undefined && rows.length > 0) out.push("");
773
+ let area: Area | undefined;
774
+ for (const row of rows) {
775
+ const next = describe?.(row.address).area;
776
+ if (next !== undefined && next.start !== area?.start) {
777
+ area = next;
778
+ out.push("", `; ${area.name[0]!.toUpperCase()}${area.name.slice(1)} ${hex4(area.start)}-${hex4(area.end)}`);
779
+ if (area.meaning !== undefined) out.push(`; ${area.meaning}`);
780
+ }
781
+ const line = `${row.name.padEnd(nameColumn)} = ${row.value.padEnd(valueColumn)}`;
782
+ out.push(row.comment === "" ? line.trimEnd() : `${line} ; ${row.comment}`);
783
+ }
784
+ return `${out.join("\n")}\n`;
785
+ }
786
+
542
787
  // --- Rendering --------------------------------------------------------------
543
788
 
544
789
  export interface Rendered {
@@ -554,6 +799,45 @@ export interface Rendered {
554
799
  expressions: Array<{ address: number; length: number; file: string; line: number }>;
555
800
  /** Operand expressions written as numbers, in address order (D65). */
556
801
  operandsAsNumbers: OperandAsNumber[];
802
+ /** The patches written, in address order (D72). */
803
+ patches: number;
804
+ /** Each macro use written: its file and line (1 is the first line of the text) (D73). */
805
+ uses: Array<{ start: number; end: number; macro: string; file: string; line: number }>;
806
+ /** The macro uses written as instructions, in address order, with the reason. */
807
+ macrosAsInstructions: MacroAsInstructions[];
808
+ /** The lines of each macro in macros.asm, from its !macro line to its closing brace. */
809
+ macroLines: Map<string, { first: number; last: number }>;
810
+ }
811
+
812
+ /** macros.asm (D73): each macro with its comment, as ACME reads it; a param is .name in the body. */
813
+ function macrosText(layout: Layout): { text: string; lines: Map<string, { first: number; last: number }> } {
814
+ const out = ["; Macros of the knowledge. The source writes a use as +name args."];
815
+ const lines = new Map<string, { first: number; last: number }>();
816
+ for (const macro of layout.macros) {
817
+ out.push("");
818
+ if (macro.comment !== undefined) out.push(...macro.comment.split(/\r?\n/).map((line) => `; ${line}`.trimEnd()));
819
+ const first = out.length + 1;
820
+ out.push(`!macro ${macro.name}${macro.params.map((param, index) => `${index === 0 ? " " : ", "}.${param}`).join("")} {`, ...macro.body.split(/\r?\n/), "}");
821
+ lines.set(macro.name, { first, last: out.length });
822
+ }
823
+ return { text: `${out.join("\n")}\n`, lines };
824
+ }
825
+
826
+ /** The lines that open a patch (D72): its zone, the switch, the code, and the guard that stops a patch longer than its range. */
827
+ function patchOpening(patch: PatchBlock): string[] {
828
+ const bytes = patch.end - patch.start + 1;
829
+ const range = `${hex4(patch.start)}-${hex4(patch.end)}`;
830
+ const comment = patch.comment === undefined ? [] : patch.comment.split(/\r?\n/).map((line) => `${INDENT}; ${line}`.trimEnd());
831
+ return [
832
+ `!zone patch_${patch.switch}_${patch.start.toString(16).padStart(4, "0")} {`,
833
+ ".patch_start",
834
+ `!if ${patch.switch} {`,
835
+ `${INDENT}; ${patch.switch} on: ${bytes} byte${bytes === 1 ? "" : "s"} at ${range}, NOP after the code`,
836
+ ...comment,
837
+ ...(patch.code === "" ? [] : patch.code.split(/\r?\n/)),
838
+ `${INDENT}!if * - .patch_start > ${bytes} { !error "The patch ${patch.switch} at ${hex4(patch.start)} has ", * - .patch_start, " bytes; the range has ${bytes}." } else { !fill ${bytes} - (* - .patch_start), $ea }`,
839
+ "} else {",
840
+ ];
557
841
  }
558
842
 
559
843
  const withSide = (text: string, side: string | undefined) => (side === undefined ? text : `${text.padEnd(SIDE_COLUMN - 1)} ; ${side}`);
@@ -563,7 +847,7 @@ const withSide = (text: string, side: string | undefined) => (side === undefined
563
847
  * holds the opcode form keys that the probe refused; `numbers` the operand
564
848
  * expressions that ACME did not assemble to the image, with the reason.
565
849
  */
566
- export function render(layout: Layout, asBytes: ReadonlySet<string>, header: string[], numbers: ReadonlyMap<number, string> = new Map()): Rendered {
850
+ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: string[], numbers: ReadonlyMap<number, string> = new Map(), asInstructions: ReadonlyMap<number, string> = new Map()): Rendered {
567
851
  const lineComments = new Map<number, string[]>();
568
852
  const sideComments = new Map<number, string>();
569
853
  for (const comment of layout.comments) {
@@ -572,7 +856,24 @@ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: str
572
856
  else sideComments.set(comment.address, comment.text.replace(/\s*\r?\n\s*/g, " "));
573
857
  }
574
858
  const breaks = new Set<number>([...layout.names.keys(), ...lineComments.keys(), ...sideComments.keys()]);
575
- const result: Rendered = { files: [], instructions: 0, dataBytes: 0, undecodedBytes: 0, asBytes: [], expressions: [], operandsAsNumbers: [...layout.operandsAsNumbers] };
859
+ const result: Rendered = {
860
+ files: [],
861
+ instructions: 0,
862
+ dataBytes: 0,
863
+ undecodedBytes: 0,
864
+ asBytes: [],
865
+ expressions: [],
866
+ operandsAsNumbers: [...layout.operandsAsNumbers],
867
+ patches: 0,
868
+ uses: [],
869
+ macrosAsInstructions: [...layout.macrosAsInstructions],
870
+ macroLines: new Map(),
871
+ };
872
+ for (const [start, reason] of asInstructions) {
873
+ const use = layout.macroUses.get(start);
874
+ if (use !== undefined) result.macrosAsInstructions.push({ start, end: use.end, macro: use.macro, reason });
875
+ }
876
+ result.macrosAsInstructions.sort((a, b) => a.start - b.start);
576
877
  const unwritten = new Map(layout.operandsAsNumbers.map((row) => [row.address, row.expression]));
577
878
  const { coverage } = layout;
578
879
  const notExecuted = { instructions: 0, ranges: [] as Range[] };
@@ -583,34 +884,79 @@ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: str
583
884
  let running = false;
584
885
  /** The line of the mark of the open range of instructions that did not execute. */
585
886
  let markLine = 0;
887
+ /** The open patch: its labels are defined before it, so the original code writes none (D72). */
888
+ let patch: PatchBlock | undefined;
889
+ /** The last address of the macro use just written: its instructions are in the use line (D73). */
890
+ let useEnd = -1;
891
+ /** Marks the start of a range of instructions that no run executed, or makes the open range longer. */
892
+ const mark = (address: number, last: number, count: number) => {
893
+ const executed = coverage === undefined || (coverage[address]! & COVERAGE.ramExecute) !== 0;
894
+ if (!executed) {
895
+ if (running) notExecuted.ranges.at(-1)!.end = last;
896
+ else {
897
+ notExecuted.ranges.push({ start: address, end: last });
898
+ markLine = out.length;
899
+ out.push("");
900
+ }
901
+ // The mark gives the range, so a reader finds it in the full list (D49).
902
+ const range = notExecuted.ranges.at(-1)!;
903
+ out[markLine] = `; not executed ${hex4(range.start)}-${hex4(range.end)}`;
904
+ notExecuted.instructions += count;
905
+ }
906
+ running = !executed;
907
+ };
586
908
  const head = (address: number) => {
909
+ if (patch?.start === address) return;
587
910
  for (const text of lineComments.get(address) ?? []) out.push(`; ${text}`.trimEnd());
588
911
  const label = layout.names.get(address);
589
- if (label !== undefined) out.push(label);
912
+ if (label !== undefined && patch === undefined) out.push(label);
590
913
  };
591
914
  for (const item of piece.items) {
915
+ const itemStart = item.kind === "instruction" ? item.address : item.start;
916
+ const opened = layout.patches.get(itemStart);
917
+ if (opened !== undefined) {
918
+ head(itemStart);
919
+ for (const address of opened.inner) out.push(`${layout.names.get(address)!} = * + ${address - opened.start}`);
920
+ out.push(...patchOpening(opened));
921
+ patch = opened;
922
+ result.patches++;
923
+ }
924
+ const close = () => {
925
+ const itemEnd = item.kind === "instruction" ? item.address + item.length - 1 : item.end;
926
+ if (patch === undefined || itemEnd !== patch.end) return;
927
+ out.push("}", "}");
928
+ patch = undefined;
929
+ };
930
+ if (item.kind === "instruction" && item.address <= useEnd) {
931
+ result.instructions++;
932
+ continue;
933
+ }
934
+ const macroUse = item.kind === "instruction" && !asInstructions.has(item.address) ? layout.macroUses.get(item.address) : undefined;
935
+ if (macroUse !== undefined) {
936
+ head(macroUse.start);
937
+ const count = piece.items.filter((other) => other.kind === "instruction" && other.address >= macroUse.start && other.address <= macroUse.end).length;
938
+ mark(macroUse.start, macroUse.end, count);
939
+ // The comments inside the use go to its side comment, as for the bytes of an instruction.
940
+ const sides: string[] = [];
941
+ for (let address = macroUse.start; address <= macroUse.end; address++) {
942
+ if (address !== macroUse.start) for (const text of lineComments.get(address) ?? []) sides.push(text);
943
+ if (sideComments.has(address)) sides.push(sideComments.get(address)!);
944
+ }
945
+ out.push(withSide(`${INDENT}+${macroUse.macro}${macroUse.args.length === 0 ? "" : ` ${macroUse.args.join(", ")}`}`, sides.length === 0 ? undefined : sides.join(" ")));
946
+ result.uses.push({ start: macroUse.start, end: macroUse.end, macro: macroUse.macro, file: piece.file, line: out.length });
947
+ result.instructions++;
948
+ useEnd = macroUse.end;
949
+ continue;
950
+ }
592
951
  if (item.kind === "instruction") {
593
952
  head(item.address);
594
- const executed = coverage === undefined || (coverage[item.address]! & COVERAGE.ramExecute) !== 0;
595
- if (!executed) {
596
- if (running) notExecuted.ranges.at(-1)!.end = item.address + item.length - 1;
597
- else {
598
- notExecuted.ranges.push({ start: item.address, end: item.address + item.length - 1 });
599
- markLine = out.length;
600
- out.push("");
601
- }
602
- // The mark gives the range, so a reader finds it in the full list (D49).
603
- const range = notExecuted.ranges.at(-1)!;
604
- out[markLine] = `; not executed ${hex4(range.start)}-${hex4(range.end)}`;
605
- notExecuted.instructions++;
606
- }
607
- running = !executed;
953
+ mark(item.address, item.address + item.length - 1, 1);
608
954
  const sides: string[] = [];
609
955
  if (sideComments.has(item.address)) sides.push(sideComments.get(item.address)!);
610
956
  for (let offset = 1; offset < item.length; offset++) {
611
957
  const inside = item.address + offset;
612
958
  const label = layout.names.get(inside);
613
- if (label !== undefined) out.push(`${label} = * + ${offset}`);
959
+ if (label !== undefined && patch === undefined) out.push(`${label} = * + ${offset}`);
614
960
  for (const text of lineComments.get(inside) ?? []) sides.push(text);
615
961
  if (sideComments.has(inside)) sides.push(sideComments.get(inside)!);
616
962
  }
@@ -642,6 +988,7 @@ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: str
642
988
  out.push(withSide(`${INDENT}${instructionText(item.opcode, form.suffix, expression)}`, sides.length === 0 ? undefined : sides.join(" ")));
643
989
  }
644
990
  result.instructions++;
991
+ close();
645
992
  continue;
646
993
  }
647
994
  running = false;
@@ -655,13 +1002,18 @@ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: str
655
1002
  const bytes = item.end - item.start + 1;
656
1003
  if (item.type === "undecoded") result.undecodedBytes += bytes;
657
1004
  else result.dataBytes += bytes;
1005
+ close();
658
1006
  }
659
1007
  result.files.push({ name: piece.file, text: `${out.join("\n")}\n` });
660
1008
  }
661
1009
 
662
- const constants = ["; Names outside the image.", ...layout.constants.map((address) => `${layout.names.get(address)} = ${constantValue(address)}`)];
663
- result.files.push({ name: "constants.asm", text: `${constants.join("\n")}\n` });
664
- const main = [...header.map((line) => `; ${line}`), "", "!cpu 6510", '!source "constants.asm"'];
1010
+ result.files.push({ name: "constants.asm", text: constantsText(layout) });
1011
+ if (layout.macros.length > 0) {
1012
+ const macros = macrosText(layout);
1013
+ result.files.push({ name: "macros.asm", text: macros.text });
1014
+ result.macroLines = macros.lines;
1015
+ }
1016
+ const main = [...header.map((line) => `; ${line}`), "", "!cpu 6510", '!source "constants.asm"', ...(layout.macros.length > 0 ? ['!source "macros.asm"'] : [])];
665
1017
  for (const piece of layout.pieces) {
666
1018
  if (piece.segmentStart) main.push("", `* = ${hex4(piece.start)}`);
667
1019
  main.push(`!source "${piece.file}"`);
@@ -11,13 +11,13 @@ Find the static structure of a C64 program. Record the structure in the project
11
11
 
12
12
  ## Inputs
13
13
 
14
- - The program: a PRG file, relative to the project directory. Or 64 KiB of memory from `$0000`, for example a memory dump.
14
+ - The program: a PRG file, relative to the project folder. Or 64 KiB of memory from `$0000`, for example a memory dump.
15
15
  - Entry points: addresses where code starts, for example the SYS address from the c64-basic skill. Routine symbols in the knowledge are entry points too. Ghidra needs at least one. DXA finds the SYS of a BASIC start by itself.
16
16
  - Optional: routines to decompile (Ghidra), or a file for the listing (DXA).
17
17
 
18
18
  ## Tools / execution path
19
19
 
20
- The script reads the current knowledge and runs DXA or Ghidra on this machine. The host runtime is not necessary. The script records the result in `.c64-re-tools/knowledge.db`.
20
+ The script reads the current knowledge and runs DXA or Ghidra on this machine. The host runtime is not necessary. The script records the result in the project knowledge, `.c64-re-tools/knowledge/`.
21
21
 
22
22
  ```
23
23
  node <skill>/scripts/analyze.ts extracted/game.prg --analyzer dxa --listing analysis/game.lst
@@ -35,6 +35,7 @@ node <skill>/scripts/analyze.ts extracted/game.prg --entry '$080d' --decompile '
35
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
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
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. |
38
+ | `--macro-batch .c64-re-tools/local/macros.json` | Write the macro candidates to this project file as a batch for `apply` of the c64-knowledge skill. |
38
39
 
39
40
  The script prints one JSON object. Addresses are `$` and four hex digits.
40
41
 
@@ -53,6 +54,13 @@ More seeds come from evidence:
53
54
 
54
55
  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
56
 
57
+ ### Project folder
58
+
59
+ - Paths are relative to the project folder. The project folder is the nearest folder at or above the working directory that holds `.c64-re-tools/`.
60
+ - If the working directory is not in a project and has projects below it, the script refuses and lists them. Then give `--project <folder>` to each script.
61
+ - Keep the results, batches and step files that you save for yourself in `.c64-re-tools/local/` of the project. Git ignores that folder.
62
+ - A shell redirect (`>`) is relative to the working directory, not to the project folder. From the folder above the project, write `<folder>/.c64-re-tools/local/result.json`.
63
+
56
64
  ## Workflow
57
65
 
58
66
  1. Read the knowledge for the program with the c64-knowledge skill.
@@ -67,6 +75,7 @@ The analyzers take at most 1024 entry points. Knowledge routines and `--entry` c
67
75
  - `conflicts` lists the places where the analyzer and the current knowledge disagree.
68
76
  - `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 (`dop`) and opcodes that the program wrote and executed (`selfModified`). For `selfModified`, the write and the execution must be in one coverage file. A coverage file does not record the order. Thus a candidate can also be code that ran first, and that the program then wrote over. To prevent this, save one coverage file for each part of the program, for example the intro and the game. Clear the memory map between the parts. Each list has a `count`. The script does not write the candidates. Look at them and decide.
69
77
  - `candidates.pointers` gives code that makes an address from immediates. A `vector` or a `zero-page pointer` candidate stores a low byte (`low`) and a high byte (`high`) to a vector or to a zero-page pair that `(zp),y` uses. Its `target` is in the image or has a platform name. A `sprite pointer` candidate stores an immediate (`at`) to a sprite pointer of the screen at $0400 in VIC bank 0. Its `target` (the value × 64) is in a `sprite` region. For each candidate that you confirm, write operand expressions with `operand` of the c64-knowledge skill, for example `operand '<low>' --lo <target name>` and `operand '<high>' --hi <target name>`. Then a rebuild can move the target.
78
+ - `candidates.macros` gives runs of instructions that a macro writes as one line, for example `+inc16 ptr`. The script knows these 6502 idioms: `inc16`, `dec16`, `add16_8`, `add16`, `sub16`, `set16`, `mov16` and `wait_line`. It reads the code regions after the analysis. An arg has the name of a `user` or `llm` symbol or a platform name. Each candidate has `macro`, `start`, `end`, `args` and `text`. `known` counts the uses that the knowledge has. `leftOut` counts the candidates with a label inside, or with a patch or a use over them. The c64-source skill writes such a candidate as instructions. With `--macro-batch`, `batch` gives the file, the count of writes and the `apply` command. The batch holds the macros that the knowledge does not have, and the uses. When the knowledge has a macro with the same name and another body, `otherMacros` tells that the batch does not have its uses. The script does not write the candidates. Apply the batch, then generate the source again.
70
79
  - With `--coverage`, a code finding replaces the DXA or Ghidra data where a run executed an instruction start. `coverageConflicts` lists the analyzer data regions where the coverage still shows executed code. Classify them as code with the c64-knowledge skill, or as `dual-use` when the bytes are also data.
71
80
  6. Give each important routine a name with the c64-knowledge skill. Then run the analysis again. The analyzers use the new names.
72
81