@henols/c64-re-tools 2.1.0-rc.1 → 2.1.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/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +24 -9
- package/skills/c64-assembler/scripts/assembler.ts +95 -0
- package/skills/c64-basic/SKILL.md +17 -9
- package/skills/c64-basic/scripts/basic.ts +26 -36
- package/skills/c64-disk/SKILL.md +17 -7
- package/skills/c64-disk/scripts/disk.ts +130 -104
- package/skills/c64-disk/scripts/lib/containers.ts +10 -12
- package/skills/c64-docs/SKILL.md +47 -28
- package/skills/c64-docs/references/style.md +6 -6
- package/skills/c64-docs/scripts/docs.ts +323 -0
- package/skills/c64-docs/scripts/lib/source-map.ts +1 -1
- package/skills/c64-emulator/SKILL.md +11 -7
- package/skills/c64-knowledge/SKILL.md +34 -21
- package/skills/c64-knowledge/scripts/knowledge.ts +597 -718
- package/skills/c64-knowledge/scripts/lib/listing.ts +5 -7
- package/skills/c64-memory-map/SKILL.md +18 -10
- package/skills/c64-memory-map/scripts/memory-map.ts +81 -0
- package/skills/c64-provenance/SKILL.md +33 -24
- package/skills/c64-provenance/scripts/lib/compare.ts +8 -12
- package/skills/c64-provenance/scripts/lib/leftovers.ts +19 -20
- package/skills/c64-provenance/scripts/lib/library.ts +15 -25
- package/skills/c64-provenance/scripts/lib/runtime.ts +12 -37
- package/skills/c64-provenance/scripts/lib/separate.ts +9 -10
- package/skills/c64-provenance/scripts/provenance.ts +177 -279
- package/skills/c64-reverse-engineering/SKILL.md +13 -12
- package/skills/c64-source/SKILL.md +28 -16
- package/skills/c64-source/scripts/lib/loader.ts +52 -77
- package/skills/c64-source/scripts/lib/source.ts +33 -42
- package/skills/c64-source/scripts/{generate.ts → source.ts} +68 -109
- package/skills/c64-static-analysis/SKILL.md +24 -13
- package/skills/c64-static-analysis/scripts/lib/coverage.ts +3 -4
- package/skills/c64-static-analysis/scripts/lib/findings.ts +2 -2
- package/skills/c64-static-analysis/scripts/lib/instructions.ts +1 -1
- package/skills/c64-static-analysis/scripts/lib/macros.ts +6 -8
- package/skills/c64-static-analysis/scripts/{analyze.ts → static-analysis.ts} +58 -118
- package/skills/c64-testing/SKILL.md +48 -32
- package/skills/c64-testing/references/recording-format.md +48 -19
- package/skills/c64-testing/references/scenario-format.md +24 -19
- package/skills/c64-testing/scripts/{diff.ts → lib/diff-files.ts} +18 -69
- package/skills/c64-testing/scripts/lib/diff.ts +5 -3
- package/skills/c64-testing/scripts/lib/play.ts +244 -54
- package/skills/c64-testing/scripts/lib/recording.ts +153 -120
- package/skills/c64-testing/scripts/{replay.ts → lib/replay.ts} +28 -86
- package/skills/c64-testing/scripts/{run.ts → lib/runs.ts} +36 -59
- package/skills/c64-testing/scripts/lib/scenario.ts +73 -81
- package/skills/c64-testing/scripts/testing.ts +189 -0
- package/skills/c64-unpacker/SKILL.md +42 -25
- package/skills/c64-unpacker/scripts/lib/evidence.ts +3 -4
- package/skills/c64-unpacker/scripts/lib/loads.ts +12 -10
- package/skills/c64-unpacker/scripts/lib/machine.ts +0 -2
- package/skills/c64-unpacker/scripts/lib/stages.ts +14 -19
- package/skills/c64-unpacker/scripts/{unpack.ts → unpacker.ts} +109 -211
- package/src/c64.ts +35 -20
- package/src/cli/main.ts +2 -2
- package/src/errors.ts +38 -0
- package/src/graphics.ts +3 -1
- package/src/host/server.ts +3 -2
- package/src/host/vice/session.ts +4 -4
- package/src/host-client/entry-state.ts +106 -0
- package/src/host-client/image.ts +21 -10
- package/src/host-client/steps.ts +285 -108
- package/src/host-client/transfer.ts +21 -15
- package/src/host-client/vice-session.ts +1 -1
- package/src/knowledge/database.ts +3 -23
- package/src/knowledge/expression.ts +8 -11
- package/src/knowledge/history.ts +2 -2
- package/src/knowledge/import.ts +13 -14
- package/src/knowledge/names.ts +50 -0
- package/src/knowledge/program.ts +11 -10
- package/src/knowledge/read.ts +16 -21
- package/src/knowledge/schema.ts +9 -3
- package/src/knowledge/text.ts +5 -5
- package/src/knowledge/write.ts +93 -137
- package/src/mcp/inputs.ts +105 -0
- package/src/mcp/output.ts +2 -13
- package/src/mcp/server.ts +12 -60
- package/src/mcp/tools/debug.ts +17 -12
- package/src/mcp/tools/execution.ts +5 -32
- package/src/mcp/tools/input.ts +7 -32
- package/src/mcp/tools/media.ts +6 -5
- package/src/mcp/tools/memory.ts +11 -38
- package/src/mcp/tools/video.ts +6 -5
- package/src/native/c64run.ts +1 -1
- package/src/native/dxa.ts +7 -6
- package/src/native/ghidra/analyze.ts +2 -1
- package/src/native/ghidra/index.ts +2 -1
- package/src/native/processes.ts +4 -3
- package/src/native/status.ts +2 -2
- package/src/native/types.ts +3 -7
- package/src/opcodes.ts +13 -13
- package/src/platform.ts +9 -9
- package/src/project.ts +16 -26
- package/src/protocol/messages.ts +3 -21
- package/src/protocol/vice.ts +7 -7
- package/src/script.ts +491 -49
- package/skills/c64-assembler/scripts/assemble.ts +0 -138
- package/skills/c64-docs/scripts/generate.ts +0 -109
- package/skills/c64-docs/scripts/link.ts +0 -239
- package/skills/c64-memory-map/scripts/memmap.ts +0 -92
- package/skills/c64-testing/scripts/checklist.ts +0 -51
- package/skills/c64-testing/scripts/record.ts +0 -96
- package/skills/c64-testing/scripts/test.ts +0 -65
|
@@ -6,10 +6,7 @@
|
|
|
6
6
|
// << >> >>>, unary < and >, &, XOR, |. A unary < or > takes everything of a
|
|
7
7
|
// higher priority after it, so <a+1 is the low byte of a+1.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
export class ExpressionError extends Error {
|
|
11
|
-
override name = "ExpressionError";
|
|
12
|
-
}
|
|
9
|
+
import { Refusal } from "../errors.ts";
|
|
13
10
|
|
|
14
11
|
/** The selectors of an operand expression: a byte of a symbol, its value, or free expression text. */
|
|
15
12
|
export const OPERAND_SELECTS = ["lo", "hi", "value", "expr"] as const;
|
|
@@ -54,7 +51,7 @@ function tokenize(text: string): Token[] {
|
|
|
54
51
|
}
|
|
55
52
|
const number: RegExpExecArray | null = operandNext ? /^(\$[0-9A-Fa-f]+|%[01]+|[0-9]+)/.exec(rest) : null;
|
|
56
53
|
const match: RegExpExecArray | null = number ?? /^(>>>|<<|>>|XOR\b)/.exec(rest) ?? NAME.exec(rest) ?? /^[-+*/%&|!<>()]/.exec(rest);
|
|
57
|
-
if (!match) throw new
|
|
54
|
+
if (!match) throw new Refusal("invalid-input", `"${text}" has an unknown character at position ${at + 1}: "${rest[0]}".`);
|
|
58
55
|
const token: string = match[0];
|
|
59
56
|
tokens.push({ text: token, at });
|
|
60
57
|
at += token.length;
|
|
@@ -65,13 +62,13 @@ function tokenize(text: string): Token[] {
|
|
|
65
62
|
}
|
|
66
63
|
|
|
67
64
|
function parse(text: string): Node {
|
|
68
|
-
if (text.trim().length === 0) throw new
|
|
69
|
-
if (text.length > MAX_TEXT) throw new
|
|
65
|
+
if (text.trim().length === 0) throw new Refusal("invalid-input", "An expression is empty.");
|
|
66
|
+
if (text.length > MAX_TEXT) throw new Refusal("invalid-input", `An expression has at most ${MAX_TEXT} characters.`);
|
|
70
67
|
const tokens = tokenize(text);
|
|
71
68
|
let index = 0;
|
|
72
69
|
const peek = () => tokens[index]?.text;
|
|
73
70
|
const fail = (what: string): never => {
|
|
74
|
-
throw new
|
|
71
|
+
throw new Refusal("invalid-input", `"${text}" ${what}.`);
|
|
75
72
|
};
|
|
76
73
|
|
|
77
74
|
const operand = (): Node => {
|
|
@@ -122,7 +119,7 @@ function value(node: Node, resolve: (name: string) => number | undefined): numbe
|
|
|
122
119
|
return node.value;
|
|
123
120
|
case "name": {
|
|
124
121
|
const found = resolve(node.name);
|
|
125
|
-
if (found === undefined) throw new
|
|
122
|
+
if (found === undefined) throw new Refusal("invalid-input", `"${node.name}" is no symbol of the knowledge and no platform name.`);
|
|
126
123
|
return found;
|
|
127
124
|
}
|
|
128
125
|
case "unary": {
|
|
@@ -139,7 +136,7 @@ function value(node: Node, resolve: (name: string) => number | undefined): numbe
|
|
|
139
136
|
return left * right;
|
|
140
137
|
case "/":
|
|
141
138
|
case "%":
|
|
142
|
-
if (right === 0) throw new
|
|
139
|
+
if (right === 0) throw new Refusal("invalid-input", "An expression divides by zero.");
|
|
143
140
|
return node.operator === "/" ? Math.trunc(left / right) : left % right;
|
|
144
141
|
case "+":
|
|
145
142
|
return left + right;
|
|
@@ -162,7 +159,7 @@ function value(node: Node, resolve: (name: string) => number | undefined): numbe
|
|
|
162
159
|
}
|
|
163
160
|
}
|
|
164
161
|
|
|
165
|
-
/** Throws
|
|
162
|
+
/** Throws an invalid-input Refusal when the text is no expression of this syntax. */
|
|
166
163
|
export function checkExpression(text: string): void {
|
|
167
164
|
parse(text);
|
|
168
165
|
}
|
package/src/knowledge/history.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import type { DatabaseSync } from "node:sqlite";
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { Refusal } from "../errors.ts";
|
|
7
7
|
import {
|
|
8
8
|
COMMENT_COLUMNS,
|
|
9
9
|
MACRO_COLUMNS,
|
|
@@ -182,7 +182,7 @@ export interface RevisionChanges {
|
|
|
182
182
|
export function revision(db: DatabaseSync | undefined, text: string): RevisionChanges {
|
|
183
183
|
const id = resolveRevision(db, text);
|
|
184
184
|
const raw = db?.prepare("SELECT * FROM revisions WHERE id = ?").get(id) as Raw | undefined;
|
|
185
|
-
if (db === undefined || raw === undefined) throw new
|
|
185
|
+
if (db === undefined || raw === undefined) throw new Refusal("not-found", `There is no revision ${id}.`);
|
|
186
186
|
const tables = [
|
|
187
187
|
["symbol", "symbols", SYMBOL_COLUMNS, toSymbol],
|
|
188
188
|
["analyzer-name", "analyzer_names", SYMBOL_COLUMNS, toSymbol],
|
package/src/knowledge/import.ts
CHANGED
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
import { createHash } from "node:crypto";
|
|
14
14
|
import type { DatabaseSync } from "node:sqlite";
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import { type AddressRange, formatC64Address } from "../c64.ts";
|
|
17
|
+
import { Refusal } from "../errors.ts";
|
|
18
|
+
import { transaction } from "./database.ts";
|
|
17
19
|
import {
|
|
18
20
|
analyzerNamed,
|
|
19
21
|
REFERENCE_COLUMNS,
|
|
@@ -28,13 +30,10 @@ import {
|
|
|
28
30
|
type RegionRow,
|
|
29
31
|
type SymbolRow,
|
|
30
32
|
} from "./read.ts";
|
|
31
|
-
import { CODE_REGION_TYPES, DEFAULT_SPACE, POINTER_KIND, REFERENCE_KINDS, REGION_TYPES, SYMBOL_KINDS, type Origin, type ReferenceKind, type RegionType, type Space, type SymbolKind } from "./schema.ts";
|
|
32
|
-
import { Change, checkAddress, checkSpace,
|
|
33
|
+
import { ANALYZERS, CODE_REGION_TYPES, DEFAULT_SPACE, POINTER_KIND, REFERENCE_KINDS, REGION_TYPES, SYMBOL_KINDS, type Analyzer, type Origin, type ReferenceKind, type RegionType, type Space, type SymbolKind } from "./schema.ts";
|
|
34
|
+
import { Change, checkAddress, checkSpace, subtract, SYMBOL_NAME, type Evidence } from "./write.ts";
|
|
33
35
|
|
|
34
|
-
export type { AddressRange };
|
|
35
36
|
|
|
36
|
-
export const ANALYZERS = ["dxa", "ghidra", "heuristic"] as const satisfies readonly Origin[];
|
|
37
|
-
export type Analyzer = (typeof ANALYZERS)[number];
|
|
38
37
|
|
|
39
38
|
export interface NormalizedFindings {
|
|
40
39
|
analyzer: Analyzer;
|
|
@@ -106,7 +105,7 @@ const isCode = (value: SymbolKind | RegionType) => value === "label" || value ==
|
|
|
106
105
|
const counts = (): ImportCounts => ({ added: 0, changed: 0, retired: 0, unchanged: 0, suppressed: 0 });
|
|
107
106
|
|
|
108
107
|
function invalid(message: string): never {
|
|
109
|
-
throw new
|
|
108
|
+
throw new Refusal("invalid-input", `The analyzer result is not usable: ${message} Nothing was imported.`);
|
|
110
109
|
}
|
|
111
110
|
|
|
112
111
|
/** Sorted, merged coverage ranges. */
|
|
@@ -133,7 +132,7 @@ export function validateFindings(findings: NormalizedFindings): AddressRange[] {
|
|
|
133
132
|
for (const range of findings.coverage) {
|
|
134
133
|
checkAddress(range.start, "coverage start");
|
|
135
134
|
checkAddress(range.end, "coverage end");
|
|
136
|
-
if (range.end < range.start) invalid(`coverage ${
|
|
135
|
+
if (range.end < range.start) invalid(`coverage ${formatC64Address(range.start)}-${formatC64Address(range.end)} ends before it starts.`);
|
|
137
136
|
}
|
|
138
137
|
const coverage = mergeCoverage(findings.coverage);
|
|
139
138
|
const covering = (start: number, end: number) => coverage.some((range) => range.start <= start && end <= range.end);
|
|
@@ -142,10 +141,10 @@ export function validateFindings(findings: NormalizedFindings): AddressRange[] {
|
|
|
142
141
|
const names = new Set<string>();
|
|
143
142
|
for (const symbol of findings.symbols) {
|
|
144
143
|
checkAddress(symbol.address);
|
|
145
|
-
if (!covering(symbol.address, symbol.address)) invalid(`the symbol at ${
|
|
144
|
+
if (!covering(symbol.address, symbol.address)) invalid(`the symbol at ${formatC64Address(symbol.address)} is outside the coverage.`);
|
|
146
145
|
if (!SYMBOL_NAME.test(symbol.name)) invalid(`the symbol name ${JSON.stringify(symbol.name)} is not valid.`);
|
|
147
146
|
if (!(SYMBOL_KINDS as readonly string[]).includes(symbol.kind)) invalid(`the symbol kind ${symbol.kind} is not known.`);
|
|
148
|
-
if (addresses.has(symbol.address)) invalid(`two symbols are at ${
|
|
147
|
+
if (addresses.has(symbol.address)) invalid(`two symbols are at ${formatC64Address(symbol.address)}.`);
|
|
149
148
|
if (names.has(symbol.name)) invalid(`the name ${symbol.name} is at two addresses.`);
|
|
150
149
|
addresses.add(symbol.address);
|
|
151
150
|
names.add(symbol.name);
|
|
@@ -154,16 +153,16 @@ export function validateFindings(findings: NormalizedFindings): AddressRange[] {
|
|
|
154
153
|
regions.forEach((region, index) => {
|
|
155
154
|
checkAddress(region.start, "region start");
|
|
156
155
|
checkAddress(region.end, "region end");
|
|
157
|
-
if (region.end < region.start) invalid(`the region ${
|
|
158
|
-
if (!covering(region.start, region.end)) invalid(`the region ${
|
|
156
|
+
if (region.end < region.start) invalid(`the region ${formatC64Address(region.start)}-${formatC64Address(region.end)} ends before it starts.`);
|
|
157
|
+
if (!covering(region.start, region.end)) invalid(`the region ${formatC64Address(region.start)}-${formatC64Address(region.end)} is not inside the coverage.`);
|
|
159
158
|
if (!(REGION_TYPES as readonly string[]).includes(region.type)) invalid(`the region type ${region.type} is not known.`);
|
|
160
159
|
const previous = regions[index - 1];
|
|
161
|
-
if (previous !== undefined && region.start <= previous.end) invalid(`the regions at ${
|
|
160
|
+
if (previous !== undefined && region.start <= previous.end) invalid(`the regions at ${formatC64Address(previous.start)} and ${formatC64Address(region.start)} overlap.`);
|
|
162
161
|
});
|
|
163
162
|
for (const reference of findings.references) {
|
|
164
163
|
checkAddress(reference.from, "reference source");
|
|
165
164
|
checkAddress(reference.to, "reference target");
|
|
166
|
-
if (!covering(reference.from, reference.from)) invalid(`the reference from ${
|
|
165
|
+
if (!covering(reference.from, reference.from)) invalid(`the reference from ${formatC64Address(reference.from)} is outside the coverage.`);
|
|
167
166
|
if (!(REFERENCE_KINDS as readonly string[]).includes(reference.kind)) invalid(`the reference kind ${reference.kind} is not known.`);
|
|
168
167
|
if (reference.kind === POINTER_KIND) invalid(`only an operand expression writes a pointer reference, not an analyzer.`);
|
|
169
168
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Names as addresses: the one place where a name in a command line, a step
|
|
2
|
+
// or a scenario becomes an address. A symbol comes first. An analyzer name
|
|
3
|
+
// counts when no symbol has the name and the analyzers that give it give it
|
|
4
|
+
// to one address.
|
|
5
|
+
|
|
6
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
7
|
+
|
|
8
|
+
import { c64AddressOf, formatC64Address } from "../c64.ts";
|
|
9
|
+
import { Refusal } from "../errors.ts";
|
|
10
|
+
import { openForRead } from "./database.ts";
|
|
11
|
+
import { analyzerNamed, listAnalyzerNames, listSymbols, symbolNamed } from "./read.ts";
|
|
12
|
+
import { DEFAULT_SPACE, type Space, type SymbolKind } from "./schema.ts";
|
|
13
|
+
|
|
14
|
+
/** The address of a name in a space. Refuses a name that no symbol and no analyzer has, and an analyzer name at two addresses. */
|
|
15
|
+
export function addressOfName(db: DatabaseSync | undefined, name: string, space: Space = DEFAULT_SPACE): number {
|
|
16
|
+
const symbol = symbolNamed(db, name, space);
|
|
17
|
+
if (symbol !== undefined) return symbol.address;
|
|
18
|
+
const rows = analyzerNamed(db, name, space);
|
|
19
|
+
const addresses = [...new Set(rows.map((row) => row.address))];
|
|
20
|
+
if (addresses.length === 1) return addresses[0]!;
|
|
21
|
+
const where = space === DEFAULT_SPACE ? "" : ` in ${space}`;
|
|
22
|
+
if (addresses.length > 1) throw new Refusal("invalid-input", `the analyzers give ${name} to ${rows.map((row) => `${formatC64Address(row.address)} (${row.origin})`).join(" and ")}${where}. Give the address`);
|
|
23
|
+
throw new Refusal("not-found", `no symbol or analyzer name${where} is ${name}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Every name of a space with its address: the symbols, and each analyzer name that addressOfName accepts. With kinds, only names of those kinds. */
|
|
27
|
+
export function nameTable(db: DatabaseSync | undefined, space: Space = DEFAULT_SPACE, kinds?: readonly SymbolKind[]): Map<string, number> {
|
|
28
|
+
const fits = (row: { kind: SymbolKind }) => kinds === undefined || kinds.includes(row.kind);
|
|
29
|
+
const names = new Map<string, number>();
|
|
30
|
+
for (const symbol of listSymbols(db, undefined, undefined, undefined, space)) if (fits(symbol)) names.set(symbol.name, symbol.address);
|
|
31
|
+
const analyzer = new Map<string, Set<number>>();
|
|
32
|
+
for (const row of listAnalyzerNames(db, undefined, undefined, undefined, space)) if (fits(row)) analyzer.set(row.name, (analyzer.get(row.name) ?? new Set()).add(row.address));
|
|
33
|
+
for (const [name, addresses] of analyzer) if (!names.has(name) && addresses.size === 1) names.set(name, [...addresses][0]!);
|
|
34
|
+
return names;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** addressOfName on the knowledge of the project: the name resolver of the scripts. */
|
|
38
|
+
export function knowledgeNames(name: string, space: string): number {
|
|
39
|
+
const db = openForRead();
|
|
40
|
+
try {
|
|
41
|
+
return addressOfName(db, name, space);
|
|
42
|
+
} finally {
|
|
43
|
+
db?.close();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** An address of a step or a file: $ and 1 to 4 hex digits, else a name of the knowledge in the space. */
|
|
48
|
+
export function knowledgeAt(space: Space = DEFAULT_SPACE): (location: string) => number {
|
|
49
|
+
return (location) => c64AddressOf(location) ?? knowledgeNames(location, space);
|
|
50
|
+
}
|
package/src/knowledge/program.ts
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
7
7
|
import type { DatabaseSync } from "node:sqlite";
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { Refusal } from "../errors.ts";
|
|
10
|
+
import { type AddressRange, COVERAGE, formatC64Address } from "../c64.ts";
|
|
11
|
+
|
|
11
12
|
import { COVERAGE_KINDS, DEFAULT_SPACE, MEMORY_ROW, type CoverageKind, type Space } from "./schema.ts";
|
|
12
|
-
import { checkSpace,
|
|
13
|
+
import { checkSpace, write, type Evidence, type WriteContext } from "./write.ts";
|
|
13
14
|
|
|
14
15
|
/** The 64 KiB of one space: offset n is address n. */
|
|
15
16
|
const SIZE = 0x10000;
|
|
@@ -84,13 +85,13 @@ function memoryRows(segments: readonly AddressRange[], byteAt: (address: number)
|
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
function checkSegments(segments: readonly AddressRange[]): void {
|
|
87
|
-
if (segments.length === 0) throw new
|
|
88
|
+
if (segments.length === 0) throw new Refusal("invalid-input", "An image has at least one segment.");
|
|
88
89
|
const sorted = [...segments].sort((a, b) => a.start - b.start);
|
|
89
90
|
sorted.forEach((segment, index) => {
|
|
90
91
|
if (!Number.isInteger(segment.start) || !Number.isInteger(segment.end) || segment.start < 0 || segment.end > 0xffff || segment.end < segment.start) {
|
|
91
|
-
throw new
|
|
92
|
+
throw new Refusal("invalid-input", `The segment ${formatC64Address(segment.start)}-${formatC64Address(segment.end)} is not a range from $0000 to $ffff.`);
|
|
92
93
|
}
|
|
93
|
-
if (index > 0 && segment.start <= sorted[index - 1]!.end) throw new
|
|
94
|
+
if (index > 0 && segment.start <= sorted[index - 1]!.end) throw new Refusal("invalid-input", `The segments at ${formatC64Address(sorted[index - 1]!.start)} and ${formatC64Address(segment.start)} overlap.`);
|
|
94
95
|
});
|
|
95
96
|
}
|
|
96
97
|
|
|
@@ -102,8 +103,8 @@ function checkSegments(segments: readonly AddressRange[]): void {
|
|
|
102
103
|
export function setImage(db: DatabaseSync, context: WriteContext, request: ImageRequest): { revision: string | null; current: { space: Space; bytes: number; rowsAdded: number; rowsClosed: number; entry: boolean } } {
|
|
103
104
|
checkSpace(request);
|
|
104
105
|
checkSegments(request.segments);
|
|
105
|
-
if (request.entry !== undefined && (!Number.isInteger(request.entry) || request.entry < 0 || request.entry > 0xffff)) throw new
|
|
106
|
-
if (request.entry === undefined && (request.state !== undefined || request.stages !== undefined)) throw new
|
|
106
|
+
if (request.entry !== undefined && (!Number.isInteger(request.entry) || request.entry < 0 || request.entry > 0xffff)) throw new Refusal("invalid-input", "The entry must be from $0000 to $ffff.");
|
|
107
|
+
if (request.entry === undefined && (request.state !== undefined || request.stages !== undefined)) throw new Refusal("invalid-input", "A machine state or stages need an entry.");
|
|
107
108
|
const space = request.space ?? DEFAULT_SPACE;
|
|
108
109
|
const wanted = memoryRows(request.segments, request.byteAt);
|
|
109
110
|
return write(db, context, "image", (change) => {
|
|
@@ -177,7 +178,7 @@ export function addCoverage(
|
|
|
177
178
|
checkSpace(request);
|
|
178
179
|
const space = request.space ?? DEFAULT_SPACE;
|
|
179
180
|
for (const map of request.maps) {
|
|
180
|
-
if (map.bytes.length !== SIZE) throw new
|
|
181
|
+
if (map.bytes.length !== SIZE) throw new Refusal("invalid-input", `${map.name} has ${map.bytes.length} bytes; a coverage map has 65536, one for each address.`);
|
|
181
182
|
}
|
|
182
183
|
const hashed = request.maps.map((map) => ({ ...map, sha256: createHash("sha256").update(map.bytes).digest("hex") }));
|
|
183
184
|
const known = new Set(coverageRuns(db).filter((run) => run.space === space).map((run) => `${run.name}\n${run.sha256}`));
|
|
@@ -235,7 +236,7 @@ export function requireProgram(db: DatabaseSync | undefined, space: Space = DEFA
|
|
|
235
236
|
if (image !== undefined) return image;
|
|
236
237
|
const where = space === DEFAULT_SPACE ? "" : ` in ${space}`;
|
|
237
238
|
const option = space === DEFAULT_SPACE ? "" : ` --space ${space}`;
|
|
238
|
-
throw new
|
|
239
|
+
throw new Refusal("not-found", `The knowledge has no program${where}. Put it in with knowledge.ts image <image.json|prg>${option} of the c64-knowledge skill.`);
|
|
239
240
|
}
|
|
240
241
|
|
|
241
242
|
/** The bytes for an analyzer: a PRG for one segment without an entry state, else the 64 KiB. */
|
package/src/knowledge/read.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import type { DatabaseSync } from "node:sqlite";
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import type { AddressRange } from "../c64.ts";
|
|
8
|
+
import { Refusal } from "../errors.ts";
|
|
8
9
|
import { DEFAULT_SPACE, type CommentPlacement, type OperandSelect, type Origin, type ReferenceKind, type RegionType, type Space, type SuppressionEntity, type SymbolKind } from "./schema.ts";
|
|
9
10
|
import { NO_REVISION } from "./text.ts";
|
|
10
11
|
|
|
@@ -251,12 +252,12 @@ const NAMED_REVISIONS = 5;
|
|
|
251
252
|
*/
|
|
252
253
|
export function resolveRevision(db: DatabaseSync | undefined, text: string): string {
|
|
253
254
|
if (text === NO_REVISION) return NO_REVISION;
|
|
254
|
-
if (!/^[0-9a-zT.-]+$/.test(text)) throw new
|
|
255
|
+
if (!/^[0-9a-zT.-]+$/.test(text)) throw new Refusal("invalid-input", `${JSON.stringify(text)} is not a revision id. A revision id looks like 20261015T214501.123-k3f9qz; its start is enough when only one revision has it.`);
|
|
255
256
|
const found = (db?.prepare("SELECT id FROM revisions WHERE substr(id, 1, ?) = ? ORDER BY id LIMIT ?").all(text.length, text, NAMED_REVISIONS + 1) ?? []) as Array<{ id: string }>;
|
|
256
|
-
if (found.length === 0) throw new
|
|
257
|
+
if (found.length === 0) throw new Refusal("not-found", `There is no revision ${text}.`);
|
|
257
258
|
if (found.length > 1) {
|
|
258
259
|
const named = found.slice(0, NAMED_REVISIONS).map((row) => row.id).join(", ");
|
|
259
|
-
throw new
|
|
260
|
+
throw new Refusal("invalid-input", `${text} is the start of more than one revision: ${named}${found.length > NAMED_REVISIONS ? ", ..." : ""}. Give more of the id.`);
|
|
260
261
|
}
|
|
261
262
|
return found[0]!.id;
|
|
262
263
|
}
|
|
@@ -288,7 +289,7 @@ export const ANALYZER_NAME_ORDER: readonly Origin[] = ["heuristic", "ghidra", "d
|
|
|
288
289
|
const byAnalyzer = (a: SymbolRow, b: SymbolRow) => a.address - b.address || ANALYZER_NAME_ORDER.indexOf(a.origin) - ANALYZER_NAME_ORDER.indexOf(b.origin);
|
|
289
290
|
|
|
290
291
|
/** Current analyzer names in a range, in address order and then heuristic, Ghidra, DXA. */
|
|
291
|
-
export function listAnalyzerNames(db: DatabaseSync | undefined, range:
|
|
292
|
+
export function listAnalyzerNames(db: DatabaseSync | undefined, range: AddressRange = ALL, kind?: SymbolKind, origins?: OriginFilter, space: Space = DEFAULT_SPACE): SymbolRow[] {
|
|
292
293
|
if (db === undefined) return [];
|
|
293
294
|
return (db.prepare(`SELECT ${SYMBOL_COLUMNS} FROM current_analyzer_names WHERE space = ? AND address BETWEEN ? AND ?`).all(space, range.start, range.end) as Raw[])
|
|
294
295
|
.map(toSymbol)
|
|
@@ -442,19 +443,13 @@ export function at(db: DatabaseSync | undefined, address: number, space: Space =
|
|
|
442
443
|
return result;
|
|
443
444
|
}
|
|
444
445
|
|
|
445
|
-
|
|
446
|
-
start: number;
|
|
447
|
-
end: number;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
const ALL: Range = { start: 0x0000, end: 0xffff };
|
|
446
|
+
const ALL: AddressRange = { start: 0x0000, end: 0xffff };
|
|
451
447
|
|
|
452
448
|
/** Rows of these origins only; undefined keeps every origin. */
|
|
453
449
|
export type OriginFilter = readonly Origin[] | undefined;
|
|
454
450
|
const kept = (origins: OriginFilter) => (row: { origin: Origin }) => origins === undefined || origins.includes(row.origin);
|
|
455
451
|
|
|
456
|
-
|
|
457
|
-
export function listSymbols(db: DatabaseSync | undefined, range: Range = ALL, kind?: SymbolKind, origins?: OriginFilter, space: Space = DEFAULT_SPACE): SymbolRow[] {
|
|
452
|
+
export function listSymbols(db: DatabaseSync | undefined, range: AddressRange = ALL, kind?: SymbolKind, origins?: OriginFilter, space: Space = DEFAULT_SPACE): SymbolRow[] {
|
|
458
453
|
if (db === undefined) return [];
|
|
459
454
|
const rows =
|
|
460
455
|
kind === undefined
|
|
@@ -463,11 +458,11 @@ export function listSymbols(db: DatabaseSync | undefined, range: Range = ALL, ki
|
|
|
463
458
|
return rows.map((row) => toSymbol(row as Raw)).filter(kept(origins));
|
|
464
459
|
}
|
|
465
460
|
|
|
466
|
-
export function listRegions(db: DatabaseSync | undefined, range:
|
|
461
|
+
export function listRegions(db: DatabaseSync | undefined, range: AddressRange = ALL, type?: RegionType, origins?: OriginFilter, space: Space = DEFAULT_SPACE): RegionRow[] {
|
|
467
462
|
return regionsOverlapping(db, range.start, range.end, space).filter((region) => (type === undefined || region.type === type) && kept(origins)(region));
|
|
468
463
|
}
|
|
469
464
|
|
|
470
|
-
export function listComments(db: DatabaseSync | undefined, range:
|
|
465
|
+
export function listComments(db: DatabaseSync | undefined, range: AddressRange = ALL, origins?: OriginFilter, space: Space = DEFAULT_SPACE): CommentRow[] {
|
|
471
466
|
if (db === undefined) return [];
|
|
472
467
|
return db
|
|
473
468
|
.prepare(`SELECT ${COMMENT_COLUMNS} FROM current_comments WHERE space = ? AND address BETWEEN ? AND ? ORDER BY address, placement`)
|
|
@@ -477,7 +472,7 @@ export function listComments(db: DatabaseSync | undefined, range: Range = ALL, o
|
|
|
477
472
|
}
|
|
478
473
|
|
|
479
474
|
/** Current references whose source or target lies in the range. */
|
|
480
|
-
export function listReferences(db: DatabaseSync | undefined, range:
|
|
475
|
+
export function listReferences(db: DatabaseSync | undefined, range: AddressRange = ALL, kind?: ReferenceKind, origins?: OriginFilter, space: Space = DEFAULT_SPACE): ReferenceRow[] {
|
|
481
476
|
if (db === undefined) return [];
|
|
482
477
|
return db
|
|
483
478
|
.prepare(
|
|
@@ -489,7 +484,7 @@ export function listReferences(db: DatabaseSync | undefined, range: Range = ALL,
|
|
|
489
484
|
}
|
|
490
485
|
|
|
491
486
|
/** Current suppressions whose place lies in the range: a reference by its source or target. */
|
|
492
|
-
export function listSuppressions(db: DatabaseSync | undefined, range:
|
|
487
|
+
export function listSuppressions(db: DatabaseSync | undefined, range: AddressRange = ALL, space: Space = DEFAULT_SPACE): SuppressionRow[] {
|
|
493
488
|
if (db === undefined) return [];
|
|
494
489
|
return db
|
|
495
490
|
.prepare(
|
|
@@ -500,7 +495,7 @@ export function listSuppressions(db: DatabaseSync | undefined, range: Range = AL
|
|
|
500
495
|
}
|
|
501
496
|
|
|
502
497
|
/** Current modules that start in the range, in address order. */
|
|
503
|
-
export function listModules(db: DatabaseSync | undefined, range:
|
|
498
|
+
export function listModules(db: DatabaseSync | undefined, range: AddressRange = ALL, origins?: OriginFilter, space: Space = DEFAULT_SPACE): ModuleRow[] {
|
|
504
499
|
if (db === undefined) return [];
|
|
505
500
|
return db
|
|
506
501
|
.prepare(`SELECT ${MODULE_COLUMNS} FROM current_modules WHERE space = ? AND start_address BETWEEN ? AND ? ORDER BY start_address`)
|
|
@@ -510,7 +505,7 @@ export function listModules(db: DatabaseSync | undefined, range: Range = ALL, or
|
|
|
510
505
|
}
|
|
511
506
|
|
|
512
507
|
/** Current operand expressions of instructions in the range, in address order. */
|
|
513
|
-
export function listOperands(db: DatabaseSync | undefined, range:
|
|
508
|
+
export function listOperands(db: DatabaseSync | undefined, range: AddressRange = ALL, origins?: OriginFilter, space: Space = DEFAULT_SPACE): OperandRow[] {
|
|
514
509
|
if (db === undefined) return [];
|
|
515
510
|
return db
|
|
516
511
|
.prepare(`SELECT ${OPERAND_COLUMNS} FROM current_operands WHERE space = ? AND address BETWEEN ? AND ? ORDER BY address`)
|
|
@@ -520,7 +515,7 @@ export function listOperands(db: DatabaseSync | undefined, range: Range = ALL, o
|
|
|
520
515
|
}
|
|
521
516
|
|
|
522
517
|
/** Current patches that share an address with the range, of one switch or of all, in address order. */
|
|
523
|
-
export function listPatches(db: DatabaseSync | undefined, range:
|
|
518
|
+
export function listPatches(db: DatabaseSync | undefined, range: AddressRange = ALL, switchName?: string, origins?: OriginFilter, space: Space = DEFAULT_SPACE): PatchRow[] {
|
|
524
519
|
return patchesOverlapping(db, range.start, range.end, space).filter((patch) => (switchName === undefined || patch.switch === switchName) && kept(origins)(patch));
|
|
525
520
|
}
|
|
526
521
|
|
|
@@ -535,7 +530,7 @@ export function listMacros(db: DatabaseSync | undefined, origins?: OriginFilter,
|
|
|
535
530
|
}
|
|
536
531
|
|
|
537
532
|
/** Current macro uses that share an address with the range, of one macro or of all, in address order. */
|
|
538
|
-
export function listMacroUses(db: DatabaseSync | undefined, range:
|
|
533
|
+
export function listMacroUses(db: DatabaseSync | undefined, range: AddressRange = ALL, macro?: string, origins?: OriginFilter, space: Space = DEFAULT_SPACE): MacroUseRow[] {
|
|
539
534
|
return macroUsesOverlapping(db, range.start, range.end, space).filter((use) => (macro === undefined || use.macro === macro) && kept(origins)(use));
|
|
540
535
|
}
|
|
541
536
|
|
package/src/knowledge/schema.ts
CHANGED
|
@@ -10,6 +10,13 @@ import { OPERAND_SELECTS } from "./expression.ts";
|
|
|
10
10
|
/** heuristic: 6502 idioms that the static analysis finds by pattern; an analyzer origin. */
|
|
11
11
|
export const ORIGINS = ["user", "llm", "dxa", "ghidra", "heuristic"] as const;
|
|
12
12
|
export type Origin = (typeof ORIGINS)[number];
|
|
13
|
+
/** The origins of semantic knowledge: what the user or the LLM decided. It wins over analyzer facts. */
|
|
14
|
+
export const SEMANTIC_ORIGINS = ["user", "llm"] as const satisfies readonly Origin[];
|
|
15
|
+
/** The analyzers: their facts and names give way to semantic knowledge. */
|
|
16
|
+
export const ANALYZERS = ["dxa", "ghidra", "heuristic"] as const satisfies readonly Origin[];
|
|
17
|
+
export type Analyzer = (typeof ANALYZERS)[number];
|
|
18
|
+
export const isSemantic = (origin: string | undefined): boolean => (SEMANTIC_ORIGINS as readonly (string | undefined)[]).includes(origin);
|
|
19
|
+
export const isAnalyzer = (origin: string | undefined): boolean => (ANALYZERS as readonly (string | undefined)[]).includes(origin);
|
|
13
20
|
export const SYMBOL_KINDS = ["label", "routine", "variable", "data", "vector"] as const;
|
|
14
21
|
export type SymbolKind = (typeof SYMBOL_KINDS)[number];
|
|
15
22
|
export const REGION_TYPES = [
|
|
@@ -76,10 +83,9 @@ export function isGeneratedFile(file: string): boolean {
|
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
/**
|
|
79
|
-
* The address space of a knowledge row:
|
|
80
|
-
*
|
|
86
|
+
* The address space of a knowledge row: a name that SPACE_PATTERN of c64.ts
|
|
87
|
+
* accepts. Drive code and banks reuse C64 addresses.
|
|
81
88
|
*/
|
|
82
|
-
export const SPACE = /^(c64|drive8|bank(0|[1-9][0-9]?|1[0-9]{2}|2[0-4][0-9]|25[0-5]))$/;
|
|
83
89
|
export type Space = string;
|
|
84
90
|
export const DEFAULT_SPACE: Space = "c64";
|
|
85
91
|
const SPACE_CHECK = "space IN ('c64', 'drive8') OR space GLOB 'bank[0-9]' OR space GLOB 'bank[1-9][0-9]' OR space GLOB 'bank[12][0-9][0-9]'";
|
package/src/knowledge/text.ts
CHANGED
|
@@ -10,8 +10,8 @@ import { readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "no
|
|
|
10
10
|
import type { DatabaseSync } from "node:sqlite";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
|
|
13
|
+
import { Refusal } from "../errors.ts";
|
|
13
14
|
import { isoCet } from "../time.ts";
|
|
14
|
-
import { KnowledgeError } from "./database.ts";
|
|
15
15
|
|
|
16
16
|
/** The value of "no revision": empty knowledge. It sorts before every revision id. */
|
|
17
17
|
export const NO_REVISION = "0";
|
|
@@ -248,7 +248,7 @@ export function readText(directory: string): Map<string, { text: string; sha256:
|
|
|
248
248
|
const files = new Map<string, { text: string; sha256: string }>();
|
|
249
249
|
for (const name of readdirSync(directory).sort()) {
|
|
250
250
|
if (!name.endsWith(".jsonl")) continue;
|
|
251
|
-
if (!known.has(name)) throw new
|
|
251
|
+
if (!known.has(name)) throw new Refusal("invalid-database", `.c64-re-tools/knowledge/${name} is not a knowledge file. Knowledge files are ${[...known].join(", ")}.`);
|
|
252
252
|
const bytes = readFileSync(join(directory, name));
|
|
253
253
|
files.set(name, { text: bytes.toString("utf8"), sha256: sha256(bytes) });
|
|
254
254
|
}
|
|
@@ -269,7 +269,7 @@ export function writeAtomic(path: string, text: string): void {
|
|
|
269
269
|
|
|
270
270
|
/** One line of a knowledge file that cannot be read. */
|
|
271
271
|
function bad(file: string, line: number, message: string): never {
|
|
272
|
-
throw new
|
|
272
|
+
throw new Refusal("invalid-database", `.c64-re-tools/knowledge/${file}:${line}: ${message}`);
|
|
273
273
|
}
|
|
274
274
|
|
|
275
275
|
function parseLines(file: string, text: string): Array<{ line: number; value: Raw }> {
|
|
@@ -396,7 +396,7 @@ export function loadText(db: DatabaseSync, files: ReadonlyMap<string, { text: st
|
|
|
396
396
|
insertRevision.run(...(REVISION_FIELDS.map((item) => revision.columns[item.column]) as Array<string | null>));
|
|
397
397
|
for (const item of revision.evidence) insertEvidence.run(revision.id, item.kind as string, item.ref as string, item.sha256 as string | null, item.space as string | null);
|
|
398
398
|
} catch (error) {
|
|
399
|
-
throw new
|
|
399
|
+
throw new Refusal("invalid-database", `.c64-re-tools/knowledge/${REVISIONS_FILE}: the revision ${revision.id} is not valid: ${(error as Error).message}`);
|
|
400
400
|
}
|
|
401
401
|
}
|
|
402
402
|
for (const spec of TABLES) {
|
|
@@ -408,7 +408,7 @@ export function loadText(db: DatabaseSync, files: ReadonlyMap<string, { text: st
|
|
|
408
408
|
try {
|
|
409
409
|
insert.run(...(columns.map((column) => row.columns[column]) as Array<string | number | null>));
|
|
410
410
|
} catch (error) {
|
|
411
|
-
throw new
|
|
411
|
+
throw new Refusal("invalid-database", `.c64-re-tools/knowledge/${spec.file}: the row ${row.id} is not valid: ${(error as Error).message}`);
|
|
412
412
|
}
|
|
413
413
|
}
|
|
414
414
|
}
|