@henols/c64-re-tools 2.0.1 → 2.1.0-rc.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.
- package/README.md +8 -4
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +24 -9
- package/skills/c64-assembler/scripts/assembler.ts +95 -0
- package/skills/c64-basic/SKILL.md +17 -9
- package/skills/c64-basic/scripts/basic.ts +26 -36
- package/skills/c64-disk/SKILL.md +17 -7
- package/skills/c64-disk/scripts/disk.ts +130 -104
- package/skills/c64-disk/scripts/lib/containers.ts +10 -12
- package/skills/c64-docs/SKILL.md +47 -28
- package/skills/c64-docs/references/style.md +6 -6
- package/skills/c64-docs/scripts/docs.ts +323 -0
- package/skills/c64-docs/scripts/lib/source-map.ts +1 -1
- package/skills/c64-emulator/SKILL.md +11 -7
- package/skills/c64-knowledge/SKILL.md +34 -21
- package/skills/c64-knowledge/scripts/knowledge.ts +597 -718
- package/skills/c64-knowledge/scripts/lib/listing.ts +5 -7
- package/skills/c64-memory-map/SKILL.md +18 -10
- package/skills/c64-memory-map/scripts/memory-map.ts +81 -0
- package/skills/c64-provenance/SKILL.md +82 -26
- package/skills/c64-provenance/scripts/lib/compare.ts +8 -12
- package/skills/c64-provenance/scripts/lib/leftovers.ts +442 -0
- package/skills/c64-provenance/scripts/lib/library.ts +15 -25
- package/skills/c64-provenance/scripts/lib/runtime.ts +12 -37
- package/skills/c64-provenance/scripts/lib/separate.ts +9 -10
- package/skills/c64-provenance/scripts/provenance.ts +222 -252
- package/skills/c64-reverse-engineering/SKILL.md +18 -15
- package/skills/c64-source/SKILL.md +28 -16
- package/skills/c64-source/scripts/lib/loader.ts +52 -77
- package/skills/c64-source/scripts/lib/source.ts +33 -42
- package/skills/c64-source/scripts/{generate.ts → source.ts} +68 -109
- package/skills/c64-static-analysis/SKILL.md +24 -13
- package/skills/c64-static-analysis/scripts/lib/coverage.ts +3 -4
- package/skills/c64-static-analysis/scripts/lib/findings.ts +2 -2
- package/skills/c64-static-analysis/scripts/lib/instructions.ts +1 -1
- package/skills/c64-static-analysis/scripts/lib/macros.ts +6 -8
- package/skills/c64-static-analysis/scripts/{analyze.ts → static-analysis.ts} +58 -118
- package/skills/c64-testing/SKILL.md +94 -19
- package/skills/c64-testing/references/recording-format.md +125 -0
- package/skills/c64-testing/references/scenario-format.md +24 -19
- package/skills/c64-testing/scripts/{diff.ts → lib/diff-files.ts} +18 -69
- package/skills/c64-testing/scripts/lib/diff.ts +5 -3
- package/skills/c64-testing/scripts/lib/play.ts +334 -0
- package/skills/c64-testing/scripts/lib/recording.ts +662 -0
- package/skills/c64-testing/scripts/lib/replay.ts +105 -0
- package/skills/c64-testing/scripts/{run.ts → lib/runs.ts} +36 -59
- package/skills/c64-testing/scripts/lib/scenario.ts +73 -81
- package/skills/c64-testing/scripts/testing.ts +189 -0
- package/skills/c64-unpacker/SKILL.md +42 -25
- package/skills/c64-unpacker/scripts/lib/evidence.ts +3 -4
- package/skills/c64-unpacker/scripts/lib/loads.ts +12 -10
- package/skills/c64-unpacker/scripts/lib/machine.ts +0 -2
- package/skills/c64-unpacker/scripts/lib/stages.ts +14 -19
- package/skills/c64-unpacker/scripts/{unpack.ts → unpacker.ts} +109 -211
- package/src/c64.ts +35 -20
- package/src/cli/main.ts +2 -2
- package/src/errors.ts +38 -0
- package/src/graphics.ts +3 -1
- package/src/host/server.ts +3 -2
- package/src/host/vice/session.ts +4 -4
- package/src/host-client/entry-state.ts +106 -0
- package/src/host-client/image.ts +21 -10
- package/src/host-client/steps.ts +285 -108
- package/src/host-client/transfer.ts +21 -15
- package/src/host-client/vice-session.ts +1 -1
- package/src/knowledge/database.ts +3 -23
- package/src/knowledge/expression.ts +8 -11
- package/src/knowledge/history.ts +2 -2
- package/src/knowledge/import.ts +13 -14
- package/src/knowledge/names.ts +50 -0
- package/src/knowledge/program.ts +11 -10
- package/src/knowledge/read.ts +16 -21
- package/src/knowledge/schema.ts +9 -3
- package/src/knowledge/text.ts +5 -5
- package/src/knowledge/write.ts +93 -137
- package/src/mcp/inputs.ts +105 -0
- package/src/mcp/output.ts +2 -13
- package/src/mcp/server.ts +12 -60
- package/src/mcp/tools/debug.ts +17 -12
- package/src/mcp/tools/execution.ts +5 -32
- package/src/mcp/tools/input.ts +7 -32
- package/src/mcp/tools/media.ts +6 -5
- package/src/mcp/tools/memory.ts +11 -38
- package/src/mcp/tools/video.ts +6 -5
- package/src/native/c64run.ts +162 -0
- package/src/native/discover.ts +8 -0
- package/src/native/dxa.ts +7 -6
- package/src/native/ghidra/analyze.ts +2 -1
- package/src/native/ghidra/index.ts +2 -1
- package/src/native/processes.ts +4 -3
- package/src/native/status.ts +25 -4
- package/src/native/types.ts +3 -7
- package/src/opcodes.ts +13 -13
- package/src/platform.ts +9 -9
- package/src/project.ts +16 -26
- package/src/protocol/messages.ts +3 -21
- package/src/protocol/vice.ts +7 -7
- package/src/script.ts +491 -49
- package/skills/c64-assembler/scripts/assemble.ts +0 -138
- package/skills/c64-docs/scripts/generate.ts +0 -109
- package/skills/c64-docs/scripts/link.ts +0 -239
- package/skills/c64-memory-map/scripts/memmap.ts +0 -92
- package/skills/c64-testing/scripts/checklist.ts +0 -51
- package/skills/c64-testing/scripts/test.ts +0 -65
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// The replay of c64-testing: replays a recording in the replay core
|
|
2
|
+
// (c64run). First on the original program, which must end with the recorded
|
|
3
|
+
// state hash. Then, with --rebuild, on the rebuild: it compares the state at
|
|
4
|
+
// each checkpoint with the original's. --checkpoint and --state choose the
|
|
5
|
+
// comparison, else the record file gave it.
|
|
6
|
+
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
|
|
9
|
+
import { readProjectFile, writeReported, type Written } from "#src/host-client/transfer.ts";
|
|
10
|
+
import { C64RunSession } from "#src/native/c64run.ts";
|
|
11
|
+
import { localToolContext } from "#src/native/local.ts";
|
|
12
|
+
import { Workspace } from "#src/native/staging.ts";
|
|
13
|
+
import { UsageError } from "#src/script.ts";
|
|
14
|
+
import { firstDifference, parseStateItem, pinLines, readRecording, replaySide, siteMap, type Comparison, type ReplayResult, type ReplaySubject } from "./recording.ts";
|
|
15
|
+
import { namesOf } from "./scenario.ts";
|
|
16
|
+
|
|
17
|
+
const MAX_STATE_ITEMS = 16;
|
|
18
|
+
|
|
19
|
+
export interface ReplayOptions {
|
|
20
|
+
folder: string;
|
|
21
|
+
checkpoint?: string;
|
|
22
|
+
state: string[];
|
|
23
|
+
pin: string[];
|
|
24
|
+
rebuild?: string;
|
|
25
|
+
symbols?: string;
|
|
26
|
+
out?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Replays the recording on the original, and with `rebuild` on the rebuild: PASS, FAIL or INCONCLUSIVE. */
|
|
30
|
+
export async function replay(values: ReplayOptions): Promise<ReplayResult> {
|
|
31
|
+
if (values.symbols !== undefined && values.rebuild === undefined) throw new UsageError("--symbols is for the rebuild: give --rebuild too");
|
|
32
|
+
if ((values.checkpoint === undefined) !== (values.state.length === 0)) throw new UsageError("give --checkpoint and --state together");
|
|
33
|
+
if (values.state.length + values.pin.length > MAX_STATE_ITEMS) throw new UsageError(`give at most ${MAX_STATE_ITEMS} --state and --pin items`);
|
|
34
|
+
if (values.pin.length > 0 && values.checkpoint === undefined) throw new UsageError("--pin needs --checkpoint and --state");
|
|
35
|
+
const folder = values.folder.replace(/\/+$/, "").replace(/\/recording\.json$/, "");
|
|
36
|
+
const recording = readRecording(folder);
|
|
37
|
+
const given: Comparison | undefined =
|
|
38
|
+
values.checkpoint === undefined ? undefined : { checkpoint: values.checkpoint, state: values.state.map(parseStateItem), ...(values.pin.length === 0 ? {} : { pin: values.pin.map(parseStateItem) }) };
|
|
39
|
+
const compare = given ?? recording.compare;
|
|
40
|
+
if (values.rebuild !== undefined && compare === undefined) throw new UsageError("the recording has no checkpoint and state: give --checkpoint and --state");
|
|
41
|
+
const result = (outcome: ReplayResult["result"], side: ReplayResult["side"], fields: Partial<ReplayResult> = {}): ReplayResult => ({
|
|
42
|
+
result: outcome,
|
|
43
|
+
recording: folder,
|
|
44
|
+
side,
|
|
45
|
+
...(compare === undefined ? {} : { checkpoint: compare.checkpoint }),
|
|
46
|
+
checkpoints: 0,
|
|
47
|
+
compared: 0,
|
|
48
|
+
...fields,
|
|
49
|
+
});
|
|
50
|
+
const sha256 = createHash("sha256").update(readProjectFile(recording.program).bytes).digest("hex");
|
|
51
|
+
if (sha256 !== recording.sha256) return result("INCONCLUSIVE", "original", { reason: `${recording.program} is not the program of the recording (its SHA-256 changed); record the session again` });
|
|
52
|
+
|
|
53
|
+
const context = localToolContext();
|
|
54
|
+
const core = await C64RunSession.open(context);
|
|
55
|
+
const workspace = Workspace.create(context.supervisor, "c64-re-tools-replay-");
|
|
56
|
+
try {
|
|
57
|
+
// A recording made without names takes the names that the knowledge has now.
|
|
58
|
+
const originalSymbols = recording.symbols === "none" ? "knowledge" : recording.symbols;
|
|
59
|
+
const original: ReplaySubject = { side: "original", program: recording.program, symbols: originalSymbols };
|
|
60
|
+
const side = (subject: ReplaySubject, options: Parameters<typeof replaySide>[5]) => replaySide(core, recording, folder, subject, workspace, options);
|
|
61
|
+
// The self-check: the original ends the session with the recorded state, to the cycle.
|
|
62
|
+
const self = await side(original, { ...(compare === undefined ? {} : { compare }) });
|
|
63
|
+
if (self.problem !== undefined) return result("INCONCLUSIVE", "original", { reason: `the recording does not reproduce on the original: ${self.problem}` });
|
|
64
|
+
if (self.unknown > 0 || self.misses > 0) {
|
|
65
|
+
return result("INCONCLUSIVE", "original", { reason: `the recording does not reproduce on the original: ${self.unknown + self.misses} input reads had no recorded value; record the session again with this c64run` });
|
|
66
|
+
}
|
|
67
|
+
if (self.end.tick !== recording.ticks || self.end.hash !== recording.hash) {
|
|
68
|
+
return result("INCONCLUSIVE", "original", { reason: "the recording does not reproduce on the original: the state at its end is different; record the session again with this c64run" });
|
|
69
|
+
}
|
|
70
|
+
const expected = self.samples;
|
|
71
|
+
if (compare !== undefined && expected.length === 0) return result("INCONCLUSIVE", "original", { reason: `the original never came to the checkpoint ${compare.checkpoint} in the session` });
|
|
72
|
+
if (values.rebuild === undefined) return result("PASS", "original", { checkpoints: expected.length });
|
|
73
|
+
|
|
74
|
+
const rebuild: ReplaySubject = { side: "rebuild", program: values.rebuild, symbols: values.symbols ?? originalSymbols };
|
|
75
|
+
let movedSites: number;
|
|
76
|
+
try {
|
|
77
|
+
movedSites = siteMap(recording.sites, namesOf(originalSymbols, "code"), namesOf(rebuild.symbols, "code")).length;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
throw new UsageError(`--symbols ${rebuild.symbols}: ${(error as Error).message}`);
|
|
80
|
+
}
|
|
81
|
+
const pins = pinLines(expected, compare!.state);
|
|
82
|
+
const replayed = await side(rebuild, { compare: compare!, stop: expected.length, pins });
|
|
83
|
+
const difference = firstDifference(expected, replayed.samples, compare!.state);
|
|
84
|
+
// A read that the recording has no value for gets the idle value: it is in the result, never silent.
|
|
85
|
+
const base = { checkpoints: expected.length, movedSites, ...(replayed.unknown > 0 ? { idleReads: replayed.unknown } : {}) };
|
|
86
|
+
if (difference === undefined && replayed.samples.length >= expected.length) return result("PASS", "rebuild", { ...base, compared: expected.length });
|
|
87
|
+
const reason =
|
|
88
|
+
difference !== undefined
|
|
89
|
+
? `the state differs at checkpoint ${difference.checkpoint} of ${expected.length}${replayed.unknown > 0 ? `; the rebuild read input ${replayed.unknown} times at sites that the recording does not have` : ""}`
|
|
90
|
+
: `the rebuild came to ${replayed.samples.length} of ${expected.length} checkpoints${replayed.problem === undefined ? "" : `: ${replayed.problem}`}`;
|
|
91
|
+
const failed = result("FAIL", "rebuild", { ...base, compared: (difference?.checkpoint ?? replayed.samples.length + 1) - 1, reason, ...(difference === undefined ? {} : { firstDifference: difference }) });
|
|
92
|
+
if (values.out !== undefined && difference !== undefined) {
|
|
93
|
+
const files: Written[] = [];
|
|
94
|
+
for (const subject of [original, rebuild]) {
|
|
95
|
+
const dumped = await side(subject, { compare: compare!, dump: difference.checkpoint, ...(subject.side === "rebuild" ? { pins } : {}) });
|
|
96
|
+
files.push(writeReported(`${values.out}/${subject.side}-${difference.checkpoint}.bin`, dumped.ram!, { what: "--out" }));
|
|
97
|
+
}
|
|
98
|
+
failed.files = files;
|
|
99
|
+
}
|
|
100
|
+
return failed;
|
|
101
|
+
} finally {
|
|
102
|
+
await core.close();
|
|
103
|
+
workspace.remove();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -1,28 +1,17 @@
|
|
|
1
|
-
// The c64-testing
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// ran, 1 when a step did not, 2 for a bad call.
|
|
6
|
-
|
|
7
|
-
import { parseArgs } from "node:util";
|
|
1
|
+
// The runs of c64-testing: steps in their own emulator, from a program or a
|
|
2
|
+
// snapshot file, and what the steps ask for written into a folder: screens,
|
|
3
|
+
// coverage maps, snapshot files, and results.json with the observations. One
|
|
4
|
+
// side, no comparison.
|
|
8
5
|
|
|
9
6
|
import { formatC64Address, formatSprite, formatVicii, parseC64Address } from "#src/c64.ts";
|
|
10
7
|
import { runProgram } from "#src/host-client/autostart.ts";
|
|
11
8
|
import { readCoverage } from "#src/host-client/coverage.ts";
|
|
12
|
-
import {
|
|
9
|
+
import { ACTION_STEP_KINDS, checkActionStep, isInteger, isObject, runActionStep, type ActionStep, type ActionStepKind } from "#src/host-client/steps.ts";
|
|
13
10
|
import { MAX_MEMORY_READ, MAX_TRACE_FRAMES, MAX_TRACE_HITS, MAX_TRACE_POINTS, TRACE_KINDS, WireFailure, type TraceHit, type TraceKind } from "#src/host-client/tools.ts";
|
|
14
|
-
import { readProjectFile,
|
|
11
|
+
import { readProjectFile, writeReported, type Written } from "#src/host-client/transfer.ts";
|
|
15
12
|
import { readMemoryInParts, ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import { loadSymbols } from "./lib/scenario.ts";
|
|
19
|
-
|
|
20
|
-
const USAGE = `run.ts <run.json> --out <dir>
|
|
21
|
-
|
|
22
|
-
<run.json> is a run file relative to the project folder: where to start,
|
|
23
|
-
optional symbols, and the steps. See references/scenario-format.md, section
|
|
24
|
-
Runs. The files that the steps write, and results.json, go into <dir>. The
|
|
25
|
-
result is one JSON object.`;
|
|
13
|
+
import { UsageError } from "#src/script.ts";
|
|
14
|
+
import { namesOf } from "./scenario.ts";
|
|
26
15
|
|
|
27
16
|
type RunStep =
|
|
28
17
|
| ActionStep
|
|
@@ -30,8 +19,8 @@ type RunStep =
|
|
|
30
19
|
| { save: string }
|
|
31
20
|
| { screen: string }
|
|
32
21
|
| { coverage: string }
|
|
33
|
-
| { trace: string; points: Array<{ kind: TraceKind;
|
|
34
|
-
| { observe: string; memory?: "all" | Array<string | {
|
|
22
|
+
| { trace: string; points: Array<{ kind: TraceKind; start: string; end?: string }>; frames: number; maxHits?: number }
|
|
23
|
+
| { observe: string; memory?: "all" | Array<string | { address: string; size?: number; view?: "cpu" | "ram" }>; registers?: boolean; vicii?: boolean; sprites?: number[] };
|
|
35
24
|
|
|
36
25
|
interface Run {
|
|
37
26
|
name: string;
|
|
@@ -73,9 +62,13 @@ function readRun(path: string): Run {
|
|
|
73
62
|
const kinds = "observe" in step ? ["observe"] : "trace" in step ? ["trace"] : Object.keys(step).filter((key) => (ACTION_STEP_KINDS as readonly string[]).includes(key) || (OWN_KINDS as readonly string[]).includes(key));
|
|
74
63
|
if (kinds.length !== 1) return fail(`${at} must have exactly one of ${[...ACTION_STEP_KINDS, ...OWN_KINDS].join(", ")}`);
|
|
75
64
|
const kind = kinds[0]!;
|
|
76
|
-
const allowed = kind === "observe" ? OBSERVE_KEYS : kind === "trace" ? TRACE_KEYS :
|
|
65
|
+
const allowed = kind === "observe" ? OBSERVE_KEYS : kind === "trace" ? TRACE_KEYS : [kind];
|
|
77
66
|
for (const key of Object.keys(step)) if (!allowed.includes(key)) fail(`${at} has the key ${key}, which a ${kind} step does not have`);
|
|
78
|
-
if ((ACTION_STEP_KINDS as readonly string[]).includes(kind))
|
|
67
|
+
if ((ACTION_STEP_KINDS as readonly string[]).includes(kind)) {
|
|
68
|
+
// The step with its defaults filled in, as the shared schema of the action steps gives it.
|
|
69
|
+
(value.steps as unknown[])[index] = checkActionStep(kind as ActionStepKind, step, at, fail);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
79
72
|
const name = step[kind];
|
|
80
73
|
if (kind === "snapshot") {
|
|
81
74
|
if (typeof name !== "string" || name === "") fail(`${at}: snapshot needs a project file`);
|
|
@@ -86,11 +79,11 @@ function readRun(path: string): Run {
|
|
|
86
79
|
if (labels.has(name as string)) fail(`${at} needs a new name`);
|
|
87
80
|
labels.add(name as string);
|
|
88
81
|
const points = step.points;
|
|
89
|
-
if (!Array.isArray(points) || points.length === 0 || points.length > MAX_TRACE_POINTS) return fail(`${at}: points is a list of 1 to ${MAX_TRACE_POINTS} objects with kind and
|
|
82
|
+
if (!Array.isArray(points) || points.length === 0 || points.length > MAX_TRACE_POINTS) return fail(`${at}: points is a list of 1 to ${MAX_TRACE_POINTS} objects with kind and start`);
|
|
90
83
|
for (const point of points as unknown[]) {
|
|
91
|
-
if (!isObject(point) || Object.keys(point).some((key) => !["kind", "
|
|
84
|
+
if (!isObject(point) || Object.keys(point).some((key) => !["kind", "start", "end"].includes(key))) return fail(`${at}: a point has kind, start and end, and nothing else`);
|
|
92
85
|
if (!(TRACE_KINDS as readonly unknown[]).includes(point.kind)) fail(`${at}: a point kind is exec, read or write`);
|
|
93
|
-
if (typeof point.
|
|
86
|
+
if (typeof point.start !== "string" || (point.end !== undefined && typeof point.end !== "string")) fail(`${at}: a point needs start, and end when it is a range, as names or $ addresses`);
|
|
94
87
|
}
|
|
95
88
|
if (!isInteger(step.frames, 1, MAX_TRACE_FRAMES)) fail(`${at}: frames must be a number from 1 to ${MAX_TRACE_FRAMES}`);
|
|
96
89
|
if (step.maxHits !== undefined && !isInteger(step.maxHits, 1, MAX_TRACE_HITS)) fail(`${at}: maxHits must be a number from 1 to ${MAX_TRACE_HITS}`);
|
|
@@ -102,12 +95,12 @@ function readRun(path: string): Run {
|
|
|
102
95
|
const memoryItem = (item: unknown) =>
|
|
103
96
|
typeof item === "string" ||
|
|
104
97
|
(isObject(item) &&
|
|
105
|
-
typeof item.
|
|
106
|
-
Object.keys(item).every((key) => ["
|
|
98
|
+
typeof item.address === "string" &&
|
|
99
|
+
Object.keys(item).every((key) => ["address", "size", "view"].includes(key)) &&
|
|
107
100
|
(item.size === undefined || isInteger(item.size, 1, 0x10000)) &&
|
|
108
101
|
(item.view === undefined || item.view === "cpu" || item.view === "ram"));
|
|
109
102
|
if (step.memory !== undefined && step.memory !== "all" && (!Array.isArray(step.memory) || !step.memory.every(memoryItem))) {
|
|
110
|
-
fail(`${at}: memory is "all", or a list of names, $ addresses, or objects with
|
|
103
|
+
fail(`${at}: memory is "all", or a list of names, $ addresses, or objects with address, size (1 to 65536) and view (cpu or ram)`);
|
|
111
104
|
}
|
|
112
105
|
if (step.registers !== undefined && typeof step.registers !== "boolean") fail(`${at}: registers must be true or false`);
|
|
113
106
|
if (step.vicii !== undefined && typeof step.vicii !== "boolean") fail(`${at}: vicii must be true or false`);
|
|
@@ -121,9 +114,10 @@ function showHit(hit: TraceHit) {
|
|
|
121
114
|
return { ...hit, pc: formatC64Address(hit.pc), ...(hit.address === undefined ? {} : { address: formatC64Address(hit.address) }) };
|
|
122
115
|
}
|
|
123
116
|
|
|
124
|
-
|
|
117
|
+
/** Runs the run file and writes its files into `out`. failed: a step did not run. */
|
|
118
|
+
export async function runFile(path: string, out: string): Promise<{ output: Record<string, unknown>; failed: boolean }> {
|
|
125
119
|
const run = readRun(path);
|
|
126
|
-
const symbols =
|
|
120
|
+
const symbols = namesOf(run.symbols ?? "none");
|
|
127
121
|
const at = (location: string): number => {
|
|
128
122
|
if (location.startsWith("$")) return parseC64Address(location);
|
|
129
123
|
const address = symbols.get(location);
|
|
@@ -131,8 +125,9 @@ async function runFile(path: string, out: string): Promise<{ output: Record<stri
|
|
|
131
125
|
return address;
|
|
132
126
|
};
|
|
133
127
|
const files: string[] = [];
|
|
128
|
+
const written: Written[] = [];
|
|
134
129
|
const write = (name: string, bytes: Uint8Array) => {
|
|
135
|
-
|
|
130
|
+
written.push(writeReported(`${out}/${name}`, bytes, { what: "--out" }));
|
|
136
131
|
files.push(name);
|
|
137
132
|
};
|
|
138
133
|
const observations: Record<string, unknown> = {};
|
|
@@ -158,7 +153,7 @@ async function runFile(path: string, out: string): Promise<{ output: Record<stri
|
|
|
158
153
|
} else if ("save" in step) {
|
|
159
154
|
await client.snapshot({ action: "save", name: "run-save" });
|
|
160
155
|
write(`${step.save}.vsf`, await client.snapshotExport("run-save"));
|
|
161
|
-
await client.snapshot({ action: "
|
|
156
|
+
await client.snapshot({ action: "remove", name: "run-save" });
|
|
162
157
|
} else if ("screen" in step) {
|
|
163
158
|
write(`${step.screen}.png`, Buffer.from((await client.screenCapture()).png, "base64"));
|
|
164
159
|
} else if ("coverage" in step) {
|
|
@@ -166,7 +161,7 @@ async function runFile(path: string, out: string): Promise<{ output: Record<stri
|
|
|
166
161
|
await client.memmap({ action: "clear" });
|
|
167
162
|
} else if ("trace" in step) {
|
|
168
163
|
// A trace: every hit of the points while the program runs on, to <name>.json.
|
|
169
|
-
const points = step.points.map((point) => ({ kind: point.kind, start: at(point.
|
|
164
|
+
const points = step.points.map((point) => ({ kind: point.kind, start: at(point.start), end: at(point.end ?? point.start) }));
|
|
170
165
|
if (points.some((point) => point.end < point.start)) throw new UsageError(`the trace ${step.trace} has a point whose end is before its start`);
|
|
171
166
|
const traced = await client.trace({ points, frames: step.frames, maxHits: step.maxHits ?? MAX_TRACE_HITS });
|
|
172
167
|
write(`${step.trace}.json`, Buffer.from(`${JSON.stringify({ points: step.points, frames: traced.frames, end: traced.end, hits: traced.hits.map(showHit) }, null, 1)}\n`));
|
|
@@ -184,15 +179,15 @@ async function runFile(path: string, out: string): Promise<{ output: Record<stri
|
|
|
184
179
|
} else if (step.memory !== undefined) {
|
|
185
180
|
const memory: Record<string, string | { file: string }> = {};
|
|
186
181
|
for (const item of step.memory) {
|
|
187
|
-
const spec = typeof item === "string" ? {
|
|
182
|
+
const spec = typeof item === "string" ? { address: item } : item;
|
|
188
183
|
const size = spec.size ?? 1;
|
|
189
|
-
const bytes = await readMemoryInParts(client, at(spec.
|
|
184
|
+
const bytes = await readMemoryInParts(client, at(spec.address), size, spec.view ?? "cpu");
|
|
190
185
|
// More than one read gives goes to a file: results.json stays small.
|
|
191
|
-
if (size <= MAX_MEMORY_READ) memory[spec.
|
|
186
|
+
if (size <= MAX_MEMORY_READ) memory[spec.address] = bytes.toString("hex");
|
|
192
187
|
else {
|
|
193
|
-
const file = `${step.observe}-${spec.
|
|
188
|
+
const file = `${step.observe}-${spec.address.replace(/^\$/, "")}.bin`;
|
|
194
189
|
write(file, bytes);
|
|
195
|
-
memory[spec.
|
|
190
|
+
memory[spec.address] = { file };
|
|
196
191
|
}
|
|
197
192
|
}
|
|
198
193
|
seen.memory = memory;
|
|
@@ -207,7 +202,7 @@ async function runFile(path: string, out: string): Promise<{ output: Record<stri
|
|
|
207
202
|
} else {
|
|
208
203
|
const notReached = await runActionStep(client, step, at);
|
|
209
204
|
if (notReached !== undefined && "runUntil" in step) {
|
|
210
|
-
reason = `Step ${done + 1} did not reach its target in ${step.timeoutFrames} frames. It stopped by ${notReached.stopReason} at ${formatC64Address(notReached.pc)}.`;
|
|
205
|
+
reason = `Step ${done + 1} did not reach its target in ${step.runUntil.timeoutFrames} frames. It stopped by ${notReached.stopReason} at ${formatC64Address(notReached.pc)}.`;
|
|
211
206
|
break;
|
|
212
207
|
}
|
|
213
208
|
}
|
|
@@ -216,26 +211,8 @@ async function runFile(path: string, out: string): Promise<{ output: Record<stri
|
|
|
216
211
|
const pc = (await client.registersGet("c64")).pc;
|
|
217
212
|
const results = { name: run.name, completed: reason === undefined, steps: run.steps.length, done, ...(reason === undefined ? {} : { reason }), pc: formatC64Address(pc), files: [...files], observations, ...(Object.keys(traces).length === 0 ? {} : { traces }) };
|
|
218
213
|
write("results.json", Buffer.from(`${JSON.stringify(results, null, 2)}\n`));
|
|
219
|
-
return { output: { ...results, files, observations: Object.keys(observations), out }, failed: reason !== undefined };
|
|
214
|
+
return { output: { ...results, files: written, observations: Object.keys(observations), out }, failed: reason !== undefined };
|
|
220
215
|
} finally {
|
|
221
216
|
await client.close();
|
|
222
217
|
}
|
|
223
218
|
}
|
|
224
|
-
|
|
225
|
-
await runScript(
|
|
226
|
-
async (argv) => {
|
|
227
|
-
const { values, positionals } = parseArgs({ args: argv, strict: true, allowPositionals: true, options: { out: { type: "string" }, help: { type: "boolean" } } });
|
|
228
|
-
if (values.help) throw new UsageError("");
|
|
229
|
-
if (positionals.length !== 1) throw new UsageError("give one run file");
|
|
230
|
-
if (values.out === undefined) throw new UsageError("run needs --out <dir>");
|
|
231
|
-
try {
|
|
232
|
-
resolveProjectPath(values.out);
|
|
233
|
-
} catch {
|
|
234
|
-
throw new UsageError(`--out must be a path relative to the project folder that stays inside it, not ${values.out}`);
|
|
235
|
-
}
|
|
236
|
-
const { output, failed } = await runFile(positionals[0]!, values.out);
|
|
237
|
-
if (failed) process.exitCode = 1;
|
|
238
|
-
return output;
|
|
239
|
-
},
|
|
240
|
-
{ usage: USAGE },
|
|
241
|
-
);
|
|
@@ -3,18 +3,19 @@
|
|
|
3
3
|
// Observations name semantic symbols that resolve per side, comparisons are
|
|
4
4
|
// explicit, and the result is PASS, FAIL or INCONCLUSIVE.
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { isRefusal, Refusal } from "#src/errors.ts";
|
|
7
|
+
import { c64AddressOf, formatC64Address, formatSprite, formatVicii, parseC64Address } from "#src/c64.ts";
|
|
7
8
|
import { MAX_BASELINES, MAX_MASK_RECTANGLES, maskCoversFrame, WireFailure } from "#src/host-client/tools.ts";
|
|
8
9
|
import { LOAD_LIMIT_FRAMES, RUN_LIMIT_FRAMES, runProgram } from "#src/host-client/autostart.ts";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
10
|
+
import { ACTION_STEP_KINDS, checkActionStep, checkLocation, isInteger, isObject, runActionStep, type ActionStep, type ActionStepKind, type Location } from "#src/host-client/steps.ts";
|
|
11
|
+
import { applyEntryState, parseEntryState } from "#src/host-client/entry-state.ts";
|
|
12
|
+
import { readProjectFile, writeReported, type Written } from "#src/host-client/transfer.ts";
|
|
11
13
|
import { readMemoryInParts, type ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
12
14
|
import { openForRead } from "#src/knowledge/database.ts";
|
|
13
|
-
import {
|
|
15
|
+
import { nameTable } from "#src/knowledge/names.ts";
|
|
16
|
+
import { DEFAULT_SPACE } from "#src/knowledge/schema.ts";
|
|
14
17
|
import { programImage } from "#src/knowledge/program.ts";
|
|
15
18
|
|
|
16
|
-
export class ScenarioError extends Error {}
|
|
17
|
-
|
|
18
19
|
export type Side = "original" | "rebuild";
|
|
19
20
|
export const SIDES: Side[] = ["original", "rebuild"];
|
|
20
21
|
|
|
@@ -22,15 +23,15 @@ export const SIDES: Side[] = ["original", "rebuild"];
|
|
|
22
23
|
export type { Location };
|
|
23
24
|
|
|
24
25
|
export interface MemoryObservation {
|
|
25
|
-
|
|
26
|
-
/** Bytes from `
|
|
26
|
+
address: Location;
|
|
27
|
+
/** Bytes from `address`; 1 when not given, at most 64 KiB. */
|
|
27
28
|
size?: number;
|
|
28
29
|
/** cpu (the default) reads as the CPU sees memory; ram reads the RAM under the ROMs and the I/O area. */
|
|
29
30
|
view?: "cpu" | "ram";
|
|
30
31
|
/** The largest allowed difference per byte; 0 when not given. */
|
|
31
32
|
tolerance?: number;
|
|
32
33
|
/** Bytes in the range that the comparison leaves out, each with the reason. */
|
|
33
|
-
exclude?: Array<{
|
|
34
|
+
exclude?: Array<{ address: Location; reason: string }>;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
export interface ScreenPolicy {
|
|
@@ -46,7 +47,7 @@ type StepBody =
|
|
|
46
47
|
| { autostart: true }
|
|
47
48
|
| { snapshot: string }
|
|
48
49
|
| { normalize: true }
|
|
49
|
-
| ActionStep
|
|
50
|
+
| Exclude<ActionStep, { press: string }>
|
|
50
51
|
| { observe: string; memory?: Array<Location | MemoryObservation>; registers?: ObservedRegister[]; screen?: ScreenPolicy; vicii?: boolean; sprites?: number[] };
|
|
51
52
|
|
|
52
53
|
/** A step; with side, only that side does it. An observe step has no side: it compares both. */
|
|
@@ -83,16 +84,15 @@ export interface ScenarioResult {
|
|
|
83
84
|
checkpoints: string[];
|
|
84
85
|
differences: Difference[];
|
|
85
86
|
/** The bytes that the comparison left out, with their reasons. */
|
|
86
|
-
excluded?: Array<{ checkpoint: string;
|
|
87
|
+
excluded?: Array<{ checkpoint: string; address: Location; reason: string }>;
|
|
87
88
|
/** The files that a FAIL wrote with --out. */
|
|
88
|
-
files?:
|
|
89
|
+
files?: Written[];
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
const STEP_KINDS = ["reset", "load", "autostart", "snapshot", "normalize",
|
|
92
|
+
const STEP_KINDS = ["reset", "load", "autostart", "snapshot", "normalize", ...ACTION_STEP_KINDS, "observe"];
|
|
92
93
|
|
|
93
94
|
/** The keys each kind of step can have. */
|
|
94
95
|
const STEP_KEYS: Record<string, readonly string[]> = {
|
|
95
|
-
...ACTION_STEP_KEYS,
|
|
96
96
|
observe: ["observe", "memory", "registers", "screen", "vicii", "sprites"],
|
|
97
97
|
};
|
|
98
98
|
|
|
@@ -100,16 +100,17 @@ const STEP_KEYS: Record<string, readonly string[]> = {
|
|
|
100
100
|
export const OBSERVED_REGISTERS = ["pc", "a", "x", "y", "sp", "flags"] as const;
|
|
101
101
|
type ObservedRegister = (typeof OBSERVED_REGISTERS)[number];
|
|
102
102
|
|
|
103
|
-
/** Reads and checks a scenario file.
|
|
103
|
+
/** Reads and checks a scenario file. Refuses it with the first problem. */
|
|
104
104
|
export function readScenario(path: string): Scenario {
|
|
105
105
|
let value: unknown;
|
|
106
106
|
try {
|
|
107
107
|
value = JSON.parse(readProjectFile(path).bytes.toString("utf8"));
|
|
108
108
|
} catch (error) {
|
|
109
|
-
|
|
109
|
+
if (isRefusal(error)) throw error;
|
|
110
|
+
throw new Refusal("invalid-input", `cannot read the scenario ${path}: ${(error as Error).message}`);
|
|
110
111
|
}
|
|
111
112
|
const fail = (message: string): never => {
|
|
112
|
-
throw new
|
|
113
|
+
throw new Refusal("invalid-input", `scenario ${path}: ${message}`);
|
|
113
114
|
};
|
|
114
115
|
if (!isObject(value)) return fail("is not a JSON object");
|
|
115
116
|
const scenario = value as unknown as Scenario;
|
|
@@ -132,6 +133,11 @@ export function readScenario(path: string): Scenario {
|
|
|
132
133
|
const allowed = [...(STEP_KEYS[kind] ?? [kind]), ...(kind === "observe" ? [] : ["side"])];
|
|
133
134
|
for (const key of Object.keys(step)) if (!allowed.includes(key)) fail(`${at} has the key ${key}, which a ${kind} step does not have`);
|
|
134
135
|
if (step.side !== undefined && !(SIDES as unknown[]).includes(step.side)) fail(`${at}: side must be original or rebuild`);
|
|
136
|
+
if ((ACTION_STEP_KINDS as readonly string[]).includes(kind)) {
|
|
137
|
+
// The step with its defaults filled in, as the shared schema of the action steps gives it.
|
|
138
|
+
scenario.steps[index] = { ...checkActionStep(kind as ActionStepKind, { [kind]: step[kind] }, at, fail), ...(step.side === undefined ? {} : { side: step.side as Side }) } as Step;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
135
141
|
checkStep(kind, step, at, fail, scenario.videoStandard ?? "pal");
|
|
136
142
|
if (kind === "observe") {
|
|
137
143
|
if (labels.has(step.observe as string)) fail(`${at} needs a new checkpoint name`);
|
|
@@ -160,13 +166,6 @@ function checkStep(kind: string, step: Record<string, unknown>, at: string, fail
|
|
|
160
166
|
case "normalize":
|
|
161
167
|
if (step.normalize !== true) fail(`${at}: normalize is true: it writes the entry state of the knowledge`);
|
|
162
168
|
return;
|
|
163
|
-
case "write":
|
|
164
|
-
case "registers":
|
|
165
|
-
case "joystick":
|
|
166
|
-
case "type":
|
|
167
|
-
case "frames":
|
|
168
|
-
case "runUntil":
|
|
169
|
-
return checkActionStep(kind, step, at, fail);
|
|
170
169
|
default:
|
|
171
170
|
checkObserve(step, at, fail, videoStandard);
|
|
172
171
|
}
|
|
@@ -175,23 +174,23 @@ function checkStep(kind: string, step: Record<string, unknown>, at: string, fail
|
|
|
175
174
|
function checkObserve(step: Record<string, unknown>, at: string, fail: (message: string) => never, videoStandard: "pal" | "ntsc"): void {
|
|
176
175
|
if (typeof step.observe !== "string" || step.observe === "") fail(`${at} needs a new checkpoint name`);
|
|
177
176
|
if (step.memory !== undefined) {
|
|
178
|
-
if (!Array.isArray(step.memory) || step.memory.length === 0) fail(`${at}: memory must be a list of names, $ addresses or objects with
|
|
177
|
+
if (!Array.isArray(step.memory) || step.memory.length === 0) fail(`${at}: memory must be a list of names, $ addresses or objects with address`);
|
|
179
178
|
for (const item of step.memory as unknown[]) {
|
|
180
179
|
if (typeof item === "string") {
|
|
181
180
|
checkLocation(item, `${at}: memory`, fail);
|
|
182
181
|
continue;
|
|
183
182
|
}
|
|
184
|
-
if (!isObject(item) || Object.keys(item).some((key) => !["
|
|
183
|
+
if (!isObject(item) || Object.keys(item).some((key) => !["address", "size", "view", "tolerance", "exclude"].includes(key))) return fail(`${at}: a memory object has address, size, view, tolerance and exclude, and nothing else`);
|
|
185
184
|
if (item.view !== undefined && item.view !== "cpu" && item.view !== "ram") fail(`${at}: memory.view must be cpu or ram`);
|
|
186
185
|
if (item.exclude !== undefined) {
|
|
187
|
-
if (!Array.isArray(item.exclude) || item.exclude.length === 0) fail(`${at}: memory.exclude must be a list of objects with
|
|
186
|
+
if (!Array.isArray(item.exclude) || item.exclude.length === 0) fail(`${at}: memory.exclude must be a list of objects with address and reason`);
|
|
188
187
|
for (const excluded of item.exclude as unknown[]) {
|
|
189
|
-
if (!isObject(excluded) || Object.keys(excluded).some((key) => key !== "
|
|
190
|
-
checkLocation(excluded.
|
|
188
|
+
if (!isObject(excluded) || Object.keys(excluded).some((key) => key !== "address" && key !== "reason")) return fail(`${at}: an exclude has address and reason, and nothing else`);
|
|
189
|
+
checkLocation(excluded.address, `${at}: memory.exclude.address`, fail);
|
|
191
190
|
if (typeof excluded.reason !== "string" || excluded.reason.trim() === "") fail(`${at}: an exclude needs a reason: why this byte may differ`);
|
|
192
191
|
}
|
|
193
192
|
}
|
|
194
|
-
checkLocation(item.
|
|
193
|
+
checkLocation(item.address, `${at}: memory.address`, fail);
|
|
195
194
|
if (item.size !== undefined && !isInteger(item.size, 1, 0x10000)) fail(`${at}: memory.size must be a number from 1 to 65536`);
|
|
196
195
|
if (item.tolerance !== undefined && !isInteger(item.tolerance, 0, 0xff)) fail(`${at}: memory.tolerance must be a number from 0 to 255`);
|
|
197
196
|
}
|
|
@@ -233,46 +232,48 @@ function checkObserve(step: Record<string, unknown>, at: string, fail: (message:
|
|
|
233
232
|
}
|
|
234
233
|
}
|
|
235
234
|
|
|
236
|
-
/**
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
235
|
+
/**
|
|
236
|
+
* Name → address from a source of names: none; the knowledge (its symbols and
|
|
237
|
+
* the analyzer names that only one address has); the c64-assembler output; or
|
|
238
|
+
* a JSON map of name to $ address. With kinds "code", only the names that move
|
|
239
|
+
* with the code: labels and routines of the knowledge, and the labels of the
|
|
240
|
+
* assembler output. With "all", also the data names and the assembler
|
|
241
|
+
* constants of four hex digits: RAM variables such as "counter = $c100" lie
|
|
242
|
+
* outside the program, so the assembler reports them as constants.
|
|
243
|
+
*/
|
|
244
|
+
export function namesOf(source: string, kinds: "code" | "all" = "all"): Map<string, number> {
|
|
245
|
+
if (source === "none") return new Map();
|
|
246
|
+
if (source === "knowledge") {
|
|
241
247
|
const db = openForRead();
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
db?.close();
|
|
248
|
-
return symbols;
|
|
248
|
+
try {
|
|
249
|
+
return nameTable(db, DEFAULT_SPACE, kinds === "code" ? ["label", "routine"] : undefined);
|
|
250
|
+
} finally {
|
|
251
|
+
db?.close();
|
|
252
|
+
}
|
|
249
253
|
}
|
|
250
254
|
let value: unknown;
|
|
251
255
|
try {
|
|
252
|
-
value = JSON.parse(readProjectFile(
|
|
256
|
+
value = JSON.parse(readProjectFile(source).bytes.toString("utf8"));
|
|
253
257
|
} catch (error) {
|
|
254
|
-
|
|
258
|
+
if (isRefusal(error)) throw error;
|
|
259
|
+
throw new Refusal("invalid-input", `cannot read the symbols ${source}: ${(error as Error).message}`);
|
|
255
260
|
}
|
|
256
|
-
// c64-assembler output, or a plain name → "$xxxx" map. A constant in the
|
|
257
|
-
// assembler output counts too: RAM variables such as "counter = $c100" lie
|
|
258
|
-
// outside the program, so the assembler reports them as constants.
|
|
259
261
|
const assembled = (value as { symbols?: Array<{ name: string; address?: string; value?: string }> }).symbols;
|
|
260
262
|
const entries: Array<[string, unknown]> = Array.isArray(assembled)
|
|
261
263
|
? assembled.flatMap((symbol): Array<[string, unknown]> => {
|
|
262
264
|
if (symbol.address !== undefined) return [[symbol.name, symbol.address]];
|
|
263
265
|
// A constant above $ffff has more digits and is no address.
|
|
264
|
-
if (typeof symbol.value === "string" && /^\$[0-9a-f]{4}$/.test(symbol.value)) return [[symbol.name, symbol.value]];
|
|
266
|
+
if (kinds === "all" && typeof symbol.value === "string" && /^\$[0-9a-f]{4}$/.test(symbol.value)) return [[symbol.name, symbol.value]];
|
|
265
267
|
return [];
|
|
266
268
|
})
|
|
267
269
|
: Object.entries(value as Record<string, unknown>);
|
|
270
|
+
const names = new Map<string, number>();
|
|
268
271
|
for (const [name, address] of entries) {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
throw new ScenarioError(`the symbol ${name} in ${subject.symbols} has no $xxxx address`);
|
|
273
|
-
}
|
|
272
|
+
const at = typeof address === "string" ? c64AddressOf(address) : undefined;
|
|
273
|
+
if (at === undefined) throw new Refusal("invalid-input", `the symbol ${name} in ${source} has no $ address`);
|
|
274
|
+
names.set(name, at);
|
|
274
275
|
}
|
|
275
|
-
return
|
|
276
|
+
return names;
|
|
276
277
|
}
|
|
277
278
|
|
|
278
279
|
/** A failure that makes the comparison impossible: the result is INCONCLUSIVE. */
|
|
@@ -283,7 +284,7 @@ export function resolve(location: Location, symbols: Map<string, number>, side:
|
|
|
283
284
|
try {
|
|
284
285
|
return parseC64Address(location);
|
|
285
286
|
} catch {
|
|
286
|
-
throw new
|
|
287
|
+
throw new Refusal("invalid-input", `${location} is not $ and 1 to 4 hex digits`);
|
|
287
288
|
}
|
|
288
289
|
}
|
|
289
290
|
const address = symbols.get(location);
|
|
@@ -292,7 +293,7 @@ export function resolve(location: Location, symbols: Map<string, number>, side:
|
|
|
292
293
|
}
|
|
293
294
|
|
|
294
295
|
interface Observation {
|
|
295
|
-
memory: Array<{ label: string; address: number; bytes: number[]; tolerance: number; excluded: Array<{ offset: number;
|
|
296
|
+
memory: Array<{ label: string; address: number; bytes: number[]; tolerance: number; excluded: Array<{ offset: number; address: Location; reason: string }> }>;
|
|
296
297
|
registers: Partial<Record<ObservedRegister, unknown>>;
|
|
297
298
|
screen?: { match: boolean; mismatchingPixels: number; comparedPixels: number; mismatchRatio: number; bounds?: unknown };
|
|
298
299
|
vicii?: unknown;
|
|
@@ -305,22 +306,14 @@ interface Observation {
|
|
|
305
306
|
/** Writes pages 0-3 and $00/$01 from the entry state of the knowledge: both sides then start from the same RAM. */
|
|
306
307
|
async function normalize(client: ViceSessionClient): Promise<void> {
|
|
307
308
|
const db = openForRead();
|
|
308
|
-
let state:
|
|
309
|
+
let state: unknown;
|
|
309
310
|
try {
|
|
310
|
-
state = programImage(db)?.state
|
|
311
|
+
state = programImage(db)?.state;
|
|
311
312
|
} finally {
|
|
312
313
|
db?.close();
|
|
313
314
|
}
|
|
314
|
-
if (state === undefined) throw new
|
|
315
|
-
|
|
316
|
-
const port = [state.cpuPort?.$00, state.cpuPort?.$01];
|
|
317
|
-
if (pages.some((page) => typeof page !== "string" || !/^[0-9a-f]{512}$/.test(page)) || port.some((value) => !isInteger(value, 0, 0xff))) {
|
|
318
|
-
throw new ScenarioError("the entry state of the knowledge is not the entry state of a c64-unpacker capture: it needs pages $0000 to $0300 and cpuPort $00 and $01");
|
|
319
|
-
}
|
|
320
|
-
if ((await client.status()).state === "running") await client.execution({ action: "pause", space: "c64" });
|
|
321
|
-
// Pages 0 to 3 in RAM from $0002, then the CPU port: the port bits switch what the CPU sees.
|
|
322
|
-
await client.memoryWrite({ address: 0x0002, data: pages.join("").slice(4), space: "c64", view: "ram" });
|
|
323
|
-
await client.memoryWrite({ address: 0x0000, data: Buffer.from(port as number[]).toString("hex"), space: "c64", view: "cpu" });
|
|
315
|
+
if (state === undefined) throw new Refusal("invalid-input", "normalize needs the entry state of a c64-unpacker capture in the knowledge: put the capture in with knowledge.ts image <image.json> of the c64-knowledge skill");
|
|
316
|
+
await applyEntryState(client, parseEntryState(state, "the entry state of the knowledge"), { registers: false });
|
|
324
317
|
}
|
|
325
318
|
|
|
326
319
|
/**
|
|
@@ -355,16 +348,16 @@ async function runSide(client: ViceSessionClient, scenario: Scenario, side: Side
|
|
|
355
348
|
checkpoint++;
|
|
356
349
|
const observation: Observation = { memory: [], registers: {} };
|
|
357
350
|
for (const item of step.memory ?? []) {
|
|
358
|
-
const spec: MemoryObservation = typeof item === "string" ? {
|
|
359
|
-
const address = at(spec.
|
|
351
|
+
const spec: MemoryObservation = typeof item === "string" ? { address: item } : item;
|
|
352
|
+
const address = at(spec.address);
|
|
360
353
|
const size = spec.size ?? 1;
|
|
361
354
|
const read = { data: (await readMemoryInParts(client, address, size, spec.view ?? "cpu")).toString("hex") };
|
|
362
355
|
const excluded = (spec.exclude ?? []).map((exclude) => {
|
|
363
|
-
const offset = at(exclude.
|
|
364
|
-
if (offset < 0 || offset >= size) throw new
|
|
365
|
-
return { offset,
|
|
356
|
+
const offset = at(exclude.address) - address;
|
|
357
|
+
if (offset < 0 || offset >= size) throw new Refusal("invalid-input", `the exclude ${exclude.address} lies outside the observed ${spec.address} (${size} bytes)`);
|
|
358
|
+
return { offset, address: exclude.address, reason: exclude.reason };
|
|
366
359
|
});
|
|
367
|
-
observation.memory.push({ label: size > 1 ? `${spec.
|
|
360
|
+
observation.memory.push({ label: size > 1 ? `${spec.address} (${size} bytes)` : spec.address, address, bytes: [...Buffer.from(read.data, "hex")], tolerance: spec.tolerance ?? 0, excluded });
|
|
368
361
|
}
|
|
369
362
|
if (step.registers !== undefined) {
|
|
370
363
|
const registers = await client.registersGet("c64");
|
|
@@ -407,7 +400,7 @@ async function runSide(client: ViceSessionClient, scenario: Scenario, side: Side
|
|
|
407
400
|
const { side: _side, ...action } = step;
|
|
408
401
|
const notReached = await runActionStep(client, action as ActionStep, at);
|
|
409
402
|
if (notReached !== undefined && "runUntil" in step) {
|
|
410
|
-
throw new Inconclusive(`the ${side} side did not reach the checkpoint before ${step.timeoutFrames} frames (it stopped by ${notReached.stopReason} at ${formatC64Address(notReached.pc)})`);
|
|
403
|
+
throw new Inconclusive(`the ${side} side did not reach the checkpoint before ${step.runUntil.timeoutFrames} frames (it stopped by ${notReached.stopReason} at ${formatC64Address(notReached.pc)})`);
|
|
411
404
|
}
|
|
412
405
|
}
|
|
413
406
|
}
|
|
@@ -443,11 +436,10 @@ export function compare(original: Map<string, Observation>, rebuild: Map<string,
|
|
|
443
436
|
* Writes the files of a FAIL into `out`: for a screen difference both
|
|
444
437
|
* screens and the difference image, and memory.json with each differing byte.
|
|
445
438
|
*/
|
|
446
|
-
function writeFailFiles(out: string, original: Map<string, Observation>, rebuild: Map<string, Observation>, differences: Difference[]):
|
|
447
|
-
const files:
|
|
439
|
+
function writeFailFiles(out: string, original: Map<string, Observation>, rebuild: Map<string, Observation>, differences: Difference[]): Written[] {
|
|
440
|
+
const files: Written[] = [];
|
|
448
441
|
const write = (name: string, bytes: Uint8Array) => {
|
|
449
|
-
|
|
450
|
-
files.push(name);
|
|
442
|
+
files.push(writeReported(`${out}/${name}`, bytes, { what: "--out" }));
|
|
451
443
|
};
|
|
452
444
|
const memory: Array<{ checkpoint: string; what: string; bytes: Array<{ address: string; original: string; rebuild: string }> }> = [];
|
|
453
445
|
const hex = (byte: number) => byte.toString(16).padStart(2, "0");
|
|
@@ -479,7 +471,7 @@ export async function runScenario(client: ViceSessionClient, scenario: Scenario,
|
|
|
479
471
|
const observed = new Map<Side, Map<string, Observation>>();
|
|
480
472
|
for (const side of SIDES) {
|
|
481
473
|
try {
|
|
482
|
-
observed.set(side, await runSide(client, scenario, side,
|
|
474
|
+
observed.set(side, await runSide(client, scenario, side, namesOf(scenario[side].symbols), options.out !== undefined));
|
|
483
475
|
} catch (error) {
|
|
484
476
|
if (error instanceof Inconclusive) return { result: "INCONCLUSIVE", scenario: scenario.name, reason: error.message, checkpoints, differences: [] };
|
|
485
477
|
if (error instanceof WireFailure) {
|
|
@@ -491,7 +483,7 @@ export async function runScenario(client: ViceSessionClient, scenario: Scenario,
|
|
|
491
483
|
const original = observed.get("original")!;
|
|
492
484
|
const rebuild = observed.get("rebuild")!;
|
|
493
485
|
const differences = compare(original, rebuild);
|
|
494
|
-
const excluded = [...original].flatMap(([checkpoint, observation]) => observation.memory.flatMap((item) => item.excluded.map((excluded) => ({ checkpoint,
|
|
486
|
+
const excluded = [...original].flatMap(([checkpoint, observation]) => observation.memory.flatMap((item) => item.excluded.map((excluded) => ({ checkpoint, address: excluded.address, reason: excluded.reason }))));
|
|
495
487
|
const files = differences.length > 0 && options.out !== undefined ? writeFailFiles(options.out, original, rebuild, differences) : [];
|
|
496
488
|
return {
|
|
497
489
|
result: differences.length === 0 ? "PASS" : "FAIL",
|