@henols/c64-re-tools 2.0.0-rc.2 → 2.0.0-rc.21

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.
Files changed (126) hide show
  1. package/README.md +28 -62
  2. package/distribution/package.ts +1 -16
  3. package/distribution/plugin.ts +5 -3
  4. package/package.json +1 -1
  5. package/skills/c64-assembler/SKILL.md +25 -6
  6. package/skills/c64-assembler/scripts/assemble.ts +45 -10
  7. package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
  8. package/skills/c64-basic/SKILL.md +15 -7
  9. package/skills/c64-basic/scripts/basic.ts +4 -4
  10. package/skills/c64-disk/SKILL.md +34 -11
  11. package/skills/c64-disk/scripts/disk.ts +65 -10
  12. package/skills/c64-disk/scripts/lib/containers.ts +124 -0
  13. package/skills/c64-docs/SKILL.md +138 -0
  14. package/skills/c64-docs/references/page-types.md +141 -0
  15. package/skills/c64-docs/references/style.md +43 -0
  16. package/skills/c64-docs/scripts/generate.ts +102 -0
  17. package/skills/c64-docs/scripts/lib/links.ts +155 -0
  18. package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
  19. package/skills/c64-docs/scripts/lib/reference.ts +406 -0
  20. package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
  21. package/skills/c64-docs/scripts/link.ts +207 -0
  22. package/skills/c64-emulator/SKILL.md +29 -8
  23. package/skills/c64-knowledge/SKILL.md +201 -15
  24. package/skills/c64-knowledge/scripts/knowledge.ts +927 -78
  25. package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
  26. package/skills/c64-memory-map/SKILL.md +14 -2
  27. package/skills/c64-memory-map/scripts/memmap.ts +15 -4
  28. package/skills/c64-provenance/SKILL.md +100 -11
  29. package/skills/c64-provenance/scripts/lib/compare.ts +152 -0
  30. package/skills/c64-provenance/scripts/lib/library.ts +584 -0
  31. package/skills/c64-provenance/scripts/lib/runtime.ts +191 -0
  32. package/skills/c64-provenance/scripts/lib/separate.ts +140 -0
  33. package/skills/c64-provenance/scripts/provenance.ts +477 -24
  34. package/skills/c64-reverse-engineering/SKILL.md +90 -11
  35. package/skills/c64-source/SKILL.md +174 -0
  36. package/skills/c64-source/scripts/generate.ts +788 -0
  37. package/skills/c64-source/scripts/lib/loader.ts +630 -0
  38. package/skills/c64-source/scripts/lib/source.ts +1121 -0
  39. package/skills/c64-static-analysis/SKILL.md +53 -13
  40. package/skills/c64-static-analysis/scripts/analyze.ts +260 -31
  41. package/skills/c64-static-analysis/scripts/lib/coverage.ts +72 -0
  42. package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +11 -5
  43. package/skills/c64-static-analysis/scripts/lib/idioms.ts +158 -0
  44. package/skills/c64-static-analysis/scripts/lib/instructions.ts +105 -0
  45. package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
  46. package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
  47. package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
  48. package/skills/c64-testing/SKILL.md +57 -4
  49. package/skills/c64-testing/references/scenario-format.md +50 -6
  50. package/skills/c64-testing/scripts/checklist.ts +3 -3
  51. package/skills/c64-testing/scripts/diff.ts +167 -0
  52. package/skills/c64-testing/scripts/lib/diff.ts +70 -0
  53. package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +183 -152
  54. package/skills/c64-testing/scripts/run.ts +241 -0
  55. package/skills/c64-testing/scripts/test.ts +20 -10
  56. package/skills/c64-unpacker/SKILL.md +88 -21
  57. package/skills/c64-unpacker/scripts/lib/evidence.ts +207 -0
  58. package/skills/c64-unpacker/scripts/lib/loads.ts +209 -0
  59. package/skills/c64-unpacker/scripts/lib/machine.ts +80 -0
  60. package/skills/c64-unpacker/scripts/lib/stages.ts +142 -0
  61. package/skills/c64-unpacker/scripts/unpack.ts +545 -61
  62. package/src/c64.ts +240 -12
  63. package/src/cli/cleanup.ts +1 -1
  64. package/src/cli/main.ts +14 -8
  65. package/src/cli/status.ts +105 -11
  66. package/src/graphics.ts +158 -0
  67. package/src/host/main.ts +57 -31
  68. package/src/host/server.ts +30 -47
  69. package/src/host/tools/c1541.ts +34 -5
  70. package/src/host/tools/index.ts +6 -2
  71. package/src/host/tools/petcat.ts +10 -3
  72. package/src/host/tools/status.ts +23 -6
  73. package/src/host/vice/adapter.ts +64 -20
  74. package/src/host/vice/chips.ts +11 -1
  75. package/src/host/vice/process.ts +19 -8
  76. package/src/host/vice/screen.ts +4 -44
  77. package/src/host/vice/session.ts +343 -60
  78. package/src/host-client/autostart.ts +55 -0
  79. package/src/host-client/connect.ts +14 -14
  80. package/src/host-client/coverage.ts +36 -0
  81. package/src/host-client/image.ts +63 -0
  82. package/src/host-client/steps.ts +175 -0
  83. package/src/host-client/tools.ts +7 -6
  84. package/src/host-client/transfer.ts +60 -10
  85. package/src/host-client/vice-session.ts +107 -28
  86. package/src/knowledge/conflicts.ts +161 -0
  87. package/src/knowledge/database.ts +150 -38
  88. package/src/knowledge/expression.ts +209 -0
  89. package/src/knowledge/graph.ts +104 -0
  90. package/src/knowledge/history.ts +105 -27
  91. package/src/knowledge/import.ts +141 -85
  92. package/src/knowledge/join.ts +211 -0
  93. package/src/knowledge/program.ts +258 -0
  94. package/src/knowledge/read.ts +445 -63
  95. package/src/knowledge/schema.ts +277 -65
  96. package/src/knowledge/text.ts +415 -0
  97. package/src/knowledge/write.ts +1145 -150
  98. package/src/mcp/main.ts +44 -19
  99. package/src/mcp/output.ts +39 -0
  100. package/src/mcp/server.ts +15 -4
  101. package/src/mcp/tools/debug.ts +202 -31
  102. package/src/mcp/tools/execution.ts +39 -5
  103. package/src/mcp/tools/input.ts +7 -4
  104. package/src/mcp/tools/machine.ts +14 -2
  105. package/src/mcp/tools/media.ts +44 -16
  106. package/src/mcp/tools/memory.ts +68 -12
  107. package/src/mcp/tools/video.ts +20 -9
  108. package/src/native/discover.ts +44 -6
  109. package/src/native/ghidra/analyze.ts +2 -2
  110. package/src/native/ghidra/index.ts +19 -3
  111. package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
  112. package/src/native/ghidra/scripts/C64Export.java +33 -1
  113. package/src/native/ghidra/scripts/C64Prepare.java +7 -1
  114. package/src/native/status.ts +15 -3
  115. package/src/native/types.ts +43 -8
  116. package/src/opcodes.ts +133 -0
  117. package/src/package.ts +21 -0
  118. package/{skills/c64-memory-map/scripts → src}/platform.ts +87 -7
  119. package/src/project.ts +166 -3
  120. package/src/protocol/messages.ts +17 -10
  121. package/src/protocol/tools.ts +27 -22
  122. package/src/protocol/vice.ts +255 -33
  123. package/src/script.ts +49 -9
  124. package/src/trace.ts +55 -23
  125. package/skills/c64-provenance/scripts/compare.ts +0 -92
  126. package/skills/c64-unpacker/scripts/evidence.ts +0 -135
@@ -3,40 +3,96 @@
3
3
 
4
4
  import type { DatabaseSync } from "node:sqlite";
5
5
 
6
- import { SYMBOL_NAME } from "../c64.ts";
6
+ import { instructionLength, SYMBOL_NAME } from "../c64.ts";
7
7
  import { isoCet } from "../time.ts";
8
+ import { CONFLICT_RULES, conflicts, conflictTable, type Conflict, type ConflictRow } from "./conflicts.ts";
8
9
  import { KnowledgeError, transaction } from "./database.ts";
10
+ import { checkExpression, evaluate, ExpressionError, identifiers, operandText, replaceIdentifiers } from "./expression.ts";
9
11
  import {
10
12
  COMMENT_COLUMNS,
11
13
  commentsAt,
12
14
  currentRevision,
15
+ listMacroUses,
16
+ listOperands,
17
+ listPatches,
18
+ listText,
19
+ MACRO_COLUMNS,
20
+ MACRO_USE_COLUMNS,
21
+ macroNamed,
22
+ macroUsesOverlapping,
23
+ MODULE_COLUMNS,
24
+ moduleAt,
25
+ moduleWithFile,
26
+ OPERAND_COLUMNS,
27
+ operandAt,
28
+ PATCH_COLUMNS,
29
+ patchesOverlapping,
13
30
  REFERENCE_COLUMNS,
14
31
  referencesFrom,
15
32
  REGION_COLUMNS,
16
33
  regionsOverlapping,
34
+ resolveRevision,
35
+ SUPPRESSION_COLUMNS,
36
+ suppressionsOverlapping,
17
37
  SYMBOL_COLUMNS,
18
38
  symbolAt,
19
39
  symbolNamed,
20
40
  toComment,
41
+ toMacro,
42
+ toMacroUse,
43
+ toModule,
44
+ toOperand,
45
+ toPatch,
21
46
  toReference,
22
47
  toRegion,
48
+ toSuppression,
23
49
  toSymbol,
24
50
  type CommentRow,
51
+ type MacroRow,
52
+ type MacroUseRow,
53
+ type ModuleRow,
54
+ type OperandRow,
55
+ type PatchRow,
25
56
  type ReferenceRow,
26
57
  type RegionRow,
58
+ type SuppressionRow,
27
59
  type SymbolRow,
28
60
  } from "./read.ts";
29
61
  import {
30
62
  COMMENT_PLACEMENTS,
63
+ EVIDENCE_KINDS,
64
+ MAX_ELEMENT,
65
+ MODULE_FILE,
66
+ DEFAULT_SPACE,
67
+ GENERATED_FILES,
68
+ isGeneratedFile,
69
+ MACRO_NAME,
70
+ MACRO_PARAM,
71
+ SPACE,
72
+ SWITCH_NAME,
73
+ NO_ELEMENT_TYPES,
74
+ OPERAND_SELECTS,
75
+ POINTER_KIND,
31
76
  REFERENCE_KINDS,
32
77
  REGION_TYPES,
33
78
  SYMBOL_KINDS,
34
79
  type CommentPlacement,
80
+ type EvidenceKind,
81
+ type OperandSelect,
35
82
  type Origin,
36
83
  type ReferenceKind,
37
84
  type RegionType,
85
+ type Space,
38
86
  type SymbolKind,
39
87
  } from "./schema.ts";
88
+ import { newRevisionId, rowId, sqlName } from "./text.ts";
89
+
90
+ /** What a change rests on (D42): a coverage run, a test or another project file with its SHA-256, or a note. */
91
+ export interface Evidence {
92
+ kind: EvidenceKind;
93
+ ref: string;
94
+ sha256?: string;
95
+ }
40
96
 
41
97
  /** Who writes and why. Semantic writes come from the user or the LLM, never as an analyzer. */
42
98
  export interface WriteContext {
@@ -44,12 +100,22 @@ export interface WriteContext {
44
100
  /** A short reason, kept with the revision for later review. */
45
101
  description?: string;
46
102
  /** The revision the caller read; the write is refused if knowledge changed since. */
47
- expectedRevision?: number;
103
+ expectedRevision?: string;
104
+ /** Kept with the revision when the write changes something. */
105
+ evidence?: Evidence[];
106
+ /** The image that an operand, patch or macro use write is checked against (D65, D72, D73). */
107
+ image?: OperandImage;
108
+ }
109
+
110
+ /** The bytes of the image and the platform names (C64 space only) that operand expressions, patches and macro uses are checked against. */
111
+ export interface OperandImage {
112
+ byteAt(address: number): number | undefined;
113
+ platform: ReadonlyMap<string, number>;
48
114
  }
49
115
 
50
116
  /** The revision a write created, or null when nothing needed to change. */
51
117
  export interface WriteResult<T, P = never> {
52
- revision: number | null;
118
+ revision: string | null;
53
119
  current: T;
54
120
  /** The current rows of another origin that this write replaced, as they were before it. */
55
121
  previous?: P;
@@ -57,6 +123,11 @@ export interface WriteResult<T, P = never> {
57
123
 
58
124
  export { SYMBOL_NAME };
59
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;
130
+ const MAX_EVIDENCE_REF = 1000;
60
131
 
61
132
  export function checkAddress(value: number, what = "address"): void {
62
133
  if (!Number.isInteger(value) || value < 0 || value > 0xffff) throw new KnowledgeError("invalid-input", `${what} must be from $0000 to $ffff.`);
@@ -70,13 +141,33 @@ export function hex(value: number): string {
70
141
  return `$${value.toString(16).padStart(4, "0")}`;
71
142
  }
72
143
 
144
+ export interface AddressRange {
145
+ start: number;
146
+ end: number;
147
+ }
148
+
149
+ /** Parts of `range` that no range in `holes` covers. */
150
+ export function subtract(range: AddressRange, holes: AddressRange[]): AddressRange[] {
151
+ let pieces = [range];
152
+ for (const hole of holes) {
153
+ pieces = pieces.flatMap((piece) => {
154
+ if (hole.end < piece.start || hole.start > piece.end) return [piece];
155
+ const left = hole.start > piece.start ? [{ start: piece.start, end: hole.start - 1 }] : [];
156
+ const right = hole.end < piece.end ? [{ start: hole.end + 1, end: piece.end }] : [];
157
+ return [...left, ...right];
158
+ });
159
+ }
160
+ return pieces;
161
+ }
162
+
73
163
  /** What a revision records about its writer. Analyzer imports may add tool details. */
74
164
  export interface ChangeContext {
75
165
  origin: Origin;
76
166
  description?: string;
77
- expectedRevision?: number;
167
+ expectedRevision?: string;
78
168
  inputHash?: string;
79
169
  toolVersion?: string;
170
+ evidence?: Evidence[];
80
171
  }
81
172
 
82
173
  /**
@@ -84,7 +175,9 @@ export interface ChangeContext {
84
175
  * only when the first actual change happens, so a no-op leaves no revision.
85
176
  */
86
177
  export class Change {
87
- revision: number | null = null;
178
+ revision: string | null = null;
179
+ /** The rows this write inserted, for their ids. */
180
+ #rows = 0;
88
181
  readonly #db: DatabaseSync;
89
182
  readonly #context: ChangeContext;
90
183
  readonly #operation: string;
@@ -93,8 +186,16 @@ export class Change {
93
186
  this.#db = db;
94
187
  this.#context = context;
95
188
  this.#operation = operation;
189
+ // A merge of two computers' work can leave conflicts: only a merge revision writes then (D69).
190
+ const found = operation === "merge" ? [] : conflicts(db);
191
+ if (found.length > 0) {
192
+ throw new KnowledgeError(
193
+ "conflict",
194
+ `The knowledge has ${found.length} conflict${found.length === 1 ? "" : "s"} from a git merge, for example ${describe(found[0]!)}. Nothing was written. Run knowledge.ts merge to decide them, then write again.`,
195
+ );
196
+ }
96
197
  const current = currentRevision(db);
97
- if (context.expectedRevision !== undefined && context.expectedRevision !== current) {
198
+ if (context.expectedRevision !== undefined && resolveRevision(db, context.expectedRevision) !== current) {
98
199
  throw new KnowledgeError(
99
200
  "stale-revision",
100
201
  `Knowledge changed since revision ${context.expectedRevision}; it is now at revision ${current}. Read it again and decide again.`,
@@ -102,22 +203,33 @@ export class Change {
102
203
  }
103
204
  }
104
205
 
105
- /** The id of this write's revision, created on first use. */
106
- id(): number {
206
+ /** The id of this write's revision, created on first use with its evidence (D69: an id that sorts after every revision). */
207
+ id(): string {
107
208
  if (this.revision !== null) return this.revision;
108
- const created = this.#db
109
- .prepare("INSERT INTO revisions (created_at, origin, operation, input_hash, tool_version, description) VALUES (?, ?, ?, ?, ?, ?) RETURNING id")
110
- .get(
111
- isoCet(new Date()),
112
- this.#context.origin,
113
- this.#operation,
114
- this.#context.inputHash ?? null,
115
- this.#context.toolVersion ?? null,
116
- this.#context.description ?? null,
117
- ) as { id: number };
118
- this.revision = created.id;
119
- this.#db.prepare("UPDATE meta SET value = ? WHERE key = 'current_revision'").run(String(created.id));
120
- return created.id;
209
+ const now = new Date();
210
+ const id = newRevisionId(currentRevision(this.#db), now);
211
+ this.#db
212
+ .prepare("INSERT INTO revisions (id, created_at, origin, operation, input_hash, tool_version, description) VALUES (?, ?, ?, ?, ?, ?, ?)")
213
+ .run(id, isoCet(now), this.#context.origin, this.#operation, this.#context.inputHash ?? null, this.#context.toolVersion ?? null, this.#context.description ?? null);
214
+ this.revision = id;
215
+ const evidence = this.#db.prepare("INSERT INTO evidence (revision, kind, ref, sha256) VALUES (?, ?, ?, ?)");
216
+ for (const item of this.#context.evidence ?? []) evidence.run(id, item.kind, item.ref, item.sha256 ?? null);
217
+ return id;
218
+ }
219
+
220
+ /**
221
+ * Refuses a write that leaves two current rows where one is allowed: the
222
+ * index has no unique indexes, because a merged text can hold such rows.
223
+ */
224
+ check(): void {
225
+ if (this.revision === null) return;
226
+ const [found] = conflicts(this.#db);
227
+ if (found !== undefined) throw new KnowledgeError("conflict", `The write would leave ${describe(found)}. Nothing was written.`);
228
+ }
229
+
230
+ /** The id of the next row this write inserts. */
231
+ #rowId(): string {
232
+ return rowId(this.id(), ++this.#rows);
121
233
  }
122
234
 
123
235
  /** Closes the current rows of `table` that `where` selects. */
@@ -126,223 +238,1106 @@ export class Change {
126
238
  }
127
239
 
128
240
  insertSymbol(row: Omit<SymbolRow, "revision">): void {
129
- this.#db.prepare("INSERT INTO symbols (address, name, kind, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?)").run(row.address, row.name, row.kind, row.origin, this.id());
241
+ this.#db.prepare("INSERT INTO symbols (id, space, address, name, kind, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?)").run(this.#rowId(), row.space, row.address, row.name, row.kind, row.origin, this.id());
242
+ }
243
+
244
+ /** A second name of an address from an analyzer (D74). */
245
+ insertAnalyzerName(row: Omit<SymbolRow, "revision">): void {
246
+ this.#db.prepare("INSERT INTO analyzer_names (id, space, address, name, kind, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?)").run(this.#rowId(), row.space, row.address, row.name, row.kind, row.origin, this.id());
130
247
  }
131
248
 
132
249
  insertRegion(row: Omit<RegionRow, "revision">): void {
133
250
  this.#db
134
- .prepare("INSERT INTO regions (start_address, end_address, type, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?)")
135
- .run(row.start, row.end, row.type, row.origin, this.id());
251
+ .prepare("INSERT INTO regions (id, space, start_address, end_address, type, element, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
252
+ .run(this.#rowId(), row.space, row.start, row.end, row.type, row.element ?? null, row.origin, this.id());
136
253
  }
137
254
 
138
255
  insertComment(row: Omit<CommentRow, "revision">): void {
139
- this.#db.prepare("INSERT INTO comments (address, placement, text, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?)").run(row.address, row.placement, row.text, row.origin, this.id());
256
+ this.#db
257
+ .prepare("INSERT INTO comments (id, space, address, placement, text, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?)")
258
+ .run(this.#rowId(), row.space, row.address, row.placement, row.text, row.origin, this.id());
140
259
  }
141
260
 
142
261
  insertReference(row: Omit<ReferenceRow, "revision">): void {
143
262
  this.#db
144
- .prepare('INSERT INTO "references" (from_address, to_address, kind, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?)')
145
- .run(row.from, row.to, row.kind, row.origin, this.id());
263
+ .prepare('INSERT INTO "references" (id, space, from_address, to_address, kind, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?)')
264
+ .run(this.#rowId(), row.space, row.from, row.to, row.kind, row.origin, this.id());
265
+ }
266
+
267
+ insertModule(row: Omit<ModuleRow, "revision">): void {
268
+ this.#db
269
+ .prepare("INSERT INTO modules (id, space, start_address, file, title, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?)")
270
+ .run(this.#rowId(), row.space, row.start, row.file, row.title ?? null, row.origin, this.id());
271
+ }
272
+
273
+ insertOperand(row: Omit<OperandRow, "revision">): void {
274
+ this.#db
275
+ .prepare("INSERT INTO operands (id, space, address, selector, symbol, offset, expression, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
276
+ .run(this.#rowId(), row.space, row.address, row.select, row.symbol ?? null, row.offset, row.expression ?? null, row.origin, this.id());
277
+ }
278
+
279
+ insertPatch(row: Omit<PatchRow, "revision">): void {
280
+ this.#db
281
+ .prepare("INSERT INTO patches (id, space, start_address, end_address, switch_name, code, comment, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
282
+ .run(this.#rowId(), row.space, row.start, row.end, row.switch, row.code, row.comment ?? null, row.origin, this.id());
283
+ }
284
+
285
+ insertMacro(row: Omit<MacroRow, "revision">): void {
286
+ this.#db
287
+ .prepare("INSERT INTO macros (id, space, name, params, body, comment, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
288
+ .run(this.#rowId(), row.space, row.name, listText(row.params), row.body, row.comment ?? null, row.origin, this.id());
289
+ }
290
+
291
+ insertMacroUse(row: Omit<MacroUseRow, "revision">): void {
292
+ this.#db
293
+ .prepare("INSERT INTO macro_uses (id, space, start_address, end_address, macro, args, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
294
+ .run(this.#rowId(), row.space, row.start, row.end, row.macro, listText(row.args), row.origin, this.id());
295
+ }
296
+
297
+ insertSuppression(row: Omit<SuppressionRow, "revision">): void {
298
+ this.#db
299
+ .prepare("INSERT INTO suppressions (id, space, entity, start_address, end_address, to_address, kind, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
300
+ .run(this.#rowId(), row.space, row.entity, row.start, row.end, row.to ?? null, row.kind ?? null, row.origin, this.id());
301
+ }
302
+
303
+ /** Bytes of the program (D83), as hex digits. */
304
+ insertMemory(row: { space: Space; address: number; bytes: string; origin: Origin }): void {
305
+ this.#db.prepare("INSERT INTO memory (id, space, address, bytes, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?)").run(this.#rowId(), row.space, row.address, row.bytes, row.origin, this.id());
306
+ }
307
+
308
+ /** The entry of the program, with the machine state and the unpacker stages as JSON text when it has them (D83). */
309
+ insertEntry(row: { space: Space; address: number; state: string | null; stages: string | null; origin: Origin }): void {
310
+ this.#db
311
+ .prepare("INSERT INTO entries (id, space, address, state, stages, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?)")
312
+ .run(this.#rowId(), row.space, row.address, row.state, row.stages, row.origin, this.id());
313
+ }
314
+
315
+ /** One coverage range of one kind (D83). */
316
+ insertCoverage(row: { space: Space; kind: string; start: number; end: number; origin: Origin }): void {
317
+ this.#db
318
+ .prepare("INSERT INTO coverage (id, space, kind, start_address, end_address, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?)")
319
+ .run(this.#rowId(), row.space, row.kind, row.start, row.end, row.origin, this.id());
146
320
  }
147
321
  }
148
322
 
149
- /** Runs one semantic write in a transaction. */
150
- function write<T>(db: DatabaseSync, context: WriteContext, operation: string, work: (change: Change) => T): WriteResult<T> {
323
+ /** A conflict in words: its rule, its space and the addresses of its rows. */
324
+ function describe(conflict: Conflict): string {
325
+ const place = (entry: ConflictRow) => {
326
+ if (entry.entity === "macro") return entry.row.name;
327
+ const row = entry.row as { address?: number; start?: number; end?: number; from?: number };
328
+ if (row.address !== undefined) return hex(row.address);
329
+ if (row.from !== undefined && entry.entity === "reference") return hex(row.from);
330
+ return row.end === undefined || row.end === row.start ? hex(row.start!) : `${hex(row.start!)}-${hex(row.end)}`;
331
+ };
332
+ return `${CONFLICT_RULES[conflict.rule]} in ${conflict.space} (${[...new Set(conflict.rows.map(place))].join(", ")})`;
333
+ }
334
+
335
+ /** Closes the current region suppressions over [start, end] and keeps their parts outside it: a user/LLM region there decides now. */
336
+ function liftRegionSuppressions(db: DatabaseSync, change: Change, start: number, end: number, space: Space): void {
337
+ const overlapping = suppressionsOverlapping(db, "region", start, end, space);
338
+ if (overlapping.length === 0) return;
339
+ change.close("suppressions", "space = ? AND entity = 'region' AND start_address <= ? AND end_address >= ?", space, end, start);
340
+ for (const row of overlapping) {
341
+ if (row.start < start) change.insertSuppression({ ...row, end: start - 1 });
342
+ if (row.end > end) change.insertSuppression({ ...row, start: end + 1 });
343
+ }
344
+ }
345
+
346
+ function checkEvidence(evidence: Evidence[] | undefined): void {
347
+ for (const item of evidence ?? []) {
348
+ checkOneOf(EVIDENCE_KINDS, item.kind, "evidence kind");
349
+ if (item.ref.trim().length === 0 || item.ref.length > MAX_EVIDENCE_REF) throw new KnowledgeError("invalid-input", `evidence has 1 to ${MAX_EVIDENCE_REF} characters after its kind.`);
350
+ if (item.sha256 !== undefined && !/^[0-9a-f]{64}$/.test(item.sha256)) throw new KnowledgeError("invalid-input", "evidence sha256 must be 64 lowercase hex digits.");
351
+ }
352
+ }
353
+
354
+ /** Runs semantic writes in one transaction, as one revision. */
355
+ export function write<T>(db: DatabaseSync, context: WriteContext, operation: string, work: (change: Change) => T): { revision: string | null; current: T } {
151
356
  if (context.origin !== "user" && context.origin !== "llm") {
152
357
  throw new KnowledgeError("invalid-input", "A semantic write comes from the user or the LLM.");
153
358
  }
359
+ checkEvidence(context.evidence);
154
360
  return transaction(db, () => {
155
361
  const change = new Change(db, context, operation);
156
362
  const current = work(change);
363
+ change.check();
157
364
  return { revision: change.revision, current };
158
365
  });
159
366
  }
160
367
 
161
- /** Names the symbol at an address (one current symbol per address, unique names). */
162
- export function renameSymbol(
163
- db: DatabaseSync,
164
- context: WriteContext,
165
- request: { address: number; name: string; kind?: SymbolKind },
166
- ): WriteResult<SymbolRow, SymbolRow> {
368
+ /** One step of a write inside a change: its current value, and the rows of another origin it replaced. */
369
+ interface Step<T, P = never> {
370
+ current: T;
371
+ previous?: P;
372
+ }
373
+
374
+ const result = <T, P>(written: { revision: string | null; current: Step<T, P> }): WriteResult<T, P> =>
375
+ written.current.previous === undefined ? { revision: written.revision, current: written.current.current } : { revision: written.revision, current: written.current.current, previous: written.current.previous };
376
+
377
+ // Each write has a check, which refuses bad input before anything opens, and a
378
+ // step, which runs inside a change. A single write and a batch share both (D42).
379
+
380
+ /** Every request names its space (D53); without one it is the C64. */
381
+ type InSpace = { space?: Space };
382
+ type SymbolRequest = InSpace & { address: number; name: string; kind?: SymbolKind };
383
+ type RegionRequest = InSpace & { start: number; end: number; type: RegionType; element?: number };
384
+ type RangeRequest = InSpace & { start: number; end: number };
385
+ type CommentRequest = InSpace & { address: number; placement: CommentPlacement; text: string };
386
+ type PlacementRequest = InSpace & { address: number; placement: CommentPlacement };
387
+ type ReferenceRequest = InSpace & { from: number; to: number; kind: ReferenceKind };
388
+ type AddressRequest = InSpace & { address: number };
389
+
390
+ const spaceOf = (request: InSpace): Space => request.space ?? DEFAULT_SPACE;
391
+
392
+ export function checkSpace(request: InSpace): void {
393
+ if (request.space !== undefined && !SPACE.test(request.space)) throw new KnowledgeError("invalid-input", "space must be c64, drive8, or bank0 to bank255.");
394
+ }
395
+
396
+ function checkSymbol(request: SymbolRequest): void {
397
+ checkSpace(request);
167
398
  checkAddress(request.address);
168
399
  if (!SYMBOL_NAME.test(request.name)) {
169
400
  throw new KnowledgeError("invalid-input", "A symbol name is a letter or underscore, then letters, digits or underscores (at most 64), with an optional leading dot.");
170
401
  }
171
402
  if (request.kind !== undefined) checkOneOf(SYMBOL_KINDS, request.kind, "kind");
172
- let previous: SymbolRow | undefined;
173
- const result = write(db, context, "rename-symbol", (change) => {
174
- const existing = symbolAt(db, request.address);
175
- const kind = request.kind ?? existing?.kind ?? "label";
176
- if (existing !== undefined && existing.name === request.name && existing.kind === kind) return existing;
177
- const owner = symbolNamed(db, request.name);
178
- if (owner !== undefined && owner.address !== request.address) {
179
- throw new KnowledgeError("conflict", `The name ${request.name} already belongs to ${hex(owner.address)}. Rename or remove that symbol first.`);
180
- }
181
- if (existing !== undefined) {
182
- change.close("symbols", "address = ?", request.address);
183
- if (existing.origin !== context.origin) previous = existing;
184
- }
185
- change.insertSymbol({ address: request.address, name: request.name, kind, origin: context.origin });
186
- return symbolAt(db, request.address)!;
187
- });
188
- return previous === undefined ? result : { ...result, previous };
189
403
  }
190
404
 
191
- export function removeSymbol(db: DatabaseSync, context: WriteContext, request: { address: number }): WriteResult<null> {
192
- checkAddress(request.address);
193
- return write(db, context, "remove-symbol", (change) => {
194
- if (symbolAt(db, request.address) === undefined) throw new KnowledgeError("not-found", `There is no symbol at ${hex(request.address)}.`);
195
- change.close("symbols", "address = ?", request.address);
196
- return null;
197
- });
405
+ function renameStep(db: DatabaseSync, change: Change, origin: Origin, request: SymbolRequest): Step<SymbolRow, SymbolRow> {
406
+ const space = spaceOf(request);
407
+ const existing = symbolAt(db, request.address, space);
408
+ const kind = request.kind ?? existing?.kind ?? "label";
409
+ if (existing !== undefined && existing.name === request.name && existing.kind === kind) return { current: existing };
410
+ const owner = symbolNamed(db, request.name, space);
411
+ if (owner !== undefined && owner.address !== request.address) {
412
+ throw new KnowledgeError("conflict", `The name ${request.name} already belongs to ${hex(owner.address)}. Rename or remove that symbol first.`);
413
+ }
414
+ const [patch] = listPatches(db, undefined, request.name, undefined, space);
415
+ 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.`);
416
+ if (existing !== undefined) change.close("symbols", "space = ? AND address = ?", space, request.address);
417
+ change.insertSymbol({ space, address: request.address, name: request.name, kind, origin });
418
+ if (existing !== undefined && existing.name !== request.name) {
419
+ renameInOperands(db, change, space, existing.name, request.name);
420
+ renameInUses(db, change, space, existing.name, request.name);
421
+ }
422
+ change.close("suppressions", "space = ? AND entity = 'symbol' AND start_address = ?", space, request.address);
423
+ const current = symbolAt(db, request.address, space)!;
424
+ return existing !== undefined && existing.origin !== origin ? { current, previous: existing } : { current };
198
425
  }
199
426
 
200
- /** Closes the current regions over [start, end] and keeps their parts outside it. */
201
- export function clearRange(db: DatabaseSync, change: Change, start: number, end: number): void {
202
- const overlapping = regionsOverlapping(db, start, end);
203
- if (overlapping.length === 0) return;
204
- change.close("regions", "start_address <= ? AND end_address >= ?", end, start);
205
- for (const region of overlapping) {
206
- if (region.start < start) change.insertRegion({ start: region.start, end: start - 1, type: region.type, origin: region.origin });
207
- if (region.end > end) change.insertRegion({ start: end + 1, end: region.end, type: region.type, origin: region.origin });
427
+ function removeSymbolStep(db: DatabaseSync, change: Change, origin: Origin, request: AddressRequest): Step<null> {
428
+ const space = spaceOf(request);
429
+ const existing = symbolAt(db, request.address, space);
430
+ if (existing === undefined) throw new KnowledgeError("not-found", `There is no symbol at ${hex(request.address)}.`);
431
+ const users = operandsNaming(db, space, existing.name);
432
+ if (users.length > 0) {
433
+ throw new KnowledgeError(
434
+ "conflict",
435
+ `The operand expressions at ${users.map((row) => hex(row.address)).join(", ")} use ${existing.name}. Change or remove them first (unoperand), or rename the symbol.`,
436
+ );
437
+ }
438
+ const uses = usesNaming(db, space, existing.name);
439
+ if (uses.length > 0) {
440
+ 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.`);
208
441
  }
442
+ // No suppression: an analyzer name is a second name and never comes back as the name (D74).
443
+ change.close("symbols", "space = ? AND address = ?", space, request.address);
444
+ return { current: null };
209
445
  }
210
446
 
211
- /** Classifies [start, end]; overlapping regions are split so current regions never overlap. */
212
- export function classifyRegion(
213
- db: DatabaseSync,
214
- context: WriteContext,
215
- request: { start: number; end: number; type: RegionType },
216
- ): WriteResult<RegionRow[], RegionRow[]> {
447
+ function checkRange(request: RangeRequest): void {
448
+ checkSpace(request);
217
449
  checkAddress(request.start, "start");
218
450
  checkAddress(request.end, "end");
219
451
  if (request.end < request.start) throw new KnowledgeError("invalid-input", "end must not be before start.");
452
+ }
453
+
454
+ function checkRegion(request: RegionRequest): void {
455
+ checkRange(request);
220
456
  checkOneOf(REGION_TYPES, request.type, "type");
221
- let previous: RegionRow[] = [];
222
- const result = write(db, context, "classify-region", (change) => {
223
- const overlapping = regionsOverlapping(db, request.start, request.end);
224
- const same = overlapping.length === 1 && overlapping[0]!.start === request.start && overlapping[0]!.end === request.end && overlapping[0]!.type === request.type;
225
- if (!same) {
226
- clearRange(db, change, request.start, request.end);
227
- change.insertRegion({ start: request.start, end: request.end, type: request.type, origin: context.origin });
228
- previous = overlapping.filter((row) => row.origin !== context.origin);
229
- }
230
- return regionsOverlapping(db, request.start, request.end);
231
- });
232
- return previous.length === 0 ? result : { ...result, previous };
457
+ if (request.element === undefined) return;
458
+ if (NO_ELEMENT_TYPES.includes(request.type)) throw new KnowledgeError("invalid-input", `A ${request.type} region has no element size.`);
459
+ if (!Number.isInteger(request.element) || request.element < 1 || request.element > MAX_ELEMENT) throw new KnowledgeError("invalid-input", `element must be from 1 to ${MAX_ELEMENT}.`);
460
+ const length = request.end - request.start + 1;
461
+ if (length % request.element !== 0) throw new KnowledgeError("invalid-input", `The region has ${length} bytes, which is not a multiple of the element size ${request.element}.`);
233
462
  }
234
463
 
235
- /** Makes [start, end] unclassified again; parts of regions outside it stay. */
236
- export function unclassifyRegion(db: DatabaseSync, context: WriteContext, request: { start: number; end: number }): WriteResult<null> {
237
- checkAddress(request.start, "start");
238
- checkAddress(request.end, "end");
239
- if (request.end < request.start) throw new KnowledgeError("invalid-input", "end must not be before start.");
240
- return write(db, context, "unclassify-region", (change) => {
241
- if (regionsOverlapping(db, request.start, request.end).length === 0) {
242
- throw new KnowledgeError("not-found", `No region covers ${hex(request.start)}-${hex(request.end)}.`);
243
- }
244
- clearRange(db, change, request.start, request.end);
245
- return null;
246
- });
464
+ /** The element size of a piece of a region: kept when the piece still holds whole elements. */
465
+ function elementOf(piece: AddressRange, element: number | undefined): { element?: number } {
466
+ return element !== undefined && (piece.end - piece.start + 1) % element === 0 ? { element } : {};
247
467
  }
248
468
 
249
- export function setComment(
250
- db: DatabaseSync,
251
- context: WriteContext,
252
- request: { address: number; placement: CommentPlacement; text: string },
253
- ): WriteResult<CommentRow> {
469
+ /** Closes the current regions over [start, end] and keeps their parts outside it. */
470
+ export function clearRange(db: DatabaseSync, change: Change, start: number, end: number, space: Space = DEFAULT_SPACE): void {
471
+ const overlapping = regionsOverlapping(db, start, end, space);
472
+ if (overlapping.length === 0) return;
473
+ change.close("regions", "space = ? AND start_address <= ? AND end_address >= ?", space, end, start);
474
+ for (const region of overlapping) {
475
+ const left = { start: region.start, end: start - 1 };
476
+ const right = { start: end + 1, end: region.end };
477
+ if (region.start < start) change.insertRegion({ space, ...left, type: region.type, ...elementOf(left, region.element), origin: region.origin });
478
+ if (region.end > end) change.insertRegion({ space, ...right, type: region.type, ...elementOf(right, region.element), origin: region.origin });
479
+ }
480
+ }
481
+
482
+ function classifyStep(db: DatabaseSync, change: Change, origin: Origin, request: RegionRequest): Step<RegionRow[], RegionRow[]> {
483
+ const space = spaceOf(request);
484
+ const overlapping = regionsOverlapping(db, request.start, request.end, space);
485
+ const [only] = overlapping;
486
+ const same = overlapping.length === 1 && only!.start === request.start && only!.end === request.end && only!.type === request.type && only!.element === request.element;
487
+ if (same) return { current: overlapping };
488
+ clearRange(db, change, request.start, request.end, space);
489
+ change.insertRegion({ space, start: request.start, end: request.end, type: request.type, ...(request.element === undefined ? {} : { element: request.element }), origin });
490
+ liftRegionSuppressions(db, change, request.start, request.end, space);
491
+ const previous = overlapping.filter((row) => row.origin !== origin);
492
+ const current = regionsOverlapping(db, request.start, request.end, space);
493
+ return previous.length === 0 ? { current } : { current, previous };
494
+ }
495
+
496
+ function unclassifyStep(db: DatabaseSync, change: Change, origin: Origin, request: RangeRequest): Step<null> {
497
+ const space = spaceOf(request);
498
+ if (regionsOverlapping(db, request.start, request.end, space).length === 0) {
499
+ throw new KnowledgeError("not-found", `No region covers ${hex(request.start)}-${hex(request.end)}.`);
500
+ }
501
+ clearRange(db, change, request.start, request.end, space);
502
+ // Suppress only the parts that no region suppression holds yet, so suppressions never overlap.
503
+ for (const piece of subtract({ start: request.start, end: request.end }, suppressionsOverlapping(db, "region", request.start, request.end, space))) {
504
+ change.insertSuppression({ space, entity: "region", ...piece, origin: origin as SuppressionRow["origin"] });
505
+ }
506
+ return { current: null };
507
+ }
508
+
509
+ function checkComment(request: CommentRequest): string {
510
+ checkSpace(request);
254
511
  checkAddress(request.address);
255
512
  checkOneOf(COMMENT_PLACEMENTS, request.placement, "placement");
256
513
  const text = request.text.trim();
257
514
  if (text.length === 0 || text.length > MAX_COMMENT) throw new KnowledgeError("invalid-input", `A comment has 1 to ${MAX_COMMENT} characters.`);
258
- return write(db, context, "set-comment", (change) => {
259
- const existing = commentsAt(db, request.address).find((comment) => comment.placement === request.placement);
260
- if (existing?.text === text) return existing;
261
- if (existing !== undefined) change.close("comments", "address = ? AND placement = ?", request.address, request.placement);
262
- change.insertComment({ address: request.address, placement: request.placement, text, origin: context.origin });
263
- return commentsAt(db, request.address).find((comment) => comment.placement === request.placement)!;
264
- });
515
+ return text;
516
+ }
517
+
518
+ function commentStep(db: DatabaseSync, change: Change, origin: Origin, request: CommentRequest): Step<CommentRow> {
519
+ const space = spaceOf(request);
520
+ const text = request.text.trim();
521
+ const existing = commentsAt(db, request.address, space).find((comment) => comment.placement === request.placement);
522
+ if (existing?.text === text) return { current: existing };
523
+ if (existing !== undefined) change.close("comments", "space = ? AND address = ? AND placement = ?", space, request.address, request.placement);
524
+ change.insertComment({ space, address: request.address, placement: request.placement, text, origin });
525
+ return { current: commentsAt(db, request.address, space).find((comment) => comment.placement === request.placement)! };
265
526
  }
266
527
 
267
- export function removeComment(db: DatabaseSync, context: WriteContext, request: { address: number; placement: CommentPlacement }): WriteResult<null> {
528
+ function checkPlacement(request: PlacementRequest): void {
529
+ checkSpace(request);
268
530
  checkAddress(request.address);
269
531
  checkOneOf(COMMENT_PLACEMENTS, request.placement, "placement");
270
- return write(db, context, "remove-comment", (change) => {
271
- if (!commentsAt(db, request.address).some((comment) => comment.placement === request.placement)) {
272
- throw new KnowledgeError("not-found", `There is no ${request.placement} comment at ${hex(request.address)}.`);
273
- }
274
- change.close("comments", "address = ? AND placement = ?", request.address, request.placement);
275
- return null;
276
- });
277
532
  }
278
533
 
279
- export function addReference(
280
- db: DatabaseSync,
281
- context: WriteContext,
282
- request: { from: number; to: number; kind: ReferenceKind },
283
- ): WriteResult<ReferenceRow> {
284
- checkAddress(request.from, "from");
285
- checkAddress(request.to, "to");
286
- checkOneOf(REFERENCE_KINDS, request.kind, "kind");
287
- return write(db, context, "add-reference", (change) => {
288
- const find = () => referencesFrom(db, request.from).find((reference) => reference.to === request.to && reference.kind === request.kind);
289
- const existing = find();
290
- if (existing !== undefined) return existing;
291
- change.insertReference({ ...request, origin: context.origin });
292
- return find()!;
293
- });
534
+ function uncommentStep(db: DatabaseSync, change: Change, _origin: Origin, request: PlacementRequest): Step<null> {
535
+ const space = spaceOf(request);
536
+ if (!commentsAt(db, request.address, space).some((comment) => comment.placement === request.placement)) {
537
+ throw new KnowledgeError("not-found", `There is no ${request.placement} comment at ${hex(request.address)}.`);
538
+ }
539
+ change.close("comments", "space = ? AND address = ? AND placement = ?", space, request.address, request.placement);
540
+ return { current: null };
294
541
  }
295
542
 
296
- export function removeReference(db: DatabaseSync, context: WriteContext, request: { from: number; to: number; kind: ReferenceKind }): WriteResult<null> {
543
+ function checkReference(request: ReferenceRequest): void {
544
+ checkSpace(request);
297
545
  checkAddress(request.from, "from");
298
546
  checkAddress(request.to, "to");
299
547
  checkOneOf(REFERENCE_KINDS, request.kind, "kind");
300
- return write(db, context, "remove-reference", (change) => {
301
- if (!referencesFrom(db, request.from).some((reference) => reference.to === request.to && reference.kind === request.kind)) {
302
- throw new KnowledgeError("not-found", `There is no ${request.kind} reference from ${hex(request.from)} to ${hex(request.to)}.`);
548
+ if (request.kind === POINTER_KIND) throw new KnowledgeError("invalid-input", "Only an operand expression writes a pointer reference (D65): use operand and unoperand.");
549
+ }
550
+
551
+ function referenceStep(db: DatabaseSync, change: Change, origin: Origin, request: ReferenceRequest): Step<ReferenceRow> {
552
+ const space = spaceOf(request);
553
+ const find = () => referencesFrom(db, request.from, space).find((reference) => reference.to === request.to && reference.kind === request.kind);
554
+ const existing = find();
555
+ if (existing !== undefined) return { current: existing };
556
+ change.insertReference({ space, from: request.from, to: request.to, kind: request.kind, origin });
557
+ change.close("suppressions", "space = ? AND entity = 'reference' AND start_address = ? AND to_address = ? AND kind = ?", space, request.from, request.to, request.kind);
558
+ return { current: find()! };
559
+ }
560
+
561
+ function unreferenceStep(db: DatabaseSync, change: Change, origin: Origin, request: ReferenceRequest): Step<null> {
562
+ const space = spaceOf(request);
563
+ if (!referencesFrom(db, request.from, space).some((reference) => reference.to === request.to && reference.kind === request.kind)) {
564
+ throw new KnowledgeError("not-found", `There is no ${request.kind} reference from ${hex(request.from)} to ${hex(request.to)}.`);
565
+ }
566
+ change.close('"references"', "space = ? AND from_address = ? AND to_address = ? AND kind = ?", space, request.from, request.to, request.kind);
567
+ change.insertSuppression({ space, entity: "reference", start: request.from, end: request.from, to: request.to, kind: request.kind, origin: origin as SuppressionRow["origin"] });
568
+ return { current: null };
569
+ }
570
+
571
+ type OperandRequest = InSpace & { address: number; select: OperandSelect; symbol?: string; offset?: number; expression?: string };
572
+
573
+ /** Current operand expressions that use a name, as a symbol or inside expression text. */
574
+ function operandsNaming(db: DatabaseSync, space: Space, name: string): OperandRow[] {
575
+ return listOperands(db, undefined, undefined, space).filter((row) => row.symbol === name || (row.expression !== undefined && identifiers(row.expression).includes(name)));
576
+ }
577
+
578
+ /** A rename of a symbol renames it in every operand expression that uses it; the rows keep their origin. */
579
+ function renameInOperands(db: DatabaseSync, change: Change, space: Space, from: string, to: string): void {
580
+ for (const row of operandsNaming(db, space, from)) {
581
+ change.close("operands", "space = ? AND address = ?", space, row.address);
582
+ const renamed: Omit<OperandRow, "revision"> = { ...row };
583
+ if (row.symbol === from) renamed.symbol = to;
584
+ if (row.expression !== undefined) renamed.expression = replaceIdentifiers(row.expression, (name) => (name === from ? to : name));
585
+ change.insertOperand(renamed);
586
+ }
587
+ }
588
+
589
+ function checkOperand(request: OperandRequest): void {
590
+ checkSpace(request);
591
+ checkAddress(request.address);
592
+ checkOneOf(OPERAND_SELECTS, request.select, "select");
593
+ if (request.select === "expr") {
594
+ if (request.symbol !== undefined || request.offset !== undefined) throw new KnowledgeError("invalid-input", "An expr operand has expression text, and no symbol or offset.");
595
+ if (typeof request.expression !== "string") throw new KnowledgeError("invalid-input", "An expr operand has expression text.");
596
+ try {
597
+ checkExpression(request.expression);
598
+ } catch (error) {
599
+ if (error instanceof ExpressionError) throw new KnowledgeError("invalid-input", error.message);
600
+ throw error;
601
+ }
602
+ return;
603
+ }
604
+ if (request.expression !== undefined) throw new KnowledgeError("invalid-input", `A ${request.select} operand has a symbol and an optional offset, and no expression text.`);
605
+ if (request.symbol === undefined || !SYMBOL_NAME.test(request.symbol)) throw new KnowledgeError("invalid-input", `A ${request.select} operand names a symbol: a letter or underscore, then letters, digits or underscores.`);
606
+ if (request.offset !== undefined && (!Number.isInteger(request.offset) || Math.abs(request.offset) > 0xffff)) throw new KnowledgeError("invalid-input", "offset must be a whole number from -$ffff to $ffff.");
607
+ }
608
+
609
+ const shown = (value: number, size: number) => (value < 0 ? String(value) : `$${value.toString(16).padStart(size === 0x100 ? 2 : 4, "0")}`);
610
+
611
+ /**
612
+ * Refuses an expression whose value is not the operand in the image (D65), and
613
+ * returns the addresses of its names. Symbols of the knowledge come first, then
614
+ * the platform names of the C64 space.
615
+ */
616
+ function checkOperandValue(db: DatabaseSync, space: Space, image: OperandImage, request: OperandRequest, text: string): number[] {
617
+ const where = hex(request.address);
618
+ const opcode = image.byteAt(request.address);
619
+ if (opcode === undefined) throw new KnowledgeError("invalid-input", `${where} is outside the image.`);
620
+ const length = instructionLength(opcode);
621
+ if (length === 1) throw new KnowledgeError("invalid-input", `The instruction at ${where} (opcode ${shown(opcode, 0x100)}) has no operand.`);
622
+ if ((opcode & 0x1f) === 0x10) throw new KnowledgeError("invalid-input", `The instruction at ${where} is a branch: its operand is a distance, not a value.`);
623
+ if ((request.select === "lo" || request.select === "hi") && length === 3) {
624
+ throw new KnowledgeError("invalid-input", `lo and hi give one byte, but the instruction at ${where} has a 2-byte operand. Use value.`);
625
+ }
626
+ const bytes = [image.byteAt(request.address + 1), length === 3 ? image.byteAt(request.address + 2) : 0];
627
+ if (bytes.some((byte) => byte === undefined)) throw new KnowledgeError("invalid-input", `The instruction at ${where} runs past the end of the image.`);
628
+ const operand = bytes[0]! | (bytes[1]! << 8);
629
+ const resolve = (name: string) => symbolNamed(db, name, space)?.address ?? (space === DEFAULT_SPACE ? image.platform.get(name) : undefined);
630
+ let value: number;
631
+ try {
632
+ value = evaluate(text, resolve);
633
+ } catch (error) {
634
+ if (error instanceof ExpressionError) throw new KnowledgeError("invalid-input", `${where}: ${error.message}`);
635
+ throw error;
636
+ }
637
+ const size = length === 2 ? 0x100 : 0x10000;
638
+ if (value < -size / 2 || value >= size || ((value % size) + size) % size !== operand) {
639
+ throw new KnowledgeError("invalid-input", `At ${where}, ${text} gives ${shown(value, size)}, but the operand in the image is ${shown(operand, size)}.`);
640
+ }
641
+ return identifiers(text).map((name) => resolve(name)!);
642
+ }
643
+
644
+ function operandStep(db: DatabaseSync, change: Change, context: ChangeContext & { image?: OperandImage }, request: OperandRequest): Step<OperandRow, OperandRow> {
645
+ const space = spaceOf(request);
646
+ if (context.image === undefined) throw new KnowledgeError("invalid-input", "An operand expression is checked against the image, and no image was given.");
647
+ const wanted: Omit<OperandRow, "space" | "address" | "origin" | "revision"> = { select: request.select, offset: request.offset ?? 0 };
648
+ if (request.symbol !== undefined) wanted.symbol = request.symbol;
649
+ if (request.expression !== undefined) wanted.expression = request.expression.trim();
650
+ const targets = new Set(checkOperandValue(db, space, context.image, request, operandText(wanted)));
651
+ const existing = operandAt(db, request.address, space);
652
+ if (existing !== undefined && existing.select === wanted.select && existing.symbol === wanted.symbol && existing.offset === wanted.offset && existing.expression === wanted.expression) {
653
+ return { current: existing };
654
+ }
655
+ const origin = context.origin as OperandRow["origin"];
656
+ if (existing !== undefined) change.close("operands", "space = ? AND address = ?", space, request.address);
657
+ change.insertOperand({ space, address: request.address, ...wanted, origin });
658
+ const pointers = referencesFrom(db, request.address, space).filter((reference) => reference.kind === POINTER_KIND);
659
+ for (const reference of pointers) {
660
+ if (!targets.has(reference.to)) change.close('"references"', "space = ? AND from_address = ? AND to_address = ? AND kind = ?", space, request.address, reference.to, POINTER_KIND);
661
+ }
662
+ for (const to of targets) {
663
+ if (!pointers.some((reference) => reference.to === to)) change.insertReference({ space, from: request.address, to, kind: POINTER_KIND, origin });
664
+ }
665
+ const current = operandAt(db, request.address, space)!;
666
+ return existing !== undefined && existing.origin !== origin ? { current, previous: existing } : { current };
667
+ }
668
+
669
+ function unoperandStep(db: DatabaseSync, change: Change, _origin: Origin, request: AddressRequest): Step<null> {
670
+ const space = spaceOf(request);
671
+ if (operandAt(db, request.address, space) === undefined) throw new KnowledgeError("not-found", `There is no operand expression at ${hex(request.address)}.`);
672
+ change.close("operands", "space = ? AND address = ?", space, request.address);
673
+ change.close('"references"', "space = ? AND from_address = ? AND kind = ?", space, request.address, POINTER_KIND);
674
+ return { current: null };
675
+ }
676
+
677
+ type ModuleRequest = InSpace & { start: number; file: string; title?: string };
678
+
679
+ function checkModule(request: ModuleRequest): void {
680
+ checkSpace(request);
681
+ checkAddress(request.start, "start");
682
+ if (!MODULE_FILE.test(request.file)) throw new KnowledgeError("invalid-input", "A module file is a name of letters, digits, underscores or hyphens (at most 60) that ends in .asm, without a directory.");
683
+ if (isGeneratedFile(request.file)) {
684
+ throw new KnowledgeError("invalid-input", `c64-source writes ${GENERATED_FILES.join(", ")} itself, in any letter case, so a module cannot have the file ${request.file}. Give the module another file.`);
685
+ }
686
+ if (request.title !== undefined && (request.title.trim().length === 0 || request.title.length > MAX_COMMENT)) throw new KnowledgeError("invalid-input", `A module title has 1 to ${MAX_COMMENT} characters.`);
687
+ }
688
+
689
+ function moduleStep(db: DatabaseSync, change: Change, origin: Origin, request: ModuleRequest): Step<ModuleRow> {
690
+ const space = spaceOf(request);
691
+ const existing = moduleAt(db, request.start, space);
692
+ // A module write without a title keeps the title it has (F-26).
693
+ const title = request.title === undefined ? existing?.title : request.title.trim();
694
+ if (existing !== undefined && existing.file === request.file && existing.title === title) return { current: existing };
695
+ const owner = moduleWithFile(db, request.file, space);
696
+ if (owner !== undefined && owner.start !== request.start) {
697
+ throw new KnowledgeError("conflict", `The file ${request.file} already belongs to the module at ${hex(owner.start)}. Give this module another file, or remove that module first.`);
698
+ }
699
+ if (existing !== undefined) change.close("modules", "space = ? AND start_address = ?", space, request.start);
700
+ change.insertModule({ space, start: request.start, file: request.file, ...(title === undefined ? {} : { title }), origin: origin as ModuleRow["origin"] });
701
+ return { current: moduleAt(db, request.start, space)! };
702
+ }
703
+
704
+ function unmoduleStep(db: DatabaseSync, change: Change, _origin: Origin, request: InSpace & { start: number }): Step<null> {
705
+ const space = spaceOf(request);
706
+ if (moduleAt(db, request.start, space) === undefined) throw new KnowledgeError("not-found", `No module starts at ${hex(request.start)}.`);
707
+ change.close("modules", "space = ? AND start_address = ?", space, request.start);
708
+ return { current: null };
709
+ }
710
+
711
+ /**
712
+ * Refuses ACME text whose braces do not pair outside comments and strings:
713
+ * c64-source writes patch code and a macro body inside { }.
714
+ */
715
+ function checkBraces(text: string, what: string): void {
716
+ let depth = 0;
717
+ for (const line of text.split("\n")) {
718
+ let quote: string | undefined;
719
+ for (let index = 0; index < line.length; index++) {
720
+ const character = line[index]!;
721
+ if (quote !== undefined) {
722
+ if (character === "\\") index++;
723
+ else if (character === quote) quote = undefined;
724
+ continue;
725
+ }
726
+ if (character === ";") break;
727
+ if (character === '"' || character === "'") quote = character;
728
+ else if (character === "{") depth++;
729
+ else if (character === "}" && --depth < 0) throw new KnowledgeError("invalid-input", `${what} has a } with no { before it.`);
730
+ }
731
+ }
732
+ if (depth !== 0) throw new KnowledgeError("invalid-input", `${what} has a { with no }.`);
733
+ }
734
+
735
+ function checkCommentText(comment: string | undefined, what: string): void {
736
+ if (comment !== undefined && (comment.trim().length === 0 || comment.length > MAX_COMMENT)) throw new KnowledgeError("invalid-input", `${what} has 1 to ${MAX_COMMENT} characters.`);
737
+ }
738
+
739
+ /** Refuses a range with a byte outside the image. */
740
+ function checkInImage(image: OperandImage, start: number, end: number, what: string): void {
741
+ for (let address = start; address <= end; address++) {
742
+ if (image.byteAt(address) === undefined) throw new KnowledgeError("invalid-input", `${hex(address)} is outside the image: ${what} is a range of the image.`);
743
+ }
744
+ }
745
+
746
+ type PatchRequest = InSpace & { start: number; end: number; switch: string; code: string; comment?: string };
747
+
748
+ function checkPatch(request: PatchRequest): void {
749
+ checkRange(request);
750
+ if (typeof request.switch !== "string" || !SWITCH_NAME.test(request.switch)) {
751
+ 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.");
752
+ }
753
+ 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.`);
754
+ checkBraces(request.code, "The patch code");
755
+ checkCommentText(request.comment, "A patch comment");
756
+ }
757
+
758
+ function patchStep(db: DatabaseSync, change: Change, context: ChangeContext & { image?: OperandImage }, request: PatchRequest): Step<PatchRow, PatchRow> {
759
+ const space = spaceOf(request);
760
+ const range = `${hex(request.start)}-${hex(request.end)}`;
761
+ if (context.image === undefined) throw new KnowledgeError("invalid-input", "A patch is checked against the image, and no image was given.");
762
+ checkInImage(context.image, request.start, request.end, "a patch");
763
+ const symbol = symbolNamed(db, request.switch, space);
764
+ 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.`);
765
+ 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.`);
766
+ const other = patchesOverlapping(db, request.start, request.end, space).find((row) => row.start !== request.start);
767
+ if (other !== undefined) {
768
+ 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).`);
769
+ }
770
+ const [use] = macroUsesOverlapping(db, request.start, request.end, space);
771
+ if (use !== undefined) {
772
+ 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).`);
773
+ }
774
+ const existing = patchesOverlapping(db, request.start, request.start, space).find((row) => row.start === request.start);
775
+ const code = request.code.trimEnd();
776
+ // A patch write without a comment keeps the comment it has, as a module keeps its title.
777
+ const comment = request.comment === undefined ? existing?.comment : request.comment.trim();
778
+ if (existing !== undefined && existing.end === request.end && existing.switch === request.switch && existing.code === code && existing.comment === comment) return { current: existing };
779
+ const origin = context.origin as PatchRow["origin"];
780
+ if (existing !== undefined) change.close("patches", "space = ? AND start_address = ?", space, request.start);
781
+ change.insertPatch({ space, start: request.start, end: request.end, switch: request.switch, code, ...(comment === undefined ? {} : { comment }), origin });
782
+ const current = patchesOverlapping(db, request.start, request.start, space).find((row) => row.start === request.start)!;
783
+ return existing !== undefined && existing.origin !== origin ? { current, previous: existing } : { current };
784
+ }
785
+
786
+ function unpatchStep(db: DatabaseSync, change: Change, _origin: Origin, request: InSpace & { start: number }): Step<null> {
787
+ const space = spaceOf(request);
788
+ 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)}.`);
789
+ change.close("patches", "space = ? AND start_address = ?", space, request.start);
790
+ return { current: null };
791
+ }
792
+
793
+ type MacroRequest = InSpace & { name: string; params?: string[]; body: string; comment?: string };
794
+
795
+ function checkMacroName(name: unknown, what = "A macro name"): void {
796
+ 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.`);
797
+ }
798
+
799
+ function checkMacro(request: MacroRequest): void {
800
+ checkSpace(request);
801
+ checkMacroName(request.name);
802
+ const params = request.params ?? [];
803
+ if (!Array.isArray(params) || params.length > MAX_PARAMS) throw new KnowledgeError("invalid-input", `A macro has 0 to ${MAX_PARAMS} params.`);
804
+ for (const param of params) {
805
+ if (typeof param !== "string" || !MACRO_PARAM.test(param)) {
806
+ 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.");
807
+ }
808
+ }
809
+ if (new Set(params).size !== params.length) throw new KnowledgeError("invalid-input", "The params of a macro have different names.");
810
+ 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.`);
811
+ checkBraces(request.body, "The macro body");
812
+ checkCommentText(request.comment, "A macro comment");
813
+ }
814
+
815
+ function macroStep(db: DatabaseSync, change: Change, origin: Origin, request: MacroRequest): Step<MacroRow, MacroRow> {
816
+ const space = spaceOf(request);
817
+ const existing = macroNamed(db, request.name, space);
818
+ const params = request.params ?? [];
819
+ const body = request.body.trimEnd();
820
+ const comment = request.comment === undefined ? existing?.comment : request.comment.trim();
821
+ if (existing !== undefined && listText(existing.params) === listText(params) && existing.body === body && existing.comment === comment) return { current: existing };
822
+ if (existing !== undefined && existing.params.length !== params.length) {
823
+ const uses = listMacroUses(db, undefined, request.name, undefined, space);
824
+ if (uses.length > 0) {
825
+ throw new KnowledgeError(
826
+ "conflict",
827
+ `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.`,
828
+ );
829
+ }
830
+ }
831
+ if (existing !== undefined) change.close("macros", "space = ? AND name = ?", space, request.name);
832
+ change.insertMacro({ space, name: request.name, params, body, ...(comment === undefined ? {} : { comment }), origin: origin as MacroRow["origin"] });
833
+ const current = macroNamed(db, request.name, space)!;
834
+ return existing !== undefined && existing.origin !== origin ? { current, previous: existing } : { current };
835
+ }
836
+
837
+ function unmacroStep(db: DatabaseSync, change: Change, _origin: Origin, request: InSpace & { name: string }): Step<null> {
838
+ const space = spaceOf(request);
839
+ if (macroNamed(db, request.name, space) === undefined) throw new KnowledgeError("not-found", `There is no macro ${request.name}.`);
840
+ const uses = listMacroUses(db, undefined, request.name, undefined, space);
841
+ 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).`);
842
+ change.close("macros", "space = ? AND name = ?", space, request.name);
843
+ return { current: null };
844
+ }
845
+
846
+ type MacroUseRequest = InSpace & { start: number; end: number; macro: string; args?: string[] };
847
+
848
+ function checkMacroUse(request: MacroUseRequest): void {
849
+ checkRange(request);
850
+ checkMacroName(request.macro, "The macro of a use");
851
+ const args = request.args ?? [];
852
+ if (!Array.isArray(args) || args.length > MAX_PARAMS) throw new KnowledgeError("invalid-input", `A macro use has 0 to ${MAX_PARAMS} args.`);
853
+ for (const arg of args) {
854
+ if (typeof arg !== "string") throw new KnowledgeError("invalid-input", "Each arg of a macro use is expression text.");
855
+ try {
856
+ checkExpression(arg);
857
+ } catch (error) {
858
+ if (error instanceof ExpressionError) throw new KnowledgeError("invalid-input", error.message);
859
+ throw error;
860
+ }
861
+ }
862
+ }
863
+
864
+ /** Current macro uses that name a symbol in an arg. */
865
+ function usesNaming(db: DatabaseSync, space: Space, name: string): MacroUseRow[] {
866
+ return listMacroUses(db, undefined, undefined, undefined, space).filter((row) => row.args.some((arg) => identifiers(arg).includes(name)));
867
+ }
868
+
869
+ /** A rename of a symbol renames it in the args of every macro use; the rows keep their origin. */
870
+ function renameInUses(db: DatabaseSync, change: Change, space: Space, from: string, to: string): void {
871
+ for (const row of usesNaming(db, space, from)) {
872
+ change.close("macro_uses", "space = ? AND start_address = ?", space, row.start);
873
+ change.insertMacroUse({ ...row, args: row.args.map((arg) => replaceIdentifiers(arg, (name) => (name === from ? to : name))) });
874
+ }
875
+ }
876
+
877
+ function useStep(db: DatabaseSync, change: Change, context: ChangeContext & { image?: OperandImage }, request: MacroUseRequest): Step<MacroUseRow, MacroUseRow> {
878
+ const space = spaceOf(request);
879
+ const image = context.image;
880
+ const range = `${hex(request.start)}-${hex(request.end)}`;
881
+ if (image === undefined) throw new KnowledgeError("invalid-input", "A macro use is checked against the image, and no image was given.");
882
+ const macro = macroNamed(db, request.macro, space);
883
+ if (macro === undefined) throw new KnowledgeError("not-found", `There is no macro ${request.macro}. Write it first (macro).`);
884
+ const args = (request.args ?? []).map((arg) => arg.trim());
885
+ if (args.length !== macro.params.length) {
886
+ throw new KnowledgeError("invalid-input", `The macro ${macro.name} has ${macro.params.length} params (${listText(macro.params)}), and the use gives ${args.length} args.`);
887
+ }
888
+ const resolve = (name: string) => symbolNamed(db, name, space)?.address ?? (space === DEFAULT_SPACE ? image.platform.get(name) : undefined);
889
+ for (const arg of args) {
890
+ try {
891
+ evaluate(arg, resolve);
892
+ } catch (error) {
893
+ if (error instanceof ExpressionError) throw new KnowledgeError("invalid-input", `${range}: ${error.message}`);
894
+ throw error;
895
+ }
896
+ }
897
+ checkInImage(image, request.start, request.end, "a macro use");
898
+ let next = request.start;
899
+ while (next <= request.end) next += instructionLength(image.byteAt(next)!);
900
+ if (next !== request.end + 1) {
901
+ 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)}.`);
902
+ }
903
+ const [patch] = patchesOverlapping(db, request.start, request.end, space);
904
+ if (patch !== undefined) {
905
+ 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).`);
906
+ }
907
+ const other = macroUsesOverlapping(db, request.start, request.end, space).find((row) => row.start !== request.start);
908
+ if (other !== undefined) {
909
+ 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).`);
910
+ }
911
+ const existing = macroUsesOverlapping(db, request.start, request.start, space).find((row) => row.start === request.start);
912
+ if (existing !== undefined && existing.end === request.end && existing.macro === request.macro && listText(existing.args) === listText(args)) return { current: existing };
913
+ const origin = context.origin as MacroUseRow["origin"];
914
+ if (existing !== undefined) change.close("macro_uses", "space = ? AND start_address = ?", space, request.start);
915
+ change.insertMacroUse({ space, start: request.start, end: request.end, macro: request.macro, args, origin });
916
+ const current = macroUsesOverlapping(db, request.start, request.start, space).find((row) => row.start === request.start)!;
917
+ return existing !== undefined && existing.origin !== origin ? { current, previous: existing } : { current };
918
+ }
919
+
920
+ function unuseStep(db: DatabaseSync, change: Change, _origin: Origin, request: InSpace & { start: number }): Step<null> {
921
+ const space = spaceOf(request);
922
+ 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)}.`);
923
+ change.close("macro_uses", "space = ? AND start_address = ?", space, request.start);
924
+ return { current: null };
925
+ }
926
+
927
+ /** Names the symbol at an address (one current symbol per address, unique names). */
928
+ export function renameSymbol(db: DatabaseSync, context: WriteContext, request: SymbolRequest): WriteResult<SymbolRow, SymbolRow> {
929
+ checkSymbol(request);
930
+ return result(write(db, context, "rename-symbol", (change) => renameStep(db, change, context.origin, request)));
931
+ }
932
+
933
+ export function removeSymbol(db: DatabaseSync, context: WriteContext, request: AddressRequest): WriteResult<null> {
934
+ checkSpace(request);
935
+ checkAddress(request.address);
936
+ return result(write(db, context, "remove-symbol", (change) => removeSymbolStep(db, change, context.origin, request)));
937
+ }
938
+
939
+ /** Classifies [start, end]; overlapping regions are split so current regions never overlap. */
940
+ export function classifyRegion(db: DatabaseSync, context: WriteContext, request: RegionRequest): WriteResult<RegionRow[], RegionRow[]> {
941
+ checkRegion(request);
942
+ return result(write(db, context, "classify-region", (change) => classifyStep(db, change, context.origin, request)));
943
+ }
944
+
945
+ /** Makes [start, end] unclassified again; parts of regions outside it stay. */
946
+ export function unclassifyRegion(db: DatabaseSync, context: WriteContext, request: RangeRequest): WriteResult<null> {
947
+ checkRange(request);
948
+ return result(write(db, context, "unclassify-region", (change) => unclassifyStep(db, change, context.origin, request)));
949
+ }
950
+
951
+ export function setComment(db: DatabaseSync, context: WriteContext, request: CommentRequest): WriteResult<CommentRow> {
952
+ checkComment(request);
953
+ return result(write(db, context, "set-comment", (change) => commentStep(db, change, context.origin, request)));
954
+ }
955
+
956
+ export function removeComment(db: DatabaseSync, context: WriteContext, request: PlacementRequest): WriteResult<null> {
957
+ checkPlacement(request);
958
+ return result(write(db, context, "remove-comment", (change) => uncommentStep(db, change, context.origin, request)));
959
+ }
960
+
961
+ export function addReference(db: DatabaseSync, context: WriteContext, request: ReferenceRequest): WriteResult<ReferenceRow> {
962
+ checkReference(request);
963
+ return result(write(db, context, "add-reference", (change) => referenceStep(db, change, context.origin, request)));
964
+ }
965
+
966
+ export function removeReference(db: DatabaseSync, context: WriteContext, request: ReferenceRequest): WriteResult<null> {
967
+ checkReference(request);
968
+ return result(write(db, context, "remove-reference", (change) => unreferenceStep(db, change, context.origin, request)));
969
+ }
970
+
971
+ /** Starts a file of the generated source at an address (D48); one module per address and per file. */
972
+ export function setModule(db: DatabaseSync, context: WriteContext, request: ModuleRequest): WriteResult<ModuleRow> {
973
+ checkModule(request);
974
+ return result(write(db, context, "set-module", (change) => moduleStep(db, change, context.origin, request)));
975
+ }
976
+
977
+ export function removeModule(db: DatabaseSync, context: WriteContext, request: InSpace & { start: number }): WriteResult<null> {
978
+ checkSpace(request);
979
+ checkAddress(request.start, "start");
980
+ return result(write(db, context, "remove-module", (change) => unmoduleStep(db, change, context.origin, request)));
981
+ }
982
+
983
+ /**
984
+ * Writes the operand expression of the instruction at an address (D65), after
985
+ * a check against the image, and a pointer reference to each name it uses.
986
+ */
987
+ export function setOperand(db: DatabaseSync, context: WriteContext, request: OperandRequest): WriteResult<OperandRow, OperandRow> {
988
+ checkOperand(request);
989
+ return result(write(db, context, "set-operand", (change) => operandStep(db, change, context, request)));
990
+ }
991
+
992
+ export function removeOperand(db: DatabaseSync, context: WriteContext, request: AddressRequest): WriteResult<null> {
993
+ checkSpace(request);
994
+ checkAddress(request.address);
995
+ return result(write(db, context, "remove-operand", (change) => unoperandStep(db, change, context.origin, request)));
996
+ }
997
+
998
+ /**
999
+ * Writes code that replaces [start, end] when the build defines the switch
1000
+ * (D72), after a check that the range is in the image and overlaps no other
1001
+ * patch or macro use.
1002
+ */
1003
+ export function setPatch(db: DatabaseSync, context: WriteContext, request: PatchRequest): WriteResult<PatchRow, PatchRow> {
1004
+ checkPatch(request);
1005
+ return result(write(db, context, "set-patch", (change) => patchStep(db, change, context, request)));
1006
+ }
1007
+
1008
+ export function removePatch(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-patch", (change) => unpatchStep(db, change, context.origin, request)));
1012
+ }
1013
+
1014
+ /** Writes a macro of the generated source (D73). */
1015
+ export function setMacro(db: DatabaseSync, context: WriteContext, request: MacroRequest): WriteResult<MacroRow, MacroRow> {
1016
+ checkMacro(request);
1017
+ return result(write(db, context, "set-macro", (change) => macroStep(db, change, context.origin, request)));
1018
+ }
1019
+
1020
+ export function removeMacro(db: DatabaseSync, context: WriteContext, request: InSpace & { name: string }): WriteResult<null> {
1021
+ checkSpace(request);
1022
+ checkMacroName(request.name);
1023
+ return result(write(db, context, "remove-macro", (change) => unmacroStep(db, change, context.origin, request)));
1024
+ }
1025
+
1026
+ /** Writes the instructions in [start, end] as one use of a macro (D73), after a check against the image. */
1027
+ export function setMacroUse(db: DatabaseSync, context: WriteContext, request: MacroUseRequest): WriteResult<MacroUseRow, MacroUseRow> {
1028
+ checkMacroUse(request);
1029
+ return result(write(db, context, "set-macro-use", (change) => useStep(db, change, context, request)));
1030
+ }
1031
+
1032
+ export function removeMacroUse(db: DatabaseSync, context: WriteContext, request: InSpace & { start: number }): WriteResult<null> {
1033
+ checkSpace(request);
1034
+ checkAddress(request.start, "start");
1035
+ return result(write(db, context, "remove-macro-use", (change) => unuseStep(db, change, context.origin, request)));
1036
+ }
1037
+
1038
+ /** One write of a batch (D42), named as the command that does it alone. */
1039
+ export type BatchOperation =
1040
+ | ({ op: "rename" } & SymbolRequest)
1041
+ | ({ op: "remove-symbol" } & AddressRequest)
1042
+ | ({ op: "classify" } & RegionRequest)
1043
+ | ({ op: "unclassify" } & RangeRequest)
1044
+ | ({ op: "comment" } & CommentRequest)
1045
+ | ({ op: "uncomment" } & PlacementRequest)
1046
+ | ({ op: "reference" } & ReferenceRequest)
1047
+ | ({ op: "unreference" } & ReferenceRequest)
1048
+ | ({ op: "module" } & ModuleRequest)
1049
+ | ({ op: "unmodule" } & InSpace & { start: number })
1050
+ | ({ op: "operand" } & OperandRequest)
1051
+ | ({ op: "unoperand" } & AddressRequest)
1052
+ | ({ op: "patch" } & PatchRequest)
1053
+ | ({ op: "unpatch" } & InSpace & { start: number })
1054
+ | ({ op: "macro" } & MacroRequest)
1055
+ | ({ op: "unmacro" } & InSpace & { name: string })
1056
+ | ({ op: "use" } & MacroUseRequest)
1057
+ | ({ op: "unuse" } & InSpace & { start: number });
1058
+
1059
+ export const MAX_BATCH = 10_000;
1060
+
1061
+ /** The result of each write of a batch, in order. */
1062
+ export type BatchStep = Step<
1063
+ SymbolRow | RegionRow[] | CommentRow | ReferenceRow | ModuleRow | OperandRow | PatchRow | MacroRow | MacroUseRow | null,
1064
+ SymbolRow | RegionRow[] | OperandRow | PatchRow | MacroRow | MacroUseRow
1065
+ >;
1066
+
1067
+ /**
1068
+ * Applies many writes as one revision in one transaction (D42): all of them,
1069
+ * or none when one is refused. Every write is checked before anything opens.
1070
+ */
1071
+ export function applyBatch(db: DatabaseSync, context: WriteContext, operations: BatchOperation[]): { revision: string | null; steps: BatchStep[] } {
1072
+ if (operations.length === 0 || operations.length > MAX_BATCH) throw new KnowledgeError("invalid-input", `A batch has 1 to ${MAX_BATCH} writes.`);
1073
+ operations.forEach((operation, index) => {
1074
+ try {
1075
+ switch (operation.op) {
1076
+ case "rename":
1077
+ return checkSymbol(operation);
1078
+ case "remove-symbol":
1079
+ checkSpace(operation);
1080
+ return checkAddress(operation.address);
1081
+ case "classify":
1082
+ return checkRegion(operation);
1083
+ case "unclassify":
1084
+ return checkRange(operation);
1085
+ case "comment":
1086
+ return void checkComment(operation);
1087
+ case "uncomment":
1088
+ return checkPlacement(operation);
1089
+ case "reference":
1090
+ case "unreference":
1091
+ return checkReference(operation);
1092
+ case "module":
1093
+ return checkModule(operation);
1094
+ case "unmodule":
1095
+ checkSpace(operation);
1096
+ return checkAddress(operation.start, "start");
1097
+ case "operand":
1098
+ return checkOperand(operation);
1099
+ case "unoperand":
1100
+ checkSpace(operation);
1101
+ return checkAddress(operation.address);
1102
+ case "patch":
1103
+ return checkPatch(operation);
1104
+ case "unpatch":
1105
+ case "unuse":
1106
+ checkSpace(operation);
1107
+ return checkAddress(operation.start, "start");
1108
+ case "macro":
1109
+ return checkMacro(operation);
1110
+ case "unmacro":
1111
+ checkSpace(operation);
1112
+ return checkMacroName(operation.name);
1113
+ case "use":
1114
+ return checkMacroUse(operation);
1115
+ default:
1116
+ throw new KnowledgeError(
1117
+ "invalid-input",
1118
+ `op must be one of rename, remove-symbol, classify, unclassify, comment, uncomment, reference, unreference, module, unmodule, operand, unoperand, patch, unpatch, macro, unmacro, use, unuse.`,
1119
+ );
1120
+ }
1121
+ } catch (error) {
1122
+ if (error instanceof KnowledgeError) throw new KnowledgeError(error.code, `Write ${index + 1}: ${error.message} Nothing was written.`);
1123
+ throw error;
303
1124
  }
304
- change.close('"references"', "from_address = ? AND to_address = ? AND kind = ?", request.from, request.to, request.kind);
305
- return null;
306
1125
  });
1126
+ const written = write(db, context, "apply", (change) =>
1127
+ operations.map((operation, index): BatchStep => {
1128
+ try {
1129
+ switch (operation.op) {
1130
+ case "rename":
1131
+ return renameStep(db, change, context.origin, operation);
1132
+ case "remove-symbol":
1133
+ return removeSymbolStep(db, change, context.origin, operation);
1134
+ case "classify":
1135
+ return classifyStep(db, change, context.origin, operation);
1136
+ case "unclassify":
1137
+ return unclassifyStep(db, change, context.origin, operation);
1138
+ case "comment":
1139
+ return commentStep(db, change, context.origin, operation);
1140
+ case "uncomment":
1141
+ return uncommentStep(db, change, context.origin, operation);
1142
+ case "reference":
1143
+ return referenceStep(db, change, context.origin, operation);
1144
+ case "unreference":
1145
+ return unreferenceStep(db, change, context.origin, operation);
1146
+ case "module":
1147
+ return moduleStep(db, change, context.origin, operation);
1148
+ case "unmodule":
1149
+ return unmoduleStep(db, change, context.origin, operation);
1150
+ case "operand":
1151
+ return operandStep(db, change, context, operation);
1152
+ case "unoperand":
1153
+ return unoperandStep(db, change, context.origin, operation);
1154
+ case "patch":
1155
+ return patchStep(db, change, context, operation);
1156
+ case "unpatch":
1157
+ return unpatchStep(db, change, context.origin, operation);
1158
+ case "macro":
1159
+ return macroStep(db, change, context.origin, operation);
1160
+ case "unmacro":
1161
+ return unmacroStep(db, change, context.origin, operation);
1162
+ case "use":
1163
+ return useStep(db, change, context, operation);
1164
+ case "unuse":
1165
+ return unuseStep(db, change, context.origin, operation);
1166
+ }
1167
+ } catch (error) {
1168
+ if (error instanceof KnowledgeError) throw new KnowledgeError(error.code, `Write ${index + 1}: ${error.message} Nothing was written.`);
1169
+ throw error;
1170
+ }
1171
+ }),
1172
+ );
1173
+ return { revision: written.revision, steps: written.current };
307
1174
  }
308
1175
 
309
1176
  /** Row content without its revision, as a comparable key. */
1177
+ /** The tables of the program (D83) and the columns that make a row. */
1178
+ const PROGRAM_TABLES = [
1179
+ ["memory", "space, address, bytes, origin"],
1180
+ ["entries", "space, address, state, stages, origin"],
1181
+ ["coverage", "space, kind, start_address, end_address, origin"],
1182
+ ] as const;
1183
+
310
1184
  const KEYS = {
311
- symbols: { columns: SYMBOL_COLUMNS, map: toSymbol, key: (row: SymbolRow) => `${row.address}|${row.name}|${row.kind}|${row.origin}` },
312
- regions: { columns: REGION_COLUMNS, map: toRegion, key: (row: RegionRow) => `${row.start}|${row.end}|${row.type}|${row.origin}` },
313
- comments: { columns: COMMENT_COLUMNS, map: toComment, key: (row: CommentRow) => `${row.address}|${row.placement}|${row.text}|${row.origin}` },
314
- references: { columns: REFERENCE_COLUMNS, map: toReference, key: (row: ReferenceRow) => `${row.from}|${row.to}|${row.kind}|${row.origin}` },
1185
+ symbols: { columns: SYMBOL_COLUMNS, map: toSymbol, key: (row: SymbolRow) => `${row.space}|${row.address}|${row.name}|${row.kind}|${row.origin}` },
1186
+ analyzer_names: { columns: SYMBOL_COLUMNS, map: toSymbol, key: (row: SymbolRow) => `${row.space}|${row.address}|${row.name}|${row.kind}|${row.origin}` },
1187
+ regions: { columns: REGION_COLUMNS, map: toRegion, key: (row: RegionRow) => `${row.space}|${row.start}|${row.end}|${row.type}|${row.element ?? ""}|${row.origin}` },
1188
+ comments: { columns: COMMENT_COLUMNS, map: toComment, key: (row: CommentRow) => `${row.space}|${row.address}|${row.placement}|${row.text}|${row.origin}` },
1189
+ references: { columns: REFERENCE_COLUMNS, map: toReference, key: (row: ReferenceRow) => `${row.space}|${row.from}|${row.to}|${row.kind}|${row.origin}` },
1190
+ modules: { columns: MODULE_COLUMNS, map: toModule, key: (row: ModuleRow) => `${row.space}|${row.start}|${row.file}|${row.title ?? ""}|${row.origin}` },
1191
+ operands: {
1192
+ columns: OPERAND_COLUMNS,
1193
+ map: toOperand,
1194
+ key: (row: OperandRow) => `${row.space}|${row.address}|${row.select}|${row.symbol ?? ""}|${row.offset}|${row.expression ?? ""}|${row.origin}`,
1195
+ },
1196
+ patches: {
1197
+ columns: PATCH_COLUMNS,
1198
+ map: toPatch,
1199
+ key: (row: PatchRow) => `${row.space}|${row.start}|${row.end}|${row.switch}|${row.code}|${row.comment ?? ""}|${row.origin}`,
1200
+ },
1201
+ macros: { columns: MACRO_COLUMNS, map: toMacro, key: (row: MacroRow) => `${row.space}|${row.name}|${listText(row.params)}|${row.body}|${row.comment ?? ""}|${row.origin}` },
1202
+ macro_uses: { columns: MACRO_USE_COLUMNS, map: toMacroUse, key: (row: MacroUseRow) => `${row.space}|${row.start}|${row.end}|${row.macro}|${listText(row.args)}|${row.origin}` },
1203
+ suppressions: {
1204
+ columns: SUPPRESSION_COLUMNS,
1205
+ map: toSuppression,
1206
+ key: (row: SuppressionRow) => `${row.space}|${row.entity}|${row.start}|${row.end}|${row.to ?? ""}|${row.kind ?? ""}|${row.origin}`,
1207
+ },
315
1208
  } as const;
316
1209
 
317
1210
  /**
318
1211
  * Makes the current knowledge equal to what it was at `revision`, as a new
319
1212
  * revision. History is never erased; restored rows keep their origin.
320
1213
  */
321
- export function revert(db: DatabaseSync, context: WriteContext, request: { revision: number }): WriteResult<{ restoredRevision: number }> {
1214
+ export function revert(db: DatabaseSync, context: WriteContext, request: { revision: string }): WriteResult<{ restoredRevision: string }> {
322
1215
  return write(db, context, "revert", (change) => {
323
- const current = currentRevision(db);
324
- if (!Number.isInteger(request.revision) || request.revision < 0 || request.revision > current) {
325
- throw new KnowledgeError("not-found", `There is no revision ${request.revision}; knowledge is at revision ${current}.`);
326
- }
1216
+ const restored = resolveRevision(db, request.revision);
327
1217
  const at = "valid_from_revision <= ? AND (valid_to_revision IS NULL OR valid_to_revision > ?)";
328
1218
  for (const [table, spec] of Object.entries(KEYS)) {
329
1219
  const name = table === "references" ? '"references"' : table;
330
- const then = db.prepare(`SELECT ${spec.columns}, id FROM ${name} WHERE ${at}`).all(request.revision, request.revision) as Array<Record<string, unknown>>;
1220
+ const then = db.prepare(`SELECT ${spec.columns}, id FROM ${name} WHERE ${at}`).all(restored, restored) as Array<Record<string, unknown>>;
331
1221
  const now = db.prepare(`SELECT ${spec.columns}, id FROM ${name} WHERE valid_to_revision IS NULL`).all() as Array<Record<string, unknown>>;
332
1222
  const key = (row: Record<string, unknown>) => (spec.key as (row: unknown) => string)(spec.map(row as never));
333
1223
  const thenKeys = new Set(then.map(key));
334
1224
  const nowKeys = new Set(now.map(key));
335
1225
  // Close first, so restored rows never collide with the current-row rules.
336
- for (const row of now) if (!thenKeys.has(key(row))) change.close(name, "id = ?", row.id as number);
1226
+ for (const row of now) if (!thenKeys.has(key(row))) change.close(name, "id = ?", row.id as string);
337
1227
  for (const row of then) {
338
1228
  if (nowKeys.has(key(row))) continue;
1229
+ if (table === "suppressions") {
1230
+ change.insertSuppression(spec.map(row as never) as SuppressionRow);
1231
+ continue;
1232
+ }
1233
+ if (table === "modules") {
1234
+ change.insertModule(spec.map(row as never) as ModuleRow);
1235
+ continue;
1236
+ }
1237
+ if (table === "operands") {
1238
+ change.insertOperand(spec.map(row as never) as OperandRow);
1239
+ continue;
1240
+ }
1241
+ if (table === "patches") {
1242
+ change.insertPatch(spec.map(row as never) as PatchRow);
1243
+ continue;
1244
+ }
1245
+ if (table === "macros") {
1246
+ change.insertMacro(spec.map(row as never) as MacroRow);
1247
+ continue;
1248
+ }
1249
+ if (table === "macro_uses") {
1250
+ change.insertMacroUse(spec.map(row as never) as MacroUseRow);
1251
+ continue;
1252
+ }
339
1253
  const mapped = spec.map(row as never) as SymbolRow & RegionRow & CommentRow & ReferenceRow;
340
1254
  if (table === "symbols") change.insertSymbol(mapped);
1255
+ else if (table === "analyzer_names") change.insertAnalyzerName(mapped);
341
1256
  else if (table === "regions") change.insertRegion(mapped);
342
1257
  else if (table === "comments") change.insertComment(mapped);
343
1258
  else change.insertReference(mapped);
344
1259
  }
345
1260
  }
346
- return { restoredRevision: request.revision };
1261
+ // The program (D83): a row is the same when all its values are the same.
1262
+ for (const [table, columns] of PROGRAM_TABLES) {
1263
+ const then = db.prepare(`SELECT ${columns}, id FROM ${table} WHERE ${at}`).all(restored, restored) as Array<Record<string, unknown>>;
1264
+ const now = db.prepare(`SELECT ${columns}, id FROM ${table} WHERE valid_to_revision IS NULL`).all() as Array<Record<string, unknown>>;
1265
+ const key = (row: Record<string, unknown>) => JSON.stringify(columns.split(", ").map((column) => row[column]));
1266
+ const thenKeys = new Set(then.map(key));
1267
+ const nowKeys = new Set(now.map(key));
1268
+ for (const row of now) if (!thenKeys.has(key(row))) change.close(table, "id = ?", row.id as string);
1269
+ for (const row of then) {
1270
+ if (nowKeys.has(key(row))) continue;
1271
+ const values = row as never;
1272
+ if (table === "memory") change.insertMemory(values);
1273
+ else if (table === "entries") change.insertEntry(values);
1274
+ else change.insertCoverage({ ...(values as { space: Space; kind: string; origin: Origin }), start: row.start_address as number, end: row.end_address as number });
1275
+ }
1276
+ }
1277
+ return { restoredRevision: restored };
347
1278
  });
348
1279
  }
1280
+
1281
+ /** What a merge decided for one conflict: the newest row stays and the older rows close. */
1282
+ export interface MergeChoice {
1283
+ rule: Conflict["rule"];
1284
+ space: Space;
1285
+ kept: ConflictRow;
1286
+ closed: ConflictRow[];
1287
+ }
1288
+
1289
+ /** The most choices one merge makes; more means the knowledge text is broken. */
1290
+ const MAX_CHOICES = 100_000;
1291
+
1292
+ /**
1293
+ * Decides every conflict of a git merge in one merge revision (D69). Of the
1294
+ * rows of one conflict, the row of the newest revision stays and the others
1295
+ * close; an older region keeps its parts outside the newer regions. Where an
1296
+ * operand expression closes, the pointer references of its address follow the
1297
+ * expression that stays. `platform` gives the C64 platform names.
1298
+ */
1299
+ export function mergeConflicts(db: DatabaseSync, context: WriteContext, platform: ReadonlyMap<string, number>): WriteResult<MergeChoice[]> {
1300
+ return write(db, context, "merge", (change) => {
1301
+ const choices: MergeChoice[] = [];
1302
+ for (let found = conflicts(db); found.length > 0; found = conflicts(db)) {
1303
+ if (choices.length >= MAX_CHOICES) throw new KnowledgeError("invalid-database", `The merge made ${MAX_CHOICES} choices and still finds conflicts. Nothing was written.`);
1304
+ const conflict = found[0]!;
1305
+ const newest = conflict.rows.at(-1)!;
1306
+ const older = conflict.rows.slice(0, -1);
1307
+ const table = sqlName(conflictTable(conflict.rule));
1308
+ for (const entry of older) change.close(table, "id = ?", entry.id);
1309
+ if (conflict.rule === "region" || conflict.rule === "suppression-region") keepOutside(change, conflict, conflict.rule === "region");
1310
+ if (conflict.rule === "operand") followOperand(db, change, newest.row as OperandRow, platform);
1311
+ choices.push({ rule: conflict.rule, space: conflict.space, kept: newest, closed: older });
1312
+ }
1313
+ return choices;
1314
+ });
1315
+ }
1316
+
1317
+ /** Puts back the parts of the older ranges of a conflict that no newer range covers. */
1318
+ function keepOutside(change: Change, conflict: Conflict, regions: boolean): void {
1319
+ const newer: AddressRange[] = [];
1320
+ for (const entry of [...conflict.rows].reverse()) {
1321
+ const row = entry.row as RegionRow & SuppressionRow;
1322
+ if (newer.length > 0) {
1323
+ for (const piece of subtract({ start: row.start, end: row.end }, newer)) {
1324
+ if (regions) change.insertRegion({ space: row.space, ...piece, type: row.type, ...elementOf(piece, row.element), origin: row.origin });
1325
+ else change.insertSuppression({ space: row.space, entity: "region", ...piece, origin: row.origin });
1326
+ }
1327
+ }
1328
+ newer.push({ start: row.start, end: row.end });
1329
+ }
1330
+ }
1331
+
1332
+ /** Makes the pointer references of an address the ones of the operand expression that stays there. */
1333
+ function followOperand(db: DatabaseSync, change: Change, kept: OperandRow, platform: ReadonlyMap<string, number>): void {
1334
+ const resolve = (name: string) => symbolNamed(db, name, kept.space)?.address ?? (kept.space === DEFAULT_SPACE ? platform.get(name) : undefined);
1335
+ const targets = new Set(identifiers(operandText(kept)).flatMap((name) => resolve(name) ?? []));
1336
+ const pointers = referencesFrom(db, kept.address, kept.space).filter((reference) => reference.kind === POINTER_KIND);
1337
+ for (const reference of pointers) {
1338
+ if (!targets.has(reference.to)) change.close('"references"', "space = ? AND from_address = ? AND to_address = ? AND kind = ?", kept.space, kept.address, reference.to, POINTER_KIND);
1339
+ }
1340
+ for (const to of targets) {
1341
+ if (!pointers.some((reference) => reference.to === to)) change.insertReference({ space: kept.space, from: kept.address, to, kind: POINTER_KIND, origin: kept.origin });
1342
+ }
1343
+ }