@henols/c64-re-tools 2.0.0-rc.6 → 2.0.0-rc.7

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 (48) hide show
  1. package/package.json +1 -1
  2. package/skills/c64-assembler/SKILL.md +1 -1
  3. package/skills/c64-basic/SKILL.md +1 -1
  4. package/skills/c64-disk/SKILL.md +3 -3
  5. package/skills/c64-disk/scripts/disk.ts +2 -1
  6. package/skills/c64-emulator/SKILL.md +3 -1
  7. package/skills/c64-knowledge/SKILL.md +1 -1
  8. package/skills/c64-provenance/SKILL.md +7 -5
  9. package/skills/c64-provenance/scripts/compare.ts +26 -0
  10. package/skills/c64-provenance/scripts/provenance.ts +24 -10
  11. package/skills/c64-provenance/scripts/separate.ts +12 -4
  12. package/skills/c64-reverse-engineering/SKILL.md +2 -2
  13. package/skills/c64-source/SKILL.md +15 -9
  14. package/skills/c64-source/scripts/generate.ts +36 -10
  15. package/skills/c64-source/scripts/loader.ts +88 -6
  16. package/skills/c64-source/scripts/source.ts +40 -9
  17. package/skills/c64-static-analysis/SKILL.md +4 -4
  18. package/skills/c64-static-analysis/scripts/analyze.ts +13 -6
  19. package/skills/c64-static-analysis/scripts/coverage.ts +4 -4
  20. package/skills/c64-static-analysis/scripts/idioms.ts +14 -9
  21. package/skills/c64-testing/SKILL.md +1 -1
  22. package/skills/c64-testing/scripts/scenario.ts +2 -2
  23. package/skills/c64-unpacker/SKILL.md +9 -7
  24. package/skills/c64-unpacker/scripts/loads.ts +12 -1
  25. package/skills/c64-unpacker/scripts/stages.ts +66 -3
  26. package/skills/c64-unpacker/scripts/unpack.ts +96 -30
  27. package/src/c64.ts +14 -5
  28. package/src/cli/status.ts +26 -3
  29. package/src/host/tools/c1541.ts +34 -5
  30. package/src/host/tools/petcat.ts +5 -2
  31. package/src/host/tools/status.ts +6 -3
  32. package/src/host/vice/adapter.ts +23 -9
  33. package/src/host/vice/process.ts +7 -2
  34. package/src/host/vice/session.ts +47 -10
  35. package/src/host-client/connect.ts +10 -10
  36. package/src/host-client/coverage.ts +19 -10
  37. package/src/host-client/tools.ts +5 -5
  38. package/src/host-client/vice-session.ts +1 -1
  39. package/src/knowledge/schema.ts +7 -0
  40. package/src/knowledge/write.ts +5 -0
  41. package/src/mcp/tools/debug.ts +3 -2
  42. package/src/mcp/tools/machine.ts +4 -1
  43. package/src/mcp/tools/media.ts +5 -1
  44. package/src/mcp/tools/video.ts +2 -0
  45. package/src/native/discover.ts +2 -1
  46. package/src/platform.ts +3 -2
  47. package/src/protocol/tools.ts +19 -4
  48. package/src/protocol/vice.ts +11 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henols/c64-re-tools",
3
- "version": "2.0.0-rc.6",
3
+ "version": "2.0.0-rc.7",
4
4
  "description": "Agent-friendly Commodore 64 engineering toolkit for reverse engineering, debugging, development and testing.",
5
5
  "license": "MIT",
6
6
  "author": "Henrik Olsson",
@@ -67,7 +67,7 @@ The exit status is 0 for a PRG. The exit status is 1 for `assembled: false`, for
67
67
  ## Failure and conflicts
68
68
 
69
69
  - `assembled: false`: the source has errors. This is a normal result. Correct the source.
70
- - `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 disk parser, disassembler or emulator script.
70
+ - `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.
71
71
  - `invalid-input`: an option or a path is wrong. Read the message and correct the call.
72
72
 
73
73
  ## Handoffs
@@ -56,7 +56,7 @@ The exit status is 0 for a listing. The exit status is 1 for `decoded: false` an
56
56
 
57
57
  - `decoded: false`: the file is not a BASIC program. This is a normal result. Usually the file is machine code. Give it to the c64-static-analysis skill.
58
58
  - An empty `handoffs` list: the BASIC does not start machine code with SYS or USR. Do not make up a handoff. Look for a POKE into a vector or a LOAD of another file in the listing.
59
- - `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 disk parser, disassembler or emulator script.
59
+ - `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.
60
60
  - `not-found` or `invalid-input`: the path or an argument is wrong. Read the message and correct the call.
61
61
 
62
62
  ## Handoffs
@@ -29,7 +29,7 @@ node <skill>/scripts/disk.ts <action> <image> [name] [--out <file>]
29
29
  | Need | Command |
30
30
  |---|---|
31
31
  | The disk name, the free blocks and the files | `directory original/game.d64` |
32
- | The free and used sectors of each track | `bam original/game.d64` |
32
+ | The free and used sectors of each track, and the used sectors that no file uses | `bam original/game.d64` |
33
33
  | The type, size and first block of one file | `entry original/game.d64 GAME` |
34
34
  | The track and sector of each block of one file | `chain original/game.d64 GAME` |
35
35
  | Extract one file | `read original/game.d64 GAME --out extracted/game.prg` |
@@ -47,7 +47,7 @@ File names show the characters that the C64 shows in a directory list. A byte th
47
47
  1. Run `directory`. Find the file in `entries`. Each entry has a `name`, a `type`, a size in `blocks` and the `closed` and `locked` flags.
48
48
  2. Run `read` with an output path to extract the file. A PRG file keeps its 2-byte load address at the start.
49
49
  3. Run `entry` or `chain` only when the disk structure is important, for example for a loader that reads sectors directly.
50
- 4. Run `bam` only to find used sectors that no file uses.
50
+ 4. Run `bam` only to find used sectors that no file uses. `unreferenced` lists them. The list does not include the header, the directory and the BAM blocks. These sectors can hold data that a loader reads directly.
51
51
 
52
52
  ## Knowledge
53
53
 
@@ -79,7 +79,7 @@ The exit status is 0 for a result with the file. The exit status is 1 for `found
79
79
 
80
80
  - `found: false`: the image has no file with that name. This is a normal result. Read the directory again and compare the names.
81
81
  - `media-error`: the image has a defect or is not a disk image. The message tells the problem, for example a sector chain that loops. Tell the user. Do not change the image.
82
- - `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 disk parser, disassembler or emulator script.
82
+ - `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.
83
83
  - `invalid-input`: an argument or a path is wrong. Read the message and correct the call.
84
84
 
85
85
  A file that is not `closed` was not written fully. Its data can be incomplete.
@@ -16,7 +16,8 @@ import { ContainerError, parseCrt, parseT64, tapeFile } from "./containers.ts";
16
16
  const USAGE = `disk.ts <action> <image> [name] [--out <file>]
17
17
 
18
18
  directory <image> disk name, id, free blocks and the files
19
- bam <image> free and used sectors of each track
19
+ bam <image> free and used sectors of each track, and the
20
+ used sectors that no file holds
20
21
  entry <image> <name> the directory entry of one file
21
22
  chain <image> <name> the track and sector of each block of one file
22
23
  read <image> <name> --out <file> write the bytes of one file into the project
@@ -64,7 +64,9 @@ For a large result, give `out` with a path in the project. `c64_memory_read`, `c
64
64
 
65
65
  Write operations need a stopped CPU. Stop the CPU with `c64_execution` action `pause` first.
66
66
 
67
- `c64_reset` with `run` false stops the CPU at the first instruction of the reset routine. The result gives that address in `pc`.
67
+ `c64_reset` with `run` false stops the CPU at the first instruction of the reset routine. The result gives that address in `pc`. A reset and `c64_autostart` set `c64_timing` to zero.
68
+
69
+ A cartridge from `c64_autostart` stays in the C64 through every reset, as on a real C64. `c64_status` gives `cartridge: true` while the C64 has it. `c64_autostart` of a file that is not a cartridge removes the cartridge first.
68
70
 
69
71
  Step, next and until-return work only in space `c64`. The emulator cannot stop the disk drive CPU after one instruction. To find when the drive executes an address, use `c64_run_until` with an address target in space `drive8`, or a breakpoint in space `drive8`. The drive can execute a few more instructions before it stops. The computer completes its current instruction. The `pc` in the result is the drive address where it stopped.
70
72
 
@@ -115,7 +115,7 @@ A `line` comment is a line of its own above the address. A `side` comment is at
115
115
 
116
116
  ## Modules
117
117
 
118
- A module tells the c64-source skill where a file of the generated source starts. The file runs to the next module or to the end of its segment. Each address has one module, and each file name has one module. A file name has letters, digits, underscores or hyphens, and ends in `.asm`. The title becomes the first line of the file. A `module` write without a title keeps the title that the module has. Modules have a history, and `revert` restores them.
118
+ A module tells the c64-source skill where a file of the generated source starts. The file runs to the next module or to the end of its segment. Each address has one module, and each file name has one module. A file name has letters, digits, underscores or hyphens, and ends in `.asm`. The c64-source skill writes `main.asm`, `loader.asm` and `constants.asm` itself. Thus a module cannot have one of these names, in upper or lower case. The title becomes the first line of the file. A `module` write without a title keeps the title that the module has. Modules have a history, and `revert` restores them.
119
119
 
120
120
  ## Removals
121
121
 
@@ -40,12 +40,14 @@ node <skill>/scripts/provenance.ts separate unpacked/image.json --coverage cover
40
40
 
41
41
  The script puts each PRG at its load address. An image has bytes only in its segments. The script compares the addresses where all releases have bytes. It prints one JSON object.
42
42
 
43
+ With `--shift`, the addresses of `segments`, `common` and `differences` are the release addresses plus the `shift` of the release. The `start` and `end` of `runtime` are the addresses in the release itself, without the shift.
44
+
43
45
  With `--run`, a PRG starts with autostart and RUN. An image starts from its bytes: the script writes the segments and the entry state from `entry_state.json` into the emulator, and starts at the entry. Watchpoints on each difference stop the run at its first access.
44
46
 
45
47
  ## Workflow
46
48
 
47
49
  1. Run the script on the releases.
48
- 2. Read `identicalShare`. If it is low and `suggestedShifts` is present, the releases are at different addresses. Run the script again with `--shift`.
50
+ 2. Read `identicalShare`. If it is low and `suggestedShifts` is present, the releases are at different addresses. Run the script again with the `--shift` of `suggestedShifts`. `shiftsNotSuggested` tells why a found shift does not fit in `$0000-$ffff`.
49
51
  3. Read `differences`. Each one has an address range, the bytes of each release, and `groups`: the releases that have the same bytes there.
50
52
  4. To know how the program uses the differences, run the script again with `--run` and the frames that get the program to the part you examine. Read `runtime` of each difference:
51
53
  - `first`: the first access in that release: `read`, `write`, `execute` or `none`. An instruction that reads and then writes the same byte, for example INC, counts as `read`. The operand bytes of an executed instruction count as `execute`, for example the value of `LDA #$03`.
@@ -66,9 +68,9 @@ Use `separate` when you have only one release. It gives evidence, not a decision
66
68
  - `ranges`: the image in ranges with the same stage and access. `stage` is the last unpack stage that wrote the range. In `image.json`, the last stage is the program. An earlier stage is a depacker, an intro or a loader. Only an image from c64-unpacker `capture` has stages. For an image from `loads`, `noStages` tells this.
67
69
  - `access`: from the joined `--coverage` files: `executed`, `read`, `written`, `read-written` or `untouched`. `untouched` bytes did not take part in the runs.
68
70
  - `codeRegions.unreachable`: code regions of the knowledge that no chain of call and jump references reaches from the entry. Run c64-static-analysis first, so that the knowledge has the code regions and the references.
69
- - `outside`: RAM outside the image that the program `read` or `executed` but did not write, and code it `generated`. A loader or an earlier stage put the bytes that the program did not write.
71
+ - `outside`: RAM outside the image that the program `read` or `executed` but did not write, and code it `generated`. A loader or an earlier stage put the bytes that the program did not write. A `generated` range can have gaps of two bytes or less: these are operand bytes.
70
72
 
71
- The result lists the first ranges. `--out` writes all of them. Run more coverage runs for parts that are `untouched` before you decide.
73
+ Each list has `count` and `listed`. `listed` has the first ranges, at most `--max-listed`. `--out` writes all of them. Run more coverage runs for parts that are `untouched` before you decide.
72
74
 
73
75
  ## Knowledge
74
76
 
@@ -79,7 +81,7 @@ Record a confirmed conclusion as a normal comment, region or symbol with the c64
79
81
 
80
82
  ## Result
81
83
 
82
- The differing ranges with the bytes of each release and the groups that agree, and the share of identical bytes. `common` gives the ranges where all releases have bytes. With `--run`, each difference also has `runtime`, and `run` tells if each release started, or why not.
84
+ The differing ranges with the bytes of each release and the groups that agree, and the share of identical bytes. `common` gives the ranges where all releases have bytes. `releases` gives the segments and the `shift` of each release. With `--run`, each difference also has `runtime`, and `run` tells if each release started, or why not.
83
85
 
84
86
  The runtime evidence is evidence only. The script does not decide which bytes are original.
85
87
 
@@ -91,7 +93,7 @@ The exit status is 0 for a result. The exit status is 1 for an error. The exit s
91
93
  - Do not call a byte original only because one release has it. A majority of releases is also not proof.
92
94
  - `common` is empty: the releases do not cover the same addresses. Use `--shift`, or compare a range that all releases cover.
93
95
  - `run.releases` shows `started: false`: a PRG without a BASIC start, or an image without `entry_state.json`. The reason tells which. The other releases still give evidence.
94
- - `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 emulator script.
96
+ - `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 emulator script.
95
97
  - `moreDifferences: true`: the script shows only the first differences. Use `--from` and `--to` to see the others.
96
98
  - `not-found`: the project has no file at that path.
97
99
  - `invalid-input`: a file, an address or a shift is wrong. A path that goes out of the project directory is also wrong, also through a link. Read the message.
@@ -121,3 +121,29 @@ export function suggestShift(base: Release, other: Release): { shift: number; ma
121
121
  for (const [shift, matches] of votes) if (best === undefined || matches > best.matches) best = { shift, matches };
122
122
  return best;
123
123
  }
124
+
125
+ export type ShiftSuggestion = { release: number; shift: number; matchingSequences: number } | { release: number; matchingSequences: number; reason: string };
126
+
127
+ /**
128
+ * The --shift to suggest for release `index` (0 = the first) from `found`,
129
+ * the best shift of that release over the first one (F-25). The shift is the
130
+ * full value for --shift: `applied` holds the shifts already given. A shift
131
+ * that moves a segment out of $0000-$ffff is not suggested: with two releases,
132
+ * the opposite shift of the first release is suggested when it fits, else the
133
+ * result gives the reason.
134
+ */
135
+ export function shiftSuggestion(releases: Release[], applied: number[], index: number, found: { shift: number; matches: number }): ShiftSuggestion {
136
+ const outside = (release: Release, by: number) => release.segments.find((segment) => segment.start + by < 0 || segment.end + by > 0xffff);
137
+ const hex = (value: number) => `$${value.toString(16).padStart(4, "0")}`;
138
+ const why = (number: number, by: number, segment: Range) =>
139
+ `release ${number} moved by ${by} puts ${hex(segment.start)}-${hex(segment.end)} ${segment.start + by < 0 ? "below $0000" : "past $ffff"}`;
140
+ const own = outside(releases[index]!, found.shift);
141
+ if (own === undefined) return { release: index + 1, shift: applied[index]! + found.shift, matchingSequences: found.matches };
142
+ const reasons = [why(index + 1, found.shift, own)];
143
+ if (releases.length === 2) {
144
+ const base = outside(releases[0]!, -found.shift);
145
+ if (base === undefined) return { release: 1, shift: applied[0]! - found.shift, matchingSequences: found.matches };
146
+ reasons.push(why(1, -found.shift, base));
147
+ }
148
+ return { release: index + 1, matchingSequences: found.matches, reason: `No shift fits: ${reasons.join(", and ")}.` };
149
+ }
@@ -18,7 +18,7 @@ import { KnowledgeError, openForRead } from "#src/knowledge/database.ts";
18
18
  import { listReferences, listRegions } from "#src/knowledge/read.ts";
19
19
  import { CODE_REGION_TYPES } from "#src/knowledge/schema.ts";
20
20
  import { runScript, UsageError } from "#src/script.ts";
21
- import { compareReleases, suggestShift, type Range, type Release } from "./compare.ts";
21
+ import { compareReleases, shiftSuggestion, suggestShift, type Range, type Release } from "./compare.ts";
22
22
  import { watchRelease, type ImageState, type Start } from "./runtime.ts";
23
23
  import { separate } from "./separate.ts";
24
24
 
@@ -36,7 +36,7 @@ separate gives evidence from one release: the unpack stage that wrote each range
36
36
  its access in the --coverage files (64 KiB each, repeatable), whether call and
37
37
  jump references in knowledge reach each code region from the entry, and what the
38
38
  program reads or executes outside the image. --out writes all ranges to a file;
39
- the result lists at most --max-listed of them (default 50).
39
+ the result lists at most --max-listed of each list (default 50) and its count.
40
40
 
41
41
  A release is a PRG, placed at its load address, or an image.json of the
42
42
  c64-unpacker skill, compared inside its segments. Paths are relative to the
@@ -208,12 +208,14 @@ async function run(argv: string[]): Promise<unknown> {
208
208
  identicalShare < 0.5
209
209
  ? releases.slice(1).flatMap((release, index) => {
210
210
  const best = suggestShift(releases[0]!, release);
211
- return best === undefined || best.shift === 0 ? [] : [{ release: index + 2, shift: best.shift, matchingSequences: best.matches }];
211
+ return best === undefined || best.shift === 0 ? [] : [shiftSuggestion(releases, loaded.map((item) => item.shift), index + 1, best)];
212
212
  })
213
213
  : [];
214
+ const suggested = suggestions.filter((suggestion) => "shift" in suggestion);
215
+ const notSuggested = suggestions.filter((suggestion) => "reason" in suggestion);
214
216
 
215
217
  // The runtime evidence: one emulator for each release, the differences at the release's own addresses.
216
- const runtime = new Map<string, Array<{ first: string; changed: boolean }>>();
218
+ const runtime = new Map<string, Array<Range & { first: string; changed: boolean }>>();
217
219
  const runs: Record<string, unknown> = {};
218
220
  if (frames !== undefined && comparison.differences.length > 0) {
219
221
  for (const item of loaded) {
@@ -229,7 +231,7 @@ async function run(argv: string[]): Promise<unknown> {
229
231
  if (!watched.started) runs[name] = { started: false, reason: watched.reason };
230
232
  else {
231
233
  runs[name] = { started: true };
232
- runtime.set(name, watched.accesses);
234
+ runtime.set(name, watched.accesses.map((access, index) => ({ ...ranges[index]!, ...access })));
233
235
  }
234
236
  } finally {
235
237
  await client.close();
@@ -238,7 +240,7 @@ async function run(argv: string[]): Promise<unknown> {
238
240
  }
239
241
 
240
242
  return {
241
- releases: loaded.map((item) => ({ name: item.release.name, segments: item.release.segments.map((segment) => ({ start: formatC64Address(segment.start), end: formatC64Address(segment.end) })) })),
243
+ releases: loaded.map((item) => ({ name: item.release.name, shift: item.shift, segments: item.release.segments.map((segment) => ({ start: formatC64Address(segment.start), end: formatC64Address(segment.end) })) })),
242
244
  common: comparison.common.map((range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) })),
243
245
  identicalBytes: comparison.identicalBytes,
244
246
  differingBytes: comparison.differingBytes,
@@ -248,10 +250,21 @@ async function run(argv: string[]): Promise<unknown> {
248
250
  end: formatC64Address(difference.end),
249
251
  values: Object.fromEntries(Object.entries(difference.values).map(([name, bytes]) => [name, hexBytes(bytes.slice(0, 32)) + (bytes.length > 32 ? " ..." : "")])),
250
252
  groups: difference.groups,
251
- ...(runtime.size === 0 ? {} : { runtime: Object.fromEntries([...runtime].map(([name, accesses]) => [name, accesses[index]!])) }),
253
+ // runtime gives the release's own addresses: with a shift, they are not the addresses of the difference (F-24).
254
+ ...(runtime.size === 0
255
+ ? {}
256
+ : {
257
+ runtime: Object.fromEntries(
258
+ [...runtime].map(([name, accesses]) => {
259
+ const { start, end, first, changed } = accesses[index]!;
260
+ return [name, { start: formatC64Address(start), end: formatC64Address(end), first, changed }];
261
+ }),
262
+ ),
263
+ }),
252
264
  })),
253
265
  ...(comparison.moreDifferences ? { moreDifferences: true } : {}),
254
- ...(suggestions.length > 0 ? { suggestedShifts: suggestions } : {}),
266
+ ...(suggested.length > 0 ? { suggestedShifts: suggested } : {}),
267
+ ...(notSuggested.length > 0 ? { shiftsNotSuggested: notSuggested } : {}),
255
268
  ...(frames === undefined
256
269
  ? {}
257
270
  : { run: { frames, steps: steps.length, releases: runs, ...(comparison.differences.length === 0 ? { skipped: "The releases have no differences, so no release ran." } : {}) } }),
@@ -288,6 +301,7 @@ function separateCommand(path: string, coveragePaths: string[], out: string | un
288
301
  const ranges = found.ranges.map((range) => ({ ...show(range), ...(range.stage === undefined ? {} : { stage: range.stage }), ...(range.access === undefined ? {} : { access: range.access }) }));
289
302
  const codeRegions = found.codeRegions.map((region) => ({ ...show(region), type: region.type, ...(region.reachable === undefined ? {} : { reachable: region.reachable }), ...(region.executed === undefined ? {} : { executed: region.executed }) }));
290
303
  const outside = { read: found.outside.read.map(show), executed: found.outside.executed.map(show), generated: found.outside.generated.map(show) };
304
+ const listed = <T>(items: T[]) => ({ count: items.length, listed: items.slice(0, Number(max)) });
291
305
  if (out !== undefined) writeProjectFile(out, Buffer.from(`${JSON.stringify({ release: path, ranges, codeRegions, outside }, null, 1)}\n`), { what: "--out" });
292
306
  const bytes = (filter: (range: (typeof found.ranges)[number]) => boolean) => found.ranges.filter(filter).reduce((sum, range) => sum + range.end - range.start + 1, 0);
293
307
  const lastStage = loaded.stages === undefined ? undefined : loaded.stages.length - 1;
@@ -301,12 +315,12 @@ function separateCommand(path: string, coveragePaths: string[], out: string | un
301
315
  bytesByStage: Object.fromEntries([...[...Array(lastStage + 1).keys()].map((stage): [string, number] => [String(stage), bytes((range) => range.stage === stage)]), ["none", bytes((range) => range.stage === undefined)]]),
302
316
  }),
303
317
  ...(coverage === undefined ? {} : { bytesByAccess: Object.fromEntries((["executed", "read", "written", "read-written", "untouched"] as const).map((access) => [access, bytes((range) => range.access === access)])) }),
304
- ranges: { count: ranges.length, listed: ranges.slice(0, Number(max)) },
318
+ ranges: listed(ranges),
305
319
  codeRegions: {
306
320
  count: codeRegions.length,
307
321
  ...(loaded.entry === undefined ? {} : { unreachable: codeRegions.filter((region) => region.reachable === false) }),
308
322
  },
309
- ...(coverage === undefined ? {} : { outside }),
323
+ ...(coverage === undefined ? {} : { outside: { read: listed(outside.read), executed: listed(outside.executed), generated: listed(outside.generated) } }),
310
324
  ...(out === undefined ? {} : { out }),
311
325
  };
312
326
  }
@@ -42,18 +42,20 @@ export interface Separation {
42
42
  /**
43
43
  * RAM outside the image and outside pages 0-3, from the coverage: `read`
44
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.
45
+ * those bytes there), and `generated` it wrote and then executed, in
46
+ * pieces of code with gaps of up to two operand bytes.
46
47
  */
47
48
  outside: { read: Range[]; executed: Range[]; generated: Range[] };
48
49
  }
49
50
 
50
51
  const SYSTEM_PAGES_END = 0x03ff;
51
52
 
52
- function merge(addresses: number[]): Range[] {
53
+ /** Joins addresses in order into ranges; `gap` is the most bytes between two addresses in one range. */
54
+ function merge(addresses: number[], gap = 0, joins: (from: number, to: number) => boolean = () => true): Range[] {
53
55
  const ranges: Range[] = [];
54
56
  for (const address of addresses) {
55
57
  const last = ranges.at(-1);
56
- if (last !== undefined && last.end + 1 === address) last.end = address;
58
+ if (last !== undefined && address - last.end - 1 <= gap && joins(last.end, address)) last.end = address;
57
59
  else ranges.push({ start: address, end: address });
58
60
  }
59
61
  return ranges;
@@ -128,5 +130,11 @@ export function separate(input: SeparationInput): Separation {
128
130
  else if (!written && (input.coverage[address]! & COVERAGE.ramRead) !== 0) read.push(address);
129
131
  }
130
132
  }
131
- return { ranges, codeRegions, outside: { read: merge(read), executed: merge(ranOutside), generated: merge(generated) } };
133
+ // The image has no bytes of generated code, so only its opcode bytes count as executed. VICE marks only
134
+ // opcode bytes: a gap of at most two operand bytes outside the image stays inside one piece of code (F-28).
135
+ const outsideImage = (from: number, to: number) => {
136
+ for (let address = from + 1; address < to; address++) if (inImage(address)) return false;
137
+ return true;
138
+ };
139
+ return { ranges, codeRegions, outside: { read: merge(read), executed: merge(ranOutside), generated: merge(generated, 2, outsideImage) } };
132
140
  }
@@ -92,7 +92,7 @@ In stage 5, if you cannot get to a part of the program, ask the user how to get
92
92
  1. Acceptance: each criterion of stage 1 and its result, and each exception that the user accepted.
93
93
  2. The program: what the program does, in a few sentences.
94
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.
95
+ 4. Untested code: all ranges in the `not_executed.json` file of c64-source. Tell why no run executed them, if you know.
96
96
  5. Open questions: what the evidence does not explain.
97
97
 
98
98
  Keep observed facts apart from your conclusions.
@@ -133,7 +133,7 @@ A clear answer for the scope, with the durable conclusions in the project knowle
133
133
 
134
134
  ## Failure and conflicts
135
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.
136
+ - A skill reports `installation-incomplete`, or `machine-unavailable` with 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.
137
137
  - If the evidence does not support a conclusion, keep the unknown and get more evidence. Do not make up a meaning.
138
138
  - Before you replace current knowledge from the user or the LLM, read its history and the evidence for it.
139
139
  - A conflict from the static analysis: find the correct answer at run time with the c64-emulator skill.
@@ -32,7 +32,7 @@ node <skill>/scripts/generate.ts unpacked/image.json --out src --loader
32
32
  |---|---|
33
33
  | `--out src` | The directory for the source files. The script writes over the files that it generated before. |
34
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. |
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` with its range. `notExecuted` lists it. |
36
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
37
  | `--loader` | Also write `loader.asm` and `loader.prg`. This needs `image.json` with an entry, and `entry_state.json`. |
38
38
 
@@ -56,10 +56,11 @@ node <skill>/scripts/generate.ts unpacked/image.json --out src --loader
56
56
  - Code regions, and the instructions that Ghidra decodes outside of data regions, become instructions.
57
57
  - Data regions become data by their type: `!byte`, `!word`, `!pet`, `!scr`, `!fill`, tables of labels, and sprite bytes in binary.
58
58
  - `lda+2 $0012` is an absolute operand below $0100. `+1` is a zero-page operand with a label.
59
+ - `sta NMINV+1` is an address inside a platform entry of more than one byte, for example the high byte of a vector.
59
60
  - `name = * + 1` is a label inside an instruction, for example for code that changes its own operand.
60
61
  - `!byte` with an instruction as its comment: ACME writes this instruction with a different opcode. The bytes keep the source identical.
61
62
  - `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
+ - `; not executed $xxxx-$yyyy` starts code that no run of the `--coverage` files executed. The range is the same as in `not_executed.json`.
63
64
 
64
65
  Do not edit the generated files. The next run writes over them.
65
66
 
@@ -70,11 +71,12 @@ Do not edit the generated files. The next run writes over them.
70
71
  1. `10 SYS 2061` starts it.
71
72
  2. It moves its copier to two free pages. The copier runs with interrupts off and with all RAM visible.
72
73
  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
+ 4. It sets the display registers of the VIC-II and the VIC bank from `io` in the entry state. Then it waits for the raster line of the entry.
75
+ 5. It sets $00/$01, the stack pointer, A, X, Y and the flags, and jumps to the entry.
74
76
 
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.
77
+ The flags go through the free stack byte below the stack pointer (`stackByte`). The loader changes this byte. The loader does not set the SID, the CIA timers or the interrupt registers of the VIC-II. They stay as the KERNAL set them. Thus a scenario can still find a difference in an interrupt or a timer. Then use `runUntil` with `raster` in the c64-testing skill.
76
78
 
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.
79
+ 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, the registers, the display registers of the VIC-II, the VIC bank and the raster line 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
80
 
79
81
  ## Knowledge
80
82
 
@@ -94,22 +96,26 @@ One JSON object:
94
96
  - `files`: the files in the output directory.
95
97
  - `instructions`, `dataBytes` and `undecodedBytes`.
96
98
  - `labels`: `named` labels from the knowledge and `generated` labels `l_xxxx`.
99
+ - `entriesInData`: each `--entry` in a data region of the knowledge, with the `region` and its `origin`. Ghidra does not decode an entry in a data region.
97
100
  - `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.
101
+ - `unusedModules`: modules in the knowledge whose start is in no segment of the image. Often this tells that the image does not hold code that the knowledge describes.
102
+ - `entryOutside`: the entry of `image.json` is in no segment of the image, with a `reason`. The image does not hold the program.
103
+ - `notExecuted`: with `--coverage`, the count of instructions that no run executed, the count of their ranges, and the first ranges. `all` names the file `not_executed.json` in `--out`. It has all of the ranges. Put all of these ranges in the report.
100
104
  - `check`: `identical`, and the result for each segment. A different segment gives its first different address.
101
105
  - `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
106
 
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.
107
+ 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, for an entry in no segment, and for an error. The exit status is 2 for a wrong argument.
104
108
 
105
109
  ## Failure and conflicts
106
110
 
107
111
  - `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
112
  - `check.identical: false`: the script has a defect. Give the result to the user.
109
113
  - `invalid-input` that names a symbol or a module: change that symbol or module in the knowledge.
114
+ - `entryOutside`: the run fails, but the script still writes and checks the source. With `--loader`, `loader.verified` is `false` with the same `reason`. Capture the image again with c64-unpacker `loads` or `capture`. Make sure that a segment holds the entry.
115
+ - `entriesInData`: an analyzer region of data hides the code at the entry. If the entry is code, record a code region at the entry with the c64-knowledge skill, for example `classify '$0500' '$05ff' code`. A region of the user or the LLM replaces the analyzer region. Then generate again.
110
116
  - `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
117
  - `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.
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 disassembler or emulator script.
113
119
 
114
120
  ## Handoffs
115
121
 
@@ -25,9 +25,9 @@ import { GHIDRA_LIMITS, type AssembledSymbol, type Diagnostic } from "#src/nativ
25
25
  import { platformNames } from "#src/platform.ts";
26
26
  import { resolveProjectPath } from "#src/project.ts";
27
27
  import { runScript, UsageError } from "#src/script.ts";
28
- import { LOADER_JUMP, planLoader, readEntryState, simulate, type EntryState, type LoaderPlan } from "./loader.ts";
28
+ import { LOADER_JUMP, planLoader, rasterLine, readEntryState, simulate, VIC_DISPLAY_REGISTERS, type EntryState, type LoaderPlan } from "./loader.ts";
29
29
  import { OPCODES } from "./opcodes.ts";
30
- import { instructionText, layout, literalOperand, probeForms, render, type Layout, type Range } from "./source.ts";
30
+ import { entriesInData, entryOutsideImage, instructionText, layout, literalOperand, probeForms, render, type Layout, type Range } from "./source.ts";
31
31
 
32
32
  const USAGE = `generate.ts <image> --out <dir> [options]
33
33
 
@@ -36,7 +36,8 @@ const USAGE = `generate.ts <image> --out <dir> [options]
36
36
  --out <dir> the directory for the source files, relative to the project
37
37
  --entry <address> an entry point for Ghidra, for example '$080d' (repeatable)
38
38
  --coverage <file> a coverage map of 64 KiB from c64_memmap (repeatable): code
39
- that no run executed is marked and listed
39
+ that no run executed is marked and listed; <dir>/not_executed.json
40
+ has all of its ranges
40
41
  --space <space> the knowledge space of the image: c64 (the default), drive8, or
41
42
  bank0 to bank255; --coverage and --loader are for c64 only
42
43
  --loader also write loader.asm and loader.prg: the loader copies the
@@ -276,6 +277,15 @@ async function runLoader(path: string, program: Buffer, plan: LoaderPlan, image:
276
277
  differ("sp", registers.sp, state.sp);
277
278
  const flags = (["n", "v", "d", "i", "z", "c"] as const).reduce((value, name, index) => value | (registers.flags[name] ? [0x80, 0x40, 0x08, 0x04, 0x02, 0x01][index]! : 0), 0);
278
279
  differ("flags", flags, state.flags & 0xcf);
280
+ if (state.vic !== undefined) {
281
+ // The display registers and the raster line (D60): the loader waited for the line, so the jump comes in it or the next one.
282
+ const vic = Buffer.from((await client.ioRead(0xd000, 0x2f)).data, "hex");
283
+ for (const { register, mask } of VIC_DISPLAY_REGISTERS) differ(formatC64Address(0xd000 + register), vic[register]! & mask, state.vic.registers[register]! & mask);
284
+ differ("the VIC bank", Buffer.from((await client.ioRead(0xdd00, 1)).data, "hex")[0]! & 0x03, state.vic.bank);
285
+ const line = rasterLine(vic);
286
+ const wanted = rasterLine(state.vic.registers);
287
+ if (line !== wanted && line !== wanted + 1) differences.push(`the raster line is ${line}, not ${wanted}`);
288
+ }
279
289
  return { verified: differences.length === 0, differences: differences.slice(0, MAX_LISTED) };
280
290
  } finally {
281
291
  await client.close();
@@ -284,8 +294,10 @@ async function runLoader(path: string, program: Buffer, plan: LoaderPlan, image:
284
294
 
285
295
  /** The first words of every file that the script writes. */
286
296
  const GENERATED_MARK = "; Generated by c64-source";
297
+ /** The file with every range of instructions that no run executed (D49). */
298
+ const NOT_EXECUTED_FILE = "not_executed.json";
287
299
 
288
- /** Removes the .asm files of an earlier run that this run does not write, and an earlier loader.prg; other files stay. */
300
+ /** Removes the .asm files of an earlier run that this run does not write, and an earlier loader.prg and not_executed.json; other files stay. */
289
301
  function removeOldFiles(out: string, keep: ReadonlySet<string>): void {
290
302
  const directory = resolveProjectPath(out);
291
303
  if (!existsSync(directory)) return;
@@ -293,7 +305,7 @@ function removeOldFiles(out: string, keep: ReadonlySet<string>): void {
293
305
  if (!entry.isFile()) continue;
294
306
  const name = entry.name;
295
307
  const path = join(directory, name);
296
- if (name === "loader.prg") rmSync(path, { force: true });
308
+ if (name === "loader.prg" || name === NOT_EXECUTED_FILE) rmSync(path, { force: true });
297
309
  else if (name.endsWith(".asm") && !keep.has(name) && readFileSync(path, "utf8").startsWith(GENERATED_MARK)) rmSync(path, { force: true });
298
310
  }
299
311
  }
@@ -344,16 +356,18 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
344
356
  referenceTargets: knowledge.references.map((reference) => reference.to),
345
357
  ...(coverage === undefined ? {} : { coverage }),
346
358
  // The platform names are C64 names: drive code and banks get none.
347
- ...(space === DEFAULT_SPACE ? { platform: new Map(platformNames().map((item) => [item.address, item.name])) } : {}),
359
+ ...(space === DEFAULT_SPACE ? { platform: platformNames() } : {}),
348
360
  });
349
361
  const asBytes = await probe(laid);
350
362
  const header = [`Generated by c64-source from ${image.path}${space === DEFAULT_SPACE ? "" : ` in space ${space}`} and knowledge revision ${knowledge.revision}.`, "Change names, comments, regions and modules in knowledge, then generate again."];
351
363
  const rendered = render(laid, asBytes, header);
352
364
  const files = [...rendered.files];
353
365
  let loader: LoaderPlan | undefined;
366
+ // An image without its entry has no program: the source is still written and checked, but the run fails (R2-02).
367
+ const missingEntry = entryOutsideImage(image.entry, image.segments);
354
368
  // A loader that cannot be planned does not stop the source: the source is written and checked, and the result says why.
355
- let loaderRefused: string | undefined;
356
- if (state !== undefined) {
369
+ let loaderRefused: string | undefined = state === undefined ? undefined : missingEntry;
370
+ if (state !== undefined && missingEntry === undefined) {
357
371
  const segments = image.segments.map((segment) => ({ ...segment, files: laid.pieces.filter((piece) => piece.start >= segment.start && piece.start <= segment.end).map((piece) => piece.file) }));
358
372
  try {
359
373
  loader = planLoader({ segments, byteAt: image.byteAt, state, entry: image.entry!, header: [`Loader generated by c64-source from ${image.path}.`, "It copies the segments into place, sets the entry state and jumps to the entry."] });
@@ -367,6 +381,12 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
367
381
  for (const [index, file] of files.entries()) if (!file.text.startsWith(GENERATED_MARK)) files[index] = { ...file, text: `${GENERATED_MARK}. Do not edit: change the knowledge and generate again.\n${file.text}` };
368
382
  removeOldFiles(values.out, new Set(files.map((file) => file.name)));
369
383
  for (const file of files) writeProjectFile(`${values.out}/${file.name}`, Buffer.from(file.text, "utf8"), { what: "--out" });
384
+ const showRange = (range: Range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) });
385
+ const inData = entriesInData(entries, knowledge.regions);
386
+ if (rendered.notExecuted !== undefined) {
387
+ const all = { image: image.path, revision: knowledge.revision, instructions: rendered.notExecuted.instructions, ranges: rendered.notExecuted.ranges.map(showRange) };
388
+ writeProjectFile(`${values.out}/${NOT_EXECUTED_FILE}`, Buffer.from(`${JSON.stringify(all, null, 2)}\n`, "utf8"), { what: "--out" });
389
+ }
370
390
 
371
391
  const output: Record<string, unknown> = {
372
392
  out: values.out,
@@ -375,7 +395,12 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
375
395
  instructions: rendered.instructions,
376
396
  dataBytes: rendered.dataBytes,
377
397
  ...(rendered.undecodedBytes === 0 ? {} : { undecodedBytes: rendered.undecodedBytes }),
398
+ ...(missingEntry === undefined ? {} : { entryOutside: { entry: formatC64Address(image.entry!), reason: missingEntry } }),
378
399
  labels: { named: laid.namedLabels, generated: laid.generatedLabels },
400
+ // An --entry in a data region decodes nothing: the result says so, and a code region in the knowledge changes it (F-27).
401
+ ...(inData.length === 0
402
+ ? {}
403
+ : { entriesInData: inData.map(({ entry, region }) => ({ entry: formatC64Address(entry), region: { ...showRange(region), type: region.type }, origin: region.origin })) }),
379
404
  ...(rendered.asBytes.length === 0
380
405
  ? {}
381
406
  : { asBytes: { count: rendered.asBytes.length, listed: rendered.asBytes.slice(0, MAX_LISTED).map((item) => ({ address: formatC64Address(item.address), instruction: item.text })) } }),
@@ -385,7 +410,8 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
385
410
  notExecuted: {
386
411
  instructions: rendered.notExecuted.instructions,
387
412
  ranges: rendered.notExecuted.ranges.length,
388
- listed: rendered.notExecuted.ranges.slice(0, MAX_LISTED).map((range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) })),
413
+ listed: rendered.notExecuted.ranges.slice(0, MAX_LISTED).map(showRange),
414
+ all: `${values.out}/${NOT_EXECUTED_FILE}`,
389
415
  },
390
416
  }),
391
417
  ...(laid.unusedModules.length === 0 ? {} : { unusedModules: laid.unusedModules.map((module) => ({ start: formatC64Address(module.start), file: module.file })) }),
@@ -400,7 +426,7 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
400
426
  output.check = checked;
401
427
  if (!checked.identical) return { output: { ...output, ...notBuilt("The source does not assemble to the image, and the loader includes it.") }, failed: true };
402
428
  if (loaderRefused !== undefined) return { output: { ...output, loader: { verified: false, reason: loaderRefused } }, failed: true };
403
- if (loader === undefined || state === undefined) return { output, failed: false };
429
+ if (loader === undefined || state === undefined) return { output, failed: missingEntry !== undefined };
404
430
 
405
431
  const loaderBuilt = await assembleFiles(files, "loader.asm");
406
432
  if (loaderBuilt.program === undefined) return { output: { ...output, loader: { verified: false, reason: "loader.asm does not assemble.", diagnostics: loaderBuilt.diagnostics.slice(0, MAX_LISTED) } }, failed: true };