@henols/c64-re-tools 2.0.0-rc.2 → 2.0.0-rc.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -62
- package/distribution/package.ts +1 -16
- package/distribution/plugin.ts +5 -3
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +25 -6
- package/skills/c64-assembler/scripts/assemble.ts +45 -10
- package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
- package/skills/c64-basic/SKILL.md +15 -7
- package/skills/c64-basic/scripts/basic.ts +4 -4
- package/skills/c64-disk/SKILL.md +34 -11
- package/skills/c64-disk/scripts/disk.ts +65 -10
- package/skills/c64-disk/scripts/lib/containers.ts +124 -0
- package/skills/c64-docs/SKILL.md +138 -0
- package/skills/c64-docs/references/page-types.md +141 -0
- package/skills/c64-docs/references/style.md +43 -0
- package/skills/c64-docs/scripts/generate.ts +102 -0
- package/skills/c64-docs/scripts/lib/links.ts +155 -0
- package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
- package/skills/c64-docs/scripts/lib/reference.ts +406 -0
- package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
- package/skills/c64-docs/scripts/link.ts +207 -0
- package/skills/c64-emulator/SKILL.md +29 -8
- package/skills/c64-knowledge/SKILL.md +201 -15
- package/skills/c64-knowledge/scripts/knowledge.ts +927 -78
- package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
- package/skills/c64-memory-map/SKILL.md +14 -2
- package/skills/c64-memory-map/scripts/memmap.ts +15 -4
- package/skills/c64-provenance/SKILL.md +100 -11
- package/skills/c64-provenance/scripts/lib/compare.ts +152 -0
- package/skills/c64-provenance/scripts/lib/library.ts +584 -0
- package/skills/c64-provenance/scripts/lib/runtime.ts +191 -0
- package/skills/c64-provenance/scripts/lib/separate.ts +140 -0
- package/skills/c64-provenance/scripts/provenance.ts +477 -24
- package/skills/c64-reverse-engineering/SKILL.md +90 -11
- package/skills/c64-source/SKILL.md +174 -0
- package/skills/c64-source/scripts/generate.ts +788 -0
- package/skills/c64-source/scripts/lib/loader.ts +630 -0
- package/skills/c64-source/scripts/lib/source.ts +1121 -0
- package/skills/c64-static-analysis/SKILL.md +53 -13
- package/skills/c64-static-analysis/scripts/analyze.ts +260 -31
- package/skills/c64-static-analysis/scripts/lib/coverage.ts +72 -0
- package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +11 -5
- package/skills/c64-static-analysis/scripts/lib/idioms.ts +158 -0
- package/skills/c64-static-analysis/scripts/lib/instructions.ts +105 -0
- package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
- package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
- package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
- package/skills/c64-testing/SKILL.md +57 -4
- package/skills/c64-testing/references/scenario-format.md +50 -6
- package/skills/c64-testing/scripts/checklist.ts +3 -3
- package/skills/c64-testing/scripts/diff.ts +167 -0
- package/skills/c64-testing/scripts/lib/diff.ts +70 -0
- package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +183 -152
- package/skills/c64-testing/scripts/run.ts +241 -0
- package/skills/c64-testing/scripts/test.ts +20 -10
- package/skills/c64-unpacker/SKILL.md +88 -21
- package/skills/c64-unpacker/scripts/lib/evidence.ts +207 -0
- package/skills/c64-unpacker/scripts/lib/loads.ts +209 -0
- package/skills/c64-unpacker/scripts/lib/machine.ts +80 -0
- package/skills/c64-unpacker/scripts/lib/stages.ts +142 -0
- package/skills/c64-unpacker/scripts/unpack.ts +545 -61
- package/src/c64.ts +240 -12
- package/src/cli/cleanup.ts +1 -1
- package/src/cli/main.ts +14 -8
- package/src/cli/status.ts +105 -11
- package/src/graphics.ts +158 -0
- package/src/host/main.ts +57 -31
- package/src/host/server.ts +30 -47
- package/src/host/tools/c1541.ts +34 -5
- package/src/host/tools/index.ts +6 -2
- package/src/host/tools/petcat.ts +10 -3
- package/src/host/tools/status.ts +23 -6
- package/src/host/vice/adapter.ts +64 -20
- package/src/host/vice/chips.ts +11 -1
- package/src/host/vice/process.ts +19 -8
- package/src/host/vice/screen.ts +4 -44
- package/src/host/vice/session.ts +343 -60
- package/src/host-client/autostart.ts +55 -0
- package/src/host-client/connect.ts +14 -14
- package/src/host-client/coverage.ts +36 -0
- package/src/host-client/image.ts +63 -0
- package/src/host-client/steps.ts +175 -0
- package/src/host-client/tools.ts +7 -6
- package/src/host-client/transfer.ts +60 -10
- package/src/host-client/vice-session.ts +107 -28
- package/src/knowledge/conflicts.ts +161 -0
- package/src/knowledge/database.ts +150 -38
- package/src/knowledge/expression.ts +209 -0
- package/src/knowledge/graph.ts +104 -0
- package/src/knowledge/history.ts +105 -27
- package/src/knowledge/import.ts +141 -85
- package/src/knowledge/join.ts +211 -0
- package/src/knowledge/program.ts +258 -0
- package/src/knowledge/read.ts +445 -63
- package/src/knowledge/schema.ts +277 -65
- package/src/knowledge/text.ts +415 -0
- package/src/knowledge/write.ts +1145 -150
- package/src/mcp/main.ts +44 -19
- package/src/mcp/output.ts +39 -0
- package/src/mcp/server.ts +15 -4
- package/src/mcp/tools/debug.ts +202 -31
- package/src/mcp/tools/execution.ts +39 -5
- package/src/mcp/tools/input.ts +7 -4
- package/src/mcp/tools/machine.ts +14 -2
- package/src/mcp/tools/media.ts +44 -16
- package/src/mcp/tools/memory.ts +68 -12
- package/src/mcp/tools/video.ts +20 -9
- package/src/native/discover.ts +44 -6
- package/src/native/ghidra/analyze.ts +2 -2
- package/src/native/ghidra/index.ts +19 -3
- package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
- package/src/native/ghidra/scripts/C64Export.java +33 -1
- package/src/native/ghidra/scripts/C64Prepare.java +7 -1
- package/src/native/status.ts +15 -3
- package/src/native/types.ts +43 -8
- package/src/opcodes.ts +133 -0
- package/src/package.ts +21 -0
- package/{skills/c64-memory-map/scripts → src}/platform.ts +87 -7
- package/src/project.ts +166 -3
- package/src/protocol/messages.ts +17 -10
- package/src/protocol/tools.ts +27 -22
- package/src/protocol/vice.ts +255 -33
- package/src/script.ts +49 -9
- package/src/trace.ts +55 -23
- package/skills/c64-provenance/scripts/compare.ts +0 -92
- package/skills/c64-unpacker/scripts/evidence.ts +0 -135
|
@@ -1,21 +1,84 @@
|
|
|
1
1
|
// The c64-provenance script: compares two or more releases byte by byte at
|
|
2
|
-
// their addresses and reports where they differ and which releases agree.
|
|
2
|
+
// their addresses and reports where they differ and which releases agree. A
|
|
3
|
+
// release is a PRG or an unpacker image.json (D50). With --run, each release
|
|
4
|
+
// runs in its own emulator, and each listed difference gets its first access
|
|
5
|
+
// and whether the run changed it. The separate command gives evidence from
|
|
6
|
+
// the program in the knowledge (D51, D83). The routine and match commands keep
|
|
7
|
+
// routines that several programs share in a library folder and find them in the
|
|
8
|
+
// program (D71).
|
|
3
9
|
|
|
10
|
+
import { basename, posix, relative } from "node:path";
|
|
4
11
|
import { parseArgs } from "node:util";
|
|
5
12
|
|
|
6
13
|
import { formatC64Address, parseC64Address } from "#src/c64.ts";
|
|
7
|
-
import {
|
|
14
|
+
import { checkActionSteps, type ActionStep } from "#src/host-client/steps.ts";
|
|
15
|
+
import { MAX_EXECUTION_COUNT, WireFailure } from "#src/host-client/tools.ts";
|
|
16
|
+
import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
|
|
17
|
+
import { ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
18
|
+
import { KnowledgeError, openForRead } from "#src/knowledge/database.ts";
|
|
19
|
+
import { programCoverage, requireProgram } from "#src/knowledge/program.ts";
|
|
20
|
+
import {
|
|
21
|
+
commentsAt,
|
|
22
|
+
currentRevision,
|
|
23
|
+
listComments,
|
|
24
|
+
listModules,
|
|
25
|
+
listOperands,
|
|
26
|
+
listReferences,
|
|
27
|
+
listRegions,
|
|
28
|
+
listSymbols,
|
|
29
|
+
operandAt,
|
|
30
|
+
referencesFrom,
|
|
31
|
+
referenceSuppressed,
|
|
32
|
+
regionsOverlapping,
|
|
33
|
+
suppressionsOverlapping,
|
|
34
|
+
symbolAt,
|
|
35
|
+
symbolNamed,
|
|
36
|
+
symbolSuppressed,
|
|
37
|
+
} from "#src/knowledge/read.ts";
|
|
38
|
+
import { CODE_REGION_TYPES, type Origin } from "#src/knowledge/schema.ts";
|
|
39
|
+
import { platformNames } from "#src/platform.ts";
|
|
40
|
+
import { projectRoot } from "#src/project.ts";
|
|
8
41
|
import { runScript, UsageError } from "#src/script.ts";
|
|
9
|
-
import { compareReleases,
|
|
42
|
+
import { compareReleases, shiftSuggestion, suggestShifts, type Range, type Release } from "./lib/compare.ts";
|
|
43
|
+
import { findRoutine, libraryFolder, LibraryError, readLibrary, routineFrom, saveRoutine, suggest } from "./lib/library.ts";
|
|
44
|
+
import { watchRelease, type ImageState, type Start } from "./lib/runtime.ts";
|
|
45
|
+
import { separate } from "./lib/separate.ts";
|
|
10
46
|
|
|
11
|
-
const USAGE = `provenance.ts <release
|
|
47
|
+
const USAGE = `provenance.ts <release> <release> [<release> ...] [options]
|
|
48
|
+
provenance.ts separate [--out <file>] [--max-listed <n>]
|
|
49
|
+
provenance.ts routine --library <folder> (--module <address> | --start <address> --end <address>)
|
|
50
|
+
[--name <name>] [--title <text>] [--replace]
|
|
51
|
+
provenance.ts match --library <folder> [--out <file>] [--max-listed <n>]
|
|
12
52
|
|
|
13
53
|
--shift <n>:<bytes> move release n (1 = the first) by a number of bytes, for example 2:-256
|
|
14
54
|
--from <address> compare from this address
|
|
15
55
|
--to <address> compare up to this address
|
|
56
|
+
--run <frames> run each release in its own emulator for this many frames, and
|
|
57
|
+
give each listed difference its first access and whether it changed
|
|
58
|
+
--steps <file> with --run: action steps (JSON) to run before the frames
|
|
16
59
|
|
|
17
|
-
|
|
18
|
-
|
|
60
|
+
separate gives evidence from the program in the knowledge (D83): the unpack stage
|
|
61
|
+
that wrote each range, its access in the coverage of the knowledge, whether call
|
|
62
|
+
and jump references in knowledge reach each code region from the entry, and what
|
|
63
|
+
the program reads or executes outside its segments. --out writes all ranges to a file;
|
|
64
|
+
the result lists at most --max-listed of each list (default 50) and its count.
|
|
65
|
+
|
|
66
|
+
routine saves a routine of the program in the knowledge to a library: a plain folder of routine
|
|
67
|
+
files, beside the projects (--library, relative to the working directory, as
|
|
68
|
+
--project). It takes a module (D48) of the knowledge, or --start and --end. Its
|
|
69
|
+
code regions give the instructions; the user and LLM names, regions, comments,
|
|
70
|
+
references and operand expressions go into the file at offsets. --name defaults
|
|
71
|
+
to the user or LLM name at the start, --title to the module title.
|
|
72
|
+
|
|
73
|
+
match finds each routine of the library in the program of the knowledge: the opcodes and other
|
|
74
|
+
fixed bytes equal, and the addresses into the routine moved by one shift.
|
|
75
|
+
Operands that address something outside the routine may differ; the result
|
|
76
|
+
lists them. --out writes the names of the routines at the found addresses as a
|
|
77
|
+
batch for knowledge.ts apply, and the result lists what it left out.
|
|
78
|
+
|
|
79
|
+
A release to compare is a PRG, placed at its load address, or an image.json of
|
|
80
|
+
the c64-unpacker skill, compared inside its segments. Paths are relative to the
|
|
81
|
+
project folder. The result is one JSON object.`;
|
|
19
82
|
|
|
20
83
|
function address(text: string, option: string): number {
|
|
21
84
|
try {
|
|
@@ -27,14 +90,143 @@ function address(text: string, option: string): number {
|
|
|
27
90
|
|
|
28
91
|
const hexBytes = (bytes: number[]) => Buffer.from(bytes).toString("hex").replace(/(..)(?!$)/g, "$1 ");
|
|
29
92
|
|
|
30
|
-
|
|
93
|
+
interface LoadedRelease {
|
|
94
|
+
release: Release;
|
|
95
|
+
shift: number;
|
|
96
|
+
start: Start | { kind: "none"; reason: string };
|
|
97
|
+
/** 64 KiB with the release bytes at their own addresses. */
|
|
98
|
+
memory: Uint8Array;
|
|
99
|
+
entry?: number;
|
|
100
|
+
stages?: Array<{ entry: number; wrote: Range[] }>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Reads entry_state.json of the c64-unpacker skill for a start by writes. */
|
|
104
|
+
function entryState(path: string): ImageState | string {
|
|
105
|
+
let value: Record<string, any>;
|
|
106
|
+
try {
|
|
107
|
+
value = JSON.parse(readProjectFile(path).bytes.toString("utf8")) as Record<string, any>;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (error instanceof WireFailure && error.code === "not-found") return `It has no ${path}; capture it with c64-unpacker.`;
|
|
110
|
+
if (error instanceof WireFailure) throw error;
|
|
111
|
+
return `${path} is not JSON: ${(error as Error).message}`;
|
|
112
|
+
}
|
|
113
|
+
const registers = value.registers ?? {};
|
|
114
|
+
const byte = (item: unknown) => Number.isInteger(item) && (item as number) >= 0 && (item as number) <= 0xff;
|
|
115
|
+
const pages = ["$0000", "$0100", "$0200", "$0300"].map((page) => value.pages?.[page]);
|
|
116
|
+
const ok =
|
|
117
|
+
typeof registers.pc === "string" &&
|
|
118
|
+
/^\$[0-9a-f]{4}$/i.test(registers.pc) &&
|
|
119
|
+
["a", "x", "y", "sp"].every((name) => byte(registers[name])) &&
|
|
120
|
+
typeof registers.flags === "object" &&
|
|
121
|
+
byte(value.cpuPort?.$00) &&
|
|
122
|
+
byte(value.cpuPort?.$01) &&
|
|
123
|
+
pages.every((page) => typeof page === "string" && /^[0-9a-f]{512}$/i.test(page));
|
|
124
|
+
if (!ok) return `${path} is not an entry_state.json of the c64-unpacker skill.`;
|
|
125
|
+
const flag = (name: string) => registers.flags[name] === true;
|
|
126
|
+
return {
|
|
127
|
+
pc: Number.parseInt(registers.pc.slice(1), 16),
|
|
128
|
+
a: registers.a,
|
|
129
|
+
x: registers.x,
|
|
130
|
+
y: registers.y,
|
|
131
|
+
sp: registers.sp,
|
|
132
|
+
flags: { n: flag("n"), v: flag("v"), d: flag("d"), i: flag("i"), z: flag("z"), c: flag("c") },
|
|
133
|
+
port: [value.cpuPort.$00, value.cpuPort.$01],
|
|
134
|
+
pages: Buffer.from(pages.join(""), "hex"),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function readRelease(path: string, shift: number): LoadedRelease {
|
|
139
|
+
const { bytes } = readProjectFile(path);
|
|
140
|
+
const moved = (range: Range) => ({ start: range.start + shift, end: range.end + shift });
|
|
141
|
+
const inside = (range: Range) => range.start >= 0 && range.end <= 0xffff;
|
|
142
|
+
if (!path.endsWith(".json")) {
|
|
143
|
+
if (bytes.length < 3) throw new UsageError(`${path} is too short for a PRG`);
|
|
144
|
+
const load = bytes.readUInt16LE(0);
|
|
145
|
+
const segment = moved({ start: load, end: load + bytes.length - 3 });
|
|
146
|
+
if (!inside(segment)) throw new UsageError(`the shift moves ${path} outside $0000-$ffff`);
|
|
147
|
+
const memory = new Uint8Array(0x10000);
|
|
148
|
+
memory.set(bytes.subarray(2), load);
|
|
149
|
+
return { release: { name: path, segments: [segment], byteAt: (at) => bytes[2 + at - segment.start]! }, shift, start: { kind: "program", path }, memory };
|
|
150
|
+
}
|
|
151
|
+
const fail = (what: string): never => {
|
|
152
|
+
throw new UsageError(`${path} is not an image.json of the c64-unpacker skill: ${what}`);
|
|
153
|
+
};
|
|
154
|
+
let manifest: Record<string, unknown>;
|
|
155
|
+
try {
|
|
156
|
+
manifest = JSON.parse(bytes.toString("utf8")) as Record<string, unknown>;
|
|
157
|
+
} catch (error) {
|
|
158
|
+
return fail((error as Error).message);
|
|
159
|
+
}
|
|
160
|
+
const parse = (value: unknown) => (typeof value === "string" && /^\$[0-9a-f]{4}$/i.test(value) ? Number.parseInt(value.slice(1), 16) : fail("a segment needs start and end as $ addresses"));
|
|
161
|
+
if (typeof manifest.image !== "string" || !Array.isArray(manifest.segments) || manifest.segments.length === 0) fail("it needs image and segments");
|
|
162
|
+
const own = (manifest.segments as Array<Record<string, unknown>>).map((segment) => ({ start: parse(segment.start), end: parse(segment.end) })).sort((a, b) => a.start - b.start);
|
|
163
|
+
const directory = posix.dirname(path);
|
|
164
|
+
const memory = readProjectFile(posix.join(directory, manifest.image as string)).bytes;
|
|
165
|
+
if (memory.length !== 0x10000) fail(`its image has ${memory.length} bytes, not 65536`);
|
|
166
|
+
const segments = own.map(moved);
|
|
167
|
+
if (!segments.every(inside)) throw new UsageError(`the shift moves ${path} outside $0000-$ffff`);
|
|
168
|
+
const state = entryState(posix.join(directory, "entry_state.json"));
|
|
169
|
+
const stages = Array.isArray(manifest.stages)
|
|
170
|
+
? (manifest.stages as Array<Record<string, unknown>>).map((stage) => ({
|
|
171
|
+
entry: parse(stage.entry),
|
|
172
|
+
wrote: (Array.isArray(stage.wrote) ? (stage.wrote as Array<Record<string, unknown>>) : []).map((range) => ({ start: parse(range.start), end: parse(range.end) })),
|
|
173
|
+
}))
|
|
174
|
+
: undefined;
|
|
175
|
+
return {
|
|
176
|
+
release: { name: path, segments, byteAt: (at) => memory[at - shift]! },
|
|
177
|
+
shift,
|
|
178
|
+
start: typeof state === "string" ? { kind: "none", reason: state } : { kind: "image", segments: own, byteAt: (at) => memory[at]!, state },
|
|
179
|
+
memory,
|
|
180
|
+
...(manifest.entry === undefined ? {} : { entry: parse(manifest.entry) }),
|
|
181
|
+
...(stages === undefined ? {} : { stages }),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function run(argv: string[]): Promise<unknown> {
|
|
31
186
|
const { values, positionals } = parseArgs({
|
|
32
187
|
args: argv,
|
|
33
188
|
strict: true,
|
|
34
189
|
allowPositionals: true,
|
|
35
|
-
options: {
|
|
190
|
+
options: {
|
|
191
|
+
shift: { type: "string", multiple: true },
|
|
192
|
+
from: { type: "string" },
|
|
193
|
+
to: { type: "string" },
|
|
194
|
+
run: { type: "string" },
|
|
195
|
+
steps: { type: "string" },
|
|
196
|
+
out: { type: "string" },
|
|
197
|
+
"max-listed": { type: "string" },
|
|
198
|
+
library: { type: "string" },
|
|
199
|
+
module: { type: "string" },
|
|
200
|
+
start: { type: "string" },
|
|
201
|
+
end: { type: "string" },
|
|
202
|
+
name: { type: "string" },
|
|
203
|
+
title: { type: "string" },
|
|
204
|
+
replace: { type: "boolean" },
|
|
205
|
+
help: { type: "boolean" },
|
|
206
|
+
},
|
|
36
207
|
});
|
|
37
208
|
if (values.help) throw new UsageError("");
|
|
209
|
+
/** Refuses the options that `command` does not take. */
|
|
210
|
+
const only = (command: string, allowed: readonly string[]) => {
|
|
211
|
+
const used = Object.keys(values).filter((option) => option !== "help" && !allowed.includes(option));
|
|
212
|
+
if (used.length > 0) throw new UsageError(`${command} does not take --${used.join(", --")}`);
|
|
213
|
+
};
|
|
214
|
+
if (positionals[0] === "separate") {
|
|
215
|
+
only("separate", ["out", "max-listed"]);
|
|
216
|
+
if (positionals.length !== 1) throw new UsageError("separate takes no release: it reads the program from the knowledge");
|
|
217
|
+
return separateCommand(values.out, values["max-listed"]);
|
|
218
|
+
}
|
|
219
|
+
if (positionals[0] === "routine") {
|
|
220
|
+
only("routine", ["library", "module", "start", "end", "name", "title", "replace"]);
|
|
221
|
+
if (positionals.length !== 1) throw new UsageError("routine takes no image: it reads the program from the knowledge");
|
|
222
|
+
return routineCommand(values);
|
|
223
|
+
}
|
|
224
|
+
if (positionals[0] === "match") {
|
|
225
|
+
only("match", ["library", "out", "max-listed"]);
|
|
226
|
+
if (positionals.length !== 1) throw new UsageError("match takes no image: it reads the program from the knowledge");
|
|
227
|
+
return matchCommand(values.library, values.out, values["max-listed"]);
|
|
228
|
+
}
|
|
229
|
+
only("a comparison", ["shift", "from", "to", "run", "steps"]);
|
|
38
230
|
if (positionals.length < 2) throw new UsageError("give two or more releases");
|
|
39
231
|
const shifts = new Map<number, number>();
|
|
40
232
|
for (const text of values.shift ?? []) {
|
|
@@ -42,13 +234,27 @@ function run(argv: string[]): unknown {
|
|
|
42
234
|
if (match === null || Number(match[1]) < 1 || Number(match[1]) > positionals.length) throw new UsageError(`--shift must be <release number>:<bytes>, not ${text}`);
|
|
43
235
|
shifts.set(Number(match[1]), Number(match[2]));
|
|
44
236
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
237
|
+
if (values.steps !== undefined && values.run === undefined) throw new UsageError("--steps works only with --run");
|
|
238
|
+
let frames: number | undefined;
|
|
239
|
+
if (values.run !== undefined) {
|
|
240
|
+
if (!/^\d{1,6}$/.test(values.run) || Number(values.run) < 1 || Number(values.run) > MAX_EXECUTION_COUNT) throw new UsageError(`--run needs a frame count from 1 to ${MAX_EXECUTION_COUNT}, not ${values.run}`);
|
|
241
|
+
frames = Number(values.run);
|
|
242
|
+
}
|
|
243
|
+
let steps: ActionStep[] = [];
|
|
244
|
+
if (values.steps !== undefined) {
|
|
245
|
+
let parsed: unknown;
|
|
246
|
+
try {
|
|
247
|
+
parsed = JSON.parse(readProjectFile(values.steps).bytes.toString("utf8"));
|
|
248
|
+
} catch (error) {
|
|
249
|
+
if (error instanceof WireFailure) throw error;
|
|
250
|
+
throw new UsageError(`the steps file ${values.steps} is not JSON: ${(error as Error).message}`);
|
|
251
|
+
}
|
|
252
|
+
steps = checkActionSteps(parsed, (message) => {
|
|
253
|
+
throw new UsageError(`steps file ${values.steps}: ${message}`);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
const loaded = positionals.map((path, index) => readRelease(path, shifts.get(index + 1) ?? 0));
|
|
257
|
+
const releases = loaded.map((item) => item.release);
|
|
52
258
|
const comparison = compareReleases(releases, {
|
|
53
259
|
...(values.from === undefined ? {} : { from: address(values.from, "--from") }),
|
|
54
260
|
...(values.to === undefined ? {} : { to: address(values.to, "--to") }),
|
|
@@ -56,28 +262,275 @@ function run(argv: string[]): unknown {
|
|
|
56
262
|
const compared = comparison.identicalBytes + comparison.differingBytes;
|
|
57
263
|
const identicalShare = compared === 0 ? 0 : comparison.identicalBytes / compared;
|
|
58
264
|
// A poor match at the load addresses can mean that the releases are placed differently.
|
|
265
|
+
// Each release gets its best shifts: data that stayed can outvote code that moved, so one is not enough (b2/04).
|
|
266
|
+
const applied = loaded.map((item) => item.shift);
|
|
59
267
|
const suggestions =
|
|
60
268
|
identicalShare < 0.5
|
|
61
|
-
? releases.slice(1).flatMap((release, index) =>
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
269
|
+
? releases.slice(1).flatMap((release, index) =>
|
|
270
|
+
suggestShifts(releases[0]!, release).map((found) => {
|
|
271
|
+
const suggestion = shiftSuggestion(releases, applied, index + 1, found);
|
|
272
|
+
// The shift that the run already has: its votes tell how good the present alignment is.
|
|
273
|
+
return "shift" in suggestion && suggestion.shift === applied[suggestion.release - 1] ? { ...suggestion, current: true } : suggestion;
|
|
274
|
+
}),
|
|
275
|
+
)
|
|
65
276
|
: [];
|
|
277
|
+
const suggested = suggestions.filter((suggestion) => "shift" in suggestion);
|
|
278
|
+
const notSuggested = suggestions.filter((suggestion) => "reason" in suggestion);
|
|
279
|
+
|
|
280
|
+
// The runtime evidence: one emulator for each release, the differences at the release's own addresses.
|
|
281
|
+
const runtime = new Map<string, Array<Range & { first: string; changed: boolean }>>();
|
|
282
|
+
const runs: Record<string, unknown> = {};
|
|
283
|
+
if (frames !== undefined && comparison.differences.length > 0) {
|
|
284
|
+
for (const item of loaded) {
|
|
285
|
+
const name = item.release.name;
|
|
286
|
+
if (item.start.kind === "none") {
|
|
287
|
+
runs[name] = { started: false, reason: item.start.reason };
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const ranges = comparison.differences.map((difference) => ({ start: difference.start - item.shift, end: difference.end - item.shift }));
|
|
291
|
+
const client = await ViceSessionClient.open({ videoStandard: "pal" });
|
|
292
|
+
try {
|
|
293
|
+
const watched = await watchRelease(client, item.start, ranges, (at) => item.release.byteAt(at + item.shift), { frames, steps });
|
|
294
|
+
if (!watched.started) runs[name] = { started: false, reason: watched.reason };
|
|
295
|
+
else {
|
|
296
|
+
runs[name] = { started: true };
|
|
297
|
+
runtime.set(name, watched.accesses.map((access, index) => ({ ...ranges[index]!, ...access })));
|
|
298
|
+
}
|
|
299
|
+
} finally {
|
|
300
|
+
await client.close();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
66
305
|
return {
|
|
67
|
-
releases:
|
|
68
|
-
common: comparison.common
|
|
306
|
+
releases: loaded.map((item) => ({ name: item.release.name, shift: item.shift, segments: item.release.segments.map((segment) => ({ start: formatC64Address(segment.start), end: formatC64Address(segment.end) })) })),
|
|
307
|
+
common: comparison.common.map((range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) })),
|
|
69
308
|
identicalBytes: comparison.identicalBytes,
|
|
70
309
|
differingBytes: comparison.differingBytes,
|
|
71
310
|
identicalShare: Number(identicalShare.toFixed(3)),
|
|
72
|
-
differences: comparison.differences.map((difference) => ({
|
|
311
|
+
differences: comparison.differences.map((difference, index) => ({
|
|
73
312
|
start: formatC64Address(difference.start),
|
|
74
313
|
end: formatC64Address(difference.end),
|
|
75
314
|
values: Object.fromEntries(Object.entries(difference.values).map(([name, bytes]) => [name, hexBytes(bytes.slice(0, 32)) + (bytes.length > 32 ? " ..." : "")])),
|
|
76
315
|
groups: difference.groups,
|
|
316
|
+
// runtime gives the release's own addresses: with a shift, they are not the addresses of the difference (F-24).
|
|
317
|
+
...(runtime.size === 0
|
|
318
|
+
? {}
|
|
319
|
+
: {
|
|
320
|
+
runtime: Object.fromEntries(
|
|
321
|
+
[...runtime].map(([name, accesses]) => {
|
|
322
|
+
const { start, end, first, changed } = accesses[index]!;
|
|
323
|
+
return [name, { start: formatC64Address(start), end: formatC64Address(end), first, changed }];
|
|
324
|
+
}),
|
|
325
|
+
),
|
|
326
|
+
}),
|
|
77
327
|
})),
|
|
78
328
|
...(comparison.moreDifferences ? { moreDifferences: true } : {}),
|
|
79
|
-
...(
|
|
329
|
+
...(suggested.length > 0 ? { suggestedShifts: suggested } : {}),
|
|
330
|
+
...(notSuggested.length > 0 ? { shiftsNotSuggested: notSuggested } : {}),
|
|
331
|
+
...(frames === undefined
|
|
332
|
+
? {}
|
|
333
|
+
: { run: { frames, steps: steps.length, releases: runs, ...(comparison.differences.length === 0 ? { skipped: "The releases have no differences, so no release ran." } : {}) } }),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** The stages of the capture in the knowledge as ranges; a stage that is not a capture stage is refused. */
|
|
338
|
+
function stagesOf(stages: Array<Record<string, unknown>> | undefined): Array<{ entry: number; wrote: Range[] }> | undefined {
|
|
339
|
+
if (stages === undefined) return undefined;
|
|
340
|
+
const parse = (value: unknown) => {
|
|
341
|
+
if (typeof value === "string" && /^\$[0-9a-f]{4}$/i.test(value)) return Number.parseInt(value.slice(1), 16);
|
|
342
|
+
throw new UsageError("the stages of the program in the knowledge are not the stages of a c64-unpacker capture; put the capture in again with knowledge.ts image");
|
|
343
|
+
};
|
|
344
|
+
return stages.map((stage) => ({
|
|
345
|
+
entry: parse(stage.entry),
|
|
346
|
+
wrote: (Array.isArray(stage.wrote) ? (stage.wrote as Array<Record<string, unknown>>) : []).map((range) => ({ start: parse(range.start), end: parse(range.end) })),
|
|
347
|
+
}));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** separate: evidence from the program in the knowledge (D51, D83), with all ranges in --out and the first ones in the result. */
|
|
351
|
+
function separateCommand(out: string | undefined, maxListed: string | undefined): unknown {
|
|
352
|
+
const max = maxListed ?? "50";
|
|
353
|
+
if (!/^\d{1,5}$/.test(max)) throw new UsageError(`--max-listed must be a whole number, not ${max}`);
|
|
354
|
+
const db = openForRead();
|
|
355
|
+
let program: ReturnType<typeof requireProgram>;
|
|
356
|
+
let coverage: Uint8Array | undefined;
|
|
357
|
+
let regions: ReturnType<typeof listRegions>;
|
|
358
|
+
let references: ReturnType<typeof listReferences>;
|
|
359
|
+
try {
|
|
360
|
+
program = requireProgram(db);
|
|
361
|
+
coverage = programCoverage(db)?.map;
|
|
362
|
+
regions = listRegions(db).filter((region) => CODE_REGION_TYPES.includes(region.type));
|
|
363
|
+
references = listReferences(db);
|
|
364
|
+
} finally {
|
|
365
|
+
db?.close();
|
|
366
|
+
}
|
|
367
|
+
const stages = stagesOf(program.stages);
|
|
368
|
+
const loaded = { entry: program.entry, stages };
|
|
369
|
+
const found = separate({
|
|
370
|
+
segments: program.segments,
|
|
371
|
+
memory: program.memory,
|
|
372
|
+
...(program.entry === undefined ? {} : { entry: program.entry }),
|
|
373
|
+
...(stages === undefined ? {} : { stages }),
|
|
374
|
+
...(coverage === undefined ? {} : { coverage }),
|
|
375
|
+
codeRegions: regions,
|
|
376
|
+
references,
|
|
377
|
+
});
|
|
378
|
+
const show = (range: Range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) });
|
|
379
|
+
const ranges = found.ranges.map((range) => ({ ...show(range), ...(range.stage === undefined ? {} : { stage: range.stage }), ...(range.access === undefined ? {} : { access: range.access }) }));
|
|
380
|
+
const codeRegions = found.codeRegions.map((region) => ({ ...show(region), type: region.type, ...(region.reachable === undefined ? {} : { reachable: region.reachable }), ...(region.executed === undefined ? {} : { executed: region.executed }) }));
|
|
381
|
+
const outside = { read: found.outside.read.map(show), executed: found.outside.executed.map(show), generated: found.outside.generated.map(show) };
|
|
382
|
+
const listed = <T>(items: T[]) => ({ count: items.length, listed: items.slice(0, Number(max)) });
|
|
383
|
+
if (out !== undefined) writeProjectFile(out, Buffer.from(`${JSON.stringify({ ranges, codeRegions, outside }, null, 1)}\n`), { what: "--out" });
|
|
384
|
+
const bytes = (filter: (range: (typeof found.ranges)[number]) => boolean) => found.ranges.filter(filter).reduce((sum, range) => sum + range.end - range.start + 1, 0);
|
|
385
|
+
const lastStage = loaded.stages === undefined ? undefined : loaded.stages.length - 1;
|
|
386
|
+
return {
|
|
387
|
+
...(loaded.entry === undefined ? {} : { entry: formatC64Address(loaded.entry) }),
|
|
388
|
+
...(lastStage === undefined
|
|
389
|
+
? { noStages: "The program in the knowledge has no stage log. Only a capture of c64-unpacker gives one. A capture of loads holds one part of a program and has none." }
|
|
390
|
+
: {
|
|
391
|
+
stages: lastStage + 1,
|
|
392
|
+
bytesByStage: Object.fromEntries([...[...Array(lastStage + 1).keys()].map((stage): [string, number] => [String(stage), bytes((range) => range.stage === stage)]), ["none", bytes((range) => range.stage === undefined)]]),
|
|
393
|
+
}),
|
|
394
|
+
...(coverage === undefined ? {} : { bytesByAccess: Object.fromEntries((["executed", "read", "written", "read-written", "untouched"] as const).map((access) => [access, bytes((range) => range.access === access)])) }),
|
|
395
|
+
ranges: listed(ranges),
|
|
396
|
+
// total counts every code region; unreachable is a list like the others (b2/08).
|
|
397
|
+
codeRegions: {
|
|
398
|
+
total: codeRegions.length,
|
|
399
|
+
...(loaded.entry === undefined ? {} : { unreachable: listed(codeRegions.filter((region) => region.reachable === false)) }),
|
|
400
|
+
},
|
|
401
|
+
...(coverage === undefined ? {} : { outside: { read: listed(outside.read), executed: listed(outside.executed), generated: listed(outside.generated) } }),
|
|
402
|
+
...(out === undefined ? {} : { out }),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const SEMANTIC: readonly Origin[] = ["user", "llm"];
|
|
407
|
+
|
|
408
|
+
const platform = () => new Map(platformNames().map((entry) => [entry.name, entry.address]));
|
|
409
|
+
|
|
410
|
+
/** routine: saves a module or a range of the program in the knowledge, with its knowledge, as a routine of the library (D71). */
|
|
411
|
+
function routineCommand(values: { library?: string; module?: string; start?: string; end?: string; name?: string; title?: string; replace?: boolean }): unknown {
|
|
412
|
+
if (values.library === undefined) throw new UsageError("routine needs --library <folder>");
|
|
413
|
+
if (values.module !== undefined && (values.start !== undefined || values.end !== undefined)) throw new UsageError("routine takes --module, or --start and --end, not both");
|
|
414
|
+
if (values.module === undefined && values.start === undefined && values.end === undefined) throw new UsageError("routine needs --module <address>, or --start and --end");
|
|
415
|
+
if (values.module === undefined && (values.start === undefined || values.end === undefined)) throw new UsageError("routine needs both --start and --end");
|
|
416
|
+
const db = openForRead();
|
|
417
|
+
try {
|
|
418
|
+
const image = requireProgram(db);
|
|
419
|
+
const segmentOf = (at: number) => image.segments.find((segment) => at >= segment.start && at <= segment.end);
|
|
420
|
+
let start: number;
|
|
421
|
+
let end: number;
|
|
422
|
+
let title = values.title;
|
|
423
|
+
if (values.module !== undefined) {
|
|
424
|
+
start = address(values.module, "--module");
|
|
425
|
+
const modules = listModules(db);
|
|
426
|
+
const module = modules.find((item) => item.start === start);
|
|
427
|
+
if (module === undefined) throw new LibraryError("not-found", `No module starts at ${formatC64Address(start)}. List the modules with knowledge.ts modules, or give --start and --end.`);
|
|
428
|
+
const segment = segmentOf(start);
|
|
429
|
+
if (segment === undefined) throw new UsageError(`The module at ${formatC64Address(start)} is outside the segments of the program.`);
|
|
430
|
+
end = (modules.find((item) => item.start > start && item.start <= segment.end)?.start ?? segment.end + 1) - 1;
|
|
431
|
+
title ??= module.title;
|
|
432
|
+
} else {
|
|
433
|
+
start = address(values.start!, "--start");
|
|
434
|
+
end = address(values.end!, "--end");
|
|
435
|
+
if (end < start) throw new UsageError("--end is before --start");
|
|
436
|
+
if (segmentOf(start) === undefined || segmentOf(start) !== segmentOf(end)) throw new UsageError(`${formatC64Address(start)}-${formatC64Address(end)} is not inside one segment of the program.`);
|
|
437
|
+
}
|
|
438
|
+
const range = { start, end };
|
|
439
|
+
const name = values.name ?? listSymbols(db, { start, end: start }, undefined, SEMANTIC)[0]?.name;
|
|
440
|
+
if (name === undefined) throw new UsageError(`The knowledge has no user or LLM name at ${formatC64Address(start)}: give --name.`);
|
|
441
|
+
const { routine, leftOut } = routineFrom({
|
|
442
|
+
name,
|
|
443
|
+
...(title === undefined ? {} : { title }),
|
|
444
|
+
program: basename(projectRoot()),
|
|
445
|
+
image: `knowledge revision ${currentRevision(db)}`,
|
|
446
|
+
start,
|
|
447
|
+
end,
|
|
448
|
+
byteAt: image.byteAt,
|
|
449
|
+
analyzed: listRegions(db, range),
|
|
450
|
+
symbols: listSymbols(db, range, undefined, SEMANTIC),
|
|
451
|
+
regions: listRegions(db, range, undefined, SEMANTIC),
|
|
452
|
+
comments: listComments(db, range, SEMANTIC),
|
|
453
|
+
references: listReferences(db, range, undefined, SEMANTIC),
|
|
454
|
+
operands: listOperands(db, range, SEMANTIC),
|
|
455
|
+
platform: platform(),
|
|
456
|
+
});
|
|
457
|
+
const file = saveRoutine(libraryFolder(values.library, { create: true }), routine, values.replace === true);
|
|
458
|
+
return {
|
|
459
|
+
routine: routine.name,
|
|
460
|
+
file: relative(process.cwd(), file),
|
|
461
|
+
start: formatC64Address(start),
|
|
462
|
+
end: formatC64Address(end),
|
|
463
|
+
length: routine.bytes.length,
|
|
464
|
+
instructions: routine.instructions.length,
|
|
465
|
+
relocations: routine.relocations.length,
|
|
466
|
+
variable: routine.variable.length,
|
|
467
|
+
counts: { symbols: routine.symbols.length, regions: routine.regions.length, comments: routine.comments.length, references: routine.references.length, operands: routine.operands.length },
|
|
468
|
+
...(leftOut.length === 0 ? {} : { leftOut }),
|
|
469
|
+
};
|
|
470
|
+
} finally {
|
|
471
|
+
db?.close();
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** match: finds the routines of the library in the program of the knowledge, and with --out writes their names there as an apply batch (D71). */
|
|
476
|
+
function matchCommand(library: string | undefined, out: string | undefined, maxListed: string | undefined): unknown {
|
|
477
|
+
if (library === undefined) throw new UsageError("match needs --library <folder>");
|
|
478
|
+
const max = maxListed ?? "50";
|
|
479
|
+
if (!/^\d{1,5}$/.test(max)) throw new UsageError(`--max-listed must be a whole number, not ${max}`);
|
|
480
|
+
const listed = <T>(items: T[]) => ({ count: items.length, listed: items.slice(0, Number(max)) });
|
|
481
|
+
const routines = readLibrary(libraryFolder(library, { create: false }));
|
|
482
|
+
if (routines.length === 0) throw new LibraryError("not-found", `The library ${library} has no routine files. Save a routine with routine first.`);
|
|
483
|
+
const programDb = openForRead();
|
|
484
|
+
let image: ReturnType<typeof requireProgram>;
|
|
485
|
+
try {
|
|
486
|
+
image = requireProgram(programDb);
|
|
487
|
+
} finally {
|
|
488
|
+
programDb?.close();
|
|
489
|
+
}
|
|
490
|
+
const found = routines.map((routine) => ({ routine, matches: findRoutine(routine, image) }));
|
|
491
|
+
const show = (value: number, size: 1 | 2) => `$${value.toString(16).padStart(2 * size, "0")}`;
|
|
492
|
+
const matches = found
|
|
493
|
+
.flatMap(({ routine, matches: each }) =>
|
|
494
|
+
each.map((match) => ({
|
|
495
|
+
routine: match.routine,
|
|
496
|
+
start: formatC64Address(match.start),
|
|
497
|
+
end: formatC64Address(match.start + routine.bytes.length - 1),
|
|
498
|
+
shift: match.shift,
|
|
499
|
+
differing: match.differing.map((operand) => ({ address: formatC64Address(operand.address), library: show(operand.library, operand.size), program: show(operand.program, operand.size) })),
|
|
500
|
+
})),
|
|
501
|
+
)
|
|
502
|
+
.sort((a, b) => a.start.localeCompare(b.start));
|
|
503
|
+
const result: Record<string, unknown> = { library, routines: routines.length, matches: listed(matches) };
|
|
504
|
+
if (out === undefined) return result;
|
|
505
|
+
const db = openForRead();
|
|
506
|
+
let suggestions: ReturnType<typeof suggest>;
|
|
507
|
+
try {
|
|
508
|
+
suggestions = suggest(
|
|
509
|
+
found,
|
|
510
|
+
{
|
|
511
|
+
symbolAt: (at) => symbolAt(db, at),
|
|
512
|
+
symbolNamed: (name) => symbolNamed(db, name),
|
|
513
|
+
regionsOverlapping: (start, end) => regionsOverlapping(db, start, end),
|
|
514
|
+
commentsAt: (at) => commentsAt(db, at),
|
|
515
|
+
referencesFrom: (at) => referencesFrom(db, at),
|
|
516
|
+
operandAt: (at) => operandAt(db, at),
|
|
517
|
+
symbolSuppressed: (at) => symbolSuppressed(db, at),
|
|
518
|
+
regionSuppressed: (start, end) => suppressionsOverlapping(db, "region", start, end).length > 0,
|
|
519
|
+
referenceSuppressed: (reference) => referenceSuppressed(db, reference),
|
|
520
|
+
},
|
|
521
|
+
image,
|
|
522
|
+
platform(),
|
|
523
|
+
);
|
|
524
|
+
} finally {
|
|
525
|
+
db?.close();
|
|
526
|
+
}
|
|
527
|
+
const { batch, known, leftOut } = suggestions;
|
|
528
|
+
writeProjectFile(out, Buffer.from(batch.length === 0 ? "[]\n" : `[\n${batch.map((write) => ` ${JSON.stringify(write)}`).join(",\n")}\n]\n`), { what: "--out" });
|
|
529
|
+
return {
|
|
530
|
+
...result,
|
|
531
|
+
batch: { out, writes: batch.length, known, leftOut: listed(leftOut) },
|
|
532
|
+
...(batch.length === 0 ? {} : { apply: `knowledge.ts apply ${out} --reason "names of library routines"` }),
|
|
80
533
|
};
|
|
81
534
|
}
|
|
82
535
|
|
|
83
|
-
await runScript(() => run(
|
|
536
|
+
await runScript((argv) => run(argv), { usage: USAGE, refusals: [KnowledgeError, LibraryError] });
|