@henols/c64-re-tools 2.1.0 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +7 -3
- package/skills/c64-assembler/scripts/assembler.ts +29 -11
- package/skills/c64-assembler/scripts/lib/compare.ts +4 -3
- package/skills/c64-basic/SKILL.md +1 -0
- package/skills/c64-disk/SKILL.md +3 -2
- package/skills/c64-disk/scripts/disk.ts +26 -12
- package/skills/c64-disk/scripts/lib/containers.ts +10 -11
- package/skills/c64-docs/SKILL.md +2 -1
- package/skills/c64-docs/scripts/docs.ts +1 -1
- package/skills/c64-docs/scripts/lib/links.ts +5 -3
- package/skills/c64-docs/scripts/lib/reference.ts +2 -2
- package/skills/c64-emulator/SKILL.md +9 -2
- package/skills/c64-knowledge/SKILL.md +9 -5
- package/skills/c64-knowledge/scripts/knowledge.ts +91 -53
- package/skills/c64-knowledge/scripts/lib/listing.ts +8 -30
- package/skills/c64-memory-map/SKILL.md +2 -1
- package/skills/c64-memory-map/scripts/memory-map.ts +4 -3
- package/skills/c64-provenance/SKILL.md +6 -4
- package/skills/c64-provenance/scripts/lib/library.ts +18 -6
- package/skills/c64-provenance/scripts/lib/separate.ts +45 -6
- package/skills/c64-provenance/scripts/provenance.ts +14 -10
- package/skills/c64-source/SKILL.md +3 -0
- package/skills/c64-source/scripts/lib/source.ts +9 -31
- package/skills/c64-source/scripts/source.ts +14 -10
- package/skills/c64-static-analysis/SKILL.md +2 -0
- package/skills/c64-static-analysis/scripts/lib/idioms.ts +6 -2
- package/skills/c64-static-analysis/scripts/static-analysis.ts +18 -8
- package/skills/c64-testing/SKILL.md +14 -3
- package/skills/c64-testing/references/recording-format.md +2 -0
- package/skills/c64-testing/references/scenario-format.md +6 -0
- package/skills/c64-testing/scripts/lib/diff-files.ts +76 -14
- package/skills/c64-testing/scripts/lib/diff.ts +3 -3
- package/skills/c64-testing/scripts/lib/recording.ts +50 -15
- package/skills/c64-testing/scripts/lib/replay.ts +12 -1
- package/skills/c64-testing/scripts/lib/runs.ts +84 -4
- package/skills/c64-testing/scripts/lib/scenario.ts +43 -6
- package/skills/c64-testing/scripts/testing.ts +20 -7
- package/skills/c64-unpacker/SKILL.md +17 -10
- package/skills/c64-unpacker/scripts/lib/evidence.ts +4 -5
- package/skills/c64-unpacker/scripts/lib/loads.ts +14 -7
- package/skills/c64-unpacker/scripts/lib/machine.ts +30 -3
- package/skills/c64-unpacker/scripts/lib/stages.ts +15 -7
- package/skills/c64-unpacker/scripts/unpacker.ts +93 -42
- package/src/host/vice/adapter.ts +4 -1
- package/src/host/vice/session.ts +148 -13
- package/src/host-client/image.ts +40 -8
- package/src/host-client/steps.ts +145 -9
- package/src/host-client/tools.ts +1 -1
- package/src/host-client/transfer.ts +33 -18
- package/src/knowledge/expression.ts +1 -1
- package/src/knowledge/history.ts +74 -4
- package/src/knowledge/import.ts +79 -7
- package/src/knowledge/names.ts +2 -1
- package/src/knowledge/program.ts +32 -18
- package/src/knowledge/read.ts +46 -0
- package/src/knowledge/schema.ts +4 -2
- package/src/knowledge/text.ts +2 -0
- package/src/knowledge/write.ts +58 -16
- package/src/mcp/inputs.ts +6 -3
- package/src/mcp/output.ts +5 -0
- package/src/mcp/server.ts +20 -4
- package/src/mcp/tools/debug.ts +36 -9
- package/src/mcp/tools/execution.ts +18 -5
- package/src/mcp/tools/machine.ts +3 -1
- package/src/mcp/tools/media.ts +14 -2
- package/src/mcp/tools/memory.ts +11 -6
- package/src/mcp/tools/video.ts +5 -3
- package/src/media.ts +42 -0
- package/src/opcodes.ts +51 -18
- package/src/project.ts +111 -3
- package/src/protocol/vice.ts +8 -2
- package/src/script.ts +55 -3
|
@@ -9,17 +9,17 @@
|
|
|
9
9
|
import { type AddressRange, COVERAGE, EXECUTE, formatC64Address, READ, WRITE } from "#src/c64.ts";
|
|
10
10
|
import { autostartLoaded, LOAD_LIMIT_FRAMES, RUN_LIMIT_FRAMES, typeRun } from "#src/host-client/autostart.ts";
|
|
11
11
|
import { readCoverage, writtenBlocks } from "#src/host-client/coverage.ts";
|
|
12
|
-
import { resetToReady } from "#src/host-client/entry-state.ts";
|
|
13
12
|
import { readActionSteps, runActionStep, type ActionStep } from "#src/host-client/steps.ts";
|
|
14
13
|
import { WireFailure } from "#src/host-client/tools.ts";
|
|
15
|
-
import {
|
|
14
|
+
import { readPrg } from "#src/host-client/image.ts";
|
|
15
|
+
import { writeReported, type Written } from "#src/host-client/transfer.ts";
|
|
16
16
|
import { ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
17
17
|
import { knowledgeAt, knowledgeNames } from "#src/knowledge/names.ts";
|
|
18
|
-
import { OPTIONS, runScript, UsageError, type Call, type CommandSpec, type OptionSpec } from "#src/script.ts";
|
|
18
|
+
import { listed, OPTIONS, runScript, UsageError, type Call, type CommandSpec, type OptionSpec } from "#src/script.ts";
|
|
19
19
|
import { basicStart, inspect, readMemoryMap, underRom, writtenThenExecuted } from "./lib/evidence.ts";
|
|
20
20
|
import { loads, readLoadSteps } from "./lib/loads.ts";
|
|
21
21
|
import { callReturn, captureSegments, executedIn, inRanges, mergeMaps, stageCandidates, stagePoints, withoutRanges } from "./lib/stages.ts";
|
|
22
|
-
import {
|
|
22
|
+
import { entryStateFile, machineState, readRam, readyState, vectorsOf } from "./lib/machine.ts";
|
|
23
23
|
|
|
24
24
|
/** Written-then-executed bytes from this many on are unpacking, decrypting or relocating, not self-changing code. */
|
|
25
25
|
const UNPACKED_BYTES = 256;
|
|
@@ -38,10 +38,12 @@ async function runSteps(client: ViceSessionClient, steps: ActionStep[]): Promise
|
|
|
38
38
|
return undefined;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
/** inspect, trace, stages and capture take a PRG: a disk, tape or cartridge file goes to loads. */
|
|
42
|
+
const NOT_PRG_REMEDY = "Use loads, which runs a disk, tape or cartridge image, or get the PRG out of it with disk.ts read of the c64-disk skill";
|
|
43
|
+
const readProgram = (path: string) => readPrg(path, NOT_PRG_REMEDY).bytes;
|
|
44
|
+
|
|
45
|
+
async function trace(program: string, options: { frames: number; entry?: number; steps: ActionStep[]; max: number }): Promise<{ output: Record<string, unknown>; failed: boolean }> {
|
|
46
|
+
const bytes = startable(program, options.entry);
|
|
45
47
|
const client = await ViceSessionClient.open({ videoStandard: "pal" });
|
|
46
48
|
try {
|
|
47
49
|
// Loaded but not started, so the memory map can start empty: the KERNAL start-up writes all of RAM.
|
|
@@ -54,7 +56,12 @@ async function trace(program: string, options: { frames: number; entry?: number;
|
|
|
54
56
|
const step = await client.execution({ action: "advance-frames", count: Math.min(MAX_ADVANCE, options.frames - ran), space: "c64" });
|
|
55
57
|
const advanced = step.advancedFrames ?? 0;
|
|
56
58
|
ran += advanced;
|
|
57
|
-
|
|
59
|
+
// A JAM or a point stops the run before its frames: the evidence would be of a run that did not happen.
|
|
60
|
+
if ((step.stopReason !== undefined && step.stopReason !== "frames") || advanced === 0) {
|
|
61
|
+
const stopReason = step.stopReason ?? "no-frames";
|
|
62
|
+
const at = step.pc ?? (await client.registersGet("c64")).pc;
|
|
63
|
+
return { output: { traced: false, frames: ran, stopReason, pc: formatC64Address(at), reason: stoppedEarly(stopReason, at, ran, options.frames) }, failed: true };
|
|
64
|
+
}
|
|
58
65
|
}
|
|
59
66
|
const { pc } = await client.registersGet("c64");
|
|
60
67
|
const ranges = await readMemoryMap(async (start) => {
|
|
@@ -80,7 +87,7 @@ async function trace(program: string, options: { frames: number; entry?: number;
|
|
|
80
87
|
traced: true,
|
|
81
88
|
frames: ran,
|
|
82
89
|
pc: formatC64Address(pc),
|
|
83
|
-
writtenThenExecuted: written.
|
|
90
|
+
writtenThenExecuted: listed(written, options.max, (range) => ({
|
|
84
91
|
start: formatC64Address(range.start),
|
|
85
92
|
end: formatC64Address(range.end),
|
|
86
93
|
...(underRom(range.start) ? { underRom: true } : {}),
|
|
@@ -107,6 +114,12 @@ async function trace(program: string, options: { frames: number; entry?: number;
|
|
|
107
114
|
|
|
108
115
|
const NO_BASIC = "the program is no BASIC program at $0801, so RUN cannot start it. Give --entry <address>.";
|
|
109
116
|
|
|
117
|
+
/** Why a run stopped before its frames: what stopped the CPU, where, and after how many frames. */
|
|
118
|
+
function stoppedEarly(stopReason: string, pc: number, ran: number, frames: number): string {
|
|
119
|
+
const what = stopReason === "jam" ? "a JAM instruction" : stopReason === "no-frames" ? "the emulator, which ran no frame" : `a ${stopReason}`;
|
|
120
|
+
return `The CPU stopped at ${formatC64Address(pc)} by ${what} after ${ran} of ${frames} frames. Check --entry and the steps: the program must run for the frames.`;
|
|
121
|
+
}
|
|
122
|
+
|
|
110
123
|
/** The first PC in one of the ranges within SYS_FRAMES frames, found with exec breakpoints over them. */
|
|
111
124
|
async function firstPcIn(client: ViceSessionClient, ranges: Array<{ start: number; end: number }>): Promise<number | undefined> {
|
|
112
125
|
const points = [];
|
|
@@ -151,6 +164,13 @@ async function firstPcAfterBasic(client: ViceSessionClient, from: number, to: nu
|
|
|
151
164
|
return from > to ? undefined : firstPcIn(client, [{ start: from, end: to }]);
|
|
152
165
|
}
|
|
153
166
|
|
|
167
|
+
/**
|
|
168
|
+
* What a stage wrote: every written block, also a small one. --min-stage
|
|
169
|
+
* decides only where a stage starts; the wrote list and the capture segments
|
|
170
|
+
* keep each byte that a stage wrote.
|
|
171
|
+
*/
|
|
172
|
+
const stageWrote = (map: Uint8Array) => writtenBlocks(map, 1);
|
|
173
|
+
|
|
154
174
|
/** Frames between two looks at the memory map while stages are followed. */
|
|
155
175
|
const STAGE_CHUNK_FRAMES = 25;
|
|
156
176
|
const MAX_STAGES = 16;
|
|
@@ -175,6 +195,8 @@ interface Followed {
|
|
|
175
195
|
reached: boolean;
|
|
176
196
|
/** Why the program did not start, when it did not. */
|
|
177
197
|
failed?: string;
|
|
198
|
+
/** What stopped the CPU before the frames ran: a JAM or a point of the program. */
|
|
199
|
+
halted?: { stopReason: string; pc: number };
|
|
178
200
|
}
|
|
179
201
|
|
|
180
202
|
type RunUntilStep = Extract<ActionStep, { runUntil: unknown }>;
|
|
@@ -197,13 +219,11 @@ function trackedRunUntil(step: ActionStep): step is RunUntilStep & { runUntil: {
|
|
|
197
219
|
async function follow(
|
|
198
220
|
client: ViceSessionClient,
|
|
199
221
|
program: string,
|
|
222
|
+
bytes: Uint8Array,
|
|
200
223
|
options: { frames: number; entry?: number; steps: ActionStep[]; minStage: number; until?: number },
|
|
201
224
|
): Promise<Followed> {
|
|
202
|
-
const { bytes } = readProjectFile(program);
|
|
203
|
-
if (bytes.length < 3) throw new UsageError("the file is too short for a PRG");
|
|
204
225
|
const load = bytes[0]! | (bytes[1]! << 8);
|
|
205
226
|
const basic = basicStart(bytes);
|
|
206
|
-
if (options.entry === undefined && basic === undefined) throw new UsageError(NO_BASIC);
|
|
207
227
|
const notStarted = await startLoaded(client, program, bytes, options.entry);
|
|
208
228
|
if (notStarted !== undefined) return { stages: [], frames: 0, reached: false, failed: notStarted };
|
|
209
229
|
// The frames of each step end where a scenario's do: the first step counts from RUN, a later one from where it starts.
|
|
@@ -216,6 +236,7 @@ async function follow(
|
|
|
216
236
|
let firstFrame = 0;
|
|
217
237
|
let total = 0;
|
|
218
238
|
let ended = false;
|
|
239
|
+
let halted: { stopReason: string; pc: number } | undefined;
|
|
219
240
|
/** Where the run stops: the target of a runUntil step while it runs, then until. */
|
|
220
241
|
let stopAt: number | undefined;
|
|
221
242
|
const atTarget = (pc: number) => stopAt !== undefined && pc === stopAt;
|
|
@@ -223,7 +244,7 @@ async function follow(
|
|
|
223
244
|
let stage = { map: new Uint8Array(0x10000), own: new Set<number>(), called: [] as AddressRange[] };
|
|
224
245
|
const startStage = async (next: number, last: Uint8Array) => {
|
|
225
246
|
mergeMaps(stage.map, last);
|
|
226
|
-
found.push({ entry, firstFrame, frames: total - firstFrame, wrote:
|
|
247
|
+
found.push({ entry, firstFrame, frames: total - firstFrame, wrote: stageWrote(stage.map), next });
|
|
227
248
|
entry = next;
|
|
228
249
|
firstFrame = total;
|
|
229
250
|
stage = { map: new Uint8Array(0x10000), own: new Set<number>(), called: [] };
|
|
@@ -235,7 +256,7 @@ async function follow(
|
|
|
235
256
|
* blocks, and stops at the first opcode that the stage wrote and the CPU
|
|
236
257
|
* then executes. Own code stops the run once; a write watch arms it again.
|
|
237
258
|
*/
|
|
238
|
-
const rerun = async (candidates: AddressRange[], frames: number): Promise<{ used: number; map: Uint8Array; next?: number; atTarget: boolean }> => {
|
|
259
|
+
const rerun = async (candidates: AddressRange[], frames: number): Promise<{ used: number; map: Uint8Array; next?: number; atTarget: boolean; halted?: { stopReason: string; pc: number } }> => {
|
|
239
260
|
const points = new Map<string, { kind: "exec" | "watch"; id: number }>();
|
|
240
261
|
const arm = async (wanted: { exec: AddressRange[]; watch: AddressRange[] }) => {
|
|
241
262
|
const keys = new Set([...wanted.exec.map((range) => `exec:${range.start}:${range.end}`), ...wanted.watch.map((range) => `watch:${range.start}:${range.end}`)]);
|
|
@@ -295,6 +316,7 @@ async function follow(
|
|
|
295
316
|
left -= advanced;
|
|
296
317
|
const pc = run.pc ?? (await client.registersGet("c64")).pc;
|
|
297
318
|
map = await readCoverage(client);
|
|
319
|
+
if (run.stopReason === "jam") return { used, map, atTarget: false, halted: { stopReason: "jam", pc } };
|
|
298
320
|
// A frame can end in written code that the stage called before: that stop starts no stage.
|
|
299
321
|
if (inRanges(candidates, pc) && !inRanges(stage.called, pc)) {
|
|
300
322
|
if (written(pc)) {
|
|
@@ -341,6 +363,14 @@ async function follow(
|
|
|
341
363
|
const first = await client.execution({ action: "advance-frames", count: chunk, space: "c64", ...framed() });
|
|
342
364
|
const firstPc = first.pc ?? (await client.registersGet("c64")).pc;
|
|
343
365
|
const map = await readCoverage(client);
|
|
366
|
+
// A JAM, or a point that is not the target, ends the run before its frames.
|
|
367
|
+
if (first.stopReason !== undefined && first.stopReason !== "frames" && !atTarget(firstPc)) {
|
|
368
|
+
total += first.advancedFrames ?? 0;
|
|
369
|
+
mergeMaps(stage.map, map);
|
|
370
|
+
halted = { stopReason: first.stopReason, pc: firstPc };
|
|
371
|
+
ended = true;
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
344
374
|
const candidates = stageCandidates(stage.map, map, stage.own, options.minStage, stage.called);
|
|
345
375
|
if (candidates.length === 0) {
|
|
346
376
|
const advanced = first.advancedFrames ?? 0;
|
|
@@ -369,6 +399,10 @@ async function follow(
|
|
|
369
399
|
if (again.next !== undefined) await startStage(again.next, again.map);
|
|
370
400
|
else mergeMaps(stage.map, again.map);
|
|
371
401
|
if (again.atTarget) stopped = true;
|
|
402
|
+
if (again.halted !== undefined) {
|
|
403
|
+
halted = again.halted;
|
|
404
|
+
ended = true;
|
|
405
|
+
}
|
|
372
406
|
}
|
|
373
407
|
return stopped;
|
|
374
408
|
};
|
|
@@ -389,7 +423,8 @@ async function follow(
|
|
|
389
423
|
else if (trackedRunUntil(step)) {
|
|
390
424
|
if (!(await trackTo(knowledgeAt()(step.runUntil.target.address), step.runUntil.timeoutFrames))) {
|
|
391
425
|
const { pc } = await client.registersGet("c64");
|
|
392
|
-
|
|
426
|
+
const why = halted === undefined ? "" : ` ${stoppedEarly(halted.stopReason, halted.pc, total, step.runUntil.timeoutFrames)}`;
|
|
427
|
+
return { stages: found, frames: total, reached: false, failed: `Step ${index + 1} did not reach its target in ${step.runUntil.timeoutFrames} frames. It is at ${formatC64Address(pc)}.${why}` };
|
|
393
428
|
}
|
|
394
429
|
} else await runActionStep(client, step, knowledgeAt());
|
|
395
430
|
}
|
|
@@ -399,8 +434,8 @@ async function follow(
|
|
|
399
434
|
else reached = await trackTo(options.until, left);
|
|
400
435
|
await client.snapshot({ action: "remove", name: STAGE_SNAPSHOT }).catch(() => undefined);
|
|
401
436
|
mergeMaps(stage.map, await readCoverage(client));
|
|
402
|
-
found.push({ entry, firstFrame, frames: total - firstFrame, wrote:
|
|
403
|
-
return { stages: found, frames: total, reached };
|
|
437
|
+
found.push({ entry, firstFrame, frames: total - firstFrame, wrote: stageWrote(stage.map) });
|
|
438
|
+
return { stages: found, frames: total, reached, ...(halted === undefined ? {} : { halted }) };
|
|
404
439
|
}
|
|
405
440
|
|
|
406
441
|
const showRange = (range: { start: number; end: number }) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) });
|
|
@@ -413,11 +448,23 @@ const showStages = (stages: Stage[]) =>
|
|
|
413
448
|
...(stage.next === undefined ? {} : { next: formatC64Address(stage.next) }),
|
|
414
449
|
}));
|
|
415
450
|
|
|
451
|
+
/** The bytes of a PRG that the emulator starts: RUN needs a BASIC program at $0801, else --entry gives the start. Checked before the emulator opens. */
|
|
452
|
+
function startable(program: string, entry: number | undefined): Uint8Array {
|
|
453
|
+
const bytes = readProgram(program);
|
|
454
|
+
if (entry === undefined && basicStart(bytes) === undefined) throw new UsageError(NO_BASIC);
|
|
455
|
+
return bytes;
|
|
456
|
+
}
|
|
457
|
+
|
|
416
458
|
async function stages(program: string, options: { frames: number; entry?: number; steps: ActionStep[]; minStage: number }): Promise<{ output: Record<string, unknown>; failed: boolean }> {
|
|
459
|
+
const bytes = startable(program, options.entry);
|
|
417
460
|
const client = await ViceSessionClient.open({ videoStandard: "pal" });
|
|
418
461
|
try {
|
|
419
|
-
const followed = await follow(client, program, options);
|
|
462
|
+
const followed = await follow(client, program, bytes, options);
|
|
420
463
|
if (followed.failed !== undefined) return { output: { followed: false, reason: followed.failed }, failed: true };
|
|
464
|
+
if (followed.halted !== undefined) {
|
|
465
|
+
const { stopReason, pc } = followed.halted;
|
|
466
|
+
return { output: { followed: false, frames: followed.frames, stopReason, pc: formatC64Address(pc), reason: stoppedEarly(stopReason, pc, followed.frames, options.frames), stages: showStages(followed.stages) }, failed: true };
|
|
467
|
+
}
|
|
421
468
|
return {
|
|
422
469
|
output: {
|
|
423
470
|
followed: true,
|
|
@@ -437,14 +484,20 @@ async function capture(
|
|
|
437
484
|
program: string,
|
|
438
485
|
options: { until: number; out: string; range?: AddressRange; exclude: AddressRange[]; timeoutFrames: number; entry?: number; steps: ActionStep[]; minStage: number },
|
|
439
486
|
): Promise<{ output: Record<string, unknown>; failed: boolean }> {
|
|
487
|
+
const bytes = startable(program, options.entry);
|
|
440
488
|
const client = await ViceSessionClient.open({ videoStandard: "pal" });
|
|
441
489
|
try {
|
|
442
|
-
const followed = await follow(client, program, { frames: options.timeoutFrames, steps: options.steps, minStage: options.minStage, until: options.until, ...(options.entry === undefined ? {} : { entry: options.entry }) });
|
|
490
|
+
const followed = await follow(client, program, bytes, { frames: options.timeoutFrames, steps: options.steps, minStage: options.minStage, until: options.until, ...(options.entry === undefined ? {} : { entry: options.entry }) });
|
|
443
491
|
if (followed.failed !== undefined) return { output: { captured: false, reason: followed.failed }, failed: true };
|
|
444
492
|
const registers = await client.registersGet("c64");
|
|
445
493
|
if (!followed.reached) {
|
|
446
494
|
return {
|
|
447
|
-
output: {
|
|
495
|
+
output: {
|
|
496
|
+
captured: false,
|
|
497
|
+
reason: `The program did not get to ${formatC64Address(options.until)} in ${options.timeoutFrames} frames. It is at ${formatC64Address(registers.pc)}.${followed.halted === undefined ? "" : ` ${stoppedEarly(followed.halted.stopReason, followed.halted.pc, followed.frames, options.timeoutFrames)}`}`,
|
|
498
|
+
...(followed.halted === undefined ? {} : { stopReason: followed.halted.stopReason }),
|
|
499
|
+
stages: showStages(followed.stages),
|
|
500
|
+
},
|
|
448
501
|
failed: true,
|
|
449
502
|
};
|
|
450
503
|
}
|
|
@@ -453,10 +506,6 @@ async function capture(
|
|
|
453
506
|
const state = await machineState(client);
|
|
454
507
|
// The segments: what the stages wrote, without loader leftovers, with no overlap.
|
|
455
508
|
const found = options.range !== undefined ? [options.range] : captureSegments(followed.stages, registers.pc);
|
|
456
|
-
const inside = options.exclude.find((range) => registers.pc >= range.start && registers.pc <= range.end);
|
|
457
|
-
if (inside !== undefined) {
|
|
458
|
-
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 };
|
|
459
|
-
}
|
|
460
509
|
const segments = withoutRanges(found, options.exclude);
|
|
461
510
|
const file = (path: string) => `${options.out}/${path}`;
|
|
462
511
|
const written: Written[] = [];
|
|
@@ -486,14 +535,8 @@ async function capture(
|
|
|
486
535
|
),
|
|
487
536
|
);
|
|
488
537
|
// A clean machine at the READY prompt shows what the loader and the stages changed.
|
|
489
|
-
await
|
|
490
|
-
|
|
491
|
-
const differs: Array<{ address: string; entry: number; reset: number }> = [];
|
|
492
|
-
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]! }));
|
|
493
|
-
compare(0x0000, state.port, clean.port);
|
|
494
|
-
compare(0x0002, state.pages.subarray(2), clean.pages.subarray(2));
|
|
495
|
-
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]);
|
|
496
|
-
write("entry_state.json", json(entryState(registers, state, vectors, differs)));
|
|
538
|
+
const entry = entryStateFile(registers, state, vectors, await readyState(client));
|
|
539
|
+
write("entry_state.json", entry.bytes);
|
|
497
540
|
return {
|
|
498
541
|
output: {
|
|
499
542
|
captured: true,
|
|
@@ -503,7 +546,7 @@ async function capture(
|
|
|
503
546
|
segments: segments.map(showRange),
|
|
504
547
|
...(options.exclude.length === 0 ? {} : { excluded: options.exclude.map(showRange) }),
|
|
505
548
|
stages: followed.stages.length,
|
|
506
|
-
differsFromReset:
|
|
549
|
+
differsFromReset: entry.differsFromReset,
|
|
507
550
|
},
|
|
508
551
|
failed: false,
|
|
509
552
|
};
|
|
@@ -526,7 +569,7 @@ const entryOf = (call: Call) => {
|
|
|
526
569
|
};
|
|
527
570
|
const stepsOf = (call: Call) => {
|
|
528
571
|
const path = call.string("steps");
|
|
529
|
-
return path === undefined ? [] : readActionSteps(path, "--steps");
|
|
572
|
+
return path === undefined ? [] : readActionSteps(path, "--steps", knowledgeAt());
|
|
530
573
|
};
|
|
531
574
|
|
|
532
575
|
const COMMANDS: CommandSpec[] = [
|
|
@@ -534,14 +577,14 @@ const COMMANDS: CommandSpec[] = [
|
|
|
534
577
|
name: "inspect",
|
|
535
578
|
summary: "Packing evidence: the BASIC start and the entropy of the bytes.",
|
|
536
579
|
subjects: [PROGRAM],
|
|
537
|
-
run: (call) => inspect(
|
|
580
|
+
run: (call) => inspect(readProgram(call.subject(0))),
|
|
538
581
|
},
|
|
539
582
|
{
|
|
540
583
|
name: "trace",
|
|
541
584
|
summary: "Runs the program and reports the memory that it wrote and then executed.",
|
|
542
585
|
subjects: [PROGRAM],
|
|
543
|
-
options: { frames: FRAMES, entry: ENTRY, steps: STEPS },
|
|
544
|
-
run: async (call) => outcome(await trace(call.subject(0), { frames: call.count("frames")!, steps: stepsOf(call), ...entryOf(call) })),
|
|
586
|
+
options: { frames: FRAMES, entry: ENTRY, steps: STEPS, "max-listed": OPTIONS["max-listed"] },
|
|
587
|
+
run: async (call) => outcome(await trace(call.subject(0), { frames: call.count("frames")!, steps: stepsOf(call), max: call.count("max-listed")!, ...entryOf(call) })),
|
|
545
588
|
},
|
|
546
589
|
{
|
|
547
590
|
name: "stages",
|
|
@@ -552,7 +595,8 @@ const COMMANDS: CommandSpec[] = [
|
|
|
552
595
|
run: async (call) => {
|
|
553
596
|
const steps = stepsOf(call);
|
|
554
597
|
// A runUntil step to a raster line or a memory value cannot be tracked in chunks of frames; capture takes it.
|
|
555
|
-
|
|
598
|
+
const untracked = steps.findIndex((step) => "runUntil" in step && !trackedRunUntil(step));
|
|
599
|
+
if (untracked >= 0) throw new UsageError(`--steps ${call.string("steps")!}: step ${untracked + 1}: stages takes a runUntil step only with an address target without a condition. Use frames steps, or capture`);
|
|
556
600
|
return outcome(await stages(call.subject(0), { frames: call.count("frames")!, steps, minStage: call.count("min-stage")!, ...entryOf(call) }));
|
|
557
601
|
},
|
|
558
602
|
},
|
|
@@ -562,7 +606,7 @@ const COMMANDS: CommandSpec[] = [
|
|
|
562
606
|
subjects: [PROGRAM],
|
|
563
607
|
options: {
|
|
564
608
|
until: { kind: "address", value: "<address>", required: true, help: "the unpacked entry point" },
|
|
565
|
-
out: { ...OPTIONS.out, value: "<dir>", required: true, help: "the folder for the capture, relative to the project folder" },
|
|
609
|
+
out: { ...OPTIONS.out, output: "folder", value: "<dir>", required: true, help: "the folder for the capture, relative to the project folder" },
|
|
566
610
|
range: { ...OPTIONS.range, help: "one segment in place of the found ones" },
|
|
567
611
|
exclude: { kind: "range", value: "<start>-<end>", multiple: true, help: "a range to leave out of the segments, for example bytes that a crack left and the program never reads" },
|
|
568
612
|
"timeout-frames": { ...OPTIONS["timeout-frames"], max: 30_000, default: 3000, help: "stop the wait for --until after n frames (default 3000)" },
|
|
@@ -573,7 +617,14 @@ const COMMANDS: CommandSpec[] = [
|
|
|
573
617
|
notes: "entry_state.json has the registers, $00/$01, pages 0-3, the VIC, SID and CIA registers, the vectors, and what differs from a clean reset.",
|
|
574
618
|
run: async (call) => {
|
|
575
619
|
const range = call.range("range");
|
|
576
|
-
|
|
620
|
+
const until = call.address("until")!;
|
|
621
|
+
// A segment must hold the entry: refused before the emulator runs.
|
|
622
|
+
const show = (item: AddressRange) => `${formatC64Address(item.start)}-${formatC64Address(item.end)}`;
|
|
623
|
+
const holds = (item: AddressRange) => until >= item.start && until <= item.end;
|
|
624
|
+
const excluded = call.ranges("exclude").find(holds);
|
|
625
|
+
if (excluded !== undefined) throw new UsageError(`--exclude ${show(excluded)} holds the entry --until ${formatC64Address(until)}. A segment must hold the entry: give an --exclude range without it`);
|
|
626
|
+
if (range !== undefined && !holds(range)) throw new UsageError(`--range ${show(range)} does not hold the entry --until ${formatC64Address(until)}. Give a --range that holds the entry`);
|
|
627
|
+
return outcome(await capture(call.subject(0), { until, out: call.string("out")!, ...(range === undefined ? {} : { range }), exclude: call.ranges("exclude"), timeoutFrames: call.count("timeout-frames")!, steps: stepsOf(call), minStage: call.count("min-stage")!, ...entryOf(call) }));
|
|
577
628
|
},
|
|
578
629
|
},
|
|
579
630
|
{
|
|
@@ -582,7 +633,7 @@ const COMMANDS: CommandSpec[] = [
|
|
|
582
633
|
subjects: [{ name: "<program>", help: "a PRG, or a disk, tape or cartridge image, relative to the project folder" }],
|
|
583
634
|
options: {
|
|
584
635
|
steps: { ...STEPS, required: true, help: "action steps and capture steps (JSON)" },
|
|
585
|
-
out: { ...OPTIONS.out, value: "<dir>", required: true, help: "the folder for the captures, relative to the project folder" },
|
|
636
|
+
out: { ...OPTIONS.out, output: "folder", value: "<dir>", required: true, help: "the folder for the captures, relative to the project folder" },
|
|
586
637
|
drive: { kind: "flag", help: "also write the 2 KiB drive RAM as drive8.prg, for knowledge space drive8" },
|
|
587
638
|
"min-block": { kind: "count", value: "<bytes>", min: 1, max: 0x10000, default: 64, help: "write a changed block of at least this many bytes (default 64)" },
|
|
588
639
|
},
|
package/src/host/vice/adapter.ts
CHANGED
|
@@ -59,7 +59,10 @@ export function conditionExpression(condition: Condition, space: Space): string
|
|
|
59
59
|
}
|
|
60
60
|
case "memory": {
|
|
61
61
|
if (condition.space !== "c64") {
|
|
62
|
-
throw new WireFailure(
|
|
62
|
+
throw new WireFailure(
|
|
63
|
+
"unsupported-in-space",
|
|
64
|
+
"A memory target or a memory condition tests only space c64 memory. For the drive, use a watchpoint in space drive8, or a c64_run_until address target in space drive8.",
|
|
65
|
+
);
|
|
63
66
|
}
|
|
64
67
|
return `(@${condition.view}:${formatC64Address(condition.address)} ${operator(condition.operator)} ${formatC64Byte(condition.value)})`;
|
|
65
68
|
}
|