@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.
@@ -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 {
@@ -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[] = [];
@@ -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)));
@@ -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 = 1;
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();