@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.
@@ -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 of its address. */
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
- address: string;
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", address: "address", fields: [space, field("address", "address", "address"), field("name", "name", "text"), field("kind", "kind", "text"), origin] },
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
- address: "start_address",
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
- address: "address",
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
- address: "from_address",
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
- address: "start_address",
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
- address: "start_address",
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
- address: "address",
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.address] as number) - (b[spec.address] as number) ||
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);
@@ -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) renameInOperands(db, change, space, 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<SymbolRow | RegionRow[] | CommentRow | ReferenceRow | ModuleRow | OperandRow | null, SymbolRow | RegionRow[] | OperandRow>;
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("invalid-input", `op must be one of rename, remove-symbol, classify, unclassify, comment, uncomment, reference, unreference, module, unmodule, operand, unoperand.`);
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);