@henols/c64-re-tools 2.1.0 → 2.1.2

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 (73) hide show
  1. package/package.json +1 -1
  2. package/skills/c64-assembler/SKILL.md +7 -3
  3. package/skills/c64-assembler/scripts/assembler.ts +29 -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 +3 -2
  7. package/skills/c64-disk/scripts/disk.ts +26 -12
  8. package/skills/c64-disk/scripts/lib/containers.ts +10 -11
  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 +5 -3
  12. package/skills/c64-docs/scripts/lib/reference.ts +2 -2
  13. package/skills/c64-emulator/SKILL.md +9 -2
  14. package/skills/c64-knowledge/SKILL.md +9 -5
  15. package/skills/c64-knowledge/scripts/knowledge.ts +91 -53
  16. package/skills/c64-knowledge/scripts/lib/listing.ts +8 -30
  17. package/skills/c64-memory-map/SKILL.md +2 -1
  18. package/skills/c64-memory-map/scripts/memory-map.ts +4 -3
  19. package/skills/c64-provenance/SKILL.md +6 -4
  20. package/skills/c64-provenance/scripts/lib/library.ts +18 -6
  21. package/skills/c64-provenance/scripts/lib/separate.ts +45 -6
  22. package/skills/c64-provenance/scripts/provenance.ts +14 -10
  23. package/skills/c64-source/SKILL.md +3 -0
  24. package/skills/c64-source/scripts/lib/source.ts +9 -31
  25. package/skills/c64-source/scripts/source.ts +14 -10
  26. package/skills/c64-static-analysis/SKILL.md +2 -0
  27. package/skills/c64-static-analysis/scripts/lib/idioms.ts +6 -2
  28. package/skills/c64-static-analysis/scripts/static-analysis.ts +18 -8
  29. package/skills/c64-testing/SKILL.md +14 -3
  30. package/skills/c64-testing/references/recording-format.md +2 -0
  31. package/skills/c64-testing/references/scenario-format.md +6 -0
  32. package/skills/c64-testing/scripts/lib/diff-files.ts +76 -14
  33. package/skills/c64-testing/scripts/lib/diff.ts +3 -3
  34. package/skills/c64-testing/scripts/lib/recording.ts +50 -15
  35. package/skills/c64-testing/scripts/lib/replay.ts +12 -1
  36. package/skills/c64-testing/scripts/lib/runs.ts +84 -4
  37. package/skills/c64-testing/scripts/lib/scenario.ts +43 -6
  38. package/skills/c64-testing/scripts/testing.ts +20 -7
  39. package/skills/c64-unpacker/SKILL.md +17 -10
  40. package/skills/c64-unpacker/scripts/lib/evidence.ts +4 -5
  41. package/skills/c64-unpacker/scripts/lib/loads.ts +14 -7
  42. package/skills/c64-unpacker/scripts/lib/machine.ts +30 -3
  43. package/skills/c64-unpacker/scripts/lib/stages.ts +15 -7
  44. package/skills/c64-unpacker/scripts/unpacker.ts +93 -42
  45. package/src/host/vice/adapter.ts +4 -1
  46. package/src/host/vice/session.ts +148 -13
  47. package/src/host-client/image.ts +40 -8
  48. package/src/host-client/steps.ts +145 -9
  49. package/src/host-client/tools.ts +1 -1
  50. package/src/host-client/transfer.ts +33 -18
  51. package/src/knowledge/expression.ts +1 -1
  52. package/src/knowledge/history.ts +74 -4
  53. package/src/knowledge/import.ts +79 -7
  54. package/src/knowledge/names.ts +2 -1
  55. package/src/knowledge/program.ts +32 -18
  56. package/src/knowledge/read.ts +46 -0
  57. package/src/knowledge/schema.ts +4 -2
  58. package/src/knowledge/text.ts +2 -0
  59. package/src/knowledge/write.ts +58 -16
  60. package/src/mcp/inputs.ts +6 -3
  61. package/src/mcp/output.ts +5 -0
  62. package/src/mcp/server.ts +20 -4
  63. package/src/mcp/tools/debug.ts +36 -9
  64. package/src/mcp/tools/execution.ts +18 -5
  65. package/src/mcp/tools/machine.ts +3 -1
  66. package/src/mcp/tools/media.ts +14 -2
  67. package/src/mcp/tools/memory.ts +11 -6
  68. package/src/mcp/tools/video.ts +5 -3
  69. package/src/media.ts +42 -0
  70. package/src/opcodes.ts +51 -18
  71. package/src/project.ts +111 -3
  72. package/src/protocol/vice.ts +8 -2
  73. package/src/script.ts +55 -3
@@ -8,7 +8,7 @@
8
8
  // routines that several programs share in a library folder and find them in the
9
9
  // program (D71).
10
10
 
11
- import { basename, relative } from "node:path";
11
+ import { basename } from "node:path";
12
12
 
13
13
  import { type AddressRange, c64AddressOf, formatC64Address } from "#src/c64.ts";
14
14
  import { Refusal } from "#src/errors.ts";
@@ -41,7 +41,7 @@ import {
41
41
  import { CODE_REGION_TYPES, SEMANTIC_ORIGINS } from "#src/knowledge/schema.ts";
42
42
  import { platformNames } from "#src/platform.ts";
43
43
  import { projectRoot } from "#src/project.ts";
44
- import { knowledgeNames } from "#src/knowledge/names.ts";
44
+ import { knowledgeAt, knowledgeNames } from "#src/knowledge/names.ts";
45
45
  import { listed, OPTIONS, runScript, UsageError, type Call, type CommandSpec } from "#src/script.ts";
46
46
  import { compareReleases, shiftSuggestion, suggestShifts, type Release } from "./lib/compare.ts";
47
47
  import { findRoutine, libraryFolder, readLibrary, routineFrom, saveRoutine, suggest } from "./lib/library.ts";
@@ -107,13 +107,17 @@ async function compare(call: Call): Promise<unknown> {
107
107
  const shifts = new Map<number, number>();
108
108
  for (const text of call.strings("shift")) {
109
109
  const match = /^(\d+):([+-]?\d+)$/.exec(text);
110
- if (match === null || Number(match[1]) < 1 || Number(match[1]) > positionals.length) throw new UsageError(`--shift must be <release number>:<bytes>, not ${text}`);
110
+ if (match === null) throw new UsageError(`--shift must be <release number>:<bytes>, for example 2:-256, not ${text}`);
111
+ if (Number(match[1]) < 1 || Number(match[1]) > positionals.length) {
112
+ throw new UsageError(`--shift ${text}: there is no release ${match[1]}. This compare has ${positionals.length} releases: give a release number from 1 to ${positionals.length}`);
113
+ }
114
+ if (shifts.has(Number(match[1]))) throw new UsageError(`--shift gives release ${match[1]} more than one shift (${shifts.get(Number(match[1]))} and ${match[2]}). Give one --shift for each release`);
111
115
  shifts.set(Number(match[1]), Number(match[2]));
112
116
  }
113
117
  const frames = call.count("frames");
114
118
  const stepsFile = call.string("steps");
115
119
  if (stepsFile !== undefined && frames === undefined) throw new UsageError("--steps works only with --frames");
116
- const steps: ActionStep[] = stepsFile === undefined ? [] : readActionSteps(stepsFile, "--steps");
120
+ const steps: ActionStep[] = stepsFile === undefined ? [] : readActionSteps(stepsFile, "--steps", knowledgeAt());
117
121
  const loaded = positionals.map((path, index) => readRelease(path, shifts.get(index + 1) ?? 0));
118
122
  const releases = loaded.map((item) => item.release);
119
123
  const range = call.range("range");
@@ -364,10 +368,10 @@ function routineCommand(call: Call): unknown {
364
368
  operands: listOperands(db, range, SEMANTIC_ORIGINS),
365
369
  platform: platform(),
366
370
  });
367
- const file = saveRoutine(libraryFolder(library, { create: true, cwd: projectRoot() }), routine, call.flag("replace"));
371
+ const written = saveRoutine(libraryFolder(library, { create: true, cwd: projectRoot() }), routine, call.flag("replace"), projectRoot());
368
372
  return {
369
373
  routine: routine.name,
370
- file: relative(projectRoot(), file).split("\\").join("/"),
374
+ ...written,
371
375
  start: formatC64Address(start),
372
376
  end: formatC64Address(end),
373
377
  length: routine.bytes.length,
@@ -460,15 +464,15 @@ const COMMANDS: CommandSpec[] = [
460
464
  },
461
465
  {
462
466
  name: "separate",
463
- summary: "Evidence from the program in the knowledge: the unpack stage that wrote each range, its access in the coverage, whether call and jump references reach each code region from the entry, and what the program reads or executes outside its segments.",
464
- options: { out: { ...OPTIONS.out, value: "<file>", help: "write all ranges to this file" }, "max-listed": OPTIONS["max-listed"] },
467
+ summary: "Evidence from the program in the knowledge: the unpack stage that wrote each range, its access in the coverage, whether call and jump references, or the run on from the code before it, reach each code region from the entry, and what the program reads or executes outside its segments.",
468
+ options: { out: { ...OPTIONS.out, output: "file", value: "<file>", help: "write all ranges to this file" }, "max-listed": OPTIONS["max-listed"] },
465
469
  run: separateCommand,
466
470
  },
467
471
  {
468
472
  name: "leftovers",
469
473
  summary: "What the program in the knowledge holds besides its working code: texts in PETSCII, screen codes, ASCII or with bit 7 set (Apple II), assembler source, BASIC lines away from the BASIC start, fills, untouched bytes outside code, copies of earlier bytes, and code regions that nothing reaches and no run executed.",
470
474
  options: {
471
- out: { ...OPTIONS.out, value: "<file>", help: "write all findings to this file" },
475
+ out: { ...OPTIONS.out, output: "file", value: "<file>", help: "write all findings to this file" },
472
476
  "min-text": { kind: "count", value: "<n>", min: 4, max: 999, default: 8, help: "the shortest text to find (default 8)" },
473
477
  "max-listed": OPTIONS["max-listed"],
474
478
  },
@@ -491,7 +495,7 @@ const COMMANDS: CommandSpec[] = [
491
495
  summary: "Finds each routine of the library in the program of the knowledge: the opcodes and other fixed bytes equal, and the addresses into the routine moved by one shift.",
492
496
  options: {
493
497
  library: { ...LIBRARY, help: "the library folder, relative to the project folder, for example ../library" },
494
- out: { ...OPTIONS.out, value: "<file>", help: "write the names of the routines at the found addresses as a batch for knowledge.ts apply" },
498
+ out: { ...OPTIONS.out, output: "file", value: "<file>", help: "write the names of the routines at the found addresses as a batch for knowledge.ts apply" },
495
499
  "max-listed": OPTIONS["max-listed"],
496
500
  },
497
501
  run: matchCommand,
@@ -47,6 +47,7 @@ When the knowledge has coverage, code that no run executed gets the line `; not
47
47
  - 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.
48
48
  - A range is `<start>-<end>`, for example `'$c000-$cfff'`. In a JSON file, a range is `start` and `end`.
49
49
  - Bytes in a JSON file are hex in a field `data`, for example `"data": "a900"`.
50
+ - 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.
50
51
  - 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.
51
52
  - An error is `{"error": {"code": "...", "message": "..."}}`. The message tells what is wrong and what to do.
52
53
  - The exit status is 0 for done or PASS, 1 for FAIL, 2 for an error and 3 for INCONCLUSIVE.
@@ -150,6 +151,7 @@ Do not store:
150
151
  - `operandsAsNumbers`: the operand expressions that the source has as a number with a side comment, each with its `address`, `expression` and `reason`. An expression becomes a number when no instruction with an operand value is at the address, or when a name has no address. An expression that ACME refuses or does not assemble to the program also becomes a number. Then the script assembles the source again, at most six times.
151
152
  - `unusedModules`: modules in the knowledge whose start is in no segment of the program. Often this tells that the program does not hold code that the knowledge describes.
152
153
  - `entryOutside`: the entry is in no segment of the program in the knowledge, with a `reason`. The capture does not hold the program.
154
+ - `dataOnly`: the knowledge has no entry, no routine, no code region and no executed address in the space. Thus the source has only data, and Ghidra does not run. A table or a key page in a cartridge bank is an example.
153
155
  - `notExecuted`: when the knowledge has coverage, the count of instructions that no run executed, the count of their ranges, and the first ranges. `rest` names the file `not_executed.json` in `--out` when the list is not complete. This file has all of the ranges. Each range has its source `file` and the `routine` before it. `files` counts the ranges in each file.
154
156
  - `macroUses`: the count of macro uses that the source has.
155
157
  - `macrosAsInstructions`: the macro uses that the source has as instructions, each with its `start`, `end`, `macro` and `reason`.
@@ -171,6 +173,7 @@ Do not store:
171
173
  - `patchesNotWritten`: read each `reason`. Move the start or the end of the patch to an instruction, or give the switch another name.
172
174
  - `macrosAsInstructions`: read each `reason`. Correct the macro or the args, or remove the use with `unuse`. The source stays identical, so this is not a failure.
173
175
  - `entryOutside`: the run fails, but the script still writes and checks the source. With `--loader`, `loader.verified` is `false` with the same `reason`. Capture the program again with c64-unpacker `loads` or `capture`. Make sure that a segment holds the entry. Then put it into the knowledge again with `image`.
176
+ - `dataOnly`: this is not a failure. If the space has code, give `--entry` or classify the code with the c64-knowledge skill. Then generate again.
174
177
  - `entriesInData`: an analyzer region of data hides the code at the entry. If the entry is code, record a code region at the entry with the c64-knowledge skill, for example `classify '$0500-$05ff' code`. A region of the user or the LLM replaces the analyzer region. Then generate again.
175
178
  - `loader.verified: false` with a `reason`: the script did not make the loader. The script still writes and checks the source. When no copy order or no free pages exist, or the loader needs too many copies, the reason gives the ranges. Capture the program again with fewer segments, or write the loader by hand.
176
179
  - `loader.verified: false` with `differences`: the loader ran, and `differences` gives the bytes and registers that are not equal.
@@ -13,7 +13,7 @@ import { WireFailure } from "#src/host-client/tools.ts";
13
13
  import { identifiers, operandText, replaceIdentifiers, type OperandExpression } from "#src/knowledge/expression.ts";
14
14
  import { type CommentPlacement, isAnalyzer, isGeneratedFile, type Origin, type RegionType } from "#src/knowledge/schema.ts";
15
15
  import type { Area } from "#src/platform.ts";
16
- import { hasZeroPageForm, MNEMONICS, MODE_LENGTH, OPCODES, takesAddress, USE_KINDS, useKind, type Mode, type UseKind } from "#src/opcodes.ts";
16
+ import { hasZeroPageForm, instructionWith, labelProblem, MODE_LENGTH, OPCODES, takesAddress, USE_KINDS, useKind, type UseKind } from "#src/opcodes.ts";
17
17
 
18
18
  export interface SourceInput {
19
19
  /** In address order, without overlap. */
@@ -319,7 +319,7 @@ export function layout(input: SourceInput): Layout {
319
319
  const outside = new Set<number>();
320
320
  // A platform name that knowledge gives to another address, or that reads as an instruction, is not used.
321
321
  const knowledgeNames = new Set(input.symbols.map((symbol) => symbol.name));
322
- const platform = new Map((input.platform ?? []).filter(({ name }) => !knowledgeNames.has(name) && !MNEMONICS.has(name.toLowerCase())).map(({ address, name }) => [address, name]));
322
+ const platform = new Map((input.platform ?? []).filter(({ name }) => !knowledgeNames.has(name) && labelProblem(name) === undefined).map(({ address, name }) => [address, name]));
323
323
  // An analyzer name (DXA's ld020) gives way to the platform name (EXTCOL); a user or LLM name wins.
324
324
  const symbols = new Map(input.symbols.filter((symbol) => !(isAnalyzer(symbol.origin) && platform.has(symbol.address))).map((symbol) => [symbol.address, symbol.name]));
325
325
  /** Addresses outside the image without a name of their own, inside a named platform entry of more bytes: the start of that entry. */
@@ -384,7 +384,7 @@ export function layout(input: SourceInput): Layout {
384
384
  }
385
385
  const missing = [...new Set(use.args.flatMap((arg) => identifiers(arg)))].filter((name) => !byName.has(name));
386
386
  if (missing.length > 0) {
387
- fallBack(`No symbol and no platform name is ${missing.join(", ")}.`);
387
+ fallBack(`${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} not a symbol of the knowledge or a platform name. Name the address with knowledge.ts rename, or give other args with knowledge.ts use.`);
388
388
  continue;
389
389
  }
390
390
  candidates.push({ use, items: run as InstructionItem[] });
@@ -403,7 +403,7 @@ export function layout(input: SourceInput): Layout {
403
403
  }
404
404
  const missing = identifiers(expression).filter((name) => !byName.has(name));
405
405
  if (missing.length > 0) {
406
- operandsAsNumbers.push({ address: operand.address, expression, reason: `No symbol and no platform name is ${missing.join(", ")}.` });
406
+ operandsAsNumbers.push({ address: operand.address, expression, reason: `${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} not a symbol of the knowledge or a platform name. Name the address with knowledge.ts rename, or write the operand again with knowledge.ts operand.` });
407
407
  continue;
408
408
  }
409
409
  // An immediate operand builds the address of a name; any other operand uses the address as the instruction does.
@@ -450,7 +450,8 @@ export function layout(input: SourceInput): Layout {
450
450
  for (const address of [...wanted, ...outside].sort((a, b) => a - b)) {
451
451
  const known = symbols.get(address);
452
452
  if (known !== undefined) {
453
- if (MNEMONICS.has(known.toLowerCase())) throw new WireFailure("invalid-input", `The symbol ${known} at ${formatC64Address(address)} has the name of an instruction, which ACME cannot read as a label. Rename the symbol with c64-knowledge.`);
453
+ const problem = labelProblem(known);
454
+ if (problem !== undefined) throw new WireFailure("invalid-input", `The symbol ${known} at ${formatC64Address(address)}: ${problem} Rename the symbol with c64-knowledge.`);
454
455
  names.set(address, known);
455
456
  if (inImage(address)) namedLabels++;
456
457
  continue;
@@ -574,27 +575,6 @@ export function formOf(layout: Pick<Layout, "inImage" | "byteAt">, item: Instruc
574
575
  return { key: `${item.opcode.toString(16).padStart(2, "0")}${suffix}`, opcode: item.opcode, suffix };
575
576
  }
576
577
 
577
- /** The instruction text with an operand expression; the expression is a name or a $ number. */
578
- export function instructionText(opcode: number, suffix: string, expression: string): string {
579
- const { mnemonic, mode } = OPCODES[opcode]!;
580
- const operand: Record<Mode, string> = {
581
- imp: "",
582
- imm: `#${expression}`,
583
- zp: expression,
584
- zpx: `${expression},x`,
585
- zpy: `${expression},y`,
586
- abs: expression,
587
- abx: `${expression},x`,
588
- aby: `${expression},y`,
589
- ind: `(${expression})`,
590
- izx: `(${expression},x)`,
591
- izy: `(${expression}),y`,
592
- rel: expression,
593
- };
594
- const text = operand[mode];
595
- return text === "" ? `${mnemonic}${suffix}` : `${mnemonic}${suffix} ${text}`;
596
- }
597
-
598
578
  /** The plain number of an operand: what the probe assembles, and what a comment shows. */
599
579
  export function literalOperand(opcode: number, value: number): string {
600
580
  const { mode } = OPCODES[opcode]!;
@@ -1046,7 +1026,7 @@ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: str
1046
1026
  const form = formOf(layout, item);
1047
1027
  const { mode } = OPCODES[item.opcode]!;
1048
1028
  const value = mode === "imp" ? 0 : operandValue(item.address, item.opcode, layout.byteAt)!;
1049
- const literal = instructionText(item.opcode, "", literalOperand(item.opcode, value));
1029
+ const literal = instructionWith(item.opcode, literalOperand(item.opcode, value));
1050
1030
  let written = layout.operands.get(item.address);
1051
1031
  // An expression that is not written stays visible as a side comment.
1052
1032
  const asNumber = (expression: string, reason: string) => {
@@ -1062,13 +1042,11 @@ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: str
1062
1042
  out.push(withSide(`${INDENT}!byte ${bytes.join(",")}`, [literal, ...sides].join(" ")));
1063
1043
  result.asBytes.push({ address: item.address, text: literal });
1064
1044
  } else if (written !== undefined) {
1065
- // ACME reads an operand in brackets as indirect, so a non-immediate expression does not start with one.
1066
- const expression = mode !== "imm" && written.startsWith("(") ? `0+${written}` : written;
1067
- out.push(withSide(`${INDENT}${instructionText(item.opcode, form.suffix, expression)}`, sides.length === 0 ? undefined : sides.join(" ")));
1045
+ out.push(withSide(`${INDENT}${instructionWith(item.opcode, written, form.suffix)}`, sides.length === 0 ? undefined : sides.join(" ")));
1068
1046
  result.expressions.push({ address: item.address, length: item.length, file: piece.file, line: out.length });
1069
1047
  } else {
1070
1048
  const expression = takesAddress(mode) ? (layout.names.get(value) ?? literalOperand(item.opcode, value)) : literalOperand(item.opcode, value);
1071
- out.push(withSide(`${INDENT}${instructionText(item.opcode, form.suffix, expression)}`, sides.length === 0 ? undefined : sides.join(" ")));
1049
+ out.push(withSide(`${INDENT}${instructionWith(item.opcode, expression, form.suffix)}`, sides.length === 0 ? undefined : sides.join(" ")));
1072
1050
  }
1073
1051
  mapLine(item.address, item.address + item.length - 1);
1074
1052
  result.instructions++;
@@ -31,8 +31,8 @@ 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;
@@ -40,6 +40,9 @@ const MAX_DIAGNOSTICS = 20;
40
40
  const ROUNDS = 6;
41
41
  const PROBE_BASE = 0x1000;
42
42
  const PROBE_SLOT = 4;
43
+ /** Why a source has no instructions when the knowledge has no code. */
44
+ const DATA_ONLY = "The knowledge has no entry, routine, code region or executed address in this space. Thus the source has only data. For instructions, give --entry, or classify the code with knowledge.ts classify, then generate again.";
45
+
43
46
  /** The copies of a full 64 KiB take about 140 frames. */
44
47
  const LOADER_FRAMES = 1500;
45
48
 
@@ -99,9 +102,10 @@ function withAnalyzerNames(symbols: SymbolRow[], second: SymbolRow[]): SymbolRow
99
102
  * Ghidra's instruction starts and lengths inside the image, seeded from the
100
103
  * entry, from knowledge of any origin, and from the coverage: a run's flow
101
104
  * starts, also after a JSR, so code that an analyzer does not reach by itself
102
- * is in the source.
105
+ * is in the source. A program without a seed has no known code: it is data
106
+ * only, and Ghidra does not run.
103
107
  */
104
- async function decode(image: ProgramImage, knowledge: ReturnType<typeof readKnowledge>, entries: number[], coverage: Uint8Array | undefined): Promise<Map<number, number>> {
108
+ async function decode(image: ProgramImage, knowledge: ReturnType<typeof readKnowledge>, entries: number[], coverage: Uint8Array | undefined): Promise<{ instructions: Map<number, number>; dataOnly: boolean }> {
105
109
  const inImage = (address: number) => image.segments.some((segment) => address >= segment.start && address <= segment.end);
106
110
  const seeds = new Set<number>([...(image.entry === undefined ? [] : [image.entry]), ...entries]);
107
111
  for (const address of knowledge.routines) seeds.add(address);
@@ -114,8 +118,7 @@ async function decode(image: ProgramImage, knowledge: ReturnType<typeof readKnow
114
118
  const entryPoints = [...seeds].filter(inImage).sort((a, b) => a - b);
115
119
  if (entryPoints.length === 0) {
116
120
  // An entry in no segment is a capture fault: the image is data only, and the result says entryOutside.
117
- if (image.entry !== undefined && !inImage(image.entry)) return new Map();
118
- throw new UsageError("Ghidra needs an entry point: give --entry, or record a routine or a code region in knowledge with the c64-knowledge skill");
121
+ return { instructions: new Map(), dataOnly: image.entry === undefined || inImage(image.entry) };
119
122
  }
120
123
  const dataRanges = knowledge.regions
121
124
  .filter((region) => !CODE_REGION_TYPES.includes(region.type) && region.type !== "dual-use")
@@ -138,7 +141,7 @@ async function decode(image: ProgramImage, knowledge: ReturnType<typeof readKnow
138
141
  const inside = Array.from({ length: bytes.length }, (_, offset) => instruction.address + offset).every((address, offset) => inImage(address) && image.byteAt(address) === bytes[offset]);
139
142
  if (inside) instructions.set(instruction.address, bytes.length);
140
143
  }
141
- return instructions;
144
+ return { instructions, dataOnly: false };
142
145
  }
143
146
 
144
147
  /** Assembles files given as text with ACME; the program or the diagnostics. */
@@ -179,7 +182,7 @@ async function probe(laid: Layout): Promise<Set<string>> {
179
182
  ? formatC64Address((slot + 2 + (item.bytes[1]! < 0x80 ? item.bytes[1]! : item.bytes[1]! - 0x100)) & 0xffff)
180
183
  : literalOperand(item.form.opcode, item.bytes.length === 3 ? item.bytes[1]! | (item.bytes[2]! << 8) : item.bytes[1]!);
181
184
  lines.push(`* = ${formatC64Address(slot)}`);
182
- lines.push(` ${instructionText(item.form.opcode, item.form.suffix, operand)}`);
185
+ lines.push(` ${instructionWith(item.form.opcode, operand, item.form.suffix)}`);
183
186
  lineOf.set(lines.length, item.form.key);
184
187
  });
185
188
  const built = await assembleFiles([{ name: "probe.asm", text: `${lines.join("\n")}\n` }], "probe.asm");
@@ -547,7 +550,7 @@ async function generate(call: Call): Promise<{ output: Record<string, unknown>;
547
550
  // Read the entry state before the long steps, so a bad state stops the run at once.
548
551
  const state = loaderWanted ? loaderState(image.state, "the entry state of the knowledge") : undefined;
549
552
  const romUse = romsInUse(image, coverage);
550
- const instructions = await decode(image, knowledge, entries, coverage);
553
+ const { instructions, dataOnly } = await decode(image, knowledge, entries, coverage);
551
554
  const laid = layout({
552
555
  segments: image.segments,
553
556
  byteAt: image.byteAt,
@@ -634,6 +637,7 @@ async function generate(call: Call): Promise<{ output: Record<string, unknown>;
634
637
  // The ROM names that the source leaves out, and the evidence.
635
638
  ...(space !== DEFAULT_SPACE || (romUse.basic && romUse.kernal) ? {} : { romNamesLeftOut: { roms: [...(romUse.basic ? [] : ["BASIC"]), ...(romUse.kernal ? [] : ["KERNAL"])], ...(romUse.basic || romUse.kernal ? {} : { systemArea: true }), from: romUse.from } }),
636
639
  ...(missingEntry === undefined ? {} : { entryOutside: { entry: formatC64Address(image.entry!), reason: missingEntry } }),
640
+ ...(dataOnly ? { dataOnly: DATA_ONLY } : {}),
637
641
  labels: { named: laid.namedLabels, generated: laid.generatedLabels },
638
642
  // An --entry in a data region decodes nothing: the result says so, and a code region in the knowledge changes it.
639
643
  ...(inData.length === 0
@@ -730,7 +734,7 @@ await runScript({
730
734
  name: "generate",
731
735
  summary: "Writes the source files, source-map.json and not_executed.json, and checks them. result is PASS when every check passes, else FAIL.",
732
736
  options: {
733
- out: { ...OPTIONS.out, value: "<dir>", help: "the folder for the source files, relative to the project folder", required: true },
737
+ out: { ...OPTIONS.out, output: "folder", value: "<dir>", help: "the folder for the source files, relative to the project folder", required: true },
734
738
  entry: { ...OPTIONS.entry, multiple: true, help: "an entry point for Ghidra" },
735
739
  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" },
736
740
  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" },
@@ -61,6 +61,7 @@ The analyzers take at most 1024 entry points. Knowledge routines and `--entry` c
61
61
  - 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
62
  - A range is `<start>-<end>`, for example `'$c000-$cfff'`. In a JSON file, a range is `start` and `end`.
63
63
  - Bytes in a JSON file are hex in a field `data`, for example `"data": "a900"`.
64
+ - 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
65
  - 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
66
  - An error is `{"error": {"code": "...", "message": "..."}}`. The message tells what is wrong and what to do.
66
67
  - The exit status is 0 for done or PASS, 1 for FAIL, 2 for an error and 3 for INCONCLUSIVE.
@@ -87,6 +88,7 @@ The analyzers take at most 1024 entry points. Knowledge routines and `--entry` c
87
88
  - 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
89
  - 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
90
  - `conflicts` lists the places where the analyzer and the current knowledge disagree.
91
+ - 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
92
  - `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
93
  - `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.
92
94
  - `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.
@@ -31,7 +31,7 @@ export interface Idioms {
31
31
  candidates: { dop: number[]; selfModified: number[] };
32
32
  }
33
33
 
34
- /** True when an instruction can start at the address: inside the program, and no JAM. */
34
+ /** True when an instruction can start at the address: inside the range and the program (`byteAt` gives a byte), and no JAM. */
35
35
  function plausible(byteAt: ImageByte, address: number, range: { start: number; end: number }): boolean {
36
36
  const opcode = byteAt(address);
37
37
  return address >= range.start && address <= range.end && opcode !== undefined && !JAMS.has(opcode);
@@ -43,7 +43,11 @@ const word = (byteAt: ImageByte, address: number) => {
43
43
  return low === undefined || high === undefined ? undefined : low | (high << 8);
44
44
  };
45
45
 
46
- /** `covered` is the coverage of the knowledge: the joined map, and the addresses that one run wrote and executed. */
46
+ /**
47
+ * `byteAt` gives the bytes of the program and undefined outside it, so a
48
+ * table and its targets stay inside the program. `covered` is the coverage of
49
+ * the knowledge: the joined map, and the addresses that one run wrote and executed.
50
+ */
47
51
  export function findIdioms(byteAt: ImageByte, range: { start: number; end: number }, covered?: { map: Uint8Array; selfModified: Uint8Array }): Idioms {
48
52
  const coverage = covered?.map;
49
53
  const regions: NormalizedFindings["regions"] = [];
@@ -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";
@@ -121,6 +121,15 @@ function withRemedy(reason: string): string {
121
121
 
122
122
  /** One conflict: what the knowledge has, and what the analyzer found, under the analyzer's name. */
123
123
  function showConflict(conflict: ImportConflict, analyzer: Analyzer): Record<string, unknown> {
124
+ if (conflict.category === "code-evidence") {
125
+ return {
126
+ range: { start: formatC64Address(conflict.start), end: formatC64Address(conflict.end) },
127
+ [analyzer]: conflict.finding,
128
+ ...(conflict.executed === undefined ? {} : { executed: formatC64Address(conflict.executed) }),
129
+ ...(conflict.symbol === undefined ? {} : { knowledge: { ...conflict.symbol, address: formatC64Address(conflict.symbol.address) } }),
130
+ 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",
131
+ };
132
+ }
124
133
  if (conflict.category === "symbol") {
125
134
  return {
126
135
  at: formatC64Address(conflict.address),
@@ -184,7 +193,8 @@ async function analyze(call: Call): Promise<unknown> {
184
193
  // With coverage, a code finding over an executed instruction start replaces the data of the other analyzer.
185
194
  const executed = coverage === undefined ? {} : { executed: (address: number) => (coverage[address]! & COVERAGE.ramExecute) !== 0 };
186
195
  const loaded = imageKind === "flat64k" ? { start: 0x0000, end: 0xffff } : prgRange(bytes);
187
- const idioms = findIdioms(byteAt, { start: loaded.start, end: loaded.end }, covered);
196
+ // The idioms read only the program: a table that runs past a segment gets no targets from the 0 bytes of the 64 KiB.
197
+ const idioms = findIdioms(inProgram, { start: loaded.start, end: loaded.end }, covered);
188
198
 
189
199
  const revision = currentRevision(reader);
190
200
  const seeds = seedsFromKnowledge(reader, entries, space);
@@ -269,7 +279,7 @@ async function analyze(call: Call): Promise<unknown> {
269
279
  written = writeReported(listingPath, listing, { what: "--out-listing" });
270
280
  } catch (error) {
271
281
  const recorded = imported.revision === null ? "The knowledge did not change." : `The knowledge has the findings as revision ${imported.revision}.`;
272
- throw new WireFailure("operation-failed", `The script did not write the listing to ${listingPath}: ${(error as Error).message}. ${recorded}`);
282
+ throw new WireFailure(isRefusal(error) ? error.code : "operation-failed", `The script did not write the listing to ${listingPath}: ${(error as Error).message.replace(/\.$/, "")}. ${recorded}`);
273
283
  }
274
284
  }
275
285
  const found = idiomsResult();
@@ -281,7 +291,7 @@ async function analyze(call: Call): Promise<unknown> {
281
291
  coverage: result.coverage.map((range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) })),
282
292
  ...imported,
283
293
  regions: result.regions.map((region) => ({ start: formatC64Address(region.start), end: formatC64Address(region.end), classification: region.classification })),
284
- labels: listed(result.labels, max, (label) => ({ address: formatC64Address(label.address), name: label.name }), "c64-knowledge: knowledge.ts symbols"),
294
+ labels: listed(result.labels, max, (label) => ({ address: formatC64Address(label.address), name: label.name }), "c64-knowledge: knowledge.ts symbols --analyzer --origin dxa"),
285
295
  ...(written === undefined ? {} : { listing: written }),
286
296
  };
287
297
  }
@@ -311,7 +321,7 @@ async function analyze(call: Call): Promise<unknown> {
311
321
  ...(coverage === undefined ? {} : { coverageGaps: listed(gaps, max, formatC64Address, "a larger --max-listed") }),
312
322
  coverage: result.coverage.map((range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) })),
313
323
  ...imported,
314
- functions: listed(result.functions, max, (fn) => ({ entry: formatC64Address(fn.entry), name: fn.name }), "c64-knowledge: knowledge.ts symbols --kind routine"),
324
+ functions: listed(result.functions, max, (fn) => ({ entry: formatC64Address(fn.entry), name: fn.name }), "c64-knowledge: knowledge.ts symbols --analyzer --origin ghidra"),
315
325
  decompilations: result.decompilations.map((item) => ({ entry: formatC64Address(item.entry), text: item.text, ...(item.truncated ? { truncated: true } : {}) })),
316
326
  ...((result.notDecompiled ?? []).length === 0
317
327
  ? {}
@@ -333,9 +343,9 @@ await runScript({
333
343
  decompile: { kind: "address", value: "<address>", multiple: true, help: "ghidra: a routine to decompile, at most 32" },
334
344
  space: OPTIONS.space,
335
345
  "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" },
336
- "out-listing": { kind: "path", value: "<file>", help: "dxa: write the disassembly listing to this project file" },
337
- "out-instructions": { kind: "path", value: "<file>", help: "ghidra: write every instruction (address, bytes, mnemonic, mode, operand, target, executed) to this project file as JSON" },
338
- "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" },
346
+ "out-listing": { kind: "path", output: "file", value: "<file>", help: "dxa: write the disassembly listing to this project file" },
347
+ "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" },
348
+ "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" },
339
349
  },
340
350
  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.",
341
351
  run: analyze,
@@ -30,6 +30,8 @@ node <skill>/scripts/testing.ts checklist tests/moves-left.json --item 'FIRE sta
30
30
 
31
31
  `testing.ts checklist` gives a Markdown checklist for a human tester in `text`.
32
32
 
33
+ Before the emulator starts, `testing.ts test` examines each file that the scenario names: the program and the symbols of each side, and each snapshot file. A missing file gives `not-found`. A program must be a PRG. The script refuses a disk, tape or cartridge image.
34
+
33
35
  ### Runs
34
36
 
35
37
  A run does steps on one program, with no comparison, and keeps the results as files. Use it for many steps. One run replaces many emulator calls, for example a visit to each room of a game.
@@ -50,6 +52,8 @@ The run file gives where to start (a program, or a snapshot file), the steps, an
50
52
  | `{ "trace": "d011", "points": [{ "kind": "write", "start": "$d011" }], "frames": 100 }` | Record each hit of the points for 100 frames in `d011.json`: the instruction, its registers and the raster line and cycle. A point has the `kind` `exec`, `read` or `write`, a `start` and an optional `end`. `maxHits` limits the hits. |
51
53
  | `{ "observe": "all3", "memory": "all" }` | Write all 64 KiB of RAM to `all3.bin`, also the RAM under the ROMs and the I/O area. |
52
54
 
55
+ Each step that writes a file must have its own file name. The script refuses a run where two steps write the same file, for example `{ "coverage": "x" }` and `{ "observe": "x", "memory": "all" }`, which both write `x.bin`. The script also examines the start program and each snapshot file before the emulator starts.
56
+
53
57
  A memory item can have `"view": "ram"`. Then it reads the RAM under the ROMs and the I/O area, for example code at `$e000` under the KERNAL. The default view `cpu` reads as the CPU sees memory. An item larger than 4096 bytes goes into its own `.bin` file, and `results.json` gives the file name.
54
58
 
55
59
  See [references/scenario-format.md](references/scenario-format.md), section Runs. `result` is PASS when all steps ran, and FAIL when a step did not run. `reason` tells why. A long run can need more time than one shell call. Then run it in the background.
@@ -116,9 +120,13 @@ node <skill>/scripts/testing.ts diff .c64-re-tools/local/runs/rooms/results.json
116
120
 
117
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.
118
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.
119
- - 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
+ `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
+
127
+ The script refuses a snapshot that does not restore in its emulator. An example is a PAL snapshot with `--ntsc`. Give `--ntsc` only for snapshots of an NTSC machine.
120
128
 
121
- `--gap N` joins ranges across N equal bytes. `--out` writes all ranges to a project file.
129
+ `--gap N` joins ranges across N equal bytes. `--out` writes all ranges and all changed registers to a project file. The file uses the names of the result.
122
130
 
123
131
  ### Calling the script
124
132
 
@@ -127,6 +135,7 @@ node <skill>/scripts/testing.ts diff .c64-re-tools/local/runs/rooms/results.json
127
135
  - 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.
128
136
  - A range is `<start>-<end>`, for example `'$c000-$cfff'`. In a JSON file, a range is `start` and `end`.
129
137
  - Bytes in a JSON file are hex in a field `data`, for example `"data": "a900"`.
138
+ - 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.
130
139
  - 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.
131
140
  - An error is `{"error": {"code": "...", "message": "..."}}`. The message tells what is wrong and what to do.
132
141
  - The exit status is 0 for done or PASS, 1 for FAIL, 2 for an error and 3 for INCONCLUSIVE.
@@ -146,7 +155,7 @@ node <skill>/scripts/testing.ts diff .c64-re-tools/local/runs/rooms/results.json
146
155
  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.
147
156
  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.
148
157
  3. Make each checkpoint a condition, for example a `runUntil` step with an `address` target at a routine. Do not wait for a time.
149
- 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`.
158
+ 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.
150
159
  5. Run `testing.ts test`.
151
160
  6. Read the result:
152
161
  - `PASS`: all observations are equal, or the differences are in the allowed tolerance.
@@ -180,6 +189,8 @@ PASS, FAIL or INCONCLUSIVE, the checkpoints and the smallest differences that ex
180
189
  ## Failure and conflicts
181
190
 
182
191
  - `invalid-input`: the scenario file has an error. The message names the step. Change the step and run the test again.
192
+ - `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.
193
+ - `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.
183
194
  - 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.
184
195
  - INCONCLUSIVE, "has no symbol": one side has no symbol with that name. Add the name to the knowledge or to the rebuild source.
185
196
  - INCONCLUSIVE, "no emulator" or "could not run": the host runtime or the emulator failed. Tell the user the message.
@@ -101,6 +101,8 @@ The page sends input only between Start and End. The input of the page goes into
101
101
 
102
102
  A site is the address of the instruction that reads. The values are hex. `<from>` and `<count>` are decimal.
103
103
 
104
+ `testing.ts replay` checks `recording.json` before the replay core starts. A recording of version 2.0 can have events or a comparison in the old form, for example `"at"` in place of `"address"`. The script refuses it with `invalid-input`. The message names the event, the old field and the form to write. Record the session again.
105
+
104
106
  ## Replay
105
107
 
106
108
  `testing.ts replay` replays a recording folder:
@@ -123,3 +123,9 @@ Example: visit two rooms from the same start.
123
123
  ## Checks
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
+
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`.
128
+
129
+ 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
+
131
+ In a run, two steps must not write the same file name. The script refuses such a run before it starts. Names that differ only in upper and lower case are the same name.