@henols/c64-re-tools 2.0.0-rc.6 → 2.0.0-rc.7
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 +1 -1
- package/skills/c64-basic/SKILL.md +1 -1
- package/skills/c64-disk/SKILL.md +3 -3
- package/skills/c64-disk/scripts/disk.ts +2 -1
- package/skills/c64-emulator/SKILL.md +3 -1
- package/skills/c64-knowledge/SKILL.md +1 -1
- package/skills/c64-provenance/SKILL.md +7 -5
- package/skills/c64-provenance/scripts/compare.ts +26 -0
- package/skills/c64-provenance/scripts/provenance.ts +24 -10
- package/skills/c64-provenance/scripts/separate.ts +12 -4
- package/skills/c64-reverse-engineering/SKILL.md +2 -2
- package/skills/c64-source/SKILL.md +15 -9
- package/skills/c64-source/scripts/generate.ts +36 -10
- package/skills/c64-source/scripts/loader.ts +88 -6
- package/skills/c64-source/scripts/source.ts +40 -9
- package/skills/c64-static-analysis/SKILL.md +4 -4
- package/skills/c64-static-analysis/scripts/analyze.ts +13 -6
- package/skills/c64-static-analysis/scripts/coverage.ts +4 -4
- package/skills/c64-static-analysis/scripts/idioms.ts +14 -9
- package/skills/c64-testing/SKILL.md +1 -1
- package/skills/c64-testing/scripts/scenario.ts +2 -2
- package/skills/c64-unpacker/SKILL.md +9 -7
- package/skills/c64-unpacker/scripts/loads.ts +12 -1
- package/skills/c64-unpacker/scripts/stages.ts +66 -3
- package/skills/c64-unpacker/scripts/unpack.ts +96 -30
- package/src/c64.ts +14 -5
- package/src/cli/status.ts +26 -3
- package/src/host/tools/c1541.ts +34 -5
- package/src/host/tools/petcat.ts +5 -2
- package/src/host/tools/status.ts +6 -3
- package/src/host/vice/adapter.ts +23 -9
- package/src/host/vice/process.ts +7 -2
- package/src/host/vice/session.ts +47 -10
- package/src/host-client/connect.ts +10 -10
- package/src/host-client/coverage.ts +19 -10
- package/src/host-client/tools.ts +5 -5
- package/src/host-client/vice-session.ts +1 -1
- package/src/knowledge/schema.ts +7 -0
- package/src/knowledge/write.ts +5 -0
- package/src/mcp/tools/debug.ts +3 -2
- package/src/mcp/tools/machine.ts +4 -1
- package/src/mcp/tools/media.ts +5 -1
- package/src/mcp/tools/video.ts +2 -0
- package/src/native/discover.ts +2 -1
- package/src/platform.ts +3 -2
- package/src/protocol/tools.ts +19 -4
- package/src/protocol/vice.ts +11 -2
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
// starts only where the CPU executes an opcode byte that the current stage
|
|
5
5
|
// wrote before the CPU got there. Code that the stage executed before it wrote
|
|
6
6
|
// it is the stage's own code: it stops the run once, and then a write watch
|
|
7
|
-
// over it arms it again.
|
|
7
|
+
// over it arms it again. Written code that the stage calls, and that returns
|
|
8
|
+
// to the stage, is the stage's own code too (D59): a packer hands off by JMP,
|
|
9
|
+
// RTS or RTI and never runs its own code again.
|
|
8
10
|
|
|
9
11
|
import { COVERAGE } from "#src/c64.ts";
|
|
10
12
|
import { writtenBlocks } from "#src/host-client/coverage.ts";
|
|
@@ -26,12 +28,13 @@ export function mergeMaps(into: Uint8Array, from: Uint8Array): void {
|
|
|
26
28
|
* code that can start a stage: every executed address except own code that
|
|
27
29
|
* the stage has not written.
|
|
28
30
|
*/
|
|
29
|
-
export function stageCandidates(stage: Uint8Array, chunk: Uint8Array, own: ReadonlySet<number>, minimum: number): Range[] {
|
|
31
|
+
export function stageCandidates(stage: Uint8Array, chunk: Uint8Array, own: ReadonlySet<number>, minimum: number, called: readonly Range[] = []): Range[] {
|
|
30
32
|
const combined = Uint8Array.from(stage);
|
|
31
33
|
mergeMaps(combined, chunk);
|
|
32
34
|
return writtenBlocks(combined, minimum).filter((block) => {
|
|
33
35
|
for (let address = block.start; address <= block.end; address++) {
|
|
34
36
|
if ((chunk[address]! & COVERAGE.ramExecute) === 0) continue;
|
|
37
|
+
if (inRanges(called, address)) continue;
|
|
35
38
|
if (own.has(address) && (combined[address]! & COVERAGE.ramWrite) === 0) continue;
|
|
36
39
|
return true;
|
|
37
40
|
}
|
|
@@ -44,7 +47,7 @@ export function stageCandidates(stage: Uint8Array, chunk: Uint8Array, own: Reado
|
|
|
44
47
|
* them except own code that is not written yet, and write watches over that
|
|
45
48
|
* own code.
|
|
46
49
|
*/
|
|
47
|
-
export function stagePoints(blocks: readonly Range[], own: ReadonlySet<number>, written: (address: number) => boolean): { exec: Range[]; watch: Range[] } {
|
|
50
|
+
export function stagePoints(blocks: readonly Range[], own: ReadonlySet<number>, written: (address: number) => boolean, called: readonly Range[] = []): { exec: Range[]; watch: Range[] } {
|
|
48
51
|
const exec: Range[] = [];
|
|
49
52
|
const watch: Range[] = [];
|
|
50
53
|
const add = (list: Range[], address: number) => {
|
|
@@ -54,6 +57,7 @@ export function stagePoints(blocks: readonly Range[], own: ReadonlySet<number>,
|
|
|
54
57
|
};
|
|
55
58
|
for (const block of blocks) {
|
|
56
59
|
for (let address = block.start; address <= block.end; address++) {
|
|
60
|
+
if (inRanges(called, address)) continue;
|
|
57
61
|
if (own.has(address) && !written(address)) add(watch, address);
|
|
58
62
|
else add(exec, address);
|
|
59
63
|
}
|
|
@@ -65,3 +69,62 @@ export function stagePoints(blocks: readonly Range[], own: ReadonlySet<number>,
|
|
|
65
69
|
export function inRanges(ranges: readonly Range[], address: number): boolean {
|
|
66
70
|
return ranges.some((range) => contains(range, address));
|
|
67
71
|
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Where a call returns to the stage (D59): the return address on top of the
|
|
75
|
+
* stack, when a JSR that the stage executed pushed it. `stack` is page 1.
|
|
76
|
+
* Returns the address of the instruction after that JSR.
|
|
77
|
+
*/
|
|
78
|
+
export function callReturn(stack: Uint8Array, sp: number, byteAt: (address: number) => number, executed: (address: number) => boolean): number | undefined {
|
|
79
|
+
const pushed = stack[(sp + 1) & 0xff]! | (stack[(sp + 2) & 0xff]! << 8);
|
|
80
|
+
const jsr = (pushed - 2) & 0xffff;
|
|
81
|
+
if (byteAt(jsr) !== 0x20 || !executed(jsr)) return undefined;
|
|
82
|
+
return (pushed + 1) & 0xffff;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The code that ran in a block: its executed opcode bytes with the two bytes
|
|
87
|
+
* after each, which can be operands, joined and cut at the block's end.
|
|
88
|
+
*/
|
|
89
|
+
export function executedIn(map: Uint8Array, block: Range): Range[] {
|
|
90
|
+
const ranges: Range[] = [];
|
|
91
|
+
for (let address = block.start; address <= block.end; address++) {
|
|
92
|
+
if ((map[address]! & COVERAGE.ramExecute) === 0) continue;
|
|
93
|
+
const end = Math.min(block.end, address + 2);
|
|
94
|
+
const last = ranges[ranges.length - 1];
|
|
95
|
+
if (last !== undefined && address <= last.end + 1) last.end = Math.max(last.end, end);
|
|
96
|
+
else ranges.push({ start: address, end });
|
|
97
|
+
}
|
|
98
|
+
return ranges;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The segments of a capture: the blocks that the stages wrote, without loader
|
|
103
|
+
* leftovers, joined into ranges that do not overlap or touch (F-26). A block
|
|
104
|
+
* is a leftover when it holds the entry of an intermediate stage that started
|
|
105
|
+
* after the block's writer, and no write from that stage on replaced the code
|
|
106
|
+
* at that entry (R2-01). Code that a stage ran before a later stage wrote over
|
|
107
|
+
* it never drops a block, and the block that holds the capture entry is always
|
|
108
|
+
* a segment. The filter comes before the join: a join first would drop the
|
|
109
|
+
* program with a leftover. `writtenBy` is the last stage that wrote into the
|
|
110
|
+
* range.
|
|
111
|
+
*/
|
|
112
|
+
export function captureSegments(stages: ReadonlyArray<{ entry: number; wrote: readonly Range[] }>, entry: number): Array<Range & { writtenBy: number }> {
|
|
113
|
+
const last = stages.length - 1;
|
|
114
|
+
const replaced = (from: number, address: number) => stages.slice(from).some((stage) => inRanges(stage.wrote, address));
|
|
115
|
+
const leftover = (block: Range, writer: number) =>
|
|
116
|
+
!contains(block, entry) && stages.some((stage, index) => index > writer && index < last && contains(block, stage.entry) && !replaced(index, stage.entry));
|
|
117
|
+
const blocks = stages
|
|
118
|
+
.flatMap((stage, index) => stage.wrote.map((range) => ({ start: range.start, end: range.end, writtenBy: index })))
|
|
119
|
+
.filter((block) => !leftover(block, block.writtenBy))
|
|
120
|
+
.sort((a, b) => a.start - b.start);
|
|
121
|
+
const segments: Array<Range & { writtenBy: number }> = [];
|
|
122
|
+
for (const block of blocks) {
|
|
123
|
+
const previous = segments[segments.length - 1];
|
|
124
|
+
if (previous !== undefined && block.start <= previous.end + 1) {
|
|
125
|
+
previous.end = Math.max(previous.end, block.end);
|
|
126
|
+
previous.writtenBy = Math.max(previous.writtenBy, block.writtenBy);
|
|
127
|
+
} else segments.push({ ...block });
|
|
128
|
+
}
|
|
129
|
+
return segments;
|
|
130
|
+
}
|
|
@@ -19,7 +19,7 @@ import { resolveProjectPath } from "#src/project.ts";
|
|
|
19
19
|
import { runScript, UsageError } from "#src/script.ts";
|
|
20
20
|
import { basicStart, inspect, PackingError, readMemoryMap, underRom, writtenThenExecuted } from "./evidence.ts";
|
|
21
21
|
import { loads, readLoadSteps } from "./loads.ts";
|
|
22
|
-
import { inRanges, mergeMaps, stageCandidates, stagePoints, type Range } from "./stages.ts";
|
|
22
|
+
import { callReturn, captureSegments, executedIn, inRanges, mergeMaps, stageCandidates, stagePoints, type Range } from "./stages.ts";
|
|
23
23
|
import { entryState, IO_AREAS, json, machineState, readRam, READY_FRAMES, vectorsOf } from "./machine.ts";
|
|
24
24
|
|
|
25
25
|
const USAGE = `unpack.ts inspect <program.prg>
|
|
@@ -31,7 +31,7 @@ unpack.ts loads <program> --steps <file> --out <dir> [--drive] [--min-block <byt
|
|
|
31
31
|
inspect packing evidence: the BASIC start and the entropy of the bytes
|
|
32
32
|
trace run the program for <n> frames (default 3000) and report the memory
|
|
33
33
|
that it wrote and then executed; --entry starts it there instead of RUN
|
|
34
|
-
capture run the program until the CPU gets to <address
|
|
34
|
+
capture run the program until the CPU gets to <address> after the steps, and write into the
|
|
35
35
|
directory --out: image.bin (64 KiB of RAM), image.json (the entry,
|
|
36
36
|
the segments that the stages wrote, the vectors, the stages), a PRG
|
|
37
37
|
for each segment, and entry_state.json (registers, $00/$01, pages
|
|
@@ -54,7 +54,8 @@ loads run a multi-load program (a PRG, or a disk, tape or cartridge image)
|
|
|
54
54
|
--steps <file> a JSON list of input steps that run right after the program
|
|
55
55
|
starts: write, registers, joystick, type, frames and runUntil, as in
|
|
56
56
|
c64-testing scenarios, with $ addresses only. Use it to press a key
|
|
57
|
-
in an intro or to get through a menu.
|
|
57
|
+
in an intro or to get through a menu. stages and capture track the
|
|
58
|
+
stages in frames steps and in runUntil steps with at alone.
|
|
58
59
|
|
|
59
60
|
Addresses are $ and four hex digits. The result is one JSON object.`;
|
|
60
61
|
|
|
@@ -200,6 +201,8 @@ const MAX_STAGES = 16;
|
|
|
200
201
|
/** Most stops of one re-run of a chunk: own code and writes over it each stop once. */
|
|
201
202
|
const MAX_STAGE_STOPS = 4096;
|
|
202
203
|
const STAGE_SNAPSHOT = "stage-chunk";
|
|
204
|
+
/** The snapshot at a call into written code, while the run checks if the call returns (D59). */
|
|
205
|
+
const CALL_SNAPSHOT = "stage-call";
|
|
203
206
|
|
|
204
207
|
interface Stage {
|
|
205
208
|
entry: number;
|
|
@@ -218,14 +221,20 @@ interface Followed {
|
|
|
218
221
|
failed?: string;
|
|
219
222
|
}
|
|
220
223
|
|
|
224
|
+
/** A runUntil step to an address alone: a breakpoint stops it, so the stages are tracked while it runs. */
|
|
225
|
+
function trackedRunUntil(step: ActionStep): step is { runUntil: { at: string }; timeoutFrames: number } {
|
|
226
|
+
return "runUntil" in step && "at" in step.runUntil && !("raster" in step.runUntil);
|
|
227
|
+
}
|
|
228
|
+
|
|
221
229
|
/**
|
|
222
230
|
* Starts the program and follows its stages (D43). The memory map holds what
|
|
223
231
|
* the current stage wrote; when the CPU executed a block of at least
|
|
224
232
|
* `minStage` written bytes during a chunk of frames, the chunk runs again from
|
|
225
233
|
* its snapshot with exec breakpoints over those blocks, which stop at the
|
|
226
234
|
* first instruction of the next stage. Steps run in the tracked time: a frames
|
|
227
|
-
* step
|
|
228
|
-
* run ends when the CPU gets there
|
|
235
|
+
* step and a runUntil step to an address alone are tracked, the others act
|
|
236
|
+
* between two chunks. With `until`, the run ends when the CPU gets there
|
|
237
|
+
* after the steps.
|
|
229
238
|
*/
|
|
230
239
|
async function follow(
|
|
231
240
|
client: ViceSessionClient,
|
|
@@ -241,7 +250,6 @@ async function follow(
|
|
|
241
250
|
await client.memmap({ action: "clear" });
|
|
242
251
|
if (options.entry !== undefined) await client.registersSet("c64", { pc: options.entry });
|
|
243
252
|
else await client.keyboard(textToPetscii("RUN\n"));
|
|
244
|
-
const target = options.until === undefined ? undefined : await client.breakpoint({ action: "add", address: options.until, space: "c64" });
|
|
245
253
|
const found: Stage[] = [];
|
|
246
254
|
const start = options.entry ?? basic!.sys?.address ?? (await firstPcAfterBasic(client, basic!.basicEnd, load + bytes.length - 3));
|
|
247
255
|
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>.` };
|
|
@@ -249,16 +257,17 @@ async function follow(
|
|
|
249
257
|
let firstFrame = 0;
|
|
250
258
|
let total = 0;
|
|
251
259
|
let ended = false;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
260
|
+
/** Where the run stops: the target of a runUntil step while it runs, then until (R2-06). */
|
|
261
|
+
let stopAt: number | undefined;
|
|
262
|
+
const atTarget = (pc: number) => stopAt !== undefined && pc === stopAt;
|
|
263
|
+
// The current stage: its memory map since it started, chunk by chunk, its own code, and the written code it calls (D59).
|
|
264
|
+
let stage = { map: new Uint8Array(0x10000), own: new Set<number>(), called: [] as Range[] };
|
|
256
265
|
const startStage = async (next: number, last: Uint8Array) => {
|
|
257
266
|
mergeMaps(stage.map, last);
|
|
258
267
|
found.push({ entry, firstFrame, frames: total - firstFrame, wrote: writtenBlocks(stage.map, options.minStage), next });
|
|
259
268
|
entry = next;
|
|
260
269
|
firstFrame = total;
|
|
261
|
-
stage = { map: new Uint8Array(0x10000), own: new Set<number>() };
|
|
270
|
+
stage = { map: new Uint8Array(0x10000), own: new Set<number>(), called: [] };
|
|
262
271
|
await client.memmap({ action: "clear" });
|
|
263
272
|
};
|
|
264
273
|
|
|
@@ -286,6 +295,31 @@ async function follow(
|
|
|
286
295
|
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 });
|
|
287
296
|
}
|
|
288
297
|
};
|
|
298
|
+
/**
|
|
299
|
+
* At written code that a JSR of the stage called: runs on until the call
|
|
300
|
+
* returns, in the frames left. Gives the frames used when it returns;
|
|
301
|
+
* otherwise the machine goes back to the stop and nothing is given.
|
|
302
|
+
*/
|
|
303
|
+
const returns = async (map: Uint8Array, frames: number): Promise<number | undefined> => {
|
|
304
|
+
const { sp } = await client.registersGet("c64");
|
|
305
|
+
const stack = await readRam(client, 0x0100, 0x100);
|
|
306
|
+
const pushed = stack[(sp + 1) & 0xff]! | (stack[(sp + 2) & 0xff]! << 8);
|
|
307
|
+
const [opcode] = await readRam(client, (pushed - 2) & 0xffff, 1);
|
|
308
|
+
const back = callReturn(stack, sp, () => opcode!, (address) => ((stage.map[address]! | map[address]!) & COVERAGE.ramExecute) !== 0);
|
|
309
|
+
if (back === undefined) return undefined;
|
|
310
|
+
await arm({ exec: [], watch: [] });
|
|
311
|
+
await client.snapshot({ action: "save", name: CALL_SNAPSHOT });
|
|
312
|
+
const point = await client.breakpoint({ action: "add", address: back, space: "c64" });
|
|
313
|
+
try {
|
|
314
|
+
const run = await client.execution({ action: "advance-frames", count: frames, space: "c64" });
|
|
315
|
+
if ((run.pc ?? (await client.registersGet("c64")).pc) === back) return run.advancedFrames ?? 0;
|
|
316
|
+
await client.snapshot({ action: "restore", name: CALL_SNAPSHOT });
|
|
317
|
+
return undefined;
|
|
318
|
+
} finally {
|
|
319
|
+
await client.breakpoint({ action: "remove", id: point.id });
|
|
320
|
+
await client.snapshot({ action: "discard", name: CALL_SNAPSHOT });
|
|
321
|
+
}
|
|
322
|
+
};
|
|
289
323
|
let left = frames;
|
|
290
324
|
let used = 0;
|
|
291
325
|
let map: Uint8Array = new Uint8Array(0x10000);
|
|
@@ -295,15 +329,27 @@ async function follow(
|
|
|
295
329
|
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.`);
|
|
296
330
|
}
|
|
297
331
|
const written = (address: number) => ((stage.map[address]! | map[address]!) & COVERAGE.ramWrite) !== 0;
|
|
298
|
-
await arm(stagePoints(candidates, stage.own, written));
|
|
332
|
+
await arm(stagePoints(candidates, stage.own, written, stage.called));
|
|
299
333
|
const run = await client.execution({ action: "advance-frames", count: left, space: "c64" });
|
|
300
334
|
const advanced = run.advancedFrames ?? 0;
|
|
301
335
|
used += advanced;
|
|
302
336
|
left -= advanced;
|
|
303
337
|
const pc = run.pc ?? (await client.registersGet("c64")).pc;
|
|
304
338
|
map = await readCoverage(client);
|
|
305
|
-
|
|
306
|
-
|
|
339
|
+
// A frame can end in written code that the stage called before (D59): that stop starts no stage.
|
|
340
|
+
if (inRanges(candidates, pc) && !inRanges(stage.called, pc)) {
|
|
341
|
+
if (written(pc)) {
|
|
342
|
+
const back = left > 0 && !atTarget(pc) ? await returns(map, left) : undefined;
|
|
343
|
+
if (back === undefined) return { used, map, next: pc, atTarget: atTarget(pc) };
|
|
344
|
+
used += back;
|
|
345
|
+
left -= back;
|
|
346
|
+
map = await readCoverage(client);
|
|
347
|
+
// The stage called the block and got the CPU back: the code that ran there is its own code.
|
|
348
|
+
const block = candidates.find((range) => pc >= range.start && pc <= range.end)!;
|
|
349
|
+
stage.called.push(...executedIn(map, block));
|
|
350
|
+
if (left <= 0) return { used, map, atTarget: false };
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
307
353
|
stage.own.add(pc);
|
|
308
354
|
}
|
|
309
355
|
if (atTarget(pc)) return { used, map, atTarget: true };
|
|
@@ -317,28 +363,30 @@ async function follow(
|
|
|
317
363
|
}
|
|
318
364
|
};
|
|
319
365
|
|
|
320
|
-
|
|
366
|
+
/** Tracks the stages for some frames; true when the CPU got to stopAt. */
|
|
367
|
+
const track = async (frames: number): Promise<boolean> => {
|
|
321
368
|
let left = frames;
|
|
322
|
-
|
|
369
|
+
let stopped = false;
|
|
370
|
+
while (left > 0 && !ended && !stopped && found.length < MAX_STAGES) {
|
|
323
371
|
await client.snapshot({ action: "save", name: STAGE_SNAPSHOT });
|
|
324
372
|
await client.memmap({ action: "clear" });
|
|
325
373
|
const chunk = Math.min(STAGE_CHUNK_FRAMES, left);
|
|
326
374
|
const first = await client.execution({ action: "advance-frames", count: chunk, space: "c64" });
|
|
327
375
|
const firstPc = first.pc ?? (await client.registersGet("c64")).pc;
|
|
328
376
|
const map = await readCoverage(client);
|
|
329
|
-
const candidates = stageCandidates(stage.map, map, stage.own, options.minStage);
|
|
377
|
+
const candidates = stageCandidates(stage.map, map, stage.own, options.minStage, stage.called);
|
|
330
378
|
if (candidates.length === 0) {
|
|
331
379
|
const advanced = first.advancedFrames ?? 0;
|
|
332
380
|
total += advanced;
|
|
333
381
|
left -= advanced;
|
|
334
382
|
if (atTarget(firstPc)) {
|
|
335
|
-
// The stop at
|
|
383
|
+
// The stop at a target comes before its instruction runs: a target that the stage wrote starts a stage too.
|
|
336
384
|
const combined = Uint8Array.from(stage.map);
|
|
337
385
|
mergeMaps(combined, map);
|
|
338
386
|
const wrote = (combined[firstPc]! & COVERAGE.ramWrite) !== 0 && inRanges(writtenBlocks(combined, options.minStage), firstPc);
|
|
339
387
|
if (wrote) await startStage(firstPc, map);
|
|
340
388
|
else mergeMaps(stage.map, map);
|
|
341
|
-
|
|
389
|
+
stopped = true;
|
|
342
390
|
} else {
|
|
343
391
|
mergeMaps(stage.map, map);
|
|
344
392
|
if (advanced === 0) ended = true;
|
|
@@ -353,15 +401,35 @@ async function follow(
|
|
|
353
401
|
left -= again.used;
|
|
354
402
|
if (again.next !== undefined) await startStage(again.next, again.map);
|
|
355
403
|
else mergeMaps(stage.map, again.map);
|
|
356
|
-
if (again.atTarget)
|
|
404
|
+
if (again.atTarget) stopped = true;
|
|
357
405
|
}
|
|
406
|
+
return stopped;
|
|
358
407
|
};
|
|
359
|
-
|
|
408
|
+
/** Tracks the stages until the CPU gets to an address, with a breakpoint there. */
|
|
409
|
+
const trackTo = async (address: number, frames: number): Promise<boolean> => {
|
|
410
|
+
stopAt = address;
|
|
411
|
+
const point = await client.breakpoint({ action: "add", address, space: "c64" });
|
|
412
|
+
try {
|
|
413
|
+
return await track(frames);
|
|
414
|
+
} finally {
|
|
415
|
+
await client.breakpoint({ action: "remove", id: point.id });
|
|
416
|
+
stopAt = undefined;
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
// 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.
|
|
420
|
+
for (const [index, step] of options.steps.entries()) {
|
|
360
421
|
if ("frames" in step) await track(step.frames);
|
|
361
|
-
else
|
|
422
|
+
else if (trackedRunUntil(step)) {
|
|
423
|
+
if (!(await trackTo(parseC64Address(step.runUntil.at), step.timeoutFrames))) {
|
|
424
|
+
const { pc } = await client.registersGet("c64");
|
|
425
|
+
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)}.` };
|
|
426
|
+
}
|
|
427
|
+
} else await runActionStep(client, step, parseC64Address);
|
|
362
428
|
}
|
|
363
|
-
|
|
364
|
-
|
|
429
|
+
const left = Math.max(0, options.frames - total);
|
|
430
|
+
let reached = false;
|
|
431
|
+
if (options.until === undefined) await track(left);
|
|
432
|
+
else reached = await trackTo(options.until, left);
|
|
365
433
|
await client.snapshot({ action: "discard", name: STAGE_SNAPSHOT }).catch(() => undefined);
|
|
366
434
|
mergeMaps(stage.map, await readCoverage(client));
|
|
367
435
|
found.push({ entry, firstFrame, frames: total - firstFrame, wrote: writtenBlocks(stage.map, options.minStage) });
|
|
@@ -417,10 +485,8 @@ async function capture(
|
|
|
417
485
|
// The RAM view: unpacked code is often under the ROMs or the I/O area.
|
|
418
486
|
const memory = await readRam(client, 0x0000, 0x10000);
|
|
419
487
|
const state = await machineState(client);
|
|
420
|
-
// The segments: what the stages wrote, without
|
|
421
|
-
const
|
|
422
|
-
const written = followed.stages.flatMap((stage, index) => stage.wrote.map((range) => ({ ...range, writtenBy: index })));
|
|
423
|
-
const segments = options.range !== undefined ? [{ start: options.range[0], end: options.range[1] }] : written.filter((range) => !earlier.some((entry) => entry >= range.start && entry <= range.end));
|
|
488
|
+
// The segments: what the stages wrote, without loader leftovers, with no overlap.
|
|
489
|
+
const segments = options.range !== undefined ? [{ start: options.range[0], end: options.range[1] }] : captureSegments(followed.stages, registers.pc);
|
|
424
490
|
const file = (path: string) => `${options.out}/${path}`;
|
|
425
491
|
const write = (path: string, bytes: Uint8Array) => writeProjectFile(file(path), bytes, { what: "--out" });
|
|
426
492
|
write("image.bin", memory);
|
|
@@ -533,8 +599,8 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
|
|
|
533
599
|
const minStage = values["min-stage"] === undefined ? 64 : Number(values["min-stage"]);
|
|
534
600
|
if (!Number.isInteger(minStage) || minStage < 1 || minStage > 0x10000) throw new UsageError("--min-stage must be from 1 to 65536");
|
|
535
601
|
const steps = values.steps === undefined ? [] : readSteps(values.steps);
|
|
536
|
-
// A runUntil step cannot be tracked in chunks of frames; capture takes it.
|
|
537
|
-
if (steps.some((step) => "runUntil" in step)) throw new UsageError("stages takes
|
|
602
|
+
// A runUntil step to a raster line or a memory value cannot be tracked in chunks of frames; capture takes it.
|
|
603
|
+
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");
|
|
538
604
|
return stages(program, { frames, steps, minStage, ...(values.entry === undefined ? {} : { entry: address(values.entry, "--entry") }) });
|
|
539
605
|
}
|
|
540
606
|
if (command === "loads") {
|
package/src/c64.ts
CHANGED
|
@@ -153,29 +153,38 @@ export type BasicHandoff =
|
|
|
153
153
|
*/
|
|
154
154
|
export type TokenizedProgram = { loadAddress: number; basicEnd: number; textBytes: number; lines: TokenizedLine[] } | { reason: string };
|
|
155
155
|
|
|
156
|
+
/** BASIC text cannot start below the screen: there are the zero page, the stack and the system pages. */
|
|
157
|
+
const MIN_BASIC_LOAD = 0x0400;
|
|
158
|
+
|
|
156
159
|
/**
|
|
157
160
|
* Splits a PRG into BASIC lines the way BASIC relinks a loaded program: each
|
|
158
161
|
* line ends at a zero byte, and a link whose high byte is zero ends the
|
|
159
|
-
* program.
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
+
* program. A real end marker is two zero bytes; when only the high byte is
|
|
163
|
+
* zero, the "marker" is the first machine code after the BASIC text (SYS
|
|
164
|
+
* lines that jump right behind themselves). The stored links are not read:
|
|
165
|
+
* BASIC relinks a loaded program, and a crack can store odd links to hide
|
|
166
|
+
* lines from LIST. A load address below $0400 is a memory dump (F-12).
|
|
162
167
|
*/
|
|
163
168
|
export function parseTokenized(program: Uint8Array): TokenizedProgram {
|
|
164
169
|
if (program.length < 4) return { reason: "The file is too short for a BASIC program." };
|
|
165
170
|
const loadAddress = program[0]! | (program[1]! << 8);
|
|
171
|
+
if (loadAddress < MIN_BASIC_LOAD) {
|
|
172
|
+
return { reason: `The load address ${formatC64Address(loadAddress)} is below $0400, in the zero page, the stack or the system pages. The file is a memory dump or machine code, not a BASIC program.` };
|
|
173
|
+
}
|
|
166
174
|
const lines: TokenizedLine[] = [];
|
|
167
175
|
let at = 2;
|
|
176
|
+
const ended = (basicEnd: number): TokenizedProgram => ({ loadAddress, basicEnd, textBytes: at, lines });
|
|
168
177
|
// File offset k (from 2 on) loads at loadAddress + k - 2.
|
|
169
178
|
for (;;) {
|
|
170
179
|
// A file that ends right after a line leaves the end marker to the zeros after it in memory;
|
|
171
180
|
// basicEnd is the address after that marker, as when the file holds it.
|
|
172
|
-
if (at === program.length && lines.length > 0) return
|
|
181
|
+
if (at === program.length && lines.length > 0) return ended(loadAddress + at);
|
|
173
182
|
if (at + 1 >= program.length) return { reason: "The BASIC program has no end marker." };
|
|
174
183
|
if (program[at + 1] === 0) {
|
|
175
184
|
// An end marker before any line, with more bytes after it, is how machine code at a non-BASIC address often starts.
|
|
176
185
|
if (lines.length === 0 && at + 2 < program.length) return { reason: "The file holds no BASIC line before other bytes, so it is no BASIC program." };
|
|
177
186
|
const marker = program[at] === 0;
|
|
178
|
-
return
|
|
187
|
+
return ended(loadAddress + (marker ? at : at - 2));
|
|
179
188
|
}
|
|
180
189
|
if (at + 4 > program.length) return { reason: "The last BASIC line is cut off." };
|
|
181
190
|
const number = program[at + 2]! | (program[at + 3]! << 8);
|
package/src/cli/status.ts
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
// Runtime with its tools.
|
|
3
3
|
|
|
4
4
|
import { hostTools, type ToolCallOptions } from "../host-client/tools.ts";
|
|
5
|
+
import { viceVersionOf } from "../native/discover.ts";
|
|
6
|
+
import { PACKAGE } from "../package.ts";
|
|
5
7
|
import { STOP_AND_ASK } from "../protocol/messages.ts";
|
|
6
|
-
import type { ToolStatus } from "../protocol/tools.ts";
|
|
8
|
+
import type { HostInfo, ToolStatus } from "../protocol/tools.ts";
|
|
7
9
|
|
|
8
10
|
/** One line per tool under `heading`: its version, or why it does not run; then each known flaw of it. */
|
|
9
11
|
export function toolLines(heading: string, tools: readonly ToolStatus[]): string[] {
|
|
@@ -27,12 +29,33 @@ export function toolLines(heading: string, tools: readonly ToolStatus[]): string
|
|
|
27
29
|
* every tool on it runs.
|
|
28
30
|
*/
|
|
29
31
|
export async function hostStatusLines(options: ToolCallOptions = {}): Promise<{ lines: string[]; ready: boolean }> {
|
|
32
|
+
let host: HostInfo;
|
|
30
33
|
let tools: ToolStatus[];
|
|
31
34
|
try {
|
|
32
|
-
tools = await hostTools(options);
|
|
35
|
+
({ host, tools } = await hostTools(options));
|
|
33
36
|
} catch (error) {
|
|
34
37
|
// status is read by a person: the instruction for an agent (D37) stays out.
|
|
35
38
|
return { lines: [`Host Runtime: not reachable. ${(error as Error).message.replace(` ${STOP_AND_ASK}`, "")}`], ready: false };
|
|
36
39
|
}
|
|
37
|
-
|
|
40
|
+
const mixed = mixedViceBuilds(tools);
|
|
41
|
+
return {
|
|
42
|
+
lines: [
|
|
43
|
+
`Host Runtime: reachable, c64-re-tools ${host.version} on ${host.platform} ${host.release} (${host.arch})`,
|
|
44
|
+
...(host.version === PACKAGE.version ? [] : [` Warning: the Host Runtime is c64-re-tools ${host.version} and this is ${PACKAGE.version}. Start the host again so that both are the same version.`]),
|
|
45
|
+
...toolLines("Tools on the host:", tools),
|
|
46
|
+
...(mixed === undefined ? [] : [` Warning: ${mixed}`]),
|
|
47
|
+
],
|
|
48
|
+
ready: tools.every((tool) => tool.runs),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A warning when x64sc, c1541 and petcat that run come from different VICE versions or builds. */
|
|
53
|
+
export function mixedViceBuilds(tools: readonly ToolStatus[]): string | undefined {
|
|
54
|
+
const running = tools.filter((tool) => tool.runs && tool.version !== undefined);
|
|
55
|
+
const builds = running.map((tool) => {
|
|
56
|
+
const version = viceVersionOf(tool.version!);
|
|
57
|
+
return version === undefined ? tool.version! : `VICE ${version.major}.${version.minor}${version.revision === undefined ? "" : ` SVN r${version.revision}`}`;
|
|
58
|
+
});
|
|
59
|
+
if (new Set(builds).size <= 1) return undefined;
|
|
60
|
+
return `the VICE tools come from different VICE builds: ${running.map((tool) => `${tool.version} (${tool.path})`).join(", ")}. Put x64sc, c1541 and petcat of one VICE first on PATH, or set C64RT_VICE, C64RT_C1541 and C64RT_PETCAT.`;
|
|
38
61
|
}
|
package/src/host/tools/c1541.ts
CHANGED
|
@@ -37,11 +37,23 @@ const LAYOUTS: Record<string, Layout> = {
|
|
|
37
37
|
"1581": { header: { track: 40, sector: 0 }, name: 0x04, id: 0x16, dos: 0x19 },
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
+
/** Used sectors of the format outside the header and directory chain: the 1571 BAM track 53, the two 1581 BAM blocks. */
|
|
41
|
+
const RESERVED: Record<string, DiskSector[]> = {
|
|
42
|
+
"1541": [],
|
|
43
|
+
"1571": Array.from({ length: 19 }, (_, sector) => ({ track: 53, sector })),
|
|
44
|
+
"1581": [
|
|
45
|
+
{ track: 40, sector: 1 },
|
|
46
|
+
{ track: 40, sector: 2 },
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
|
|
40
50
|
export interface DiskEntry extends DiskFile {
|
|
41
51
|
/** The name's PETSCII bytes, up to the first shifted space ($a0). */
|
|
42
52
|
nameBytes: Uint8Array;
|
|
43
53
|
startTrack: number;
|
|
44
54
|
startSector: number;
|
|
55
|
+
/** REL only: the first side sector. */
|
|
56
|
+
sideStart?: DiskSector;
|
|
45
57
|
}
|
|
46
58
|
|
|
47
59
|
const damaged = (message: string) => new WireFailure("media-error", `The disk image is damaged: ${message}`);
|
|
@@ -115,7 +127,7 @@ export function parseDirectory(blocks: Buffer[], layout: Layout): { diskName: st
|
|
|
115
127
|
// Type 0 is an empty or scratched slot; the DOS lists no file there.
|
|
116
128
|
if (typeByte === 0) continue;
|
|
117
129
|
const nameBytes = nameField(block, offset + 5, 16);
|
|
118
|
-
|
|
130
|
+
const entry: DiskEntry = {
|
|
119
131
|
name: petsciiNameToText(nameBytes),
|
|
120
132
|
nameBytes,
|
|
121
133
|
type: fileType(typeByte),
|
|
@@ -124,7 +136,10 @@ export function parseDirectory(blocks: Buffer[], layout: Layout): { diskName: st
|
|
|
124
136
|
locked: (typeByte & 0x40) !== 0,
|
|
125
137
|
startTrack: block.readUInt8(offset + 3),
|
|
126
138
|
startSector: block.readUInt8(offset + 4),
|
|
127
|
-
}
|
|
139
|
+
};
|
|
140
|
+
const side = { track: block.readUInt8(offset + 0x15), sector: block.readUInt8(offset + 0x16) };
|
|
141
|
+
if (entry.type === "rel" && side.track !== 0) entry.sideStart = side;
|
|
142
|
+
entries.push(entry);
|
|
128
143
|
}
|
|
129
144
|
}
|
|
130
145
|
return {
|
|
@@ -151,6 +166,13 @@ export function fileData(blocks: Buffer[], chain: DiskSector[]): Buffer {
|
|
|
151
166
|
return Buffer.concat(parts);
|
|
152
167
|
}
|
|
153
168
|
|
|
169
|
+
/** The used sectors of the BAM that are not in `referenced`, in track and sector order. */
|
|
170
|
+
export function unreferencedSectors(tracks: ReadonlyArray<{ track: number; usedSectors: number[] }>, referenced: Iterable<DiskSector>): DiskSector[] {
|
|
171
|
+
const known = new Set<string>();
|
|
172
|
+
for (const at of referenced) known.add(`${at.track}/${at.sector}`);
|
|
173
|
+
return tracks.flatMap(({ track, usedSectors }) => usedSectors.filter((sector) => !known.has(`${track}/${sector}`)).map((sector) => ({ track, sector })));
|
|
174
|
+
}
|
|
175
|
+
|
|
154
176
|
const toFile = (entry: DiskEntry): DiskFile => ({ name: entry.name, type: entry.type, blocks: entry.blocks, closed: entry.closed, locked: entry.locked });
|
|
155
177
|
|
|
156
178
|
const sameBytes = (a: Uint8Array, b: Uint8Array) => a.length === b.length && a.every((byte, index) => byte === b[index]);
|
|
@@ -226,11 +248,18 @@ export async function inspect(params: C1541Params, image: Buffer, context: ToolC
|
|
|
226
248
|
const layout = LAYOUTS[info.format];
|
|
227
249
|
if (layout === undefined) throw new WireFailure("media-error", `The disk image has the drive format ${info.format}; only 1541, 1571 and 1581 disks are supported.`);
|
|
228
250
|
|
|
229
|
-
if (params.action === "bam") {
|
|
230
|
-
return { result: { action: "bam", tracks: parseBam(await c1541.run(["-bam"]), info.tracks) } };
|
|
231
|
-
}
|
|
232
251
|
const headerChain = await c1541.chain(layout.header);
|
|
233
252
|
const directory = parseDirectory(await c1541.blocks(headerChain), layout);
|
|
253
|
+
if (params.action === "bam") {
|
|
254
|
+
const tracks = parseBam(await c1541.run(["-bam"]), info.tracks);
|
|
255
|
+
// Every sector that the header, the directory or a file holds; a file without blocks has no chain.
|
|
256
|
+
const referenced = [...headerChain, ...(RESERVED[info.format] ?? [])];
|
|
257
|
+
for (const entry of directory.entries) {
|
|
258
|
+
if (entry.startTrack !== 0) referenced.push(...(await c1541.chain({ track: entry.startTrack, sector: entry.startSector })));
|
|
259
|
+
if (entry.sideStart !== undefined) referenced.push(...(await c1541.chain(entry.sideStart)));
|
|
260
|
+
}
|
|
261
|
+
return { result: { action: "bam", tracks, unreferenced: unreferencedSectors(tracks, referenced) } };
|
|
262
|
+
}
|
|
234
263
|
if (params.action === "directory") {
|
|
235
264
|
const { diskName, diskId, dosType, entries } = directory;
|
|
236
265
|
return { result: { action: "directory", diskName, diskId, dosType, freeBlocks: parseFreeBlocks(overview), entries: entries.map(toFile) } };
|
package/src/host/tools/petcat.ts
CHANGED
|
@@ -16,14 +16,17 @@ import { runToolOrFail, type ToolContext } from "../../native/run.ts";
|
|
|
16
16
|
|
|
17
17
|
const TIMEOUT_MS = 30_000;
|
|
18
18
|
const LISTING = "listing.txt";
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* petcat's listing lines (" 10 sys2061"), as number and text. petcat
|
|
21
|
+
* prints π ($ff) as `~`, which is no PETSCII character: the text has π (F-13).
|
|
22
|
+
*/
|
|
20
23
|
export function parseListing(listing: string): Array<{ number: number; text: string }> {
|
|
21
24
|
const lines: Array<{ number: number; text: string }> = [];
|
|
22
25
|
for (const line of listing.split(/\r?\n/)) {
|
|
23
26
|
if (line.trim() === "") continue;
|
|
24
27
|
const match = /^\s*(\d+) ?(.*)$/.exec(line);
|
|
25
28
|
if (match === null) throw new WireFailure("operation-failed", "petcat printed a listing line without a line number.");
|
|
26
|
-
lines.push({ number: Number(match[1]), text: match[2]
|
|
29
|
+
lines.push({ number: Number(match[1]), text: match[2]!.replaceAll("~", "π") });
|
|
27
30
|
}
|
|
28
31
|
return lines;
|
|
29
32
|
}
|
package/src/host/tools/status.ts
CHANGED
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
// running emulator at start: the Homebrew x64sc of VICE 3.10 on macOS stops
|
|
6
6
|
// on -version ("argv[0] is NULL") although the emulator runs.
|
|
7
7
|
|
|
8
|
-
import
|
|
8
|
+
import { arch, platform, release } from "node:os";
|
|
9
|
+
|
|
10
|
+
import { PACKAGE } from "../../package.ts";
|
|
11
|
+
import type { HostInfo, ToolStatus } from "../../protocol/tools.ts";
|
|
9
12
|
import { C1541, findTool, isOlderThanMinimum, MIN_VICE, PETCAT, VICE, viceFlaws, viceVersionOf, type ToolSpec, type ViceVersion } from "../../native/discover.ts";
|
|
10
13
|
import type { ToolContext } from "../../native/run.ts";
|
|
11
14
|
import { probeTools } from "../../native/status.ts";
|
|
@@ -23,7 +26,7 @@ function requireMinimum(status: ToolStatus, tool: ToolSpec): ToolStatus {
|
|
|
23
26
|
const versionText = (version: ViceVersion) => `x64sc (VICE ${version.major}.${version.minor}${version.revision === undefined ? "" : ` SVN r${version.revision}`})`;
|
|
24
27
|
|
|
25
28
|
/** `started`: the version that the host read from the emulator at start; without it, VICE is probed with -version too. */
|
|
26
|
-
export async function hostStatus(context: ToolContext, started?: ViceVersion): Promise<{ result: { tools: ToolStatus[] } }> {
|
|
29
|
+
export async function hostStatus(context: ToolContext, started?: ViceVersion): Promise<{ result: { host: HostInfo; tools: ToolStatus[] } }> {
|
|
27
30
|
const specs = [VICE, C1541, PETCAT];
|
|
28
31
|
const probed = await probeTools(
|
|
29
32
|
[
|
|
@@ -40,5 +43,5 @@ export async function hostStatus(context: ToolContext, started?: ViceVersion): P
|
|
|
40
43
|
const version = vice.runs ? viceVersionOf(vice.version ?? "") : undefined;
|
|
41
44
|
const flaws = version === undefined ? [] : viceFlaws(version);
|
|
42
45
|
if (flaws.length > 0) checked[0] = { ...vice, flaws };
|
|
43
|
-
return { result: { tools: checked } };
|
|
46
|
+
return { result: { host: { version: PACKAGE.version, platform: platform(), release: release(), arch: arch() }, tools: checked } };
|
|
44
47
|
}
|