@henols/c64-re-tools 2.1.0-rc.1 → 2.1.0
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/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 +33 -24
- package/skills/c64-provenance/scripts/lib/compare.ts +8 -12
- package/skills/c64-provenance/scripts/lib/leftovers.ts +19 -20
- 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 +177 -279
- package/skills/c64-reverse-engineering/SKILL.md +13 -12
- 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 +48 -32
- package/skills/c64-testing/references/recording-format.md +48 -19
- 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 +244 -54
- package/skills/c64-testing/scripts/lib/recording.ts +153 -120
- package/skills/c64-testing/scripts/{replay.ts → lib/replay.ts} +28 -86
- 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 +1 -1
- 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 +2 -2
- 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/record.ts +0 -96
- package/skills/c64-testing/scripts/test.ts +0 -65
|
@@ -1,81 +1,41 @@
|
|
|
1
|
-
// The c64-testing
|
|
1
|
+
// The replay of c64-testing: replays a recording in the replay core
|
|
2
2
|
// (c64run). First on the original program, which must end with the recorded
|
|
3
3
|
// state hash. Then, with --rebuild, on the rebuild: it compares the state at
|
|
4
4
|
// each checkpoint with the original's. --checkpoint and --state choose the
|
|
5
5
|
// comparison, else the record file gave it.
|
|
6
|
-
// Exit status: 0 PASS, 1 FAIL, 3 INCONCLUSIVE, 2 a bad call.
|
|
7
6
|
|
|
8
7
|
import { createHash } from "node:crypto";
|
|
9
|
-
import { parseArgs } from "node:util";
|
|
10
8
|
|
|
11
|
-
import { readProjectFile,
|
|
9
|
+
import { readProjectFile, writeReported, type Written } from "#src/host-client/transfer.ts";
|
|
12
10
|
import { C64RunSession } from "#src/native/c64run.ts";
|
|
13
11
|
import { localToolContext } from "#src/native/local.ts";
|
|
14
12
|
import { Workspace } from "#src/native/staging.ts";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import { ScenarioError } from "./lib/scenario.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";
|
|
19
16
|
|
|
20
|
-
const USAGE = `replay.ts <recording folder> [--checkpoint <routine> --state <item> ... [--pin <item> ...]] [--rebuild <prg> [--symbols <file>]] [--out <dir>]
|
|
21
|
-
|
|
22
|
-
<recording folder> is a folder that record.ts wrote, relative to the project
|
|
23
|
-
folder. --checkpoint is the name or $ address of a routine; each entry is a
|
|
24
|
-
checkpoint. --state is a name or $ address with :size, one for each item.
|
|
25
|
-
--pin is random state in RAM in the same form: at each checkpoint the rebuild
|
|
26
|
-
gets the original's values there.
|
|
27
|
-
Without them, the replay uses the comparison of the record file. --symbols
|
|
28
|
-
gives the names of the rebuild: the c64-assembler output (default: the
|
|
29
|
-
symbols of the recording). A recording without names takes the names of the
|
|
30
|
-
knowledge for the original. A $ address is an address of the original: on the
|
|
31
|
-
rebuild it moves with the name before it. With --out, a FAIL writes the RAM
|
|
32
|
-
of both sides at the first different checkpoint into <dir>. The result is one
|
|
33
|
-
JSON object.`;
|
|
34
|
-
|
|
35
|
-
const EXIT = { PASS: 0, FAIL: 1, INCONCLUSIVE: 3 } as const;
|
|
36
17
|
const MAX_STATE_ITEMS = 16;
|
|
37
18
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
help: { type: "boolean" },
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
if (values.help) throw new UsageError("");
|
|
54
|
-
if (positionals.length !== 1) throw new UsageError("give one recording folder");
|
|
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> {
|
|
55
31
|
if (values.symbols !== undefined && values.rebuild === undefined) throw new UsageError("--symbols is for the rebuild: give --rebuild too");
|
|
56
|
-
if ((values.checkpoint === undefined) !== (values.state ===
|
|
57
|
-
if (
|
|
58
|
-
if (values.pin
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
throw new UsageError(`--out must be a path relative to the project folder that stays inside it, not ${values.out}`);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
const folder = positionals[0]!.replace(/\/+$/, "").replace(/\/recording\.json$/, "");
|
|
67
|
-
let recording;
|
|
68
|
-
let given: Comparison | undefined;
|
|
69
|
-
try {
|
|
70
|
-
recording = readRecording(folder);
|
|
71
|
-
given =
|
|
72
|
-
values.checkpoint === undefined
|
|
73
|
-
? undefined
|
|
74
|
-
: { checkpoint: values.checkpoint, state: values.state!.map(parseStateItem), ...(values.pin === undefined ? {} : { pin: values.pin.map(parseStateItem) }) };
|
|
75
|
-
} catch (error) {
|
|
76
|
-
if (error instanceof ScenarioError) throw new UsageError(error.message);
|
|
77
|
-
throw error;
|
|
78
|
-
}
|
|
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) }) };
|
|
79
39
|
const compare = given ?? recording.compare;
|
|
80
40
|
if (values.rebuild !== undefined && compare === undefined) throw new UsageError("the recording has no checkpoint and state: give --checkpoint and --state");
|
|
81
41
|
const result = (outcome: ReplayResult["result"], side: ReplayResult["side"], fields: Partial<ReplayResult> = {}): ReplayResult => ({
|
|
@@ -97,14 +57,7 @@ async function run(argv: string[]): Promise<ReplayResult> {
|
|
|
97
57
|
// A recording made without names takes the names that the knowledge has now.
|
|
98
58
|
const originalSymbols = recording.symbols === "none" ? "knowledge" : recording.symbols;
|
|
99
59
|
const original: ReplaySubject = { side: "original", program: recording.program, symbols: originalSymbols };
|
|
100
|
-
const side =
|
|
101
|
-
try {
|
|
102
|
-
return await replaySide(core, recording, folder, subject, workspace, options);
|
|
103
|
-
} catch (error) {
|
|
104
|
-
if (error instanceof ScenarioError) throw new UsageError(error.message);
|
|
105
|
-
throw error;
|
|
106
|
-
}
|
|
107
|
-
};
|
|
60
|
+
const side = (subject: ReplaySubject, options: Parameters<typeof replaySide>[5]) => replaySide(core, recording, folder, subject, workspace, options);
|
|
108
61
|
// The self-check: the original ends the session with the recorded state, to the cycle.
|
|
109
62
|
const self = await side(original, { ...(compare === undefined ? {} : { compare }) });
|
|
110
63
|
if (self.problem !== undefined) return result("INCONCLUSIVE", "original", { reason: `the recording does not reproduce on the original: ${self.problem}` });
|
|
@@ -121,7 +74,7 @@ async function run(argv: string[]): Promise<ReplayResult> {
|
|
|
121
74
|
const rebuild: ReplaySubject = { side: "rebuild", program: values.rebuild, symbols: values.symbols ?? originalSymbols };
|
|
122
75
|
let movedSites: number;
|
|
123
76
|
try {
|
|
124
|
-
movedSites = siteMap(recording.sites,
|
|
77
|
+
movedSites = siteMap(recording.sites, namesOf(originalSymbols, "code"), namesOf(rebuild.symbols, "code")).length;
|
|
125
78
|
} catch (error) {
|
|
126
79
|
throw new UsageError(`--symbols ${rebuild.symbols}: ${(error as Error).message}`);
|
|
127
80
|
}
|
|
@@ -137,12 +90,10 @@ async function run(argv: string[]): Promise<ReplayResult> {
|
|
|
137
90
|
: `the rebuild came to ${replayed.samples.length} of ${expected.length} checkpoints${replayed.problem === undefined ? "" : `: ${replayed.problem}`}`;
|
|
138
91
|
const failed = result("FAIL", "rebuild", { ...base, compared: (difference?.checkpoint ?? replayed.samples.length + 1) - 1, reason, ...(difference === undefined ? {} : { firstDifference: difference }) });
|
|
139
92
|
if (values.out !== undefined && difference !== undefined) {
|
|
140
|
-
const files:
|
|
93
|
+
const files: Written[] = [];
|
|
141
94
|
for (const subject of [original, rebuild]) {
|
|
142
95
|
const dumped = await side(subject, { compare: compare!, dump: difference.checkpoint, ...(subject.side === "rebuild" ? { pins } : {}) });
|
|
143
|
-
|
|
144
|
-
writeProjectFile(`${values.out}/${name}`, dumped.ram!, { what: "--out" });
|
|
145
|
-
files.push(name);
|
|
96
|
+
files.push(writeReported(`${values.out}/${subject.side}-${difference.checkpoint}.bin`, dumped.ram!, { what: "--out" }));
|
|
146
97
|
}
|
|
147
98
|
failed.files = files;
|
|
148
99
|
}
|
|
@@ -152,12 +103,3 @@ async function run(argv: string[]): Promise<ReplayResult> {
|
|
|
152
103
|
workspace.remove();
|
|
153
104
|
}
|
|
154
105
|
}
|
|
155
|
-
|
|
156
|
-
await runScript(
|
|
157
|
-
async (argv) => {
|
|
158
|
-
const result = await run(argv);
|
|
159
|
-
process.exitCode = EXIT[result.result];
|
|
160
|
-
return result;
|
|
161
|
-
},
|
|
162
|
-
{ usage: USAGE },
|
|
163
|
-
);
|
|
@@ -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
|
-
);
|