@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
package/src/knowledge/text.ts
CHANGED
|
@@ -64,12 +64,12 @@ interface Field {
|
|
|
64
64
|
optional?: boolean;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
/** One fact table: its file, its fields in line order, and the column
|
|
67
|
+
/** One fact table: its file, its fields in line order, and the column that sorts its lines after the space (an address, or a name). */
|
|
68
68
|
export interface TableSpec {
|
|
69
69
|
table: string;
|
|
70
70
|
file: string;
|
|
71
71
|
fields: readonly Field[];
|
|
72
|
-
|
|
72
|
+
sort: string;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
const field = (key: string, column: string, format: Format, optional = false): Field => ({ key, column, format, ...(optional ? { optional } : {}) });
|
|
@@ -78,29 +78,29 @@ const origin = field("origin", "origin", "text");
|
|
|
78
78
|
|
|
79
79
|
/** The fact tables. Each line has `id`, these fields, then `added` and `closed` (the revisions; closed is null while the row is current). */
|
|
80
80
|
export const TABLES: readonly TableSpec[] = [
|
|
81
|
-
{ table: "symbols", file: "symbols.jsonl",
|
|
81
|
+
{ table: "symbols", file: "symbols.jsonl", sort: "address", fields: [space, field("address", "address", "address"), field("name", "name", "text"), field("kind", "kind", "text"), origin] },
|
|
82
82
|
{
|
|
83
83
|
table: "regions",
|
|
84
84
|
file: "regions.jsonl",
|
|
85
|
-
|
|
85
|
+
sort: "start_address",
|
|
86
86
|
fields: [space, field("start", "start_address", "address"), field("end", "end_address", "address"), field("type", "type", "text"), field("element", "element", "integer", true), origin],
|
|
87
87
|
},
|
|
88
88
|
{
|
|
89
89
|
table: "comments",
|
|
90
90
|
file: "comments.jsonl",
|
|
91
|
-
|
|
91
|
+
sort: "address",
|
|
92
92
|
fields: [space, field("address", "address", "address"), field("placement", "placement", "text"), field("text", "text", "text"), origin],
|
|
93
93
|
},
|
|
94
94
|
{
|
|
95
95
|
table: "references",
|
|
96
96
|
file: "references.jsonl",
|
|
97
|
-
|
|
97
|
+
sort: "from_address",
|
|
98
98
|
fields: [space, field("from", "from_address", "address"), field("to", "to_address", "address"), field("kind", "kind", "text"), origin],
|
|
99
99
|
},
|
|
100
100
|
{
|
|
101
101
|
table: "suppressions",
|
|
102
102
|
file: "suppressions.jsonl",
|
|
103
|
-
|
|
103
|
+
sort: "start_address",
|
|
104
104
|
fields: [
|
|
105
105
|
space,
|
|
106
106
|
field("entity", "entity", "text"),
|
|
@@ -114,13 +114,13 @@ export const TABLES: readonly TableSpec[] = [
|
|
|
114
114
|
{
|
|
115
115
|
table: "modules",
|
|
116
116
|
file: "modules.jsonl",
|
|
117
|
-
|
|
117
|
+
sort: "start_address",
|
|
118
118
|
fields: [space, field("start", "start_address", "address"), field("file", "file", "text"), field("title", "title", "text", true), origin],
|
|
119
119
|
},
|
|
120
120
|
{
|
|
121
121
|
table: "operands",
|
|
122
122
|
file: "operands.jsonl",
|
|
123
|
-
|
|
123
|
+
sort: "address",
|
|
124
124
|
fields: [
|
|
125
125
|
space,
|
|
126
126
|
field("address", "address", "address"),
|
|
@@ -131,6 +131,32 @@ export const TABLES: readonly TableSpec[] = [
|
|
|
131
131
|
origin,
|
|
132
132
|
],
|
|
133
133
|
},
|
|
134
|
+
{
|
|
135
|
+
table: "patches",
|
|
136
|
+
file: "patches.jsonl",
|
|
137
|
+
sort: "start_address",
|
|
138
|
+
fields: [
|
|
139
|
+
space,
|
|
140
|
+
field("start", "start_address", "address"),
|
|
141
|
+
field("end", "end_address", "address"),
|
|
142
|
+
field("switch", "switch_name", "text"),
|
|
143
|
+
field("code", "code", "text"),
|
|
144
|
+
field("comment", "comment", "text", true),
|
|
145
|
+
origin,
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
table: "macros",
|
|
150
|
+
file: "macros.jsonl",
|
|
151
|
+
sort: "name",
|
|
152
|
+
fields: [space, field("name", "name", "text"), field("params", "params", "text"), field("body", "body", "text"), field("comment", "comment", "text", true), origin],
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
table: "macro_uses",
|
|
156
|
+
file: "macro_uses.jsonl",
|
|
157
|
+
sort: "start_address",
|
|
158
|
+
fields: [space, field("start", "start_address", "address"), field("end", "end_address", "address"), field("macro", "macro", "text"), field("args", "args", "text"), origin],
|
|
159
|
+
},
|
|
134
160
|
];
|
|
135
161
|
|
|
136
162
|
export const REVISIONS_FILE = "revisions.jsonl";
|
|
@@ -167,12 +193,13 @@ function lineOf(raw: Raw, fields: readonly Field[], extra: Raw): string {
|
|
|
167
193
|
|
|
168
194
|
const spaceOrder = (space: string) => (space === "c64" ? -2 : space === "drive8" ? -1 : Number(space.slice(4)));
|
|
169
195
|
|
|
170
|
-
/** The sort key of a row: space, address, the revision that added it, its order in that revision. */
|
|
196
|
+
/** The sort key of a row: space, address (or name), the revision that added it, its order in that revision. */
|
|
171
197
|
function compareRows(spec: TableSpec) {
|
|
172
198
|
const order = (raw: Raw) => Number(ROW_ID.exec(raw.id as string)?.[2] ?? 0);
|
|
199
|
+
const key = (a: unknown, b: unknown) => (typeof a === "number" ? a - (b as number) : compareText(a as string, b as string));
|
|
173
200
|
return (a: Raw, b: Raw) =>
|
|
174
201
|
spaceOrder(a.space as string) - spaceOrder(b.space as string) ||
|
|
175
|
-
(a[spec.
|
|
202
|
+
key(a[spec.sort], b[spec.sort]) ||
|
|
176
203
|
compareText(a.valid_from_revision as string, b.valid_from_revision as string) ||
|
|
177
204
|
order(a) - order(b) ||
|
|
178
205
|
compareText(a.id as string, b.id as string);
|
package/src/knowledge/write.ts
CHANGED
|
@@ -12,12 +12,21 @@ import {
|
|
|
12
12
|
COMMENT_COLUMNS,
|
|
13
13
|
commentsAt,
|
|
14
14
|
currentRevision,
|
|
15
|
+
listMacroUses,
|
|
15
16
|
listOperands,
|
|
17
|
+
listPatches,
|
|
18
|
+
listText,
|
|
19
|
+
MACRO_COLUMNS,
|
|
20
|
+
MACRO_USE_COLUMNS,
|
|
21
|
+
macroNamed,
|
|
22
|
+
macroUsesOverlapping,
|
|
16
23
|
MODULE_COLUMNS,
|
|
17
24
|
moduleAt,
|
|
18
25
|
moduleWithFile,
|
|
19
26
|
OPERAND_COLUMNS,
|
|
20
27
|
operandAt,
|
|
28
|
+
PATCH_COLUMNS,
|
|
29
|
+
patchesOverlapping,
|
|
21
30
|
REFERENCE_COLUMNS,
|
|
22
31
|
referencesFrom,
|
|
23
32
|
REGION_COLUMNS,
|
|
@@ -29,15 +38,21 @@ import {
|
|
|
29
38
|
symbolAt,
|
|
30
39
|
symbolNamed,
|
|
31
40
|
toComment,
|
|
41
|
+
toMacro,
|
|
42
|
+
toMacroUse,
|
|
32
43
|
toModule,
|
|
33
44
|
toOperand,
|
|
45
|
+
toPatch,
|
|
34
46
|
toReference,
|
|
35
47
|
toRegion,
|
|
36
48
|
toSuppression,
|
|
37
49
|
toSymbol,
|
|
38
50
|
type CommentRow,
|
|
51
|
+
type MacroRow,
|
|
52
|
+
type MacroUseRow,
|
|
39
53
|
type ModuleRow,
|
|
40
54
|
type OperandRow,
|
|
55
|
+
type PatchRow,
|
|
41
56
|
type ReferenceRow,
|
|
42
57
|
type RegionRow,
|
|
43
58
|
type SuppressionRow,
|
|
@@ -51,7 +66,10 @@ import {
|
|
|
51
66
|
DEFAULT_SPACE,
|
|
52
67
|
GENERATED_FILES,
|
|
53
68
|
isGeneratedFile,
|
|
69
|
+
MACRO_NAME,
|
|
70
|
+
MACRO_PARAM,
|
|
54
71
|
SPACE,
|
|
72
|
+
SWITCH_NAME,
|
|
55
73
|
NO_ELEMENT_TYPES,
|
|
56
74
|
OPERAND_SELECTS,
|
|
57
75
|
POINTER_KIND,
|
|
@@ -85,11 +103,11 @@ export interface WriteContext {
|
|
|
85
103
|
expectedRevision?: string;
|
|
86
104
|
/** Kept with the revision when the write changes something. */
|
|
87
105
|
evidence?: Evidence[];
|
|
88
|
-
/** The image that an operand write is checked against (D65). */
|
|
106
|
+
/** The image that an operand, patch or macro use write is checked against (D65, D72, D73). */
|
|
89
107
|
image?: OperandImage;
|
|
90
108
|
}
|
|
91
109
|
|
|
92
|
-
/** The bytes of the image and the platform names (C64 space only) that operand expressions are checked against. */
|
|
110
|
+
/** The bytes of the image and the platform names (C64 space only) that operand expressions, patches and macro uses are checked against. */
|
|
93
111
|
export interface OperandImage {
|
|
94
112
|
byteAt(address: number): number | undefined;
|
|
95
113
|
platform: ReadonlyMap<string, number>;
|
|
@@ -105,6 +123,10 @@ export interface WriteResult<T, P = never> {
|
|
|
105
123
|
|
|
106
124
|
export { SYMBOL_NAME };
|
|
107
125
|
const MAX_COMMENT = 4000;
|
|
126
|
+
/** The most characters of patch code and of a macro body. */
|
|
127
|
+
const MAX_CODE = 8000;
|
|
128
|
+
/** The most parameters of a macro. */
|
|
129
|
+
const MAX_PARAMS = 16;
|
|
108
130
|
const MAX_EVIDENCE_REF = 1000;
|
|
109
131
|
|
|
110
132
|
export function checkAddress(value: number, what = "address"): void {
|
|
@@ -249,6 +271,24 @@ export class Change {
|
|
|
249
271
|
.run(this.#rowId(), row.space, row.address, row.select, row.symbol ?? null, row.offset, row.expression ?? null, row.origin, this.id());
|
|
250
272
|
}
|
|
251
273
|
|
|
274
|
+
insertPatch(row: Omit<PatchRow, "revision">): void {
|
|
275
|
+
this.#db
|
|
276
|
+
.prepare("INSERT INTO patches (id, space, start_address, end_address, switch_name, code, comment, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
|
277
|
+
.run(this.#rowId(), row.space, row.start, row.end, row.switch, row.code, row.comment ?? null, row.origin, this.id());
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
insertMacro(row: Omit<MacroRow, "revision">): void {
|
|
281
|
+
this.#db
|
|
282
|
+
.prepare("INSERT INTO macros (id, space, name, params, body, comment, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
|
|
283
|
+
.run(this.#rowId(), row.space, row.name, listText(row.params), row.body, row.comment ?? null, row.origin, this.id());
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
insertMacroUse(row: Omit<MacroUseRow, "revision">): void {
|
|
287
|
+
this.#db
|
|
288
|
+
.prepare("INSERT INTO macro_uses (id, space, start_address, end_address, macro, args, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
|
|
289
|
+
.run(this.#rowId(), row.space, row.start, row.end, row.macro, listText(row.args), row.origin, this.id());
|
|
290
|
+
}
|
|
291
|
+
|
|
252
292
|
insertSuppression(row: Omit<SuppressionRow, "revision">): void {
|
|
253
293
|
this.#db
|
|
254
294
|
.prepare("INSERT INTO suppressions (id, space, entity, start_address, end_address, to_address, kind, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
|
@@ -259,6 +299,7 @@ export class Change {
|
|
|
259
299
|
/** A conflict in words: its rule, its space and the addresses of its rows. */
|
|
260
300
|
function describe(conflict: Conflict): string {
|
|
261
301
|
const place = (entry: ConflictRow) => {
|
|
302
|
+
if (entry.entity === "macro") return entry.row.name;
|
|
262
303
|
const row = entry.row as { address?: number; start?: number; end?: number; from?: number };
|
|
263
304
|
if (row.address !== undefined) return hex(row.address);
|
|
264
305
|
if (row.from !== undefined && entry.entity === "reference") return hex(row.from);
|
|
@@ -346,9 +387,14 @@ function renameStep(db: DatabaseSync, change: Change, origin: Origin, request: S
|
|
|
346
387
|
if (owner !== undefined && owner.address !== request.address) {
|
|
347
388
|
throw new KnowledgeError("conflict", `The name ${request.name} already belongs to ${hex(owner.address)}. Rename or remove that symbol first.`);
|
|
348
389
|
}
|
|
390
|
+
const [patch] = listPatches(db, undefined, request.name, undefined, space);
|
|
391
|
+
if (patch !== undefined) throw new KnowledgeError("conflict", `${request.name} is the switch of the patch at ${hex(patch.start)}-${hex(patch.end)}. Give the symbol another name.`);
|
|
349
392
|
if (existing !== undefined) change.close("symbols", "space = ? AND address = ?", space, request.address);
|
|
350
393
|
change.insertSymbol({ space, address: request.address, name: request.name, kind, origin });
|
|
351
|
-
if (existing !== undefined && existing.name !== request.name)
|
|
394
|
+
if (existing !== undefined && existing.name !== request.name) {
|
|
395
|
+
renameInOperands(db, change, space, existing.name, request.name);
|
|
396
|
+
renameInUses(db, change, space, existing.name, request.name);
|
|
397
|
+
}
|
|
352
398
|
change.close("suppressions", "space = ? AND entity = 'symbol' AND start_address = ?", space, request.address);
|
|
353
399
|
const current = symbolAt(db, request.address, space)!;
|
|
354
400
|
return existing !== undefined && existing.origin !== origin ? { current, previous: existing } : { current };
|
|
@@ -365,6 +411,10 @@ function removeSymbolStep(db: DatabaseSync, change: Change, origin: Origin, requ
|
|
|
365
411
|
`The operand expressions at ${users.map((row) => hex(row.address)).join(", ")} use ${existing.name}. Change or remove them first (unoperand), or rename the symbol.`,
|
|
366
412
|
);
|
|
367
413
|
}
|
|
414
|
+
const uses = usesNaming(db, space, existing.name);
|
|
415
|
+
if (uses.length > 0) {
|
|
416
|
+
throw new KnowledgeError("conflict", `The macro uses at ${uses.map((row) => hex(row.start)).join(", ")} use ${existing.name}. Change or remove them first (unuse), or rename the symbol.`);
|
|
417
|
+
}
|
|
368
418
|
change.close("symbols", "space = ? AND address = ?", space, request.address);
|
|
369
419
|
change.insertSuppression({ space, entity: "symbol", start: request.address, end: request.address, origin: origin as SuppressionRow["origin"] });
|
|
370
420
|
return { current: null };
|
|
@@ -634,6 +684,222 @@ function unmoduleStep(db: DatabaseSync, change: Change, _origin: Origin, request
|
|
|
634
684
|
return { current: null };
|
|
635
685
|
}
|
|
636
686
|
|
|
687
|
+
/**
|
|
688
|
+
* Refuses ACME text whose braces do not pair outside comments and strings:
|
|
689
|
+
* c64-source writes patch code and a macro body inside { }.
|
|
690
|
+
*/
|
|
691
|
+
function checkBraces(text: string, what: string): void {
|
|
692
|
+
let depth = 0;
|
|
693
|
+
for (const line of text.split("\n")) {
|
|
694
|
+
let quote: string | undefined;
|
|
695
|
+
for (let index = 0; index < line.length; index++) {
|
|
696
|
+
const character = line[index]!;
|
|
697
|
+
if (quote !== undefined) {
|
|
698
|
+
if (character === "\\") index++;
|
|
699
|
+
else if (character === quote) quote = undefined;
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
if (character === ";") break;
|
|
703
|
+
if (character === '"' || character === "'") quote = character;
|
|
704
|
+
else if (character === "{") depth++;
|
|
705
|
+
else if (character === "}" && --depth < 0) throw new KnowledgeError("invalid-input", `${what} has a } with no { before it.`);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
if (depth !== 0) throw new KnowledgeError("invalid-input", `${what} has a { with no }.`);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function checkCommentText(comment: string | undefined, what: string): void {
|
|
712
|
+
if (comment !== undefined && (comment.trim().length === 0 || comment.length > MAX_COMMENT)) throw new KnowledgeError("invalid-input", `${what} has 1 to ${MAX_COMMENT} characters.`);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/** Refuses a range with a byte outside the image. */
|
|
716
|
+
function checkInImage(image: OperandImage, start: number, end: number, what: string): void {
|
|
717
|
+
for (let address = start; address <= end; address++) {
|
|
718
|
+
if (image.byteAt(address) === undefined) throw new KnowledgeError("invalid-input", `${hex(address)} is outside the image: ${what} is a range of the image.`);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
type PatchRequest = InSpace & { start: number; end: number; switch: string; code: string; comment?: string };
|
|
723
|
+
|
|
724
|
+
function checkPatch(request: PatchRequest): void {
|
|
725
|
+
checkRange(request);
|
|
726
|
+
if (typeof request.switch !== "string" || !SWITCH_NAME.test(request.switch)) {
|
|
727
|
+
throw new KnowledgeError("invalid-input", "A switch name is an upper-case letter, then 1 to 31 upper-case letters, digits or underscores, for example CRACK.");
|
|
728
|
+
}
|
|
729
|
+
if (typeof request.code !== "string" || request.code.length > MAX_CODE) throw new KnowledgeError("invalid-input", `Patch code has at most ${MAX_CODE} characters; empty code fills the range with NOP.`);
|
|
730
|
+
checkBraces(request.code, "The patch code");
|
|
731
|
+
checkCommentText(request.comment, "A patch comment");
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function patchStep(db: DatabaseSync, change: Change, context: ChangeContext & { image?: OperandImage }, request: PatchRequest): Step<PatchRow, PatchRow> {
|
|
735
|
+
const space = spaceOf(request);
|
|
736
|
+
const range = `${hex(request.start)}-${hex(request.end)}`;
|
|
737
|
+
if (context.image === undefined) throw new KnowledgeError("invalid-input", "A patch is checked against the image, and no image was given.");
|
|
738
|
+
checkInImage(context.image, request.start, request.end, "a patch");
|
|
739
|
+
const symbol = symbolNamed(db, request.switch, space);
|
|
740
|
+
if (symbol !== undefined) throw new KnowledgeError("conflict", `The switch name ${request.switch} is the name of the symbol at ${hex(symbol.address)}. Give the switch another name.`);
|
|
741
|
+
if (space === DEFAULT_SPACE && context.image.platform.has(request.switch)) throw new KnowledgeError("conflict", `The switch name ${request.switch} is a C64 platform name. Give the switch another name.`);
|
|
742
|
+
const other = patchesOverlapping(db, request.start, request.end, space).find((row) => row.start !== request.start);
|
|
743
|
+
if (other !== undefined) {
|
|
744
|
+
throw new KnowledgeError("conflict", `The patch ${range} overlaps the patch of ${other.switch} at ${hex(other.start)}-${hex(other.end)}. Change or remove that patch first (unpatch).`);
|
|
745
|
+
}
|
|
746
|
+
const [use] = macroUsesOverlapping(db, request.start, request.end, space);
|
|
747
|
+
if (use !== undefined) {
|
|
748
|
+
throw new KnowledgeError("conflict", `The patch ${range} overlaps the use of the macro ${use.macro} at ${hex(use.start)}-${hex(use.end)}. Remove that use first (unuse).`);
|
|
749
|
+
}
|
|
750
|
+
const existing = patchesOverlapping(db, request.start, request.start, space).find((row) => row.start === request.start);
|
|
751
|
+
const code = request.code.trimEnd();
|
|
752
|
+
// A patch write without a comment keeps the comment it has, as a module keeps its title.
|
|
753
|
+
const comment = request.comment === undefined ? existing?.comment : request.comment.trim();
|
|
754
|
+
if (existing !== undefined && existing.end === request.end && existing.switch === request.switch && existing.code === code && existing.comment === comment) return { current: existing };
|
|
755
|
+
const origin = context.origin as PatchRow["origin"];
|
|
756
|
+
if (existing !== undefined) change.close("patches", "space = ? AND start_address = ?", space, request.start);
|
|
757
|
+
change.insertPatch({ space, start: request.start, end: request.end, switch: request.switch, code, ...(comment === undefined ? {} : { comment }), origin });
|
|
758
|
+
const current = patchesOverlapping(db, request.start, request.start, space).find((row) => row.start === request.start)!;
|
|
759
|
+
return existing !== undefined && existing.origin !== origin ? { current, previous: existing } : { current };
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function unpatchStep(db: DatabaseSync, change: Change, _origin: Origin, request: InSpace & { start: number }): Step<null> {
|
|
763
|
+
const space = spaceOf(request);
|
|
764
|
+
if (!patchesOverlapping(db, request.start, request.start, space).some((row) => row.start === request.start)) throw new KnowledgeError("not-found", `No patch starts at ${hex(request.start)}.`);
|
|
765
|
+
change.close("patches", "space = ? AND start_address = ?", space, request.start);
|
|
766
|
+
return { current: null };
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
type MacroRequest = InSpace & { name: string; params?: string[]; body: string; comment?: string };
|
|
770
|
+
|
|
771
|
+
function checkMacroName(name: unknown, what = "A macro name"): void {
|
|
772
|
+
if (typeof name !== "string" || !MACRO_NAME.test(name)) throw new KnowledgeError("invalid-input", `${what} is a letter or underscore, then at most 31 letters, digits or underscores.`);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function checkMacro(request: MacroRequest): void {
|
|
776
|
+
checkSpace(request);
|
|
777
|
+
checkMacroName(request.name);
|
|
778
|
+
const params = request.params ?? [];
|
|
779
|
+
if (!Array.isArray(params) || params.length > MAX_PARAMS) throw new KnowledgeError("invalid-input", `A macro has 0 to ${MAX_PARAMS} params.`);
|
|
780
|
+
for (const param of params) {
|
|
781
|
+
if (typeof param !== "string" || !MACRO_PARAM.test(param)) {
|
|
782
|
+
throw new KnowledgeError("invalid-input", "A macro param is a lower-case letter or underscore, then lower-case letters, digits or underscores, without a dot. The body names it with a dot: .dst.");
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
if (new Set(params).size !== params.length) throw new KnowledgeError("invalid-input", "The params of a macro have different names.");
|
|
786
|
+
if (typeof request.body !== "string" || request.body.trim().length === 0 || request.body.length > MAX_CODE) throw new KnowledgeError("invalid-input", `A macro body has 1 to ${MAX_CODE} characters.`);
|
|
787
|
+
checkBraces(request.body, "The macro body");
|
|
788
|
+
checkCommentText(request.comment, "A macro comment");
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function macroStep(db: DatabaseSync, change: Change, origin: Origin, request: MacroRequest): Step<MacroRow, MacroRow> {
|
|
792
|
+
const space = spaceOf(request);
|
|
793
|
+
const existing = macroNamed(db, request.name, space);
|
|
794
|
+
const params = request.params ?? [];
|
|
795
|
+
const body = request.body.trimEnd();
|
|
796
|
+
const comment = request.comment === undefined ? existing?.comment : request.comment.trim();
|
|
797
|
+
if (existing !== undefined && listText(existing.params) === listText(params) && existing.body === body && existing.comment === comment) return { current: existing };
|
|
798
|
+
if (existing !== undefined && existing.params.length !== params.length) {
|
|
799
|
+
const uses = listMacroUses(db, undefined, request.name, undefined, space);
|
|
800
|
+
if (uses.length > 0) {
|
|
801
|
+
throw new KnowledgeError(
|
|
802
|
+
"conflict",
|
|
803
|
+
`The macro ${request.name} has ${existing.params.length} params, and its uses at ${uses.map((row) => hex(row.start)).join(", ")} give that many args. Remove the uses first (unuse), or keep the number of params.`,
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
if (existing !== undefined) change.close("macros", "space = ? AND name = ?", space, request.name);
|
|
808
|
+
change.insertMacro({ space, name: request.name, params, body, ...(comment === undefined ? {} : { comment }), origin: origin as MacroRow["origin"] });
|
|
809
|
+
const current = macroNamed(db, request.name, space)!;
|
|
810
|
+
return existing !== undefined && existing.origin !== origin ? { current, previous: existing } : { current };
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function unmacroStep(db: DatabaseSync, change: Change, _origin: Origin, request: InSpace & { name: string }): Step<null> {
|
|
814
|
+
const space = spaceOf(request);
|
|
815
|
+
if (macroNamed(db, request.name, space) === undefined) throw new KnowledgeError("not-found", `There is no macro ${request.name}.`);
|
|
816
|
+
const uses = listMacroUses(db, undefined, request.name, undefined, space);
|
|
817
|
+
if (uses.length > 0) throw new KnowledgeError("conflict", `The macro ${request.name} has uses at ${uses.map((row) => hex(row.start)).join(", ")}. Remove them first (unuse).`);
|
|
818
|
+
change.close("macros", "space = ? AND name = ?", space, request.name);
|
|
819
|
+
return { current: null };
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
type MacroUseRequest = InSpace & { start: number; end: number; macro: string; args?: string[] };
|
|
823
|
+
|
|
824
|
+
function checkMacroUse(request: MacroUseRequest): void {
|
|
825
|
+
checkRange(request);
|
|
826
|
+
checkMacroName(request.macro, "The macro of a use");
|
|
827
|
+
const args = request.args ?? [];
|
|
828
|
+
if (!Array.isArray(args) || args.length > MAX_PARAMS) throw new KnowledgeError("invalid-input", `A macro use has 0 to ${MAX_PARAMS} args.`);
|
|
829
|
+
for (const arg of args) {
|
|
830
|
+
if (typeof arg !== "string") throw new KnowledgeError("invalid-input", "Each arg of a macro use is expression text.");
|
|
831
|
+
try {
|
|
832
|
+
checkExpression(arg);
|
|
833
|
+
} catch (error) {
|
|
834
|
+
if (error instanceof ExpressionError) throw new KnowledgeError("invalid-input", error.message);
|
|
835
|
+
throw error;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/** Current macro uses that name a symbol in an arg. */
|
|
841
|
+
function usesNaming(db: DatabaseSync, space: Space, name: string): MacroUseRow[] {
|
|
842
|
+
return listMacroUses(db, undefined, undefined, undefined, space).filter((row) => row.args.some((arg) => identifiers(arg).includes(name)));
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/** A rename of a symbol renames it in the args of every macro use; the rows keep their origin. */
|
|
846
|
+
function renameInUses(db: DatabaseSync, change: Change, space: Space, from: string, to: string): void {
|
|
847
|
+
for (const row of usesNaming(db, space, from)) {
|
|
848
|
+
change.close("macro_uses", "space = ? AND start_address = ?", space, row.start);
|
|
849
|
+
change.insertMacroUse({ ...row, args: row.args.map((arg) => replaceIdentifiers(arg, (name) => (name === from ? to : name))) });
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function useStep(db: DatabaseSync, change: Change, context: ChangeContext & { image?: OperandImage }, request: MacroUseRequest): Step<MacroUseRow, MacroUseRow> {
|
|
854
|
+
const space = spaceOf(request);
|
|
855
|
+
const image = context.image;
|
|
856
|
+
const range = `${hex(request.start)}-${hex(request.end)}`;
|
|
857
|
+
if (image === undefined) throw new KnowledgeError("invalid-input", "A macro use is checked against the image, and no image was given.");
|
|
858
|
+
const macro = macroNamed(db, request.macro, space);
|
|
859
|
+
if (macro === undefined) throw new KnowledgeError("not-found", `There is no macro ${request.macro}. Write it first (macro).`);
|
|
860
|
+
const args = (request.args ?? []).map((arg) => arg.trim());
|
|
861
|
+
if (args.length !== macro.params.length) {
|
|
862
|
+
throw new KnowledgeError("invalid-input", `The macro ${macro.name} has ${macro.params.length} params (${listText(macro.params)}), and the use gives ${args.length} args.`);
|
|
863
|
+
}
|
|
864
|
+
const resolve = (name: string) => symbolNamed(db, name, space)?.address ?? (space === DEFAULT_SPACE ? image.platform.get(name) : undefined);
|
|
865
|
+
for (const arg of args) {
|
|
866
|
+
try {
|
|
867
|
+
evaluate(arg, resolve);
|
|
868
|
+
} catch (error) {
|
|
869
|
+
if (error instanceof ExpressionError) throw new KnowledgeError("invalid-input", `${range}: ${error.message}`);
|
|
870
|
+
throw error;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
checkInImage(image, request.start, request.end, "a macro use");
|
|
874
|
+
let next = request.start;
|
|
875
|
+
while (next <= request.end) next += instructionLength(image.byteAt(next)!);
|
|
876
|
+
if (next !== request.end + 1) {
|
|
877
|
+
throw new KnowledgeError("invalid-input", `A macro use is a run of whole instructions, but the instructions from ${hex(request.start)} end at ${hex(next - 1)}, not at ${hex(request.end)}.`);
|
|
878
|
+
}
|
|
879
|
+
const [patch] = patchesOverlapping(db, request.start, request.end, space);
|
|
880
|
+
if (patch !== undefined) {
|
|
881
|
+
throw new KnowledgeError("conflict", `The macro use ${range} overlaps the patch of ${patch.switch} at ${hex(patch.start)}-${hex(patch.end)}. Change or remove that patch first (unpatch).`);
|
|
882
|
+
}
|
|
883
|
+
const other = macroUsesOverlapping(db, request.start, request.end, space).find((row) => row.start !== request.start);
|
|
884
|
+
if (other !== undefined) {
|
|
885
|
+
throw new KnowledgeError("conflict", `The macro use ${range} overlaps the use of ${other.macro} at ${hex(other.start)}-${hex(other.end)}. Remove that use first (unuse).`);
|
|
886
|
+
}
|
|
887
|
+
const existing = macroUsesOverlapping(db, request.start, request.start, space).find((row) => row.start === request.start);
|
|
888
|
+
if (existing !== undefined && existing.end === request.end && existing.macro === request.macro && listText(existing.args) === listText(args)) return { current: existing };
|
|
889
|
+
const origin = context.origin as MacroUseRow["origin"];
|
|
890
|
+
if (existing !== undefined) change.close("macro_uses", "space = ? AND start_address = ?", space, request.start);
|
|
891
|
+
change.insertMacroUse({ space, start: request.start, end: request.end, macro: request.macro, args, origin });
|
|
892
|
+
const current = macroUsesOverlapping(db, request.start, request.start, space).find((row) => row.start === request.start)!;
|
|
893
|
+
return existing !== undefined && existing.origin !== origin ? { current, previous: existing } : { current };
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function unuseStep(db: DatabaseSync, change: Change, _origin: Origin, request: InSpace & { start: number }): Step<null> {
|
|
897
|
+
const space = spaceOf(request);
|
|
898
|
+
if (!macroUsesOverlapping(db, request.start, request.start, space).some((row) => row.start === request.start)) throw new KnowledgeError("not-found", `No macro use starts at ${hex(request.start)}.`);
|
|
899
|
+
change.close("macro_uses", "space = ? AND start_address = ?", space, request.start);
|
|
900
|
+
return { current: null };
|
|
901
|
+
}
|
|
902
|
+
|
|
637
903
|
/** Names the symbol at an address (one current symbol per address, unique names). */
|
|
638
904
|
export function renameSymbol(db: DatabaseSync, context: WriteContext, request: SymbolRequest): WriteResult<SymbolRow, SymbolRow> {
|
|
639
905
|
checkSymbol(request);
|
|
@@ -705,6 +971,46 @@ export function removeOperand(db: DatabaseSync, context: WriteContext, request:
|
|
|
705
971
|
return result(write(db, context, "remove-operand", (change) => unoperandStep(db, change, context.origin, request)));
|
|
706
972
|
}
|
|
707
973
|
|
|
974
|
+
/**
|
|
975
|
+
* Writes code that replaces [start, end] when the build defines the switch
|
|
976
|
+
* (D72), after a check that the range is in the image and overlaps no other
|
|
977
|
+
* patch or macro use.
|
|
978
|
+
*/
|
|
979
|
+
export function setPatch(db: DatabaseSync, context: WriteContext, request: PatchRequest): WriteResult<PatchRow, PatchRow> {
|
|
980
|
+
checkPatch(request);
|
|
981
|
+
return result(write(db, context, "set-patch", (change) => patchStep(db, change, context, request)));
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
export function removePatch(db: DatabaseSync, context: WriteContext, request: InSpace & { start: number }): WriteResult<null> {
|
|
985
|
+
checkSpace(request);
|
|
986
|
+
checkAddress(request.start, "start");
|
|
987
|
+
return result(write(db, context, "remove-patch", (change) => unpatchStep(db, change, context.origin, request)));
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/** Writes a macro of the generated source (D73). */
|
|
991
|
+
export function setMacro(db: DatabaseSync, context: WriteContext, request: MacroRequest): WriteResult<MacroRow, MacroRow> {
|
|
992
|
+
checkMacro(request);
|
|
993
|
+
return result(write(db, context, "set-macro", (change) => macroStep(db, change, context.origin, request)));
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
export function removeMacro(db: DatabaseSync, context: WriteContext, request: InSpace & { name: string }): WriteResult<null> {
|
|
997
|
+
checkSpace(request);
|
|
998
|
+
checkMacroName(request.name);
|
|
999
|
+
return result(write(db, context, "remove-macro", (change) => unmacroStep(db, change, context.origin, request)));
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/** Writes the instructions in [start, end] as one use of a macro (D73), after a check against the image. */
|
|
1003
|
+
export function setMacroUse(db: DatabaseSync, context: WriteContext, request: MacroUseRequest): WriteResult<MacroUseRow, MacroUseRow> {
|
|
1004
|
+
checkMacroUse(request);
|
|
1005
|
+
return result(write(db, context, "set-macro-use", (change) => useStep(db, change, context, request)));
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
export function removeMacroUse(db: DatabaseSync, context: WriteContext, request: InSpace & { start: number }): WriteResult<null> {
|
|
1009
|
+
checkSpace(request);
|
|
1010
|
+
checkAddress(request.start, "start");
|
|
1011
|
+
return result(write(db, context, "remove-macro-use", (change) => unuseStep(db, change, context.origin, request)));
|
|
1012
|
+
}
|
|
1013
|
+
|
|
708
1014
|
/** One write of a batch (D42), named as the command that does it alone. */
|
|
709
1015
|
export type BatchOperation =
|
|
710
1016
|
| ({ op: "rename" } & SymbolRequest)
|
|
@@ -718,12 +1024,21 @@ export type BatchOperation =
|
|
|
718
1024
|
| ({ op: "module" } & ModuleRequest)
|
|
719
1025
|
| ({ op: "unmodule" } & InSpace & { start: number })
|
|
720
1026
|
| ({ op: "operand" } & OperandRequest)
|
|
721
|
-
| ({ op: "unoperand" } & AddressRequest)
|
|
1027
|
+
| ({ op: "unoperand" } & AddressRequest)
|
|
1028
|
+
| ({ op: "patch" } & PatchRequest)
|
|
1029
|
+
| ({ op: "unpatch" } & InSpace & { start: number })
|
|
1030
|
+
| ({ op: "macro" } & MacroRequest)
|
|
1031
|
+
| ({ op: "unmacro" } & InSpace & { name: string })
|
|
1032
|
+
| ({ op: "use" } & MacroUseRequest)
|
|
1033
|
+
| ({ op: "unuse" } & InSpace & { start: number });
|
|
722
1034
|
|
|
723
1035
|
export const MAX_BATCH = 10_000;
|
|
724
1036
|
|
|
725
1037
|
/** The result of each write of a batch, in order. */
|
|
726
|
-
export type BatchStep = Step<
|
|
1038
|
+
export type BatchStep = Step<
|
|
1039
|
+
SymbolRow | RegionRow[] | CommentRow | ReferenceRow | ModuleRow | OperandRow | PatchRow | MacroRow | MacroUseRow | null,
|
|
1040
|
+
SymbolRow | RegionRow[] | OperandRow | PatchRow | MacroRow | MacroUseRow
|
|
1041
|
+
>;
|
|
727
1042
|
|
|
728
1043
|
/**
|
|
729
1044
|
* Applies many writes as one revision in one transaction (D42): all of them,
|
|
@@ -760,8 +1075,24 @@ export function applyBatch(db: DatabaseSync, context: WriteContext, operations:
|
|
|
760
1075
|
case "unoperand":
|
|
761
1076
|
checkSpace(operation);
|
|
762
1077
|
return checkAddress(operation.address);
|
|
1078
|
+
case "patch":
|
|
1079
|
+
return checkPatch(operation);
|
|
1080
|
+
case "unpatch":
|
|
1081
|
+
case "unuse":
|
|
1082
|
+
checkSpace(operation);
|
|
1083
|
+
return checkAddress(operation.start, "start");
|
|
1084
|
+
case "macro":
|
|
1085
|
+
return checkMacro(operation);
|
|
1086
|
+
case "unmacro":
|
|
1087
|
+
checkSpace(operation);
|
|
1088
|
+
return checkMacroName(operation.name);
|
|
1089
|
+
case "use":
|
|
1090
|
+
return checkMacroUse(operation);
|
|
763
1091
|
default:
|
|
764
|
-
throw new KnowledgeError(
|
|
1092
|
+
throw new KnowledgeError(
|
|
1093
|
+
"invalid-input",
|
|
1094
|
+
`op must be one of rename, remove-symbol, classify, unclassify, comment, uncomment, reference, unreference, module, unmodule, operand, unoperand, patch, unpatch, macro, unmacro, use, unuse.`,
|
|
1095
|
+
);
|
|
765
1096
|
}
|
|
766
1097
|
} catch (error) {
|
|
767
1098
|
if (error instanceof KnowledgeError) throw new KnowledgeError(error.code, `Write ${index + 1}: ${error.message} Nothing was written.`);
|
|
@@ -796,6 +1127,18 @@ export function applyBatch(db: DatabaseSync, context: WriteContext, operations:
|
|
|
796
1127
|
return operandStep(db, change, context, operation);
|
|
797
1128
|
case "unoperand":
|
|
798
1129
|
return unoperandStep(db, change, context.origin, operation);
|
|
1130
|
+
case "patch":
|
|
1131
|
+
return patchStep(db, change, context, operation);
|
|
1132
|
+
case "unpatch":
|
|
1133
|
+
return unpatchStep(db, change, context.origin, operation);
|
|
1134
|
+
case "macro":
|
|
1135
|
+
return macroStep(db, change, context.origin, operation);
|
|
1136
|
+
case "unmacro":
|
|
1137
|
+
return unmacroStep(db, change, context.origin, operation);
|
|
1138
|
+
case "use":
|
|
1139
|
+
return useStep(db, change, context, operation);
|
|
1140
|
+
case "unuse":
|
|
1141
|
+
return unuseStep(db, change, context.origin, operation);
|
|
799
1142
|
}
|
|
800
1143
|
} catch (error) {
|
|
801
1144
|
if (error instanceof KnowledgeError) throw new KnowledgeError(error.code, `Write ${index + 1}: ${error.message} Nothing was written.`);
|
|
@@ -818,6 +1161,13 @@ const KEYS = {
|
|
|
818
1161
|
map: toOperand,
|
|
819
1162
|
key: (row: OperandRow) => `${row.space}|${row.address}|${row.select}|${row.symbol ?? ""}|${row.offset}|${row.expression ?? ""}|${row.origin}`,
|
|
820
1163
|
},
|
|
1164
|
+
patches: {
|
|
1165
|
+
columns: PATCH_COLUMNS,
|
|
1166
|
+
map: toPatch,
|
|
1167
|
+
key: (row: PatchRow) => `${row.space}|${row.start}|${row.end}|${row.switch}|${row.code}|${row.comment ?? ""}|${row.origin}`,
|
|
1168
|
+
},
|
|
1169
|
+
macros: { columns: MACRO_COLUMNS, map: toMacro, key: (row: MacroRow) => `${row.space}|${row.name}|${listText(row.params)}|${row.body}|${row.comment ?? ""}|${row.origin}` },
|
|
1170
|
+
macro_uses: { columns: MACRO_USE_COLUMNS, map: toMacroUse, key: (row: MacroUseRow) => `${row.space}|${row.start}|${row.end}|${row.macro}|${listText(row.args)}|${row.origin}` },
|
|
821
1171
|
suppressions: {
|
|
822
1172
|
columns: SUPPRESSION_COLUMNS,
|
|
823
1173
|
map: toSuppression,
|
|
@@ -856,6 +1206,18 @@ export function revert(db: DatabaseSync, context: WriteContext, request: { revis
|
|
|
856
1206
|
change.insertOperand(spec.map(row as never) as OperandRow);
|
|
857
1207
|
continue;
|
|
858
1208
|
}
|
|
1209
|
+
if (table === "patches") {
|
|
1210
|
+
change.insertPatch(spec.map(row as never) as PatchRow);
|
|
1211
|
+
continue;
|
|
1212
|
+
}
|
|
1213
|
+
if (table === "macros") {
|
|
1214
|
+
change.insertMacro(spec.map(row as never) as MacroRow);
|
|
1215
|
+
continue;
|
|
1216
|
+
}
|
|
1217
|
+
if (table === "macro_uses") {
|
|
1218
|
+
change.insertMacroUse(spec.map(row as never) as MacroUseRow);
|
|
1219
|
+
continue;
|
|
1220
|
+
}
|
|
859
1221
|
const mapped = spec.map(row as never) as SymbolRow & RegionRow & CommentRow & ReferenceRow;
|
|
860
1222
|
if (table === "symbols") change.insertSymbol(mapped);
|
|
861
1223
|
else if (table === "regions") change.insertRegion(mapped);
|