@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,209 @@
|
|
|
1
|
+
// Operand expressions: the part of ACME's expression syntax that names
|
|
2
|
+
// an operand by symbols and numbers. Knowledge checks an expression against
|
|
3
|
+
// the image with it, and c64-source writes it with the names of its layout.
|
|
4
|
+
//
|
|
5
|
+
// Priorities are ACME's, from high to low: unary - and !, then * / %, + -,
|
|
6
|
+
// << >> >>>, unary < and >, &, XOR, |. A unary < or > takes everything of a
|
|
7
|
+
// higher priority after it, so <a+1 is the low byte of a+1.
|
|
8
|
+
|
|
9
|
+
/** An expression that does not parse, or that names a symbol with no value. */
|
|
10
|
+
export class ExpressionError extends Error {
|
|
11
|
+
override name = "ExpressionError";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The selectors of an operand expression: a byte of a symbol, its value, or free expression text. */
|
|
15
|
+
export const OPERAND_SELECTS = ["lo", "hi", "value", "expr"] as const;
|
|
16
|
+
export type OperandSelect = (typeof OPERAND_SELECTS)[number];
|
|
17
|
+
|
|
18
|
+
/** An operand expression as knowledge stores it: a symbol with an offset, or text for expr. */
|
|
19
|
+
export interface OperandExpression {
|
|
20
|
+
select: OperandSelect;
|
|
21
|
+
symbol?: string;
|
|
22
|
+
offset?: number;
|
|
23
|
+
expression?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type Node =
|
|
27
|
+
| { kind: "number"; value: number }
|
|
28
|
+
| { kind: "name"; name: string }
|
|
29
|
+
| { kind: "unary"; operator: string; operand: Node }
|
|
30
|
+
| { kind: "binary"; operator: string; left: Node; right: Node };
|
|
31
|
+
|
|
32
|
+
/** Binary operators by priority (higher binds tighter); unary < and > sit at LOW_HIGH. */
|
|
33
|
+
const BINARY: Record<string, number> = { "*": 10, "/": 10, "%": 10, "+": 9, "-": 9, "<<": 8, ">>": 8, ">>>": 8, "&": 4, XOR: 3, "|": 2 };
|
|
34
|
+
const LOW_HIGH = 7;
|
|
35
|
+
const NAME = /^[A-Za-z_.][A-Za-z0-9_]*/;
|
|
36
|
+
const MAX_TEXT = 200;
|
|
37
|
+
|
|
38
|
+
interface Token {
|
|
39
|
+
text: string;
|
|
40
|
+
at: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The expression as tokens with their offsets; context decides % (binary literal or remainder). */
|
|
44
|
+
function tokenize(text: string): Token[] {
|
|
45
|
+
const tokens: Token[] = [];
|
|
46
|
+
let at = 0;
|
|
47
|
+
let operandNext = true;
|
|
48
|
+
while (at < text.length) {
|
|
49
|
+
const rest = text.slice(at);
|
|
50
|
+
const space = /^\s+/.exec(rest);
|
|
51
|
+
if (space) {
|
|
52
|
+
at += space[0].length;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const number: RegExpExecArray | null = operandNext ? /^(\$[0-9A-Fa-f]+|%[01]+|[0-9]+)/.exec(rest) : null;
|
|
56
|
+
const match: RegExpExecArray | null = number ?? /^(>>>|<<|>>|XOR\b)/.exec(rest) ?? NAME.exec(rest) ?? /^[-+*/%&|!<>()]/.exec(rest);
|
|
57
|
+
if (!match) throw new ExpressionError(`"${text}" has an unknown character at position ${at + 1}: "${rest[0]}".`);
|
|
58
|
+
const token: string = match[0];
|
|
59
|
+
tokens.push({ text: token, at });
|
|
60
|
+
at += token.length;
|
|
61
|
+
// After an operand or a ), an operator comes next; after anything else, an operand.
|
|
62
|
+
operandNext = !(number !== null || (NAME.test(token) && token !== "XOR") || token === ")");
|
|
63
|
+
}
|
|
64
|
+
return tokens;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parse(text: string): Node {
|
|
68
|
+
if (text.trim().length === 0) throw new ExpressionError("An expression is empty.");
|
|
69
|
+
if (text.length > MAX_TEXT) throw new ExpressionError(`An expression has at most ${MAX_TEXT} characters.`);
|
|
70
|
+
const tokens = tokenize(text);
|
|
71
|
+
let index = 0;
|
|
72
|
+
const peek = () => tokens[index]?.text;
|
|
73
|
+
const fail = (what: string): never => {
|
|
74
|
+
throw new ExpressionError(`"${text}" ${what}.`);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const operand = (): Node => {
|
|
78
|
+
const token = tokens[index++]?.text ?? fail("ends where an operand is missing");
|
|
79
|
+
if (token === "(") {
|
|
80
|
+
const inner = binary(0);
|
|
81
|
+
if (tokens[index++]?.text !== ")") fail("has a ( with no )");
|
|
82
|
+
return inner;
|
|
83
|
+
}
|
|
84
|
+
if (token === "-" || token === "!") return { kind: "unary", operator: token, operand: operand() };
|
|
85
|
+
if (token === "<" || token === ">") return { kind: "unary", operator: token, operand: binary(LOW_HIGH + 1) };
|
|
86
|
+
if (token.startsWith("$")) return { kind: "number", value: Number.parseInt(token.slice(1), 16) };
|
|
87
|
+
if (token.startsWith("%")) return { kind: "number", value: Number.parseInt(token.slice(1), 2) };
|
|
88
|
+
if (/^[0-9]/.test(token)) return { kind: "number", value: Number.parseInt(token, 10) };
|
|
89
|
+
if (NAME.test(token) && token !== "XOR") return { kind: "name", name: token };
|
|
90
|
+
return fail(`has "${token}" where an operand must be`);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const binary = (minimum: number): Node => {
|
|
94
|
+
let left = operand();
|
|
95
|
+
for (;;) {
|
|
96
|
+
const operator = peek();
|
|
97
|
+
const priority = operator === undefined ? undefined : BINARY[operator];
|
|
98
|
+
if (priority === undefined || priority < minimum) return left;
|
|
99
|
+
index++;
|
|
100
|
+
left = { kind: "binary", operator: operator!, left, right: binary(priority + 1) };
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const tree = binary(0);
|
|
105
|
+
if (index < tokens.length) fail(`has "${tokens[index]!.text}" after the end of the expression`);
|
|
106
|
+
return tree;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function names(node: Node, into: string[]): string[] {
|
|
110
|
+
if (node.kind === "name" && !into.includes(node.name)) into.push(node.name);
|
|
111
|
+
if (node.kind === "unary") names(node.operand, into);
|
|
112
|
+
if (node.kind === "binary") {
|
|
113
|
+
names(node.left, into);
|
|
114
|
+
names(node.right, into);
|
|
115
|
+
}
|
|
116
|
+
return into;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function value(node: Node, resolve: (name: string) => number | undefined): number {
|
|
120
|
+
switch (node.kind) {
|
|
121
|
+
case "number":
|
|
122
|
+
return node.value;
|
|
123
|
+
case "name": {
|
|
124
|
+
const found = resolve(node.name);
|
|
125
|
+
if (found === undefined) throw new ExpressionError(`"${node.name}" is no symbol of the knowledge and no platform name.`);
|
|
126
|
+
return found;
|
|
127
|
+
}
|
|
128
|
+
case "unary": {
|
|
129
|
+
const inner = value(node.operand, resolve);
|
|
130
|
+
if (node.operator === "-") return -inner;
|
|
131
|
+
if (node.operator === "!") return ~inner;
|
|
132
|
+
return node.operator === "<" ? inner & 0xff : (inner >> 8) & 0xff;
|
|
133
|
+
}
|
|
134
|
+
case "binary": {
|
|
135
|
+
const left = value(node.left, resolve);
|
|
136
|
+
const right = value(node.right, resolve);
|
|
137
|
+
switch (node.operator) {
|
|
138
|
+
case "*":
|
|
139
|
+
return left * right;
|
|
140
|
+
case "/":
|
|
141
|
+
case "%":
|
|
142
|
+
if (right === 0) throw new ExpressionError("An expression divides by zero.");
|
|
143
|
+
return node.operator === "/" ? Math.trunc(left / right) : left % right;
|
|
144
|
+
case "+":
|
|
145
|
+
return left + right;
|
|
146
|
+
case "-":
|
|
147
|
+
return left - right;
|
|
148
|
+
case "<<":
|
|
149
|
+
return left << right;
|
|
150
|
+
case ">>":
|
|
151
|
+
return left >> right;
|
|
152
|
+
case ">>>":
|
|
153
|
+
return left >>> right;
|
|
154
|
+
case "&":
|
|
155
|
+
return left & right;
|
|
156
|
+
case "XOR":
|
|
157
|
+
return left ^ right;
|
|
158
|
+
default:
|
|
159
|
+
return left | right;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Throws ExpressionError when the text is no expression of this syntax. */
|
|
166
|
+
export function checkExpression(text: string): void {
|
|
167
|
+
parse(text);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The value of the expression; `resolve` gives the value of a name, or undefined. */
|
|
171
|
+
export function evaluate(text: string, resolve: (name: string) => number | undefined): number {
|
|
172
|
+
return value(parse(text), resolve);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The names the expression uses, each once, in their order. */
|
|
176
|
+
export function identifiers(text: string): string[] {
|
|
177
|
+
return names(parse(text), []);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The text with each name replaced by `replace(name)` (a name it returns
|
|
182
|
+
* unchanged stays). Spaces and other tokens stay as written.
|
|
183
|
+
*/
|
|
184
|
+
export function replaceIdentifiers(text: string, replace: (name: string) => string): string {
|
|
185
|
+
parse(text);
|
|
186
|
+
let out = "";
|
|
187
|
+
let from = 0;
|
|
188
|
+
for (const token of tokenize(text)) {
|
|
189
|
+
if (!NAME.test(token.text) || token.text === "XOR") continue;
|
|
190
|
+
out += text.slice(from, token.at) + replace(token.text);
|
|
191
|
+
from = token.at + token.text.length;
|
|
192
|
+
}
|
|
193
|
+
return out + text.slice(from);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function offsetText(offset: number): string {
|
|
197
|
+
const size = Math.abs(offset);
|
|
198
|
+
return `${offset < 0 ? "-" : "+"}${size < 10 ? String(size) : `$${size.toString(16)}`}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** ACME text of an operand expression, for example <irq_handler, >(sprite_bank+$1000) or bar_colours-1. */
|
|
202
|
+
export function operandText(operand: OperandExpression): string {
|
|
203
|
+
if (operand.select === "expr") return operand.expression!;
|
|
204
|
+
const offset = operand.offset ?? 0;
|
|
205
|
+
const sum = offset === 0 ? operand.symbol! : `${operand.symbol!}${offsetText(offset)}`;
|
|
206
|
+
if (operand.select === "value") return sum;
|
|
207
|
+
const selector = operand.select === "lo" ? "<" : ">";
|
|
208
|
+
return offset === 0 ? `${selector}${sum}` : `${selector}(${sum})`;
|
|
209
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// The routine graph of the knowledge: which routine calls, jumps
|
|
2
|
+
// to or dispatches through a table to which other routine, and the same
|
|
3
|
+
// graph as a mermaid flowchart. The callers give the rows, so the graph needs
|
|
4
|
+
// no database and the documentation and the knowledge script draw one graph.
|
|
5
|
+
|
|
6
|
+
import { formatC64Address } from "../c64.ts";
|
|
7
|
+
import type { ReferenceKind, SymbolKind } from "./schema.ts";
|
|
8
|
+
|
|
9
|
+
/** How one routine reaches another: a call, a jump, or an entry of a table of addresses. */
|
|
10
|
+
export type EdgeKind = "call" | "jump" | "table";
|
|
11
|
+
|
|
12
|
+
export interface GraphReference {
|
|
13
|
+
from: number;
|
|
14
|
+
to: number;
|
|
15
|
+
kind: ReferenceKind | "table";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface RoutineHead {
|
|
19
|
+
address: number;
|
|
20
|
+
name: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface GraphEdge {
|
|
24
|
+
from: number;
|
|
25
|
+
to: number;
|
|
26
|
+
/** The strongest kind of the references that make the edge: a call, then a table entry, then a jump. */
|
|
27
|
+
kind: EdgeKind;
|
|
28
|
+
/** The references that make the edge. */
|
|
29
|
+
count: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const STRENGTH: Record<EdgeKind, number> = { call: 3, table: 2, jump: 1 };
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The routine heads: every routine of the knowledge, and every call or table
|
|
36
|
+
* target, which another routine enters at its start. A head without a name of
|
|
37
|
+
* the knowledge gets the name that `nameOf` gives.
|
|
38
|
+
*/
|
|
39
|
+
export function routineHeads(symbols: ReadonlyArray<{ address: number; name: string; kind: SymbolKind }>, references: readonly GraphReference[], nameOf: (address: number) => string): RoutineHead[] {
|
|
40
|
+
const names = new Map(symbols.map((symbol) => [symbol.address, symbol.name]));
|
|
41
|
+
const heads = new Set(symbols.filter((symbol) => symbol.kind === "routine").map((symbol) => symbol.address));
|
|
42
|
+
for (const reference of references) if (reference.kind === "call" || reference.kind === "table") heads.add(reference.to);
|
|
43
|
+
return [...heads].sort((a, b) => a - b).map((address) => ({ address, name: names.get(address) ?? nameOf(address) }));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The head that holds an address: the nearest head at or below it, when no
|
|
48
|
+
* boundary (a module start) lies between the two.
|
|
49
|
+
*/
|
|
50
|
+
export function ownerOf(heads: readonly RoutineHead[], boundaries: readonly number[], address: number): RoutineHead | undefined {
|
|
51
|
+
let low = 0;
|
|
52
|
+
let high = heads.length - 1;
|
|
53
|
+
let found: RoutineHead | undefined;
|
|
54
|
+
while (low <= high) {
|
|
55
|
+
const middle = (low + high) >> 1;
|
|
56
|
+
if (heads[middle]!.address <= address) {
|
|
57
|
+
found = heads[middle];
|
|
58
|
+
low = middle + 1;
|
|
59
|
+
} else high = middle - 1;
|
|
60
|
+
}
|
|
61
|
+
if (found === undefined) return undefined;
|
|
62
|
+
return boundaries.some((boundary) => boundary > found.address && boundary <= address) ? undefined : found;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The edges between routines: each call, jump or table reference from one head's code to another head; a jump inside a routine is no edge. */
|
|
66
|
+
export function routineEdges(heads: readonly RoutineHead[], boundaries: readonly number[], references: readonly GraphReference[]): GraphEdge[] {
|
|
67
|
+
const edges = new Map<string, GraphEdge>();
|
|
68
|
+
for (const reference of references) {
|
|
69
|
+
if (reference.kind !== "call" && reference.kind !== "jump" && reference.kind !== "table") continue;
|
|
70
|
+
const from = ownerOf(heads, boundaries, reference.from);
|
|
71
|
+
const to = ownerOf(heads, boundaries, reference.to);
|
|
72
|
+
if (from === undefined || to === undefined || from.address === to.address) continue;
|
|
73
|
+
// A jump into the middle of another routine is an edge to that routine.
|
|
74
|
+
const key = `${from.address}:${to.address}`;
|
|
75
|
+
const edge = edges.get(key);
|
|
76
|
+
if (edge === undefined) edges.set(key, { from: from.address, to: to.address, kind: reference.kind, count: 1 });
|
|
77
|
+
else {
|
|
78
|
+
edge.count++;
|
|
79
|
+
if (STRENGTH[reference.kind] > STRENGTH[edge.kind]) edge.kind = reference.kind;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return [...edges.values()].sort((a, b) => a.from - b.from || a.to - b.to);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const ARROWS: Record<EdgeKind, string> = { call: "-->", table: "==>", jump: "-.->" };
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A mermaid flowchart of the edges: a node per head with its name and
|
|
89
|
+
* address, a solid arrow for a call, a thick one for a table entry and a
|
|
90
|
+
* dotted one for a jump. The heads in `own` are drawn as rounded nodes.
|
|
91
|
+
*/
|
|
92
|
+
export function mermaidGraph(heads: readonly RoutineHead[], edges: readonly GraphEdge[], own: ReadonlySet<number> = new Set()): string {
|
|
93
|
+
const used = new Set(edges.flatMap((edge) => [edge.from, edge.to]));
|
|
94
|
+
for (const address of own) used.add(address);
|
|
95
|
+
const id = (address: number) => `n${address.toString(16).padStart(4, "0")}`;
|
|
96
|
+
const lines = ["flowchart LR"];
|
|
97
|
+
for (const head of heads) {
|
|
98
|
+
if (!used.has(head.address)) continue;
|
|
99
|
+
const label = `${head.name}<br/>${formatC64Address(head.address).toUpperCase()}`;
|
|
100
|
+
lines.push(own.has(head.address) ? ` ${id(head.address)}("${label}")` : ` ${id(head.address)}["${label}"]`);
|
|
101
|
+
}
|
|
102
|
+
for (const edge of edges) lines.push(` ${id(edge.from)} ${ARROWS[edge.kind]} ${id(edge.to)}`);
|
|
103
|
+
return lines.join("\n");
|
|
104
|
+
}
|
package/src/knowledge/history.ts
CHANGED
|
@@ -6,53 +6,72 @@ import type { DatabaseSync } from "node:sqlite";
|
|
|
6
6
|
import { KnowledgeError } from "./database.ts";
|
|
7
7
|
import {
|
|
8
8
|
COMMENT_COLUMNS,
|
|
9
|
+
MACRO_COLUMNS,
|
|
10
|
+
MACRO_USE_COLUMNS,
|
|
9
11
|
MODULE_COLUMNS,
|
|
12
|
+
OPERAND_COLUMNS,
|
|
13
|
+
PATCH_COLUMNS,
|
|
10
14
|
REFERENCE_COLUMNS,
|
|
11
15
|
REGION_COLUMNS,
|
|
16
|
+
resolveRevision,
|
|
12
17
|
SUPPRESSION_COLUMNS,
|
|
13
18
|
SYMBOL_COLUMNS,
|
|
14
19
|
toComment,
|
|
20
|
+
toMacro,
|
|
21
|
+
toMacroUse,
|
|
15
22
|
toModule,
|
|
23
|
+
toOperand,
|
|
24
|
+
toPatch,
|
|
16
25
|
toReference,
|
|
17
26
|
toRegion,
|
|
18
27
|
toSuppression,
|
|
19
28
|
toSymbol,
|
|
20
29
|
type CommentRow,
|
|
30
|
+
type MacroRow,
|
|
31
|
+
type MacroUseRow,
|
|
21
32
|
type ModuleRow,
|
|
33
|
+
type OperandRow,
|
|
34
|
+
type PatchRow,
|
|
22
35
|
type ReferenceRow,
|
|
23
36
|
type RegionRow,
|
|
24
37
|
type SuppressionRow,
|
|
25
38
|
type SymbolRow,
|
|
26
39
|
} from "./read.ts";
|
|
27
40
|
import { DEFAULT_SPACE, type EvidenceKind, type Origin, type Space } from "./schema.ts";
|
|
41
|
+
import { compareText } from "./text.ts";
|
|
28
42
|
import type { Evidence } from "./write.ts";
|
|
29
43
|
|
|
30
44
|
export interface Revision {
|
|
31
|
-
id:
|
|
45
|
+
id: string;
|
|
32
46
|
createdAt: string;
|
|
33
47
|
origin: Origin;
|
|
34
48
|
operation: string;
|
|
35
49
|
description?: string;
|
|
36
50
|
inputHash?: string;
|
|
37
51
|
toolVersion?: string;
|
|
38
|
-
/** What the change rests on
|
|
52
|
+
/** What the change rests on. */
|
|
39
53
|
evidence?: Evidence[];
|
|
40
54
|
}
|
|
41
55
|
|
|
42
56
|
/** One knowledge row with its validity: from its first revision until the revision that closed it. */
|
|
43
57
|
export type HistoryEntry =
|
|
44
|
-
| { entity: "symbol"; row: SymbolRow; from:
|
|
45
|
-
| { entity: "
|
|
46
|
-
| { entity: "
|
|
47
|
-
| { entity: "
|
|
48
|
-
| { entity: "
|
|
49
|
-
| { entity: "
|
|
58
|
+
| { entity: "symbol"; row: SymbolRow; from: string; to?: string }
|
|
59
|
+
| { entity: "analyzer-name"; row: SymbolRow; from: string; to?: string }
|
|
60
|
+
| { entity: "region"; row: RegionRow; from: string; to?: string }
|
|
61
|
+
| { entity: "comment"; row: CommentRow; from: string; to?: string }
|
|
62
|
+
| { entity: "reference"; row: ReferenceRow; from: string; to?: string }
|
|
63
|
+
| { entity: "suppression"; row: SuppressionRow; from: string; to?: string }
|
|
64
|
+
| { entity: "module"; row: ModuleRow; from: string; to?: string }
|
|
65
|
+
| { entity: "operand"; row: OperandRow; from: string; to?: string }
|
|
66
|
+
| { entity: "patch"; row: PatchRow; from: string; to?: string }
|
|
67
|
+
| { entity: "macro"; row: MacroRow; from: string; to?: string }
|
|
68
|
+
| { entity: "macro-use"; row: MacroUseRow; from: string; to?: string };
|
|
50
69
|
|
|
51
70
|
type Raw = Record<string, unknown>;
|
|
52
71
|
|
|
53
72
|
function toRevision(row: Raw): Revision {
|
|
54
73
|
const revision: Revision = {
|
|
55
|
-
id: row.id as
|
|
74
|
+
id: row.id as string,
|
|
56
75
|
createdAt: row.created_at as string,
|
|
57
76
|
origin: row.origin as Origin,
|
|
58
77
|
operation: row.operation as string,
|
|
@@ -64,13 +83,14 @@ function toRevision(row: Raw): Revision {
|
|
|
64
83
|
}
|
|
65
84
|
|
|
66
85
|
function entry<E extends HistoryEntry["entity"]>(entity: E, row: unknown, raw: Raw): HistoryEntry {
|
|
67
|
-
const result = { entity, row, from: raw.valid_from_revision as
|
|
68
|
-
if (raw.valid_to_revision !== null) result.to = raw.valid_to_revision as
|
|
86
|
+
const result = { entity, row, from: raw.valid_from_revision as string } as HistoryEntry;
|
|
87
|
+
if (raw.valid_to_revision !== null) result.to = raw.valid_to_revision as string;
|
|
69
88
|
return result;
|
|
70
89
|
}
|
|
71
90
|
|
|
91
|
+
/** Revision ids sort in time order as text; a current row sorts after every closed one. */
|
|
72
92
|
function bySequence(a: HistoryEntry, b: HistoryEntry): number {
|
|
73
|
-
return a.from
|
|
93
|
+
return compareText(a.from, b.from) || compareText(a.to ?? "~", b.to ?? "~");
|
|
74
94
|
}
|
|
75
95
|
|
|
76
96
|
/** Every row, current or closed, that concerns one address of a space, oldest first. */
|
|
@@ -79,6 +99,7 @@ export function historyAt(db: DatabaseSync | undefined, address: number, space:
|
|
|
79
99
|
const validity = ", valid_to_revision";
|
|
80
100
|
const entries: HistoryEntry[] = [];
|
|
81
101
|
for (const raw of db.prepare(`SELECT ${SYMBOL_COLUMNS}${validity} FROM symbols WHERE space = ? AND address = ?`).all(space, address) as Raw[]) entries.push(entry("symbol", toSymbol(raw), raw));
|
|
102
|
+
for (const raw of db.prepare(`SELECT ${SYMBOL_COLUMNS}${validity} FROM analyzer_names WHERE space = ? AND address = ?`).all(space, address) as Raw[]) entries.push(entry("analyzer-name", toSymbol(raw), raw));
|
|
82
103
|
for (const raw of db.prepare(`SELECT ${REGION_COLUMNS}${validity} FROM regions WHERE space = ? AND start_address <= ? AND end_address >= ?`).all(space, address, address) as Raw[]) {
|
|
83
104
|
entries.push(entry("region", toRegion(raw), raw));
|
|
84
105
|
}
|
|
@@ -92,9 +113,24 @@ export function historyAt(db: DatabaseSync | undefined, address: number, space:
|
|
|
92
113
|
entries.push(entry("suppression", toSuppression(raw), raw));
|
|
93
114
|
}
|
|
94
115
|
for (const raw of db.prepare(`SELECT ${MODULE_COLUMNS}${validity} FROM modules WHERE space = ? AND start_address = ?`).all(space, address) as Raw[]) entries.push(entry("module", toModule(raw), raw));
|
|
116
|
+
for (const raw of db.prepare(`SELECT ${OPERAND_COLUMNS}${validity} FROM operands WHERE space = ? AND address = ?`).all(space, address) as Raw[]) entries.push(entry("operand", toOperand(raw), raw));
|
|
117
|
+
for (const raw of db.prepare(`SELECT ${PATCH_COLUMNS}${validity} FROM patches WHERE space = ? AND start_address <= ? AND end_address >= ?`).all(space, address, address) as Raw[]) {
|
|
118
|
+
entries.push(entry("patch", toPatch(raw), raw));
|
|
119
|
+
}
|
|
120
|
+
for (const raw of db.prepare(`SELECT ${MACRO_USE_COLUMNS}${validity} FROM macro_uses WHERE space = ? AND start_address <= ? AND end_address >= ?`).all(space, address, address) as Raw[]) {
|
|
121
|
+
entries.push(entry("macro-use", toMacroUse(raw), raw));
|
|
122
|
+
}
|
|
95
123
|
return entries.sort(bySequence);
|
|
96
124
|
}
|
|
97
125
|
|
|
126
|
+
/** Every row of a macro with this name, and every use of it, oldest first. */
|
|
127
|
+
export function historyOfMacro(db: DatabaseSync | undefined, name: string, space: Space = DEFAULT_SPACE): HistoryEntry[] {
|
|
128
|
+
if (db === undefined) return [];
|
|
129
|
+
const macros = (db.prepare(`SELECT ${MACRO_COLUMNS}, valid_to_revision FROM macros WHERE space = ? AND name = ?`).all(space, name) as Raw[]).map((raw) => entry("macro", toMacro(raw), raw));
|
|
130
|
+
const uses = (db.prepare(`SELECT ${MACRO_USE_COLUMNS}, valid_to_revision FROM macro_uses WHERE space = ? AND macro = ?`).all(space, name) as Raw[]).map((raw) => entry("macro-use", toMacroUse(raw), raw));
|
|
131
|
+
return [...macros, ...uses].sort(bySequence);
|
|
132
|
+
}
|
|
133
|
+
|
|
98
134
|
/** Every symbol row of a space that ever had this name, oldest first: a name can move between addresses. */
|
|
99
135
|
export function historyNamed(db: DatabaseSync | undefined, name: string, space: Space = DEFAULT_SPACE): HistoryEntry[] {
|
|
100
136
|
if (db === undefined) return [];
|
|
@@ -104,7 +140,7 @@ export function historyNamed(db: DatabaseSync | undefined, name: string, space:
|
|
|
104
140
|
}
|
|
105
141
|
|
|
106
142
|
/** Why a revision was written and what it rests on: its description and its evidence, when it has them. */
|
|
107
|
-
export function revisionNote(db: DatabaseSync | undefined, id:
|
|
143
|
+
export function revisionNote(db: DatabaseSync | undefined, id: string): { reason?: string; evidence?: Evidence[] } {
|
|
108
144
|
const raw = db?.prepare("SELECT * FROM revisions WHERE id = ?").get(id) as Raw | undefined;
|
|
109
145
|
if (db === undefined || raw === undefined) return {};
|
|
110
146
|
const { description, evidence } = withEvidence(db, toRevision(raw));
|
|
@@ -113,20 +149,25 @@ export function revisionNote(db: DatabaseSync | undefined, id: number): { reason
|
|
|
113
149
|
|
|
114
150
|
/** The revision with its evidence, when it has any. */
|
|
115
151
|
function withEvidence(db: DatabaseSync, revision: Revision): Revision {
|
|
116
|
-
const rows = db.prepare("SELECT kind, ref, sha256 FROM evidence WHERE revision = ? ORDER BY id").all(revision.id) as Raw[];
|
|
152
|
+
const rows = db.prepare("SELECT kind, ref, sha256, space FROM evidence WHERE revision = ? ORDER BY id").all(revision.id) as Raw[];
|
|
117
153
|
if (rows.length === 0) return revision;
|
|
118
|
-
const evidence = rows.map((row): Evidence => ({
|
|
154
|
+
const evidence = rows.map((row): Evidence => ({
|
|
155
|
+
kind: row.kind as EvidenceKind,
|
|
156
|
+
ref: row.ref as string,
|
|
157
|
+
...(row.sha256 === null ? {} : { sha256: row.sha256 as string }),
|
|
158
|
+
...(row.space === null ? {} : { space: row.space as Space }),
|
|
159
|
+
}));
|
|
119
160
|
return { ...revision, evidence };
|
|
120
161
|
}
|
|
121
162
|
|
|
122
163
|
/** Revisions, newest first; with `before`, only revisions older than it. */
|
|
123
|
-
export function revisions(db: DatabaseSync | undefined, options: { limit?: number; before?:
|
|
164
|
+
export function revisions(db: DatabaseSync | undefined, options: { limit?: number; before?: string } = {}): Revision[] {
|
|
124
165
|
if (db === undefined) return [];
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
.prepare("SELECT * FROM revisions
|
|
128
|
-
.all(options.before
|
|
129
|
-
).map((row) => withEvidence(db, toRevision(row)));
|
|
166
|
+
const rows =
|
|
167
|
+
options.before === undefined
|
|
168
|
+
? db.prepare("SELECT * FROM revisions ORDER BY id DESC LIMIT ?").all(options.limit ?? 50)
|
|
169
|
+
: db.prepare("SELECT * FROM revisions WHERE id < ? ORDER BY id DESC LIMIT ?").all(resolveRevision(db, options.before), options.limit ?? 50);
|
|
170
|
+
return (rows as Raw[]).map((row) => withEvidence(db, toRevision(row)));
|
|
130
171
|
}
|
|
131
172
|
|
|
132
173
|
export interface RevisionChanges {
|
|
@@ -138,16 +179,22 @@ export interface RevisionChanges {
|
|
|
138
179
|
}
|
|
139
180
|
|
|
140
181
|
/** One revision and exactly what it changed. */
|
|
141
|
-
export function revision(db: DatabaseSync | undefined,
|
|
182
|
+
export function revision(db: DatabaseSync | undefined, text: string): RevisionChanges {
|
|
183
|
+
const id = resolveRevision(db, text);
|
|
142
184
|
const raw = db?.prepare("SELECT * FROM revisions WHERE id = ?").get(id) as Raw | undefined;
|
|
143
185
|
if (db === undefined || raw === undefined) throw new KnowledgeError("not-found", `There is no revision ${id}.`);
|
|
144
186
|
const tables = [
|
|
145
187
|
["symbol", "symbols", SYMBOL_COLUMNS, toSymbol],
|
|
188
|
+
["analyzer-name", "analyzer_names", SYMBOL_COLUMNS, toSymbol],
|
|
146
189
|
["region", "regions", REGION_COLUMNS, toRegion],
|
|
147
190
|
["comment", "comments", COMMENT_COLUMNS, toComment],
|
|
148
191
|
["reference", '"references"', REFERENCE_COLUMNS, toReference],
|
|
149
192
|
["suppression", "suppressions", SUPPRESSION_COLUMNS, toSuppression],
|
|
150
193
|
["module", "modules", MODULE_COLUMNS, toModule],
|
|
194
|
+
["operand", "operands", OPERAND_COLUMNS, toOperand],
|
|
195
|
+
["patch", "patches", PATCH_COLUMNS, toPatch],
|
|
196
|
+
["macro", "macros", MACRO_COLUMNS, toMacro],
|
|
197
|
+
["macro-use", "macro_uses", MACRO_USE_COLUMNS, toMacroUse],
|
|
151
198
|
] as const;
|
|
152
199
|
const added: HistoryEntry[] = [];
|
|
153
200
|
const closed: HistoryEntry[] = [];
|
|
@@ -161,3 +208,4 @@ export function revision(db: DatabaseSync | undefined, id: number): RevisionChan
|
|
|
161
208
|
}
|
|
162
209
|
return { revision: withEvidence(db, toRevision(raw)), added, closed };
|
|
163
210
|
}
|
|
211
|
+
|