@henols/c64-re-tools 2.0.0-rc.2 → 2.0.0-rc.21
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 +28 -62
- package/distribution/package.ts +1 -16
- package/distribution/plugin.ts +5 -3
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +25 -6
- package/skills/c64-assembler/scripts/assemble.ts +45 -10
- package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
- package/skills/c64-basic/SKILL.md +15 -7
- package/skills/c64-basic/scripts/basic.ts +4 -4
- package/skills/c64-disk/SKILL.md +34 -11
- package/skills/c64-disk/scripts/disk.ts +65 -10
- package/skills/c64-disk/scripts/lib/containers.ts +124 -0
- package/skills/c64-docs/SKILL.md +138 -0
- package/skills/c64-docs/references/page-types.md +141 -0
- package/skills/c64-docs/references/style.md +43 -0
- package/skills/c64-docs/scripts/generate.ts +102 -0
- package/skills/c64-docs/scripts/lib/links.ts +155 -0
- package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
- package/skills/c64-docs/scripts/lib/reference.ts +406 -0
- package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
- package/skills/c64-docs/scripts/link.ts +207 -0
- package/skills/c64-emulator/SKILL.md +29 -8
- package/skills/c64-knowledge/SKILL.md +201 -15
- package/skills/c64-knowledge/scripts/knowledge.ts +927 -78
- package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
- package/skills/c64-memory-map/SKILL.md +14 -2
- package/skills/c64-memory-map/scripts/memmap.ts +15 -4
- package/skills/c64-provenance/SKILL.md +100 -11
- package/skills/c64-provenance/scripts/lib/compare.ts +152 -0
- package/skills/c64-provenance/scripts/lib/library.ts +584 -0
- package/skills/c64-provenance/scripts/lib/runtime.ts +191 -0
- package/skills/c64-provenance/scripts/lib/separate.ts +140 -0
- package/skills/c64-provenance/scripts/provenance.ts +477 -24
- package/skills/c64-reverse-engineering/SKILL.md +90 -11
- package/skills/c64-source/SKILL.md +174 -0
- package/skills/c64-source/scripts/generate.ts +788 -0
- package/skills/c64-source/scripts/lib/loader.ts +630 -0
- package/skills/c64-source/scripts/lib/source.ts +1121 -0
- package/skills/c64-static-analysis/SKILL.md +53 -13
- package/skills/c64-static-analysis/scripts/analyze.ts +260 -31
- package/skills/c64-static-analysis/scripts/lib/coverage.ts +72 -0
- package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +11 -5
- package/skills/c64-static-analysis/scripts/lib/idioms.ts +158 -0
- package/skills/c64-static-analysis/scripts/lib/instructions.ts +105 -0
- package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
- package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
- package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
- package/skills/c64-testing/SKILL.md +57 -4
- package/skills/c64-testing/references/scenario-format.md +50 -6
- package/skills/c64-testing/scripts/checklist.ts +3 -3
- package/skills/c64-testing/scripts/diff.ts +167 -0
- package/skills/c64-testing/scripts/lib/diff.ts +70 -0
- package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +183 -152
- package/skills/c64-testing/scripts/run.ts +241 -0
- package/skills/c64-testing/scripts/test.ts +20 -10
- package/skills/c64-unpacker/SKILL.md +88 -21
- package/skills/c64-unpacker/scripts/lib/evidence.ts +207 -0
- package/skills/c64-unpacker/scripts/lib/loads.ts +209 -0
- package/skills/c64-unpacker/scripts/lib/machine.ts +80 -0
- package/skills/c64-unpacker/scripts/lib/stages.ts +142 -0
- package/skills/c64-unpacker/scripts/unpack.ts +545 -61
- package/src/c64.ts +240 -12
- package/src/cli/cleanup.ts +1 -1
- package/src/cli/main.ts +14 -8
- package/src/cli/status.ts +105 -11
- package/src/graphics.ts +158 -0
- package/src/host/main.ts +57 -31
- package/src/host/server.ts +30 -47
- package/src/host/tools/c1541.ts +34 -5
- package/src/host/tools/index.ts +6 -2
- package/src/host/tools/petcat.ts +10 -3
- package/src/host/tools/status.ts +23 -6
- package/src/host/vice/adapter.ts +64 -20
- package/src/host/vice/chips.ts +11 -1
- package/src/host/vice/process.ts +19 -8
- package/src/host/vice/screen.ts +4 -44
- package/src/host/vice/session.ts +343 -60
- package/src/host-client/autostart.ts +55 -0
- package/src/host-client/connect.ts +14 -14
- package/src/host-client/coverage.ts +36 -0
- package/src/host-client/image.ts +63 -0
- package/src/host-client/steps.ts +175 -0
- package/src/host-client/tools.ts +7 -6
- package/src/host-client/transfer.ts +60 -10
- package/src/host-client/vice-session.ts +107 -28
- package/src/knowledge/conflicts.ts +161 -0
- package/src/knowledge/database.ts +150 -38
- package/src/knowledge/expression.ts +209 -0
- package/src/knowledge/graph.ts +104 -0
- package/src/knowledge/history.ts +105 -27
- package/src/knowledge/import.ts +141 -85
- package/src/knowledge/join.ts +211 -0
- package/src/knowledge/program.ts +258 -0
- package/src/knowledge/read.ts +445 -63
- package/src/knowledge/schema.ts +277 -65
- package/src/knowledge/text.ts +415 -0
- package/src/knowledge/write.ts +1145 -150
- package/src/mcp/main.ts +44 -19
- package/src/mcp/output.ts +39 -0
- package/src/mcp/server.ts +15 -4
- package/src/mcp/tools/debug.ts +202 -31
- package/src/mcp/tools/execution.ts +39 -5
- package/src/mcp/tools/input.ts +7 -4
- package/src/mcp/tools/machine.ts +14 -2
- package/src/mcp/tools/media.ts +44 -16
- package/src/mcp/tools/memory.ts +68 -12
- package/src/mcp/tools/video.ts +20 -9
- package/src/native/discover.ts +44 -6
- package/src/native/ghidra/analyze.ts +2 -2
- package/src/native/ghidra/index.ts +19 -3
- package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
- package/src/native/ghidra/scripts/C64Export.java +33 -1
- package/src/native/ghidra/scripts/C64Prepare.java +7 -1
- package/src/native/status.ts +15 -3
- package/src/native/types.ts +43 -8
- package/src/opcodes.ts +133 -0
- package/src/package.ts +21 -0
- package/{skills/c64-memory-map/scripts → src}/platform.ts +87 -7
- package/src/project.ts +166 -3
- package/src/protocol/messages.ts +17 -10
- package/src/protocol/tools.ts +27 -22
- package/src/protocol/vice.ts +255 -33
- package/src/script.ts +49 -9
- package/src/trace.ts +55 -23
- package/skills/c64-provenance/scripts/compare.ts +0 -92
- package/skills/c64-unpacker/scripts/evidence.ts +0 -135
|
@@ -3,33 +3,64 @@
|
|
|
3
3
|
// wrote and then executed: the run-time evidence that also finds a simple
|
|
4
4
|
// cruncher whose bytes look like code. "capture" runs the program in an
|
|
5
5
|
// emulator until it reaches a given address (the unpacked entry point) and
|
|
6
|
-
// writes the memory into the project.
|
|
6
|
+
// writes the memory into the project. "loads" captures each part of a
|
|
7
|
+
// multi-load program (D53). No native unpack tool is used.
|
|
7
8
|
|
|
8
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
9
|
-
import { dirname } from "node:path";
|
|
10
9
|
import { parseArgs } from "node:util";
|
|
11
10
|
|
|
12
|
-
import { formatC64Address, parseC64Address,
|
|
11
|
+
import { COVERAGE, EXECUTE, formatC64Address, parseC64Address, READ, WRITE } from "#src/c64.ts";
|
|
12
|
+
import { autostartLoaded, LOAD_LIMIT_FRAMES, RUN_LIMIT_FRAMES, typeRun } from "#src/host-client/autostart.ts";
|
|
13
|
+
import { readCoverage, writtenBlocks } from "#src/host-client/coverage.ts";
|
|
14
|
+
import { checkActionSteps, runActionStep, type ActionStep } from "#src/host-client/steps.ts";
|
|
13
15
|
import { WireFailure } from "#src/host-client/tools.ts";
|
|
14
|
-
import { readProjectFile } from "#src/host-client/transfer.ts";
|
|
16
|
+
import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
|
|
15
17
|
import { ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
16
18
|
import { resolveProjectPath } from "#src/project.ts";
|
|
17
19
|
import { runScript, UsageError } from "#src/script.ts";
|
|
18
|
-
import { inspect, PackingError, readMemoryMap, underRom, writtenThenExecuted } from "./evidence.ts";
|
|
20
|
+
import { basicStart, inspect, PackingError, readMemoryMap, underRom, writtenThenExecuted } from "./lib/evidence.ts";
|
|
21
|
+
import { loads, readLoadSteps } from "./lib/loads.ts";
|
|
22
|
+
import { callReturn, captureSegments, executedIn, inRanges, mergeMaps, stageCandidates, stagePoints, withoutRanges, type Range } from "./lib/stages.ts";
|
|
23
|
+
import { entryState, IO_AREAS, json, machineState, readRam, READY_FRAMES, vectorsOf } from "./lib/machine.ts";
|
|
19
24
|
|
|
20
25
|
const USAGE = `unpack.ts inspect <program.prg>
|
|
21
|
-
unpack.ts trace <program.prg> [--frames <n>] [--entry <address>]
|
|
22
|
-
unpack.ts capture <program.prg> --until <address> --out <
|
|
26
|
+
unpack.ts trace <program.prg> [--frames <n>] [--entry <address>] [--steps <file>]
|
|
27
|
+
unpack.ts capture <program.prg> --until <address> --out <dir> [--range <start> <end>] [--exclude <start>-<end>]... [--timeout-frames <n>] [--entry <address>] [--steps <file>] [--min-stage <bytes>]
|
|
28
|
+
unpack.ts stages <program.prg> [--frames <n>] [--entry <address>] [--steps <file>] [--min-stage <bytes>]
|
|
29
|
+
unpack.ts loads <program> --steps <file> --out <dir> [--drive] [--min-block <bytes>]
|
|
23
30
|
|
|
24
31
|
inspect packing evidence: the BASIC start and the entropy of the bytes
|
|
25
32
|
trace run the program for <n> frames (default 3000) and report the memory
|
|
26
33
|
that it wrote and then executed; --entry starts it there instead of RUN
|
|
27
|
-
capture run the program until the CPU gets to <address
|
|
28
|
-
|
|
34
|
+
capture run the program until the CPU gets to <address> after the steps, and write into the
|
|
35
|
+
directory --out: image.bin (64 KiB of RAM), image.json (the entry,
|
|
36
|
+
the segments that the stages wrote, the vectors, the stages), a PRG
|
|
37
|
+
for each segment, and entry_state.json (registers, $00/$01, pages
|
|
38
|
+
0-3, VIC, SID and CIA registers, vectors, and what differs from a
|
|
39
|
+
clean reset). --range gives one segment in place of the found ones.
|
|
40
|
+
--exclude (more than once) leaves a range out of the segments, for
|
|
41
|
+
example bytes that a crack left and the program never reads.
|
|
42
|
+
--entry starts it there instead of RUN.
|
|
43
|
+
|
|
44
|
+
stages run the program for <n> frames (default 3000) and report each stage:
|
|
45
|
+
where it starts, the memory it wrote, and where the next one starts.
|
|
46
|
+
A stage starts when the CPU executes a block of at least
|
|
47
|
+
--min-stage bytes (default 64) that the stage before wrote.
|
|
48
|
+
|
|
49
|
+
loads run a multi-load program (a PRG, or a disk, tape or cartridge image)
|
|
50
|
+
with the steps file; each capture step {"capture": "<name>"} writes
|
|
51
|
+
<dir>/<name>/ with image.bin, image.json, entry_state.json and a PRG
|
|
52
|
+
for each block of at least --min-block bytes (default 64) that the C64
|
|
53
|
+
side changed since the previous capture. --drive also writes the 2 KiB
|
|
54
|
+
drive RAM as drive8.prg, for knowledge space drive8.
|
|
55
|
+
|
|
56
|
+
--steps <file> a JSON list of input steps that run right after the program
|
|
57
|
+
starts: write, registers, joystick, type, frames and runUntil, as in
|
|
58
|
+
c64-testing scenarios, with $ addresses only. Use it to press a key
|
|
59
|
+
in an intro or to get through a menu. stages and capture track the
|
|
60
|
+
stages in frames steps and in runUntil steps with at alone.
|
|
29
61
|
|
|
30
62
|
Addresses are $ and four hex digits. The result is one JSON object.`;
|
|
31
63
|
|
|
32
|
-
const MAX_READ = 4096;
|
|
33
64
|
|
|
34
65
|
function address(text: string, option: string): number {
|
|
35
66
|
try {
|
|
@@ -41,32 +72,46 @@ function address(text: string, option: string): number {
|
|
|
41
72
|
|
|
42
73
|
/** Written-then-executed bytes from this many on are unpacking, decrypting or relocating, not self-changing code. */
|
|
43
74
|
const UNPACKED_BYTES = 256;
|
|
44
|
-
const LOAD_POLL_FRAMES = 25;
|
|
45
|
-
const LOAD_LIMIT_FRAMES = 1500;
|
|
46
75
|
const MAX_ADVANCE = 10_000;
|
|
47
76
|
/** The most ranges one memory map read gives. */
|
|
48
77
|
const MAX_RANGES = 1000;
|
|
49
78
|
|
|
50
|
-
|
|
79
|
+
/** Reads and checks a steps file: action steps with $ addresses only (D43). */
|
|
80
|
+
function readSteps(path: string): ActionStep[] {
|
|
81
|
+
let value: unknown;
|
|
82
|
+
try {
|
|
83
|
+
value = JSON.parse(readProjectFile(path).bytes.toString("utf8"));
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (error instanceof WireFailure) throw error;
|
|
86
|
+
throw new UsageError(`--steps ${path} is not JSON: ${(error as Error).message}`);
|
|
87
|
+
}
|
|
88
|
+
return checkActionSteps(value, (message) => {
|
|
89
|
+
throw new UsageError(`--steps ${path}: ${message}`);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Runs the steps; the reason when a runUntil step did not reach its target. */
|
|
94
|
+
async function runSteps(client: ViceSessionClient, steps: ActionStep[]): Promise<string | undefined> {
|
|
95
|
+
for (const [index, step] of steps.entries()) {
|
|
96
|
+
const notReached = await runActionStep(client, step, parseC64Address);
|
|
97
|
+
if (notReached !== undefined && "runUntil" in step) {
|
|
98
|
+
return `Step ${index + 1} did not reach its target in ${step.timeoutFrames} frames. It stopped by ${notReached.stopReason} at ${formatC64Address(notReached.pc)}.`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function trace(program: string, options: { frames: number; entry?: number; steps: ActionStep[] }): Promise<{ output: Record<string, unknown>; failed: boolean }> {
|
|
51
105
|
const { bytes } = readProjectFile(program);
|
|
52
106
|
if (bytes.length < 3) throw new UsageError("the file is too short for a PRG");
|
|
53
|
-
|
|
54
|
-
const basic = inspect(bytes).basicStart;
|
|
55
|
-
if (options.entry === undefined && basic === null) throw new UsageError("the program has no BASIC start line; give --entry <address>");
|
|
107
|
+
if (options.entry === undefined && basicStart(bytes) === undefined) throw new UsageError(NO_BASIC);
|
|
56
108
|
const client = await ViceSessionClient.open({ videoStandard: "pal" });
|
|
57
109
|
try {
|
|
58
110
|
// Loaded but not started, so the memory map can start empty: the KERNAL start-up writes all of RAM.
|
|
59
|
-
await client
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
await client.execution({ action: "advance-frames", count: LOAD_POLL_FRAMES, space: "c64" });
|
|
64
|
-
loaded = (await client.memoryRead({ address: load, size: head.length / 2, space: "c64", view: "ram" })).data === head;
|
|
65
|
-
}
|
|
66
|
-
if (!loaded) return { output: { traced: false, reason: `The program was not in memory at ${formatC64Address(load)} after ${LOAD_LIMIT_FRAMES} frames.` }, failed: true };
|
|
67
|
-
await client.memmap({ action: "clear" });
|
|
68
|
-
if (options.entry !== undefined) await client.registersSet("c64", { pc: options.entry });
|
|
69
|
-
else await client.keyboard(textToPetscii("RUN\n"));
|
|
111
|
+
const notStarted = await startLoaded(client, program, bytes, options.entry);
|
|
112
|
+
if (notStarted !== undefined) return { output: { traced: false, reason: notStarted }, failed: true };
|
|
113
|
+
const stepFailed = await runSteps(client, options.steps);
|
|
114
|
+
if (stepFailed !== undefined) return { output: { traced: false, reason: stepFailed }, failed: true };
|
|
70
115
|
let ran = 0;
|
|
71
116
|
while (ran < options.frames) {
|
|
72
117
|
const step = await client.execution({ action: "advance-frames", count: Math.min(MAX_ADVANCE, options.frames - ran), space: "c64" });
|
|
@@ -75,12 +120,23 @@ async function trace(program: string, options: { frames: number; entry?: number
|
|
|
75
120
|
if (advanced === 0) break;
|
|
76
121
|
}
|
|
77
122
|
const { pc } = await client.registersGet("c64");
|
|
78
|
-
const ranges = await readMemoryMap(async (start
|
|
79
|
-
const map = await client.memmap({ action: "read", start, end, maxRanges: MAX_RANGES });
|
|
123
|
+
const ranges = await readMemoryMap(async (start) => {
|
|
124
|
+
const map = await client.memmap({ action: "read", start, end: 0xffff, maxRanges: MAX_RANGES });
|
|
80
125
|
if (!("ranges" in map)) throw new WireFailure("operation-failed", "The emulator did not give the memory map.");
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
126
|
+
const ranges = map.ranges.map(({ start: from, end, flags }) => ({ start: from, end, read: (flags & READ) !== 0, write: (flags & WRITE) !== 0, execute: (flags & EXECUTE) !== 0 }));
|
|
127
|
+
return map.next === undefined ? { ranges } : { ranges, next: map.next };
|
|
128
|
+
});
|
|
129
|
+
const { written, counted, bytes: count, running: atStop } = writtenThenExecuted(ranges, pc);
|
|
130
|
+
// The run often ends in a KERNAL interrupt: the written code that runs next gives the range and the point after the unpacking.
|
|
131
|
+
let running = atStop;
|
|
132
|
+
let until = pc;
|
|
133
|
+
if (running === undefined && counted.length > 0) {
|
|
134
|
+
const next = await firstPcIn(client, counted);
|
|
135
|
+
if (next !== undefined) {
|
|
136
|
+
running = counted.find((range) => next >= range.start && next <= range.end);
|
|
137
|
+
until = next;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
84
140
|
const packing = count >= UNPACKED_BYTES || running !== undefined ? "likely" : count > 0 ? "unclear" : "unlikely";
|
|
85
141
|
return {
|
|
86
142
|
output: {
|
|
@@ -97,13 +153,339 @@ async function trace(program: string, options: { frames: number; entry?: number
|
|
|
97
153
|
evidence:
|
|
98
154
|
packing === "likely"
|
|
99
155
|
? running !== undefined
|
|
100
|
-
? `The CPU
|
|
156
|
+
? `The CPU runs code at ${formatC64Address(until)} that the program wrote: it unpacked, decrypted or relocated it.`
|
|
101
157
|
: `The program wrote ${count} bytes and then executed them: it unpacked, decrypted or relocated code.`
|
|
102
158
|
: packing === "unclear"
|
|
103
159
|
? `The program executed ${count} bytes that it wrote. This can be code that changes itself or a small loader.`
|
|
104
160
|
: "The program executed no code that it wrote while it ran.",
|
|
105
161
|
// A point after the unpacking where the unpacked code runs; the entry point can be at the start of its range.
|
|
106
|
-
...(running === undefined ? {} : { suggestedUntil: formatC64Address(
|
|
162
|
+
...(running === undefined ? {} : { suggestedUntil: formatC64Address(until), runningRange: { start: formatC64Address(running.start), end: formatC64Address(running.end) } }),
|
|
163
|
+
},
|
|
164
|
+
failed: false,
|
|
165
|
+
};
|
|
166
|
+
} finally {
|
|
167
|
+
await client.close();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const NO_BASIC = "the program is no BASIC program at $0801, so RUN cannot start it. Give --entry <address>.";
|
|
172
|
+
|
|
173
|
+
/** The first PC in one of the ranges within SYS_FRAMES frames, found with exec breakpoints over them. */
|
|
174
|
+
async function firstPcIn(client: ViceSessionClient, ranges: Array<{ start: number; end: number }>): Promise<number | undefined> {
|
|
175
|
+
const points = [];
|
|
176
|
+
for (const range of ranges) points.push(await client.breakpoint({ action: "add", address: range.start, size: range.end - range.start + 1, space: "c64" }));
|
|
177
|
+
try {
|
|
178
|
+
const stop = await client.execution({ action: "advance-frames", count: SYS_FRAMES, space: "c64" });
|
|
179
|
+
const pc = stop.pc ?? (await client.registersGet("c64")).pc;
|
|
180
|
+
return ranges.some((range) => pc >= range.start && pc <= range.end) ? pc : undefined;
|
|
181
|
+
} finally {
|
|
182
|
+
for (const point of points) await client.breakpoint({ action: "remove", id: point.id });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Frames that RUN gets to reach the machine code of a SYS whose argument needs run-time values. */
|
|
187
|
+
const SYS_FRAMES = 50;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The first PC in the program after its BASIC text, for a SYS whose address
|
|
191
|
+
* BASIC computes: an exec breakpoint over that part of the program stops there.
|
|
192
|
+
*/
|
|
193
|
+
/**
|
|
194
|
+
* Starts the program as a c64-testing scenario does (D62): hard reset, load,
|
|
195
|
+
* an empty memory map, then RUN up to the first BASIC line, or the pc at
|
|
196
|
+
* `entry`. Gives why it did not start.
|
|
197
|
+
*/
|
|
198
|
+
async function startLoaded(client: ViceSessionClient, program: string, bytes: Uint8Array, entry: number | undefined): Promise<string | undefined> {
|
|
199
|
+
await client.reset({ mode: "hard", run: false });
|
|
200
|
+
if (!(await autostartLoaded(client, program, bytes))) return `The program was not in memory at ${formatC64Address(bytes[0]! | (bytes[1]! << 8))} after ${LOAD_LIMIT_FRAMES} frames.`;
|
|
201
|
+
await client.memmap({ action: "clear" });
|
|
202
|
+
if (entry !== undefined) await client.registersSet("c64", { pc: entry });
|
|
203
|
+
else if (!(await typeRun(client))) return `After RUN, BASIC ran no program line in ${RUN_LIMIT_FRAMES} frames.`;
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** The raster position now: where a step's frames end (D62). */
|
|
208
|
+
async function rasterAt(client: ViceSessionClient): Promise<{ line: number; cycle: number }> {
|
|
209
|
+
const { timing } = await client.observe({ timing: true });
|
|
210
|
+
return { line: timing!.rasterLine, cycle: timing!.rasterCycle };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function firstPcAfterBasic(client: ViceSessionClient, from: number, to: number): Promise<number | undefined> {
|
|
214
|
+
return from > to ? undefined : firstPcIn(client, [{ start: from, end: to }]);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Frames between two looks at the memory map while stages are followed. */
|
|
218
|
+
const STAGE_CHUNK_FRAMES = 25;
|
|
219
|
+
const MAX_STAGES = 16;
|
|
220
|
+
/** Most stops of one re-run of a chunk: own code and writes over it each stop once. */
|
|
221
|
+
const MAX_STAGE_STOPS = 4096;
|
|
222
|
+
const STAGE_SNAPSHOT = "stage-chunk";
|
|
223
|
+
/** The snapshot at a call into written code, while the run checks if the call returns (D59). */
|
|
224
|
+
const CALL_SNAPSHOT = "stage-call";
|
|
225
|
+
|
|
226
|
+
interface Stage {
|
|
227
|
+
entry: number;
|
|
228
|
+
firstFrame: number;
|
|
229
|
+
frames: number;
|
|
230
|
+
wrote: Array<{ start: number; end: number }>;
|
|
231
|
+
next?: number;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
interface Followed {
|
|
235
|
+
stages: Stage[];
|
|
236
|
+
frames: number;
|
|
237
|
+
/** True when the CPU got to `until`. */
|
|
238
|
+
reached: boolean;
|
|
239
|
+
/** Why the program did not start, when it did not. */
|
|
240
|
+
failed?: string;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** A runUntil step to an address alone: a breakpoint stops it, so the stages are tracked while it runs. */
|
|
244
|
+
function trackedRunUntil(step: ActionStep): step is { runUntil: { at: string }; timeoutFrames: number } {
|
|
245
|
+
return "runUntil" in step && "at" in step.runUntil && !("raster" in step.runUntil);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Starts the program and follows its stages (D43). The memory map holds what
|
|
250
|
+
* the current stage wrote; when the CPU executed a block of at least
|
|
251
|
+
* `minStage` written bytes during a chunk of frames, the chunk runs again from
|
|
252
|
+
* its snapshot with exec breakpoints over those blocks, which stop at the
|
|
253
|
+
* first instruction of the next stage. Steps run in the tracked time: a frames
|
|
254
|
+
* step and a runUntil step to an address alone are tracked, the others act
|
|
255
|
+
* between two chunks. With `until`, the run ends when the CPU gets there
|
|
256
|
+
* after the steps.
|
|
257
|
+
*/
|
|
258
|
+
async function follow(
|
|
259
|
+
client: ViceSessionClient,
|
|
260
|
+
program: string,
|
|
261
|
+
options: { frames: number; entry?: number; steps: ActionStep[]; minStage: number; until?: number },
|
|
262
|
+
): Promise<Followed> {
|
|
263
|
+
const { bytes } = readProjectFile(program);
|
|
264
|
+
if (bytes.length < 3) throw new UsageError("the file is too short for a PRG");
|
|
265
|
+
const load = bytes[0]! | (bytes[1]! << 8);
|
|
266
|
+
const basic = basicStart(bytes);
|
|
267
|
+
if (options.entry === undefined && basic === undefined) throw new UsageError(NO_BASIC);
|
|
268
|
+
const notStarted = await startLoaded(client, program, bytes, options.entry);
|
|
269
|
+
if (notStarted !== undefined) return { stages: [], frames: 0, reached: false, failed: notStarted };
|
|
270
|
+
// The frames of each step end where a scenario's do: the first step counts from RUN, a later one from where it starts (D62).
|
|
271
|
+
let anchor: { line: number; cycle: number } | undefined = await rasterAt(client);
|
|
272
|
+
const framed = () => (anchor === undefined ? {} : { anchor });
|
|
273
|
+
const found: Stage[] = [];
|
|
274
|
+
const start = options.entry ?? basic!.sys?.address ?? (await firstPcAfterBasic(client, basic!.basicEnd, load + bytes.length - 3));
|
|
275
|
+
if (start === undefined) return { stages: [], frames: 0, reached: false, failed: `After RUN, the CPU did not execute the program after its BASIC text (${formatC64Address(basic!.basicEnd)} on) in ${SYS_FRAMES} frames. Give --entry <address>.` };
|
|
276
|
+
let entry = start;
|
|
277
|
+
let firstFrame = 0;
|
|
278
|
+
let total = 0;
|
|
279
|
+
let ended = false;
|
|
280
|
+
/** Where the run stops: the target of a runUntil step while it runs, then until (R2-06). */
|
|
281
|
+
let stopAt: number | undefined;
|
|
282
|
+
const atTarget = (pc: number) => stopAt !== undefined && pc === stopAt;
|
|
283
|
+
// The current stage: its memory map since it started, chunk by chunk, its own code, and the written code it calls (D59).
|
|
284
|
+
let stage = { map: new Uint8Array(0x10000), own: new Set<number>(), called: [] as Range[] };
|
|
285
|
+
const startStage = async (next: number, last: Uint8Array) => {
|
|
286
|
+
mergeMaps(stage.map, last);
|
|
287
|
+
found.push({ entry, firstFrame, frames: total - firstFrame, wrote: writtenBlocks(stage.map, options.minStage), next });
|
|
288
|
+
entry = next;
|
|
289
|
+
firstFrame = total;
|
|
290
|
+
stage = { map: new Uint8Array(0x10000), own: new Set<number>(), called: [] };
|
|
291
|
+
await client.memmap({ action: "clear" });
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Runs the chunk again from its snapshot with points over the candidate
|
|
296
|
+
* blocks, and stops at the first opcode that the stage wrote and the CPU
|
|
297
|
+
* then executes. Own code stops the run once; a write watch arms it again.
|
|
298
|
+
*/
|
|
299
|
+
const rerun = async (candidates: Range[], frames: number): Promise<{ used: number; map: Uint8Array; next?: number; atTarget: boolean }> => {
|
|
300
|
+
const points = new Map<string, { kind: "exec" | "watch"; id: number }>();
|
|
301
|
+
const arm = async (wanted: { exec: Range[]; watch: Range[] }) => {
|
|
302
|
+
const keys = new Set([...wanted.exec.map((range) => `exec:${range.start}:${range.end}`), ...wanted.watch.map((range) => `watch:${range.start}:${range.end}`)]);
|
|
303
|
+
for (const [key, point] of points) {
|
|
304
|
+
if (keys.has(key)) continue;
|
|
305
|
+
if (point.kind === "exec") await client.breakpoint({ action: "remove", id: point.id });
|
|
306
|
+
else await client.watchpoint({ action: "remove", id: point.id });
|
|
307
|
+
points.delete(key);
|
|
308
|
+
}
|
|
309
|
+
for (const range of wanted.exec) {
|
|
310
|
+
const key = `exec:${range.start}:${range.end}`;
|
|
311
|
+
if (!points.has(key)) points.set(key, { kind: "exec", id: (await client.breakpoint({ action: "add", address: range.start, size: range.end - range.start + 1, space: "c64" })).id });
|
|
312
|
+
}
|
|
313
|
+
for (const range of wanted.watch) {
|
|
314
|
+
const key = `watch:${range.start}:${range.end}`;
|
|
315
|
+
if (!points.has(key)) points.set(key, { kind: "watch", id: (await client.watchpoint({ action: "add", address: range.start, size: range.end - range.start + 1, access: "write", space: "c64" })).id });
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
/**
|
|
319
|
+
* At written code that a JSR of the stage called: runs on until the call
|
|
320
|
+
* returns, in the frames left. Gives the frames used when it returns;
|
|
321
|
+
* otherwise the machine goes back to the stop and nothing is given.
|
|
322
|
+
*/
|
|
323
|
+
const returns = async (map: Uint8Array, frames: number): Promise<number | undefined> => {
|
|
324
|
+
const { sp } = await client.registersGet("c64");
|
|
325
|
+
const stack = await readRam(client, 0x0100, 0x100);
|
|
326
|
+
const pushed = stack[(sp + 1) & 0xff]! | (stack[(sp + 2) & 0xff]! << 8);
|
|
327
|
+
const [opcode] = await readRam(client, (pushed - 2) & 0xffff, 1);
|
|
328
|
+
const back = callReturn(stack, sp, () => opcode!, (address) => ((stage.map[address]! | map[address]!) & COVERAGE.ramExecute) !== 0);
|
|
329
|
+
if (back === undefined) return undefined;
|
|
330
|
+
await arm({ exec: [], watch: [] });
|
|
331
|
+
await client.snapshot({ action: "save", name: CALL_SNAPSHOT });
|
|
332
|
+
const point = await client.breakpoint({ action: "add", address: back, space: "c64" });
|
|
333
|
+
try {
|
|
334
|
+
const run = await client.execution({ action: "advance-frames", count: frames, space: "c64", ...framed() });
|
|
335
|
+
if ((run.pc ?? (await client.registersGet("c64")).pc) === back) return run.advancedFrames ?? 0;
|
|
336
|
+
await client.snapshot({ action: "restore", name: CALL_SNAPSHOT });
|
|
337
|
+
return undefined;
|
|
338
|
+
} finally {
|
|
339
|
+
await client.breakpoint({ action: "remove", id: point.id });
|
|
340
|
+
await client.snapshot({ action: "discard", name: CALL_SNAPSHOT });
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
let left = frames;
|
|
344
|
+
let used = 0;
|
|
345
|
+
let map: Uint8Array = new Uint8Array(0x10000);
|
|
346
|
+
try {
|
|
347
|
+
for (let stops = 0; ; stops++) {
|
|
348
|
+
if (stops > MAX_STAGE_STOPS) {
|
|
349
|
+
throw new WireFailure("operation-failed", `The stage that starts at ${formatC64Address(entry)} stopped ${MAX_STAGE_STOPS} times in one chunk of frames without a new stage. Use --min-stage with a larger block size, or capture with --until.`);
|
|
350
|
+
}
|
|
351
|
+
const written = (address: number) => ((stage.map[address]! | map[address]!) & COVERAGE.ramWrite) !== 0;
|
|
352
|
+
await arm(stagePoints(candidates, stage.own, written, stage.called));
|
|
353
|
+
const run = await client.execution({ action: "advance-frames", count: left, space: "c64", ...framed() });
|
|
354
|
+
const advanced = run.advancedFrames ?? 0;
|
|
355
|
+
used += advanced;
|
|
356
|
+
left -= advanced;
|
|
357
|
+
const pc = run.pc ?? (await client.registersGet("c64")).pc;
|
|
358
|
+
map = await readCoverage(client);
|
|
359
|
+
// A frame can end in written code that the stage called before (D59): that stop starts no stage.
|
|
360
|
+
if (inRanges(candidates, pc) && !inRanges(stage.called, pc)) {
|
|
361
|
+
if (written(pc)) {
|
|
362
|
+
const back = left > 0 && !atTarget(pc) ? await returns(map, left) : undefined;
|
|
363
|
+
if (back === undefined) return { used, map, next: pc, atTarget: atTarget(pc) };
|
|
364
|
+
used += back;
|
|
365
|
+
left -= back;
|
|
366
|
+
map = await readCoverage(client);
|
|
367
|
+
// The stage called the block and got the CPU back: the code that ran there is its own code.
|
|
368
|
+
const block = candidates.find((range) => pc >= range.start && pc <= range.end)!;
|
|
369
|
+
stage.called.push(...executedIn(map, block));
|
|
370
|
+
if (left <= 0) return { used, map, atTarget: false };
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
stage.own.add(pc);
|
|
374
|
+
}
|
|
375
|
+
if (atTarget(pc)) return { used, map, atTarget: true };
|
|
376
|
+
if (left <= 0) return { used, map, atTarget: false };
|
|
377
|
+
}
|
|
378
|
+
} finally {
|
|
379
|
+
for (const point of points.values()) {
|
|
380
|
+
if (point.kind === "exec") await client.breakpoint({ action: "remove", id: point.id });
|
|
381
|
+
else await client.watchpoint({ action: "remove", id: point.id });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
/** Tracks the stages for some frames; true when the CPU got to stopAt. */
|
|
387
|
+
const track = async (frames: number): Promise<boolean> => {
|
|
388
|
+
anchor ??= await rasterAt(client);
|
|
389
|
+
try {
|
|
390
|
+
return await trackFrom(frames);
|
|
391
|
+
} finally {
|
|
392
|
+
anchor = undefined;
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
const trackFrom = async (frames: number): Promise<boolean> => {
|
|
396
|
+
let left = frames;
|
|
397
|
+
let stopped = false;
|
|
398
|
+
while (left > 0 && !ended && !stopped && found.length < MAX_STAGES) {
|
|
399
|
+
await client.snapshot({ action: "save", name: STAGE_SNAPSHOT });
|
|
400
|
+
await client.memmap({ action: "clear" });
|
|
401
|
+
const chunk = Math.min(STAGE_CHUNK_FRAMES, left);
|
|
402
|
+
const first = await client.execution({ action: "advance-frames", count: chunk, space: "c64", ...framed() });
|
|
403
|
+
const firstPc = first.pc ?? (await client.registersGet("c64")).pc;
|
|
404
|
+
const map = await readCoverage(client);
|
|
405
|
+
const candidates = stageCandidates(stage.map, map, stage.own, options.minStage, stage.called);
|
|
406
|
+
if (candidates.length === 0) {
|
|
407
|
+
const advanced = first.advancedFrames ?? 0;
|
|
408
|
+
total += advanced;
|
|
409
|
+
left -= advanced;
|
|
410
|
+
if (atTarget(firstPc)) {
|
|
411
|
+
// The stop at a target comes before its instruction runs: a target that the stage wrote starts a stage too.
|
|
412
|
+
const combined = Uint8Array.from(stage.map);
|
|
413
|
+
mergeMaps(combined, map);
|
|
414
|
+
const wrote = (combined[firstPc]! & COVERAGE.ramWrite) !== 0 && inRanges(writtenBlocks(combined, options.minStage), firstPc);
|
|
415
|
+
if (wrote) await startStage(firstPc, map);
|
|
416
|
+
else mergeMaps(stage.map, map);
|
|
417
|
+
stopped = true;
|
|
418
|
+
} else {
|
|
419
|
+
mergeMaps(stage.map, map);
|
|
420
|
+
if (advanced === 0) ended = true;
|
|
421
|
+
}
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
// Again from the start of the chunk, with points over the candidate blocks.
|
|
425
|
+
await client.snapshot({ action: "restore", name: STAGE_SNAPSHOT });
|
|
426
|
+
await client.memmap({ action: "clear" });
|
|
427
|
+
const again = await rerun(candidates, chunk);
|
|
428
|
+
total += again.used;
|
|
429
|
+
left -= again.used;
|
|
430
|
+
if (again.next !== undefined) await startStage(again.next, again.map);
|
|
431
|
+
else mergeMaps(stage.map, again.map);
|
|
432
|
+
if (again.atTarget) stopped = true;
|
|
433
|
+
}
|
|
434
|
+
return stopped;
|
|
435
|
+
};
|
|
436
|
+
/** Tracks the stages until the CPU gets to an address, with a breakpoint there. */
|
|
437
|
+
const trackTo = async (address: number, frames: number): Promise<boolean> => {
|
|
438
|
+
stopAt = address;
|
|
439
|
+
const point = await client.breakpoint({ action: "add", address, space: "c64" });
|
|
440
|
+
try {
|
|
441
|
+
return await track(frames);
|
|
442
|
+
} finally {
|
|
443
|
+
await client.breakpoint({ action: "remove", id: point.id });
|
|
444
|
+
stopAt = undefined;
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
// The steps run first and in the tracked time: a frames step and a runUntil step to an address (R2-06). The others act between two chunks.
|
|
448
|
+
for (const [index, step] of options.steps.entries()) {
|
|
449
|
+
if ("frames" in step) await track(step.frames);
|
|
450
|
+
else if (trackedRunUntil(step)) {
|
|
451
|
+
if (!(await trackTo(parseC64Address(step.runUntil.at), step.timeoutFrames))) {
|
|
452
|
+
const { pc } = await client.registersGet("c64");
|
|
453
|
+
return { stages: found, frames: total, reached: false, failed: `Step ${index + 1} did not reach its target in ${step.timeoutFrames} frames. It is at ${formatC64Address(pc)}.` };
|
|
454
|
+
}
|
|
455
|
+
} else await runActionStep(client, step, parseC64Address);
|
|
456
|
+
}
|
|
457
|
+
const left = Math.max(0, options.frames - total);
|
|
458
|
+
let reached = false;
|
|
459
|
+
if (options.until === undefined) await track(left);
|
|
460
|
+
else reached = await trackTo(options.until, left);
|
|
461
|
+
await client.snapshot({ action: "discard", name: STAGE_SNAPSHOT }).catch(() => undefined);
|
|
462
|
+
mergeMaps(stage.map, await readCoverage(client));
|
|
463
|
+
found.push({ entry, firstFrame, frames: total - firstFrame, wrote: writtenBlocks(stage.map, options.minStage) });
|
|
464
|
+
return { stages: found, frames: total, reached };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const showRange = (range: { start: number; end: number }) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) });
|
|
468
|
+
const showStages = (stages: Stage[]) =>
|
|
469
|
+
stages.map((stage, index) => ({
|
|
470
|
+
stage: index,
|
|
471
|
+
entry: formatC64Address(stage.entry),
|
|
472
|
+
frames: stage.frames,
|
|
473
|
+
wrote: stage.wrote.map(showRange),
|
|
474
|
+
...(stage.next === undefined ? {} : { next: formatC64Address(stage.next) }),
|
|
475
|
+
}));
|
|
476
|
+
|
|
477
|
+
async function stages(program: string, options: { frames: number; entry?: number; steps: ActionStep[]; minStage: number }): Promise<{ output: Record<string, unknown>; failed: boolean }> {
|
|
478
|
+
const client = await ViceSessionClient.open({ videoStandard: "pal" });
|
|
479
|
+
try {
|
|
480
|
+
const followed = await follow(client, program, options);
|
|
481
|
+
if (followed.failed !== undefined) return { output: { followed: false, reason: followed.failed }, failed: true };
|
|
482
|
+
return {
|
|
483
|
+
output: {
|
|
484
|
+
followed: true,
|
|
485
|
+
frames: followed.frames,
|
|
486
|
+
stages: showStages(followed.stages),
|
|
487
|
+
pc: formatC64Address((await client.registersGet("c64")).pc),
|
|
488
|
+
...(followed.stages.length > MAX_STAGES ? { moreStages: true } : {}),
|
|
107
489
|
},
|
|
108
490
|
failed: false,
|
|
109
491
|
};
|
|
@@ -112,37 +494,78 @@ async function trace(program: string, options: { frames: number; entry?: number
|
|
|
112
494
|
}
|
|
113
495
|
}
|
|
114
496
|
|
|
115
|
-
|
|
497
|
+
|
|
498
|
+
async function capture(
|
|
499
|
+
program: string,
|
|
500
|
+
options: { until: number; out: string; range?: [number, number]; exclude: Array<{ start: number; end: number }>; timeoutFrames: number; entry?: number; steps: ActionStep[]; minStage: number },
|
|
501
|
+
): Promise<{ output: Record<string, unknown>; failed: boolean }> {
|
|
116
502
|
const client = await ViceSessionClient.open({ videoStandard: "pal" });
|
|
117
503
|
try {
|
|
118
|
-
await client
|
|
119
|
-
|
|
120
|
-
|
|
504
|
+
const followed = await follow(client, program, { frames: options.timeoutFrames, steps: options.steps, minStage: options.minStage, until: options.until, ...(options.entry === undefined ? {} : { entry: options.entry }) });
|
|
505
|
+
if (followed.failed !== undefined) return { output: { captured: false, reason: followed.failed }, failed: true };
|
|
506
|
+
const registers = await client.registersGet("c64");
|
|
507
|
+
if (!followed.reached) {
|
|
121
508
|
return {
|
|
122
|
-
output: { captured: false, reason: `The program did not get to ${formatC64Address(options.until)} in ${options.timeoutFrames} frames. It
|
|
509
|
+
output: { captured: false, reason: `The program did not get to ${formatC64Address(options.until)} in ${options.timeoutFrames} frames. It is at ${formatC64Address(registers.pc)}.`, stages: showStages(followed.stages) },
|
|
123
510
|
failed: true,
|
|
124
511
|
};
|
|
125
512
|
}
|
|
126
|
-
|
|
127
|
-
const memory =
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
513
|
+
// The RAM view: unpacked code is often under the ROMs or the I/O area.
|
|
514
|
+
const memory = await readRam(client, 0x0000, 0x10000);
|
|
515
|
+
const state = await machineState(client);
|
|
516
|
+
// The segments: what the stages wrote, without loader leftovers, with no overlap.
|
|
517
|
+
const found = options.range !== undefined ? [{ start: options.range[0], end: options.range[1] }] : captureSegments(followed.stages, registers.pc);
|
|
518
|
+
const inside = options.exclude.find((range) => registers.pc >= range.start && registers.pc <= range.end);
|
|
519
|
+
if (inside !== undefined) {
|
|
520
|
+
return { output: { captured: false, reason: `The entry ${formatC64Address(registers.pc)} is in --exclude ${formatC64Address(inside.start)}-${formatC64Address(inside.end)}. A segment must hold the entry.` }, failed: true };
|
|
133
521
|
}
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
522
|
+
const segments = withoutRanges(found, options.exclude);
|
|
523
|
+
const file = (path: string) => `${options.out}/${path}`;
|
|
524
|
+
const write = (path: string, bytes: Uint8Array) => writeProjectFile(file(path), bytes, { what: "--out" });
|
|
525
|
+
write("image.bin", memory);
|
|
526
|
+
const files = segments.map((segment) => {
|
|
527
|
+
const name = `segment-${segment.start.toString(16).padStart(4, "0")}-${segment.end.toString(16).padStart(4, "0")}.prg`;
|
|
528
|
+
write(name, Buffer.concat([Buffer.from([segment.start & 0xff, segment.start >> 8]), memory.subarray(segment.start, segment.end + 1)]));
|
|
529
|
+
return name;
|
|
530
|
+
});
|
|
531
|
+
const vectors = vectorsOf(state, memory);
|
|
532
|
+
write(
|
|
533
|
+
"image.json",
|
|
534
|
+
Buffer.from(
|
|
535
|
+
`${JSON.stringify(
|
|
536
|
+
{
|
|
537
|
+
entry: formatC64Address(registers.pc),
|
|
538
|
+
segments: segments.map((segment, index) => ({ ...showRange(segment), ...("writtenBy" in segment ? { writtenBy: segment.writtenBy } : {}), file: files[index] })),
|
|
539
|
+
...(options.exclude.length === 0 ? {} : { excluded: options.exclude.map(showRange) }),
|
|
540
|
+
vectors,
|
|
541
|
+
stages: showStages(followed.stages),
|
|
542
|
+
image: "image.bin",
|
|
543
|
+
},
|
|
544
|
+
null,
|
|
545
|
+
2,
|
|
546
|
+
)}\n`,
|
|
547
|
+
),
|
|
548
|
+
);
|
|
549
|
+
// A clean machine at the READY prompt shows what the loader and the stages changed.
|
|
550
|
+
await client.reset({ mode: "hard", run: true });
|
|
551
|
+
await client.execution({ action: "advance-frames", count: READY_FRAMES, space: "c64" });
|
|
552
|
+
const clean = await machineState(client);
|
|
553
|
+
const differs: Array<{ address: string; entry: number; reset: number }> = [];
|
|
554
|
+
const compare = (base: number, entry: Buffer, reset: Buffer) => entry.forEach((value, offset) => value !== reset[offset] && differs.push({ address: formatC64Address(base + offset), entry: value, reset: reset[offset]! }));
|
|
555
|
+
compare(0x0000, state.port, clean.port);
|
|
556
|
+
compare(0x0002, state.pages.subarray(2), clean.pages.subarray(2));
|
|
557
|
+
for (const [name, [address]] of Object.entries(IO_AREAS)) compare(address, state.io[name as keyof typeof IO_AREAS], clean.io[name as keyof typeof IO_AREAS]);
|
|
558
|
+
write("entry_state.json", json(entryState(registers, state, vectors, differs)));
|
|
139
559
|
return {
|
|
140
560
|
output: {
|
|
141
561
|
captured: true,
|
|
142
562
|
pc: formatC64Address(registers.pc),
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
563
|
+
out: options.out,
|
|
564
|
+
files: ["image.json", "image.bin", "entry_state.json", ...files],
|
|
565
|
+
segments: segments.map(showRange),
|
|
566
|
+
...(options.exclude.length === 0 ? {} : { excluded: options.exclude.map(showRange) }),
|
|
567
|
+
stages: followed.stages.length,
|
|
568
|
+
differsFromReset: differs.length,
|
|
146
569
|
},
|
|
147
570
|
failed: false,
|
|
148
571
|
};
|
|
@@ -151,6 +574,15 @@ async function capture(program: string, options: { until: number; out: string; r
|
|
|
151
574
|
}
|
|
152
575
|
}
|
|
153
576
|
|
|
577
|
+
/** The options that each command takes; any other option is refused, not ignored. */
|
|
578
|
+
const OPTIONS_OF: Record<string, string[]> = {
|
|
579
|
+
inspect: [],
|
|
580
|
+
trace: ["frames", "entry", "steps"],
|
|
581
|
+
stages: ["frames", "entry", "steps", "min-stage"],
|
|
582
|
+
capture: ["until", "out", "range", "exclude", "timeout-frames", "entry", "steps", "min-stage"],
|
|
583
|
+
loads: ["steps", "out", "drive", "min-block"],
|
|
584
|
+
};
|
|
585
|
+
|
|
154
586
|
async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }> {
|
|
155
587
|
const { values, positionals } = parseArgs({
|
|
156
588
|
args: argv,
|
|
@@ -160,15 +592,25 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
|
|
|
160
592
|
until: { type: "string" },
|
|
161
593
|
out: { type: "string" },
|
|
162
594
|
range: { type: "string" },
|
|
595
|
+
exclude: { type: "string", multiple: true },
|
|
163
596
|
"timeout-frames": { type: "string" },
|
|
164
597
|
frames: { type: "string" },
|
|
165
598
|
entry: { type: "string" },
|
|
599
|
+
steps: { type: "string" },
|
|
600
|
+
"min-stage": { type: "string" },
|
|
601
|
+
"min-block": { type: "string" },
|
|
602
|
+
drive: { type: "boolean" },
|
|
166
603
|
help: { type: "boolean" },
|
|
167
604
|
},
|
|
168
605
|
});
|
|
169
606
|
if (values.help) throw new UsageError("");
|
|
170
607
|
const [command, program, ...rest] = positionals;
|
|
171
608
|
if (program === undefined) throw new UsageError("give a command and a program");
|
|
609
|
+
const allowed = OPTIONS_OF[command ?? ""];
|
|
610
|
+
if (allowed === undefined) throw new UsageError(`unknown command ${command}`);
|
|
611
|
+
for (const option of Object.keys(values)) {
|
|
612
|
+
if (option !== "help" && !allowed.includes(option)) throw new UsageError(`${command} does not take --${option}`);
|
|
613
|
+
}
|
|
172
614
|
if (command === "inspect") {
|
|
173
615
|
if (rest.length > 0) throw new UsageError("inspect takes one program");
|
|
174
616
|
const { bytes } = readProjectFile(program);
|
|
@@ -183,9 +625,32 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
|
|
|
183
625
|
if (rest.length > 0) throw new UsageError("trace takes one program");
|
|
184
626
|
const frames = values.frames === undefined ? 3000 : Number(values.frames);
|
|
185
627
|
if (!Number.isInteger(frames) || frames < 1 || frames > 30_000) throw new UsageError("--frames must be from 1 to 30000");
|
|
186
|
-
|
|
628
|
+
const steps = values.steps === undefined ? [] : readSteps(values.steps);
|
|
629
|
+
return trace(program, { frames, steps, ...(values.entry === undefined ? {} : { entry: address(values.entry, "--entry") }) });
|
|
630
|
+
}
|
|
631
|
+
if (command === "stages") {
|
|
632
|
+
if (rest.length > 0) throw new UsageError("stages takes one program");
|
|
633
|
+
const frames = values.frames === undefined ? 3000 : Number(values.frames);
|
|
634
|
+
if (!Number.isInteger(frames) || frames < 1 || frames > 30_000) throw new UsageError("--frames must be from 1 to 30000");
|
|
635
|
+
const minStage = values["min-stage"] === undefined ? 64 : Number(values["min-stage"]);
|
|
636
|
+
if (!Number.isInteger(minStage) || minStage < 1 || minStage > 0x10000) throw new UsageError("--min-stage must be from 1 to 65536");
|
|
637
|
+
const steps = values.steps === undefined ? [] : readSteps(values.steps);
|
|
638
|
+
// A runUntil step to a raster line or a memory value cannot be tracked in chunks of frames; capture takes it.
|
|
639
|
+
if (steps.some((step) => "runUntil" in step && !trackedRunUntil(step))) throw new UsageError("stages takes a runUntil step only with at alone; use frames steps or runUntil at");
|
|
640
|
+
return stages(program, { frames, steps, minStage, ...(values.entry === undefined ? {} : { entry: address(values.entry, "--entry") }) });
|
|
641
|
+
}
|
|
642
|
+
if (command === "loads") {
|
|
643
|
+
if (rest.length > 0) throw new UsageError("loads takes one program");
|
|
644
|
+
if (values.steps === undefined || values.out === undefined) throw new UsageError("loads needs --steps and --out");
|
|
645
|
+
try {
|
|
646
|
+
resolveProjectPath(values.out);
|
|
647
|
+
} catch {
|
|
648
|
+
throw new UsageError(`--out must be a path relative to the project folder that stays inside it, not ${values.out}`);
|
|
649
|
+
}
|
|
650
|
+
const minBlock = values["min-block"] === undefined ? 64 : Number(values["min-block"]);
|
|
651
|
+
if (!Number.isInteger(minBlock) || minBlock < 1 || minBlock > 0x10000) throw new UsageError("--min-block must be from 1 to 65536");
|
|
652
|
+
return loads(program, { out: values.out, steps: readLoadSteps(values.steps), drive: values.drive === true, minBlock });
|
|
187
653
|
}
|
|
188
|
-
if (command !== "capture") throw new UsageError(`unknown command ${command}`);
|
|
189
654
|
if (values.until === undefined || values.out === undefined) throw new UsageError("capture needs --until and --out");
|
|
190
655
|
// --range takes two values: the end is the next positional argument.
|
|
191
656
|
let range: [number, number] | undefined;
|
|
@@ -196,19 +661,38 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
|
|
|
196
661
|
} else if (rest.length > 0) {
|
|
197
662
|
throw new UsageError("too many arguments");
|
|
198
663
|
}
|
|
664
|
+
const exclude = (values.exclude ?? []).map((value) => {
|
|
665
|
+
const parts = value.split("-");
|
|
666
|
+
if (parts.length !== 2) throw new UsageError(`--exclude takes <start>-<end>, not ${value}`);
|
|
667
|
+
const range = { start: address(parts[0]!, "--exclude"), end: address(parts[1]!, "--exclude") };
|
|
668
|
+
if (range.end < range.start) throw new UsageError(`the --exclude end must not be before its start: ${value}`);
|
|
669
|
+
return range;
|
|
670
|
+
});
|
|
199
671
|
try {
|
|
200
672
|
resolveProjectPath(values.out);
|
|
201
673
|
} catch {
|
|
202
|
-
throw new UsageError(`--out must be a path relative to the project
|
|
674
|
+
throw new UsageError(`--out must be a path relative to the project folder that stays inside it, not ${values.out}`);
|
|
203
675
|
}
|
|
204
676
|
const timeoutFrames = values["timeout-frames"] === undefined ? 3000 : Number(values["timeout-frames"]);
|
|
205
677
|
if (!Number.isInteger(timeoutFrames) || timeoutFrames < 1 || timeoutFrames > 30_000) throw new UsageError("--timeout-frames must be from 1 to 30000");
|
|
206
|
-
|
|
678
|
+
const steps = values.steps === undefined ? [] : readSteps(values.steps);
|
|
679
|
+
const minStage = values["min-stage"] === undefined ? 64 : Number(values["min-stage"]);
|
|
680
|
+
if (!Number.isInteger(minStage) || minStage < 1 || minStage > 0x10000) throw new UsageError("--min-stage must be from 1 to 65536");
|
|
681
|
+
return capture(program, {
|
|
682
|
+
until: address(values.until, "--until"),
|
|
683
|
+
out: values.out,
|
|
684
|
+
...(range === undefined ? {} : { range }),
|
|
685
|
+
exclude,
|
|
686
|
+
timeoutFrames,
|
|
687
|
+
steps,
|
|
688
|
+
minStage,
|
|
689
|
+
...(values.entry === undefined ? {} : { entry: address(values.entry, "--entry") }),
|
|
690
|
+
});
|
|
207
691
|
}
|
|
208
692
|
|
|
209
693
|
await runScript(
|
|
210
|
-
async () => {
|
|
211
|
-
const { output, failed } = await run(
|
|
694
|
+
async (argv) => {
|
|
695
|
+
const { output, failed } = await run(argv);
|
|
212
696
|
if (failed) process.exitCode = 1;
|
|
213
697
|
return output;
|
|
214
698
|
},
|