@henols/c64-re-tools 2.0.0-rc.12 → 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.
@@ -4,7 +4,9 @@
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";
@@ -42,6 +44,47 @@ export interface SourceInput {
42
44
  describe?: (address: number) => { area: Area; mirrorOf?: number };
43
45
  /** Operand expressions of the knowledge (D65), by instruction address. */
44
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;
45
88
  }
46
89
 
47
90
  /** An operand expression that the source writes as a number, with its text and why. */
@@ -110,6 +153,14 @@ export interface Layout {
110
153
  operands: Map<number, string>;
111
154
  /** Operand expressions that cannot be written. */
112
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[];
113
164
  }
114
165
 
115
166
  const hex2 = (value: number) => `$${value.toString(16).padStart(2, "0")}`;
@@ -160,6 +211,11 @@ export function layout(input: SourceInput): Layout {
160
211
  const modules = [...input.modules].sort((a, b) => a.start - b.start);
161
212
  const unusedModules = modules.filter((module) => !inImage(module.start)).map(({ start, file }) => ({ start, file }));
162
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
+
163
219
  const pieces: Piece[] = [];
164
220
  for (const segment of input.segments) {
165
221
  const starts = modules.filter((module) => module.start > segment.start && module.start <= segment.end).map((module) => module.start);
@@ -202,7 +258,7 @@ export function layout(input: SourceInput): Layout {
202
258
  const type: DataItem["type"] = codeRegion ? "undecoded" : (region?.type ?? "unclassified");
203
259
  let end = address;
204
260
  const same = (at: number) => {
205
- if (at > piece.end) return false;
261
+ if (at > piece.end || cuts.has(at)) return false;
206
262
  const next = regionAt(at);
207
263
  if (region !== undefined && !codeRegion) return next === region;
208
264
  if (codeRegion) return next === region && !input.instructions.has(at);
@@ -210,7 +266,7 @@ export function layout(input: SourceInput): Layout {
210
266
  };
211
267
  while (same(end + 1)) end++;
212
268
  const last = piece.items.at(-1);
213
- 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;
214
270
  else piece.items.push({ kind: "data", start: address, end, type, ...(region === undefined ? {} : { region }) });
215
271
  address = end + 1;
216
272
  }
@@ -250,6 +306,42 @@ export function layout(input: SourceInput): Layout {
250
306
  for (const row of input.platform ?? []) if (!byName.has(row.name)) byName.set(row.name, row.address);
251
307
  for (const symbol of input.symbols) byName.set(symbol.name, symbol.address);
252
308
  const instructionAt = new Map(pieces.flatMap((piece) => piece.items.flatMap((item) => (item.kind === "instruction" ? [[item.address, item] as const] : []))));
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)));
253
345
  const expressions: Array<{ address: number; text: string; kind: UseKind }> = [];
254
346
  const operandsAsNumbers: OperandAsNumber[] = [];
255
347
  for (const operand of [...(input.operands ?? [])].sort((a, b) => a.address - b.address)) {
@@ -274,15 +366,32 @@ export function layout(input: SourceInput): Layout {
274
366
  for (const piece of pieces) {
275
367
  for (const item of piece.items) {
276
368
  if (item.kind === "instruction") {
277
- if (takesAddress(OPCODES[item.opcode]!.mode) && !written.has(item.address)) use(operandValue(item.address, item.opcode, input.byteAt)!, useKind(item.opcode));
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));
278
370
  continue;
279
371
  }
280
372
  for (const pointer of pointersOf(item, input.byteAt)) use(pointer, "table");
281
373
  }
282
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");
283
376
  for (const symbol of input.symbols) want(symbol.address);
284
377
  for (const target of input.referenceTargets) want(target);
285
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
+ }
286
395
 
287
396
  const names = new Map<number, string>();
288
397
  const taken = new Set([...knowledgeNames, ...platform.values()]);
@@ -308,8 +417,44 @@ export function layout(input: SourceInput): Layout {
308
417
  names.set(address, name);
309
418
  generatedLabels++;
310
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
+ }
311
449
  // The high byte of a vector is NMINV+1. A zero-page entry keeps zero-page instructions: its constant has two digits.
312
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) });
313
458
  const operands = new Map<number, string>();
314
459
  for (const { address, text } of expressions) {
315
460
  const unnamed: string[] = [];
@@ -337,6 +482,11 @@ export function layout(input: SourceInput): Layout {
337
482
  unusedModules,
338
483
  operands,
339
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),
340
490
  };
341
491
  }
342
492
 
@@ -585,6 +735,25 @@ function usageText(kinds: ReadonlyMap<UseKind, number> | undefined): string | un
585
735
  return parts.length === 0 ? undefined : `Used: ${parts.join(", ")}.`;
586
736
  }
587
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
+
588
757
  /** constants.asm: the names outside the image by platform area, each with what it is and how the source uses it. */
589
758
  function constantsText(layout: Layout): string {
590
759
  const { describe } = layout;
@@ -594,10 +763,11 @@ function constantsText(layout: Layout): string {
594
763
  });
595
764
  const nameColumn = Math.min(MAX_NAME_COLUMN, Math.max(0, ...rows.map((row) => row.name.length)));
596
765
  const valueColumn = Math.max(0, ...rows.map((row) => row.value.length));
597
- const out = [
766
+ const out = [...switchLines(layout)];
767
+ out.push(
598
768
  describe === undefined ? "; Names of addresses outside the image." : "; Names of addresses outside the image, by memory area.",
599
769
  '; "Used" counts the instructions and table entries of this source that use the name.',
600
- ];
770
+ );
601
771
  // Without platform areas the names form one group.
602
772
  if (describe === undefined && rows.length > 0) out.push("");
603
773
  let area: Area | undefined;
@@ -629,6 +799,45 @@ export interface Rendered {
629
799
  expressions: Array<{ address: number; length: number; file: string; line: number }>;
630
800
  /** Operand expressions written as numbers, in address order (D65). */
631
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
+ ];
632
841
  }
633
842
 
634
843
  const withSide = (text: string, side: string | undefined) => (side === undefined ? text : `${text.padEnd(SIDE_COLUMN - 1)} ; ${side}`);
@@ -638,7 +847,7 @@ const withSide = (text: string, side: string | undefined) => (side === undefined
638
847
  * holds the opcode form keys that the probe refused; `numbers` the operand
639
848
  * expressions that ACME did not assemble to the image, with the reason.
640
849
  */
641
- 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 {
642
851
  const lineComments = new Map<number, string[]>();
643
852
  const sideComments = new Map<number, string>();
644
853
  for (const comment of layout.comments) {
@@ -647,7 +856,24 @@ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: str
647
856
  else sideComments.set(comment.address, comment.text.replace(/\s*\r?\n\s*/g, " "));
648
857
  }
649
858
  const breaks = new Set<number>([...layout.names.keys(), ...lineComments.keys(), ...sideComments.keys()]);
650
- 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);
651
877
  const unwritten = new Map(layout.operandsAsNumbers.map((row) => [row.address, row.expression]));
652
878
  const { coverage } = layout;
653
879
  const notExecuted = { instructions: 0, ranges: [] as Range[] };
@@ -658,34 +884,79 @@ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: str
658
884
  let running = false;
659
885
  /** The line of the mark of the open range of instructions that did not execute. */
660
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
+ };
661
908
  const head = (address: number) => {
909
+ if (patch?.start === address) return;
662
910
  for (const text of lineComments.get(address) ?? []) out.push(`; ${text}`.trimEnd());
663
911
  const label = layout.names.get(address);
664
- if (label !== undefined) out.push(label);
912
+ if (label !== undefined && patch === undefined) out.push(label);
665
913
  };
666
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
+ }
667
951
  if (item.kind === "instruction") {
668
952
  head(item.address);
669
- const executed = coverage === undefined || (coverage[item.address]! & COVERAGE.ramExecute) !== 0;
670
- if (!executed) {
671
- if (running) notExecuted.ranges.at(-1)!.end = item.address + item.length - 1;
672
- else {
673
- notExecuted.ranges.push({ start: item.address, end: item.address + item.length - 1 });
674
- markLine = out.length;
675
- out.push("");
676
- }
677
- // The mark gives the range, so a reader finds it in the full list (D49).
678
- const range = notExecuted.ranges.at(-1)!;
679
- out[markLine] = `; not executed ${hex4(range.start)}-${hex4(range.end)}`;
680
- notExecuted.instructions++;
681
- }
682
- running = !executed;
953
+ mark(item.address, item.address + item.length - 1, 1);
683
954
  const sides: string[] = [];
684
955
  if (sideComments.has(item.address)) sides.push(sideComments.get(item.address)!);
685
956
  for (let offset = 1; offset < item.length; offset++) {
686
957
  const inside = item.address + offset;
687
958
  const label = layout.names.get(inside);
688
- if (label !== undefined) out.push(`${label} = * + ${offset}`);
959
+ if (label !== undefined && patch === undefined) out.push(`${label} = * + ${offset}`);
689
960
  for (const text of lineComments.get(inside) ?? []) sides.push(text);
690
961
  if (sideComments.has(inside)) sides.push(sideComments.get(inside)!);
691
962
  }
@@ -717,6 +988,7 @@ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: str
717
988
  out.push(withSide(`${INDENT}${instructionText(item.opcode, form.suffix, expression)}`, sides.length === 0 ? undefined : sides.join(" ")));
718
989
  }
719
990
  result.instructions++;
991
+ close();
720
992
  continue;
721
993
  }
722
994
  running = false;
@@ -730,12 +1002,18 @@ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: str
730
1002
  const bytes = item.end - item.start + 1;
731
1003
  if (item.type === "undecoded") result.undecodedBytes += bytes;
732
1004
  else result.dataBytes += bytes;
1005
+ close();
733
1006
  }
734
1007
  result.files.push({ name: piece.file, text: `${out.join("\n")}\n` });
735
1008
  }
736
1009
 
737
1010
  result.files.push({ name: "constants.asm", text: constantsText(layout) });
738
- const main = [...header.map((line) => `; ${line}`), "", "!cpu 6510", '!source "constants.asm"'];
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"'] : [])];
739
1017
  for (const piece of layout.pieces) {
740
1018
  if (piece.segmentStart) main.push("", `* = ${hex4(piece.start)}`);
741
1019
  main.push(`!source "${piece.file}"`);
@@ -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
 
@@ -74,6 +75,7 @@ The analyzers take at most 1024 entry points. Knowledge routines and `--entry` c
74
75
  - `conflicts` lists the places where the analyzer and the current knowledge disagree.
75
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.
76
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.
77
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.
78
80
  6. Give each important routine a name with the c64-knowledge skill. Then run the analysis again. The analyzers use the new names.
79
81
 
@@ -18,15 +18,16 @@ import { localToolContext } from "#src/native/local.ts";
18
18
  import { GHIDRA_LIMITS } from "#src/native/types.ts";
19
19
  import { KnowledgeError, openForRead, withWrite } from "#src/knowledge/database.ts";
20
20
  import { importFindings, runProvenance, type Analyzer, type ImportConflict, type ImportContext, type ImportResult, type NormalizedFindings } from "#src/knowledge/import.ts";
21
- import { currentRevision, listRegions } from "#src/knowledge/read.ts";
21
+ import { currentRevision, listMacros, listMacroUses, listPatches, listReferences, listRegions, listSymbols } from "#src/knowledge/read.ts";
22
22
  import { DEFAULT_SPACE, SPACE } from "#src/knowledge/schema.ts";
23
23
  import { platformNames } from "#src/platform.ts";
24
24
  import { resolveProjectPath } from "#src/project.ts";
25
25
  import { runScript, UsageError } from "#src/script.ts";
26
- import { codeEntries, coverageConflicts, coverageEntries, imageReader, readCoverageMaps } from "./coverage.ts";
26
+ import { codeEntries, coverageConflicts, coverageEntries, imageReader, readCoverageMaps, type ImageByte } from "./coverage.ts";
27
27
  import { dxaFindings, ghidraFindings, seedsFromKnowledge } from "./findings.ts";
28
28
  import { findIdioms } from "./idioms.ts";
29
29
  import { instructionList } from "./instructions.ts";
30
+ import { CATALOGUE, findMacroUses, type MacroCandidate } from "./macros.ts";
30
31
  import { findPointers, type PointerCandidate } from "./pointers.ts";
31
32
 
32
33
  const USAGE = `analyze.ts <image> [options]
@@ -46,6 +47,8 @@ const USAGE = `analyze.ts <image> [options]
46
47
  all labels and functions
47
48
  --space <space> the knowledge space of the image: c64 (the default), drive8
48
49
  for 1541 drive code, or bank0 to bank255 for a cartridge bank
50
+ --macro-batch <file> write the macro candidates as an apply batch of c64-knowledge
51
+ (the macros and their uses) to this project file
49
52
 
50
53
  Routine symbols in knowledge are entry points too. Ghidra needs at least one
51
54
  entry point; dxa finds the SYS of a BASIC start by itself. The result is one
@@ -67,6 +70,73 @@ function showPointer(candidate: PointerCandidate) {
67
70
  return { kind: candidate.kind, to: hex(candidate.to), target: hex(candidate.target), low: hex(candidate.low), high: hex(candidate.high) };
68
71
  }
69
72
 
73
+ /** A macro candidate with $ addresses and its source line (D73). */
74
+ function showMacro(candidate: MacroCandidate) {
75
+ return { macro: candidate.macro, start: formatC64Address(candidate.start), end: formatC64Address(candidate.end), args: candidate.args, text: `+${candidate.macro} ${candidate.args.join(", ")}` };
76
+ }
77
+
78
+ /**
79
+ * The macro candidates (D73) in the current code regions of every origin,
80
+ * with the names of the knowledge in their args. A candidate is left out when
81
+ * a name or a reference target is inside it, or a patch or a use overlaps it:
82
+ * c64-source would write its instructions. The same use in the knowledge is
83
+ * known. With `batchPath`, the candidates go to an apply batch with the
84
+ * macros they need; a macro of the catalogue that the knowledge has with
85
+ * another body leaves its candidates out of the batch.
86
+ */
87
+ function macroCandidates(space: string, image: string, byteAt: ImageByte, batchPath: string | undefined) {
88
+ const db = openForRead();
89
+ try {
90
+ const platform = space === DEFAULT_SPACE ? new Map(platformNames().map((entry) => [entry.address, entry.name])) : new Map<number, string>();
91
+ const semantic = new Map(listSymbols(db, undefined, undefined, ["user", "llm"], space).map((symbol) => [symbol.address, symbol.name]));
92
+ const code = listRegions(db, undefined, "code", undefined, space);
93
+ const found = findMacroUses(byteAt, code, (address) => semantic.get(address) ?? platform.get(address));
94
+ const named = new Set([...listSymbols(db, undefined, undefined, undefined, space).map((symbol) => symbol.address), ...listReferences(db, undefined, undefined, undefined, space).map((reference) => reference.to)]);
95
+ const patches = listPatches(db, undefined, undefined, undefined, space);
96
+ const uses = listMacroUses(db, undefined, undefined, undefined, space);
97
+ const overlaps = (a: { start: number; end: number }, b: { start: number; end: number }) => a.start <= b.end && b.start <= a.end;
98
+ const candidates: MacroCandidate[] = [];
99
+ let known = 0;
100
+ let leftOut = 0;
101
+ for (const candidate of found) {
102
+ const same = uses.find((use) => use.start === candidate.start && use.end === candidate.end && use.macro === candidate.macro && use.args.join(",") === candidate.args.join(","));
103
+ if (same !== undefined) known++;
104
+ else if ([...named].some((address) => address > candidate.start && address <= candidate.end) || patches.some((patch) => overlaps(patch, candidate)) || uses.some((use) => overlaps(use, candidate))) leftOut++;
105
+ else candidates.push(candidate);
106
+ }
107
+ if (batchPath === undefined) return { candidates, known, leftOut };
108
+ // A macro of the catalogue goes into the batch unless the knowledge has it; with another body, its uses stay out.
109
+ const macros = new Map(listMacros(db, undefined, space).map((macro) => [macro.name, macro]));
110
+ const batch: Array<Record<string, unknown>> = [];
111
+ const batched: MacroCandidate[] = [];
112
+ const other = new Set<string>();
113
+ for (const idiom of CATALOGUE.filter((item) => candidates.some((candidate) => candidate.macro === item.name))) {
114
+ const existing = macros.get(idiom.name);
115
+ if (existing === undefined) batch.push({ op: "macro", name: idiom.name, params: idiom.params, body: idiom.body, comment: idiom.comment, ...(space === DEFAULT_SPACE ? {} : { space }) });
116
+ else if (existing.params.join(",") !== idiom.params.join(",") || existing.body !== idiom.body) other.add(idiom.name);
117
+ }
118
+ for (const candidate of candidates) {
119
+ if (other.has(candidate.macro)) continue;
120
+ batched.push(candidate);
121
+ batch.push({ op: "use", start: formatC64Address(candidate.start), end: formatC64Address(candidate.end), macro: candidate.macro, args: candidate.args, ...(space === DEFAULT_SPACE ? {} : { space }) });
122
+ }
123
+ if (batch.length > 0) writeProjectFile(batchPath, Buffer.from(`${JSON.stringify(batch, null, 1)}\n`), { what: "--macro-batch" });
124
+ return {
125
+ candidates,
126
+ known,
127
+ leftOut,
128
+ batch: {
129
+ out: batchPath,
130
+ writes: batch.length,
131
+ ...(other.size === 0 ? {} : { otherMacros: [...other].map((name) => `The knowledge has another macro ${name}: its candidates are not in the batch.`) }),
132
+ ...(batch.length === 0 ? {} : { apply: `knowledge.ts apply ${batchPath} --image ${image} --reason "macro uses of 6502 idioms"` }),
133
+ },
134
+ };
135
+ } finally {
136
+ db?.close();
137
+ }
138
+ }
139
+
70
140
  /** A decompiler that did not start gets the remedy: Ghidra ships no native decompiler for macOS. */
71
141
  function withRemedy(reason: string): string {
72
142
  return reason.startsWith("the Ghidra decompiler did not start")
@@ -139,6 +209,7 @@ async function run(argv: string[]): Promise<unknown> {
139
209
  coverage: { type: "string", multiple: true },
140
210
  instructions: { type: "string" },
141
211
  space: { type: "string" },
212
+ "macro-batch": { type: "string" },
142
213
  help: { type: "boolean" },
143
214
  },
144
215
  });
@@ -165,6 +236,14 @@ async function run(argv: string[]): Promise<unknown> {
165
236
  }
166
237
  }
167
238
 
239
+ if (values["macro-batch"] !== undefined) {
240
+ try {
241
+ resolveProjectPath(values["macro-batch"]);
242
+ } catch {
243
+ throw new UsageError(`--macro-batch must be a path relative to the project folder that stays inside it, not ${values["macro-batch"]}`);
244
+ }
245
+ }
246
+
168
247
  const space = values.space ?? DEFAULT_SPACE;
169
248
  if (!SPACE.test(space)) throw new UsageError("--space must be c64, drive8, or bank0 to bank255");
170
249
  // A coverage map is of the C64 CPU: VICE keeps none for the drive (D53).
@@ -206,6 +285,8 @@ async function run(argv: string[]): Promise<unknown> {
206
285
  /** Imports the idioms as their own revision, after the analyzer's, and gives their part of the result. */
207
286
  const idiomsResult = () => {
208
287
  const imported = withWrite((db) => importFindings(db, { ...idioms.findings, space }, { description: `Idioms in ${image}` }));
288
+ // The macro candidates read the code regions after both imports (D73).
289
+ const macros = macroCandidates(space, image, byteAt, values["macro-batch"]);
209
290
  return {
210
291
  idioms: {
211
292
  ...idioms.counts,
@@ -217,6 +298,12 @@ async function run(argv: string[]): Promise<unknown> {
217
298
  dop: listed(idioms.candidates.dop, max, formatC64Address, "a larger --max-listed"),
218
299
  selfModified: listed(idioms.candidates.selfModified, max, formatC64Address, "a larger --max-listed"),
219
300
  pointers: listed(pointers, max, showPointer, "a larger --max-listed"),
301
+ macros: {
302
+ ...listed(macros.candidates, max, showMacro, "a larger --max-listed, or --macro-batch <file>"),
303
+ ...(macros.known === 0 ? {} : { known: macros.known }),
304
+ ...(macros.leftOut === 0 ? {} : { leftOut: macros.leftOut }),
305
+ ...(macros.batch === undefined ? {} : { batch: macros.batch }),
306
+ },
220
307
  },
221
308
  },
222
309
  };