@henols/c64-re-tools 2.0.0-rc.8 → 2.0.0
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 +98 -115
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +22 -6
- package/skills/c64-assembler/scripts/assemble.ts +42 -14
- package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
- package/skills/c64-basic/SKILL.md +10 -1
- package/skills/c64-basic/scripts/basic.ts +3 -3
- package/skills/c64-disk/SKILL.md +13 -4
- package/skills/c64-disk/scripts/disk.ts +7 -7
- package/skills/c64-disk/scripts/{containers.ts → lib/containers.ts} +2 -2
- 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 +109 -0
- package/skills/c64-docs/scripts/lib/links.ts +167 -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 +239 -0
- package/skills/c64-emulator/SKILL.md +16 -5
- package/skills/c64-knowledge/SKILL.md +140 -18
- package/skills/c64-knowledge/scripts/knowledge.ts +737 -112
- package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
- package/skills/c64-memory-map/SKILL.md +11 -2
- package/skills/c64-memory-map/scripts/memmap.ts +2 -2
- package/skills/c64-provenance/SKILL.md +66 -11
- package/skills/c64-provenance/scripts/{compare.ts → lib/compare.ts} +3 -3
- package/skills/c64-provenance/scripts/lib/library.ts +584 -0
- package/skills/c64-provenance/scripts/{runtime.ts → lib/runtime.ts} +2 -2
- package/skills/c64-provenance/scripts/{separate.ts → lib/separate.ts} +3 -3
- package/skills/c64-provenance/scripts/provenance.ts +242 -40
- package/skills/c64-reverse-engineering/SKILL.md +51 -26
- package/skills/c64-source/SKILL.md +76 -28
- package/skills/c64-source/scripts/generate.ts +425 -127
- package/skills/c64-source/scripts/{loader.ts → lib/loader.ts} +84 -25
- package/skills/c64-source/scripts/lib/source.ts +1121 -0
- package/skills/c64-static-analysis/SKILL.md +35 -18
- package/skills/c64-static-analysis/scripts/analyze.ts +184 -55
- package/skills/c64-static-analysis/scripts/{coverage.ts → lib/coverage.ts} +19 -25
- package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +6 -2
- package/skills/c64-static-analysis/scripts/{idioms.ts → lib/idioms.ts} +21 -12
- package/skills/c64-static-analysis/scripts/{instructions.ts → lib/instructions.ts} +1 -1
- 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 +36 -7
- package/skills/c64-testing/references/scenario-format.md +12 -8
- 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} +45 -34
- package/skills/c64-testing/scripts/run.ts +73 -21
- package/skills/c64-testing/scripts/test.ts +5 -5
- package/skills/c64-unpacker/SKILL.md +23 -9
- package/skills/c64-unpacker/scripts/{evidence.ts → lib/evidence.ts} +1 -1
- package/skills/c64-unpacker/scripts/{loads.ts → lib/loads.ts} +4 -4
- package/skills/c64-unpacker/scripts/{machine.ts → lib/machine.ts} +4 -4
- package/skills/c64-unpacker/scripts/{stages.ts → lib/stages.ts} +17 -5
- package/skills/c64-unpacker/scripts/unpack.ts +41 -24
- package/src/c64.ts +103 -5
- package/src/cli/cleanup.ts +1 -1
- package/src/cli/main.ts +7 -4
- package/src/cli/status.ts +70 -3
- package/src/graphics.ts +158 -0
- package/src/host/main.ts +43 -8
- package/src/host/server.ts +10 -3
- package/src/host/tools/petcat.ts +1 -1
- package/src/host/tools/status.ts +1 -1
- package/src/host/vice/adapter.ts +4 -3
- package/src/host/vice/process.ts +2 -2
- package/src/host/vice/screen.ts +1 -42
- package/src/host/vice/session.ts +196 -23
- package/src/host-client/autostart.ts +1 -1
- package/src/host-client/connect.ts +1 -1
- package/src/host-client/coverage.ts +2 -29
- package/src/host-client/image.ts +66 -0
- package/src/host-client/steps.ts +9 -4
- package/src/host-client/tools.ts +1 -0
- package/src/host-client/transfer.ts +39 -11
- package/src/host-client/vice-session.ts +33 -16
- 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 +70 -22
- package/src/knowledge/import.ts +64 -55
- package/src/knowledge/join.ts +211 -0
- package/src/knowledge/program.ts +266 -0
- package/src/knowledge/read.ts +258 -27
- package/src/knowledge/schema.ts +199 -75
- package/src/knowledge/text.ts +415 -0
- package/src/knowledge/write.ts +740 -57
- package/src/mcp/main.ts +1 -1
- package/src/mcp/output.ts +15 -8
- package/src/mcp/server.ts +3 -1
- package/src/mcp/tools/debug.ts +97 -16
- package/src/mcp/tools/execution.ts +38 -5
- package/src/mcp/tools/machine.ts +11 -2
- package/src/mcp/tools/media.ts +18 -13
- package/src/mcp/tools/memory.ts +17 -8
- package/src/mcp/tools/video.ts +9 -9
- package/src/native/discover.ts +2 -2
- package/src/native/ghidra/analyze.ts +2 -2
- package/src/native/ghidra/index.ts +1 -1
- package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
- package/src/native/ghidra/scripts/C64Export.java +1 -1
- package/src/native/ghidra/scripts/C64Prepare.java +7 -1
- package/src/native/types.ts +14 -8
- package/{skills/c64-source/scripts → src}/opcodes.ts +75 -3
- package/src/platform.ts +70 -7
- package/src/project.ts +180 -3
- package/src/protocol/messages.ts +1 -1
- package/src/protocol/tools.ts +2 -16
- package/src/protocol/vice.ts +124 -15
- package/src/script.ts +46 -7
- package/src/trace.ts +55 -23
- package/skills/c64-assembler/scripts/compare.ts +0 -84
- package/skills/c64-source/scripts/source.ts +0 -595
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
// A library of routines that several programs share (D71): a plain folder
|
|
2
|
+
// with one <name>.json for each routine, outside every project. A routine holds
|
|
3
|
+
// its bytes, which of its bytes move with it, and its knowledge at offsets from
|
|
4
|
+
// its start, so match finds it in another program at another address and
|
|
5
|
+
// suggests its names there as an apply batch. Nothing here writes knowledge.
|
|
6
|
+
|
|
7
|
+
import { mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { join, resolve } from "node:path";
|
|
9
|
+
|
|
10
|
+
import { formatC64Address, instructionLength } from "#src/c64.ts";
|
|
11
|
+
import { evaluate, ExpressionError, identifiers, operandText } from "#src/knowledge/expression.ts";
|
|
12
|
+
import type { CommentRow, OperandRow, ReferenceRow, RegionRow, SymbolRow } from "#src/knowledge/read.ts";
|
|
13
|
+
import { COMMENT_PLACEMENTS, OPERAND_SELECTS, REFERENCE_KINDS, REGION_TYPES, SYMBOL_KINDS, type CommentPlacement, type OperandSelect, type ReferenceKind, type RegionType, type SymbolKind } from "#src/knowledge/schema.ts";
|
|
14
|
+
import { PROJECT_DIRECTORY } from "#src/project.ts";
|
|
15
|
+
|
|
16
|
+
/** A routine shorter than this is not saved and not searched: its bytes occur by chance. */
|
|
17
|
+
export const MIN_ROUTINE = 16;
|
|
18
|
+
|
|
19
|
+
/** A routine name is also its file name: a letter or underscore, then letters, digits or underscores. */
|
|
20
|
+
const ROUTINE_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
|
|
21
|
+
|
|
22
|
+
/** Bytes on each line of a routine file, so that git shows a small diff. */
|
|
23
|
+
const BYTES_PER_LINE = 16;
|
|
24
|
+
|
|
25
|
+
/** A refusal of the library rules, with a stable code. */
|
|
26
|
+
export class LibraryError extends Error {
|
|
27
|
+
override name = "LibraryError";
|
|
28
|
+
readonly code: "invalid-input" | "not-found" | "conflict";
|
|
29
|
+
|
|
30
|
+
constructor(code: "invalid-input" | "not-found" | "conflict", message: string) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.code = code;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Bytes that move with the routine: a word, or the low or high byte, of the
|
|
38
|
+
* address at `value` (an offset from the routine start; -1 for the RTS entry
|
|
39
|
+
* of the first byte).
|
|
40
|
+
*/
|
|
41
|
+
export interface Relocation {
|
|
42
|
+
at: number;
|
|
43
|
+
size: "word" | "lo" | "hi";
|
|
44
|
+
value: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A routine of the library. Every position is an offset from its start. */
|
|
48
|
+
export interface Routine {
|
|
49
|
+
name: string;
|
|
50
|
+
title?: string;
|
|
51
|
+
/** The program (its project folder name), the image, and the address it came from. */
|
|
52
|
+
from: { program: string; image: string; start: number };
|
|
53
|
+
bytes: Uint8Array;
|
|
54
|
+
/** The offset of each instruction start. */
|
|
55
|
+
instructions: number[];
|
|
56
|
+
relocations: Relocation[];
|
|
57
|
+
/** Operand bytes that address something outside the routine: they may differ in another program. */
|
|
58
|
+
variable: number[];
|
|
59
|
+
symbols: Array<{ at: number; name: string; kind: SymbolKind }>;
|
|
60
|
+
regions: Array<{ start: number; end: number; type: RegionType; element?: number }>;
|
|
61
|
+
comments: Array<{ at: number; placement: CommentPlacement; text: string }>;
|
|
62
|
+
references: Array<{ from: number; to: number; kind: ReferenceKind }>;
|
|
63
|
+
/** Operand expressions; `offset` is added to the symbol, as in knowledge. */
|
|
64
|
+
operands: Array<{ at: number; select: OperandSelect; symbol?: string; offset?: number; expression?: string }>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The pointer region types: a table of addresses, or of addresses minus 1 for RTS. */
|
|
68
|
+
const POINTER_TYPES: readonly RegionType[] = ["addresses", "addresses-split", "rts-addresses", "rts-addresses-split"];
|
|
69
|
+
|
|
70
|
+
/** The code region types: their bytes are instructions. */
|
|
71
|
+
const CODE_TYPES: readonly RegionType[] = ["code", "jumps"];
|
|
72
|
+
|
|
73
|
+
/** True for an opcode with an immediate operand, undocumented ones included. */
|
|
74
|
+
function isImmediate(opcode: number): boolean {
|
|
75
|
+
return (opcode & 0x1d) === 0x09 || (opcode >= 0x80 && ((opcode & 0x1f) === 0x00 || (opcode & 0x1f) === 0x02));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const isBranch = (opcode: number) => (opcode & 0x1f) === 0x10;
|
|
79
|
+
|
|
80
|
+
/** What the program knows about the range of a routine. Symbols, regions, comments, references and operands are user and LLM rows only. */
|
|
81
|
+
export interface RoutineSource {
|
|
82
|
+
name: string;
|
|
83
|
+
title?: string;
|
|
84
|
+
program: string;
|
|
85
|
+
image: string;
|
|
86
|
+
start: number;
|
|
87
|
+
end: number;
|
|
88
|
+
byteAt: (address: number) => number;
|
|
89
|
+
/** Regions of every origin: code regions give the instructions, pointer tables give relocations. */
|
|
90
|
+
analyzed: RegionRow[];
|
|
91
|
+
symbols: SymbolRow[];
|
|
92
|
+
regions: RegionRow[];
|
|
93
|
+
comments: CommentRow[];
|
|
94
|
+
references: ReferenceRow[];
|
|
95
|
+
operands: OperandRow[];
|
|
96
|
+
/** The C64 platform names: an operand expression may use them in any program. */
|
|
97
|
+
platform: ReadonlyMap<string, number>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** A row that a routine does not keep, and why. */
|
|
101
|
+
export interface LeftOut {
|
|
102
|
+
what: string;
|
|
103
|
+
reason: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Makes a routine from the knowledge of a program. Throws LibraryError for a
|
|
108
|
+
* range that cannot be a routine. Returns the rows it did not keep.
|
|
109
|
+
*/
|
|
110
|
+
export function routineFrom(source: RoutineSource): { routine: Routine; leftOut: LeftOut[] } {
|
|
111
|
+
const { start, end } = source;
|
|
112
|
+
const where = (address: number) => formatC64Address(address);
|
|
113
|
+
if (!ROUTINE_NAME.test(source.name)) throw new LibraryError("invalid-input", `A routine name is a letter or underscore, then letters, digits or underscores (at most 64), not ${source.name}.`);
|
|
114
|
+
const length = end - start + 1;
|
|
115
|
+
if (length < MIN_ROUTINE) throw new LibraryError("invalid-input", `${where(start)}-${where(end)} has ${length} bytes. A routine has at least ${MIN_ROUTINE}, because shorter byte sequences occur by chance.`);
|
|
116
|
+
const inside = (address: number) => address >= start && address <= end;
|
|
117
|
+
const bytes = Uint8Array.from({ length }, (_, index) => source.byteAt(start + index));
|
|
118
|
+
const leftOut: LeftOut[] = [];
|
|
119
|
+
|
|
120
|
+
// The instructions: each code region decoded from its start, or from the routine start.
|
|
121
|
+
const instructions: number[] = [];
|
|
122
|
+
const codeRegions = source.analyzed.filter((region) => CODE_TYPES.includes(region.type) && region.end >= start && region.start <= end).sort((a, b) => a.start - b.start);
|
|
123
|
+
/** The address after the last instruction: a code region that an instruction runs into goes on after it. */
|
|
124
|
+
let next = start;
|
|
125
|
+
for (const region of codeRegions) {
|
|
126
|
+
for (let at = Math.max(region.start, next); at <= Math.min(region.end, end); at = next) {
|
|
127
|
+
next = at + instructionLength(bytes[at - start]!);
|
|
128
|
+
if (next - 1 > end) throw new LibraryError("invalid-input", `The instruction at ${where(at)} runs past the end ${where(end)} of the routine. Give an end after it.`);
|
|
129
|
+
instructions.push(at - start);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (instructions.length === 0) throw new LibraryError("invalid-input", `The knowledge has no code region in ${where(start)}-${where(end)}. Run c64-static-analysis first, or classify the code.`);
|
|
133
|
+
|
|
134
|
+
// The operands: an address inside the routine moves with it; one outside may differ.
|
|
135
|
+
const relocations = new Map<number, Relocation>();
|
|
136
|
+
const variable = new Set<number>();
|
|
137
|
+
for (const offset of instructions) {
|
|
138
|
+
const opcode = bytes[offset]!;
|
|
139
|
+
const size = instructionLength(opcode);
|
|
140
|
+
if (size === 3) {
|
|
141
|
+
const value = bytes[offset + 1]! | (bytes[offset + 2]! << 8);
|
|
142
|
+
if (inside(value)) relocations.set(offset + 1, { at: offset + 1, size: "word", value: value - start });
|
|
143
|
+
else variable.add(offset + 1).add(offset + 2);
|
|
144
|
+
} else if (size === 2 && !isBranch(opcode) && !isImmediate(opcode)) variable.add(offset + 1);
|
|
145
|
+
}
|
|
146
|
+
for (const region of source.analyzed.filter((item) => POINTER_TYPES.includes(item.type))) {
|
|
147
|
+
const split = region.type.endsWith("-split");
|
|
148
|
+
const minus = region.type.startsWith("rts-") ? 1 : 0;
|
|
149
|
+
const count = Math.floor((region.end - region.start + 1) / 2);
|
|
150
|
+
for (let index = 0; index < count; index++) {
|
|
151
|
+
const low = split ? region.start + index : region.start + 2 * index;
|
|
152
|
+
const high = split ? region.start + count + index : low + 1;
|
|
153
|
+
if (!inside(low) || !inside(high)) continue;
|
|
154
|
+
const raw = source.byteAt(low) | (source.byteAt(high) << 8);
|
|
155
|
+
if (!inside((raw + minus) & 0xffff)) continue;
|
|
156
|
+
if (split) {
|
|
157
|
+
relocations.set(low - start, { at: low - start, size: "lo", value: raw - start });
|
|
158
|
+
relocations.set(high - start, { at: high - start, size: "hi", value: raw - start });
|
|
159
|
+
} else relocations.set(low - start, { at: low - start, size: "word", value: raw - start });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// The names of the routine, and its operand expressions over those names and the platform names.
|
|
164
|
+
const symbols = source.symbols.filter((symbol) => inside(symbol.address));
|
|
165
|
+
const named = new Map(symbols.map((symbol) => [symbol.name, symbol.address]));
|
|
166
|
+
const instructionAt = new Set(instructions.map((offset) => start + offset));
|
|
167
|
+
const operands: Routine["operands"] = [];
|
|
168
|
+
for (const row of source.operands.filter((item) => inside(item.address))) {
|
|
169
|
+
const what = `the operand at ${where(row.address)}`;
|
|
170
|
+
if (!instructionAt.has(row.address)) {
|
|
171
|
+
leftOut.push({ what, reason: "it is not at an instruction start of the code regions" });
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const names = row.select === "expr" ? identifiers(row.expression!) : [row.symbol!];
|
|
175
|
+
const outside = names.filter((name) => !named.has(name) && !source.platform.has(name));
|
|
176
|
+
if (outside.length > 0) {
|
|
177
|
+
leftOut.push({ what, reason: `it names ${outside.join(", ")}, which is neither a user or LLM name in the routine nor a platform name` });
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const offset = row.address - start;
|
|
181
|
+
const opcode = bytes[offset]!;
|
|
182
|
+
if (instructionLength(opcode) === 2 && isImmediate(opcode) && names.some((name) => named.has(name))) {
|
|
183
|
+
// An immediate that holds a byte of an address inside the routine moves with it.
|
|
184
|
+
if (row.select === "lo" || row.select === "hi") {
|
|
185
|
+
relocations.set(offset + 1, { at: offset + 1, size: row.select, value: named.get(row.symbol!)! + row.offset - start });
|
|
186
|
+
} else variable.add(offset + 1);
|
|
187
|
+
}
|
|
188
|
+
operands.push({ at: offset, select: row.select, ...(row.symbol === undefined ? {} : { symbol: row.symbol }), ...(row.offset === 0 ? {} : { offset: row.offset }), ...(row.expression === undefined ? {} : { expression: row.expression }) });
|
|
189
|
+
}
|
|
190
|
+
for (const at of relocations.keys()) variable.delete(at);
|
|
191
|
+
|
|
192
|
+
// The code regions of every origin, cut at the border: the routine's instructions are its code in any program.
|
|
193
|
+
// Other regions only from the user and the LLM, and only inside the routine.
|
|
194
|
+
const regions: Routine["regions"] = codeRegions.map((region) => ({ start: Math.max(region.start, start) - start, end: Math.min(region.end, end) - start, type: region.type }));
|
|
195
|
+
for (const region of source.regions.filter((item) => !CODE_TYPES.includes(item.type))) {
|
|
196
|
+
if (region.start >= start && region.end <= end) regions.push({ start: region.start - start, end: region.end - start, type: region.type, ...(region.element === undefined ? {} : { element: region.element }) });
|
|
197
|
+
else leftOut.push({ what: `the ${region.type} region ${where(region.start)}-${where(region.end)}`, reason: "it crosses the border of the routine" });
|
|
198
|
+
}
|
|
199
|
+
regions.sort((a, b) => a.start - b.start);
|
|
200
|
+
const references: Routine["references"] = [];
|
|
201
|
+
for (const reference of source.references) {
|
|
202
|
+
// Operand expressions make their pointer references again.
|
|
203
|
+
if (reference.kind === "pointer") continue;
|
|
204
|
+
if (inside(reference.from) && inside(reference.to)) references.push({ from: reference.from - start, to: reference.to - start, kind: reference.kind });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const routine: Routine = {
|
|
208
|
+
name: source.name,
|
|
209
|
+
...(source.title === undefined ? {} : { title: source.title }),
|
|
210
|
+
from: { program: source.program, image: source.image, start },
|
|
211
|
+
bytes,
|
|
212
|
+
instructions,
|
|
213
|
+
relocations: [...relocations.values()].sort((a, b) => a.at - b.at),
|
|
214
|
+
variable: [...variable].sort((a, b) => a - b),
|
|
215
|
+
symbols: symbols.map((symbol) => ({ at: symbol.address - start, name: symbol.name, kind: symbol.kind })),
|
|
216
|
+
regions,
|
|
217
|
+
comments: source.comments.filter((comment) => inside(comment.address)).map((comment) => ({ at: comment.address - start, placement: comment.placement, text: comment.text })),
|
|
218
|
+
references,
|
|
219
|
+
operands,
|
|
220
|
+
};
|
|
221
|
+
return { routine, leftOut };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** The text of a routine file: keys in a fixed order, one list item on each line. */
|
|
225
|
+
export function routineText(routine: Routine): string {
|
|
226
|
+
const lines: string[] = [];
|
|
227
|
+
const field = (key: string, value: string) => lines.push(` ${JSON.stringify(key)}: ${value}`);
|
|
228
|
+
const list = (items: readonly unknown[]) => (items.length === 0 ? "[]" : `[\n${items.map((item) => ` ${JSON.stringify(item)}`).join(",\n")}\n ]`);
|
|
229
|
+
field("name", JSON.stringify(routine.name));
|
|
230
|
+
if (routine.title !== undefined) field("title", JSON.stringify(routine.title));
|
|
231
|
+
field("from", JSON.stringify({ program: routine.from.program, image: routine.from.image, start: formatC64Address(routine.from.start) }));
|
|
232
|
+
field("length", String(routine.bytes.length));
|
|
233
|
+
const hex = Buffer.from(routine.bytes).toString("hex");
|
|
234
|
+
field("bytes", list(Array.from({ length: Math.ceil(hex.length / (2 * BYTES_PER_LINE)) }, (_, index) => hex.slice(index * 2 * BYTES_PER_LINE, (index + 1) * 2 * BYTES_PER_LINE))));
|
|
235
|
+
field("instructions", JSON.stringify(routine.instructions));
|
|
236
|
+
field("relocations", list(routine.relocations.map((item) => ({ at: item.at, size: item.size, value: item.value }))));
|
|
237
|
+
field("variable", JSON.stringify(routine.variable));
|
|
238
|
+
field("symbols", list(routine.symbols.map((item) => ({ at: item.at, name: item.name, kind: item.kind }))));
|
|
239
|
+
field("regions", list(routine.regions.map((item) => ({ start: item.start, end: item.end, type: item.type, ...(item.element === undefined ? {} : { element: item.element }) }))));
|
|
240
|
+
field("comments", list(routine.comments.map((item) => ({ at: item.at, placement: item.placement, text: item.text }))));
|
|
241
|
+
field("references", list(routine.references.map((item) => ({ from: item.from, to: item.to, kind: item.kind }))));
|
|
242
|
+
field("operands", list(routine.operands.map((item) => ({ at: item.at, select: item.select, ...(item.symbol === undefined ? {} : { symbol: item.symbol }), ...(item.offset === undefined ? {} : { offset: item.offset }), ...(item.expression === undefined ? {} : { expression: item.expression }) }))));
|
|
243
|
+
return `{\n${lines.join(",\n")}\n}\n`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Reads the text of a routine file. Throws LibraryError for a file that is no routine. */
|
|
247
|
+
export function parseRoutine(text: string, file: string): Routine {
|
|
248
|
+
const fail = (what: string): never => {
|
|
249
|
+
throw new LibraryError("invalid-input", `${file} is not a routine file of the library: ${what}.`);
|
|
250
|
+
};
|
|
251
|
+
let value: Record<string, unknown>;
|
|
252
|
+
try {
|
|
253
|
+
value = JSON.parse(text) as Record<string, unknown>;
|
|
254
|
+
} catch (error) {
|
|
255
|
+
return fail((error as Error).message);
|
|
256
|
+
}
|
|
257
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) fail("it is not a JSON object");
|
|
258
|
+
if (typeof value.name !== "string" || !ROUTINE_NAME.test(value.name)) fail("name is not a routine name");
|
|
259
|
+
if (`${value.name as string}.json` !== file.split(/[\\/]/).at(-1)) fail(`its name ${value.name as string} is not its file name`);
|
|
260
|
+
if (value.title !== undefined && typeof value.title !== "string") fail("title is not text");
|
|
261
|
+
const from = value.from as Record<string, unknown> | undefined;
|
|
262
|
+
if (typeof from?.program !== "string" || typeof from.image !== "string" || typeof from.start !== "string" || !/^\$[0-9a-f]{4}$/i.test(from.start)) fail("from needs program, image and start");
|
|
263
|
+
if (!Array.isArray(value.bytes) || !value.bytes.every((line) => typeof line === "string" && /^([0-9a-f]{2})+$/i.test(line))) fail("bytes is not a list of hex lines");
|
|
264
|
+
const bytes = Uint8Array.from(Buffer.from((value.bytes as string[]).join(""), "hex"));
|
|
265
|
+
if (value.length !== bytes.length) fail(`length is not the ${bytes.length} bytes of bytes`);
|
|
266
|
+
if (bytes.length < MIN_ROUTINE) fail(`it has fewer than ${MIN_ROUTINE} bytes`);
|
|
267
|
+
const offset = (item: unknown, what: string, low = 0) => (Number.isInteger(item) && (item as number) >= low && (item as number) < bytes.length ? (item as number) : fail(`${what} is not an offset in the routine`));
|
|
268
|
+
const items = (key: string): Array<Record<string, unknown>> => (Array.isArray(value[key]) && (value[key] as unknown[]).every((item) => typeof item === "object" && item !== null) ? (value[key] as Array<Record<string, unknown>>) : fail(`${key} is not a list of objects`));
|
|
269
|
+
const oneOf = <T extends string>(allowed: readonly T[], item: unknown, what: string): T => (allowed.includes(item as T) ? (item as T) : fail(`${what} is not one of ${allowed.join(", ")}`));
|
|
270
|
+
const numbers = (key: string) => (Array.isArray(value[key]) ? (value[key] as unknown[]).map((item) => offset(item, key)) : fail(`${key} is not a list of offsets`));
|
|
271
|
+
return {
|
|
272
|
+
name: value.name as string,
|
|
273
|
+
...(value.title === undefined ? {} : { title: value.title as string }),
|
|
274
|
+
from: { program: from!.program as string, image: from!.image as string, start: Number.parseInt((from!.start as string).slice(1), 16) },
|
|
275
|
+
bytes,
|
|
276
|
+
instructions: numbers("instructions"),
|
|
277
|
+
relocations: items("relocations").map((item) => ({
|
|
278
|
+
at: offset(item.at, "a relocation at"),
|
|
279
|
+
size: oneOf(["word", "lo", "hi"] as const, item.size, "a relocation size"),
|
|
280
|
+
value: offset(item.value, "a relocation value", -1),
|
|
281
|
+
})),
|
|
282
|
+
variable: numbers("variable"),
|
|
283
|
+
symbols: items("symbols").map((item) => ({ at: offset(item.at, "a symbol at"), name: typeof item.name === "string" ? item.name : fail("a symbol has no name"), kind: oneOf(SYMBOL_KINDS, item.kind, "a symbol kind") })),
|
|
284
|
+
regions: items("regions").map((item) => ({
|
|
285
|
+
start: offset(item.start, "a region start"),
|
|
286
|
+
end: offset(item.end, "a region end"),
|
|
287
|
+
type: oneOf(REGION_TYPES, item.type, "a region type"),
|
|
288
|
+
...(item.element === undefined ? {} : { element: Number.isInteger(item.element) ? (item.element as number) : fail("a region element is not a whole number") }),
|
|
289
|
+
})),
|
|
290
|
+
comments: items("comments").map((item) => ({ at: offset(item.at, "a comment at"), placement: oneOf(COMMENT_PLACEMENTS, item.placement, "a comment placement"), text: typeof item.text === "string" ? item.text : fail("a comment has no text") })),
|
|
291
|
+
references: items("references").map((item) => ({ from: offset(item.from, "a reference from"), to: offset(item.to, "a reference to"), kind: oneOf(REFERENCE_KINDS, item.kind, "a reference kind") })),
|
|
292
|
+
operands: items("operands").map((item) => ({
|
|
293
|
+
at: offset(item.at, "an operand at"),
|
|
294
|
+
select: oneOf(OPERAND_SELECTS, item.select, "an operand select"),
|
|
295
|
+
...(item.symbol === undefined ? {} : { symbol: String(item.symbol) }),
|
|
296
|
+
...(item.offset === undefined ? {} : { offset: Number.isInteger(item.offset) ? (item.offset as number) : fail("an operand offset is not a whole number") }),
|
|
297
|
+
...(item.expression === undefined ? {} : { expression: String(item.expression) }),
|
|
298
|
+
})),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* The library folder that --library names, relative to the working directory
|
|
304
|
+
* like --project (D71). A library is a plain folder, never a project. With
|
|
305
|
+
* `create`, a missing folder is made.
|
|
306
|
+
*/
|
|
307
|
+
export function libraryFolder(folder: string, options: { create: boolean; cwd?: string }): string {
|
|
308
|
+
if (folder.length === 0) throw new LibraryError("invalid-input", "The library folder must not be empty.");
|
|
309
|
+
const path = resolve(options.cwd ?? process.cwd(), folder);
|
|
310
|
+
const stat = statSync(path, { throwIfNoEntry: false });
|
|
311
|
+
if (stat === undefined) {
|
|
312
|
+
if (!options.create) throw new LibraryError("not-found", `There is no library folder ${folder}. Save a routine to it first with routine.`);
|
|
313
|
+
mkdirSync(path, { recursive: true });
|
|
314
|
+
} else if (!stat.isDirectory()) throw new LibraryError("invalid-input", `${folder} is not a folder.`);
|
|
315
|
+
if (statSync(join(path, PROJECT_DIRECTORY), { throwIfNoEntry: false }) !== undefined) {
|
|
316
|
+
throw new LibraryError("invalid-input", `${folder} is a project folder. A library is a plain folder beside the projects, for example library/ at the workspace root.`);
|
|
317
|
+
}
|
|
318
|
+
return path;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Every routine of a library folder, by name. */
|
|
322
|
+
export function readLibrary(folder: string): Routine[] {
|
|
323
|
+
return readdirSync(folder, { withFileTypes: true })
|
|
324
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
325
|
+
.map((entry) => entry.name)
|
|
326
|
+
.sort()
|
|
327
|
+
.map((name) => parseRoutine(readFileSync(join(folder, name), "utf8"), name));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Writes a routine file into the library: a temporary file, then a rename. Refuses a name that exists unless `replace`. */
|
|
331
|
+
export function saveRoutine(folder: string, routine: Routine, replace: boolean): string {
|
|
332
|
+
const file = join(folder, `${routine.name}.json`);
|
|
333
|
+
if (!replace && statSync(file, { throwIfNoEntry: false }) !== undefined) {
|
|
334
|
+
throw new LibraryError("conflict", `The library already has the routine ${routine.name}. Give another --name, or give --replace to save over it.`);
|
|
335
|
+
}
|
|
336
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
337
|
+
writeFileSync(temporary, routineText(routine));
|
|
338
|
+
renameSync(temporary, file);
|
|
339
|
+
return file;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** An image to search: its segments and its bytes inside them. */
|
|
343
|
+
export interface Searched {
|
|
344
|
+
segments: ReadonlyArray<{ start: number; end: number }>;
|
|
345
|
+
byteAt: (address: number) => number;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** An operand that addresses something outside the routine and has other bytes in the program. */
|
|
349
|
+
export interface DifferingOperand {
|
|
350
|
+
/** The address of the instruction in the program. */
|
|
351
|
+
address: number;
|
|
352
|
+
library: number;
|
|
353
|
+
program: number;
|
|
354
|
+
size: 1 | 2;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export interface Match {
|
|
358
|
+
routine: string;
|
|
359
|
+
start: number;
|
|
360
|
+
/** The program address minus the library address. */
|
|
361
|
+
shift: number;
|
|
362
|
+
differing: DifferingOperand[];
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** The byte that a relocation gives at `start`, or undefined when the address leaves $0000-$ffff. */
|
|
366
|
+
function relocated(relocation: Relocation, start: number, part: 0 | 1): number | undefined {
|
|
367
|
+
const value = start + relocation.value;
|
|
368
|
+
if (value < -1 || value > 0xffff) return undefined;
|
|
369
|
+
const word = value & 0xffff;
|
|
370
|
+
if (relocation.size === "lo") return word & 0xff;
|
|
371
|
+
if (relocation.size === "hi") return word >> 8;
|
|
372
|
+
return part === 0 ? word & 0xff : word >> 8;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* The places where the routine occurs in the image: its fixed bytes equal,
|
|
377
|
+
* each relocation pointing into the routine at one shift, and the variable
|
|
378
|
+
* bytes free. Matches do not overlap.
|
|
379
|
+
*/
|
|
380
|
+
export function findRoutine(routine: Routine, image: Searched, origin = routine.from.start): Match[] {
|
|
381
|
+
const length = routine.bytes.length;
|
|
382
|
+
if (length < MIN_ROUTINE) return [];
|
|
383
|
+
// 0: fixed, 1: variable, 2: the first byte of a relocation, 3: the second byte of a word.
|
|
384
|
+
const kind = new Uint8Array(length);
|
|
385
|
+
for (const at of routine.variable) kind[at] = 1;
|
|
386
|
+
const relocations = new Map<number, Relocation>();
|
|
387
|
+
for (const relocation of routine.relocations) {
|
|
388
|
+
kind[relocation.at] = 2;
|
|
389
|
+
relocations.set(relocation.at, relocation);
|
|
390
|
+
if (relocation.size === "word") kind[relocation.at + 1] = 3;
|
|
391
|
+
}
|
|
392
|
+
const matches: Match[] = [];
|
|
393
|
+
for (const segment of image.segments) {
|
|
394
|
+
for (let start = segment.start; start + length - 1 <= segment.end; start++) {
|
|
395
|
+
let equal = true;
|
|
396
|
+
for (let offset = 0; offset < length && equal; offset++) {
|
|
397
|
+
const byte = image.byteAt(start + offset);
|
|
398
|
+
if (kind[offset] === 0) equal = byte === routine.bytes[offset];
|
|
399
|
+
else if (kind[offset] === 2) {
|
|
400
|
+
const relocation = relocations.get(offset)!;
|
|
401
|
+
equal = byte === relocated(relocation, start, 0) && (relocation.size !== "word" || image.byteAt(start + offset + 1) === relocated(relocation, start, 1));
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (!equal) continue;
|
|
405
|
+
const differing: DifferingOperand[] = [];
|
|
406
|
+
for (const offset of routine.instructions) {
|
|
407
|
+
const size = instructionLength(routine.bytes[offset]!) - 1;
|
|
408
|
+
if (size === 0 || kind[offset + 1] !== 1) continue;
|
|
409
|
+
const read = (at: (offset: number) => number) => (size === 2 ? at(offset + 1) | (at(offset + 2) << 8) : at(offset + 1));
|
|
410
|
+
const library = read((at) => routine.bytes[at]!);
|
|
411
|
+
const program = read((at) => image.byteAt(start + at));
|
|
412
|
+
if (library !== program) differing.push({ address: start + offset, library, program, size: size as 1 | 2 });
|
|
413
|
+
}
|
|
414
|
+
matches.push({ routine: routine.name, start, shift: start - origin, differing });
|
|
415
|
+
start += length - 1;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return matches;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** What the program knows, read at the program addresses of a match. */
|
|
422
|
+
export interface ProgramKnowledge {
|
|
423
|
+
symbolAt: (address: number) => SymbolRow | undefined;
|
|
424
|
+
symbolNamed: (name: string) => SymbolRow | undefined;
|
|
425
|
+
regionsOverlapping: (start: number, end: number) => RegionRow[];
|
|
426
|
+
commentsAt: (address: number) => CommentRow[];
|
|
427
|
+
referencesFrom: (address: number) => ReferenceRow[];
|
|
428
|
+
operandAt: (address: number) => OperandRow | undefined;
|
|
429
|
+
/** True when a user or LLM removal holds at the place. */
|
|
430
|
+
symbolSuppressed: (address: number) => boolean;
|
|
431
|
+
regionSuppressed: (start: number, end: number) => boolean;
|
|
432
|
+
referenceSuppressed: (reference: { from: number; to: number; kind: ReferenceKind }) => boolean;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** One write of an apply batch, in the form of knowledge.ts apply. */
|
|
436
|
+
export type BatchWrite = Record<string, string | number> & { op: string };
|
|
437
|
+
|
|
438
|
+
export interface Skipped {
|
|
439
|
+
routine: string;
|
|
440
|
+
address: string;
|
|
441
|
+
op?: string;
|
|
442
|
+
reason: string;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export interface Suggestions {
|
|
446
|
+
batch: BatchWrite[];
|
|
447
|
+
/** Writes that the program already has. */
|
|
448
|
+
known: number;
|
|
449
|
+
leftOut: Skipped[];
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const semantic = (row: { origin: string }) => row.origin === "user" || row.origin === "llm";
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* The names, regions, comments, references and operand expressions of each
|
|
456
|
+
* matched routine at its program addresses, as an apply batch. A write that
|
|
457
|
+
* the program already has is counted as known. A write that would replace a
|
|
458
|
+
* user or LLM row, take a name that the program uses elsewhere, undo a
|
|
459
|
+
* removal, or give an operand that is not in the image, is left out with the
|
|
460
|
+
* reason. Only the first match of a routine gets its names.
|
|
461
|
+
*/
|
|
462
|
+
export function suggest(found: ReadonlyArray<{ routine: Routine; matches: Match[] }>, program: ProgramKnowledge, image: Searched, platform: ReadonlyMap<string, number>): Suggestions {
|
|
463
|
+
const batch: BatchWrite[] = [];
|
|
464
|
+
const leftOut: Skipped[] = [];
|
|
465
|
+
let known = 0;
|
|
466
|
+
const show = formatC64Address;
|
|
467
|
+
/** The names that are at their place after the batch: written here or known. */
|
|
468
|
+
const placed = new Map<string, number>();
|
|
469
|
+
const symbolsHere = new Map<number, string>();
|
|
470
|
+
const regionsHere: Array<{ start: number; end: number }> = [];
|
|
471
|
+
|
|
472
|
+
for (const { routine, matches } of found) {
|
|
473
|
+
const [first, ...others] = [...matches].sort((a, b) => a.start - b.start);
|
|
474
|
+
for (const other of others) leftOut.push({ routine: routine.name, address: show(other.start), reason: `a second copy of the routine; the first is at ${show(first!.start)}` });
|
|
475
|
+
if (first === undefined) continue;
|
|
476
|
+
const at = (offset: number) => first.start + offset;
|
|
477
|
+
const skip = (address: number, op: string, reason: string) => leftOut.push({ routine: routine.name, address: show(address), op, reason });
|
|
478
|
+
|
|
479
|
+
for (const symbol of routine.symbols) {
|
|
480
|
+
const address = at(symbol.at);
|
|
481
|
+
const existing = program.symbolAt(address);
|
|
482
|
+
const elsewhere = program.symbolNamed(symbol.name);
|
|
483
|
+
if (existing?.name === symbol.name) {
|
|
484
|
+
known++;
|
|
485
|
+
placed.set(symbol.name, address);
|
|
486
|
+
} else if (existing !== undefined && semantic(existing)) skip(address, "rename", `the program names it ${existing.name} (${existing.origin})`);
|
|
487
|
+
else if (elsewhere !== undefined && elsewhere.address !== address) skip(address, "rename", `the program uses the name ${symbol.name} at ${show(elsewhere.address)}`);
|
|
488
|
+
else if (placed.has(symbol.name) && placed.get(symbol.name) !== address) skip(address, "rename", `the batch gives the name ${symbol.name} to ${show(placed.get(symbol.name)!)}`);
|
|
489
|
+
else if (symbolsHere.has(address)) skip(address, "rename", `the batch names it ${symbolsHere.get(address)}`);
|
|
490
|
+
else if (program.symbolSuppressed(address)) skip(address, "rename", "a removal of a name holds there");
|
|
491
|
+
else {
|
|
492
|
+
batch.push({ op: "rename", address: show(address), name: symbol.name, kind: symbol.kind });
|
|
493
|
+
placed.set(symbol.name, address);
|
|
494
|
+
symbolsHere.set(address, symbol.name);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
for (const region of routine.regions) {
|
|
499
|
+
const start = at(region.start);
|
|
500
|
+
const end = at(region.end);
|
|
501
|
+
const overlapping = program.regionsOverlapping(start, end);
|
|
502
|
+
if (overlapping.some((item) => item.start === start && item.end === end && item.type === region.type && item.element === region.element)) known++;
|
|
503
|
+
else if (overlapping.some(semantic)) {
|
|
504
|
+
const other = overlapping.find(semantic)!;
|
|
505
|
+
skip(start, "classify", `the program has the ${other.type} region ${show(other.start)}-${show(other.end)} (${other.origin})`);
|
|
506
|
+
} else if (regionsHere.some((item) => item.start <= end && item.end >= start)) skip(start, "classify", "the batch classifies an overlapping range");
|
|
507
|
+
else if (program.regionSuppressed(start, end)) skip(start, "classify", "a removal of a region holds there");
|
|
508
|
+
else {
|
|
509
|
+
batch.push({ op: "classify", start: show(start), end: show(end), type: region.type, ...(region.element === undefined ? {} : { element: region.element }) });
|
|
510
|
+
regionsHere.push({ start, end });
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
for (const comment of routine.comments) {
|
|
515
|
+
const address = at(comment.at);
|
|
516
|
+
const existing = program.commentsAt(address).find((item) => item.placement === comment.placement);
|
|
517
|
+
if (existing?.text === comment.text) known++;
|
|
518
|
+
else if (existing !== undefined) skip(address, "comment", `the program has a ${comment.placement} comment there (${existing.origin})`);
|
|
519
|
+
else batch.push({ op: "comment", address: show(address), placement: comment.placement, text: comment.text });
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
for (const reference of routine.references) {
|
|
523
|
+
const wanted = { from: at(reference.from), to: at(reference.to), kind: reference.kind };
|
|
524
|
+
if (program.referencesFrom(wanted.from).some((item) => item.to === wanted.to && item.kind === wanted.kind)) known++;
|
|
525
|
+
else if (program.referenceSuppressed(wanted)) skip(wanted.from, "reference", "a removal of this reference holds");
|
|
526
|
+
else batch.push({ op: "reference", from: show(wanted.from), to: show(wanted.to), kind: wanted.kind });
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const own = new Set(routine.symbols.map((symbol) => symbol.name));
|
|
530
|
+
for (const operand of routine.operands) {
|
|
531
|
+
const address = at(operand.at);
|
|
532
|
+
const existing = program.operandAt(address);
|
|
533
|
+
const names = operand.select === "expr" ? identifiers(operand.expression!) : [operand.symbol!];
|
|
534
|
+
const text = operandText(operand);
|
|
535
|
+
if (existing !== undefined && operandText(existing) === text) {
|
|
536
|
+
known++;
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
if (existing !== undefined) {
|
|
540
|
+
skip(address, "operand", `the program has the operand expression ${operandText(existing)} there`);
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
const missing = names.filter((name) => own.has(name) && placed.get(name) === undefined);
|
|
544
|
+
if (missing.length > 0) {
|
|
545
|
+
skip(address, "operand", `its name ${missing.join(", ")} was left out`);
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
const problem = operandProblem(address, text, image, (name) => placed.get(name) ?? program.symbolNamed(name)?.address ?? platform.get(name));
|
|
549
|
+
if (problem !== undefined) {
|
|
550
|
+
skip(address, "operand", problem);
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
batch.push({
|
|
554
|
+
op: "operand",
|
|
555
|
+
address: show(address),
|
|
556
|
+
select: operand.select,
|
|
557
|
+
...(operand.symbol === undefined ? {} : { symbol: operand.symbol }),
|
|
558
|
+
...(operand.offset === undefined ? {} : { offset: operand.offset }),
|
|
559
|
+
...(operand.expression === undefined ? {} : { expression: operand.expression }),
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return { batch, known, leftOut };
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** Why the expression is not the operand of the instruction at `address` in the image, or undefined when it is. */
|
|
567
|
+
function operandProblem(address: number, text: string, image: Searched, resolveName: (name: string) => number | undefined): string | undefined {
|
|
568
|
+
const opcode = image.byteAt(address);
|
|
569
|
+
const size = instructionLength(opcode);
|
|
570
|
+
if (size === 1 || isBranch(opcode)) return "the instruction has no value operand";
|
|
571
|
+
const operand = size === 3 ? image.byteAt(address + 1) | (image.byteAt(address + 2) << 8) : image.byteAt(address + 1);
|
|
572
|
+
let value: number;
|
|
573
|
+
try {
|
|
574
|
+
value = evaluate(text, resolveName);
|
|
575
|
+
} catch (error) {
|
|
576
|
+
if (error instanceof ExpressionError) return error.message;
|
|
577
|
+
throw error;
|
|
578
|
+
}
|
|
579
|
+
const range = size === 2 ? 0x100 : 0x10000;
|
|
580
|
+
if (value < -range / 2 || value >= range || ((value % range) + range) % range !== operand) {
|
|
581
|
+
return `${text} gives ${value}, but the operand in the image is ${operand}`;
|
|
582
|
+
}
|
|
583
|
+
return undefined;
|
|
584
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Runtime evidence for the differences between releases
|
|
1
|
+
// Runtime evidence for the differences between releases: each release
|
|
2
2
|
// runs in its own emulator, and each differing range gets its first access
|
|
3
3
|
// (read, write, execute or none) and whether its bytes changed by the end.
|
|
4
4
|
// Watchpoints and range breakpoints stop the run at an access; the coverage
|
|
@@ -112,7 +112,7 @@ export async function watchRelease(
|
|
|
112
112
|
* After a stop: the ranges that the coverage shows accessed get their first
|
|
113
113
|
* access, and lose their points. VICE marks only the opcode byte of an
|
|
114
114
|
* executed instruction, so an instruction that starts up to two bytes before
|
|
115
|
-
* a range, or in it, executes its operand bytes too
|
|
115
|
+
* a range, or in it, executes its operand bytes too.
|
|
116
116
|
*/
|
|
117
117
|
const settle = async (): Promise<boolean> => {
|
|
118
118
|
const map = await readCoverage(client);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Evidence from one release
|
|
1
|
+
// Evidence from one release: for each range of the image, the unpack
|
|
2
2
|
// stage that wrote it and the access class from coverage; for each code
|
|
3
3
|
// region of knowledge, whether call and jump references reach it from the
|
|
4
4
|
// entry; and what the program reads or executes outside the image. Evidence
|
|
@@ -16,7 +16,7 @@ export interface SeparationInput {
|
|
|
16
16
|
entry?: number;
|
|
17
17
|
/** The stages of an unpacker capture, in order; the last one is the program. */
|
|
18
18
|
stages?: Array<{ entry: number; wrote: Range[] }>;
|
|
19
|
-
/** Joined coverage files
|
|
19
|
+
/** Joined coverage files. */
|
|
20
20
|
coverage?: Uint8Array;
|
|
21
21
|
codeRegions: Array<Range & { type: string }>;
|
|
22
22
|
references: Array<{ from: number; to: number; kind: string }>;
|
|
@@ -131,7 +131,7 @@ export function separate(input: SeparationInput): Separation {
|
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
// The image has no bytes of generated code, so only its opcode bytes count as executed. VICE marks only
|
|
134
|
-
// opcode bytes: a gap of at most two operand bytes outside the image stays inside one piece of code
|
|
134
|
+
// opcode bytes: a gap of at most two operand bytes outside the image stays inside one piece of code.
|
|
135
135
|
const outsideImage = (from: number, to: number) => {
|
|
136
136
|
for (let address = from + 1; address < to; address++) if (inImage(address)) return false;
|
|
137
137
|
return true;
|