@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
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// Joins the batches of agents that worked in parallel, each on its own range
|
|
2
|
+
// of addresses (D79), into one batch for apply. The owner of an address is
|
|
3
|
+
// the batch whose range holds it: its name, comment, operand and module win
|
|
4
|
+
// there; at an address that no batch owns, the first batch in the order given
|
|
5
|
+
// wins. A name that a batch gave to an address, and that the join replaced
|
|
6
|
+
// with the name of another batch, is replaced in that batch's operand
|
|
7
|
+
// expressions and macro args too. The join writes no knowledge.
|
|
8
|
+
|
|
9
|
+
import { replaceIdentifiers } from "./expression.ts";
|
|
10
|
+
import type { BatchOperation } from "./write.ts";
|
|
11
|
+
|
|
12
|
+
export interface Batch {
|
|
13
|
+
/** The file the batch came from, for the report. */
|
|
14
|
+
file: string;
|
|
15
|
+
/** The range of addresses that the batch owns. */
|
|
16
|
+
owner?: { start: number; end: number };
|
|
17
|
+
ops: BatchOperation[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** One place where batches gave different values: the value that stays, and the batches whose value the join left out. */
|
|
21
|
+
export interface JoinChoice {
|
|
22
|
+
what: "name" | "comment" | "operand" | "module" | "macro";
|
|
23
|
+
space: string;
|
|
24
|
+
/** The address, or for a macro its name. */
|
|
25
|
+
at: number | string;
|
|
26
|
+
kept: { file: string; value: string };
|
|
27
|
+
left: Array<{ file: string; value: string }>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A clash that the join cannot decide: one name, or one module file, at two addresses. */
|
|
31
|
+
export interface JoinClash {
|
|
32
|
+
what: "name" | "file";
|
|
33
|
+
value: string;
|
|
34
|
+
space: string;
|
|
35
|
+
places: Array<{ address: number; file: string }>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface JoinResult {
|
|
39
|
+
ops: BatchOperation[];
|
|
40
|
+
choices: JoinChoice[];
|
|
41
|
+
clashes: JoinClash[];
|
|
42
|
+
/** Writes that were the same in more than one batch, and that the join wrote once. */
|
|
43
|
+
repeated: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type Op<K extends BatchOperation["op"]> = Extract<BatchOperation, { op: K }>;
|
|
47
|
+
const spaceOf = (op: { space?: string }) => op.space ?? "c64";
|
|
48
|
+
|
|
49
|
+
/** The value of a keyed write as text, to compare and report it. */
|
|
50
|
+
function valueOf(op: BatchOperation): string {
|
|
51
|
+
switch (op.op) {
|
|
52
|
+
case "rename":
|
|
53
|
+
return op.kind === undefined ? op.name : `${op.name} (${op.kind})`;
|
|
54
|
+
case "comment":
|
|
55
|
+
return op.text;
|
|
56
|
+
case "operand":
|
|
57
|
+
return op.expression ?? `${op.select} ${op.symbol ?? ""}${op.offset === undefined || op.offset === 0 ? "" : op.offset > 0 ? `+${op.offset}` : op.offset}`;
|
|
58
|
+
case "module":
|
|
59
|
+
return op.title === undefined ? op.file : `${op.file} (${op.title})`;
|
|
60
|
+
case "macro":
|
|
61
|
+
return `${op.name}(${(op.params ?? []).join(", ")})`;
|
|
62
|
+
default:
|
|
63
|
+
return JSON.stringify(op);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function joinBatches(batches: readonly Batch[]): JoinResult {
|
|
68
|
+
const owns = (batch: Batch, address: number) => batch.owner !== undefined && address >= batch.owner.start && address <= batch.owner.end;
|
|
69
|
+
const choices: JoinChoice[] = [];
|
|
70
|
+
const clashes: JoinClash[] = [];
|
|
71
|
+
|
|
72
|
+
/** For each key, the write that stays: the owner's last write, else the first batch's last write. */
|
|
73
|
+
function choose<K extends "rename" | "comment" | "operand" | "module" | "macro">(op: K, what: JoinChoice["what"], keyOf: (write: Op<K>) => string, at: (write: Op<K>) => number | string) {
|
|
74
|
+
const all = new Map<string, Array<{ batch: Batch; write: Op<K> }>>();
|
|
75
|
+
for (const batch of batches) {
|
|
76
|
+
for (const write of batch.ops) {
|
|
77
|
+
if (write.op !== op) continue;
|
|
78
|
+
const key = keyOf(write as Op<K>);
|
|
79
|
+
all.set(key, [...(all.get(key) ?? []), { batch, write: write as Op<K> }]);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const kept = new Map<string, { batch: Batch; write: Op<K> }>();
|
|
83
|
+
for (const [key, candidates] of all) {
|
|
84
|
+
const place = at(candidates[0]!.write);
|
|
85
|
+
const owner = typeof place === "number" ? candidates.filter((candidate) => owns(candidate.batch, place)) : [];
|
|
86
|
+
// A batch can write a place twice; its last write counts, as in apply.
|
|
87
|
+
const pick = (owner.length > 0 ? owner : candidates.filter((candidate) => candidate.batch === candidates[0]!.batch)).at(-1)!;
|
|
88
|
+
kept.set(key, pick);
|
|
89
|
+
const left = candidates.filter((candidate) => candidate.batch !== pick.batch && valueOf(candidate.write) !== valueOf(pick.write));
|
|
90
|
+
const distinct = [...new Map(left.map((candidate) => [`${candidate.batch.file}|${valueOf(candidate.write)}`, candidate])).values()];
|
|
91
|
+
if (distinct.length > 0) {
|
|
92
|
+
choices.push({
|
|
93
|
+
what,
|
|
94
|
+
space: spaceOf(pick.write),
|
|
95
|
+
at: place,
|
|
96
|
+
kept: { file: pick.batch.file, value: valueOf(pick.write) },
|
|
97
|
+
left: distinct.map((candidate) => ({ file: candidate.batch.file, value: valueOf(candidate.write) })),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return kept;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const names = choose("rename", "name", (write) => `${spaceOf(write)}|${write.address}`, (write) => write.address);
|
|
105
|
+
// Name clashes: one name for two addresses after the choice.
|
|
106
|
+
const byName = new Map<string, Array<{ address: number; file: string }>>();
|
|
107
|
+
for (const { batch, write } of names.values()) {
|
|
108
|
+
const key = `${spaceOf(write)}|${write.name}`;
|
|
109
|
+
byName.set(key, [...(byName.get(key) ?? []), { address: write.address, file: batch.file }]);
|
|
110
|
+
}
|
|
111
|
+
for (const [key, places] of byName) {
|
|
112
|
+
if (places.length > 1) clashes.push({ what: "name", value: key.split("|")[1]!, space: key.split("|")[0]!, places: places.sort((a, b) => a.address - b.address) });
|
|
113
|
+
}
|
|
114
|
+
// A name that a batch gave and the join replaced: each batch's names follow the join's names.
|
|
115
|
+
const finalName = new Map<string, string>();
|
|
116
|
+
for (const write of names.values()) finalName.set(`${spaceOf(write.write)}|${write.write.address}`, write.write.name);
|
|
117
|
+
const renamedIn = new Map<Batch, Map<string, string>>();
|
|
118
|
+
for (const batch of batches) {
|
|
119
|
+
const map = new Map<string, string>();
|
|
120
|
+
for (const write of batch.ops) {
|
|
121
|
+
if (write.op !== "rename") continue;
|
|
122
|
+
const now = finalName.get(`${spaceOf(write)}|${write.address}`);
|
|
123
|
+
if (now !== undefined && now !== write.name) map.set(`${spaceOf(write)}|${write.name}`, now);
|
|
124
|
+
}
|
|
125
|
+
renamedIn.set(batch, map);
|
|
126
|
+
}
|
|
127
|
+
const follow = (batch: Batch, space: string, text: string) => {
|
|
128
|
+
const map = renamedIn.get(batch)!;
|
|
129
|
+
if (map.size === 0) return text;
|
|
130
|
+
try {
|
|
131
|
+
return replaceIdentifiers(text, (name) => map.get(`${space}|${name}`) ?? name);
|
|
132
|
+
} catch {
|
|
133
|
+
// Text that is no expression stays as the batch wrote it; apply refuses it then.
|
|
134
|
+
return text;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const comments = choose("comment", "comment", (write) => `${spaceOf(write)}|${write.address}|${write.placement}`, (write) => write.address);
|
|
139
|
+
const operands = choose("operand", "operand", (write) => `${spaceOf(write)}|${write.address}`, (write) => write.address);
|
|
140
|
+
const modules = choose("module", "module", (write) => `${spaceOf(write)}|${write.start}`, (write) => write.start);
|
|
141
|
+
const macros = choose("macro", "macro", (write) => `${spaceOf(write)}|${write.name}`, (write) => write.name);
|
|
142
|
+
const byFile = new Map<string, Array<{ address: number; file: string }>>();
|
|
143
|
+
for (const { batch, write } of modules.values()) {
|
|
144
|
+
const key = `${spaceOf(write)}|${write.file}`;
|
|
145
|
+
byFile.set(key, [...(byFile.get(key) ?? []), { address: write.start, file: batch.file }]);
|
|
146
|
+
}
|
|
147
|
+
for (const [key, places] of byFile) {
|
|
148
|
+
if (places.length > 1) clashes.push({ what: "file", value: key.split("|")[1]!, space: key.split("|")[0]!, places: places.sort((a, b) => a.address - b.address) });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// The order of the joined batch: removals, the regions of other batches and then the owner's (a later classify wins
|
|
152
|
+
// where they overlap), the names, the comments, the modules, the references, the macros, then the writes that use names.
|
|
153
|
+
let repeated = 0;
|
|
154
|
+
const seen = new Set<string>();
|
|
155
|
+
const once = (write: BatchOperation) => {
|
|
156
|
+
const key = JSON.stringify(write);
|
|
157
|
+
if (seen.has(key)) {
|
|
158
|
+
repeated++;
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
seen.add(key);
|
|
162
|
+
return true;
|
|
163
|
+
};
|
|
164
|
+
const named = new Set([...names.values()].map(({ write }) => `${spaceOf(write)}|${write.address}`));
|
|
165
|
+
const removals: BatchOperation[] = [];
|
|
166
|
+
const foreignRegions: BatchOperation[] = [];
|
|
167
|
+
const ownRegions: BatchOperation[] = [];
|
|
168
|
+
const references: BatchOperation[] = [];
|
|
169
|
+
const rest: BatchOperation[] = [];
|
|
170
|
+
for (const batch of batches) {
|
|
171
|
+
for (const write of batch.ops) {
|
|
172
|
+
switch (write.op) {
|
|
173
|
+
case "remove-symbol":
|
|
174
|
+
// A name from any batch wins over a removal at the same address.
|
|
175
|
+
if (!named.has(`${spaceOf(write)}|${write.address}`) && once(write)) removals.push(write);
|
|
176
|
+
break;
|
|
177
|
+
case "classify":
|
|
178
|
+
case "unclassify":
|
|
179
|
+
if (once(write)) (owns(batch, write.start) ? ownRegions : foreignRegions).push(write);
|
|
180
|
+
break;
|
|
181
|
+
case "reference":
|
|
182
|
+
case "unreference":
|
|
183
|
+
if (once(write)) references.push(write);
|
|
184
|
+
break;
|
|
185
|
+
case "rename":
|
|
186
|
+
case "comment":
|
|
187
|
+
case "operand":
|
|
188
|
+
case "module":
|
|
189
|
+
case "macro":
|
|
190
|
+
break;
|
|
191
|
+
case "use":
|
|
192
|
+
if (once(write)) rest.push({ ...write, args: (write.args ?? []).map((arg) => follow(batch, spaceOf(write), arg)) });
|
|
193
|
+
break;
|
|
194
|
+
default:
|
|
195
|
+
if (once(write)) rest.push(write);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const kept = <T extends { batch: Batch; write: BatchOperation }>(map: Map<string, T>) => [...map.values()].map(({ write }) => write);
|
|
200
|
+
const operandWrites = [...operands.values()].map(({ batch, write }) => ({
|
|
201
|
+
...write,
|
|
202
|
+
...(write.symbol === undefined ? {} : { symbol: follow(batch, spaceOf(write), write.symbol) }),
|
|
203
|
+
...(write.expression === undefined ? {} : { expression: follow(batch, spaceOf(write), write.expression) }),
|
|
204
|
+
}));
|
|
205
|
+
return {
|
|
206
|
+
ops: [...removals, ...foreignRegions, ...ownRegions, ...kept(names), ...kept(comments), ...kept(modules), ...references, ...kept(macros), ...operandWrites, ...rest],
|
|
207
|
+
choices,
|
|
208
|
+
clashes,
|
|
209
|
+
repeated,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
// The program in the knowledge (D83): its bytes, its entry with the machine
|
|
2
|
+
// state and the stages of its unpacker, and the coverage of the runs. A
|
|
3
|
+
// capture and the coverage maps are imported one time; every later step reads
|
|
4
|
+
// them here, so a project needs no image or coverage files to be made again.
|
|
5
|
+
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
8
|
+
|
|
9
|
+
import { COVERAGE } from "../c64.ts";
|
|
10
|
+
import { KnowledgeError } from "./database.ts";
|
|
11
|
+
import { COVERAGE_KINDS, DEFAULT_SPACE, MEMORY_ROW, type CoverageKind, type Space } from "./schema.ts";
|
|
12
|
+
import { checkSpace, hex, write, type AddressRange, type Evidence, type WriteContext } from "./write.ts";
|
|
13
|
+
|
|
14
|
+
/** The 64 KiB of one space: offset n is address n. */
|
|
15
|
+
const SIZE = 0x10000;
|
|
16
|
+
|
|
17
|
+
/** The coverage map flag of each kind; wx has its own map (D83). */
|
|
18
|
+
const KIND_BITS: Record<Exclude<CoverageKind, "wx">, number> = {
|
|
19
|
+
r: COVERAGE.ramRead,
|
|
20
|
+
w: COVERAGE.ramWrite,
|
|
21
|
+
x: COVERAGE.ramExecute,
|
|
22
|
+
"rom-r": COVERAGE.romRead,
|
|
23
|
+
"rom-x": COVERAGE.romExecute,
|
|
24
|
+
"io-r": COVERAGE.ioRead,
|
|
25
|
+
"io-w": COVERAGE.ioWrite,
|
|
26
|
+
"io-x": COVERAGE.ioExecute,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** The program of one space as the knowledge holds it. */
|
|
30
|
+
export interface ProgramImage {
|
|
31
|
+
space: Space;
|
|
32
|
+
/** The address ranges that hold bytes, in address order, without overlap. */
|
|
33
|
+
segments: AddressRange[];
|
|
34
|
+
/** 64 KiB; an address outside the segments holds 0. */
|
|
35
|
+
memory: Uint8Array;
|
|
36
|
+
/** The byte at an address inside a segment. */
|
|
37
|
+
byteAt: (address: number) => number;
|
|
38
|
+
entry?: number;
|
|
39
|
+
/** The machine state at the entry: the content of the entry_state.json of the c64-unpacker skill. */
|
|
40
|
+
state?: Record<string, unknown>;
|
|
41
|
+
/** The stages of the unpacker capture, in order: the last one is the program. */
|
|
42
|
+
stages?: Array<Record<string, unknown>>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** What `setImage` writes: the bytes of the segments, and the entry when the program has one. */
|
|
46
|
+
export interface ImageRequest {
|
|
47
|
+
space?: Space;
|
|
48
|
+
segments: AddressRange[];
|
|
49
|
+
byteAt: (address: number) => number;
|
|
50
|
+
entry?: number;
|
|
51
|
+
state?: Record<string, unknown>;
|
|
52
|
+
stages?: Array<Record<string, unknown>>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** One coverage map to import, with the project path that names it as evidence. */
|
|
56
|
+
export interface CoverageMap {
|
|
57
|
+
name: string;
|
|
58
|
+
bytes: Uint8Array;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The address ranges where `has` is true, in address order. */
|
|
62
|
+
function rangesWhere(has: (address: number) => boolean): AddressRange[] {
|
|
63
|
+
const ranges: AddressRange[] = [];
|
|
64
|
+
for (let address = 0; address < SIZE; address++) {
|
|
65
|
+
if (!has(address)) continue;
|
|
66
|
+
const start = address;
|
|
67
|
+
while (address + 1 < SIZE && has(address + 1)) address++;
|
|
68
|
+
ranges.push({ start, end: address });
|
|
69
|
+
}
|
|
70
|
+
return ranges;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The memory rows of the segments: at most MEMORY_ROW bytes, never across a multiple of MEMORY_ROW. */
|
|
74
|
+
function memoryRows(segments: readonly AddressRange[], byteAt: (address: number) => number): Map<number, string> {
|
|
75
|
+
const rows = new Map<number, string>();
|
|
76
|
+
for (const segment of segments) {
|
|
77
|
+
for (let at = segment.start; at <= segment.end; ) {
|
|
78
|
+
const end = Math.min(segment.end, (at | (MEMORY_ROW - 1)));
|
|
79
|
+
rows.set(at, Buffer.from(Array.from({ length: end - at + 1 }, (_, index) => byteAt(at + index))).toString("hex"));
|
|
80
|
+
at = end + 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return rows;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function checkSegments(segments: readonly AddressRange[]): void {
|
|
87
|
+
if (segments.length === 0) throw new KnowledgeError("invalid-input", "An image has at least one segment.");
|
|
88
|
+
const sorted = [...segments].sort((a, b) => a.start - b.start);
|
|
89
|
+
sorted.forEach((segment, index) => {
|
|
90
|
+
if (!Number.isInteger(segment.start) || !Number.isInteger(segment.end) || segment.start < 0 || segment.end > 0xffff || segment.end < segment.start) {
|
|
91
|
+
throw new KnowledgeError("invalid-input", `The segment ${hex(segment.start)}-${hex(segment.end)} is not a range from $0000 to $ffff.`);
|
|
92
|
+
}
|
|
93
|
+
if (index > 0 && segment.start <= sorted[index - 1]!.end) throw new KnowledgeError("invalid-input", `The segments at ${hex(sorted[index - 1]!.start)} and ${hex(segment.start)} overlap.`);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Makes the program of a space equal to the image: closes the memory rows
|
|
99
|
+
* that changed or are gone, adds the new ones, and replaces the entry when it
|
|
100
|
+
* changed. An image that is already in the knowledge writes nothing.
|
|
101
|
+
*/
|
|
102
|
+
export function setImage(db: DatabaseSync, context: WriteContext, request: ImageRequest): { revision: string | null; current: { space: Space; bytes: number; rowsAdded: number; rowsClosed: number; entry: boolean } } {
|
|
103
|
+
checkSpace(request);
|
|
104
|
+
checkSegments(request.segments);
|
|
105
|
+
if (request.entry !== undefined && (!Number.isInteger(request.entry) || request.entry < 0 || request.entry > 0xffff)) throw new KnowledgeError("invalid-input", "The entry must be from $0000 to $ffff.");
|
|
106
|
+
if (request.entry === undefined && (request.state !== undefined || request.stages !== undefined)) throw new KnowledgeError("invalid-input", "A machine state or stages need an entry.");
|
|
107
|
+
const space = request.space ?? DEFAULT_SPACE;
|
|
108
|
+
const wanted = memoryRows(request.segments, request.byteAt);
|
|
109
|
+
return write(db, context, "image", (change) => {
|
|
110
|
+
const current = db.prepare("SELECT id, address, bytes FROM current_memory WHERE space = ?").all(space) as Array<{ id: string; address: number; bytes: string }>;
|
|
111
|
+
const kept = new Set<number>();
|
|
112
|
+
let rowsClosed = 0;
|
|
113
|
+
for (const row of current) {
|
|
114
|
+
if (wanted.get(row.address) === row.bytes) kept.add(row.address);
|
|
115
|
+
else {
|
|
116
|
+
change.close("memory", "id = ?", row.id);
|
|
117
|
+
rowsClosed++;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
let rowsAdded = 0;
|
|
121
|
+
for (const [address, bytes] of [...wanted].sort(([a], [b]) => a - b)) {
|
|
122
|
+
if (kept.has(address)) continue;
|
|
123
|
+
change.insertMemory({ space, address, bytes, origin: context.origin });
|
|
124
|
+
rowsAdded++;
|
|
125
|
+
}
|
|
126
|
+
const entryRow = db.prepare("SELECT id, address, state, stages FROM current_entries WHERE space = ?").get(space) as { id: string; address: number; state: string | null; stages: string | null } | undefined;
|
|
127
|
+
const state = request.state === undefined ? null : JSON.stringify(request.state);
|
|
128
|
+
const stages = request.stages === undefined ? null : JSON.stringify(request.stages);
|
|
129
|
+
const same = entryRow !== undefined && entryRow.address === request.entry && entryRow.state === state && entryRow.stages === stages;
|
|
130
|
+
const entryChanged = !same && (entryRow !== undefined || request.entry !== undefined);
|
|
131
|
+
if (entryChanged) {
|
|
132
|
+
if (entryRow !== undefined) change.close("entries", "id = ?", entryRow.id);
|
|
133
|
+
if (request.entry !== undefined) change.insertEntry({ space, address: request.entry, state, stages, origin: context.origin });
|
|
134
|
+
}
|
|
135
|
+
const bytes = request.segments.reduce((sum, segment) => sum + segment.end - segment.start + 1, 0);
|
|
136
|
+
return { space, bytes, rowsAdded, rowsClosed, entry: entryChanged };
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The coverage of one space as ranges of each kind. */
|
|
141
|
+
function currentCoverage(db: DatabaseSync, space: Space): Map<CoverageKind, Uint8Array> {
|
|
142
|
+
const maps = new Map<CoverageKind, Uint8Array>(COVERAGE_KINDS.map((kind) => [kind, new Uint8Array(SIZE)]));
|
|
143
|
+
for (const row of db.prepare("SELECT kind, start_address, end_address FROM current_coverage WHERE space = ?").all(space) as Array<{ kind: CoverageKind; start_address: number; end_address: number }>) {
|
|
144
|
+
maps.get(row.kind)!.fill(1, row.start_address, row.end_address + 1);
|
|
145
|
+
}
|
|
146
|
+
return maps;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The project paths of the coverage maps that the knowledge imported, with their SHA-256. */
|
|
150
|
+
export function coverageRuns(db: DatabaseSync | undefined): Array<{ name: string; sha256: string }> {
|
|
151
|
+
if (db === undefined) return [];
|
|
152
|
+
return db.prepare("SELECT DISTINCT e.ref AS name, e.sha256 AS sha256 FROM evidence e JOIN revisions r ON r.id = e.revision WHERE r.operation = 'coverage' AND e.kind = 'coverage' ORDER BY e.ref").all() as Array<{
|
|
153
|
+
name: string;
|
|
154
|
+
sha256: string;
|
|
155
|
+
}>;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Joins coverage maps into the coverage of a space (D83): adds the ranges of
|
|
160
|
+
* the flags that no imported run set before, and as wx the addresses that one
|
|
161
|
+
* run both wrote and executed. Each new map is evidence of the revision. A map
|
|
162
|
+
* that the knowledge has with the same name and bytes is not imported again.
|
|
163
|
+
*/
|
|
164
|
+
export function addCoverage(
|
|
165
|
+
db: DatabaseSync,
|
|
166
|
+
context: WriteContext,
|
|
167
|
+
request: { space?: Space; maps: CoverageMap[] },
|
|
168
|
+
): { revision: string | null; current: { space: Space; imported: string[]; known: string[]; ranges: number } } {
|
|
169
|
+
checkSpace(request);
|
|
170
|
+
const space = request.space ?? DEFAULT_SPACE;
|
|
171
|
+
for (const map of request.maps) {
|
|
172
|
+
if (map.bytes.length !== SIZE) throw new KnowledgeError("invalid-input", `${map.name} has ${map.bytes.length} bytes; a coverage map has 65536, one for each address.`);
|
|
173
|
+
}
|
|
174
|
+
const hashed = request.maps.map((map) => ({ ...map, sha256: createHash("sha256").update(map.bytes).digest("hex") }));
|
|
175
|
+
const known = new Set(coverageRuns(db).map((run) => `${run.name}\n${run.sha256}`));
|
|
176
|
+
const fresh = hashed.filter((map) => !known.has(`${map.name}\n${map.sha256}`));
|
|
177
|
+
const evidence: Evidence[] = [...(context.evidence ?? []), ...fresh.map((map): Evidence => ({ kind: "coverage", ref: map.name, sha256: map.sha256 }))];
|
|
178
|
+
return write(db, { ...context, evidence }, "coverage", (change) => {
|
|
179
|
+
const result = { space, imported: fresh.map((map) => map.name), known: hashed.filter((map) => !fresh.includes(map)).map((map) => map.name), ranges: 0 };
|
|
180
|
+
if (fresh.length === 0) return result;
|
|
181
|
+
// A revision for each import, also when no flag is new: the maps are its evidence.
|
|
182
|
+
change.id();
|
|
183
|
+
const union = currentCoverage(db, space);
|
|
184
|
+
for (const map of fresh) {
|
|
185
|
+
for (const kind of COVERAGE_KINDS) {
|
|
186
|
+
const has = kind === "wx" ? (address: number) => (map.bytes[address]! & (COVERAGE.ramWrite | COVERAGE.ramExecute)) === (COVERAGE.ramWrite | COVERAGE.ramExecute) : (address: number) => (map.bytes[address]! & KIND_BITS[kind]) !== 0;
|
|
187
|
+
const known = union.get(kind)!;
|
|
188
|
+
for (const range of rangesWhere((address) => known[address] === 0 && has(address))) {
|
|
189
|
+
change.insertCoverage({ space, kind, start: range.start, end: range.end, origin: context.origin });
|
|
190
|
+
known.fill(1, range.start, range.end + 1);
|
|
191
|
+
result.ranges++;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return result;
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The program of a space, or undefined when the knowledge has no bytes for it. */
|
|
200
|
+
export function programImage(db: DatabaseSync | undefined, space: Space = DEFAULT_SPACE): ProgramImage | undefined {
|
|
201
|
+
if (db === undefined) return undefined;
|
|
202
|
+
const rows = db.prepare("SELECT address, bytes FROM current_memory WHERE space = ? ORDER BY address").all(space) as Array<{ address: number; bytes: string }>;
|
|
203
|
+
if (rows.length === 0) return undefined;
|
|
204
|
+
const memory = new Uint8Array(SIZE);
|
|
205
|
+
const segments: AddressRange[] = [];
|
|
206
|
+
for (const row of rows) {
|
|
207
|
+
const bytes = Buffer.from(row.bytes, "hex");
|
|
208
|
+
memory.set(bytes, row.address);
|
|
209
|
+
const end = row.address + bytes.length - 1;
|
|
210
|
+
const last = segments.at(-1);
|
|
211
|
+
if (last !== undefined && last.end + 1 === row.address) last.end = end;
|
|
212
|
+
else segments.push({ start: row.address, end });
|
|
213
|
+
}
|
|
214
|
+
const image: ProgramImage = { space, segments, memory, byteAt: (address) => memory[address]! };
|
|
215
|
+
const entry = db.prepare("SELECT address, state, stages FROM current_entries WHERE space = ?").get(space) as { address: number; state: string | null; stages: string | null } | undefined;
|
|
216
|
+
if (entry !== undefined) {
|
|
217
|
+
image.entry = entry.address;
|
|
218
|
+
if (entry.state !== null) image.state = JSON.parse(entry.state) as Record<string, unknown>;
|
|
219
|
+
if (entry.stages !== null) image.stages = JSON.parse(entry.stages) as Array<Record<string, unknown>>;
|
|
220
|
+
}
|
|
221
|
+
return image;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** The program of a space for a tool; refused with the way to put it in when the knowledge has none. */
|
|
225
|
+
export function requireProgram(db: DatabaseSync | undefined, space: Space = DEFAULT_SPACE): ProgramImage {
|
|
226
|
+
const image = programImage(db, space);
|
|
227
|
+
if (image !== undefined) return image;
|
|
228
|
+
const where = space === DEFAULT_SPACE ? "" : ` in ${space}`;
|
|
229
|
+
const option = space === DEFAULT_SPACE ? "" : ` --space ${space}`;
|
|
230
|
+
throw new KnowledgeError("not-found", `The knowledge has no program${where}. Put it in with knowledge.ts image <image.json|prg>${option} of the c64-knowledge skill.`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** The bytes for an analyzer: a PRG for one segment without an entry state, else the 64 KiB. */
|
|
234
|
+
export function analyzedBytes(image: ProgramImage): { bytes: Buffer; kind: "prg" | "flat64k" } {
|
|
235
|
+
if (image.segments.length === 1 && image.state === undefined) {
|
|
236
|
+
const [segment] = image.segments;
|
|
237
|
+
return { bytes: Buffer.concat([Buffer.from([segment!.start & 0xff, segment!.start >> 8]), Buffer.from(image.memory.subarray(segment!.start, segment!.end + 1))]), kind: "prg" };
|
|
238
|
+
}
|
|
239
|
+
return { bytes: Buffer.from(image.memory), kind: "flat64k" };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The coverage of a space as a joined coverage map (the bits of COVERAGE in
|
|
244
|
+
* c64.ts), and the addresses that one run both wrote and executed; undefined
|
|
245
|
+
* when the knowledge has no coverage for the space.
|
|
246
|
+
*/
|
|
247
|
+
export function programCoverage(db: DatabaseSync | undefined, space: Space = DEFAULT_SPACE): { map: Uint8Array; selfModified: Uint8Array } | undefined {
|
|
248
|
+
if (db === undefined) return undefined;
|
|
249
|
+
const rows = db.prepare("SELECT kind, start_address, end_address FROM current_coverage WHERE space = ?").all(space) as Array<{ kind: CoverageKind; start_address: number; end_address: number }>;
|
|
250
|
+
if (rows.length === 0) return undefined;
|
|
251
|
+
const map = new Uint8Array(SIZE);
|
|
252
|
+
const selfModified = new Uint8Array(SIZE);
|
|
253
|
+
for (const row of rows) {
|
|
254
|
+
if (row.kind === "wx") selfModified.fill(1, row.start_address, row.end_address + 1);
|
|
255
|
+
else for (let address = row.start_address; address <= row.end_address; address++) map[address]! |= KIND_BITS[row.kind];
|
|
256
|
+
}
|
|
257
|
+
return { map, selfModified };
|
|
258
|
+
}
|