@henols/c64-re-tools 2.0.0-rc.3 → 2.0.0-rc.5

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 (85) hide show
  1. package/README.md +17 -3
  2. package/distribution/package.ts +1 -16
  3. package/distribution/plugin.ts +5 -3
  4. package/package.json +1 -1
  5. package/skills/c64-assembler/SKILL.md +5 -2
  6. package/skills/c64-assembler/scripts/assemble.ts +8 -1
  7. package/skills/c64-assembler/scripts/compare.ts +84 -0
  8. package/skills/c64-basic/SKILL.md +4 -5
  9. package/skills/c64-disk/SKILL.md +20 -6
  10. package/skills/c64-disk/scripts/containers.ts +124 -0
  11. package/skills/c64-disk/scripts/disk.ts +59 -5
  12. package/skills/c64-emulator/SKILL.md +12 -5
  13. package/skills/c64-knowledge/SKILL.md +68 -4
  14. package/skills/c64-knowledge/scripts/knowledge.ts +262 -30
  15. package/skills/c64-memory-map/SKILL.md +4 -1
  16. package/skills/c64-memory-map/scripts/memmap.ts +14 -3
  17. package/skills/c64-provenance/SKILL.md +38 -9
  18. package/skills/c64-provenance/scripts/compare.ts +60 -29
  19. package/skills/c64-provenance/scripts/provenance.ts +250 -19
  20. package/skills/c64-provenance/scripts/runtime.ts +191 -0
  21. package/skills/c64-provenance/scripts/separate.ts +132 -0
  22. package/skills/c64-reverse-engineering/SKILL.md +65 -11
  23. package/skills/c64-source/SKILL.md +119 -0
  24. package/skills/c64-source/scripts/generate.ts +445 -0
  25. package/skills/c64-source/scripts/loader.ts +324 -0
  26. package/skills/c64-source/scripts/opcodes.ts +61 -0
  27. package/skills/c64-source/scripts/source.ts +560 -0
  28. package/skills/c64-static-analysis/SKILL.md +28 -5
  29. package/skills/c64-static-analysis/scripts/analyze.ts +107 -14
  30. package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
  31. package/skills/c64-static-analysis/scripts/findings.ts +6 -4
  32. package/skills/c64-static-analysis/scripts/idioms.ts +144 -0
  33. package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
  34. package/skills/c64-testing/SKILL.md +27 -3
  35. package/skills/c64-testing/references/scenario-format.md +43 -3
  36. package/skills/c64-testing/scripts/run.ts +189 -0
  37. package/skills/c64-testing/scripts/scenario.ts +153 -135
  38. package/skills/c64-testing/scripts/test.ts +16 -6
  39. package/skills/c64-unpacker/SKILL.md +69 -18
  40. package/skills/c64-unpacker/scripts/evidence.ts +113 -41
  41. package/skills/c64-unpacker/scripts/loads.ts +197 -0
  42. package/skills/c64-unpacker/scripts/machine.ts +68 -0
  43. package/skills/c64-unpacker/scripts/stages.ts +67 -0
  44. package/skills/c64-unpacker/scripts/unpack.ts +429 -55
  45. package/src/c64.ts +126 -10
  46. package/src/cli/main.ts +7 -4
  47. package/src/cli/status.ts +11 -7
  48. package/src/host/main.ts +8 -3
  49. package/src/host/server.ts +6 -5
  50. package/src/host/tools/petcat.ts +5 -1
  51. package/src/host/tools/status.ts +8 -2
  52. package/src/host/vice/adapter.ts +25 -10
  53. package/src/host/vice/process.ts +10 -5
  54. package/src/host/vice/session.ts +89 -32
  55. package/src/host-client/autostart.ts +45 -0
  56. package/src/host-client/connect.ts +5 -2
  57. package/src/host-client/coverage.ts +54 -0
  58. package/src/host-client/steps.ts +170 -0
  59. package/src/host-client/transfer.ts +29 -3
  60. package/src/host-client/vice-session.ts +79 -17
  61. package/src/knowledge/history.ts +48 -13
  62. package/src/knowledge/import.ts +104 -57
  63. package/src/knowledge/read.ts +203 -52
  64. package/src/knowledge/schema.ts +94 -12
  65. package/src/knowledge/write.ts +433 -123
  66. package/src/mcp/main.ts +43 -15
  67. package/src/mcp/output.ts +32 -0
  68. package/src/mcp/server.ts +13 -4
  69. package/src/mcp/tools/debug.ts +110 -24
  70. package/src/mcp/tools/execution.ts +3 -2
  71. package/src/mcp/tools/input.ts +7 -4
  72. package/src/mcp/tools/media.ts +26 -9
  73. package/src/mcp/tools/memory.ts +57 -10
  74. package/src/mcp/tools/video.ts +9 -3
  75. package/src/native/discover.ts +43 -6
  76. package/src/native/ghidra/index.ts +19 -3
  77. package/src/native/ghidra/scripts/C64Export.java +33 -1
  78. package/src/native/status.ts +15 -3
  79. package/src/native/types.ts +30 -1
  80. package/src/package.ts +21 -0
  81. package/{skills/c64-memory-map/scripts → src}/platform.ts +18 -2
  82. package/src/protocol/messages.ts +17 -1
  83. package/src/protocol/tools.ts +7 -3
  84. package/src/protocol/vice.ts +86 -24
  85. package/src/script.ts +3 -2
@@ -0,0 +1,191 @@
1
+ // Runtime evidence for the differences between releases (D50): each release
2
+ // runs in its own emulator, and each differing range gets its first access
3
+ // (read, write, execute or none) and whether its bytes changed by the end.
4
+ // Watchpoints and range breakpoints stop the run at an access; the coverage
5
+ // map then tells which range and which access it was.
6
+
7
+ import { COVERAGE, formatC64Address, instructionLength } from "#src/c64.ts";
8
+ import { runProgram } from "#src/host-client/autostart.ts";
9
+ import { readCoverage } from "#src/host-client/coverage.ts";
10
+ import { runActionStep, type ActionStep } from "#src/host-client/steps.ts";
11
+ import { MAX_MEMORY_READ, MAX_MEMORY_WRITE } from "#src/host-client/tools.ts";
12
+ import { readProjectFile } from "#src/host-client/transfer.ts";
13
+ import type { ViceSessionClient } from "#src/host-client/vice-session.ts";
14
+ import type { Range } from "./compare.ts";
15
+
16
+ export type FirstAccess = "read" | "write" | "execute" | "none";
17
+
18
+ export interface RangeAccess {
19
+ first: FirstAccess;
20
+ /** True when the bytes at the end differ from the bytes of the release. */
21
+ changed: boolean;
22
+ }
23
+
24
+ /** How a release starts in the emulator. */
25
+ export type Start =
26
+ | { kind: "program"; path: string }
27
+ | { kind: "image"; segments: Range[]; byteAt: (address: number) => number; state: ImageState };
28
+
29
+ export interface ImageState {
30
+ pc: number;
31
+ a: number;
32
+ x: number;
33
+ y: number;
34
+ sp: number;
35
+ flags: { n: boolean; v: boolean; d: boolean; i: boolean; z: boolean; c: boolean };
36
+ port: [number, number];
37
+ /** $0000-$03ff at the entry. */
38
+ pages: Uint8Array;
39
+ }
40
+
41
+ const MAX_WATCH = 256;
42
+ /** Frames until the KERNAL is at the READY prompt, before an image is written. */
43
+ const READY_FRAMES = 150;
44
+
45
+ const READ = COVERAGE.ramRead | COVERAGE.romRead | COVERAGE.ioRead;
46
+ const WRITE = COVERAGE.ramWrite | COVERAGE.ioWrite;
47
+ const EXECUTE = COVERAGE.ramExecute | COVERAGE.romExecute | COVERAGE.ioExecute;
48
+
49
+ /** The first access that coverage flags show; a read-modify-write reads first. */
50
+ export function accessOf(flags: number): FirstAccess {
51
+ if ((flags & EXECUTE) !== 0) return "execute";
52
+ if ((flags & READ) !== 0) return "read";
53
+ if ((flags & WRITE) !== 0) return "write";
54
+ return "none";
55
+ }
56
+
57
+ /** Starts the release; returns why it did not start, or undefined. */
58
+ async function start(client: ViceSessionClient, how: Start): Promise<string | undefined> {
59
+ if (how.kind === "program") {
60
+ const failed = await runProgram(client, how.path, readProjectFile(how.path).bytes);
61
+ if (failed === "not-loaded") return "The program was not in memory after autostart.";
62
+ if (failed === "not-started") return "The program did not start after RUN; it needs a BASIC start line.";
63
+ return undefined;
64
+ }
65
+ await client.reset({ mode: "hard", run: true });
66
+ await client.execution({ action: "advance-frames", count: READY_FRAMES, space: "c64" });
67
+ for (const segment of how.segments) {
68
+ for (let at = segment.start; at <= segment.end; at += MAX_MEMORY_WRITE) {
69
+ const last = Math.min(segment.end, at + MAX_MEMORY_WRITE - 1);
70
+ const bytes = Buffer.from(Array.from({ length: last - at + 1 }, (_, offset) => how.byteAt(at + offset)));
71
+ await client.memoryWrite({ address: at, data: bytes.toString("hex"), space: "c64", view: "ram" });
72
+ }
73
+ }
74
+ const { state } = how;
75
+ // Pages 0 to 3 from $0002, then the CPU port: the port bits switch what the CPU sees.
76
+ await client.memoryWrite({ address: 0x0002, data: Buffer.from(state.pages.subarray(2)).toString("hex"), space: "c64", view: "ram" });
77
+ await client.memoryWrite({ address: 0x0000, data: Buffer.from(state.port).toString("hex"), space: "c64", view: "cpu" });
78
+ await client.registersSet("c64", { pc: state.pc, a: state.a, x: state.x, y: state.y, sp: state.sp, flags: state.flags });
79
+ return undefined;
80
+ }
81
+
82
+ /**
83
+ * Runs one release and watches the ranges (addresses of the release, after
84
+ * any shift is taken away). `release` gives the release's own bytes.
85
+ */
86
+ export async function watchRelease(
87
+ client: ViceSessionClient,
88
+ how: Start,
89
+ ranges: Range[],
90
+ release: (address: number) => number,
91
+ run: { frames: number; steps: ActionStep[] },
92
+ ): Promise<{ started: true; accesses: RangeAccess[] } | { started: false; reason: string }> {
93
+ await client.reset({ mode: "hard", run: false });
94
+ const failed = await start(client, how);
95
+ if (failed !== undefined) return { started: false, reason: failed };
96
+ if ((await client.status()).state === "running") await client.execution({ action: "pause", space: "c64" });
97
+
98
+ const first: FirstAccess[] = ranges.map(() => "none");
99
+ const points = new Map<number, Array<{ kind: "watch" | "break"; id: number }>>();
100
+ for (const [index, range] of ranges.entries()) {
101
+ const ids: Array<{ kind: "watch" | "break"; id: number }> = [];
102
+ for (let at = range.start; at <= range.end; at += MAX_WATCH) {
103
+ const size = Math.min(MAX_WATCH, range.end - at + 1);
104
+ ids.push({ kind: "watch", id: (await client.watchpoint({ action: "add", address: at, size, access: "read-write", space: "c64" })).id });
105
+ ids.push({ kind: "break", id: (await client.breakpoint({ action: "add", address: at, size, space: "c64" })).id });
106
+ }
107
+ points.set(index, ids);
108
+ }
109
+ await client.memmap({ action: "clear" });
110
+
111
+ /**
112
+ * After a stop: the ranges that the coverage shows accessed get their first
113
+ * access, and lose their points. VICE marks only the opcode byte of an
114
+ * executed instruction, so an instruction that starts up to two bytes before
115
+ * a range, or in it, executes its operand bytes too (F-27).
116
+ */
117
+ const settle = async (): Promise<boolean> => {
118
+ const map = await readCoverage(client);
119
+ let found = false;
120
+ for (const [index, ids] of points) {
121
+ const range = ranges[index]!;
122
+ let flags = 0;
123
+ for (let address = range.start; address <= range.end; address++) flags |= map[address]!;
124
+ const from = Math.max(0, range.start - 2);
125
+ if ((flags & EXECUTE) === 0 && map.subarray(from, range.start).some((byte) => (byte & COVERAGE.ramExecute) !== 0)) {
126
+ const code = Buffer.from((await client.memoryRead({ address: from, size: range.start - from, space: "c64", view: "ram" })).data, "hex");
127
+ for (let address = from; address < range.start; address++) {
128
+ if ((map[address]! & COVERAGE.ramExecute) !== 0 && address + instructionLength(code[address - from]!) - 1 >= range.start) flags |= COVERAGE.ramExecute;
129
+ }
130
+ }
131
+ if (accessOf(flags) === "none") continue;
132
+ first[index] = accessOf(flags);
133
+ for (const point of ids) {
134
+ if (point.kind === "watch") await client.watchpoint({ action: "remove", id: point.id });
135
+ else await client.breakpoint({ action: "remove", id: point.id });
136
+ }
137
+ points.delete(index);
138
+ found = true;
139
+ }
140
+ return found;
141
+ };
142
+
143
+ // Each stop at a point settles at least one range, so the stops are bounded; the guard ends a run that stops without an access.
144
+ let guard = 4 * ranges.length + 16;
145
+ /** Runs frames; a stop at a point settles and goes on with the frames that are left. */
146
+ const runFrames = async (count: number) => {
147
+ let left = count;
148
+ while (left > 0 && guard-- > 0) {
149
+ const ran = await client.execution({ action: "advance-frames", count: left, space: "c64" });
150
+ left -= ran.advancedFrames ?? left;
151
+ if (left > 0 && !(await settle())) left--;
152
+ }
153
+ };
154
+ for (const step of run.steps) {
155
+ if ("frames" in step) {
156
+ await runFrames(step.frames);
157
+ continue;
158
+ }
159
+ if ("runUntil" in step) {
160
+ // A stop at a point is not the target: settle and run to the target again.
161
+ while (guard-- > 0) {
162
+ const reached = await runActionStep(client, step, (location) => Number.parseInt(location.slice(1), 16));
163
+ if (reached === undefined || reached.stopReason !== "watchpoint" && reached.stopReason !== "breakpoint") break;
164
+ await settle();
165
+ }
166
+ continue;
167
+ }
168
+ await runActionStep(client, step, (location) => Number.parseInt(location.slice(1), 16));
169
+ }
170
+ await runFrames(run.frames);
171
+ if (points.size > 0) await settle();
172
+
173
+ const accesses: RangeAccess[] = [];
174
+ for (const [index, range] of ranges.entries()) {
175
+ const parts: Buffer[] = [];
176
+ for (let at = range.start; at <= range.end; at += MAX_MEMORY_READ) {
177
+ parts.push(Buffer.from((await client.memoryRead({ address: at, size: Math.min(MAX_MEMORY_READ, range.end - at + 1), space: "c64", view: "ram" })).data, "hex"));
178
+ }
179
+ const now = Buffer.concat(parts);
180
+ accesses.push({ first: first[index]!, changed: now.some((byte, offset) => byte !== release(range.start + offset)) });
181
+ }
182
+ for (const ids of points.values()) {
183
+ for (const point of ids) {
184
+ if (point.kind === "watch") await client.watchpoint({ action: "remove", id: point.id });
185
+ else await client.breakpoint({ action: "remove", id: point.id });
186
+ }
187
+ }
188
+ return { started: true, accesses };
189
+ }
190
+
191
+ export const showRange = (range: Range) => `${formatC64Address(range.start)}-${formatC64Address(range.end)}`;
@@ -0,0 +1,132 @@
1
+ // Evidence from one release (D51): for each range of the image, the unpack
2
+ // stage that wrote it and the access class from coverage; for each code
3
+ // region of knowledge, whether call and jump references reach it from the
4
+ // entry; and what the program reads or executes outside the image. Evidence
5
+ // only: nothing here decides what is original.
6
+
7
+ import { COVERAGE, executedRamCode } from "#src/c64.ts";
8
+ import type { Range } from "./compare.ts";
9
+
10
+ export type Access = "executed" | "read" | "written" | "read-written" | "untouched";
11
+
12
+ export interface SeparationInput {
13
+ segments: Range[];
14
+ /** 64 KiB with the image bytes at their addresses. */
15
+ memory: Uint8Array;
16
+ entry?: number;
17
+ /** The stages of an unpacker capture, in order; the last one is the program. */
18
+ stages?: Array<{ entry: number; wrote: Range[] }>;
19
+ /** Joined coverage files (D40). */
20
+ coverage?: Uint8Array;
21
+ codeRegions: Array<Range & { type: string }>;
22
+ references: Array<{ from: number; to: number; kind: string }>;
23
+ }
24
+
25
+ export interface EvidenceRange extends Range {
26
+ /** The last stage that wrote the range; absent when no stage wrote it. */
27
+ stage?: number;
28
+ access?: Access;
29
+ }
30
+
31
+ export interface CodeRegionEvidence extends Range {
32
+ type: string;
33
+ /** Absent when the image has no entry. */
34
+ reachable?: boolean;
35
+ /** Absent without coverage. */
36
+ executed?: boolean;
37
+ }
38
+
39
+ export interface Separation {
40
+ ranges: EvidenceRange[];
41
+ codeRegions: CodeRegionEvidence[];
42
+ /**
43
+ * RAM outside the image and outside pages 0-3, from the coverage: `read`
44
+ * and `executed` the program never wrote (a loader or an earlier stage put
45
+ * those bytes there), and `generated` it wrote and then executed.
46
+ */
47
+ outside: { read: Range[]; executed: Range[]; generated: Range[] };
48
+ }
49
+
50
+ const SYSTEM_PAGES_END = 0x03ff;
51
+
52
+ function merge(addresses: number[]): Range[] {
53
+ const ranges: Range[] = [];
54
+ for (const address of addresses) {
55
+ const last = ranges.at(-1);
56
+ if (last !== undefined && last.end + 1 === address) last.end = address;
57
+ else ranges.push({ start: address, end: address });
58
+ }
59
+ return ranges;
60
+ }
61
+
62
+ export function separate(input: SeparationInput): Separation {
63
+ const inImage = (address: number) => input.segments.some((segment) => address >= segment.start && address <= segment.end);
64
+ const executed = new Uint8Array(0x10000);
65
+ if (input.coverage !== undefined) for (const range of executedRamCode(input.coverage, 0, input.memory)) executed.fill(1, range.start, range.end + 1);
66
+ const accessAt = (address: number): Access | undefined => {
67
+ if (input.coverage === undefined) return undefined;
68
+ if (executed[address] === 1) return "executed";
69
+ const flags = input.coverage[address]!;
70
+ const read = (flags & COVERAGE.ramRead) !== 0;
71
+ const written = (flags & COVERAGE.ramWrite) !== 0;
72
+ return read && written ? "read-written" : read ? "read" : written ? "written" : "untouched";
73
+ };
74
+ const stageAt = (address: number): number | undefined => {
75
+ const stages = input.stages ?? [];
76
+ for (let index = stages.length - 1; index >= 0; index--) if (stages[index]!.wrote.some((range) => address >= range.start && address <= range.end)) return index;
77
+ return undefined;
78
+ };
79
+
80
+ const ranges: EvidenceRange[] = [];
81
+ for (const segment of input.segments) {
82
+ for (let address = segment.start; address <= segment.end; address++) {
83
+ const stage = stageAt(address);
84
+ const access = accessAt(address);
85
+ const last = ranges.at(-1);
86
+ if (last !== undefined && last.end + 1 === address && last.stage === stage && last.access === access) {
87
+ last.end = address;
88
+ continue;
89
+ }
90
+ ranges.push({ start: address, end: address, ...(stage === undefined ? {} : { stage }), ...(access === undefined ? {} : { access }) });
91
+ }
92
+ }
93
+
94
+ // Reachability: from the region that holds the entry, through call and jump references, to a fixed point.
95
+ const regions = input.codeRegions.filter((region) => input.segments.some((segment) => region.start <= segment.end && segment.start <= region.end));
96
+ const holding = (address: number) => regions.findIndex((region) => address >= region.start && address <= region.end);
97
+ const reached = new Set<number>();
98
+ if (input.entry !== undefined && holding(input.entry) >= 0) reached.add(holding(input.entry));
99
+ const flows = input.references.filter((reference) => reference.kind === "call" || reference.kind === "jump");
100
+ for (let grown = true; grown; ) {
101
+ grown = false;
102
+ for (const flow of flows) {
103
+ const from = holding(flow.from);
104
+ const to = holding(flow.to);
105
+ if (from >= 0 && to >= 0 && reached.has(from) && !reached.has(to)) {
106
+ reached.add(to);
107
+ grown = true;
108
+ }
109
+ }
110
+ }
111
+ const codeRegions = regions.map((region, index): CodeRegionEvidence => {
112
+ let ran: boolean | undefined;
113
+ if (input.coverage !== undefined) {
114
+ ran = false;
115
+ for (let address = region.start; address <= region.end && !ran; address++) ran = executed[address] === 1;
116
+ }
117
+ return { start: region.start, end: region.end, type: region.type, ...(input.entry === undefined ? {} : { reachable: reached.has(index) }), ...(ran === undefined ? {} : { executed: ran }) };
118
+ });
119
+
120
+ const read: number[] = [];
121
+ const ranOutside: number[] = [];
122
+ const generated: number[] = [];
123
+ if (input.coverage !== undefined) {
124
+ for (let address = SYSTEM_PAGES_END + 1; address < 0x10000; address++) {
125
+ if (inImage(address)) continue;
126
+ const written = (input.coverage[address]! & COVERAGE.ramWrite) !== 0;
127
+ if (executed[address] === 1) (written ? generated : ranOutside).push(address);
128
+ else if (!written && (input.coverage[address]! & COVERAGE.ramRead) !== 0) read.push(address);
129
+ }
130
+ }
131
+ return { ranges, codeRegions, outside: { read: merge(read), executed: merge(ranOutside), generated: merge(generated) } };
132
+ }
@@ -28,6 +28,13 @@ This skill uses the other skills. It does not use the host runtime or the emulat
28
28
 
29
29
  ## Workflow
30
30
 
31
+ Select the path for the scope:
32
+
33
+ - A narrow scope, for example one routine, the IRQ handler or the input system: use the loop.
34
+ - A full rebuild of the program: use the stages.
35
+
36
+ ### The loop
37
+
31
38
  1. Write down the scope. Stop when the scope is complete.
32
39
  2. Read the current knowledge for the subject with the c64-knowledge skill. Do not examine again what the knowledge already explains.
33
40
  3. Find the next unknown or blocker, and use the specialist for it:
@@ -41,24 +48,70 @@ This skill uses the other skills. It does not use the host runtime or the emulat
41
48
  | The routines, code, data and references of the program | c64-static-analysis |
42
49
  | The platform meaning of an address or a register | c64-memory-map |
43
50
  | Differences between releases of the program | c64-provenance |
51
+ | Source code from the knowledge | c64-source |
44
52
  | Source code to build | c64-assembler |
45
53
  | Proof that a rebuild behaves as the original | c64-testing |
46
54
 
47
55
  4. Interpret the result. Keep observed facts apart from your conclusions.
48
56
  5. Record each conclusion that the evidence supports with the c64-knowledge skill: names of routines and variables, data classifications, comments.
49
57
  6. Go back to step 3 until the scope is complete.
50
- 7. For a rebuild scope: write the source, build it with the c64-assembler skill, and test it against the original with the c64-testing skill.
51
58
 
52
- A typical order for an unknown program:
59
+ ### The stages of a full rebuild
60
+
61
+ Do the stages in this sequence. Go to the next stage only when its exit check is true, or when the user accepts an exception. Write each exception in the report.
62
+
63
+ | Stage | Work | Exit check |
64
+ |---|---|---|
65
+ | 1. Acceptance | Ask the user for the criteria of the rebuild. Offer these: identical source, a verified loader, passed c64-testing scenarios, a list of the code that no run executed. Write the criteria in `report.md`. | The user gave the criteria. |
66
+ | 2. Get | c64-disk: list the disk. Extract the start file into `original/`. | The start file is in the project. |
67
+ | 3. Start | c64-basic: find the SYS address of the BASIC start. | You have the entry address. |
68
+ | 4. Unpack | c64-unpacker: examine the program. Capture a packed program into `unpacked/`. | `image.json` and `entry_state.json` exist, or the evidence shows no packer. |
69
+ | 5. Run | c64-emulator and the run of c64-testing: run each part that the scope names. Write a coverage file for each part into `coverage/`. | Each part that the scope names has a coverage file. |
70
+ | 6. Analyze | c64-static-analysis with `--coverage` for each coverage file. | No coverage conflict is open. |
71
+ | 7. Name | c64-knowledge and c64-memory-map: name each routine in the scope and give it a comment. Record the modules. | Each routine in the scope has a name and a comment. |
72
+ | 8. Generate | c64-source with `--coverage` into `src/`. For an unpacked program, add `--loader`. | `check.identical` is true. For an unpacked program, `loader.verified` is true. |
73
+ | 9. Build and test | c64-assembler with `--compare` into `build/`. c64-testing scenarios in `tests/`. | The criteria of stage 1 are true. |
74
+ | 10. Report | Write `report.md`. | The report has all its sections. |
75
+
76
+ In stage 5, if you cannot get to a part of the program, ask the user how to get to it. Do stages 7 and 8 again until the source is clear. Each run of c64-source writes the source again.
77
+
78
+ ### Deliverables of a full rebuild
79
+
80
+ - `.c64-re-tools/knowledge.db` with the names, regions, comments and modules
81
+ - the image and the entry state
82
+ - the coverage files
83
+ - the generated source
84
+ - the built program and the loader
85
+ - the c64-testing scenarios and their results
86
+ - `report.md`.
87
+
88
+ ### The report
89
+
90
+ `report.md` is short. It has these sections:
91
+
92
+ 1. Acceptance: each criterion of stage 1 and its result, and each exception that the user accepted.
93
+ 2. The program: what the program does, in a few sentences.
94
+ 3. Modules: the file, the address range and the title of each module.
95
+ 4. Untested code: the `notExecuted` ranges from c64-source. Tell why no run executed them, if you know.
96
+ 5. Open questions: what the evidence does not explain.
97
+
98
+ Keep observed facts apart from your conclusions.
99
+
100
+ ### Project layout
101
+
102
+ If the project has no layout, use this one. It is a proposal. No script checks it. Each script takes the paths that you give.
103
+
104
+ ```
105
+ original/ the disk image or the program as you got it
106
+ unpacked/ image.json, image.bin and entry_state.json from c64-unpacker
107
+ coverage/ coverage files from c64_memmap
108
+ src/ the generated source from c64-source
109
+ build/ programs from c64-assembler
110
+ tests/ c64-testing scenarios and their results
111
+ report.md the report
112
+ ```
53
113
 
54
- 1. c64-disk: list the disk and extract the start file.
55
- 2. c64-basic: find the SYS address of the BASIC start.
56
- 3. c64-unpacker: find out if a packer compressed the program. Unpack it if necessary.
57
- 4. c64-emulator: run the program and observe it.
58
- 5. c64-static-analysis: find the routines, regions and references.
59
- 6. c64-knowledge and c64-memory-map: name the important routines and variables.
60
- 7. c64-static-analysis again: Ghidra now uses the names.
61
- 8. c64-assembler and c64-testing: rebuild and test when the scope asks for it.
114
+ If the project has a layout, use that layout.
62
115
 
63
116
  ## Knowledge
64
117
 
@@ -76,10 +129,11 @@ Record durable conclusions with the c64-knowledge skill. Do not store:
76
129
 
77
130
  ## Result
78
131
 
79
- A clear answer for the scope, with the durable conclusions in the project knowledge. For a rebuild scope, also a rebuilt program that passes its tests.
132
+ A clear answer for the scope, with the durable conclusions in the project knowledge. For a full rebuild, the deliverables and the report, with the criteria of the user met or each exception in the report.
80
133
 
81
134
  ## Failure and conflicts
82
135
 
136
+ - A skill reports `installation-incomplete`, or a message that the host runtime does not run: stop the whole task. Give the message to the user and wait for the user. Do not continue with other skills. Do not make a replacement for the tool or the host runtime, for example your own disk parser, disassembler or emulator script.
83
137
  - If the evidence does not support a conclusion, keep the unknown and get more evidence. Do not make up a meaning.
84
138
  - Before you replace current knowledge from the user or the LLM, read its history and the evidence for it.
85
139
  - A conflict from the static analysis: find the correct answer at run time with the c64-emulator skill.
@@ -0,0 +1,119 @@
1
+ ---
2
+ name: c64-source
3
+ description: Use this skill to generate ACME source code for a C64 program from its image and the project knowledge. The script runs Ghidra for the instructions and writes the source files. It checks with ACME that the source assembles to the same bytes as the image. It can also write a loader that sets the entry state of an unpacked program. Use it to make a rebuild. Do not edit the generated files. Change the knowledge and generate again.
4
+ ---
5
+
6
+ # c64-source
7
+
8
+ ## Purpose
9
+
10
+ Generate ACME source from an image and the knowledge. The same knowledge gives the same source. Make the source better through the knowledge: names, comments, regions and modules. Then generate again.
11
+
12
+ This skill does not change the knowledge.
13
+
14
+ ## Inputs
15
+
16
+ - The image: an `image.json` from c64-unpacker capture, or a PRG. The path is relative to the project directory.
17
+ - The output directory for the source files, relative to the project directory.
18
+ - Optional entry points for Ghidra.
19
+ - For the loader: the `entry_state.json` next to `image.json`.
20
+
21
+ ## Tools / execution path
22
+
23
+ The script runs Ghidra and ACME on this machine. With `--loader`, it also runs the loader in the emulator through the host runtime.
24
+
25
+ ```
26
+ node <skill>/scripts/generate.ts unpacked/image.json --out src
27
+ node <skill>/scripts/generate.ts game.prg --out src --entry '$080d'
28
+ node <skill>/scripts/generate.ts unpacked/image.json --out src --loader
29
+ ```
30
+
31
+ | Option | Meaning |
32
+ |---|---|
33
+ | `--out src` | The directory for the source files. The script writes over the files that it generated before. |
34
+ | `--entry '$080d'` | An entry point for Ghidra. Use it again for more entry points. The entry of `image.json`, the routines and the code regions in the knowledge are entry points too. |
35
+ | `--coverage coverage/attract.bin` | A coverage file of 64 KiB from `c64_memmap` with `out`. Use it again for more runs. Code that no run executed gets the line `; not executed`, and `notExecuted` lists it. |
36
+ | `--space drive8` | Generate the source of drive code or a cartridge bank from the knowledge of that space. `--coverage` and `--loader` are only for `c64`, and platform names are only for `c64`. |
37
+ | `--loader` | Also write `loader.asm` and `loader.prg`. This needs `image.json` with an entry, and `entry_state.json`. |
38
+
39
+ ## Workflow
40
+
41
+ 1. Get the image. For a packed program, use c64-unpacker capture. For a program without a packer, use the PRG.
42
+ 2. Run the script.
43
+ 3. Read `check`. `identical: true` tells that the source assembles to the image byte by byte.
44
+ 4. Read the source. A label `l_xxxx` is an address that has no name in the knowledge.
45
+ 5. Record names, comments, regions and modules with the c64-knowledge skill. Use `apply` for many changes.
46
+ 6. Run the script again. Do steps 4 to 6 again until the source is clear.
47
+ 7. Build the program with the c64-assembler skill: `--source-root src --entry main.asm`.
48
+ 8. For an unpacked program, run the script with `--loader` at the end. Test `loader.prg` against the original with the c64-testing skill.
49
+
50
+ ## The source
51
+
52
+ - `main.asm` sets the address of each segment and includes the files in address order.
53
+ - Each module in the knowledge is one file. A module starts at its address and stops at the next module or at the end of its segment. Without modules, each segment is one file, `segment_xxxx.asm`.
54
+ - `constants.asm` holds the names of addresses outside the image, for example `EXTCOL = $d020`.
55
+ - A label or a constant gets the name of the address in the knowledge. Without a knowledge name, it gets the platform name of the c64-memory-map skill, for example `EXTCOL` or `CHROUT`. An address in the image without a name gets `l_xxxx`.
56
+ - Code regions, and the instructions that Ghidra decodes outside of data regions, become instructions.
57
+ - Data regions become data by their type: `!byte`, `!word`, `!pet`, `!scr`, `!fill`, tables of labels, and sprite bytes in binary.
58
+ - `lda+2 $0012` is an absolute operand below $0100. `+1` is a zero-page operand with a label.
59
+ - `name = * + 1` is a label inside an instruction, for example for code that changes its own operand.
60
+ - `!byte` with an instruction as its comment: ACME writes this instruction with a different opcode. The bytes keep the source identical.
61
+ - `not decoded` marks bytes in a code region that Ghidra did not decode.
62
+ - `; not executed` starts code that no run of the `--coverage` files executed.
63
+
64
+ Do not edit the generated files. The next run writes over them.
65
+
66
+ ## The loader
67
+
68
+ `--loader` writes `loader.asm` and assembles it to `loader.prg`. The loader does these steps:
69
+
70
+ 1. `10 SYS 2061` starts it.
71
+ 2. It moves its copier to two free pages. The copier runs with interrupts off and with all RAM visible.
72
+ 3. It copies each segment and the pages $0002-$03ff of the entry state into place. The script selects an order in which no copy writes over the packed bytes of a copy that has not run yet.
73
+ 4. It sets $00/$01, the stack pointer, A, X, Y and the flags, and jumps to the entry.
74
+
75
+ The flags go through the free stack byte below the stack pointer (`stackByte`). The loader changes this byte. The loader does not set the VIC, SID and CIA registers. They stay as the KERNAL set them.
76
+
77
+ The script simulates the copies first. Then it runs `loader.prg` in the emulator to its jump to the entry. It compares the memory, $00/$01 and the registers with the entry state there. `loader.verified: true` tells that all of them are equal. An interrupt can run right at the entry, as in the original program.
78
+
79
+ ## Knowledge
80
+
81
+ Read:
82
+ - symbols, regions, comments, references and modules of all origins.
83
+
84
+ Automatic write:
85
+ - none.
86
+
87
+ Do not store:
88
+ - the generated source or its labels `l_xxxx`.
89
+
90
+ ## Result
91
+
92
+ One JSON object:
93
+
94
+ - `files`: the files in the output directory.
95
+ - `instructions`, `dataBytes` and `undecodedBytes`.
96
+ - `labels`: `named` labels from the knowledge and `generated` labels `l_xxxx`.
97
+ - `asBytes`: the instructions that the source has as bytes.
98
+ - `unusedModules`: modules that start outside the image.
99
+ - `notExecuted`: with `--coverage`, the count of instructions that no run executed, and their ranges. Put these ranges in the report.
100
+ - `check`: `identical`, and the result for each segment. A different segment gives its first different address.
101
+ - `loader`: the program, the copies in their order, the copier page, `stackByte`, `verified` and `differences`. When the script cannot make the loader, `loader` has `verified: false` and a `reason`. The loader includes the source. Thus a source that does not assemble to the image also stops the loader.
102
+
103
+ The exit status is 0 when the source is identical and the emulator check of the loader passes. The exit status is 1 for a failed check and for an error. The exit status is 2 for a wrong argument.
104
+
105
+ ## Failure and conflicts
106
+
107
+ - `check.diagnostics`: ACME did not assemble the source. A name in the knowledge can be wrong for ACME. Read the diagnostics and change the name with c64-knowledge.
108
+ - `check.identical: false`: the script has a defect. Give the result to the user.
109
+ - `invalid-input` that names a symbol or a module: change that symbol or module in the knowledge.
110
+ - `loader.verified: false` with a `reason`: the script did not make the loader. The script still writes and checks the source. When no copy order or no free pages exist, or the loader needs too many copies, the reason gives the ranges. Capture the image again with fewer segments, or write the loader by hand.
111
+ - `loader.verified: false` with `differences`: the loader ran, and `differences` gives the bytes and registers that are not equal.
112
+ - `installation-incomplete`, or a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own disassembler or emulator script.
113
+
114
+ ## Handoffs
115
+
116
+ - Get an image → c64-unpacker
117
+ - Names, regions, comments and modules → c64-knowledge
118
+ - Build the program → c64-assembler
119
+ - Test the rebuild against the original → c64-testing