@henols/c64-re-tools 2.1.1 → 2.1.3

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 (67) hide show
  1. package/package.json +1 -1
  2. package/skills/c64-assembler/SKILL.md +6 -3
  3. package/skills/c64-assembler/scripts/assembler.ts +19 -11
  4. package/skills/c64-assembler/scripts/lib/compare.ts +4 -3
  5. package/skills/c64-basic/SKILL.md +1 -0
  6. package/skills/c64-disk/SKILL.md +1 -0
  7. package/skills/c64-disk/scripts/disk.ts +2 -2
  8. package/skills/c64-disk/scripts/lib/containers.ts +4 -4
  9. package/skills/c64-docs/SKILL.md +2 -1
  10. package/skills/c64-docs/scripts/docs.ts +1 -1
  11. package/skills/c64-docs/scripts/lib/links.ts +2 -0
  12. package/skills/c64-emulator/SKILL.md +4 -1
  13. package/skills/c64-knowledge/SKILL.md +12 -6
  14. package/skills/c64-knowledge/scripts/knowledge.ts +41 -25
  15. package/skills/c64-knowledge/scripts/lib/listing.ts +16 -31
  16. package/skills/c64-memory-map/SKILL.md +3 -0
  17. package/skills/c64-memory-map/scripts/memory-map.ts +23 -4
  18. package/skills/c64-provenance/SKILL.md +5 -3
  19. package/skills/c64-provenance/scripts/lib/library.ts +28 -5
  20. package/skills/c64-provenance/scripts/lib/separate.ts +34 -6
  21. package/skills/c64-provenance/scripts/provenance.ts +22 -10
  22. package/skills/c64-source/SKILL.md +2 -1
  23. package/skills/c64-source/scripts/lib/source.ts +25 -33
  24. package/skills/c64-source/scripts/source.ts +13 -17
  25. package/skills/c64-static-analysis/SKILL.md +6 -3
  26. package/skills/c64-static-analysis/scripts/lib/findings.ts +23 -8
  27. package/skills/c64-static-analysis/scripts/lib/pointers.ts +38 -25
  28. package/skills/c64-static-analysis/scripts/static-analysis.ts +31 -19
  29. package/skills/c64-testing/SKILL.md +9 -5
  30. package/skills/c64-testing/references/scenario-format.md +5 -3
  31. package/skills/c64-testing/scripts/lib/diff-files.ts +41 -10
  32. package/skills/c64-testing/scripts/lib/recording.ts +3 -3
  33. package/skills/c64-testing/scripts/lib/replay.ts +1 -1
  34. package/skills/c64-testing/scripts/lib/runs.ts +45 -9
  35. package/skills/c64-testing/scripts/lib/scenario.ts +130 -81
  36. package/skills/c64-testing/scripts/testing.ts +9 -6
  37. package/skills/c64-unpacker/SKILL.md +10 -7
  38. package/skills/c64-unpacker/scripts/lib/loads.ts +15 -6
  39. package/skills/c64-unpacker/scripts/lib/machine.ts +61 -3
  40. package/skills/c64-unpacker/scripts/lib/stages.ts +15 -7
  41. package/skills/c64-unpacker/scripts/unpacker.ts +21 -17
  42. package/src/host/vice/adapter.ts +4 -1
  43. package/src/host/vice/session.ts +144 -11
  44. package/src/host-client/image.ts +27 -13
  45. package/src/host-client/steps.ts +41 -5
  46. package/src/host-client/tools.ts +4 -3
  47. package/src/host-client/transfer.ts +22 -29
  48. package/src/knowledge/history.ts +55 -2
  49. package/src/knowledge/import.ts +79 -7
  50. package/src/knowledge/program.ts +31 -2
  51. package/src/knowledge/read.ts +7 -4
  52. package/src/knowledge/schema.ts +3 -2
  53. package/src/knowledge/text.ts +1 -0
  54. package/src/knowledge/write.ts +86 -42
  55. package/src/mcp/inputs.ts +6 -3
  56. package/src/mcp/output.ts +1 -1
  57. package/src/mcp/server.ts +9 -1
  58. package/src/mcp/tools/debug.ts +31 -4
  59. package/src/mcp/tools/machine.ts +3 -1
  60. package/src/mcp/tools/media.ts +5 -1
  61. package/src/mcp/tools/memory.ts +4 -3
  62. package/src/media.ts +57 -0
  63. package/src/opcodes.ts +81 -18
  64. package/src/platform.ts +14 -1
  65. package/src/project.ts +118 -3
  66. package/src/protocol/vice.ts +4 -1
  67. package/src/script.ts +118 -5
@@ -11,7 +11,7 @@ import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
11
11
  import { join } from "node:path";
12
12
 
13
13
  import { Refusal } from "#src/errors.ts";
14
- import { type AddressRange, COVERAGE, coverageSeedKinds, formatC64Address, textToPetscii } from "#src/c64.ts";
14
+ import { type AddressRange, coverageSeedKinds, formatC64Address, textToPetscii } from "#src/c64.ts";
15
15
  import { autostartLoaded, RUN_LIMIT_FRAMES } from "#src/host-client/autostart.ts";
16
16
  import { MAX_MEMORY_READ, WireFailure } from "#src/host-client/tools.ts";
17
17
  import { writeReported, type Written } from "#src/host-client/transfer.ts";
@@ -26,13 +26,13 @@ import { assemble } from "#src/native/acme.ts";
26
26
  import { analyze } from "#src/native/ghidra/analyze.ts";
27
27
  import { localToolContext } from "#src/native/local.ts";
28
28
  import { GHIDRA_LIMITS, type AssembledSymbol, type Diagnostic } from "#src/native/types.ts";
29
- import { decode as decodeRegister, describeAddress, platformNamesFor, romUseOf, romUseOfPort, type RomUse } from "#src/platform.ts";
29
+ import { decode as decodeRegister, describeAddress, platformNames, platformNamesFor, romUseOfProgram } from "#src/platform.ts";
30
30
  import { resolveProjectPath } from "#src/project.ts";
31
31
  import { listed, OPTIONS, runScript, UsageError, type Call } from "#src/script.ts";
32
32
  import { knowledgeNames } from "#src/knowledge/names.ts";
33
33
  import { COPIER_SOURCE, LOADER_JUMP, loaderLimits, planLoader, rasterLine, loaderState, simulate, stopCause, VIC_DISPLAY_REGISTERS, type EntryState, type LoaderPlan } from "./lib/loader.ts";
34
- import { OPCODES } from "#src/opcodes.ts";
35
- import { entriesInData, entryOutsideImage, instructionText, layout, literalOperand, probeForms, render, switchesOf, type Layout, type PatchBlock, type Rendered } from "./lib/source.ts";
34
+ import { instructionWith, OPCODES } from "#src/opcodes.ts";
35
+ import { entriesInData, entryOutsideImage, layout, literalOperand, probeForms, render, switchesOf, type Layout, type PatchBlock, type Rendered } from "./lib/source.ts";
36
36
 
37
37
  /** At most this many diagnostics or differences of one check. */
38
38
  const MAX_DIAGNOSTICS = 20;
@@ -182,7 +182,7 @@ async function probe(laid: Layout): Promise<Set<string>> {
182
182
  ? formatC64Address((slot + 2 + (item.bytes[1]! < 0x80 ? item.bytes[1]! : item.bytes[1]! - 0x100)) & 0xffff)
183
183
  : literalOperand(item.form.opcode, item.bytes.length === 3 ? item.bytes[1]! | (item.bytes[2]! << 8) : item.bytes[1]!);
184
184
  lines.push(`* = ${formatC64Address(slot)}`);
185
- lines.push(` ${instructionText(item.form.opcode, item.form.suffix, operand)}`);
185
+ lines.push(` ${instructionWith(item.form.opcode, operand, item.form.suffix)}`);
186
186
  lineOf.set(lines.length, item.form.key);
187
187
  });
188
188
  const built = await assembleFiles([{ name: "probe.asm", text: `${lines.join("\n")}\n` }], "probe.asm");
@@ -491,15 +491,10 @@ async function runLoader(path: string, program: Buffer, plan: LoaderPlan, image:
491
491
  }
492
492
  }
493
493
 
494
- /**
495
- * The ROMs the program uses: from the coverage when the knowledge has
496
- * it, else from $01 of the entry state, else both.
497
- */
498
- function romsInUse(image: ProgramImage, coverage: Uint8Array | undefined): RomUse & { from: "coverage" | "entry state" | "none" } {
499
- if (coverage !== undefined) return { ...romUseOf(coverage, COVERAGE.romExecute), from: "coverage" };
500
- const port = (image.state?.cpuPort as Record<string, unknown> | undefined)?.$01;
501
- if (typeof port === "number") return { ...romUseOfPort(port), from: "entry state" };
502
- return { basic: true, kernal: true, from: "none" };
494
+ /** The platform names that `used` leaves out: the names of the ROMs that the program does not use. */
495
+ function leftOutNames(used: ReadonlyArray<{ name: string }>): Set<string> {
496
+ const kept = new Set(used.map((entry) => entry.name));
497
+ return new Set(platformNames().flatMap((entry) => (kept.has(entry.name) ? [] : [entry.name])));
503
498
  }
504
499
 
505
500
  /** The first words of every file that the script writes. */
@@ -549,7 +544,8 @@ async function generate(call: Call): Promise<{ output: Record<string, unknown>;
549
544
  }
550
545
  // Read the entry state before the long steps, so a bad state stops the run at once.
551
546
  const state = loaderWanted ? loaderState(image.state, "the entry state of the knowledge") : undefined;
552
- const romUse = romsInUse(image, coverage);
547
+ const romUse = romUseOfProgram(coverage, image.state);
548
+ const platform = platformNamesFor(romUse);
553
549
  const { instructions, dataOnly } = await decode(image, knowledge, entries, coverage);
554
550
  const laid = layout({
555
551
  segments: image.segments,
@@ -567,7 +563,7 @@ async function generate(call: Call): Promise<{ output: Record<string, unknown>;
567
563
  macroUses: knowledge.macroUses,
568
564
  ...(coverage === undefined ? {} : { coverage }),
569
565
  // The platform names are C64 names: drive code and banks get none. A ROM that the program does not use gives none.
570
- ...(space === DEFAULT_SPACE ? { platform: platformNamesFor(romUse), describe: describeAddress, decode: decodeRegister } : {}),
566
+ ...(space === DEFAULT_SPACE ? { platform, platformLeftOut: leftOutNames(platform), describe: describeAddress, decode: decodeRegister } : {}),
571
567
  });
572
568
  const asBytes = await probe(laid);
573
569
  const header = [`Generated by c64-source from knowledge revision ${knowledge.revision}${space === DEFAULT_SPACE ? "" : ` in space ${space}`}.`, "Change names, comments, regions and modules in knowledge, then generate again."];
@@ -734,7 +730,7 @@ await runScript({
734
730
  name: "generate",
735
731
  summary: "Writes the source files, source-map.json and not_executed.json, and checks them. result is PASS when every check passes, else FAIL.",
736
732
  options: {
737
- out: { ...OPTIONS.out, value: "<dir>", help: "the folder for the source files, relative to the project folder", required: true },
733
+ out: { ...OPTIONS.out, output: "folder", value: "<dir>", help: "the folder for the source files, relative to the project folder", required: true },
738
734
  entry: { ...OPTIONS.entry, multiple: true, help: "an entry point for Ghidra" },
739
735
  space: { ...OPTIONS.space, help: "the knowledge space of the program: c64 (the default), drive8, or bank0 to bank255. The coverage and --loader are for c64 only" },
740
736
  loader: { kind: "flag", help: "also write loader.asm and loader.prg: the loader copies the segments into place, sets the entry state of the knowledge, and jumps to the entry. It is checked in the emulator" },
@@ -40,9 +40,10 @@ Both analyzers use the knowledge from the user and the LLM as seeds:
40
40
 
41
41
  - Each routine is an entry point.
42
42
  - Each region that is not code stays data.
43
+ - Each symbol of the kind `data` or `variable` stays data. The region that is not code and that holds the symbol stays data, also a region of an analyzer. Without such a region, the byte of the symbol stays data. A symbol in a code region or at an executed address is not data. When the region holds an entry point or an executed address, only the byte of the symbol stays data.
43
44
  - Each name is a label. The listing, Ghidra and the decompiler show that name.
44
45
 
45
- Facts that an analyzer found are not seeds. A new analysis can then correct or remove them.
46
+ Facts that an analyzer found are not seeds alone. A new analysis can then correct or remove them.
46
47
 
47
48
  More seeds come from evidence:
48
49
 
@@ -61,6 +62,7 @@ The analyzers take at most 1024 entry points. Knowledge routines and `--entry` c
61
62
  - An address is `$` and 1 to 4 hex digits, for example `'$c000'` or `'$fb'`, or a name. A name is a symbol, else an analyzer name that only one analyzer gives. Quote an address in a shell.
62
63
  - A range is `<start>-<end>`, for example `'$c000-$cfff'`. In a JSON file, a range is `start` and `end`.
63
64
  - Bytes in a JSON file are hex in a field `data`, for example `"data": "a900"`.
65
+ - The script examines each `--out` path before it starts work. It refuses a path in `.c64-re-tools/knowledge/` and the index of the knowledge, because only c64-knowledge writes them. It refuses a folder where the command writes a file, and a file where the command writes a folder.
64
66
  - The result is one JSON object on one line. A file that the script writes shows as `{"out", "bytes", "sha256"}`. A list shows as `{"count", "listed"}`, and `rest` tells where to get the rest of a long list.
65
67
  - An error is `{"error": {"code": "...", "message": "..."}}`. The message tells what is wrong and what to do.
66
68
  - The exit status is 0 for done or PASS, 1 for FAIL, 2 for an error and 3 for INCONCLUSIVE.
@@ -87,10 +89,11 @@ The analyzers take at most 1024 entry points. Knowledge routines and `--entry` c
87
89
  - Ghidra: `functions` gives the number of routines that Ghidra found (`count`) and lists the first routines (`listed`). `decompilations` gives C-like code for the requested routines.
88
90
  - If a list is not complete, `rest` gives the c64-knowledge command that lists all of them. For `candidates`, `rest` tells you to give a larger `--max-listed`.
89
91
  - `conflicts` lists the places where the analyzer and the current knowledge disagree.
92
+ - DXA or Ghidra can find data where a run executed an instruction start (`executed`), or where a `user` or `llm` symbol of kind `label` or `routine` is (`knowledge`). Then the script does not write that data region, and the earlier region of the analyzer stays. `conflicts` lists each such place. Executed code and your names have priority over the guess of an analyzer.
90
93
  - `idioms` tells what the script found by pattern: RTS dispatch tables (`rtsTables`), JMP tables (`jumpTables`) and BIT skips (`bitSkips`). It also finds the `CBM80` header of a cartridge bank at `$8000` (`cartridgeHeaders`). The two start vectors of the header become entry points. The script writes them as knowledge with the origin `heuristic`, and seeds their targets into the same analysis. `candidates` gives DOP padding (`dop`) and opcodes that the program wrote and executed (`selfModified`). For `selfModified`, the write and the execution must be in one coverage file. A coverage file does not record the order. Thus a candidate can also be code that ran first, and that the program then wrote over. To prevent this, save one coverage file for each part of the program, for example the intro and the game. A loader that copies relocated code is also such a part. Clear the memory map between the parts. Each list has a `count`. The script does not write the candidates. Look at them and decide.
91
- - `candidates.pointers` gives code that makes an address from immediates. A `vector` or a `zero-page pointer` candidate stores a low byte (`low`) and a high byte (`high`) to a vector or to a zero-page pair that `(zp),y` uses. Its `target` is in the program or has a platform name. A `sprite pointer` candidate stores an immediate (`at`) to a sprite pointer of the screen at $0400 in VIC bank 0. Its `target` (the value × 64) is in a `sprite` region. For each candidate that you confirm, write operand expressions with `operand` of the c64-knowledge skill, for example `operand '<low>' --lo <target name>` and `operand '<high>' --hi <target name>`. Then a rebuild can move the target.
94
+ - `candidates.pointers` gives code that makes an address from immediates. The script reads only the instructions of the code regions, from the start of each region. A `vector` or a `zero-page pointer` candidate stores a low byte (`low`) and a high byte (`high`) to a vector or to a zero-page pair. A `(zp),y` instruction of the code uses the zero-page pair. Its `target` is in the program or has a platform name. A `sprite pointer` candidate stores an immediate (`at`) to a sprite pointer of the screen at $0400 in VIC bank 0. Its `target` (the value × 64) is in a `sprite` region. For each candidate that you confirm, write operand expressions with `operand` of the c64-knowledge skill, for example `operand '<low>' --lo <target name>` and `operand '<high>' --hi <target name>`. Then a rebuild can move the target.
92
95
  - `candidates.pitfalls` gives code that a C64 runs in a different way than it reads. `raster-bit-8`: the instruction at `at` reads $D011, and the instruction at `store` writes the value back. A read of $D011 gives bit 8 of the raster line in bit 7. Thus the write can move the raster interrupt to line 256 or later, and on PAL past line 311 the interrupt never comes. Examine each candidate. Where the code must not keep bit 7, add a comment and test the program.
93
- - `candidates.macros` gives runs of instructions that a macro writes as one line, for example `+inc16 ptr`. The script knows these 6502 idioms: `inc16`, `dec16`, `add16_8`, `add16`, `sub16`, `set16`, `mov16` and `wait_line`. It reads the code regions after the analysis. An arg has the name of a `user` or `llm` symbol or a platform name. Each candidate has `macro`, `start`, `end`, `args` and `text`. `known` counts the uses that the knowledge has. `leftOut` counts the candidates with a label inside, or with a patch or a use over them. The c64-source skill writes such a candidate as instructions. With `--out-macro-batch`, `batch` gives the file, the count of writes (`writes`) and the `apply` command. The batch holds the macros that the knowledge does not have, and the uses. When the knowledge has a macro with the same name and another body, `otherMacros` tells that the batch does not have its uses. The script does not write the candidates. Apply the batch, then generate the source again.
96
+ - `candidates.macros` gives runs of instructions that a macro writes as one line, for example `+inc16 ptr`. The script knows these 6502 idioms: `inc16`, `dec16`, `add16_8`, `add16`, `sub16`, `set16`, `mov16` and `wait_line`. It reads the code regions after the analysis. An arg has the name of a `user` or `llm` symbol or a platform name. A platform name of a ROM that the program does not use is not an arg, because the c64-source skill does not use it. Each candidate has `macro`, `start`, `end`, `args` and `text`. `known` counts the uses that the knowledge has. `leftOut` counts the candidates with a label inside, or with a patch or a use over them. The c64-source skill writes such a candidate as instructions. With `--out-macro-batch`, `batch` gives the file, the count of writes (`writes`) and the `apply` command. The batch holds the macros that the knowledge does not have, and the uses. When the knowledge has a macro with the same name and another body, `otherMacros` tells that the batch does not have its uses. The script does not write the candidates. Apply the batch, then generate the source again.
94
97
  - When the knowledge has coverage, a code finding replaces the DXA or Ghidra data where a run executed an instruction start. `coverageConflicts` lists the analyzer data regions where the coverage still shows executed code. Classify them as code with the c64-knowledge skill, or as `dual-use` when the bytes are also data. For many conflicts, write all classifications in one `apply` batch of the c64-knowledge skill.
95
98
  6. Give each important routine a name with the c64-knowledge skill. Then run the analysis again. The analyzers use the new names.
96
99
 
@@ -15,19 +15,34 @@ const semantic = (row: { origin: string }) => isSemantic(row.origin);
15
15
  /**
16
16
  * Seeds from current semantic knowledge: routines from the user or
17
17
  * the LLM are entry points, their non-code regions are data, and their names
18
- * are labels. Analyzer facts are never seeds: fed back, an analyzer's own
19
- * earlier guess would force the same answer again, and a re-analysis could
20
- * not retire it.
18
+ * are labels. Analyzer facts are never seeds alone: fed back, an analyzer's
19
+ * own earlier guess would force the same answer again, and a re-analysis
20
+ * could not retire it. A data or variable symbol of the user or the LLM is a
21
+ * decision that its address is data: the non-code region that holds it is
22
+ * data, also a region of an analyzer, else the byte of the symbol. Where a
23
+ * code region holds the symbol (a variable in the operand of self-modifying
24
+ * code) or the address executed, the symbol gives no data. A region with an
25
+ * executed address or an entry point in it gives only the byte of the symbol.
21
26
  */
22
- export function seedsFromKnowledge(db: DatabaseSync | undefined, extraEntryPoints: number[] = [], space: Space = DEFAULT_SPACE): Seeds {
27
+ export function seedsFromKnowledge(db: DatabaseSync | undefined, extraEntryPoints: number[] = [], space: Space = DEFAULT_SPACE, executed: (address: number) => boolean = () => false): Seeds {
23
28
  const symbols = listSymbols(db, undefined, undefined, undefined, space).filter(semantic);
24
29
  const entryPoints = [...new Set([...extraEntryPoints, ...symbols.filter((symbol) => symbol.kind === "routine").map((symbol) => symbol.address)])].sort((a, b) => a - b);
30
+ const regions = listRegions(db, undefined, undefined, undefined, space);
31
+ // A JMP table is code and dual-use bytes are both: neither is a data range.
32
+ const isData = (region: { type: string }) => !CODE_REGION_TYPES.includes(region.type as never) && region.type !== "dual-use";
33
+ const dataRanges = regions.filter((region) => semantic(region) && isData(region)).map((region) => ({ start: region.start, end: region.end }));
34
+ const covered = (range: { start: number; end: number }) => dataRanges.some((seed) => seed.start <= range.end && range.start <= seed.end);
35
+ const holdsCode = (range: { start: number; end: number }) => entryPoints.some((entry) => entry >= range.start && entry <= range.end) || Array.from({ length: range.end - range.start + 1 }, (_, offset) => range.start + offset).some(executed);
36
+ for (const symbol of symbols.filter((row) => row.kind === "data" || row.kind === "variable").sort((a, b) => a.address - b.address)) {
37
+ const own = { start: symbol.address, end: symbol.address };
38
+ const region = regions.find((row) => row.start <= symbol.address && symbol.address <= row.end);
39
+ if ((region !== undefined && !isData(region)) || holdsCode(own) || covered(own)) continue;
40
+ const range = region !== undefined && !holdsCode(region) ? { start: region.start, end: region.end } : own;
41
+ dataRanges.push(range);
42
+ }
25
43
  return {
26
44
  entryPoints,
27
- dataRanges: listRegions(db, undefined, undefined, undefined, space)
28
- // A JMP table is code and dual-use bytes are both: neither is a data range.
29
- .filter((region) => semantic(region) && !CODE_REGION_TYPES.includes(region.type) && region.type !== "dual-use")
30
- .map((region) => ({ start: region.start, end: region.end })),
45
+ dataRanges: dataRanges.sort((a, b) => a.start - b.start),
31
46
  labels: symbols.map((symbol) => ({ address: symbol.address, name: symbol.name })),
32
47
  };
33
48
  }
@@ -3,12 +3,14 @@
3
3
  // that moves the address needs an operand expression there. They are only
4
4
  // reported, never written: the LLM records the expressions with c64-knowledge.
5
5
  //
6
- // The scan reads short straight runs of instructions: LDA/LDX/LDY #imm and
7
- // STA/STX/STY to zero page or absolute, plus SEI, CLI, CLC, SEC and NOP. Any
8
- // other instruction ends the run, so no register value is guessed across it.
6
+ // The scan reads the instructions of the code regions, decoded from the start
7
+ // of each region: bytes of data are never read as an instruction. It reads
8
+ // short straight runs: LDA/LDX/LDY #imm and STA/STX/STY to zero page or
9
+ // absolute, plus SEI, CLI, CLC, SEC and NOP. Any other instruction ends the
10
+ // run, so no register value is guessed across it.
9
11
 
10
- import { instructionLength } from "#src/c64.ts";
11
12
  import type { ImageByte } from "./coverage.ts";
13
+ import { instructionAt, type Instruction } from "./macros.ts";
12
14
 
13
15
  const LOADS: Record<number, "a" | "x" | "y"> = { 0xa9: "a", 0xa2: "x", 0xa0: "y" };
14
16
  const STORES: Record<number, "a" | "x" | "y"> = { 0x85: "a", 0x8d: "a", 0x86: "x", 0x8e: "x", 0x84: "y", 0x8c: "y" };
@@ -50,46 +52,57 @@ interface Store {
50
52
  to: number;
51
53
  }
52
54
 
53
- /** The stores of immediates in the straight run that starts at `start`. */
54
- function storesFrom(byteAt: ImageByte, start: number, range: { start: number; end: number }): Store[] {
55
+ /** The stores of immediates in the straight run of decoded instructions that starts at `start`. */
56
+ function storesFrom(run: ReadonlyArray<Instruction & { at: number }>, start: number): Store[] {
55
57
  const last: Partial<Record<"a" | "x" | "y", { load: number; value: number }>> = {};
56
58
  const stores: Store[] = [];
57
- let at = start;
58
- for (let count = 0; count < MAX_RUN; count++) {
59
- const opcode = byteAt(at);
60
- if (opcode === undefined) break;
61
- const length = instructionLength(opcode);
62
- if (at + length - 1 > range.end) break;
63
- const operand = length === 2 ? byteAt(at + 1)! : length === 3 ? byteAt(at + 1)! | (byteAt(at + 2)! << 8) : 0;
64
- if (LOADS[opcode] !== undefined) last[LOADS[opcode]] = { load: at, value: operand };
59
+ for (const { opcode, value, at } of run.slice(start, start + MAX_RUN)) {
60
+ if (LOADS[opcode] !== undefined) last[LOADS[opcode]] = { load: at, value };
65
61
  else if (STORES[opcode] !== undefined) {
66
62
  const loaded = last[STORES[opcode]];
67
63
  if (loaded === undefined) break;
68
- stores.push({ ...loaded, to: operand });
64
+ stores.push({ ...loaded, to: value });
69
65
  } else if (!NEUTRAL.has(opcode)) break;
70
- at += length;
71
66
  }
72
67
  return stores;
73
68
  }
74
69
 
70
+ /** The instructions of each code region, decoded from its start, up to a byte that is no instruction. */
71
+ function decoded(byteAt: ImageByte, code: ReadonlyArray<{ start: number; end: number }>): Array<Array<Instruction & { at: number }>> {
72
+ return [...code]
73
+ .sort((a, b) => a.start - b.start)
74
+ .map((region) => {
75
+ const run: Array<Instruction & { at: number }> = [];
76
+ for (let at = region.start; at <= region.end; ) {
77
+ const instruction = instructionAt(byteAt, at, region.end);
78
+ if (instruction === undefined) break;
79
+ run.push({ ...instruction, at });
80
+ at += instruction.length;
81
+ }
82
+ return run;
83
+ });
84
+ }
85
+
75
86
  /**
76
- * Finds the pointer candidates in the image: an immediate pair stored to a
77
- * vector or to a zero-page pair that (zp),y uses, where the pair makes an
78
- * address in the image or a platform name; and an immediate stored to a sprite
79
- * pointer of the default screen whose sprite data is in a sprite region.
87
+ * Finds the pointer candidates in the code regions of the image: an immediate
88
+ * pair stored to a vector or to a zero-page pair that a (zp),y instruction of
89
+ * the code uses, where the pair makes an address in the image or a platform
90
+ * name; and an immediate stored to a sprite pointer of the default screen whose
91
+ * sprite data is in a sprite region.
80
92
  */
81
93
  export function findPointers(
82
94
  byteAt: ImageByte,
83
95
  range: { start: number; end: number },
96
+ code: ReadonlyArray<{ start: number; end: number }>,
84
97
  known: { platform: (address: number) => boolean; sprites: ReadonlyArray<{ start: number; end: number }> },
85
98
  ): PointerCandidate[] {
86
- const indirect = new Set<number>();
87
- for (let at = range.start; at < range.end; at++) if (INDIRECT_Y.has(byteAt(at)!)) indirect.add(byteAt(at + 1)!);
99
+ const runs = decoded(byteAt, code);
100
+ const indirect = new Set(runs.flatMap((run) => run.filter((instruction) => INDIRECT_Y.has(instruction.opcode)).map((instruction) => instruction.value)));
88
101
  const inImage = (address: number) => address >= range.start && address <= range.end && byteAt(address) !== undefined;
89
102
  const found = new Map<string, PointerCandidate>();
90
- for (let at = range.start; at <= range.end; at++) {
91
- if (LOADS[byteAt(at)!] === undefined) continue;
92
- const stores = storesFrom(byteAt, at, range);
103
+ for (const [run, index] of runs.flatMap((run) => run.map((_, index) => [run, index] as const))) {
104
+ if (LOADS[run[index]!.opcode] === undefined) continue;
105
+ const stores = storesFrom(run, index);
93
106
  for (const low of stores) {
94
107
  const high = stores.find((store) => store.to === low.to + 1);
95
108
  const target = high === undefined ? undefined : low.value | (high.value << 8);
@@ -4,7 +4,7 @@
4
4
  // structural findings into .c64-re-tools/knowledge/.
5
5
 
6
6
  import { COVERAGE, formatC64Address, prgRange } from "#src/c64.ts";
7
- import { Refusal } from "#src/errors.ts";
7
+ import { isRefusal, Refusal } from "#src/errors.ts";
8
8
  import { WireFailure } from "#src/host-client/tools.ts";
9
9
  import { writeReported } from "#src/host-client/transfer.ts";
10
10
  import { knowledgeNames } from "#src/knowledge/names.ts";
@@ -17,7 +17,7 @@ import { analyzedBytes, programCoverage, requireProgram } from "#src/knowledge/p
17
17
  import { importFindings, runProvenance, type ImportConflict, type ImportContext, type ImportResult, type NormalizedFindings } from "#src/knowledge/import.ts";
18
18
  import { currentRevision, listMacros, listMacroUses, listPatches, listReferences, listRegions, listSymbols } from "#src/knowledge/read.ts";
19
19
  import { DEFAULT_SPACE, SEMANTIC_ORIGINS, type Analyzer } from "#src/knowledge/schema.ts";
20
- import { platformNames } from "#src/platform.ts";
20
+ import { platformNames, platformNamesFor, romUseOfProgram, type RomUse } from "#src/platform.ts";
21
21
  import { listed, OPTIONS, runScript, UsageError, type Call } from "#src/script.ts";
22
22
  import { codeEntries, coverageConflicts, coverageEntries, coverageSeedKinds, executedStarts, imageReader, romOnlyRanges, type ImageByte } from "./lib/coverage.ts";
23
23
  import { dxaFindings, ghidraFindings, seedsFromKnowledge } from "./lib/findings.ts";
@@ -52,17 +52,19 @@ function showMacro(candidate: MacroCandidate) {
52
52
 
53
53
  /**
54
54
  * The macro candidates in the current code regions of every origin,
55
- * with the names of the knowledge in their args. A candidate is left out when
55
+ * with the names of the knowledge in their args, else the platform names of
56
+ * the ROMs that the program uses, as c64-source names them. A candidate is left out when
56
57
  * a name or a reference target is inside it, or a patch or a use overlaps it:
57
58
  * c64-source would write its instructions. The same use in the knowledge is
58
59
  * known. With `batchPath`, the candidates go to an apply batch with the
59
60
  * macros they need; a macro of the catalogue that the knowledge has with
60
61
  * another body leaves its candidates out of the batch.
61
62
  */
62
- function macroCandidates(space: string, byteAt: ImageByte, batchPath: string | undefined) {
63
+ function macroCandidates(space: string, byteAt: ImageByte, batchPath: string | undefined, romUse: RomUse) {
63
64
  const db = openForRead();
64
65
  try {
65
- const platform = space === DEFAULT_SPACE ? new Map(platformNames().map((entry) => [entry.address, entry.name])) : new Map<number, string>();
66
+ // The platform names that c64-source uses: a name of a ROM that the program does not use is no arg.
67
+ const platform = space === DEFAULT_SPACE ? new Map(platformNamesFor(romUse).map((entry) => [entry.address, entry.name])) : new Map<number, string>();
66
68
  const semantic = new Map(listSymbols(db, undefined, undefined, [...SEMANTIC_ORIGINS], space).map((symbol) => [symbol.address, symbol.name]));
67
69
  const code = listRegions(db, undefined, "code", undefined, space);
68
70
  const found = findMacroUses(byteAt, code, (address) => semantic.get(address) ?? platform.get(address));
@@ -121,6 +123,15 @@ function withRemedy(reason: string): string {
121
123
 
122
124
  /** One conflict: what the knowledge has, and what the analyzer found, under the analyzer's name. */
123
125
  function showConflict(conflict: ImportConflict, analyzer: Analyzer): Record<string, unknown> {
126
+ if (conflict.category === "code-evidence") {
127
+ return {
128
+ range: { start: formatC64Address(conflict.start), end: formatC64Address(conflict.end) },
129
+ [analyzer]: conflict.finding,
130
+ ...(conflict.executed === undefined ? {} : { executed: formatC64Address(conflict.executed) }),
131
+ ...(conflict.symbol === undefined ? {} : { knowledge: { ...conflict.symbol, address: formatC64Address(conflict.symbol.address) } }),
132
+ problem: conflict.executed !== undefined ? "a run executed code here: the data finding is not imported" : "the knowledge has a code name here: the data finding is not imported",
133
+ };
134
+ }
124
135
  if (conflict.category === "symbol") {
125
136
  return {
126
137
  at: formatC64Address(conflict.address),
@@ -188,7 +199,7 @@ async function analyze(call: Call): Promise<unknown> {
188
199
  const idioms = findIdioms(inProgram, { start: loaded.start, end: loaded.end }, covered);
189
200
 
190
201
  const revision = currentRevision(reader);
191
- const seeds = seedsFromKnowledge(reader, entries, space);
202
+ const seeds = seedsFromKnowledge(reader, entries, space, executed.executed);
192
203
  // Knowledge routines and --entry first, then code regions and JMP tables, then idioms, then coverage, up to the seed bound.
193
204
  const fromCoverage = coverage === undefined ? [] : coverageEntries(coverage, inProgram);
194
205
  const known = [...seeds.entryPoints, ...codeEntries(reader, byteAt, space)];
@@ -207,20 +218,21 @@ async function analyze(call: Call): Promise<unknown> {
207
218
  const coverageUsed = fromCoverage.filter((entry) => seeds.entryPoints.includes(entry) || codeStarts.includes(entry)).length;
208
219
  // Platform names are C64 names: drive code and banks get none.
209
220
  const platform = space === DEFAULT_SPACE ? platformNames() : [];
210
- const pointers = findPointers(byteAt, { start: loaded.start, end: loaded.end }, {
211
- platform: (address) => platform.some((entry) => address >= entry.address && address <= entry.end),
212
- sprites: listRegions(reader, undefined, "sprite", undefined, space),
213
- });
214
221
  reader?.close();
215
222
  /** Imports the idioms as their own revision, after the analyzer's, and gives their part of the result. */
216
223
  const idiomsResult = () => {
217
224
  const imported = withWrite((db) => importFindings(db, { ...idioms.findings, space }, { description: "Idioms in the program" }));
218
225
  // The macro candidates read the code regions after both imports.
219
- const macros = macroCandidates(space, byteAt, batchPath);
220
- // Platform pitfalls are C64 facts: drive code and banks get none.
221
- const pitfallDb = openForRead();
222
- const pitfalls = space === DEFAULT_SPACE ? findPitfalls(byteAt, listRegions(pitfallDb, undefined, "code", undefined, space)) : [];
223
- pitfallDb?.close();
226
+ const macros = macroCandidates(space, byteAt, batchPath, romUseOfProgram(coverage, program.state));
227
+ // Platform pitfalls are C64 facts: drive code and banks get none. Pitfalls and pointers read the code regions after both imports.
228
+ const codeDb = openForRead();
229
+ const code = listRegions(codeDb, undefined, "code", undefined, space);
230
+ const pitfalls = space === DEFAULT_SPACE ? findPitfalls(byteAt, code) : [];
231
+ const pointers = findPointers(byteAt, { start: loaded.start, end: loaded.end }, code, {
232
+ platform: (address) => platform.some((entry) => address >= entry.address && address <= entry.end),
233
+ sprites: listRegions(codeDb, undefined, "sprite", undefined, space),
234
+ });
235
+ codeDb?.close();
224
236
  return {
225
237
  idioms: {
226
238
  ...idioms.counts,
@@ -270,7 +282,7 @@ async function analyze(call: Call): Promise<unknown> {
270
282
  written = writeReported(listingPath, listing, { what: "--out-listing" });
271
283
  } catch (error) {
272
284
  const recorded = imported.revision === null ? "The knowledge did not change." : `The knowledge has the findings as revision ${imported.revision}.`;
273
- throw new WireFailure("operation-failed", `The script did not write the listing to ${listingPath}: ${(error as Error).message}. ${recorded}`);
285
+ throw new WireFailure(isRefusal(error) ? error.code : "operation-failed", `The script did not write the listing to ${listingPath}: ${(error as Error).message.replace(/\.$/, "")}. ${recorded}`);
274
286
  }
275
287
  }
276
288
  const found = idiomsResult();
@@ -334,9 +346,9 @@ await runScript({
334
346
  decompile: { kind: "address", value: "<address>", multiple: true, help: "ghidra: a routine to decompile, at most 32" },
335
347
  space: OPTIONS.space,
336
348
  "max-listed": { ...OPTIONS["max-listed"], help: "list at most n labels, functions or candidates (default 50, 0 for none). The counts are always given, and the knowledge has all labels and functions" },
337
- "out-listing": { kind: "path", value: "<file>", help: "dxa: write the disassembly listing to this project file" },
338
- "out-instructions": { kind: "path", value: "<file>", help: "ghidra: write every instruction (address, bytes, mnemonic, mode, operand, target, executed) to this project file as JSON" },
339
- "out-macro-batch": { kind: "path", value: "<file>", help: "write the macro candidates as an apply batch of c64-knowledge (the macros and their uses) to this project file" },
349
+ "out-listing": { kind: "path", output: "file", value: "<file>", help: "dxa: write the disassembly listing to this project file" },
350
+ "out-instructions": { kind: "path", output: "file", value: "<file>", help: "ghidra: write every instruction (address, bytes, mnemonic, mode, operand, target, executed) to this project file as JSON" },
351
+ "out-macro-batch": { kind: "path", output: "file", value: "<file>", help: "write the macro candidates as an apply batch of c64-knowledge (the macros and their uses) to this project file" },
340
352
  },
341
353
  notes: "The program and the coverage come from the knowledge: put them in with knowledge.ts image and coverage of the c64-knowledge skill. The executed code of the coverage is seeded. Routine symbols in the knowledge are entry points too. Ghidra needs at least one entry point. DXA finds the SYS of a BASIC start by itself.",
342
354
  run: analyze,
@@ -26,7 +26,7 @@ node <skill>/scripts/testing.ts test tests/moves-left.json --out .c64-re-tools/l
26
26
  node <skill>/scripts/testing.ts checklist tests/moves-left.json --item 'FIRE starts the game.'
27
27
  ```
28
28
 
29
- `testing.ts test` gives `result`: PASS, FAIL or INCONCLUSIVE. With `--out`, a FAIL writes files into that directory: both screens and the difference image for each screen difference, and `memory.json` with each byte that differs. `files` in the result names them. A PASS writes no files. To keep the screens of a PASS, use a run (below).
29
+ `testing.ts test` gives `result`: PASS, FAIL or INCONCLUSIVE. With `--out`, a FAIL writes files into that directory: both screens and the difference image for each screen difference, and `memory.json`. `memory.json` has all bytes of each memory item that differs, and each byte that differs. `files` in the result names them. A PASS writes no files. To keep the screens of a PASS, use a run (below).
30
30
 
31
31
  `testing.ts checklist` gives a Markdown checklist for a human tester in `text`.
32
32
 
@@ -40,7 +40,7 @@ A run does steps on one program, with no comparison, and keeps the results as fi
40
40
  node <skill>/scripts/testing.ts run .c64-re-tools/local/runs/rooms/run.json --out .c64-re-tools/local/runs/rooms
41
41
  ```
42
42
 
43
- The run file gives where to start (a program, or a snapshot file), the steps, and optional symbols. To go on from the live emulator, save its state with `c64_snapshot` action `save` and `out`, and start the run from that file. A run has the action steps of a scenario: `write`, `registers`, `joystick`, `type`, `frames` and `runUntil`. It also has these steps:
43
+ The run file gives where to start (a program, or a snapshot file), the steps, and optional symbols. A run file has no names unless it gives `"symbols": "knowledge"` or a JSON file of names. To go on from the live emulator, save its state with `c64_snapshot` action `save` and `out`, and start the run from that file. A run has the action steps of a scenario: `write`, `registers`, `joystick`, `type`, `frames` and `runUntil`. It also has these steps:
44
44
 
45
45
  | Step | Effect |
46
46
  |---|---|
@@ -120,7 +120,9 @@ node <skill>/scripts/testing.ts diff .c64-re-tools/local/runs/rooms/results.json
120
120
 
121
121
  - Two snapshot files: the script restores each one in its own emulator. It gives the changed ranges of the RAM and of the colour RAM, and the changed VIC-II, CIA, SID and CPU registers.
122
122
  - Two memory files of a run: the changed ranges. For a file of less than 64 KiB, `--address` gives the address of its first byte.
123
- - Two observations of runs, also of one run: the changed memory items and registers.
123
+ - Two observations of runs, also of one run: the changed memory items and registers. `compared` gives the count of items that both observations have. With `compared: 0`, `reason` tells what each observation has. Then the diff compared nothing, and the observations are not equal.
124
+
125
+ For two snapshot files, `$00` and `$01` are the CPU port of the snapshot: the direction register and the data register. They are not the RAM under the port. `--ntsc` is only for snapshot files, and `--address` is only for memory files. The script refuses these options for the other kinds.
124
126
 
125
127
  `registers` gives each changed register and chip value. `cpu.pc` is a `$` address. Each CPU flag is `cpu.flags.<flag>`, for example `cpu.flags.c`, with `true` or `false`.
126
128
 
@@ -135,6 +137,7 @@ The script refuses a snapshot that does not restore in its emulator. An example
135
137
  - An address is `$` and 1 to 4 hex digits, for example `'$c000'` or `'$fb'`, or a name. A name is a symbol, else an analyzer name that only one analyzer gives. Quote an address in a shell.
136
138
  - A range is `<start>-<end>`, for example `'$c000-$cfff'`. In a JSON file, a range is `start` and `end`.
137
139
  - Bytes in a JSON file are hex in a field `data`, for example `"data": "a900"`.
140
+ - The script examines each `--out` path before it starts work. It refuses a path in `.c64-re-tools/knowledge/` and the index of the knowledge, because only c64-knowledge writes them. It refuses a folder where the command writes a file, and a file where the command writes a folder.
138
141
  - The result is one JSON object on one line. A file that the script writes shows as `{"out", "bytes", "sha256"}`. A list shows as `{"count", "listed"}`, and `rest` tells where to get the rest of a long list.
139
142
  - An error is `{"error": {"code": "...", "message": "..."}}`. The message tells what is wrong and what to do.
140
143
  - The exit status is 0 for done or PASS, 1 for FAIL, 2 for an error and 3 for INCONCLUSIVE.
@@ -154,11 +157,11 @@ The script refuses a snapshot that does not restore in its emulator. An example
154
157
  1. Write the scenario file. Start the steps with a frame count, for example `{ "frames": 150 }`. Each side starts at the reset vector, and the C64 needs these frames to start.
155
158
  2. Use symbol names, for example `player_x`, not fixed addresses. Each side finds the name in its own symbols. Use a `$` address only when both programs use the same address.
156
159
  3. Make each checkpoint a condition, for example a `runUntil` step with an `address` target at a routine. Do not wait for a time.
157
- 4. Save the c64-assembler output of the rebuild to the symbol file, for example `build/game.json` in the project folder. Give `--all-symbols`, so that the file also has the constants that the source does not use. From the folder above the project, write `assembler.ts build --project <folder> ... > <folder>/build/game.json`.
160
+ 4. Write the symbol file of the rebuild with `--out-symbols build/game.json` of c64-assembler `build`. Give `--all-symbols`, so that the file also has the constants that the source does not use.
158
161
  5. Run `testing.ts test`.
159
162
  6. Read the result:
160
163
  - `PASS`: all observations are equal, or the differences are in the allowed tolerance.
161
- - `FAIL`: `differences` lists each difference with the checkpoint, the item, the original value and the rebuild value.
164
+ - `FAIL`: `differences` lists each difference with the checkpoint and the item. A memory item gives `bytes`: the count of the bytes that differ, and the first of these bytes. Each byte has the address, the original value and the rebuild value. `--max-listed` sets how many bytes it lists (default 50). `memory.json` in `--out` has all of them. Another item gives the original value and the rebuild value.
162
165
  - Each value has one form. The pc and each address are `$xxxx`. Memory is lowercase hex. Other register values and chip values are decimal numbers.
163
166
  - `INCONCLUSIVE`: `reason` tells why the script could not compare. This is not a failure of the program.
164
167
  7. For a FAIL, examine the first difference with the c64-emulator skill.
@@ -188,6 +191,7 @@ PASS, FAIL or INCONCLUSIVE, the checkpoints and the smallest differences that ex
188
191
  ## Failure and conflicts
189
192
 
190
193
  - `invalid-input`: the scenario file has an error. The message names the step. Change the step and run the test again.
194
+ - INCONCLUSIVE, `step N (kind): ...`: the step failed on one side. `reason` names the step and why.
191
195
  - `invalid-input` that names the 2.0 form: a step, a memory item or a recording has a form of version 2.0. An example is `"at"` in place of `"address"`. The message gives the form to write. Change the file. Record a recording of version 2.0 again.
192
196
  - `not-found` before the run: a file of the scenario or the run file is missing, for example the rebuild program. Build the rebuild, or change the path.
193
197
  - INCONCLUSIVE, "did not reach the checkpoint": one side did not reach the checkpoint in the frame limit. Find the reason with the c64-emulator skill. Do not report a FAIL.
@@ -47,7 +47,7 @@ Before the first step of each side, the script does a hard reset. The CPU stops
47
47
  | `{ "type": { "mode": "text", "text": "RUN\n" } }` | Put at most 1024 keys into the keyboard queue of the KERNAL. A program that reads the keyboard hardware does not see them. |
48
48
  | `{ "type": { "mode": "petscii", "bytes": [147] } }` | Put PETSCII codes from 1 to 255 into the keyboard queue of the KERNAL. Use it for keys that mode `text` does not have. |
49
49
  | `{ "runUntil": { "target": { "kind": "address", "address": "main_loop" }, "timeoutFrames": 600 } }` | Run until the CPU gets to an address. |
50
- | `{ "runUntil": { "target": { "kind": "memory", "address": "game_state", "operator": "eq", "value": 2 }, "timeoutFrames": 600 } }` | Run until a byte in memory compares true with `value`. `operator` is `eq`, `ne`, `lt`, `lte`, `gt` or `gte`. |
50
+ | `{ "runUntil": { "target": { "kind": "memory", "address": "game_state", "operator": "eq", "value": 2 }, "timeoutFrames": 600 } }` | Run until a write to the byte makes the comparison with `value` true. `operator` is `eq`, `ne`, `lt`, `lte`, `gt` or `gte`. The run does not stop for a byte that is already true at the start. It also does not stop for a register that the hardware changes, for example `$d012`. For a raster line, use a `raster` target. |
51
51
  | `{ "runUntil": { "target": { "kind": "raster", "line": 250 }, "timeoutFrames": 5 } }` | Run until the raster beam gets to a line (0 to 311 for PAL, 0 to 262 for NTSC). Optional: `cycle` from 0 to 64. |
52
52
  | `{ "runUntil": { "target": { "kind": "address", "address": "irq_tick", "condition": { "kind": "raster", "line": 250 } }, "timeoutFrames": 600 } }` | Run until the CPU gets to an address while the condition is true. |
53
53
  | `{ "snapshot": "snapshots/start.vsf" }` | Restore a snapshot file. Usually with `side`: the snapshot of one program is not a state of the other. |
@@ -90,7 +90,7 @@ A run file (for `testing.ts run`) has:
90
90
 
91
91
  - `name`: a short name.
92
92
  - `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" }`.
93
- - `symbols`: optional, `knowledge`, `none` (default), or a JSON file as for a subject.
93
+ - `symbols`: optional, `knowledge`, `none` (default), or a JSON file as for a subject. Without `symbols`, a run has no names: give `"symbols": "knowledge"` to use the names of the knowledge.
94
94
  - `videoStandard`: optional, `pal` (default) or `ntsc`.
95
95
  - `steps`: the steps `write`, `registers`, `joystick`, `type`, `frames` and `runUntil` of a scenario, and the run steps `snapshot`, `save`, `screen`, `coverage`, `trace` and `observe`.
96
96
 
@@ -124,7 +124,9 @@ Example: visit two rooms from the same start.
124
124
 
125
125
  The script checks the scenario before it starts the emulator. A step with an unknown key or a wrong value stops the script with `invalid-input`. The message names the step.
126
126
 
127
- The script also examines each file before it starts the emulator: the program and the symbols of each side, and each snapshot file. A missing file stops the script with `not-found`. A program that is not a PRG, for example a disk image, stops the script with `invalid-input`.
127
+ The script also examines each file before it starts the emulator: the program and the symbols of each side, and each snapshot file. A missing file stops the script with `not-found`. A program that is not a PRG, for example a disk image, stops the script with `invalid-input`. A snapshot file that does not start with the signature of a VICE snapshot file also stops the script with `invalid-input`. The message names the step and the file.
128
+
129
+ A run also examines each name of its steps before it starts the emulator. An unknown name stops the run with `invalid-input`, and the message names the step. In a scenario, a side that does not have a name gives INCONCLUSIVE, and `reason` names the step.
128
130
 
129
131
  A step in the form of version 2.0 stops the script with `invalid-input`. The message names the old field and gives the step to write. For example, the script refuses `{ "runUntil": { "at": "main_loop" }, "timeoutFrames": 300 }` and gives `{"runUntil": {"target": {"kind": "address", "address": "main_loop"}, "timeoutFrames": 300}}`. A memory item with `at` gets the same refusal: write `address`.
130
132
 
@@ -10,6 +10,7 @@ import { isObject } from "#src/host-client/steps.ts";
10
10
  import { WireFailure } from "#src/host-client/tools.ts";
11
11
  import { readProjectFile, writeReported } from "#src/host-client/transfer.ts";
12
12
  import { readMemoryInParts, ViceSessionClient } from "#src/host-client/vice-session.ts";
13
+ import { checkSnapshotFile, hasSignature, VSF_SIGNATURE } from "#src/media.ts";
13
14
  import { listed, UsageError } from "#src/script.ts";
14
15
  import { diffStates, type ChangedRange, type MachineState } from "./diff.ts";
15
16
 
@@ -59,30 +60,40 @@ function cpuValues(registers: Record<string, unknown>): Record<string, number |
59
60
 
60
61
  /** The size of the header of a module in a VICE snapshot file: the name (16 bytes), the version (2 bytes) and the size (4 bytes). */
61
62
  const MODULE_HEADER = 22;
62
- const VSF_MAGIC = "VICE Snapshot File\x1a";
63
63
  const VSF_VERSION = "VICE Version\x1a";
64
64
 
65
65
  /**
66
- * The 64 KiB of C64 RAM that a VICE snapshot file holds: the C64MEM module,
67
- * after the CPU port and the cartridge lines (4 bytes). Undefined when the
68
- * file has no such module.
66
+ * The data of the C64MEM module of a VICE snapshot file: the data and the
67
+ * direction register of the CPU port, the cartridge lines (2 bytes), then
68
+ * the 64 KiB of RAM. Undefined when the file has no such module.
69
69
  */
70
- export function snapshotRam(file: Uint8Array): Uint8Array | undefined {
70
+ function c64Memory(file: Uint8Array): Buffer | undefined {
71
71
  const bytes = Buffer.from(file);
72
- if (bytes.subarray(0, VSF_MAGIC.length).toString("latin1") !== VSF_MAGIC) return undefined;
72
+ if (!hasSignature("vsf", bytes)) return undefined;
73
73
  // The magic, the version (2 bytes) and the machine name (16 bytes); a newer VICE adds its own version (8 bytes).
74
- let at = VSF_MAGIC.length + 2 + 16;
74
+ let at = VSF_SIGNATURE.length + 2 + 16;
75
75
  if (bytes.subarray(at, at + VSF_VERSION.length).toString("latin1") === VSF_VERSION) at += VSF_VERSION.length + 8;
76
76
  while (at + MODULE_HEADER <= bytes.length) {
77
77
  const name = bytes.subarray(at, at + 16).toString("latin1").replace(/\0+$/, "");
78
78
  const size = bytes.readUInt32LE(at + 18);
79
79
  if (size < MODULE_HEADER) return undefined;
80
- if (name === "C64MEM") return at + MODULE_HEADER + 4 + 0x10000 <= bytes.length ? bytes.subarray(at + MODULE_HEADER + 4, at + MODULE_HEADER + 4 + 0x10000) : undefined;
80
+ if (name === "C64MEM") return at + MODULE_HEADER + 4 + 0x10000 <= bytes.length ? bytes.subarray(at + MODULE_HEADER, at + MODULE_HEADER + 4 + 0x10000) : undefined;
81
81
  at += size;
82
82
  }
83
83
  return undefined;
84
84
  }
85
85
 
86
+ /** The 64 KiB of C64 RAM that a VICE snapshot file holds, also the RAM under the CPU port at $00/$01. Undefined when the file has no C64MEM module. */
87
+ export function snapshotRam(file: Uint8Array): Uint8Array | undefined {
88
+ return c64Memory(file)?.subarray(4);
89
+ }
90
+
91
+ /** The CPU port of a VICE snapshot file: the direction register ($00) and the data register ($01). Undefined when the file has no C64MEM module. */
92
+ export function snapshotCpuPort(file: Uint8Array): { direction: number; data: number } | undefined {
93
+ const memory = c64Memory(file);
94
+ return memory === undefined ? undefined : { data: memory[0]!, direction: memory[1]! };
95
+ }
96
+
86
97
  /**
87
98
  * The state of a snapshot: restored in the session, then read with the
88
99
  * readers of the runs. The RAM must be the RAM of the file: else the
@@ -101,6 +112,10 @@ async function snapshotState(client: ViceSessionClient, path: string, ntsc: bool
101
112
  `${path} did not restore: the RAM after the restore is not the RAM of the file. A snapshot restores only in a machine of the video standard that saved it. ${ntsc ? "Leave out --ntsc for a PAL snapshot" : "Give --ntsc for an NTSC snapshot"}, or use the emulator version that saved it.`,
102
113
  );
103
114
  }
115
+ // $00 and $01 are the CPU port of the file, not the RAM under it: a program reads and writes the port there.
116
+ const port = snapshotCpuPort(file)!;
117
+ memory[0] = port.direction;
118
+ memory[1] = port.data;
104
119
  const colorRam = Buffer.from((await client.ioRead(0xd800, 0x400)).data, "hex").map((value) => value & 0x0f);
105
120
  const vic = Buffer.from((await client.ioRead(0xd000, 0x2f)).data, "hex");
106
121
  const values: Record<string, number | boolean> = {};
@@ -118,10 +133,24 @@ const show = (range: ChangedRange) => ({ start: formatC64Address(range.start), e
118
133
  /** A changed value as a person reads it: the pc as "$xxxx". */
119
134
  const showValue = (item: { name: string; a: number | boolean; b: number | boolean }) => (item.name === "cpu.pc" && typeof item.a === "number" && typeof item.b === "number" ? { name: item.name, a: formatC64Address(item.a), b: formatC64Address(item.b) } : item);
120
135
 
136
+ /**
137
+ * How many items two observations have in common, by name: the items that the
138
+ * diff compared. With none, the reason names what each side has, so that a
139
+ * count of 0 does not read as "equal".
140
+ */
141
+ export function comparedItems(what: string, a: readonly string[], b: readonly string[], first: string, second: string): { compared: number; reason?: string } {
142
+ const compared = a.filter((name) => b.includes(name)).length;
143
+ if (compared > 0) return { compared };
144
+ const has = (names: readonly string[]) => (names.length === 0 ? "none" : names.join(", "));
145
+ return { compared, reason: `No ${what} was compared: the observations have none in common. ${first} has ${has(a)}; ${second} has ${has(b)}.` };
146
+ }
147
+
121
148
  /** The differences of two states of one kind: snapshots, memory files or observations of runs. */
122
149
  export async function diffFiles(first: string, second: string, options: { gap: number; max: number; ntsc: boolean; at?: number; out?: string }): Promise<Record<string, unknown>> {
123
150
  const kind = kindOf(first);
124
151
  if (kindOf(second) !== kind) throw new UsageError("give two states of one kind: two snapshots, two memory files or two observations");
152
+ if (options.at !== undefined && kind !== "memory") throw new UsageError(`--address is only for two memory files (.bin). ${kind === "snapshot" ? "A snapshot file holds all 64 KiB" : "An observation gives the address of each item"}: leave out --address`);
153
+ if (options.ntsc && kind !== "snapshot") throw new UsageError(`--ntsc is only for two snapshot files (.vsf), which restore in an emulator. ${kind === "memory" ? "Memory files" : "Observations"} compare as they are: leave out --ntsc`);
125
154
  const { gap, max } = options;
126
155
  let a: MachineState;
127
156
  let b: MachineState;
@@ -137,6 +166,8 @@ export async function diffFiles(first: string, second: string, options: { gap: n
137
166
  a = { memory: { base, bytes: left } };
138
167
  b = { memory: { base, bytes: right } };
139
168
  } else {
169
+ // Both files are snapshot files before the emulator starts.
170
+ for (const path of [first, second]) checkSnapshotFile(path, readProjectFile(path).bytes);
140
171
  const client = await ViceSessionClient.open({ videoStandard: options.ntsc ? "ntsc" : "pal" });
141
172
  try {
142
173
  a = await snapshotState(client, first, options.ntsc);
@@ -159,8 +190,8 @@ export async function diffFiles(first: string, second: string, options: { gap: n
159
190
  b: second,
160
191
  ...(diff.memory === undefined ? {} : { memory: part(diff.memory) }),
161
192
  ...(diff.colorRam === undefined ? {} : { colorRam: part(diff.colorRam) }),
162
- ...(diff.values === undefined ? {} : { registers: listed(diff.values, max, showValue, rest) }),
163
- ...(diff.observed === undefined ? {} : { observed: listed(diff.observed, max, undefined, rest) }),
193
+ ...(diff.values === undefined ? {} : { registers: { ...(kind === "observation" ? comparedItems("register", Object.keys(a.values ?? {}), Object.keys(b.values ?? {}), first, second) : {}), ...listed(diff.values, max, showValue, rest) } }),
194
+ ...(diff.observed === undefined ? {} : { observed: { ...comparedItems("memory item", Object.keys(a.observed ?? {}), Object.keys(b.observed ?? {}), first, second), ...listed(diff.observed, max, undefined, rest) } }),
164
195
  ...(written === undefined ? {} : written),
165
196
  };
166
197
  }