@henols/c64-re-tools 2.0.0-rc.9 → 2.0.0

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 (117) hide show
  1. package/README.md +97 -116
  2. package/package.json +1 -1
  3. package/skills/c64-assembler/SKILL.md +22 -6
  4. package/skills/c64-assembler/scripts/assemble.ts +42 -14
  5. package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
  6. package/skills/c64-basic/SKILL.md +10 -1
  7. package/skills/c64-basic/scripts/basic.ts +3 -3
  8. package/skills/c64-disk/SKILL.md +13 -4
  9. package/skills/c64-disk/scripts/disk.ts +7 -7
  10. package/skills/c64-disk/scripts/{containers.ts → lib/containers.ts} +2 -2
  11. package/skills/c64-docs/SKILL.md +138 -0
  12. package/skills/c64-docs/references/page-types.md +141 -0
  13. package/skills/c64-docs/references/style.md +43 -0
  14. package/skills/c64-docs/scripts/generate.ts +109 -0
  15. package/skills/c64-docs/scripts/lib/links.ts +167 -0
  16. package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
  17. package/skills/c64-docs/scripts/lib/reference.ts +406 -0
  18. package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
  19. package/skills/c64-docs/scripts/link.ts +239 -0
  20. package/skills/c64-emulator/SKILL.md +16 -5
  21. package/skills/c64-knowledge/SKILL.md +140 -18
  22. package/skills/c64-knowledge/scripts/knowledge.ts +737 -112
  23. package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
  24. package/skills/c64-memory-map/SKILL.md +11 -2
  25. package/skills/c64-memory-map/scripts/memmap.ts +2 -2
  26. package/skills/c64-provenance/SKILL.md +66 -11
  27. package/skills/c64-provenance/scripts/{compare.ts → lib/compare.ts} +3 -3
  28. package/skills/c64-provenance/scripts/lib/library.ts +584 -0
  29. package/skills/c64-provenance/scripts/{runtime.ts → lib/runtime.ts} +2 -2
  30. package/skills/c64-provenance/scripts/{separate.ts → lib/separate.ts} +3 -3
  31. package/skills/c64-provenance/scripts/provenance.ts +242 -40
  32. package/skills/c64-reverse-engineering/SKILL.md +51 -26
  33. package/skills/c64-source/SKILL.md +76 -28
  34. package/skills/c64-source/scripts/generate.ts +425 -127
  35. package/skills/c64-source/scripts/{loader.ts → lib/loader.ts} +84 -25
  36. package/skills/c64-source/scripts/lib/source.ts +1121 -0
  37. package/skills/c64-static-analysis/SKILL.md +35 -18
  38. package/skills/c64-static-analysis/scripts/analyze.ts +184 -55
  39. package/skills/c64-static-analysis/scripts/{coverage.ts → lib/coverage.ts} +19 -25
  40. package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +6 -2
  41. package/skills/c64-static-analysis/scripts/{idioms.ts → lib/idioms.ts} +21 -12
  42. package/skills/c64-static-analysis/scripts/{instructions.ts → lib/instructions.ts} +1 -1
  43. package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
  44. package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
  45. package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
  46. package/skills/c64-testing/SKILL.md +36 -7
  47. package/skills/c64-testing/references/scenario-format.md +12 -8
  48. package/skills/c64-testing/scripts/checklist.ts +3 -3
  49. package/skills/c64-testing/scripts/diff.ts +167 -0
  50. package/skills/c64-testing/scripts/lib/diff.ts +70 -0
  51. package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +45 -34
  52. package/skills/c64-testing/scripts/run.ts +73 -21
  53. package/skills/c64-testing/scripts/test.ts +5 -5
  54. package/skills/c64-unpacker/SKILL.md +23 -9
  55. package/skills/c64-unpacker/scripts/{evidence.ts → lib/evidence.ts} +1 -1
  56. package/skills/c64-unpacker/scripts/{loads.ts → lib/loads.ts} +4 -4
  57. package/skills/c64-unpacker/scripts/{machine.ts → lib/machine.ts} +4 -4
  58. package/skills/c64-unpacker/scripts/{stages.ts → lib/stages.ts} +17 -5
  59. package/skills/c64-unpacker/scripts/unpack.ts +41 -24
  60. package/src/c64.ts +103 -5
  61. package/src/cli/cleanup.ts +1 -1
  62. package/src/cli/main.ts +7 -4
  63. package/src/cli/status.ts +70 -3
  64. package/src/graphics.ts +158 -0
  65. package/src/host/main.ts +18 -4
  66. package/src/host/server.ts +10 -3
  67. package/src/host/tools/petcat.ts +1 -1
  68. package/src/host/tools/status.ts +1 -1
  69. package/src/host/vice/adapter.ts +4 -3
  70. package/src/host/vice/process.ts +2 -2
  71. package/src/host/vice/screen.ts +1 -42
  72. package/src/host/vice/session.ts +196 -23
  73. package/src/host-client/autostart.ts +1 -1
  74. package/src/host-client/connect.ts +1 -1
  75. package/src/host-client/coverage.ts +2 -29
  76. package/src/host-client/image.ts +66 -0
  77. package/src/host-client/steps.ts +9 -4
  78. package/src/host-client/tools.ts +1 -0
  79. package/src/host-client/transfer.ts +39 -11
  80. package/src/host-client/vice-session.ts +33 -16
  81. package/src/knowledge/conflicts.ts +161 -0
  82. package/src/knowledge/database.ts +150 -38
  83. package/src/knowledge/expression.ts +209 -0
  84. package/src/knowledge/graph.ts +104 -0
  85. package/src/knowledge/history.ts +70 -22
  86. package/src/knowledge/import.ts +64 -55
  87. package/src/knowledge/join.ts +211 -0
  88. package/src/knowledge/program.ts +266 -0
  89. package/src/knowledge/read.ts +258 -27
  90. package/src/knowledge/schema.ts +199 -75
  91. package/src/knowledge/text.ts +415 -0
  92. package/src/knowledge/write.ts +740 -57
  93. package/src/mcp/main.ts +1 -1
  94. package/src/mcp/output.ts +15 -8
  95. package/src/mcp/server.ts +3 -1
  96. package/src/mcp/tools/debug.ts +97 -16
  97. package/src/mcp/tools/execution.ts +38 -5
  98. package/src/mcp/tools/machine.ts +11 -2
  99. package/src/mcp/tools/media.ts +18 -13
  100. package/src/mcp/tools/memory.ts +17 -8
  101. package/src/mcp/tools/video.ts +9 -9
  102. package/src/native/discover.ts +2 -2
  103. package/src/native/ghidra/analyze.ts +2 -2
  104. package/src/native/ghidra/index.ts +1 -1
  105. package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
  106. package/src/native/ghidra/scripts/C64Export.java +1 -1
  107. package/src/native/ghidra/scripts/C64Prepare.java +7 -1
  108. package/src/native/types.ts +14 -8
  109. package/{skills/c64-source/scripts → src}/opcodes.ts +75 -3
  110. package/src/platform.ts +70 -7
  111. package/src/project.ts +180 -3
  112. package/src/protocol/messages.ts +1 -1
  113. package/src/protocol/tools.ts +2 -16
  114. package/src/protocol/vice.ts +124 -15
  115. package/src/script.ts +46 -7
  116. package/skills/c64-assembler/scripts/compare.ts +0 -84
  117. package/skills/c64-source/scripts/source.ts +0 -595
@@ -1,13 +1,13 @@
1
- // 6502 idioms that hide code from a disassembler (D44): RTS dispatch tables,
1
+ // 6502 idioms that hide code from a disassembler: RTS dispatch tables,
2
2
  // JMP tables and BIT skips are found by their exact instruction patterns and
3
3
  // become heuristic findings and entry points; DOP padding and self-modified
4
- // opcodes are only reported. A self-modified opcode is one that one run wrote
5
- // and executed, in either order, since a coverage file holds no order (R3-04):
6
- // a copier's writes in one run and their execution in another do not count (F-16). A table's length is not known statically: entries
4
+ // opcodes are only reported. The CBM80 header of a cartridge bank at $8000
5
+ // gives its two start vectors as entry points. A self-modified opcode is one that one run wrote
6
+ // and executed, in either order, since a coverage file holds no order:
7
+ // a copier's writes in one run and their execution in another do not count. A table's length is not known statically: entries
7
8
  // count while each target is a plausible instruction in the program.
8
9
 
9
10
  import { COVERAGE } from "#src/c64.ts";
10
- import { joinCoverage } from "#src/host-client/coverage.ts";
11
11
  import type { NormalizedFindings } from "#src/knowledge/import.ts";
12
12
  import type { ImageByte } from "./coverage.ts";
13
13
 
@@ -17,6 +17,8 @@ const LDA_INDEXED = new Set([0xbd, 0xb9]);
17
17
  const JAMS = new Set([0x02, 0x12, 0x22, 0x32, 0x42, 0x52, 0x62, 0x72, 0x92, 0xb2, 0xd2, 0xf2]);
18
18
  /** Undocumented NOPs with an operand byte (DOP): padding between instructions hides the real ones. */
19
19
  const DOPS = new Set([0x04, 0x14, 0x34, 0x44, 0x54, 0x64, 0x74, 0x80, 0x82, 0x89, 0xc2, 0xd4, 0xe2, 0xf4]);
20
+ /** "CBM80" in PETSCII at $8004: the KERNAL then starts the cartridge through the vectors at $8000 (cold) and $8002 (warm). */
21
+ const CBM80 = [0xc3, 0xc2, 0xcd, 0x38, 0x30];
20
22
 
21
23
  export interface Idioms {
22
24
  findings: NormalizedFindings;
@@ -24,7 +26,7 @@ export interface Idioms {
24
26
  entryPoints: number[];
25
27
  /** Table ranges for the same run: they are data. */
26
28
  dataRanges: Array<{ start: number; end: number }>;
27
- counts: { rtsTables: number; jumpTables: number; bitSkips: number };
29
+ counts: { cartridgeHeaders: number; rtsTables: number; jumpTables: number; bitSkips: number };
28
30
  /** Seen in the coverage, not written: the LLM decides. All of them: the result lists the first ones. */
29
31
  candidates: { dop: number[]; selfModified: number[] };
30
32
  }
@@ -41,13 +43,13 @@ const word = (byteAt: ImageByte, address: number) => {
41
43
  return low === undefined || high === undefined ? undefined : low | (high << 8);
42
44
  };
43
45
 
44
- /** `runs` are the coverage maps, one for each file. */
45
- export function findIdioms(byteAt: ImageByte, range: { start: number; end: number }, runs: readonly Uint8Array[] = []): Idioms {
46
- const coverage = runs.length === 0 ? undefined : joinCoverage(runs);
46
+ /** `covered` is the coverage of the knowledge: the joined map, and the addresses that one run wrote and executed. */
47
+ export function findIdioms(byteAt: ImageByte, range: { start: number; end: number }, covered?: { map: Uint8Array; selfModified: Uint8Array }): Idioms {
48
+ const coverage = covered?.map;
47
49
  const regions: NormalizedFindings["regions"] = [];
48
50
  const references: NormalizedFindings["references"] = [];
49
51
  const entryPoints = new Set<number>();
50
- const counts = { rtsTables: 0, jumpTables: 0, bitSkips: 0 };
52
+ const counts = { cartridgeHeaders: 0, rtsTables: 0, jumpTables: 0, bitSkips: 0 };
51
53
  const taken = (start: number, end: number) => regions.some((region) => start <= region.end && end >= region.start);
52
54
  const addRegion = (start: number, end: number, type: NormalizedFindings["regions"][number]["type"]) => {
53
55
  if (start < range.start || end > range.end || taken(start, end)) return false;
@@ -55,6 +57,14 @@ export function findIdioms(byteAt: ImageByte, range: { start: number; end: numbe
55
57
  return true;
56
58
  };
57
59
 
60
+ if (range.start <= 0x8000 && range.end >= 0x8008 && CBM80.every((byte, index) => byteAt(0x8004 + index) === byte)) {
61
+ const vectors = [word(byteAt, 0x8000)!, word(byteAt, 0x8002)!].filter((target) => plausible(byteAt, target, range));
62
+ if (addRegion(0x8000, 0x8003, "addresses") && addRegion(0x8004, 0x8008, "petscii")) {
63
+ counts.cartridgeHeaders++;
64
+ for (const target of vectors) entryPoints.add(target);
65
+ }
66
+ }
67
+
58
68
  for (let at = range.start; at + 8 <= range.end; at++) {
59
69
  // RTS dispatch: LDA high,X / PHA / LDA low,X / PHA / RTS.
60
70
  if (LDA_INDEXED.has(byteAt(at)!) && byteAt(at + 3) === 0x48 && LDA_INDEXED.has(byteAt(at + 4)!) && byteAt(at + 7) === 0x48 && byteAt(at + 8) === 0x60) {
@@ -121,11 +131,10 @@ export function findIdioms(byteAt: ImageByte, range: { start: number; end: numbe
121
131
  // Candidates from the coverage: DOP padding that ran, and opcodes that one run wrote and executed.
122
132
  const candidates = { dop: [] as number[], selfModified: [] as number[] };
123
133
  if (coverage !== undefined) {
124
- const both = COVERAGE.ramWrite | COVERAGE.ramExecute;
125
134
  for (let address = range.start; address <= range.end; address++) {
126
135
  if ((coverage[address]! & COVERAGE.ramExecute) === 0) continue;
127
136
  if (DOPS.has(byteAt(address)!)) candidates.dop.push(address);
128
- if (runs.some((run) => (run[address]! & both) === both)) candidates.selfModified.push(address);
137
+ if (covered!.selfModified[address] === 1) candidates.selfModified.push(address);
129
138
  }
130
139
  }
131
140
 
@@ -1,4 +1,4 @@
1
- // The structured instruction list (D44): Ghidra's decoded instructions with
1
+ // The structured instruction list: Ghidra's decoded instructions with
2
2
  // the addressing mode, the operand in 6502 syntax, the flow target and,
3
3
  // with a coverage file, whether the instruction ran.
4
4
 
@@ -0,0 +1,233 @@
1
+ // Macro candidates: runs of instructions that are a known 6502 idiom,
2
+ // such as a 16-bit increment, and that a macro can write as one line. They
3
+ // are only reported, never written: the LLM applies them as macro uses with
4
+ // c64-knowledge, and c64-source writes a use only when it gives the bytes of
5
+ // the image.
6
+ //
7
+ // The catalogue is fixed, so each candidate has a name and a body that is
8
+ // known to be correct. A search for any repeated sequence was weighed and not
9
+ // chosen: it gives many candidates without a name.
10
+
11
+ import { instructionLength } from "#src/c64.ts";
12
+ import type { ImageByte } from "./coverage.ts";
13
+
14
+ /** A macro of the catalogue: its params and its ACME body, which names a param with a dot. */
15
+ export interface MacroIdiom {
16
+ name: string;
17
+ params: string[];
18
+ body: string;
19
+ comment: string;
20
+ }
21
+
22
+ export const CATALOGUE: readonly MacroIdiom[] = [
23
+ { name: "inc16", params: ["dst"], body: [" inc .dst", " bne +", " inc .dst+1", "+"].join("\n"), comment: "Adds 1 to the 16-bit value at dst." },
24
+ { name: "dec16", params: ["dst"], body: [" lda .dst", " bne +", " dec .dst+1", "+ dec .dst"].join("\n"), comment: "Subtracts 1 from the 16-bit value at dst. It changes A." },
25
+ {
26
+ name: "add16_8",
27
+ params: ["dst", "value"],
28
+ body: [" clc", " lda .dst", " adc #.value", " sta .dst", " bcc +", " inc .dst+1", "+"].join("\n"),
29
+ comment: "Adds the 8-bit value to the 16-bit value at dst. It changes A.",
30
+ },
31
+ {
32
+ name: "add16",
33
+ params: ["dst", "value"],
34
+ body: [" clc", " lda .dst", " adc #<.value", " sta .dst", " lda .dst+1", " adc #>.value", " sta .dst+1"].join("\n"),
35
+ comment: "Adds the 16-bit value to the 16-bit value at dst. It changes A.",
36
+ },
37
+ {
38
+ name: "sub16",
39
+ params: ["dst", "value"],
40
+ body: [" sec", " lda .dst", " sbc #<.value", " sta .dst", " lda .dst+1", " sbc #>.value", " sta .dst+1"].join("\n"),
41
+ comment: "Subtracts the 16-bit value from the 16-bit value at dst. It changes A.",
42
+ },
43
+ {
44
+ name: "set16",
45
+ params: ["dst", "value"],
46
+ body: [" lda #<.value", " sta .dst", " lda #>.value", " sta .dst+1"].join("\n"),
47
+ comment: "Puts the 16-bit value at dst. It changes A.",
48
+ },
49
+ {
50
+ name: "mov16",
51
+ params: ["dst", "src"],
52
+ body: [" lda .src", " sta .dst", " lda .src+1", " sta .dst+1"].join("\n"),
53
+ comment: "Copies the 16-bit value at src to dst. It changes A.",
54
+ },
55
+ { name: "wait_line", params: ["line"], body: [" lda #.line", "- cmp $d012", " bne -"].join("\n"), comment: "Waits until the raster line (bits 0-7) is line. It changes A." },
56
+ ];
57
+
58
+ /** A run of instructions that one macro of the catalogue can write, with its args as ACME expressions. */
59
+ export interface MacroCandidate {
60
+ macro: string;
61
+ start: number;
62
+ end: number;
63
+ args: string[];
64
+ }
65
+
66
+ /** One instruction: its opcode, its length and its operand value. */
67
+ export interface Instruction {
68
+ opcode: number;
69
+ length: number;
70
+ value: number;
71
+ }
72
+
73
+ /** An address operand: zero page or absolute, of the same memory in one idiom. */
74
+ interface Operand {
75
+ value: number;
76
+ absolute: boolean;
77
+ }
78
+
79
+ /** Opcodes by mnemonic, zero page first, then absolute. */
80
+ const MEMORY = {
81
+ lda: [0xa5, 0xad],
82
+ sta: [0x85, 0x8d],
83
+ inc: [0xe6, 0xee],
84
+ dec: [0xc6, 0xce],
85
+ } as const;
86
+ const IMMEDIATE = { lda: 0xa9, adc: 0x69, sbc: 0xe9 } as const;
87
+ const CLC = 0x18;
88
+ const SEC = 0x38;
89
+ const BNE = 0xd0;
90
+ const BCC = 0x90;
91
+ const CMP_ABSOLUTE = 0xcd;
92
+
93
+ /**
94
+ * Names the args of a candidate: `address` gives the name of an address in
95
+ * the knowledge, or undefined. An absolute operand below $0100 keeps four
96
+ * digits, so ACME writes the absolute form again.
97
+ */
98
+ export type Namer = (address: number) => string | undefined;
99
+
100
+ const hex2 = (value: number) => `$${value.toString(16).padStart(2, "0")}`;
101
+ const hex4 = (value: number) => `$${value.toString(16).padStart(4, "0")}`;
102
+
103
+ function addressArg(operand: Operand, name: Namer): string {
104
+ const named = name(operand.value);
105
+ // A name of a zero-page address gives the zero-page form: an absolute operand there keeps its number.
106
+ if (named !== undefined && (operand.value >= 0x100 || !operand.absolute)) return named;
107
+ return operand.absolute ? hex4(operand.value) : hex2(operand.value);
108
+ }
109
+
110
+ /** The memory operand of `instruction` when it is one of the opcodes of the mnemonic. */
111
+ function memory(instruction: Instruction | undefined, mnemonic: keyof typeof MEMORY): Operand | undefined {
112
+ if (instruction === undefined) return undefined;
113
+ const index = (MEMORY[mnemonic] as readonly number[]).indexOf(instruction.opcode);
114
+ return index < 0 ? undefined : { value: instruction.value, absolute: index === 1 };
115
+ }
116
+
117
+ const same = (a: Operand | undefined, b: Operand | undefined, offset = 0): boolean => a !== undefined && b !== undefined && a.absolute === b.absolute && b.value === a.value + offset;
118
+ const immediate = (instruction: Instruction | undefined, opcode: number) => (instruction?.opcode === opcode ? instruction.value : undefined);
119
+ /** A branch past the next instruction only: its distance is the length of that instruction. */
120
+ const skips = (branch: Instruction | undefined, opcode: number, next: Instruction | undefined) => branch?.opcode === opcode && next !== undefined && branch.value === next.length;
121
+
122
+ /** Each idiom: how many instructions it takes, and its args when the instructions match. */
123
+ const MATCHERS: Record<string, { count: number; match: (run: Instruction[], name: Namer) => string[] | undefined }> = {
124
+ inc16: {
125
+ count: 3,
126
+ match: ([low, branch, high], name) => {
127
+ const dst = memory(low, "inc");
128
+ return same(dst, memory(high, "inc"), 1) && skips(branch, BNE, high) ? [addressArg(dst!, name)] : undefined;
129
+ },
130
+ },
131
+ dec16: {
132
+ count: 4,
133
+ match: ([load, branch, high, low], name) => {
134
+ const dst = memory(load, "lda");
135
+ return same(dst, memory(low, "dec")) && same(dst, memory(high, "dec"), 1) && skips(branch, BNE, high) ? [addressArg(dst!, name)] : undefined;
136
+ },
137
+ },
138
+ add16_8: {
139
+ count: 6,
140
+ match: ([clc, load, add, store, branch, high], name) => {
141
+ const dst = memory(load, "lda");
142
+ const value = immediate(add, IMMEDIATE.adc);
143
+ const ok = clc?.opcode === CLC && value !== undefined && same(dst, memory(store, "sta")) && same(dst, memory(high, "inc"), 1) && skips(branch, BCC, high);
144
+ return ok ? [addressArg(dst!, name), hex2(value)] : undefined;
145
+ },
146
+ },
147
+ add16: { count: 7, match: (run, name) => wide(run, name, CLC, IMMEDIATE.adc) },
148
+ sub16: { count: 7, match: (run, name) => wide(run, name, SEC, IMMEDIATE.sbc) },
149
+ set16: {
150
+ count: 4,
151
+ match: ([loadLow, storeLow, loadHigh, storeHigh], name) => {
152
+ const low = immediate(loadLow, IMMEDIATE.lda);
153
+ const high = immediate(loadHigh, IMMEDIATE.lda);
154
+ const dst = memory(storeLow, "sta");
155
+ if (low === undefined || high === undefined || !same(dst, memory(storeHigh, "sta"), 1)) return undefined;
156
+ const value = low | (high << 8);
157
+ return [addressArg(dst!, name), name(value) ?? hex4(value)];
158
+ },
159
+ },
160
+ mov16: {
161
+ count: 4,
162
+ match: ([loadLow, storeLow, loadHigh, storeHigh], name) => {
163
+ const src = memory(loadLow, "lda");
164
+ const dst = memory(storeLow, "sta");
165
+ if (!same(src, memory(loadHigh, "lda"), 1) || !same(dst, memory(storeHigh, "sta"), 1)) return undefined;
166
+ return [addressArg(dst!, name), addressArg(src!, name)];
167
+ },
168
+ },
169
+ wait_line: {
170
+ count: 3,
171
+ match: ([load, compare, branch]) => {
172
+ const line = immediate(load, IMMEDIATE.lda);
173
+ // BNE back to the CMP: 5 bytes back from the end of the BNE.
174
+ return line !== undefined && compare?.opcode === CMP_ABSOLUTE && compare.value === 0xd012 && branch?.opcode === BNE && branch.value === 0xfb ? [hex2(line)] : undefined;
175
+ },
176
+ },
177
+ };
178
+
179
+ /** add16 and sub16: the carry, then the low and the high byte, each with an immediate of the 16-bit value. */
180
+ function wide([carry, loadLow, low, storeLow, loadHigh, high, storeHigh]: Instruction[], name: Namer, carryOpcode: number, opcode: number): string[] | undefined {
181
+ const dst = memory(loadLow, "lda");
182
+ const lowValue = immediate(low, opcode);
183
+ const highValue = immediate(high, opcode);
184
+ const ok =
185
+ carry?.opcode === carryOpcode && lowValue !== undefined && highValue !== undefined && same(dst, memory(storeLow, "sta")) && same(dst, memory(loadHigh, "lda"), 1) && same(dst, memory(storeHigh, "sta"), 1);
186
+ return ok ? [addressArg(dst!, name), hex4(lowValue | (highValue << 8))] : undefined;
187
+ }
188
+
189
+ /** The instruction at `at`, or undefined when a byte of it is outside the range or the image. */
190
+ export function instructionAt(byteAt: ImageByte, at: number, end: number): Instruction | undefined {
191
+ const opcode = byteAt(at);
192
+ if (opcode === undefined) return undefined;
193
+ const length = instructionLength(opcode);
194
+ if (at + length - 1 > end) return undefined;
195
+ const bytes = Array.from({ length: length - 1 }, (_, offset) => byteAt(at + 1 + offset));
196
+ if (bytes.some((byte) => byte === undefined)) return undefined;
197
+ return { opcode, length, value: length === 1 ? 0 : length === 2 ? bytes[0]! : bytes[0]! | (bytes[1]! << 8) };
198
+ }
199
+
200
+ /**
201
+ * Finds the macro candidates in code regions. Each region is read as
202
+ * instructions from its start; at each instruction the idioms of the
203
+ * catalogue are tried in order, and a match is a candidate. Candidates do not
204
+ * overlap: the scan goes on after the end of a match.
205
+ */
206
+ export function findMacroUses(byteAt: ImageByte, regions: ReadonlyArray<{ start: number; end: number }>, name: Namer): MacroCandidate[] {
207
+ const candidates: MacroCandidate[] = [];
208
+ for (const region of [...regions].sort((a, b) => a.start - b.start)) {
209
+ const run: Array<Instruction & { at: number }> = [];
210
+ for (let at = region.start; at <= region.end; ) {
211
+ const instruction = instructionAt(byteAt, at, region.end);
212
+ if (instruction === undefined) break;
213
+ run.push({ ...instruction, at });
214
+ at += instruction.length;
215
+ }
216
+ for (let index = 0; index < run.length; ) {
217
+ const found = CATALOGUE.flatMap((idiom) => {
218
+ const matcher = MATCHERS[idiom.name]!;
219
+ const part = run.slice(index, index + matcher.count);
220
+ const args = part.length === matcher.count ? matcher.match(part, name) : undefined;
221
+ return args === undefined ? [] : [{ idiom, count: matcher.count, args }];
222
+ })[0];
223
+ if (found === undefined) {
224
+ index++;
225
+ continue;
226
+ }
227
+ const last = run[index + found.count - 1]!;
228
+ candidates.push({ macro: found.idiom.name, start: run[index]!.at, end: last.at + last.length - 1, args: found.args });
229
+ index += found.count;
230
+ }
231
+ }
232
+ return candidates;
233
+ }
@@ -0,0 +1,68 @@
1
+ // Platform pitfalls in code regions: code that a C64 runs in another
2
+ // way than it reads. A fixed catalogue, reported as candidates only.
3
+ //
4
+ // raster-bit-8: a read of $D011 gives bit 8 of the current raster line in
5
+ // bit 7. Code that writes the value back without clearing bit 7 sets bit 8 of
6
+ // the raster compare: the next raster interrupt then comes at line 256 or
7
+ // later, or never on a PAL machine past line 311 (the Bruce Lee freeze).
8
+
9
+ import type { ImageByte } from "./coverage.ts";
10
+ import { instructionAt, type Instruction } from "./macros.ts";
11
+
12
+ export interface Pitfall {
13
+ kind: "raster-bit-8";
14
+ /** The instruction that reads $D011. */
15
+ at: number;
16
+ /** The instruction that writes the value back to $D011. */
17
+ store: number;
18
+ }
19
+
20
+ const D011 = 0xd011;
21
+ /** LDA, LDX and LDY absolute, and the stores of the same register. */
22
+ const LOADS: Record<number, "a" | "x" | "y"> = { 0xad: "a", 0xae: "x", 0xac: "y" };
23
+ const STORES: Record<number, "a" | "x" | "y"> = { 0x8d: "a", 0x8e: "x", 0x8c: "y" };
24
+ /** INC, DEC, ASL, ROL and ROR absolute: they read the register and write it back. LSR shifts a 0 into bit 7. */
25
+ const READ_MODIFY_WRITE = new Set([0xee, 0xce, 0x0e, 0x2e, 0x6e]);
26
+ /** Instructions after which the next one is not the next in the flow: a jump, a return, a call or a branch. */
27
+ const FLOW = new Set([0x4c, 0x6c, 0x20, 0x60, 0x40, 0x00, 0x10, 0x30, 0x50, 0x70, 0x90, 0xb0, 0xd0, 0xf0]);
28
+ /** Instructions that load A, X or Y with a new value, which ends what the load of $D011 gave. */
29
+ const NEW_VALUE: Record<"a" | "x" | "y", Set<number>> = {
30
+ a: new Set([0xa9, 0xa5, 0xb5, 0xad, 0xbd, 0xb9, 0xa1, 0xb1, 0x8a, 0x98, 0x68]),
31
+ x: new Set([0xa2, 0xa6, 0xb6, 0xae, 0xbe, 0xaa, 0xba]),
32
+ y: new Set([0xa0, 0xa4, 0xb4, 0xac, 0xbc, 0xa8]),
33
+ };
34
+ /** The most instructions between the load and the store. */
35
+ const WINDOW = 8;
36
+
37
+ /** The raster-bit-8 pitfalls in the code regions, in address order. */
38
+ export function findPitfalls(byteAt: ImageByte, regions: ReadonlyArray<{ start: number; end: number }>): Pitfall[] {
39
+ const found: Pitfall[] = [];
40
+ for (const region of [...regions].sort((a, b) => a.start - b.start)) {
41
+ const run: Array<Instruction & { at: number }> = [];
42
+ for (let at = region.start; at <= region.end; ) {
43
+ const instruction = instructionAt(byteAt, at, region.end);
44
+ if (instruction === undefined) break;
45
+ run.push({ ...instruction, at });
46
+ at += instruction.length;
47
+ }
48
+ run.forEach((instruction, index) => {
49
+ if (instruction.length !== 3 || instruction.value !== D011) return;
50
+ if (READ_MODIFY_WRITE.has(instruction.opcode)) {
51
+ found.push({ kind: "raster-bit-8", at: instruction.at, store: instruction.at });
52
+ return;
53
+ }
54
+ const register = LOADS[instruction.opcode];
55
+ if (register === undefined) return;
56
+ for (const next of run.slice(index + 1, index + 1 + WINDOW)) {
57
+ // AND #imm without bit 7 clears raster bit 8: the code knows about it.
58
+ if (register === "a" && next.opcode === 0x29 && (next.value & 0x80) === 0) return;
59
+ if (STORES[next.opcode] === register && next.length === 3 && next.value === D011) {
60
+ found.push({ kind: "raster-bit-8", at: instruction.at, store: next.at });
61
+ return;
62
+ }
63
+ if (FLOW.has(next.opcode) || NEW_VALUE[register].has(next.opcode)) return;
64
+ }
65
+ });
66
+ }
67
+ return found;
68
+ }
@@ -0,0 +1,108 @@
1
+ // Pointer candidates: code that builds an address from immediates. The
2
+ // byte of each immediate is the low or high byte of the address, so a rebuild
3
+ // that moves the address needs an operand expression there. They are only
4
+ // reported, never written: the LLM records the expressions with c64-knowledge.
5
+ //
6
+ // The scan reads short straight runs of instructions: LDA/LDX/LDY #imm and
7
+ // STA/STX/STY to zero page or absolute, plus SEI, CLI, CLC, SEC and NOP. Any
8
+ // other instruction ends the run, so no register value is guessed across it.
9
+
10
+ import { instructionLength } from "#src/c64.ts";
11
+ import type { ImageByte } from "./coverage.ts";
12
+
13
+ const LOADS: Record<number, "a" | "x" | "y"> = { 0xa9: "a", 0xa2: "x", 0xa0: "y" };
14
+ const STORES: Record<number, "a" | "x" | "y"> = { 0x85: "a", 0x8d: "a", 0x86: "x", 0x8e: "x", 0x84: "y", 0x8c: "y" };
15
+ const NEUTRAL = new Set([0x78, 0x58, 0x18, 0x38, 0xea]);
16
+ /** The instructions of one run: enough for two loads, two stores and a few neutral ones. */
17
+ const MAX_RUN = 8;
18
+ /** The vectors that the KERNAL and the CPU read: IRQ, BRK, NMI, and the hardware vectors. */
19
+ const VECTORS = new Set([0x0314, 0x0316, 0x0318, 0xfffa, 0xfffc, 0xfffe]);
20
+ /** (zp),y: ORA AND EOR ADC STA LDA CMP SBC. */
21
+ const INDIRECT_Y = new Set([0x11, 0x31, 0x51, 0x71, 0x91, 0xb1, 0xd1, 0xf1]);
22
+ /** The sprite pointers of the default screen at $0400 in VIC bank 0. */
23
+ const SPRITE_POINTERS = { start: 0x07f8, end: 0x07ff };
24
+
25
+ export type PointerCandidate =
26
+ | {
27
+ kind: "vector" | "zero-page pointer";
28
+ /** The vector or zero-page pair that gets the address. */
29
+ to: number;
30
+ /** The address that the two immediates make. */
31
+ target: number;
32
+ /** The instructions with the low and the high byte as immediate. */
33
+ low: number;
34
+ high: number;
35
+ }
36
+ | {
37
+ kind: "sprite pointer";
38
+ to: number;
39
+ /** value × 64: the sprite data, in VIC bank 0. */
40
+ target: number;
41
+ /** The instruction with the pointer as immediate. */
42
+ at: number;
43
+ value: number;
44
+ };
45
+
46
+ interface Store {
47
+ /** The address of the load of the immediate. */
48
+ load: number;
49
+ value: number;
50
+ to: number;
51
+ }
52
+
53
+ /** The stores of immediates in the straight run that starts at `start`. */
54
+ function storesFrom(byteAt: ImageByte, start: number, range: { start: number; end: number }): Store[] {
55
+ const last: Partial<Record<"a" | "x" | "y", { load: number; value: number }>> = {};
56
+ const stores: Store[] = [];
57
+ let at = start;
58
+ for (let count = 0; count < MAX_RUN; count++) {
59
+ const opcode = byteAt(at);
60
+ if (opcode === undefined) break;
61
+ const length = instructionLength(opcode);
62
+ if (at + length - 1 > range.end) break;
63
+ const operand = length === 2 ? byteAt(at + 1)! : length === 3 ? byteAt(at + 1)! | (byteAt(at + 2)! << 8) : 0;
64
+ if (LOADS[opcode] !== undefined) last[LOADS[opcode]] = { load: at, value: operand };
65
+ else if (STORES[opcode] !== undefined) {
66
+ const loaded = last[STORES[opcode]];
67
+ if (loaded === undefined) break;
68
+ stores.push({ ...loaded, to: operand });
69
+ } else if (!NEUTRAL.has(opcode)) break;
70
+ at += length;
71
+ }
72
+ return stores;
73
+ }
74
+
75
+ /**
76
+ * Finds the pointer candidates in the image: an immediate pair stored to a
77
+ * vector or to a zero-page pair that (zp),y uses, where the pair makes an
78
+ * address in the image or a platform name; and an immediate stored to a sprite
79
+ * pointer of the default screen whose sprite data is in a sprite region.
80
+ */
81
+ export function findPointers(
82
+ byteAt: ImageByte,
83
+ range: { start: number; end: number },
84
+ known: { platform: (address: number) => boolean; sprites: ReadonlyArray<{ start: number; end: number }> },
85
+ ): PointerCandidate[] {
86
+ const indirect = new Set<number>();
87
+ for (let at = range.start; at < range.end; at++) if (INDIRECT_Y.has(byteAt(at)!)) indirect.add(byteAt(at + 1)!);
88
+ const inImage = (address: number) => address >= range.start && address <= range.end && byteAt(address) !== undefined;
89
+ const found = new Map<string, PointerCandidate>();
90
+ for (let at = range.start; at <= range.end; at++) {
91
+ if (LOADS[byteAt(at)!] === undefined) continue;
92
+ const stores = storesFrom(byteAt, at, range);
93
+ for (const low of stores) {
94
+ const high = stores.find((store) => store.to === low.to + 1);
95
+ const target = high === undefined ? undefined : low.value | (high.value << 8);
96
+ const kind = VECTORS.has(low.to) ? "vector" : low.to < 0xff && indirect.has(low.to) ? "zero-page pointer" : undefined;
97
+ if (high !== undefined && target !== undefined && kind !== undefined && (inImage(target) || known.platform(target))) {
98
+ found.set(`${low.load}|${high.load}`, { kind, to: low.to, target, low: low.load, high: high.load });
99
+ }
100
+ const sprite = low.value * 64;
101
+ if (low.to >= SPRITE_POINTERS.start && low.to <= SPRITE_POINTERS.end && known.sprites.some((region) => sprite >= region.start && sprite <= region.end)) {
102
+ found.set(`${low.load}`, { kind: "sprite pointer", to: low.to, target: sprite, at: low.load, value: low.value });
103
+ }
104
+ }
105
+ }
106
+ const first = (candidate: PointerCandidate) => (candidate.kind === "sprite pointer" ? candidate.at : Math.min(candidate.low, candidate.high));
107
+ return [...found.values()].sort((a, b) => first(a) - first(b));
108
+ }
@@ -22,11 +22,11 @@ A PASS proves only the behavior that the scenario does and observes. It does not
22
22
  The script starts one emulator through the host runtime. It runs the scenario on the original and then on the rebuild. Before each side, it does a hard reset of the emulator. The CPU then stops at the reset vector.
23
23
 
24
24
  ```
25
- node <skill>/scripts/test.ts tests/moves-left.json --out results/moves-left
25
+ node <skill>/scripts/test.ts tests/moves-left.json --out .c64-re-tools/local/results/moves-left
26
26
  node <skill>/scripts/checklist.ts tests/moves-left.json --item 'FIRE starts the game.'
27
27
  ```
28
28
 
29
- The test script prints one JSON object. The exit status is 0 for PASS, 1 for FAIL, 3 for INCONCLUSIVE and 2 for a bad call. With `--out`, a FAIL writes files into that directory: both screens and the difference image for each screen difference, and `memory.json` with each byte that differs. `files` in the result names them.
29
+ The test script prints one JSON object. The exit status is 0 for PASS, 1 for FAIL, 3 for INCONCLUSIVE and 2 for a bad call. With `--out`, a FAIL writes files into that directory: both screens and the difference image for each screen difference, and `memory.json` with each byte that differs. `files` in the result names them. A PASS writes no files. To keep the screens of a PASS, use a run (below).
30
30
 
31
31
  The checklist script prints a Markdown checklist for a human tester.
32
32
 
@@ -35,31 +35,60 @@ The checklist script prints a Markdown checklist for a human tester.
35
35
  A run does steps on one program, with no comparison, and keeps the results as files. Use it for many steps. One run replaces many emulator calls, for example a visit to each room of a game.
36
36
 
37
37
  ```
38
- node <skill>/scripts/run.ts runs/rooms.json --out runs/rooms
38
+ node <skill>/scripts/run.ts .c64-re-tools/local/runs/rooms/run.json --out .c64-re-tools/local/runs/rooms
39
39
  ```
40
40
 
41
41
  The run file gives where to start (a program, or a snapshot file), the steps, and optional symbols. To go on from the live emulator, save its state with `c64_snapshot` action `save` and `out`, and start the run from that file. Besides the steps of a scenario, a run has these steps:
42
42
 
43
43
  | Step | Effect |
44
44
  |---|---|
45
- | `{ "snapshot": "snapshots/room0.vsf" }` | Restore a snapshot file. |
45
+ | `{ "snapshot": ".c64-re-tools/local/snapshots/room0.vsf" }` | Restore a snapshot file. |
46
46
  | `{ "save": "room3" }` | Write the state now to `room3.vsf` in the output directory. |
47
47
  | `{ "screen": "room3" }` | Write the screen to `room3.png`. |
48
- | `{ "coverage": "room3" }` | Write the coverage map since the start or the last coverage step to `room3.bin`, then clear the map. |
48
+ | `{ "coverage": "room3" }` | Write the coverage map since the start or the last coverage step to `room3.bin`, then clear the map. Add the map to the knowledge with `coverage` of the c64-knowledge skill. |
49
49
  | `{ "observe": "room3", "memory": ["$c100"], "registers": true }` | Record memory, registers, `vicii` or `sprites` in `results.json`. |
50
+ | `{ "trace": "d011", "points": [{ "kind": "write", "at": "$d011" }], "frames": 100 }` | Record each hit of the points for 100 frames in `d011.json`: the instruction, its registers and the raster line and cycle. A point is `exec`, `read` or `write`, at an address or a name, with an optional `end`. `maxHits` limits the hits. |
51
+ | `{ "observe": "all3", "memory": "all" }` | Write all 64 KiB of RAM to `all3.bin`, also the RAM under the ROMs and the I/O area. |
52
+
53
+ A memory item can have `"view": "ram"`. Then it reads the RAM under the ROMs and the I/O area, for example code at `$e000` under the KERNAL. The default view `cpu` reads as the CPU sees memory. An item larger than 4096 bytes goes into its own `.bin` file, and `results.json` gives the file name.
50
54
 
51
55
  See [references/scenario-format.md](references/scenario-format.md), section Runs. The script prints one JSON object. The exit status is 0 when all steps ran, 1 when a step did not reach its target, and 2 for a bad call. A long run can need more time than one shell call. Then run it in the background.
52
56
 
57
+ ### Differences
58
+
59
+ ```
60
+ node <skill>/scripts/diff.ts .c64-re-tools/local/runs/rooms/before.vsf .c64-re-tools/local/runs/rooms/after.vsf
61
+ node <skill>/scripts/diff.ts .c64-re-tools/local/runs/rooms/results.json:room0 .c64-re-tools/local/runs/rooms/results.json:room3
62
+ ```
63
+
64
+ `diff.ts` tells what changed between two states of one kind. Use it in place of your own compare script.
65
+
66
+ - Two snapshot files: the script restores each one in its own emulator. It gives the changed ranges of the RAM and of the colour RAM, and the changed VIC-II, CIA, SID and CPU registers.
67
+ - Two memory files of a run: the changed ranges. For a file of less than 64 KiB, `--at` gives the address of its first byte.
68
+ - Two observations of runs, also of one run: the changed memory items and registers.
69
+
70
+ `--gap N` joins ranges across N equal bytes. `--out` writes all ranges to a project file.
71
+
72
+ ### Project folder
73
+
74
+ - 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/`.
75
+ - 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.
76
+ - Write each file that you make for your work in `.c64-re-tools/local/` of the project. Git ignores that folder. Examples are results, batches, step files, runs, snapshots and memory dumps.
77
+ - Move a file out of `.c64-re-tools/local/` only when it is the evidence for a finding. Move it before you give it to `--evidence` of c64-knowledge. `--evidence` refuses a file in `.c64-re-tools/local/`.
78
+ - Put a run that is evidence in `runs/<name>/`, with its run file, its results and the snapshot files that it uses. Change the paths in the run file to the new paths. Then git keeps them, and another person can do the run again.
79
+ - 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`.
80
+
53
81
  ## Workflow
54
82
 
55
83
  1. Write the scenario file. Start the steps with a frame count, for example `{ "frames": 150 }`. Each side starts at the reset vector, and the C64 needs these frames to start.
56
84
  2. Use symbol names, for example `player_x`, not fixed addresses. Each side finds the name in its own symbols. Use a `$` address only when both programs use the same address.
57
85
  3. Make each checkpoint a condition, for example `runUntil` at a routine. Do not wait for a time.
58
- 4. Save the c64-assembler output of the rebuild to the symbol file: `assemble.ts ... > build/game.json`.
86
+ 4. Save the c64-assembler output of the rebuild to the symbol file, for example `build/game.json` in the project folder. Give `--all-symbols`, so that the file also has the constants that the source does not use. From the folder above the project, write `assemble.ts --project <folder> ... > <folder>/build/game.json`.
59
87
  5. Run the test script.
60
88
  6. Read the result:
61
89
  - `PASS`: all observations are equal, or the differences are in the allowed tolerance.
62
90
  - `FAIL`: `differences` lists each difference with the checkpoint, the item, the original value and the rebuild value.
91
+ - Each value has one form. The pc and each address are `$xxxx`. Memory is lowercase hex. Other register values and chip values are decimal numbers.
63
92
  - `INCONCLUSIVE`: `reason` tells why the script could not compare. This is not a failure of the program.
64
93
  7. For a FAIL, examine the first difference with the c64-emulator skill.
65
94
 
@@ -89,7 +118,7 @@ PASS, FAIL or INCONCLUSIVE, the checkpoints and the smallest differences that ex
89
118
  - `installation-incomplete`, or `machine-unavailable` with a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own disk parser, disassembler or emulator script.
90
119
  - A FAIL with only a `screen` difference: add `vicii` or `sprites` to the observation to find which chip state is different.
91
120
  - A program that uses random numbers: set the random seed with a `write` step on both sides. If that is not possible, observe only values that do not depend on the random numbers.
92
- - The two sides start differently, for example the original after a cracked intro. Give the steps that only one side needs a `side`. Then make the memory the same with a `normalize` step from the entry state of the c64-unpacker skill.
121
+ - The two sides start differently, for example the original after a cracked intro. Give the steps that only one side needs a `side`. Then make the memory the same with a `normalize` step. It writes the entry state of the capture in the knowledge.
93
122
  - Frame counts drift by one frame between the sides: the sides got to the same address at different raster positions. Do not sync on an IRQ handler. Sync on a value that the IRQ changes, with `runUntil` `memory`, or on an address and a raster line together.
94
123
  - A byte that cannot be the same on both sides, for example a read-only collision register: use `exclude` with the reason. Do not use `exclude` to hide a real difference.
95
124