@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.
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +2 -0
- package/skills/c64-knowledge/SKILL.md +43 -5
- package/skills/c64-knowledge/scripts/knowledge.ts +227 -16
- package/skills/c64-reverse-engineering/SKILL.md +1 -0
- package/skills/c64-source/SKILL.md +29 -5
- package/skills/c64-source/scripts/generate.ts +203 -23
- package/skills/c64-source/scripts/loader.ts +3 -1
- package/skills/c64-source/scripts/opcodes.ts +1 -1
- package/skills/c64-source/scripts/source.ts +303 -25
- package/skills/c64-static-analysis/SKILL.md +2 -0
- package/skills/c64-static-analysis/scripts/analyze.ts +89 -2
- package/skills/c64-static-analysis/scripts/macros.ts +233 -0
- package/src/knowledge/conflicts.ts +12 -0
- package/src/knowledge/history.ts +30 -1
- package/src/knowledge/read.ts +128 -1
- package/src/knowledge/schema.ts +48 -2
- package/src/knowledge/text.ts +38 -11
- package/src/knowledge/write.ts +368 -6
|
@@ -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 = {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|