@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.
Files changed (126) hide show
  1. package/README.md +28 -62
  2. package/distribution/package.ts +1 -16
  3. package/distribution/plugin.ts +5 -3
  4. package/package.json +1 -1
  5. package/skills/c64-assembler/SKILL.md +25 -6
  6. package/skills/c64-assembler/scripts/assemble.ts +45 -10
  7. package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
  8. package/skills/c64-basic/SKILL.md +15 -7
  9. package/skills/c64-basic/scripts/basic.ts +4 -4
  10. package/skills/c64-disk/SKILL.md +34 -11
  11. package/skills/c64-disk/scripts/disk.ts +65 -10
  12. package/skills/c64-disk/scripts/lib/containers.ts +124 -0
  13. package/skills/c64-docs/SKILL.md +138 -0
  14. package/skills/c64-docs/references/page-types.md +141 -0
  15. package/skills/c64-docs/references/style.md +43 -0
  16. package/skills/c64-docs/scripts/generate.ts +102 -0
  17. package/skills/c64-docs/scripts/lib/links.ts +155 -0
  18. package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
  19. package/skills/c64-docs/scripts/lib/reference.ts +406 -0
  20. package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
  21. package/skills/c64-docs/scripts/link.ts +207 -0
  22. package/skills/c64-emulator/SKILL.md +29 -8
  23. package/skills/c64-knowledge/SKILL.md +201 -15
  24. package/skills/c64-knowledge/scripts/knowledge.ts +927 -78
  25. package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
  26. package/skills/c64-memory-map/SKILL.md +14 -2
  27. package/skills/c64-memory-map/scripts/memmap.ts +15 -4
  28. package/skills/c64-provenance/SKILL.md +100 -11
  29. package/skills/c64-provenance/scripts/lib/compare.ts +152 -0
  30. package/skills/c64-provenance/scripts/lib/library.ts +584 -0
  31. package/skills/c64-provenance/scripts/lib/runtime.ts +191 -0
  32. package/skills/c64-provenance/scripts/lib/separate.ts +140 -0
  33. package/skills/c64-provenance/scripts/provenance.ts +477 -24
  34. package/skills/c64-reverse-engineering/SKILL.md +90 -11
  35. package/skills/c64-source/SKILL.md +174 -0
  36. package/skills/c64-source/scripts/generate.ts +788 -0
  37. package/skills/c64-source/scripts/lib/loader.ts +630 -0
  38. package/skills/c64-source/scripts/lib/source.ts +1121 -0
  39. package/skills/c64-static-analysis/SKILL.md +53 -13
  40. package/skills/c64-static-analysis/scripts/analyze.ts +260 -31
  41. package/skills/c64-static-analysis/scripts/lib/coverage.ts +72 -0
  42. package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +11 -5
  43. package/skills/c64-static-analysis/scripts/lib/idioms.ts +158 -0
  44. package/skills/c64-static-analysis/scripts/lib/instructions.ts +105 -0
  45. package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
  46. package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
  47. package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
  48. package/skills/c64-testing/SKILL.md +57 -4
  49. package/skills/c64-testing/references/scenario-format.md +50 -6
  50. package/skills/c64-testing/scripts/checklist.ts +3 -3
  51. package/skills/c64-testing/scripts/diff.ts +167 -0
  52. package/skills/c64-testing/scripts/lib/diff.ts +70 -0
  53. package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +183 -152
  54. package/skills/c64-testing/scripts/run.ts +241 -0
  55. package/skills/c64-testing/scripts/test.ts +20 -10
  56. package/skills/c64-unpacker/SKILL.md +88 -21
  57. package/skills/c64-unpacker/scripts/lib/evidence.ts +207 -0
  58. package/skills/c64-unpacker/scripts/lib/loads.ts +209 -0
  59. package/skills/c64-unpacker/scripts/lib/machine.ts +80 -0
  60. package/skills/c64-unpacker/scripts/lib/stages.ts +142 -0
  61. package/skills/c64-unpacker/scripts/unpack.ts +545 -61
  62. package/src/c64.ts +240 -12
  63. package/src/cli/cleanup.ts +1 -1
  64. package/src/cli/main.ts +14 -8
  65. package/src/cli/status.ts +105 -11
  66. package/src/graphics.ts +158 -0
  67. package/src/host/main.ts +57 -31
  68. package/src/host/server.ts +30 -47
  69. package/src/host/tools/c1541.ts +34 -5
  70. package/src/host/tools/index.ts +6 -2
  71. package/src/host/tools/petcat.ts +10 -3
  72. package/src/host/tools/status.ts +23 -6
  73. package/src/host/vice/adapter.ts +64 -20
  74. package/src/host/vice/chips.ts +11 -1
  75. package/src/host/vice/process.ts +19 -8
  76. package/src/host/vice/screen.ts +4 -44
  77. package/src/host/vice/session.ts +343 -60
  78. package/src/host-client/autostart.ts +55 -0
  79. package/src/host-client/connect.ts +14 -14
  80. package/src/host-client/coverage.ts +36 -0
  81. package/src/host-client/image.ts +63 -0
  82. package/src/host-client/steps.ts +175 -0
  83. package/src/host-client/tools.ts +7 -6
  84. package/src/host-client/transfer.ts +60 -10
  85. package/src/host-client/vice-session.ts +107 -28
  86. package/src/knowledge/conflicts.ts +161 -0
  87. package/src/knowledge/database.ts +150 -38
  88. package/src/knowledge/expression.ts +209 -0
  89. package/src/knowledge/graph.ts +104 -0
  90. package/src/knowledge/history.ts +105 -27
  91. package/src/knowledge/import.ts +141 -85
  92. package/src/knowledge/join.ts +211 -0
  93. package/src/knowledge/program.ts +258 -0
  94. package/src/knowledge/read.ts +445 -63
  95. package/src/knowledge/schema.ts +277 -65
  96. package/src/knowledge/text.ts +415 -0
  97. package/src/knowledge/write.ts +1145 -150
  98. package/src/mcp/main.ts +44 -19
  99. package/src/mcp/output.ts +39 -0
  100. package/src/mcp/server.ts +15 -4
  101. package/src/mcp/tools/debug.ts +202 -31
  102. package/src/mcp/tools/execution.ts +39 -5
  103. package/src/mcp/tools/input.ts +7 -4
  104. package/src/mcp/tools/machine.ts +14 -2
  105. package/src/mcp/tools/media.ts +44 -16
  106. package/src/mcp/tools/memory.ts +68 -12
  107. package/src/mcp/tools/video.ts +20 -9
  108. package/src/native/discover.ts +44 -6
  109. package/src/native/ghidra/analyze.ts +2 -2
  110. package/src/native/ghidra/index.ts +19 -3
  111. package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
  112. package/src/native/ghidra/scripts/C64Export.java +33 -1
  113. package/src/native/ghidra/scripts/C64Prepare.java +7 -1
  114. package/src/native/status.ts +15 -3
  115. package/src/native/types.ts +43 -8
  116. package/src/opcodes.ts +133 -0
  117. package/src/package.ts +21 -0
  118. package/{skills/c64-memory-map/scripts → src}/platform.ts +87 -7
  119. package/src/project.ts +166 -3
  120. package/src/protocol/messages.ts +17 -10
  121. package/src/protocol/tools.ts +27 -22
  122. package/src/protocol/vice.ts +255 -33
  123. package/src/script.ts +49 -9
  124. package/src/trace.ts +55 -23
  125. package/skills/c64-provenance/scripts/compare.ts +0 -92
  126. 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. No native unpack tool is used.
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, textToPetscii } from "#src/c64.ts";
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 <file> [--range <start> <end>] [--timeout-frames <n>]
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>, then write memory:
28
- with --range, a PRG of that range; without it, all 64 KiB of RAM
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
- async function trace(program: string, options: { frames: number; entry?: number }): Promise<{ output: Record<string, unknown>; failed: boolean }> {
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
- const load = bytes[0]! | (bytes[1]! << 8);
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.autostart({ path: program, index: 0, run: false });
60
- const head = bytes.subarray(2, Math.min(bytes.length, 2 + 64)).toString("hex");
61
- let loaded = false;
62
- for (let frames = 0; frames < LOAD_LIMIT_FRAMES && !loaded; frames += LOAD_POLL_FRAMES) {
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, end) => {
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
- return map.ranges;
82
- }, MAX_RANGES);
83
- const { written, bytes: count, running } = writtenThenExecuted(ranges, pc);
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 now runs code at ${formatC64Address(pc)} that the program wrote: it unpacked, decrypted or relocated it.`
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(pc), runningRange: { start: formatC64Address(running.start), end: formatC64Address(running.end) } }),
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
- async function capture(program: string, options: { until: number; out: string; range?: [number, number]; timeoutFrames: number }): Promise<{ output: Record<string, unknown>; failed: boolean }> {
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.autostart({ path: program, index: 0, run: true });
119
- const reached = await client.runUntil({ target: { kind: "address", address: options.until, space: "c64" }, timeoutFrames: options.timeoutFrames });
120
- if (!reached.reached) {
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 stopped by ${reached.stopReason} at ${formatC64Address(reached.pc)}.` },
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
- const [start, end] = options.range ?? [0x0000, 0xffff];
127
- const memory = Buffer.alloc(end - start + 1);
128
- for (let at = start; at <= end; at += MAX_READ) {
129
- const size = Math.min(MAX_READ, end - at + 1);
130
- // The RAM view: unpacked code is often under the ROMs or the I/O area.
131
- const read = await client.memoryRead({ address: at, size, space: "c64", view: "ram" });
132
- Buffer.from(read.data, "hex").copy(memory, at - start);
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 bytes = options.range === undefined ? memory : Buffer.concat([Buffer.from([start & 0xff, start >> 8]), memory]);
135
- const path = resolveProjectPath(options.out);
136
- mkdirSync(dirname(path), { recursive: true });
137
- writeFileSync(path, bytes);
138
- const registers = await client.registersGet("c64");
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
- output: options.out,
144
- ...(options.range === undefined ? { flat64k: true } : { loadRange: { start: formatC64Address(start), end: formatC64Address(end) } }),
145
- registers: { a: registers.a, x: registers.x, y: registers.y, sp: registers.sp },
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
- return trace(program, { frames, ...(values.entry === undefined ? {} : { entry: address(values.entry, "--entry") }) });
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 directory that stays inside it, not ${values.out}`);
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
- return capture(program, { until: address(values.until, "--until"), out: values.out, ...(range === undefined ? {} : { range }), timeoutFrames });
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(process.argv.slice(2));
694
+ async (argv) => {
695
+ const { output, failed } = await run(argv);
212
696
  if (failed) process.exitCode = 1;
213
697
  return output;
214
698
  },