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

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 (55) hide show
  1. package/package.json +1 -1
  2. package/skills/c64-assembler/SKILL.md +1 -1
  3. package/skills/c64-basic/SKILL.md +2 -2
  4. package/skills/c64-basic/scripts/basic.ts +1 -1
  5. package/skills/c64-disk/SKILL.md +3 -3
  6. package/skills/c64-disk/scripts/disk.ts +2 -1
  7. package/skills/c64-emulator/SKILL.md +4 -1
  8. package/skills/c64-knowledge/SKILL.md +2 -2
  9. package/skills/c64-knowledge/scripts/knowledge.ts +5 -2
  10. package/skills/c64-provenance/SKILL.md +10 -5
  11. package/skills/c64-provenance/scripts/compare.ts +35 -6
  12. package/skills/c64-provenance/scripts/provenance.ts +35 -15
  13. package/skills/c64-provenance/scripts/separate.ts +12 -4
  14. package/skills/c64-reverse-engineering/SKILL.md +2 -2
  15. package/skills/c64-source/SKILL.md +18 -11
  16. package/skills/c64-source/scripts/generate.ts +55 -10
  17. package/skills/c64-source/scripts/loader.ts +253 -6
  18. package/skills/c64-source/scripts/source.ts +46 -11
  19. package/skills/c64-static-analysis/SKILL.md +4 -4
  20. package/skills/c64-static-analysis/scripts/analyze.ts +13 -6
  21. package/skills/c64-static-analysis/scripts/coverage.ts +4 -4
  22. package/skills/c64-static-analysis/scripts/idioms.ts +14 -9
  23. package/skills/c64-testing/SKILL.md +1 -1
  24. package/skills/c64-testing/references/scenario-format.md +1 -1
  25. package/skills/c64-testing/scripts/scenario.ts +9 -7
  26. package/skills/c64-unpacker/SKILL.md +11 -9
  27. package/skills/c64-unpacker/scripts/loads.ts +16 -4
  28. package/skills/c64-unpacker/scripts/machine.ts +13 -1
  29. package/skills/c64-unpacker/scripts/stages.ts +66 -3
  30. package/skills/c64-unpacker/scripts/unpack.ts +135 -42
  31. package/src/c64.ts +19 -5
  32. package/src/cli/status.ts +26 -3
  33. package/src/host/tools/c1541.ts +34 -5
  34. package/src/host/tools/petcat.ts +5 -2
  35. package/src/host/tools/status.ts +6 -3
  36. package/src/host/vice/adapter.ts +37 -9
  37. package/src/host/vice/chips.ts +11 -1
  38. package/src/host/vice/process.ts +7 -2
  39. package/src/host/vice/screen.ts +3 -2
  40. package/src/host/vice/session.ts +68 -15
  41. package/src/host-client/autostart.ts +11 -1
  42. package/src/host-client/connect.ts +10 -10
  43. package/src/host-client/coverage.ts +19 -10
  44. package/src/host-client/tools.ts +6 -6
  45. package/src/host-client/vice-session.ts +1 -1
  46. package/src/knowledge/schema.ts +7 -0
  47. package/src/knowledge/write.ts +5 -0
  48. package/src/mcp/tools/debug.ts +9 -5
  49. package/src/mcp/tools/machine.ts +4 -1
  50. package/src/mcp/tools/media.ts +7 -1
  51. package/src/mcp/tools/video.ts +7 -2
  52. package/src/native/discover.ts +2 -1
  53. package/src/platform.ts +3 -2
  54. package/src/protocol/tools.ts +19 -4
  55. package/src/protocol/vice.ts +56 -5
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.8",
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
@@ -54,9 +54,9 @@ The exit status is 0 for a listing. The exit status is 1 for `decoded: false` an
54
54
 
55
55
  ## Failure and conflicts
56
56
 
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.
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. A memory dump also gives `decoded: false`: its first two bytes are not a load address, and its bytes go past `$ffff`.
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
@@ -22,7 +22,7 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
22
22
  const result = await decodeBasic({ program });
23
23
  if (!result.decoded) return { output: { decoded: false, reason: result.reason }, failed: true };
24
24
  const size = statSync(resolveProjectPath(program)).size - 2;
25
- const end = Math.min(0xffff, result.loadAddress + size - 1);
25
+ const end = result.loadAddress + size - 1;
26
26
  return {
27
27
  output: {
28
28
  decoded: true,
@@ -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,12 +64,15 @@ 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, and `cartridge: false` when it does not. `c64_autostart` of a file that is not a cartridge removes the cartridge first. `c64_autostart` also sets both joysticks to the center first. `joysticksCentred` gives the ports that it changed.
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
 
71
73
  `c64_keyboard` puts keys in the keyboard queue of the KERNAL. A program that reads the keyboard hardware directly does not see them. Many games do this. The emulator has no command that presses one key of the keyboard matrix. For such a program:
72
74
 
75
+ - After `c64_window` or a snapshot restore, the profiler of VICE keeps the routines that ran before the restore. Then `c64_profile` can give the cycles of the program to such a routine, for example `$fd50`. Do not trust the routine addresses of `c64_profile` after a restore.
73
76
  - VICE 3.10 and older have a known flaw: bits 5-7 of `$DC00` and `$DC01` are always 0. Then `emulatorFlaw` in the result of `c64_keyboard` and `c64_joystick` gives the flaw. With this flaw, a wait for any key that reads all keyboard columns at one time ends without a key. A wait for one specific key, or a compare of the full byte of a joystick port, never ends. Tell the user about the flaw. The next VICE release and the VICE nightly builds do not have it. Joystick port 1 uses the same lines as the keyboard.
74
77
  - For one specific key, for example F1 or RUN/STOP, open the window and ask the user to press the key.
75
78
  - Do not change the program to make it accept a key.
@@ -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
 
@@ -146,7 +146,7 @@ The script records the origin `llm`. Use `--origin user` only when the user deci
146
146
 
147
147
  For a read: the current or historical knowledge in compact JSON.
148
148
 
149
- For a write: the new revision and the current value after the write. If the write replaces a symbol or a region of a different origin, `previous` gives that row as it was before the write. For example, an `llm` write over a `user` name gives `previous`. Read `previous` and tell the user about the change.
149
+ For a write: the new revision and the current value after the write. If the write replaces a symbol or a region of a different origin, `previous` gives that row as it was before the write. For example, an `llm` write over a `user` name gives `previous`. Read `previous` and tell the user about the change. For `apply`, `replaced` lists each write that replaced a row. Each item has the number of the write, its `op` and its `previous`.
150
150
 
151
151
  The exit status is 0 for a result. The exit status is 1 for an error from the knowledge, for example `conflict`. The exit status is 2 for a wrong argument.
152
152
 
@@ -346,8 +346,11 @@ function applyOperation(args: string[], context: WriteContext, space: string): W
346
346
  const operations = parsed.map((item, index) => batchOperation(item, `write ${index + 1}`, space));
347
347
  return (db) => {
348
348
  const applied = applyBatch(db, context, operations);
349
- // replaced: writes that replaced a row of another origin, as previous does for one write.
350
- return { revision: applied.revision, writes: applied.steps.length, replaced: applied.steps.filter((step) => step.previous !== undefined).length };
349
+ // replaced: each write that replaced a row of another origin, with that row as previous gives it for one write (b2/07).
350
+ const replaced = applied.steps.flatMap((step, index) =>
351
+ step.previous === undefined ? [] : [{ write: index + 1, op: operations[index]!.op, previous: Array.isArray(step.previous) ? step.previous.map(region) : symbol(step.previous) }],
352
+ );
353
+ return { revision: applied.revision, writes: applied.steps.length, replaced };
351
354
  };
352
355
  }
353
356
 
@@ -40,12 +40,16 @@ 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 a `--shift` of `suggestedShifts`. `shiftsNotSuggested` tells why a found shift does not fit in `$0000-$ffff`.
51
+
52
+ `suggestedShifts` has at most 3 shifts for each release. The shift with the most `matchingSequences` is first. Data that did not move and code that moved give different shifts. Try each shift. `current: true` marks the shift of this run.
49
53
  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
54
  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
55
  - `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`.
@@ -65,10 +69,11 @@ Use `separate` when you have only one release. It gives evidence, not a decision
65
69
 
66
70
  - `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
71
  - `access`: from the joined `--coverage` files: `executed`, `read`, `written`, `read-written` or `untouched`. `untouched` bytes did not take part in the runs.
72
+ - `codeRegions.total`: the number of code regions in the knowledge.
68
73
  - `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.
74
+ - `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
75
 
71
- The result lists the first ranges. `--out` writes all of them. Run more coverage runs for parts that are `untouched` before you decide.
76
+ 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
77
 
73
78
  ## Knowledge
74
79
 
@@ -79,7 +84,7 @@ Record a confirmed conclusion as a normal comment, region or symbol with the c64
79
84
 
80
85
  ## Result
81
86
 
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.
87
+ 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
88
 
84
89
  The runtime evidence is evidence only. The script does not decide which bytes are original.
85
90
 
@@ -91,7 +96,7 @@ The exit status is 0 for a result. The exit status is 1 for an error. The exit s
91
96
  - Do not call a byte original only because one release has it. A majority of releases is also not proof.
92
97
  - `common` is empty: the releases do not cover the same addresses. Use `--shift`, or compare a range that all releases cover.
93
98
  - `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.
99
+ - `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
100
  - `moreDifferences: true`: the script shows only the first differences. Use `--from` and `--to` to see the others.
96
101
  - `not-found`: the project has no file at that path.
97
102
  - `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.
@@ -93,11 +93,15 @@ function addressesOf(release: Release): number[] {
93
93
  return release.segments.flatMap((segment) => Array.from({ length: segment.end - segment.start + 1 }, (_, offset) => segment.start + offset));
94
94
  }
95
95
 
96
+ /** Most shifts that suggestShifts gives: data that did not move can outvote code that did (b2/04). */
97
+ export const MAX_SUGGESTED_SHIFTS = 3;
98
+
96
99
  /**
97
- * The shift that puts `other` over `base` best: the most common address
98
- * difference between equal 8-byte sequences. Undefined when nothing matches.
100
+ * The shifts that put `other` over `base` best: the most common address
101
+ * differences between equal 8-byte sequences, most votes first. A shift with
102
+ * less than a tenth of the best one's votes is chance. Empty when nothing matches.
99
103
  */
100
- export function suggestShift(base: Release, other: Release): { shift: number; matches: number } | undefined {
104
+ export function suggestShifts(base: Release, other: Release): Array<{ shift: number; matches: number }> {
101
105
  const WINDOW = 8;
102
106
  const key = (release: Release, addresses: number[], at: number) =>
103
107
  String.fromCharCode(...addresses.slice(at, at + WINDOW).map((address) => release.byteAt(address)));
@@ -117,7 +121,32 @@ export function suggestShift(base: Release, other: Release): { shift: number; ma
117
121
  votes.set(shift, (votes.get(shift) ?? 0) + 1);
118
122
  }
119
123
  }
120
- let best: { shift: number; matches: number } | undefined;
121
- for (const [shift, matches] of votes) if (best === undefined || matches > best.matches) best = { shift, matches };
122
- return best;
124
+ const ranked = [...votes].map(([shift, matches]) => ({ shift, matches })).sort((a, b) => b.matches - a.matches || Math.abs(a.shift) - Math.abs(b.shift));
125
+ return ranked.filter((found) => found.matches * 10 >= ranked[0]!.matches).slice(0, MAX_SUGGESTED_SHIFTS);
126
+ }
127
+
128
+ export type ShiftSuggestion = { release: number; shift: number; matchingSequences: number } | { release: number; matchingSequences: number; reason: string };
129
+
130
+ /**
131
+ * The --shift to suggest for release `index` (0 = the first) from `found`,
132
+ * the best shift of that release over the first one (F-25). The shift is the
133
+ * full value for --shift: `applied` holds the shifts already given. A shift
134
+ * that moves a segment out of $0000-$ffff is not suggested: with two releases,
135
+ * the opposite shift of the first release is suggested when it fits, else the
136
+ * result gives the reason.
137
+ */
138
+ export function shiftSuggestion(releases: Release[], applied: number[], index: number, found: { shift: number; matches: number }): ShiftSuggestion {
139
+ const outside = (release: Release, by: number) => release.segments.find((segment) => segment.start + by < 0 || segment.end + by > 0xffff);
140
+ const hex = (value: number) => `$${value.toString(16).padStart(4, "0")}`;
141
+ const why = (number: number, by: number, segment: Range) =>
142
+ `release ${number} moved by ${by} puts ${hex(segment.start)}-${hex(segment.end)} ${segment.start + by < 0 ? "below $0000" : "past $ffff"}`;
143
+ const own = outside(releases[index]!, found.shift);
144
+ if (own === undefined) return { release: index + 1, shift: applied[index]! + found.shift, matchingSequences: found.matches };
145
+ const reasons = [why(index + 1, found.shift, own)];
146
+ if (releases.length === 2) {
147
+ const base = outside(releases[0]!, -found.shift);
148
+ if (base === undefined) return { release: 1, shift: applied[0]! - found.shift, matchingSequences: found.matches };
149
+ reasons.push(why(1, -found.shift, base));
150
+ }
151
+ return { release: index + 1, matchingSequences: found.matches, reason: `No shift fits: ${reasons.join(", and ")}.` };
123
152
  }
@@ -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, suggestShifts, 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
@@ -204,16 +204,23 @@ async function run(argv: string[]): Promise<unknown> {
204
204
  const compared = comparison.identicalBytes + comparison.differingBytes;
205
205
  const identicalShare = compared === 0 ? 0 : comparison.identicalBytes / compared;
206
206
  // A poor match at the load addresses can mean that the releases are placed differently.
207
+ // Each release gets its best shifts: data that stayed can outvote code that moved, so one is not enough (b2/04).
208
+ const applied = loaded.map((item) => item.shift);
207
209
  const suggestions =
208
210
  identicalShare < 0.5
209
- ? releases.slice(1).flatMap((release, index) => {
210
- const best = suggestShift(releases[0]!, release);
211
- return best === undefined || best.shift === 0 ? [] : [{ release: index + 2, shift: best.shift, matchingSequences: best.matches }];
212
- })
211
+ ? releases.slice(1).flatMap((release, index) =>
212
+ suggestShifts(releases[0]!, release).map((found) => {
213
+ const suggestion = shiftSuggestion(releases, applied, index + 1, found);
214
+ // The shift that the run already has: its votes tell how good the present alignment is.
215
+ return "shift" in suggestion && suggestion.shift === applied[suggestion.release - 1] ? { ...suggestion, current: true } : suggestion;
216
+ }),
217
+ )
213
218
  : [];
219
+ const suggested = suggestions.filter((suggestion) => "shift" in suggestion);
220
+ const notSuggested = suggestions.filter((suggestion) => "reason" in suggestion);
214
221
 
215
222
  // 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 }>>();
223
+ const runtime = new Map<string, Array<Range & { first: string; changed: boolean }>>();
217
224
  const runs: Record<string, unknown> = {};
218
225
  if (frames !== undefined && comparison.differences.length > 0) {
219
226
  for (const item of loaded) {
@@ -229,7 +236,7 @@ async function run(argv: string[]): Promise<unknown> {
229
236
  if (!watched.started) runs[name] = { started: false, reason: watched.reason };
230
237
  else {
231
238
  runs[name] = { started: true };
232
- runtime.set(name, watched.accesses);
239
+ runtime.set(name, watched.accesses.map((access, index) => ({ ...ranges[index]!, ...access })));
233
240
  }
234
241
  } finally {
235
242
  await client.close();
@@ -238,7 +245,7 @@ async function run(argv: string[]): Promise<unknown> {
238
245
  }
239
246
 
240
247
  return {
241
- releases: loaded.map((item) => ({ name: item.release.name, segments: item.release.segments.map((segment) => ({ start: formatC64Address(segment.start), end: formatC64Address(segment.end) })) })),
248
+ 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
249
  common: comparison.common.map((range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) })),
243
250
  identicalBytes: comparison.identicalBytes,
244
251
  differingBytes: comparison.differingBytes,
@@ -248,10 +255,21 @@ async function run(argv: string[]): Promise<unknown> {
248
255
  end: formatC64Address(difference.end),
249
256
  values: Object.fromEntries(Object.entries(difference.values).map(([name, bytes]) => [name, hexBytes(bytes.slice(0, 32)) + (bytes.length > 32 ? " ..." : "")])),
250
257
  groups: difference.groups,
251
- ...(runtime.size === 0 ? {} : { runtime: Object.fromEntries([...runtime].map(([name, accesses]) => [name, accesses[index]!])) }),
258
+ // runtime gives the release's own addresses: with a shift, they are not the addresses of the difference (F-24).
259
+ ...(runtime.size === 0
260
+ ? {}
261
+ : {
262
+ runtime: Object.fromEntries(
263
+ [...runtime].map(([name, accesses]) => {
264
+ const { start, end, first, changed } = accesses[index]!;
265
+ return [name, { start: formatC64Address(start), end: formatC64Address(end), first, changed }];
266
+ }),
267
+ ),
268
+ }),
252
269
  })),
253
270
  ...(comparison.moreDifferences ? { moreDifferences: true } : {}),
254
- ...(suggestions.length > 0 ? { suggestedShifts: suggestions } : {}),
271
+ ...(suggested.length > 0 ? { suggestedShifts: suggested } : {}),
272
+ ...(notSuggested.length > 0 ? { shiftsNotSuggested: notSuggested } : {}),
255
273
  ...(frames === undefined
256
274
  ? {}
257
275
  : { run: { frames, steps: steps.length, releases: runs, ...(comparison.differences.length === 0 ? { skipped: "The releases have no differences, so no release ran." } : {}) } }),
@@ -288,6 +306,7 @@ function separateCommand(path: string, coveragePaths: string[], out: string | un
288
306
  const ranges = found.ranges.map((range) => ({ ...show(range), ...(range.stage === undefined ? {} : { stage: range.stage }), ...(range.access === undefined ? {} : { access: range.access }) }));
289
307
  const codeRegions = found.codeRegions.map((region) => ({ ...show(region), type: region.type, ...(region.reachable === undefined ? {} : { reachable: region.reachable }), ...(region.executed === undefined ? {} : { executed: region.executed }) }));
290
308
  const outside = { read: found.outside.read.map(show), executed: found.outside.executed.map(show), generated: found.outside.generated.map(show) };
309
+ const listed = <T>(items: T[]) => ({ count: items.length, listed: items.slice(0, Number(max)) });
291
310
  if (out !== undefined) writeProjectFile(out, Buffer.from(`${JSON.stringify({ release: path, ranges, codeRegions, outside }, null, 1)}\n`), { what: "--out" });
292
311
  const bytes = (filter: (range: (typeof found.ranges)[number]) => boolean) => found.ranges.filter(filter).reduce((sum, range) => sum + range.end - range.start + 1, 0);
293
312
  const lastStage = loaded.stages === undefined ? undefined : loaded.stages.length - 1;
@@ -301,12 +320,13 @@ function separateCommand(path: string, coveragePaths: string[], out: string | un
301
320
  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
321
  }),
303
322
  ...(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)) },
323
+ ranges: listed(ranges),
324
+ // total counts every code region; unreachable is a list like the others (b2/08).
305
325
  codeRegions: {
306
- count: codeRegions.length,
307
- ...(loaded.entry === undefined ? {} : { unreachable: codeRegions.filter((region) => region.reachable === false) }),
326
+ total: codeRegions.length,
327
+ ...(loaded.entry === undefined ? {} : { unreachable: listed(codeRegions.filter((region) => region.reachable === false)) }),
308
328
  },
309
- ...(coverage === undefined ? {} : { outside }),
329
+ ...(coverage === undefined ? {} : { outside: { read: listed(outside.read), executed: listed(outside.executed), generated: listed(outside.generated) } }),
310
330
  ...(out === undefined ? {} : { out }),
311
331
  };
312
332
  }
@@ -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
 
@@ -52,14 +52,15 @@ node <skill>/scripts/generate.ts unpacked/image.json --out src --loader
52
52
  - `main.asm` sets the address of each segment and includes the files in address order.
53
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
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`.
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`. A platform name wins over a name from an analyzer (`dxa`, `ghidra` or `heuristic`), for example `EXTCOL` over `ld020`. A `user` or `llm` name wins over a platform name. An address in the image without a name gets `l_xxxx`.
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,13 @@ 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 the CIA timers, their latches and the interrupt masks from `io` and `cia` in the entry state. A running timer gets the value that counts down to the timer value of the entry at the entry.
76
+ 6. It sets $00/$01, the stack pointer, A, X, Y and the flags, and jumps to the entry.
74
77
 
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.
78
+ 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 time of day of the CIAs, the CIA interrupt flags or the interrupt registers of the VIC-II. `loader.limits` names each CIA value that the loader cannot set as it was at the entry. A program that reads the SID voice 3 for random numbers can still differ in a scenario. A raster sync does not change a timer or the SID. Then observe after the program sets up its own random numbers.
76
79
 
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.
80
+ 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. It also compares the CIA timers, latches and interrupt masks. `loader.verified: true` tells that all of them are equal. An interrupt can run right at the entry, as in the original program.
78
81
 
79
82
  ## Knowledge
80
83
 
@@ -94,22 +97,26 @@ One JSON object:
94
97
  - `files`: the files in the output directory.
95
98
  - `instructions`, `dataBytes` and `undecodedBytes`.
96
99
  - `labels`: `named` labels from the knowledge and `generated` labels `l_xxxx`.
100
+ - `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
101
  - `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.
102
+ - `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.
103
+ - `entryOutside`: the entry of `image.json` is in no segment of the image, with a `reason`. The image does not hold the program.
104
+ - `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
105
  - `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.
106
+ - `loader`: the program, the copies in their order, the copier page, `stackByte`, `verified` and `differences`. `limits` lists the CIA values that the loader cannot set. 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
107
 
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.
108
+ 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
109
 
105
110
  ## Failure and conflicts
106
111
 
107
112
  - `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
113
  - `check.identical: false`: the script has a defect. Give the result to the user.
109
114
  - `invalid-input` that names a symbol or a module: change that symbol or module in the knowledge.
115
+ - `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.
116
+ - `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
117
  - `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
118
  - `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.
119
+ - `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
120
 
114
121
  ## Handoffs
115
122