@henols/c64-re-tools 2.0.0-rc.12 → 2.0.0-rc.13
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 +2 -0
- package/skills/c64-knowledge/SKILL.md +43 -5
- package/skills/c64-knowledge/scripts/knowledge.ts +227 -16
- package/skills/c64-reverse-engineering/SKILL.md +1 -0
- package/skills/c64-source/SKILL.md +29 -5
- package/skills/c64-source/scripts/generate.ts +203 -23
- package/skills/c64-source/scripts/loader.ts +3 -1
- package/skills/c64-source/scripts/opcodes.ts +1 -1
- package/skills/c64-source/scripts/source.ts +303 -25
- package/skills/c64-static-analysis/SKILL.md +2 -0
- package/skills/c64-static-analysis/scripts/analyze.ts +89 -2
- package/skills/c64-static-analysis/scripts/macros.ts +233 -0
- package/src/knowledge/conflicts.ts +12 -0
- package/src/knowledge/history.ts +30 -1
- package/src/knowledge/read.ts +128 -1
- package/src/knowledge/schema.ts +48 -2
- package/src/knowledge/text.ts +38 -11
- package/src/knowledge/write.ts +368 -6
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// Macro candidates (D73): runs of instructions that are a known 6502 idiom,
|
|
2
|
+
// such as a 16-bit increment, and that a macro can write as one line. They
|
|
3
|
+
// are only reported, never written: the LLM applies them as macro uses with
|
|
4
|
+
// c64-knowledge, and c64-source writes a use only when it gives the bytes of
|
|
5
|
+
// the image.
|
|
6
|
+
//
|
|
7
|
+
// The catalogue is fixed, so each candidate has a name and a body that is
|
|
8
|
+
// known to be correct. A search for any repeated sequence was weighed and not
|
|
9
|
+
// chosen: it gives many candidates without a name (D73).
|
|
10
|
+
|
|
11
|
+
import { instructionLength } from "#src/c64.ts";
|
|
12
|
+
import type { ImageByte } from "./coverage.ts";
|
|
13
|
+
|
|
14
|
+
/** A macro of the catalogue: its params and its ACME body, which names a param with a dot. */
|
|
15
|
+
export interface MacroIdiom {
|
|
16
|
+
name: string;
|
|
17
|
+
params: string[];
|
|
18
|
+
body: string;
|
|
19
|
+
comment: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const CATALOGUE: readonly MacroIdiom[] = [
|
|
23
|
+
{ name: "inc16", params: ["dst"], body: [" inc .dst", " bne +", " inc .dst+1", "+"].join("\n"), comment: "Adds 1 to the 16-bit value at dst." },
|
|
24
|
+
{ name: "dec16", params: ["dst"], body: [" lda .dst", " bne +", " dec .dst+1", "+ dec .dst"].join("\n"), comment: "Subtracts 1 from the 16-bit value at dst. It changes A." },
|
|
25
|
+
{
|
|
26
|
+
name: "add16_8",
|
|
27
|
+
params: ["dst", "value"],
|
|
28
|
+
body: [" clc", " lda .dst", " adc #.value", " sta .dst", " bcc +", " inc .dst+1", "+"].join("\n"),
|
|
29
|
+
comment: "Adds the 8-bit value to the 16-bit value at dst. It changes A.",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "add16",
|
|
33
|
+
params: ["dst", "value"],
|
|
34
|
+
body: [" clc", " lda .dst", " adc #<.value", " sta .dst", " lda .dst+1", " adc #>.value", " sta .dst+1"].join("\n"),
|
|
35
|
+
comment: "Adds the 16-bit value to the 16-bit value at dst. It changes A.",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "sub16",
|
|
39
|
+
params: ["dst", "value"],
|
|
40
|
+
body: [" sec", " lda .dst", " sbc #<.value", " sta .dst", " lda .dst+1", " sbc #>.value", " sta .dst+1"].join("\n"),
|
|
41
|
+
comment: "Subtracts the 16-bit value from the 16-bit value at dst. It changes A.",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "set16",
|
|
45
|
+
params: ["dst", "value"],
|
|
46
|
+
body: [" lda #<.value", " sta .dst", " lda #>.value", " sta .dst+1"].join("\n"),
|
|
47
|
+
comment: "Puts the 16-bit value at dst. It changes A.",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "mov16",
|
|
51
|
+
params: ["dst", "src"],
|
|
52
|
+
body: [" lda .src", " sta .dst", " lda .src+1", " sta .dst+1"].join("\n"),
|
|
53
|
+
comment: "Copies the 16-bit value at src to dst. It changes A.",
|
|
54
|
+
},
|
|
55
|
+
{ name: "wait_line", params: ["line"], body: [" lda #.line", "- cmp $d012", " bne -"].join("\n"), comment: "Waits until the raster line (bits 0-7) is line. It changes A." },
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
/** A run of instructions that one macro of the catalogue can write, with its args as ACME expressions. */
|
|
59
|
+
export interface MacroCandidate {
|
|
60
|
+
macro: string;
|
|
61
|
+
start: number;
|
|
62
|
+
end: number;
|
|
63
|
+
args: string[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** One instruction: its opcode, its length and its operand value. */
|
|
67
|
+
interface Instruction {
|
|
68
|
+
opcode: number;
|
|
69
|
+
length: number;
|
|
70
|
+
value: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** An address operand: zero page or absolute, of the same memory in one idiom. */
|
|
74
|
+
interface Operand {
|
|
75
|
+
value: number;
|
|
76
|
+
absolute: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Opcodes by mnemonic, zero page first, then absolute. */
|
|
80
|
+
const MEMORY = {
|
|
81
|
+
lda: [0xa5, 0xad],
|
|
82
|
+
sta: [0x85, 0x8d],
|
|
83
|
+
inc: [0xe6, 0xee],
|
|
84
|
+
dec: [0xc6, 0xce],
|
|
85
|
+
} as const;
|
|
86
|
+
const IMMEDIATE = { lda: 0xa9, adc: 0x69, sbc: 0xe9 } as const;
|
|
87
|
+
const CLC = 0x18;
|
|
88
|
+
const SEC = 0x38;
|
|
89
|
+
const BNE = 0xd0;
|
|
90
|
+
const BCC = 0x90;
|
|
91
|
+
const CMP_ABSOLUTE = 0xcd;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Names the args of a candidate: `address` gives the name of an address in
|
|
95
|
+
* the knowledge, or undefined. An absolute operand below $0100 keeps four
|
|
96
|
+
* digits, so ACME writes the absolute form again.
|
|
97
|
+
*/
|
|
98
|
+
export type Namer = (address: number) => string | undefined;
|
|
99
|
+
|
|
100
|
+
const hex2 = (value: number) => `$${value.toString(16).padStart(2, "0")}`;
|
|
101
|
+
const hex4 = (value: number) => `$${value.toString(16).padStart(4, "0")}`;
|
|
102
|
+
|
|
103
|
+
function addressArg(operand: Operand, name: Namer): string {
|
|
104
|
+
const named = name(operand.value);
|
|
105
|
+
// A name of a zero-page address gives the zero-page form: an absolute operand there keeps its number.
|
|
106
|
+
if (named !== undefined && (operand.value >= 0x100 || !operand.absolute)) return named;
|
|
107
|
+
return operand.absolute ? hex4(operand.value) : hex2(operand.value);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The memory operand of `instruction` when it is one of the opcodes of the mnemonic. */
|
|
111
|
+
function memory(instruction: Instruction | undefined, mnemonic: keyof typeof MEMORY): Operand | undefined {
|
|
112
|
+
if (instruction === undefined) return undefined;
|
|
113
|
+
const index = (MEMORY[mnemonic] as readonly number[]).indexOf(instruction.opcode);
|
|
114
|
+
return index < 0 ? undefined : { value: instruction.value, absolute: index === 1 };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const same = (a: Operand | undefined, b: Operand | undefined, offset = 0): boolean => a !== undefined && b !== undefined && a.absolute === b.absolute && b.value === a.value + offset;
|
|
118
|
+
const immediate = (instruction: Instruction | undefined, opcode: number) => (instruction?.opcode === opcode ? instruction.value : undefined);
|
|
119
|
+
/** A branch past the next instruction only: its distance is the length of that instruction. */
|
|
120
|
+
const skips = (branch: Instruction | undefined, opcode: number, next: Instruction | undefined) => branch?.opcode === opcode && next !== undefined && branch.value === next.length;
|
|
121
|
+
|
|
122
|
+
/** Each idiom: how many instructions it takes, and its args when the instructions match. */
|
|
123
|
+
const MATCHERS: Record<string, { count: number; match: (run: Instruction[], name: Namer) => string[] | undefined }> = {
|
|
124
|
+
inc16: {
|
|
125
|
+
count: 3,
|
|
126
|
+
match: ([low, branch, high], name) => {
|
|
127
|
+
const dst = memory(low, "inc");
|
|
128
|
+
return same(dst, memory(high, "inc"), 1) && skips(branch, BNE, high) ? [addressArg(dst!, name)] : undefined;
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
dec16: {
|
|
132
|
+
count: 4,
|
|
133
|
+
match: ([load, branch, high, low], name) => {
|
|
134
|
+
const dst = memory(load, "lda");
|
|
135
|
+
return same(dst, memory(low, "dec")) && same(dst, memory(high, "dec"), 1) && skips(branch, BNE, high) ? [addressArg(dst!, name)] : undefined;
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
add16_8: {
|
|
139
|
+
count: 6,
|
|
140
|
+
match: ([clc, load, add, store, branch, high], name) => {
|
|
141
|
+
const dst = memory(load, "lda");
|
|
142
|
+
const value = immediate(add, IMMEDIATE.adc);
|
|
143
|
+
const ok = clc?.opcode === CLC && value !== undefined && same(dst, memory(store, "sta")) && same(dst, memory(high, "inc"), 1) && skips(branch, BCC, high);
|
|
144
|
+
return ok ? [addressArg(dst!, name), hex2(value)] : undefined;
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
add16: { count: 7, match: (run, name) => wide(run, name, CLC, IMMEDIATE.adc) },
|
|
148
|
+
sub16: { count: 7, match: (run, name) => wide(run, name, SEC, IMMEDIATE.sbc) },
|
|
149
|
+
set16: {
|
|
150
|
+
count: 4,
|
|
151
|
+
match: ([loadLow, storeLow, loadHigh, storeHigh], name) => {
|
|
152
|
+
const low = immediate(loadLow, IMMEDIATE.lda);
|
|
153
|
+
const high = immediate(loadHigh, IMMEDIATE.lda);
|
|
154
|
+
const dst = memory(storeLow, "sta");
|
|
155
|
+
if (low === undefined || high === undefined || !same(dst, memory(storeHigh, "sta"), 1)) return undefined;
|
|
156
|
+
const value = low | (high << 8);
|
|
157
|
+
return [addressArg(dst!, name), name(value) ?? hex4(value)];
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
mov16: {
|
|
161
|
+
count: 4,
|
|
162
|
+
match: ([loadLow, storeLow, loadHigh, storeHigh], name) => {
|
|
163
|
+
const src = memory(loadLow, "lda");
|
|
164
|
+
const dst = memory(storeLow, "sta");
|
|
165
|
+
if (!same(src, memory(loadHigh, "lda"), 1) || !same(dst, memory(storeHigh, "sta"), 1)) return undefined;
|
|
166
|
+
return [addressArg(dst!, name), addressArg(src!, name)];
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
wait_line: {
|
|
170
|
+
count: 3,
|
|
171
|
+
match: ([load, compare, branch]) => {
|
|
172
|
+
const line = immediate(load, IMMEDIATE.lda);
|
|
173
|
+
// BNE back to the CMP: 5 bytes back from the end of the BNE.
|
|
174
|
+
return line !== undefined && compare?.opcode === CMP_ABSOLUTE && compare.value === 0xd012 && branch?.opcode === BNE && branch.value === 0xfb ? [hex2(line)] : undefined;
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/** add16 and sub16: the carry, then the low and the high byte, each with an immediate of the 16-bit value. */
|
|
180
|
+
function wide([carry, loadLow, low, storeLow, loadHigh, high, storeHigh]: Instruction[], name: Namer, carryOpcode: number, opcode: number): string[] | undefined {
|
|
181
|
+
const dst = memory(loadLow, "lda");
|
|
182
|
+
const lowValue = immediate(low, opcode);
|
|
183
|
+
const highValue = immediate(high, opcode);
|
|
184
|
+
const ok =
|
|
185
|
+
carry?.opcode === carryOpcode && lowValue !== undefined && highValue !== undefined && same(dst, memory(storeLow, "sta")) && same(dst, memory(loadHigh, "lda"), 1) && same(dst, memory(storeHigh, "sta"), 1);
|
|
186
|
+
return ok ? [addressArg(dst!, name), hex4(lowValue | (highValue << 8))] : undefined;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The instruction at `at`, or undefined when a byte of it is outside the range or the image. */
|
|
190
|
+
function instructionAt(byteAt: ImageByte, at: number, end: number): Instruction | undefined {
|
|
191
|
+
const opcode = byteAt(at);
|
|
192
|
+
if (opcode === undefined) return undefined;
|
|
193
|
+
const length = instructionLength(opcode);
|
|
194
|
+
if (at + length - 1 > end) return undefined;
|
|
195
|
+
const bytes = Array.from({ length: length - 1 }, (_, offset) => byteAt(at + 1 + offset));
|
|
196
|
+
if (bytes.some((byte) => byte === undefined)) return undefined;
|
|
197
|
+
return { opcode, length, value: length === 1 ? 0 : length === 2 ? bytes[0]! : bytes[0]! | (bytes[1]! << 8) };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Finds the macro candidates in code regions. Each region is read as
|
|
202
|
+
* instructions from its start; at each instruction the idioms of the
|
|
203
|
+
* catalogue are tried in order, and a match is a candidate. Candidates do not
|
|
204
|
+
* overlap: the scan goes on after the end of a match.
|
|
205
|
+
*/
|
|
206
|
+
export function findMacroUses(byteAt: ImageByte, regions: ReadonlyArray<{ start: number; end: number }>, name: Namer): MacroCandidate[] {
|
|
207
|
+
const candidates: MacroCandidate[] = [];
|
|
208
|
+
for (const region of [...regions].sort((a, b) => a.start - b.start)) {
|
|
209
|
+
const run: Array<Instruction & { at: number }> = [];
|
|
210
|
+
for (let at = region.start; at <= region.end; ) {
|
|
211
|
+
const instruction = instructionAt(byteAt, at, region.end);
|
|
212
|
+
if (instruction === undefined) break;
|
|
213
|
+
run.push({ ...instruction, at });
|
|
214
|
+
at += instruction.length;
|
|
215
|
+
}
|
|
216
|
+
for (let index = 0; index < run.length; ) {
|
|
217
|
+
const found = CATALOGUE.flatMap((idiom) => {
|
|
218
|
+
const matcher = MATCHERS[idiom.name]!;
|
|
219
|
+
const part = run.slice(index, index + matcher.count);
|
|
220
|
+
const args = part.length === matcher.count ? matcher.match(part, name) : undefined;
|
|
221
|
+
return args === undefined ? [] : [{ idiom, count: matcher.count, args }];
|
|
222
|
+
})[0];
|
|
223
|
+
if (found === undefined) {
|
|
224
|
+
index++;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const last = run[index + found.count - 1]!;
|
|
228
|
+
candidates.push({ macro: found.idiom.name, start: run[index]!.at, end: last.at + last.length - 1, args: found.args });
|
|
229
|
+
index += found.count;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return candidates;
|
|
233
|
+
}
|
|
@@ -9,15 +9,21 @@ import type { DatabaseSync } from "node:sqlite";
|
|
|
9
9
|
import type { HistoryEntry } from "./history.ts";
|
|
10
10
|
import {
|
|
11
11
|
COMMENT_COLUMNS,
|
|
12
|
+
MACRO_COLUMNS,
|
|
13
|
+
MACRO_USE_COLUMNS,
|
|
12
14
|
MODULE_COLUMNS,
|
|
13
15
|
OPERAND_COLUMNS,
|
|
16
|
+
PATCH_COLUMNS,
|
|
14
17
|
REFERENCE_COLUMNS,
|
|
15
18
|
REGION_COLUMNS,
|
|
16
19
|
SUPPRESSION_COLUMNS,
|
|
17
20
|
SYMBOL_COLUMNS,
|
|
18
21
|
toComment,
|
|
22
|
+
toMacro,
|
|
23
|
+
toMacroUse,
|
|
19
24
|
toModule,
|
|
20
25
|
toOperand,
|
|
26
|
+
toPatch,
|
|
21
27
|
toReference,
|
|
22
28
|
toRegion,
|
|
23
29
|
toSuppression,
|
|
@@ -39,6 +45,9 @@ export const CONFLICT_RULES = {
|
|
|
39
45
|
"suppression-symbol": "the same symbol suppression two times",
|
|
40
46
|
"suppression-reference": "the same reference suppression two times",
|
|
41
47
|
"suppression-region": "region suppressions that overlap",
|
|
48
|
+
patch: "patches that overlap",
|
|
49
|
+
macro: "two macros with one name",
|
|
50
|
+
"macro-use": "macro uses that overlap",
|
|
42
51
|
} as const;
|
|
43
52
|
export type ConflictRule = keyof typeof CONFLICT_RULES;
|
|
44
53
|
|
|
@@ -86,6 +95,9 @@ const RULES: readonly TableRule[] = [
|
|
|
86
95
|
where: "entity = 'reference'",
|
|
87
96
|
},
|
|
88
97
|
{ rule: "suppression-region", table: "suppressions", entity: "suppression", columns: SUPPRESSION_COLUMNS, map: toSuppression, key: "overlap", where: "entity = 'region'" },
|
|
98
|
+
{ rule: "patch", table: "patches", entity: "patch", columns: PATCH_COLUMNS, map: toPatch, key: "overlap" },
|
|
99
|
+
{ rule: "macro", table: "macros", entity: "macro", columns: MACRO_COLUMNS, map: toMacro, key: ["name"] },
|
|
100
|
+
{ rule: "macro-use", table: "macro_uses", entity: "macro-use", columns: MACRO_USE_COLUMNS, map: toMacroUse, key: "overlap" },
|
|
89
101
|
];
|
|
90
102
|
|
|
91
103
|
function conflictRow(rule: TableRule, raw: Raw): ConflictRow {
|
package/src/knowledge/history.ts
CHANGED
|
@@ -6,23 +6,32 @@ 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,
|
|
10
12
|
OPERAND_COLUMNS,
|
|
13
|
+
PATCH_COLUMNS,
|
|
11
14
|
REFERENCE_COLUMNS,
|
|
12
15
|
REGION_COLUMNS,
|
|
13
16
|
resolveRevision,
|
|
14
17
|
SUPPRESSION_COLUMNS,
|
|
15
18
|
SYMBOL_COLUMNS,
|
|
16
19
|
toComment,
|
|
20
|
+
toMacro,
|
|
21
|
+
toMacroUse,
|
|
17
22
|
toModule,
|
|
18
23
|
toOperand,
|
|
24
|
+
toPatch,
|
|
19
25
|
toReference,
|
|
20
26
|
toRegion,
|
|
21
27
|
toSuppression,
|
|
22
28
|
toSymbol,
|
|
23
29
|
type CommentRow,
|
|
30
|
+
type MacroRow,
|
|
31
|
+
type MacroUseRow,
|
|
24
32
|
type ModuleRow,
|
|
25
33
|
type OperandRow,
|
|
34
|
+
type PatchRow,
|
|
26
35
|
type ReferenceRow,
|
|
27
36
|
type RegionRow,
|
|
28
37
|
type SuppressionRow,
|
|
@@ -52,7 +61,10 @@ export type HistoryEntry =
|
|
|
52
61
|
| { entity: "reference"; row: ReferenceRow; from: string; to?: string }
|
|
53
62
|
| { entity: "suppression"; row: SuppressionRow; from: string; to?: string }
|
|
54
63
|
| { entity: "module"; row: ModuleRow; from: string; to?: string }
|
|
55
|
-
| { entity: "operand"; row: OperandRow; from: string; to?: string }
|
|
64
|
+
| { entity: "operand"; row: OperandRow; from: string; to?: string }
|
|
65
|
+
| { entity: "patch"; row: PatchRow; from: string; to?: string }
|
|
66
|
+
| { entity: "macro"; row: MacroRow; from: string; to?: string }
|
|
67
|
+
| { entity: "macro-use"; row: MacroUseRow; from: string; to?: string };
|
|
56
68
|
|
|
57
69
|
type Raw = Record<string, unknown>;
|
|
58
70
|
|
|
@@ -100,9 +112,23 @@ export function historyAt(db: DatabaseSync | undefined, address: number, space:
|
|
|
100
112
|
}
|
|
101
113
|
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));
|
|
102
114
|
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));
|
|
115
|
+
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[]) {
|
|
116
|
+
entries.push(entry("patch", toPatch(raw), raw));
|
|
117
|
+
}
|
|
118
|
+
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[]) {
|
|
119
|
+
entries.push(entry("macro-use", toMacroUse(raw), raw));
|
|
120
|
+
}
|
|
103
121
|
return entries.sort(bySequence);
|
|
104
122
|
}
|
|
105
123
|
|
|
124
|
+
/** Every row of a macro with this name, and every use of it, oldest first. */
|
|
125
|
+
export function historyOfMacro(db: DatabaseSync | undefined, name: string, space: Space = DEFAULT_SPACE): HistoryEntry[] {
|
|
126
|
+
if (db === undefined) return [];
|
|
127
|
+
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));
|
|
128
|
+
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));
|
|
129
|
+
return [...macros, ...uses].sort(bySequence);
|
|
130
|
+
}
|
|
131
|
+
|
|
106
132
|
/** Every symbol row of a space that ever had this name, oldest first: a name can move between addresses. */
|
|
107
133
|
export function historyNamed(db: DatabaseSync | undefined, name: string, space: Space = DEFAULT_SPACE): HistoryEntry[] {
|
|
108
134
|
if (db === undefined) return [];
|
|
@@ -158,6 +184,9 @@ export function revision(db: DatabaseSync | undefined, text: string): RevisionCh
|
|
|
158
184
|
["suppression", "suppressions", SUPPRESSION_COLUMNS, toSuppression],
|
|
159
185
|
["module", "modules", MODULE_COLUMNS, toModule],
|
|
160
186
|
["operand", "operands", OPERAND_COLUMNS, toOperand],
|
|
187
|
+
["patch", "patches", PATCH_COLUMNS, toPatch],
|
|
188
|
+
["macro", "macros", MACRO_COLUMNS, toMacro],
|
|
189
|
+
["macro-use", "macro_uses", MACRO_USE_COLUMNS, toMacroUse],
|
|
161
190
|
] as const;
|
|
162
191
|
const added: HistoryEntry[] = [];
|
|
163
192
|
const closed: HistoryEntry[] = [];
|
package/src/knowledge/read.ts
CHANGED
|
@@ -84,6 +84,47 @@ export interface OperandRow {
|
|
|
84
84
|
revision: string;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Code that replaces [start, end] when the build defines the switch as 1
|
|
89
|
+
* (D72). The code fits the range; NOP fills the rest. Empty code is only NOP.
|
|
90
|
+
*/
|
|
91
|
+
export interface PatchRow {
|
|
92
|
+
space: Space;
|
|
93
|
+
start: number;
|
|
94
|
+
end: number;
|
|
95
|
+
switch: string;
|
|
96
|
+
code: string;
|
|
97
|
+
comment?: string;
|
|
98
|
+
origin: Extract<Origin, "user" | "llm">;
|
|
99
|
+
revision: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** An ACME macro of the generated source (D73). The body names a parameter with a dot in front: .dst. */
|
|
103
|
+
export interface MacroRow {
|
|
104
|
+
space: Space;
|
|
105
|
+
name: string;
|
|
106
|
+
params: string[];
|
|
107
|
+
body: string;
|
|
108
|
+
comment?: string;
|
|
109
|
+
origin: Extract<Origin, "user" | "llm">;
|
|
110
|
+
revision: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The instructions in [start, end] written as one use of a macro, with one expression for each parameter (D73). */
|
|
114
|
+
export interface MacroUseRow {
|
|
115
|
+
space: Space;
|
|
116
|
+
start: number;
|
|
117
|
+
end: number;
|
|
118
|
+
macro: string;
|
|
119
|
+
args: string[];
|
|
120
|
+
origin: Extract<Origin, "user" | "llm">;
|
|
121
|
+
revision: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** A list of params or args as the knowledge stores it: the items with ", " between them. */
|
|
125
|
+
export const listText = (items: readonly string[]) => items.join(", ");
|
|
126
|
+
const items = (text: string) => (text.trim() === "" ? [] : text.split(",").map((item) => item.trim()));
|
|
127
|
+
|
|
87
128
|
type Raw = Record<string, unknown>;
|
|
88
129
|
|
|
89
130
|
export const SYMBOL_COLUMNS = "space, address, name, kind, origin, valid_from_revision";
|
|
@@ -93,6 +134,9 @@ export const REFERENCE_COLUMNS = "space, from_address, to_address, kind, origin,
|
|
|
93
134
|
export const SUPPRESSION_COLUMNS = "space, entity, start_address, end_address, to_address, kind, origin, valid_from_revision";
|
|
94
135
|
export const MODULE_COLUMNS = "space, start_address, file, title, origin, valid_from_revision";
|
|
95
136
|
export const OPERAND_COLUMNS = "space, address, selector, symbol, offset, expression, origin, valid_from_revision";
|
|
137
|
+
export const PATCH_COLUMNS = "space, start_address, end_address, switch_name, code, comment, origin, valid_from_revision";
|
|
138
|
+
export const MACRO_COLUMNS = "space, name, params, body, comment, origin, valid_from_revision";
|
|
139
|
+
export const MACRO_USE_COLUMNS = "space, start_address, end_address, macro, args, origin, valid_from_revision";
|
|
96
140
|
|
|
97
141
|
export function toSymbol(row: Raw): SymbolRow {
|
|
98
142
|
return { space: row.space as Space, address: row.address as number, name: row.name as string, kind: row.kind as SymbolKind, origin: row.origin as Origin, revision: row.valid_from_revision as string };
|
|
@@ -160,6 +204,38 @@ export function toOperand(row: Raw): OperandRow {
|
|
|
160
204
|
return operand;
|
|
161
205
|
}
|
|
162
206
|
|
|
207
|
+
export function toPatch(row: Raw): PatchRow {
|
|
208
|
+
const patch: PatchRow = {
|
|
209
|
+
space: row.space as Space,
|
|
210
|
+
start: row.start_address as number,
|
|
211
|
+
end: row.end_address as number,
|
|
212
|
+
switch: row.switch_name as string,
|
|
213
|
+
code: row.code as string,
|
|
214
|
+
origin: row.origin as PatchRow["origin"],
|
|
215
|
+
revision: row.valid_from_revision as string,
|
|
216
|
+
};
|
|
217
|
+
if (row.comment !== null && row.comment !== undefined) patch.comment = row.comment as string;
|
|
218
|
+
return patch;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function toMacro(row: Raw): MacroRow {
|
|
222
|
+
const macro: MacroRow = { space: row.space as Space, name: row.name as string, params: items(row.params as string), body: row.body as string, origin: row.origin as MacroRow["origin"], revision: row.valid_from_revision as string };
|
|
223
|
+
if (row.comment !== null && row.comment !== undefined) macro.comment = row.comment as string;
|
|
224
|
+
return macro;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function toMacroUse(row: Raw): MacroUseRow {
|
|
228
|
+
return {
|
|
229
|
+
space: row.space as Space,
|
|
230
|
+
start: row.start_address as number,
|
|
231
|
+
end: row.end_address as number,
|
|
232
|
+
macro: row.macro as string,
|
|
233
|
+
args: items(row.args as string),
|
|
234
|
+
origin: row.origin as MacroUseRow["origin"],
|
|
235
|
+
revision: row.valid_from_revision as string,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
163
239
|
/** The id of the newest revision, or "0" when the knowledge has none (D69). */
|
|
164
240
|
export function currentRevision(db: DatabaseSync | undefined): string {
|
|
165
241
|
if (db === undefined) return "0";
|
|
@@ -271,6 +347,29 @@ export function operandAt(db: DatabaseSync | undefined, address: number, space:
|
|
|
271
347
|
return row === undefined ? undefined : toOperand(row);
|
|
272
348
|
}
|
|
273
349
|
|
|
350
|
+
/** Current patches that share at least one address with [start, end], in address order. */
|
|
351
|
+
export function patchesOverlapping(db: DatabaseSync | undefined, start: number, end: number, space: Space = DEFAULT_SPACE): PatchRow[] {
|
|
352
|
+
if (db === undefined) return [];
|
|
353
|
+
return db
|
|
354
|
+
.prepare(`SELECT ${PATCH_COLUMNS} FROM current_patches WHERE space = ? AND start_address <= ? AND end_address >= ? ORDER BY start_address`)
|
|
355
|
+
.all(space, end, start)
|
|
356
|
+
.map((row) => toPatch(row as Raw));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Current macro uses that share at least one address with [start, end], in address order. */
|
|
360
|
+
export function macroUsesOverlapping(db: DatabaseSync | undefined, start: number, end: number, space: Space = DEFAULT_SPACE): MacroUseRow[] {
|
|
361
|
+
if (db === undefined) return [];
|
|
362
|
+
return db
|
|
363
|
+
.prepare(`SELECT ${MACRO_USE_COLUMNS} FROM current_macro_uses WHERE space = ? AND start_address <= ? AND end_address >= ? ORDER BY start_address`)
|
|
364
|
+
.all(space, end, start)
|
|
365
|
+
.map((row) => toMacroUse(row as Raw));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function macroNamed(db: DatabaseSync | undefined, name: string, space: Space = DEFAULT_SPACE): MacroRow | undefined {
|
|
369
|
+
const row = db?.prepare(`SELECT ${MACRO_COLUMNS} FROM current_macros WHERE space = ? AND name = ?`).get(space, name) as Raw | undefined;
|
|
370
|
+
return row === undefined ? undefined : toMacro(row);
|
|
371
|
+
}
|
|
372
|
+
|
|
274
373
|
/** Everything currently known at one address: the main lookup for skills. */
|
|
275
374
|
export interface AddressKnowledge {
|
|
276
375
|
space: Space;
|
|
@@ -283,6 +382,10 @@ export interface AddressKnowledge {
|
|
|
283
382
|
referencesTo: ReferenceRow[];
|
|
284
383
|
/** How the operand of the instruction here is written (D65). */
|
|
285
384
|
operand?: OperandRow;
|
|
385
|
+
/** The patch whose range holds the address (D72). */
|
|
386
|
+
patch?: PatchRow;
|
|
387
|
+
/** The macro use whose range holds the address (D73). */
|
|
388
|
+
macroUse?: MacroUseRow;
|
|
286
389
|
/** What user/LLM removals block here for the analyzers. */
|
|
287
390
|
suppressions: SuppressionRow[];
|
|
288
391
|
revision: string;
|
|
@@ -304,6 +407,10 @@ export function at(db: DatabaseSync | undefined, address: number, space: Space =
|
|
|
304
407
|
if (region !== undefined) result.region = region;
|
|
305
408
|
const operand = operandAt(db, address, space);
|
|
306
409
|
if (operand !== undefined) result.operand = operand;
|
|
410
|
+
const [patch] = patchesOverlapping(db, address, address, space);
|
|
411
|
+
if (patch !== undefined) result.patch = patch;
|
|
412
|
+
const [macroUse] = macroUsesOverlapping(db, address, address, space);
|
|
413
|
+
if (macroUse !== undefined) result.macroUse = macroUse;
|
|
307
414
|
return result;
|
|
308
415
|
}
|
|
309
416
|
|
|
@@ -384,12 +491,32 @@ export function listOperands(db: DatabaseSync | undefined, range: Range = ALL, o
|
|
|
384
491
|
.filter(kept(origins));
|
|
385
492
|
}
|
|
386
493
|
|
|
494
|
+
/** Current patches that share an address with the range, of one switch or of all, in address order. */
|
|
495
|
+
export function listPatches(db: DatabaseSync | undefined, range: Range = ALL, switchName?: string, origins?: OriginFilter, space: Space = DEFAULT_SPACE): PatchRow[] {
|
|
496
|
+
return patchesOverlapping(db, range.start, range.end, space).filter((patch) => (switchName === undefined || patch.switch === switchName) && kept(origins)(patch));
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** Current macros, by name. */
|
|
500
|
+
export function listMacros(db: DatabaseSync | undefined, origins?: OriginFilter, space: Space = DEFAULT_SPACE): MacroRow[] {
|
|
501
|
+
if (db === undefined) return [];
|
|
502
|
+
return db
|
|
503
|
+
.prepare(`SELECT ${MACRO_COLUMNS} FROM current_macros WHERE space = ? ORDER BY name`)
|
|
504
|
+
.all(space)
|
|
505
|
+
.map((row) => toMacro(row as Raw))
|
|
506
|
+
.filter(kept(origins));
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** Current macro uses that share an address with the range, of one macro or of all, in address order. */
|
|
510
|
+
export function listMacroUses(db: DatabaseSync | undefined, range: Range = ALL, macro?: string, origins?: OriginFilter, space: Space = DEFAULT_SPACE): MacroUseRow[] {
|
|
511
|
+
return macroUsesOverlapping(db, range.start, range.end, space).filter((use) => (macro === undefined || use.macro === macro) && kept(origins)(use));
|
|
512
|
+
}
|
|
513
|
+
|
|
387
514
|
/** The spaces that hold current knowledge, the C64 first. */
|
|
388
515
|
export function listSpaces(db: DatabaseSync | undefined): Space[] {
|
|
389
516
|
if (db === undefined) return [];
|
|
390
517
|
const rows = db
|
|
391
518
|
.prepare(
|
|
392
|
-
`SELECT space FROM current_symbols UNION SELECT space FROM current_regions UNION SELECT space FROM current_comments UNION SELECT space FROM current_references UNION SELECT space FROM current_suppressions UNION SELECT space FROM current_modules UNION SELECT space FROM current_operands`,
|
|
519
|
+
`SELECT space FROM current_symbols UNION SELECT space FROM current_regions UNION SELECT space FROM current_comments UNION SELECT space FROM current_references UNION SELECT space FROM current_suppressions UNION SELECT space FROM current_modules UNION SELECT space FROM current_operands UNION SELECT space FROM current_patches UNION SELECT space FROM current_macros UNION SELECT space FROM current_macro_uses`,
|
|
393
520
|
)
|
|
394
521
|
.all() as Array<{ space: Space }>;
|
|
395
522
|
const order = (space: Space) => (space === "c64" ? -2 : space === "drive8" ? -1 : Number(space.slice(4)));
|
package/src/knowledge/schema.ts
CHANGED
|
@@ -50,8 +50,14 @@ export type SuppressionEntity = (typeof SUPPRESSION_ENTITIES)[number];
|
|
|
50
50
|
|
|
51
51
|
/** A module file of the generated source (D48): a plain name that ends in .asm, no directory. */
|
|
52
52
|
export const MODULE_FILE = /^[A-Za-z0-9_][A-Za-z0-9_-]{0,59}\.asm$/;
|
|
53
|
+
/** A switch of the generated source (D72): upper case, so it reads as a build option and not as a label. */
|
|
54
|
+
export const SWITCH_NAME = /^[A-Z][A-Z0-9_]{1,31}$/;
|
|
55
|
+
/** A macro name and a macro parameter (D73). The body names a parameter with a dot in front: .dst. */
|
|
56
|
+
export const MACRO_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,31}$/;
|
|
57
|
+
export const MACRO_PARAM = /^[a-z_][a-z0-9_]{0,31}$/;
|
|
58
|
+
|
|
53
59
|
/** Files that c64-source writes itself: no module can have these names. */
|
|
54
|
-
export const GENERATED_FILES = ["main.asm", "loader.asm", "constants.asm"] as const;
|
|
60
|
+
export const GENERATED_FILES = ["main.asm", "loader.asm", "constants.asm", "macros.asm"] as const;
|
|
55
61
|
|
|
56
62
|
/** True for a generated file name in any letter case: the file systems of macOS and Windows ignore case. */
|
|
57
63
|
export function isGeneratedFile(file: string): boolean {
|
|
@@ -193,6 +199,43 @@ function createSchema(db: DatabaseSync): void {
|
|
|
193
199
|
);
|
|
194
200
|
CREATE INDEX operands_current ON operands (space, address) WHERE valid_to_revision IS NULL;
|
|
195
201
|
CREATE INDEX operands_address ON operands (space, address);
|
|
202
|
+
CREATE TABLE patches (
|
|
203
|
+
id TEXT PRIMARY KEY,
|
|
204
|
+
space TEXT NOT NULL DEFAULT 'c64' CHECK (${SPACE_CHECK}),
|
|
205
|
+
start_address INTEGER NOT NULL CHECK (start_address BETWEEN 0 AND 65535),
|
|
206
|
+
end_address INTEGER NOT NULL CHECK (end_address BETWEEN start_address AND 65535),
|
|
207
|
+
switch_name TEXT NOT NULL CHECK (length(switch_name) > 0),
|
|
208
|
+
code TEXT NOT NULL,
|
|
209
|
+
comment TEXT CHECK (comment IS NULL OR length(comment) > 0),
|
|
210
|
+
origin TEXT NOT NULL CHECK (origin IN ('user', 'llm')),
|
|
211
|
+
valid_from_revision TEXT NOT NULL REFERENCES revisions (id),
|
|
212
|
+
valid_to_revision TEXT REFERENCES revisions (id)
|
|
213
|
+
);
|
|
214
|
+
CREATE INDEX patches_start ON patches (space, start_address);
|
|
215
|
+
CREATE TABLE macros (
|
|
216
|
+
id TEXT PRIMARY KEY,
|
|
217
|
+
space TEXT NOT NULL DEFAULT 'c64' CHECK (${SPACE_CHECK}),
|
|
218
|
+
name TEXT NOT NULL CHECK (length(name) > 0),
|
|
219
|
+
params TEXT NOT NULL,
|
|
220
|
+
body TEXT NOT NULL CHECK (length(body) > 0),
|
|
221
|
+
comment TEXT CHECK (comment IS NULL OR length(comment) > 0),
|
|
222
|
+
origin TEXT NOT NULL CHECK (origin IN ('user', 'llm')),
|
|
223
|
+
valid_from_revision TEXT NOT NULL REFERENCES revisions (id),
|
|
224
|
+
valid_to_revision TEXT REFERENCES revisions (id)
|
|
225
|
+
);
|
|
226
|
+
CREATE INDEX macros_name ON macros (space, name);
|
|
227
|
+
CREATE TABLE macro_uses (
|
|
228
|
+
id TEXT PRIMARY KEY,
|
|
229
|
+
space TEXT NOT NULL DEFAULT 'c64' CHECK (${SPACE_CHECK}),
|
|
230
|
+
start_address INTEGER NOT NULL CHECK (start_address BETWEEN 0 AND 65535),
|
|
231
|
+
end_address INTEGER NOT NULL CHECK (end_address BETWEEN start_address AND 65535),
|
|
232
|
+
macro TEXT NOT NULL CHECK (length(macro) > 0),
|
|
233
|
+
args TEXT NOT NULL,
|
|
234
|
+
origin TEXT NOT NULL CHECK (origin IN ('user', 'llm')),
|
|
235
|
+
valid_from_revision TEXT NOT NULL REFERENCES revisions (id),
|
|
236
|
+
valid_to_revision TEXT REFERENCES revisions (id)
|
|
237
|
+
);
|
|
238
|
+
CREATE INDEX macro_uses_start ON macro_uses (space, start_address);
|
|
196
239
|
CREATE VIEW current_symbols AS SELECT * FROM symbols WHERE valid_to_revision IS NULL;
|
|
197
240
|
CREATE VIEW current_regions AS SELECT * FROM regions WHERE valid_to_revision IS NULL;
|
|
198
241
|
CREATE VIEW current_comments AS SELECT * FROM comments WHERE valid_to_revision IS NULL;
|
|
@@ -200,11 +243,14 @@ function createSchema(db: DatabaseSync): void {
|
|
|
200
243
|
CREATE VIEW current_suppressions AS SELECT * FROM suppressions WHERE valid_to_revision IS NULL;
|
|
201
244
|
CREATE VIEW current_modules AS SELECT * FROM modules WHERE valid_to_revision IS NULL;
|
|
202
245
|
CREATE VIEW current_operands AS SELECT * FROM operands WHERE valid_to_revision IS NULL;
|
|
246
|
+
CREATE VIEW current_patches AS SELECT * FROM patches WHERE valid_to_revision IS NULL;
|
|
247
|
+
CREATE VIEW current_macros AS SELECT * FROM macros WHERE valid_to_revision IS NULL;
|
|
248
|
+
CREATE VIEW current_macro_uses AS SELECT * FROM macro_uses WHERE valid_to_revision IS NULL;
|
|
203
249
|
`);
|
|
204
250
|
}
|
|
205
251
|
|
|
206
252
|
/** The version of the index this build makes. An index with another version is emptied and made again. */
|
|
207
|
-
export const INDEX_VERSION =
|
|
253
|
+
export const INDEX_VERSION = 2;
|
|
208
254
|
|
|
209
255
|
function indexVersion(db: DatabaseSync): number | undefined {
|
|
210
256
|
const hasMeta = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'meta'").get();
|