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

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 (118) hide show
  1. package/README.md +97 -116
  2. package/package.json +1 -1
  3. package/skills/c64-assembler/SKILL.md +22 -6
  4. package/skills/c64-assembler/scripts/assemble.ts +42 -14
  5. package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
  6. package/skills/c64-basic/SKILL.md +10 -1
  7. package/skills/c64-basic/scripts/basic.ts +3 -3
  8. package/skills/c64-disk/SKILL.md +13 -4
  9. package/skills/c64-disk/scripts/disk.ts +7 -7
  10. package/skills/c64-disk/scripts/{containers.ts → lib/containers.ts} +2 -2
  11. package/skills/c64-docs/SKILL.md +138 -0
  12. package/skills/c64-docs/references/page-types.md +141 -0
  13. package/skills/c64-docs/references/style.md +43 -0
  14. package/skills/c64-docs/scripts/generate.ts +109 -0
  15. package/skills/c64-docs/scripts/lib/links.ts +167 -0
  16. package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
  17. package/skills/c64-docs/scripts/lib/reference.ts +406 -0
  18. package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
  19. package/skills/c64-docs/scripts/link.ts +239 -0
  20. package/skills/c64-emulator/SKILL.md +16 -5
  21. package/skills/c64-knowledge/SKILL.md +140 -18
  22. package/skills/c64-knowledge/scripts/knowledge.ts +737 -112
  23. package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
  24. package/skills/c64-memory-map/SKILL.md +11 -2
  25. package/skills/c64-memory-map/scripts/memmap.ts +2 -2
  26. package/skills/c64-provenance/SKILL.md +66 -11
  27. package/skills/c64-provenance/scripts/{compare.ts → lib/compare.ts} +3 -3
  28. package/skills/c64-provenance/scripts/lib/library.ts +584 -0
  29. package/skills/c64-provenance/scripts/{runtime.ts → lib/runtime.ts} +2 -2
  30. package/skills/c64-provenance/scripts/{separate.ts → lib/separate.ts} +3 -3
  31. package/skills/c64-provenance/scripts/provenance.ts +242 -40
  32. package/skills/c64-reverse-engineering/SKILL.md +51 -26
  33. package/skills/c64-source/SKILL.md +76 -28
  34. package/skills/c64-source/scripts/generate.ts +425 -127
  35. package/skills/c64-source/scripts/{loader.ts → lib/loader.ts} +84 -25
  36. package/skills/c64-source/scripts/lib/source.ts +1121 -0
  37. package/skills/c64-static-analysis/SKILL.md +35 -18
  38. package/skills/c64-static-analysis/scripts/analyze.ts +184 -55
  39. package/skills/c64-static-analysis/scripts/{coverage.ts → lib/coverage.ts} +19 -25
  40. package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +6 -2
  41. package/skills/c64-static-analysis/scripts/{idioms.ts → lib/idioms.ts} +21 -12
  42. package/skills/c64-static-analysis/scripts/{instructions.ts → lib/instructions.ts} +1 -1
  43. package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
  44. package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
  45. package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
  46. package/skills/c64-testing/SKILL.md +36 -7
  47. package/skills/c64-testing/references/scenario-format.md +12 -8
  48. package/skills/c64-testing/scripts/checklist.ts +3 -3
  49. package/skills/c64-testing/scripts/diff.ts +167 -0
  50. package/skills/c64-testing/scripts/lib/diff.ts +70 -0
  51. package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +45 -34
  52. package/skills/c64-testing/scripts/run.ts +73 -21
  53. package/skills/c64-testing/scripts/test.ts +5 -5
  54. package/skills/c64-unpacker/SKILL.md +23 -9
  55. package/skills/c64-unpacker/scripts/{evidence.ts → lib/evidence.ts} +1 -1
  56. package/skills/c64-unpacker/scripts/{loads.ts → lib/loads.ts} +4 -4
  57. package/skills/c64-unpacker/scripts/{machine.ts → lib/machine.ts} +4 -4
  58. package/skills/c64-unpacker/scripts/{stages.ts → lib/stages.ts} +17 -5
  59. package/skills/c64-unpacker/scripts/unpack.ts +41 -24
  60. package/src/c64.ts +103 -5
  61. package/src/cli/cleanup.ts +1 -1
  62. package/src/cli/main.ts +14 -5
  63. package/src/cli/status.ts +70 -3
  64. package/src/graphics.ts +158 -0
  65. package/src/host/main.ts +25 -5
  66. package/src/host/server.ts +10 -3
  67. package/src/host/tools/petcat.ts +1 -1
  68. package/src/host/tools/status.ts +1 -1
  69. package/src/host/vice/adapter.ts +4 -3
  70. package/src/host/vice/process.ts +2 -2
  71. package/src/host/vice/screen.ts +1 -42
  72. package/src/host/vice/session.ts +196 -23
  73. package/src/host-client/autostart.ts +1 -1
  74. package/src/host-client/connect.ts +1 -1
  75. package/src/host-client/coverage.ts +2 -29
  76. package/src/host-client/image.ts +66 -0
  77. package/src/host-client/steps.ts +9 -4
  78. package/src/host-client/tools.ts +1 -0
  79. package/src/host-client/transfer.ts +39 -11
  80. package/src/host-client/vice-session.ts +33 -16
  81. package/src/knowledge/conflicts.ts +161 -0
  82. package/src/knowledge/database.ts +150 -38
  83. package/src/knowledge/expression.ts +209 -0
  84. package/src/knowledge/graph.ts +104 -0
  85. package/src/knowledge/history.ts +70 -22
  86. package/src/knowledge/import.ts +64 -55
  87. package/src/knowledge/join.ts +211 -0
  88. package/src/knowledge/program.ts +266 -0
  89. package/src/knowledge/read.ts +258 -27
  90. package/src/knowledge/schema.ts +199 -75
  91. package/src/knowledge/text.ts +415 -0
  92. package/src/knowledge/write.ts +740 -57
  93. package/src/mcp/main.ts +8 -2
  94. package/src/mcp/output.ts +15 -8
  95. package/src/mcp/server.ts +3 -1
  96. package/src/mcp/tools/debug.ts +97 -16
  97. package/src/mcp/tools/execution.ts +38 -5
  98. package/src/mcp/tools/machine.ts +11 -2
  99. package/src/mcp/tools/media.ts +18 -13
  100. package/src/mcp/tools/memory.ts +17 -8
  101. package/src/mcp/tools/video.ts +9 -9
  102. package/src/native/discover.ts +2 -2
  103. package/src/native/ghidra/analyze.ts +2 -2
  104. package/src/native/ghidra/index.ts +1 -1
  105. package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
  106. package/src/native/ghidra/scripts/C64Export.java +1 -1
  107. package/src/native/ghidra/scripts/C64Prepare.java +7 -1
  108. package/src/native/types.ts +14 -8
  109. package/{skills/c64-source/scripts → src}/opcodes.ts +75 -3
  110. package/src/package.ts +13 -3
  111. package/src/platform.ts +70 -7
  112. package/src/project.ts +180 -3
  113. package/src/protocol/messages.ts +1 -1
  114. package/src/protocol/tools.ts +2 -16
  115. package/src/protocol/vice.ts +124 -15
  116. package/src/script.ts +46 -7
  117. package/skills/c64-assembler/scripts/compare.ts +0 -84
  118. package/skills/c64-source/scripts/source.ts +0 -595
@@ -23,7 +23,7 @@ A scenario is one JSON object in a project file.
23
23
 
24
24
  `original` and `rebuild` each have:
25
25
 
26
- - `program`: a PRG file, relative to the project directory.
26
+ - `program`: a PRG file, relative to the project folder.
27
27
  - `symbols`: where the names come from:
28
28
  - `knowledge`: the project knowledge.
29
29
  - A JSON file: the output of the c64-assembler skill, or an object of names and `$` addresses.
@@ -38,7 +38,7 @@ Before the first step of each side, the script does a hard reset. The CPU stops
38
38
  | Step | Effect |
39
39
  |---|---|
40
40
  | `{ "reset": "hard" }` | Reset the C64 (`hard` or `soft`). The CPU stops at the reset vector. |
41
- | `{ "frames": 150 }` | Run for an exact number of frames, from 1 to 10000. Use it after a reset to start the C64. |
41
+ | `{ "frames": 150 }` | Run for an exact number of frames, from 1 to 30000. A breakpoint or a watchpoint stops it before that. Use it after a reset to start the C64. |
42
42
  | `{ "load": true }` | Load the program of the side into memory. The CPU stays stopped. |
43
43
  | `{ "autostart": true }` | Load the program of the side, type `RUN` and wait until the program runs. The program must have a BASIC start line. The keys of a later `type` step go to the program. |
44
44
  | `{ "write": { "at": "seed", "bytes": [1, 2] } }` | Write bytes into memory: 1 to 4096 numbers from 0 to 255. |
@@ -50,7 +50,7 @@ Before the first step of each side, the script does a hard reset. The CPU stops
50
50
  | `{ "runUntil": { "raster": 250 }, "timeoutFrames": 5 }` | Run until the raster beam gets to a line (0 to 311 for PAL, 0 to 262 for NTSC). |
51
51
  | `{ "runUntil": { "at": "irq_tick", "raster": 250 }, "timeoutFrames": 600 }` | Run until the CPU gets to an address while the beam is on that line. |
52
52
  | `{ "snapshot": "snapshots/start.vsf" }` | Restore a snapshot file. Usually with `side`: the snapshot of one program is not a state of the other. |
53
- | `{ "normalize": "unpacked/entry_state.json" }` | Write pages 0 to 3 and `$00`/`$01` from an entry state file of the c64-unpacker skill. Then both sides have the same zero page, stack and KERNAL work area. |
53
+ | `{ "normalize": true }` | Write pages 0 to 3 and `$00`/`$01` from the entry state in the knowledge. The entry state comes from a capture of the c64-unpacker skill, which `image` of the c64-knowledge skill put in. Then both sides have the same zero page, stack and KERNAL work area. |
54
54
  | `{ "observe": "moved", ... }` | Record a checkpoint. |
55
55
 
56
56
  The `port` of a joystick is 1 or 2. The `direction` is `center`, `up`, `down`, `left`, `right`, `up-left`, `up-right`, `down-left` or `down-right`.
@@ -67,7 +67,7 @@ An `observe` step has a checkpoint name. A scenario can have each name only one
67
67
 
68
68
  | Key | What the script compares |
69
69
  |---|---|
70
- | `memory` | A list of names, `$` addresses, or objects with `at`, `size` (1 to 4096, default 1), `tolerance` (0 to 255, default 0) and `exclude`. |
70
+ | `memory` | A list of names, `$` addresses, or objects. An object has `at`, `size` (1 to 65536, default 1), `view` (`cpu` or `ram`, default `cpu`), `tolerance` (0 to 255, default 0) and `exclude`. The view `ram` reads the RAM under the ROMs and the I/O area. |
71
71
  | `registers` | A list of register names: `pc`, `a`, `x`, `y`, `sp`, `flags`. |
72
72
  | `screen` | The screen of the rebuild against the screen of the original. `maxMismatchRatio` (default 0) is the largest permitted part of different pixels. `mask` is a list of rectangles (`x`, `y`, `width`, `height`) that the comparison does not include. |
73
73
  | `vicii` | The VIC-II state: mode, screen address, scroll and colors. |
@@ -84,10 +84,14 @@ A `pc` value is an address. Compare `pc` only when both programs have the code a
84
84
  A run file (for `run.ts`) has:
85
85
 
86
86
  - `name`: a short name.
87
- - `start`: `{ "program": "game.prg" }` to autostart a program with a BASIC start line, `{ "program": "game.prg", "load": true }` to load it with the CPU stopped (set `pc` in a step), or `{ "snapshot": "snapshots/room0.vsf" }`.
87
+ - `start`: `{ "program": "game.prg" }` to autostart a program with a BASIC start line, `{ "program": "game.prg", "load": true }` to load it with the CPU stopped (set `pc` in a step), or `{ "snapshot": ".c64-re-tools/local/snapshots/room0.vsf" }`.
88
88
  - `symbols`: optional, `knowledge`, `none` (default), or a JSON file as for a subject.
89
89
  - `videoStandard`: optional, `pal` (default) or `ntsc`.
90
- - `steps`: the steps `write`, `registers`, `joystick`, `type`, `frames` and `runUntil` of a scenario, and the run steps `snapshot`, `save`, `screen`, `coverage` and `observe`.
90
+ - `steps`: the steps `write`, `registers`, `joystick`, `type`, `frames` and `runUntil` of a scenario, and the run steps `snapshot`, `save`, `screen`, `coverage`, `trace` and `observe`.
91
+
92
+ A `trace` step has a name, `points` (1 to 8), `frames` (1 to 3000) and an optional `maxHits` (at most 20000). A point has `kind` (`exec`, `read` or `write`), `at` and an optional `end`. The step writes `<name>.json` with each hit. `results.json` gives the file, the count of hits, the frames and why the trace ended. A trace that a breakpoint, a watchpoint or a JAM ends also ends the run.
93
+
94
+ In a run, a memory item has `at`, `size` (1 to 65536) and `view`. An item larger than 4096 bytes goes into the file `<observe>-<at>.bin`. `"memory": "all"` writes all 64 KiB of RAM to `<observe>.bin`.
91
95
 
92
96
  A name in `save`, `screen`, `coverage` and `observe` has 1 to 64 letters, digits, dots, underscores or hyphens. The files go into the `--out` directory, with `results.json`. It has the observations, the files and the steps done. If a `runUntil` step did not reach its target, it also tells why the run stopped.
93
97
 
@@ -96,13 +100,13 @@ Example: visit two rooms from the same start.
96
100
  ```json
97
101
  {
98
102
  "name": "rooms",
99
- "start": { "snapshot": "snapshots/room0.vsf" },
103
+ "start": { "snapshot": ".c64-re-tools/local/snapshots/room0.vsf" },
100
104
  "steps": [
101
105
  { "write": { "at": "$0350", "bytes": [1] } },
102
106
  { "frames": 50 },
103
107
  { "screen": "room1" },
104
108
  { "coverage": "room1" },
105
- { "snapshot": "snapshots/room0.vsf" },
109
+ { "snapshot": ".c64-re-tools/local/snapshots/room0.vsf" },
106
110
  { "write": { "at": "$0350", "bytes": [2] } },
107
111
  { "frames": 50 },
108
112
  { "screen": "room2" },
@@ -5,7 +5,7 @@
5
5
  import { parseArgs } from "node:util";
6
6
 
7
7
  import { runScript, UsageError } from "#src/script.ts";
8
- import { readScenario, ScenarioError, type Scenario } from "./scenario.ts";
8
+ import { readScenario, ScenarioError, type Scenario } from "./lib/scenario.ts";
9
9
 
10
10
  const USAGE = `checklist.ts <scenario.json> [<scenario.json> ...] [--item <text> ...]
11
11
 
@@ -44,8 +44,8 @@ function run(argv: string[]): string {
44
44
  }
45
45
 
46
46
  await runScript(
47
- () => {
48
- process.stdout.write(run(process.argv.slice(2)));
47
+ (argv) => {
48
+ process.stdout.write(run(argv));
49
49
  },
50
50
  { usage: USAGE },
51
51
  );
@@ -0,0 +1,167 @@
1
+ // The c64-testing diff script: what changed between two machine states.
2
+ // Two snapshot files are restored one after the other in an own emulator,
3
+ // which reads RAM, colour RAM and the chip and CPU registers with the readers
4
+ // that the runs use: VICE reads its own snapshot format. Two memory files of a
5
+ // run, or two observations of runs (also two of one run), compare as they are.
6
+ // Exit status: 0, or 2 for a bad call.
7
+
8
+ import { parseArgs } from "node:util";
9
+
10
+ import { formatC64Address, parseC64Address } from "#src/c64.ts";
11
+ import { WireFailure } from "#src/host-client/tools.ts";
12
+ import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
13
+ import { readMemoryInParts, ViceSessionClient } from "#src/host-client/vice-session.ts";
14
+ import { resolveProjectPath } from "#src/project.ts";
15
+ import { runScript, UsageError } from "#src/script.ts";
16
+ import { diffStates, type ChangedRange, type MachineState } from "./lib/diff.ts";
17
+
18
+ const USAGE = `diff.ts <a> <b> [--gap N] [--max-listed N] [--ntsc] [--at <address>] [--out <file>]
19
+
20
+ <a> and <b> are two of one kind, relative to the project folder:
21
+ snapshot.vsf the emulator restores each one and reads RAM, colour
22
+ RAM, the VIC-II, CIA, SID and CPU registers
23
+ memory.bin a memory file of a run (observe with memory "all" or
24
+ a size above 4096); --at gives the address of its
25
+ first byte when it is shorter than 64 KiB
26
+ results.json:<name> the observation <name> of a run; both can be in
27
+ one results.json
28
+
29
+ --gap N join changed ranges across at most N equal bytes (default 0)
30
+ --max-listed N list at most N ranges of each part (default 64)
31
+ --ntsc the snapshots are of an NTSC machine (default PAL)
32
+ --out <file> also write all ranges to a project file
33
+
34
+ The result is one JSON object. Addresses are $ and four hex digits.`;
35
+
36
+ type Kind = "snapshot" | "memory" | "observation";
37
+
38
+ function kindOf(path: string): Kind {
39
+ if (/\.vsf$/i.test(path)) return "snapshot";
40
+ if (/\.json:[^:]+$/.test(path)) return "observation";
41
+ if (/\.bin$/i.test(path)) return "memory";
42
+ throw new UsageError(`${path} is no .vsf, .bin or results.json:<observation>`);
43
+ }
44
+
45
+ /** The state of one observation of a results.json: its memory items and its registers. */
46
+ function observation(path: string): MachineState {
47
+ const [file, name] = [path.slice(0, path.lastIndexOf(":")), path.slice(path.lastIndexOf(":") + 1)];
48
+ let results: { observations?: Record<string, { memory?: Record<string, unknown>; registers?: Record<string, unknown> }> };
49
+ try {
50
+ results = JSON.parse(readProjectFile(file).bytes.toString("utf8")) as typeof results;
51
+ } catch (error) {
52
+ if (error instanceof WireFailure) throw error;
53
+ throw new UsageError(`${file} is not JSON: ${(error as Error).message}`);
54
+ }
55
+ const seen = results.observations?.[name];
56
+ if (seen === undefined) throw new UsageError(`${file} has no observation ${name}; it has ${Object.keys(results.observations ?? {}).join(", ") || "none"}`);
57
+ const observed: Record<string, string> = {};
58
+ for (const [at, value] of Object.entries(seen.memory ?? {})) {
59
+ // A memory item in a file of its own: compare its bytes.
60
+ if (typeof value === "string") observed[at] = value;
61
+ else if (typeof value === "object" && value !== null && typeof (value as { file?: unknown }).file === "string") {
62
+ const folder = file.includes("/") ? file.slice(0, file.lastIndexOf("/") + 1) : "";
63
+ observed[at] = readProjectFile(`${folder}${(value as { file: string }).file}`).bytes.toString("hex");
64
+ }
65
+ }
66
+ const values: Record<string, number> = {};
67
+ for (const [register, value] of Object.entries(seen.registers ?? {})) {
68
+ if (typeof value === "number") values[`cpu.${register}`] = value;
69
+ else if (typeof value === "string" && value.startsWith("$")) values[`cpu.${register}`] = parseC64Address(value);
70
+ }
71
+ return { observed, values };
72
+ }
73
+
74
+ /** The state of a snapshot: restored in the session, then read with the readers of the runs. */
75
+ async function snapshotState(client: ViceSessionClient, path: string): Promise<MachineState> {
76
+ await client.snapshotRestore(readProjectFile(path).bytes);
77
+ const memory = await readMemoryInParts(client, 0x0000, 0x10000, "ram");
78
+ const colorRam = Buffer.from((await client.ioRead(0xd800, 0x400)).data, "hex").map((value) => value & 0x0f);
79
+ const vic = Buffer.from((await client.ioRead(0xd000, 0x2f)).data, "hex");
80
+ const values: Record<string, number> = {};
81
+ vic.forEach((value, index) => (values[`vic.${formatC64Address(0xd000 + index)}`] = value));
82
+ for (const chip of (await client.cia("both")).chips) {
83
+ for (const field of ["portA", "portB", "ddrA", "ddrB", "timerA", "timerB", "latchA", "latchB", "controlA", "controlB", "interruptMask"] as const) values[`cia${chip.id}.${field}`] = chip[field];
84
+ }
85
+ const sid = await client.sid();
86
+ for (const voice of sid.voices) for (const [field, value] of Object.entries(voice)) if (field !== "index" && typeof value === "number") values[`sid.voice${voice.index}.${field}`] = value;
87
+ const registers = await client.registersGet("c64");
88
+ for (const [field, value] of Object.entries(registers)) if (typeof value === "number") values[`cpu.${field}`] = value;
89
+ return { memory: { base: 0, bytes: memory }, colorRam, values };
90
+ }
91
+
92
+ const show = (range: ChangedRange) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end), ...(range.a === undefined ? {} : { a: range.a, b: range.b }) });
93
+
94
+ async function run(argv: string[]): Promise<Record<string, unknown>> {
95
+ const { values, positionals } = parseArgs({
96
+ args: argv,
97
+ strict: true,
98
+ allowPositionals: true,
99
+ options: { gap: { type: "string" }, "max-listed": { type: "string" }, ntsc: { type: "boolean" }, at: { type: "string" }, out: { type: "string" }, help: { type: "boolean" } },
100
+ });
101
+ if (values.help) throw new UsageError("");
102
+ if (positionals.length !== 2) throw new UsageError("give two states to compare");
103
+ const [first, second] = positionals as [string, string];
104
+ const kind = kindOf(first);
105
+ if (kindOf(second) !== kind) throw new UsageError("give two states of one kind: two snapshots, two memory files or two observations");
106
+ const number = (value: string | undefined, fallback: number, what: string) => {
107
+ if (value === undefined) return fallback;
108
+ if (!/^\d+$/.test(value)) throw new UsageError(`${what} must be a whole number`);
109
+ return Number(value);
110
+ };
111
+ const gap = number(values.gap, 0, "--gap");
112
+ const max = number(values["max-listed"], 64, "--max-listed");
113
+ if (values.out !== undefined) {
114
+ try {
115
+ resolveProjectPath(values.out);
116
+ } catch {
117
+ throw new UsageError(`--out must be a path relative to the project folder that stays inside it, not ${values.out}`);
118
+ }
119
+ }
120
+ let a: MachineState;
121
+ let b: MachineState;
122
+ if (kind === "observation") {
123
+ a = observation(first);
124
+ b = observation(second);
125
+ } else if (kind === "memory") {
126
+ let base = 0;
127
+ if (values.at !== undefined) {
128
+ try {
129
+ base = parseC64Address(values.at);
130
+ } catch {
131
+ throw new UsageError(`--at must be $ followed by four hex digits, not ${values.at}`);
132
+ }
133
+ }
134
+ const read = (path: string) => readProjectFile(path).bytes;
135
+ const [left, right] = [read(first), read(second)];
136
+ if (left.length !== right.length) throw new UsageError(`the memory files have ${left.length} and ${right.length} bytes; give two of one size`);
137
+ if (base + left.length > 0x10000) throw new UsageError("the memory file goes past $ffff from --at");
138
+ a = { memory: { base, bytes: left } };
139
+ b = { memory: { base, bytes: right } };
140
+ } else {
141
+ const client = await ViceSessionClient.open({ videoStandard: values.ntsc ? "ntsc" : "pal" });
142
+ try {
143
+ a = await snapshotState(client, first);
144
+ b = await snapshotState(client, second);
145
+ } finally {
146
+ await client.close();
147
+ }
148
+ }
149
+ const diff = diffStates(a, b, gap);
150
+ const part = (item: { changedBytes: number; ranges: ChangedRange[] } | undefined) =>
151
+ item === undefined ? undefined : { changedBytes: item.changedBytes, ranges: item.ranges.length, listed: item.ranges.slice(0, max).map(show) };
152
+ if (values.out !== undefined) {
153
+ const all = { a: first, b: second, ...(diff.memory === undefined ? {} : { memory: diff.memory.ranges.map(show) }), ...(diff.colorRam === undefined ? {} : { colorRam: diff.colorRam.ranges.map(show) }), values: diff.values ?? [], observed: diff.observed ?? [] };
154
+ writeProjectFile(values.out, Buffer.from(`${JSON.stringify(all, null, 2)}\n`), { what: "--out" });
155
+ }
156
+ return {
157
+ a: first,
158
+ b: second,
159
+ ...(diff.memory === undefined ? {} : { memory: part(diff.memory) }),
160
+ ...(diff.colorRam === undefined ? {} : { colorRam: part(diff.colorRam) }),
161
+ ...(diff.values === undefined ? {} : { registers: diff.values.slice(0, max) }),
162
+ ...(diff.observed === undefined ? {} : { observed: diff.observed.slice(0, max) }),
163
+ ...(values.out === undefined ? {} : { out: values.out }),
164
+ };
165
+ }
166
+
167
+ await runScript(run, { usage: USAGE });
@@ -0,0 +1,70 @@
1
+ // The differences between two machine states: changed ranges of RAM and
2
+ // colour RAM, and changed chip registers and CPU registers. The states come
3
+ // from snapshots that VICE restores, from memory files, or from the
4
+ // observations of runs; this file only compares.
5
+
6
+ export interface MachineState {
7
+ /** Bytes from `base`: all 64 KiB of RAM, or the bytes of one memory file. */
8
+ memory?: { base: number; bytes: Uint8Array };
9
+ /** Colour RAM $D800-$DBFF, the low 4 bits of each byte. */
10
+ colorRam?: Uint8Array;
11
+ /** Named single values: VIC-II, CIA and SID registers and the CPU registers, as "vic.$d011", "cia1.timerA", "cpu.pc". */
12
+ values?: Record<string, number>;
13
+ /** The memory items of an observation of a run: the address or name, and its bytes as hex. */
14
+ observed?: Record<string, string>;
15
+ }
16
+
17
+ export interface ChangedRange {
18
+ start: number;
19
+ end: number;
20
+ /** The bytes of both states, as hex, when the range has at most 16 bytes. */
21
+ a?: string;
22
+ b?: string;
23
+ }
24
+
25
+ export interface StateDiff {
26
+ memory?: { changedBytes: number; ranges: ChangedRange[] };
27
+ colorRam?: { changedBytes: number; ranges: ChangedRange[] };
28
+ values?: Array<{ name: string; a: number; b: number }>;
29
+ observed?: Array<{ at: string; a: string; b: string }>;
30
+ }
31
+
32
+ /** Changed ranges of two byte arrays of one length, joined across gaps of at most `gap` equal bytes. */
33
+ export function changedRanges(a: Uint8Array, b: Uint8Array, base: number, gap = 0): { changedBytes: number; ranges: ChangedRange[] } {
34
+ const ranges: ChangedRange[] = [];
35
+ let changedBytes = 0;
36
+ for (let offset = 0; offset < a.length; offset++) {
37
+ if (a[offset] === b[offset]) continue;
38
+ changedBytes++;
39
+ const last = ranges.at(-1);
40
+ if (last !== undefined && base + offset - last.end - 1 <= gap) last.end = base + offset;
41
+ else ranges.push({ start: base + offset, end: base + offset });
42
+ }
43
+ for (const range of ranges) {
44
+ if (range.end - range.start + 1 > 16) continue;
45
+ range.a = Buffer.from(a.subarray(range.start - base, range.end - base + 1)).toString("hex");
46
+ range.b = Buffer.from(b.subarray(range.start - base, range.end - base + 1)).toString("hex");
47
+ }
48
+ return { changedBytes, ranges };
49
+ }
50
+
51
+ /** What differs between two states, for each part that both have. */
52
+ export function diffStates(a: MachineState, b: MachineState, gap = 0): StateDiff {
53
+ const result: StateDiff = {};
54
+ if (a.memory !== undefined && b.memory !== undefined) {
55
+ if (a.memory.base !== b.memory.base || a.memory.bytes.length !== b.memory.bytes.length) throw new Error("the two memory parts do not cover the same addresses");
56
+ result.memory = changedRanges(a.memory.bytes, b.memory.bytes, a.memory.base, gap);
57
+ }
58
+ if (a.colorRam !== undefined && b.colorRam !== undefined) result.colorRam = changedRanges(a.colorRam, b.colorRam, 0xd800, gap);
59
+ if (a.values !== undefined && b.values !== undefined) {
60
+ result.values = Object.keys(a.values)
61
+ .filter((name) => b.values![name] !== undefined && a.values![name] !== b.values![name])
62
+ .map((name) => ({ name, a: a.values![name]!, b: b.values![name]! }));
63
+ }
64
+ if (a.observed !== undefined && b.observed !== undefined) {
65
+ result.observed = Object.keys(a.observed)
66
+ .filter((at) => b.observed![at] !== undefined && a.observed![at] !== b.observed![at])
67
+ .map((at) => ({ at, a: a.observed![at]!, b: b.observed![at]! }));
68
+ }
69
+ return result;
70
+ }
@@ -3,14 +3,15 @@
3
3
  // Observations name semantic symbols that resolve per side, comparisons are
4
4
  // explicit, and the result is PASS, FAIL or INCONCLUSIVE.
5
5
 
6
- import { formatC64Address, parseC64Address } from "#src/c64.ts";
7
- import { MAX_BASELINES, MAX_MASK_RECTANGLES, MAX_MEMORY_READ, maskCoversFrame, WireFailure } from "#src/host-client/tools.ts";
6
+ import { formatC64Address, formatSprite, formatVicii, parseC64Address } from "#src/c64.ts";
7
+ import { MAX_BASELINES, MAX_MASK_RECTANGLES, maskCoversFrame, WireFailure } from "#src/host-client/tools.ts";
8
8
  import { LOAD_LIMIT_FRAMES, RUN_LIMIT_FRAMES, runProgram } from "#src/host-client/autostart.ts";
9
9
  import { ACTION_STEP_KEYS, checkActionStep, checkLocation, isInteger, isObject, runActionStep, type ActionStep, type Location } from "#src/host-client/steps.ts";
10
10
  import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
11
- import type { ViceSessionClient } from "#src/host-client/vice-session.ts";
11
+ import { readMemoryInParts, type ViceSessionClient } from "#src/host-client/vice-session.ts";
12
12
  import { openForRead } from "#src/knowledge/database.ts";
13
- import { listSymbols } from "#src/knowledge/read.ts";
13
+ import { listAnalyzerNames, listSymbols } from "#src/knowledge/read.ts";
14
+ import { programImage } from "#src/knowledge/program.ts";
14
15
 
15
16
  export class ScenarioError extends Error {}
16
17
 
@@ -22,11 +23,13 @@ export type { Location };
22
23
 
23
24
  export interface MemoryObservation {
24
25
  at: Location;
25
- /** Bytes from `at`; 1 when not given. */
26
+ /** Bytes from `at`; 1 when not given, at most 64 KiB. */
26
27
  size?: number;
28
+ /** cpu (the default) reads as the CPU sees memory; ram reads the RAM under the ROMs and the I/O area. */
29
+ view?: "cpu" | "ram";
27
30
  /** The largest allowed difference per byte; 0 when not given. */
28
31
  tolerance?: number;
29
- /** Bytes in the range that the comparison leaves out, each with the reason (D46). */
32
+ /** Bytes in the range that the comparison leaves out, each with the reason. */
30
33
  exclude?: Array<{ at: Location; reason: string }>;
31
34
  }
32
35
 
@@ -42,16 +45,16 @@ type StepBody =
42
45
  | { load: true }
43
46
  | { autostart: true }
44
47
  | { snapshot: string }
45
- | { normalize: string }
48
+ | { normalize: true }
46
49
  | ActionStep
47
50
  | { observe: string; memory?: Array<Location | MemoryObservation>; registers?: ObservedRegister[]; screen?: ScreenPolicy; vicii?: boolean; sprites?: number[] };
48
51
 
49
- /** A step; with side, only that side does it (D46). An observe step has no side: it compares both. */
52
+ /** A step; with side, only that side does it. An observe step has no side: it compares both. */
50
53
  export type Step = StepBody & { side?: Side };
51
54
 
52
55
  export interface Subject {
53
56
  program: string;
54
- /** "knowledge" for knowledge.db, a project JSON file (c64-assembler output or a name → "$xxxx" map), or "none". */
57
+ /** "knowledge" for the project knowledge, a project JSON file (c64-assembler output or a name → "$xxxx" map), or "none". */
55
58
  symbols: string;
56
59
  }
57
60
 
@@ -79,7 +82,7 @@ export interface ScenarioResult {
79
82
  reason?: string;
80
83
  checkpoints: string[];
81
84
  differences: Difference[];
82
- /** The bytes that the comparison left out, with their reasons (D46). */
85
+ /** The bytes that the comparison left out, with their reasons. */
83
86
  excluded?: Array<{ checkpoint: string; at: Location; reason: string }>;
84
87
  /** The files that a FAIL wrote with --out. */
85
88
  files?: string[];
@@ -155,7 +158,7 @@ function checkStep(kind: string, step: Record<string, unknown>, at: string, fail
155
158
  if (typeof step.snapshot !== "string" || step.snapshot === "") fail(`${at}: snapshot needs a .vsf file in the project`);
156
159
  return;
157
160
  case "normalize":
158
- if (typeof step.normalize !== "string" || step.normalize === "") fail(`${at}: normalize needs an entry state file in the project`);
161
+ if (step.normalize !== true) fail(`${at}: normalize is true: it writes the entry state of the knowledge`);
159
162
  return;
160
163
  case "write":
161
164
  case "registers":
@@ -178,7 +181,8 @@ function checkObserve(step: Record<string, unknown>, at: string, fail: (message:
178
181
  checkLocation(item, `${at}: memory`, fail);
179
182
  continue;
180
183
  }
181
- if (!isObject(item) || Object.keys(item).some((key) => !["at", "size", "tolerance", "exclude"].includes(key))) return fail(`${at}: a memory object has at, size, tolerance and exclude, and nothing else`);
184
+ if (!isObject(item) || Object.keys(item).some((key) => !["at", "size", "view", "tolerance", "exclude"].includes(key))) return fail(`${at}: a memory object has at, size, view, tolerance and exclude, and nothing else`);
185
+ if (item.view !== undefined && item.view !== "cpu" && item.view !== "ram") fail(`${at}: memory.view must be cpu or ram`);
182
186
  if (item.exclude !== undefined) {
183
187
  if (!Array.isArray(item.exclude) || item.exclude.length === 0) fail(`${at}: memory.exclude must be a list of objects with at and reason`);
184
188
  for (const excluded of item.exclude as unknown[]) {
@@ -188,7 +192,7 @@ function checkObserve(step: Record<string, unknown>, at: string, fail: (message:
188
192
  }
189
193
  }
190
194
  checkLocation(item.at, `${at}: memory.at`, fail);
191
- if (item.size !== undefined && !isInteger(item.size, 1, MAX_MEMORY_READ)) fail(`${at}: memory.size must be a number from 1 to ${MAX_MEMORY_READ}`);
195
+ if (item.size !== undefined && !isInteger(item.size, 1, 0x10000)) fail(`${at}: memory.size must be a number from 1 to 65536`);
192
196
  if (item.tolerance !== undefined && !isInteger(item.tolerance, 0, 0xff)) fail(`${at}: memory.tolerance must be a number from 0 to 255`);
193
197
  }
194
198
  }
@@ -216,7 +220,7 @@ function checkObserve(step: Record<string, unknown>, at: string, fail: (message:
216
220
  isInteger(rectangle.height, 1, 4096);
217
221
  if (!ok) fail(`${at}: a screen.mask rectangle needs whole numbers x and y from 0, and width and height from 1`);
218
222
  }
219
- // The emulator refuses such a compare too, but only after the original side ran (b2/03).
223
+ // The emulator refuses such a compare too, but only after the original side ran.
220
224
  if (maskCoversFrame(screen.mask as Array<{ x: number; y: number; width: number; height: number }>, videoStandard)) fail(`${at}: screen.mask covers the whole screen, so nothing is compared`);
221
225
  }
222
226
  }
@@ -236,6 +240,10 @@ export function loadSymbols(subject: Subject): Map<string, number> {
236
240
  if (subject.symbols === "knowledge") {
237
241
  const db = openForRead();
238
242
  for (const symbol of listSymbols(db)) symbols.set(symbol.name, symbol.address);
243
+ // An analyzer name gives an address too, when no symbol has the name and only one analyzer gives it.
244
+ const second = new Map<string, number[]>();
245
+ for (const row of listAnalyzerNames(db)) second.set(row.name, [...(second.get(row.name) ?? []), row.address]);
246
+ for (const [name, addresses] of second) if (!symbols.has(name) && new Set(addresses).size === 1) symbols.set(name, addresses[0]!);
239
247
  db?.close();
240
248
  return symbols;
241
249
  }
@@ -248,11 +256,12 @@ export function loadSymbols(subject: Subject): Map<string, number> {
248
256
  // c64-assembler output, or a plain name → "$xxxx" map. A constant in the
249
257
  // assembler output counts too: RAM variables such as "counter = $c100" lie
250
258
  // outside the program, so the assembler reports them as constants.
251
- const assembled = (value as { symbols?: Array<{ name: string; address?: string; value?: number }> }).symbols;
259
+ const assembled = (value as { symbols?: Array<{ name: string; address?: string; value?: string }> }).symbols;
252
260
  const entries: Array<[string, unknown]> = Array.isArray(assembled)
253
261
  ? assembled.flatMap((symbol): Array<[string, unknown]> => {
254
262
  if (symbol.address !== undefined) return [[symbol.name, symbol.address]];
255
- if (Number.isInteger(symbol.value) && symbol.value! >= 0 && symbol.value! <= 0xffff) return [[symbol.name, formatC64Address(symbol.value!)]];
263
+ // A constant above $ffff has more digits and is no address.
264
+ if (typeof symbol.value === "string" && /^\$[0-9a-f]{4}$/.test(symbol.value)) return [[symbol.name, symbol.value]];
256
265
  return [];
257
266
  })
258
267
  : Object.entries(value as Record<string, unknown>);
@@ -288,24 +297,25 @@ interface Observation {
288
297
  screen?: { match: boolean; mismatchingPixels: number; comparedPixels: number; mismatchRatio: number; bounds?: unknown };
289
298
  vicii?: unknown;
290
299
  sprites?: unknown;
291
- /** PNG images for the files of a FAIL (D46): the side's screen, and on the rebuild the difference image. */
300
+ /** PNG images for the files of a FAIL: the side's screen, and on the rebuild the difference image. */
292
301
  png?: string;
293
302
  diffPng?: string;
294
303
  }
295
304
 
296
- /** Writes pages 0-3 and $00/$01 from an entry state file (D43, D46): both sides then start from the same RAM. */
297
- async function normalize(client: ViceSessionClient, path: string): Promise<void> {
298
- let state: { pages?: Record<string, string>; cpuPort?: Record<string, number> };
305
+ /** Writes pages 0-3 and $00/$01 from the entry state of the knowledge: both sides then start from the same RAM. */
306
+ async function normalize(client: ViceSessionClient): Promise<void> {
307
+ const db = openForRead();
308
+ let state: { pages?: Record<string, string>; cpuPort?: Record<string, number> } | undefined;
299
309
  try {
300
- state = JSON.parse(readProjectFile(path).bytes.toString("utf8")) as typeof state;
301
- } catch (error) {
302
- if (error instanceof WireFailure) throw error;
303
- throw new ScenarioError(`${path} is not an entry state file: ${(error as Error).message}`);
310
+ state = programImage(db)?.state as typeof state;
311
+ } finally {
312
+ db?.close();
304
313
  }
314
+ if (state === undefined) throw new ScenarioError("normalize needs the entry state of a c64-unpacker capture in the knowledge: put the capture in with knowledge.ts image <image.json> of the c64-knowledge skill");
305
315
  const pages = ["$0000", "$0100", "$0200", "$0300"].map((page) => state.pages?.[page]);
306
316
  const port = [state.cpuPort?.$00, state.cpuPort?.$01];
307
317
  if (pages.some((page) => typeof page !== "string" || !/^[0-9a-f]{512}$/.test(page)) || port.some((value) => !isInteger(value, 0, 0xff))) {
308
- throw new ScenarioError(`${path} is not an entry state file: it needs pages $0000 to $0300 and cpuPort $00 and $01`);
318
+ throw new ScenarioError("the entry state of the knowledge is not the entry state of a c64-unpacker capture: it needs pages $0000 to $0300 and cpuPort $00 and $01");
309
319
  }
310
320
  if ((await client.status()).state === "running") await client.execution({ action: "pause", space: "c64" });
311
321
  // Pages 0 to 3 in RAM from $0002, then the CPU port: the port bits switch what the CPU sees.
@@ -348,7 +358,7 @@ async function runSide(client: ViceSessionClient, scenario: Scenario, side: Side
348
358
  const spec: MemoryObservation = typeof item === "string" ? { at: item } : item;
349
359
  const address = at(spec.at);
350
360
  const size = spec.size ?? 1;
351
- const read = await client.memoryRead({ address, size, space: "c64", view: "cpu" });
361
+ const read = { data: (await readMemoryInParts(client, address, size, spec.view ?? "cpu")).toString("hex") };
352
362
  const excluded = (spec.exclude ?? []).map((exclude) => {
353
363
  const offset = at(exclude.at) - address;
354
364
  if (offset < 0 || offset >= size) throw new ScenarioError(`the exclude ${exclude.at} lies outside the observed ${spec.at} (${size} bytes)`);
@@ -358,14 +368,14 @@ async function runSide(client: ViceSessionClient, scenario: Scenario, side: Side
358
368
  }
359
369
  if (step.registers !== undefined) {
360
370
  const registers = await client.registersGet("c64");
361
- for (const name of step.registers) observation.registers[name] = registers[name];
371
+ for (const name of step.registers) observation.registers[name] = name === "pc" ? formatC64Address(registers.pc) : registers[name];
362
372
  }
363
373
  if (step.vicii) {
364
374
  // The raster line changes from one moment to the next; the comparison leaves it out.
365
375
  const { rasterLine: _rasterLine, ...state } = await client.vicii();
366
- observation.vicii = state;
376
+ observation.vicii = formatVicii(state);
367
377
  }
368
- if (step.sprites !== undefined) observation.sprites = (await client.sprites(step.sprites)).sprites;
378
+ if (step.sprites !== undefined) observation.sprites = (await client.sprites(step.sprites)).sprites.map(formatSprite);
369
379
  if (step.screen !== undefined) {
370
380
  const baseline = baselineName(checkpoint, step.observe);
371
381
  if (side === "original") {
@@ -392,7 +402,7 @@ async function runSide(client: ViceSessionClient, scenario: Scenario, side: Side
392
402
  } else if ("snapshot" in step) {
393
403
  await client.snapshotRestore(readProjectFile(step.snapshot).bytes);
394
404
  } else if ("normalize" in step) {
395
- await normalize(client, step.normalize);
405
+ await normalize(client);
396
406
  } else {
397
407
  const { side: _side, ...action } = step;
398
408
  const notReached = await runActionStep(client, action as ActionStep, at);
@@ -413,7 +423,7 @@ export function compare(original: Map<string, Observation>, rebuild: Map<string,
413
423
  const other = right.memory[index]!;
414
424
  const skipped = new Set(item.excluded.map((excluded) => excluded.offset));
415
425
  if (item.bytes.some((byte, at) => !skipped.has(at) && Math.abs(byte - other.bytes[at]!) > item.tolerance)) {
416
- const show = (bytes: number[]) => (bytes.length === 1 ? bytes[0] : Buffer.from(bytes).toString("hex"));
426
+ const show = (bytes: number[]) => Buffer.from(bytes).toString("hex");
417
427
  differences.push({ checkpoint, what: `memory ${item.label}`, original: show(item.bytes), rebuild: show(other.bytes) });
418
428
  }
419
429
  });
@@ -430,7 +440,7 @@ export function compare(original: Map<string, Observation>, rebuild: Map<string,
430
440
  }
431
441
 
432
442
  /**
433
- * Writes the files of a FAIL into `out` (D46): for a screen difference both
443
+ * Writes the files of a FAIL into `out`: for a screen difference both
434
444
  * screens and the difference image, and memory.json with each differing byte.
435
445
  */
436
446
  function writeFailFiles(out: string, original: Map<string, Observation>, rebuild: Map<string, Observation>, differences: Difference[]): string[] {
@@ -439,7 +449,8 @@ function writeFailFiles(out: string, original: Map<string, Observation>, rebuild
439
449
  writeProjectFile(`${out}/${name}`, bytes, { what: "--out" });
440
450
  files.push(name);
441
451
  };
442
- const memory: Array<{ checkpoint: string; what: string; bytes: Array<{ address: string; original: number; rebuild: number }> }> = [];
452
+ const memory: Array<{ checkpoint: string; what: string; bytes: Array<{ address: string; original: string; rebuild: string }> }> = [];
453
+ const hex = (byte: number) => byte.toString(16).padStart(2, "0");
443
454
  for (const difference of differences) {
444
455
  const left = original.get(difference.checkpoint)!;
445
456
  const right = rebuild.get(difference.checkpoint)!;
@@ -454,7 +465,7 @@ function writeFailFiles(out: string, original: Map<string, Observation>, rebuild
454
465
  const item = left.memory[index]!;
455
466
  const other = right.memory[index]!;
456
467
  const skipped = new Set(item.excluded.map((excluded) => excluded.offset));
457
- const bytes = item.bytes.flatMap((byte, offset) => (!skipped.has(offset) && Math.abs(byte - other.bytes[offset]!) > item.tolerance ? [{ address: formatC64Address(item.address + offset), original: byte, rebuild: other.bytes[offset]! }] : []));
468
+ const bytes = item.bytes.flatMap((byte, offset) => (!skipped.has(offset) && Math.abs(byte - other.bytes[offset]!) > item.tolerance ? [{ address: formatC64Address(item.address + offset), original: hex(byte), rebuild: hex(other.bytes[offset]!) }] : []));
458
469
  memory.push({ checkpoint: difference.checkpoint, what: difference.what, bytes });
459
470
  }
460
471
  }