@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
@@ -2,18 +2,29 @@
2
2
  // one VICE. Exposes C64-domain operations only.
3
3
 
4
4
  import { ProtocolError, WireFailure, type VideoStandard } from "../protocol/messages.ts";
5
- import { validateViceResult, type Breakpoint, type BreakpointParams, type Watchpoint, type WatchpointParams, type ExecutionParams, type ExecutionResult, type JoystickState, type MachineStatus, type MemmapRange, type MemoryWriteParams, type RegisterValues, type ResetMode, type RunState, type RunTarget, type RunUntilResult, type MemoryReadParams, type MemoryReadResult, type Registers, type Space, type ViceOperation, type ViceOperations } from "../protocol/vice.ts";
5
+ import { MAX_MEMORY_READ, validateViceResult, type Breakpoint, type BreakpointParams, type PointRemoved, type Watchpoint, type WatchpointParams, type ExecutionParams, type ExecutionResult, type JoystickState, type MachineStatus, type MemmapRange, type MemoryWriteParams, type ProfileEntry, type RegisterValues, type ResetMode, type RunState, type RunTarget, type RunUntilResult, type MemoryReadParams, type MemoryReadResult, type Registers, type Space, type ViceOperation, type ViceOperations } from "../protocol/vice.ts";
6
6
  import { HostConnection } from "./connect.ts";
7
7
  import { readProjectFile } from "./transfer.ts";
8
8
 
9
9
  const HOST_LOST =
10
- "The connection to the c64-re-tools host runtime was lost, and the emulator and its machine state went with it. " +
11
- "Check that the Host Runtime (c64-re-tools-host) is running, then restart the c64-re-tools MCP server.";
10
+ "The connection to the c64-re-tools host runtime was lost. The emulator and its machine state are gone: memory, registers, breakpoints, snapshots and the loaded program. " +
11
+ "The next call starts a new machine when the Host Runtime (c64-re-tools-host) runs. Load the program again and set the breakpoints again.";
12
12
  const SESSION_CLOSED = "The C64 session is closed because the c64-re-tools MCP server stops.";
13
+ const LOST_BEFORE_ANSWER = "The connection to the c64-re-tools host runtime was lost before the emulator answered. No machine state was lost. Do the call again.";
14
+
15
+ /**
16
+ * The loss of a session that never answered (D57): it had no machine state,
17
+ * so the caller can do the call again on a new session.
18
+ */
19
+ export class LostBeforeAnswer extends WireFailure {
20
+ constructor() {
21
+ super("machine-unavailable", LOST_BEFORE_ANSWER);
22
+ }
23
+ }
13
24
 
14
25
  interface Pending {
15
26
  op: ViceOperation;
16
- resolve: (value: unknown) => void;
27
+ resolve: (value: unknown, attachments: Buffer[]) => void;
17
28
  reject: (error: Error) => void;
18
29
  }
19
30
 
@@ -24,21 +35,24 @@ export class ViceSessionClient {
24
35
  readonly #pending = new Map<number, Pending>();
25
36
  #nextId = 1;
26
37
  #lost: WireFailure | undefined;
38
+ #answered = false;
39
+ #lossReported = false;
27
40
 
28
41
  private constructor(connection: HostConnection) {
29
42
  this.#connection = connection;
30
- connection.onMessage((message) => {
43
+ connection.onMessage((message, attachments) => {
31
44
  // Anything but a reply after the handshake breaks the contract.
32
45
  if (message.type !== "reply") return this.#fail();
33
46
  const pending = this.#pending.get(message.id);
34
47
  if (pending === undefined) return; // not ours; ignore
35
48
  this.#pending.delete(message.id);
49
+ this.#answered = true;
36
50
  if ("error" in message) {
37
51
  pending.reject(new WireFailure(message.error.code, message.error.message));
38
52
  return;
39
53
  }
40
54
  try {
41
- pending.resolve(validateViceResult(pending.op, message.result));
55
+ pending.resolve(validateViceResult(pending.op, message.result), attachments);
42
56
  } catch (error) {
43
57
  pending.reject(
44
58
  new WireFailure(
@@ -50,7 +64,8 @@ export class ViceSessionClient {
50
64
  }
51
65
  });
52
66
  this.closed = connection.closed.then(() => {
53
- this.#lost ??= new WireFailure("machine-state-lost", HOST_LOST);
67
+ this.#lost ??= this.#answered ? new WireFailure("machine-state-lost", HOST_LOST) : new LostBeforeAnswer();
68
+ this.#lossReported = this.#pending.size > 0;
54
69
  for (const pending of this.#pending.values()) pending.reject(this.#lost);
55
70
  this.#pending.clear();
56
71
  });
@@ -75,6 +90,11 @@ export class ViceSessionClient {
75
90
  return this.#request("memoryRead", params);
76
91
  }
77
92
 
93
+ /** The I/O registers at $d000-$dfff as the chips hold them, without side effects. */
94
+ ioRead(address: number, size: number): Promise<MemoryReadResult> {
95
+ return this.#request("ioRead", { address, size });
96
+ }
97
+
78
98
  registersGet(space: Space): Promise<Registers> {
79
99
  return this.#request("registersGet", { space });
80
100
  }
@@ -107,21 +127,21 @@ export class ViceSessionClient {
107
127
  return this.#request("runUntil", params);
108
128
  }
109
129
 
110
- /** Loads a project PRG into memory without reset or start; finishes stopped. */
111
- async programLoad(params: { path: string; address?: number }): Promise<{ state: RunState; loadAddress: number; size: number }> {
112
- const file = readProjectFile(params.path);
130
+ /** Loads a project PRG into memory without reset or start; finishes stopped. `root` is the project folder; it stays on this side. */
131
+ async programLoad(params: { path: string; address?: number; root?: string }): Promise<{ state: RunState; loadAddress: number; size: number }> {
132
+ const file = readProjectFile(params.path, params.root === undefined ? {} : { root: params.root });
113
133
  return this.#request("programLoad", params.address === undefined ? {} : { address: params.address }, [file.bytes]);
114
134
  }
115
135
 
116
- /** Autostarts a project program or image, as VICE's autostart does. */
117
- async autostart(params: { path: string; index: number; run: boolean }): Promise<{ state: RunState }> {
118
- const file = readProjectFile(params.path);
136
+ /** Autostarts a project program or image, as VICE's autostart does. `root` is the project folder; it stays on this side. */
137
+ async autostart(params: { path: string; index: number; run: boolean; root?: string }): Promise<{ state: RunState; emulatorFlaw?: string }> {
138
+ const file = readProjectFile(params.path, params.root === undefined ? {} : { root: params.root });
119
139
  return this.#request("autostart", { type: file.type, index: params.index, run: params.run }, [file.bytes]);
120
140
  }
121
141
 
122
- /** Attaches a project disk image to drive 8. */
123
- async diskAttach(params: { path: string }): Promise<{ attached: boolean }> {
124
- const file = readProjectFile(params.path);
142
+ /** Attaches a project disk image to drive 8. `root` is the project folder; it stays on this side. */
143
+ async diskAttach(params: { path: string; root?: string }): Promise<{ attached: boolean }> {
144
+ const file = readProjectFile(params.path, params.root === undefined ? {} : { root: params.root });
125
145
  return this.#request("diskAttach", { type: file.type }, [file.bytes]);
126
146
  }
127
147
 
@@ -138,17 +158,19 @@ export class ViceSessionClient {
138
158
  return this.#request("joystick", state);
139
159
  }
140
160
 
141
- /** Action list gives all breakpoints; every other action gives the one breakpoint it added or changed. */
161
+ /** Action list gives all breakpoints, remove gives `{removed, id}`; add, enable and disable give the one breakpoint. */
142
162
  breakpoint(params: Extract<BreakpointParams, { action: "list" }>): Promise<{ breakpoints: Breakpoint[] }>;
143
- breakpoint(params: Exclude<BreakpointParams, { action: "list" }>): Promise<Breakpoint>;
144
- breakpoint(params: BreakpointParams): Promise<Breakpoint | { breakpoints: Breakpoint[] }> {
163
+ breakpoint(params: Extract<BreakpointParams, { action: "remove" }>): Promise<PointRemoved>;
164
+ breakpoint(params: Exclude<BreakpointParams, { action: "list" | "remove" }>): Promise<Breakpoint>;
165
+ breakpoint(params: BreakpointParams): Promise<Breakpoint | PointRemoved | { breakpoints: Breakpoint[] }> {
145
166
  return this.#request("breakpoint", params);
146
167
  }
147
168
 
148
- /** Action list gives all watchpoints; every other action gives the one watchpoint it added or changed. */
169
+ /** Action list gives all watchpoints, remove gives `{removed, id}`; add, enable and disable give the one watchpoint. */
149
170
  watchpoint(params: Extract<WatchpointParams, { action: "list" }>): Promise<{ watchpoints: Watchpoint[] }>;
150
- watchpoint(params: Exclude<WatchpointParams, { action: "list" }>): Promise<Watchpoint>;
151
- watchpoint(params: WatchpointParams): Promise<Watchpoint | { watchpoints: Watchpoint[] }> {
171
+ watchpoint(params: Extract<WatchpointParams, { action: "remove" }>): Promise<PointRemoved>;
172
+ watchpoint(params: Exclude<WatchpointParams, { action: "list" | "remove" }>): Promise<Watchpoint>;
173
+ watchpoint(params: WatchpointParams): Promise<Watchpoint | PointRemoved | { watchpoints: Watchpoint[] }> {
152
174
  return this.#request("watchpoint", params);
153
175
  }
154
176
 
@@ -156,6 +178,11 @@ export class ViceSessionClient {
156
178
  return this.#request("cpuHistory", params);
157
179
  }
158
180
 
181
+ /** Records each hit of the points while the program runs on (D76). */
182
+ trace(params: ViceOperations["trace"]["params"]): Promise<ViceOperations["trace"]["result"]> {
183
+ return this.#request("trace", params);
184
+ }
185
+
159
186
  backtrace(params: ViceOperations["backtrace"]["params"]): Promise<ViceOperations["backtrace"]["result"]> {
160
187
  return this.#request("backtrace", params);
161
188
  }
@@ -164,12 +191,14 @@ export class ViceSessionClient {
164
191
  return this.#request("timing", { action });
165
192
  }
166
193
 
167
- profile(limit: number): Promise<ViceOperations["profile"]["result"]> {
168
- return this.#request("profile", { limit });
194
+ profile(params: { action: "read"; limit: number }): Promise<{ entries: ProfileEntry[] }>;
195
+ profile(params: { action: "clear" }): Promise<{ cleared: boolean }>;
196
+ profile(params: ViceOperations["profile"]["params"]): Promise<ViceOperations["profile"]["result"]> {
197
+ return this.#request("profile", params);
169
198
  }
170
199
 
171
200
  /** Action read gives the ranges; action clear gives cleared. */
172
- memmap(params: Extract<ViceOperations["memmap"]["params"], { action: "read" }>): Promise<{ ranges: MemmapRange[] }>;
201
+ memmap(params: Extract<ViceOperations["memmap"]["params"], { action: "read" }>): Promise<{ ranges: MemmapRange[]; next?: number }>;
173
202
  memmap(params: { action: "clear" }): Promise<{ cleared: boolean }>;
174
203
  memmap(params: ViceOperations["memmap"]["params"]): Promise<ViceOperations["memmap"]["result"]> {
175
204
  return this.#request("memmap", params);
@@ -209,6 +238,19 @@ export class ViceSessionClient {
209
238
  return this.#request("snapshot", params);
210
239
  }
211
240
 
241
+ /** The bytes of a named snapshot, as a .vsf file holds them (D41). */
242
+ async snapshotExport(name: string): Promise<Buffer> {
243
+ const { attachments } = await this.#requestWithAttachments("snapshotFile", { action: "export", name });
244
+ const [bytes] = attachments;
245
+ if (bytes === undefined || attachments.length !== 1) throw new WireFailure("operation-failed", "The c64-re-tools host runtime sent no snapshot file.");
246
+ return bytes;
247
+ }
248
+
249
+ /** Restores the machine from the bytes of a .vsf file; the CPU stays stopped. */
250
+ snapshotRestore(bytes: Uint8Array): Promise<ViceOperations["snapshotFile"]["result"]> {
251
+ return this.#request("snapshotFile", { action: "restore" }, [bytes]);
252
+ }
253
+
212
254
  screenBaselines(): Promise<{ baselines: string[] }> {
213
255
  return this.#request("screenBaselines", {});
214
256
  }
@@ -226,6 +268,21 @@ export class ViceSessionClient {
226
268
  return this.#request("window", { action });
227
269
  }
228
270
 
271
+ /** True when the connection to the host is gone, whatever the reason. */
272
+ get lost(): boolean {
273
+ return this.#lost !== undefined;
274
+ }
275
+
276
+ /** True when the emulator answered a request: then the session had machine state to lose (D57). */
277
+ get answered(): boolean {
278
+ return this.#answered;
279
+ }
280
+
281
+ /** The loss of a lost session, when no request got it; undefined while the session lives (D57). */
282
+ get unreportedLoss(): WireFailure | undefined {
283
+ return this.#lost?.code === "machine-state-lost" && !this.#lossReported ? this.#lost : undefined;
284
+ }
285
+
229
286
  /** Ends the session; the host stops its emulator. Open and later requests fail as closed. */
230
287
  async close(): Promise<void> {
231
288
  this.#lost ??= new WireFailure("machine-unavailable", SESSION_CLOSED);
@@ -233,16 +290,28 @@ export class ViceSessionClient {
233
290
  await this.closed;
234
291
  }
235
292
 
236
- #request<O extends ViceOperation>(
293
+ async #request<O extends ViceOperation>(
237
294
  op: O,
238
295
  params: ViceOperations[O]["params"],
239
296
  attachments: readonly Uint8Array[] = [],
240
297
  ): Promise<ViceOperations[O]["result"]> {
241
- if (this.#lost !== undefined) return Promise.reject(this.#lost);
298
+ return (await this.#requestWithAttachments(op, params, attachments)).result;
299
+ }
300
+
301
+ /** One request; the reply's result and the attachments that came with it. */
302
+ #requestWithAttachments<O extends ViceOperation>(
303
+ op: O,
304
+ params: ViceOperations[O]["params"],
305
+ attachments: readonly Uint8Array[] = [],
306
+ ): Promise<{ result: ViceOperations[O]["result"]; attachments: Buffer[] }> {
307
+ if (this.#lost !== undefined) {
308
+ this.#lossReported = true;
309
+ return Promise.reject(this.#lost);
310
+ }
242
311
  const id = this.#nextId;
243
312
  this.#nextId = this.#nextId >= 0xffff_ffff ? 1 : this.#nextId + 1;
244
313
  return new Promise((resolve, reject) => {
245
- this.#pending.set(id, { op, resolve: resolve as (value: unknown) => void, reject });
314
+ this.#pending.set(id, { op, resolve: (result, received) => resolve({ result: result as ViceOperations[O]["result"], attachments: received }), reject });
246
315
  this.#connection.send({ type: "request", id, op, params }, attachments);
247
316
  });
248
317
  }
@@ -252,3 +321,13 @@ export class ViceSessionClient {
252
321
  void this.#connection.close();
253
322
  }
254
323
  }
324
+
325
+
326
+ /** Reads `size` bytes from `address` of the C64 in reads of at most 4 KiB, in one view: up to all 64 KiB (M23.9). */
327
+ export async function readMemoryInParts(client: Pick<ViceSessionClient, "memoryRead">, address: number, size: number, view: "cpu" | "ram"): Promise<Buffer> {
328
+ const parts: Buffer[] = [];
329
+ for (let at = address; at < address + size; at += MAX_MEMORY_READ) {
330
+ parts.push(Buffer.from((await client.memoryRead({ address: at, size: Math.min(MAX_MEMORY_READ, address + size - at), space: "c64", view })).data, "hex"));
331
+ }
332
+ return Buffer.concat(parts);
333
+ }
@@ -0,0 +1,161 @@
1
+ // Conflicts (D69): current rows that break a current-row rule. One computer
2
+ // never writes them, but git joins the knowledge of two computers line by
3
+ // line, so after a merge two names can be current at one address, or two
4
+ // regions can overlap. Reads show them; writes are refused until a merge
5
+ // revision decides them.
6
+
7
+ import type { DatabaseSync } from "node:sqlite";
8
+
9
+ import type { HistoryEntry } from "./history.ts";
10
+ import {
11
+ COMMENT_COLUMNS,
12
+ MACRO_COLUMNS,
13
+ MACRO_USE_COLUMNS,
14
+ MODULE_COLUMNS,
15
+ OPERAND_COLUMNS,
16
+ PATCH_COLUMNS,
17
+ REFERENCE_COLUMNS,
18
+ REGION_COLUMNS,
19
+ SUPPRESSION_COLUMNS,
20
+ SYMBOL_COLUMNS,
21
+ toComment,
22
+ toMacro,
23
+ toMacroUse,
24
+ toModule,
25
+ toOperand,
26
+ toPatch,
27
+ toReference,
28
+ toRegion,
29
+ toSuppression,
30
+ toSymbol,
31
+ } from "./read.ts";
32
+ import type { Space } from "./schema.ts";
33
+ import { compareText, sqlName } from "./text.ts";
34
+
35
+ /** The current-row rules, each with what breaks it. */
36
+ export const CONFLICT_RULES = {
37
+ "symbol-address": "two symbols at one address",
38
+ "analyzer-name-address": "two names of one analyzer at one address",
39
+ "analyzer-name-name": "one analyzer name at two addresses",
40
+ "symbol-name": "one name at two addresses",
41
+ region: "regions that overlap",
42
+ comment: "two comments at one place",
43
+ reference: "the same reference two times",
44
+ operand: "two operand expressions at one address",
45
+ "module-start": "two modules at one address",
46
+ "module-file": "one module file at two addresses",
47
+ "suppression-symbol": "the same symbol suppression two times",
48
+ "suppression-reference": "the same reference suppression two times",
49
+ "suppression-region": "region suppressions that overlap",
50
+ patch: "patches that overlap",
51
+ macro: "two macros with one name",
52
+ "macro-use": "macro uses that overlap",
53
+ } as const;
54
+ export type ConflictRule = keyof typeof CONFLICT_RULES;
55
+
56
+ /** One row of a conflict: the row with its revisions and its id. */
57
+ export type ConflictRow = HistoryEntry & { id: string };
58
+
59
+ /** Current rows that break one rule together, oldest first. */
60
+ export interface Conflict {
61
+ rule: ConflictRule;
62
+ space: Space;
63
+ rows: ConflictRow[];
64
+ }
65
+
66
+ type Raw = Record<string, unknown>;
67
+
68
+ interface TableRule {
69
+ rule: ConflictRule;
70
+ table: string;
71
+ entity: HistoryEntry["entity"];
72
+ columns: string;
73
+ map: (raw: Raw) => HistoryEntry["row"];
74
+ /** The columns that must differ between current rows, or "overlap" for address ranges. */
75
+ key: readonly string[] | "overlap";
76
+ /** Rows of the table that the rule is about. */
77
+ where?: string;
78
+ }
79
+
80
+ const RULES: readonly TableRule[] = [
81
+ { rule: "symbol-address", table: "symbols", entity: "symbol", columns: SYMBOL_COLUMNS, map: toSymbol, key: ["address"] },
82
+ { rule: "symbol-name", table: "symbols", entity: "symbol", columns: SYMBOL_COLUMNS, map: toSymbol, key: ["name"] },
83
+ { rule: "analyzer-name-address", table: "analyzer_names", entity: "analyzer-name", columns: SYMBOL_COLUMNS, map: toSymbol, key: ["address", "origin"] },
84
+ { rule: "analyzer-name-name", table: "analyzer_names", entity: "analyzer-name", columns: SYMBOL_COLUMNS, map: toSymbol, key: ["name", "origin"] },
85
+ { rule: "region", table: "regions", entity: "region", columns: REGION_COLUMNS, map: toRegion, key: "overlap" },
86
+ { rule: "comment", table: "comments", entity: "comment", columns: COMMENT_COLUMNS, map: toComment, key: ["address", "placement"] },
87
+ { rule: "reference", table: "references", entity: "reference", columns: REFERENCE_COLUMNS, map: toReference, key: ["from_address", "to_address", "kind"] },
88
+ { rule: "operand", table: "operands", entity: "operand", columns: OPERAND_COLUMNS, map: toOperand, key: ["address"] },
89
+ { rule: "module-start", table: "modules", entity: "module", columns: MODULE_COLUMNS, map: toModule, key: ["start_address"] },
90
+ { rule: "module-file", table: "modules", entity: "module", columns: MODULE_COLUMNS, map: toModule, key: ["file"] },
91
+ { rule: "suppression-symbol", table: "suppressions", entity: "suppression", columns: SUPPRESSION_COLUMNS, map: toSuppression, key: ["start_address"], where: "entity = 'symbol'" },
92
+ {
93
+ rule: "suppression-reference",
94
+ table: "suppressions",
95
+ entity: "suppression",
96
+ columns: SUPPRESSION_COLUMNS,
97
+ map: toSuppression,
98
+ key: ["start_address", "to_address", "kind"],
99
+ where: "entity = 'reference'",
100
+ },
101
+ { rule: "suppression-region", table: "suppressions", entity: "suppression", columns: SUPPRESSION_COLUMNS, map: toSuppression, key: "overlap", where: "entity = 'region'" },
102
+ { rule: "patch", table: "patches", entity: "patch", columns: PATCH_COLUMNS, map: toPatch, key: "overlap" },
103
+ { rule: "macro", table: "macros", entity: "macro", columns: MACRO_COLUMNS, map: toMacro, key: ["name"] },
104
+ { rule: "macro-use", table: "macro_uses", entity: "macro-use", columns: MACRO_USE_COLUMNS, map: toMacroUse, key: "overlap" },
105
+ ];
106
+
107
+ function conflictRow(rule: TableRule, raw: Raw): ConflictRow {
108
+ return { entity: rule.entity, row: rule.map(raw), from: raw.valid_from_revision as string, id: raw.id as string } as ConflictRow;
109
+ }
110
+
111
+ const byAge = (a: ConflictRow, b: ConflictRow) => compareText(a.from, b.from) || compareText(a.id, b.id);
112
+
113
+ /** The current rows of one rule that share a key, in groups. */
114
+ function sameKey(db: DatabaseSync, rule: TableRule, key: readonly string[]): Conflict[] {
115
+ const where = `valid_to_revision IS NULL${rule.where === undefined ? "" : ` AND ${rule.where}`}`;
116
+ const columns = ["space", ...key].join(", ");
117
+ const rows = db
118
+ .prepare(`SELECT ${rule.columns}, id FROM ${sqlName(rule.table)} WHERE ${where} AND (${columns}) IN (SELECT ${columns} FROM ${sqlName(rule.table)} WHERE ${where} GROUP BY ${columns} HAVING count(*) > 1)`)
119
+ .all() as Raw[];
120
+ const groups = new Map<string, Conflict>();
121
+ for (const raw of rows) {
122
+ const id = JSON.stringify(["space", ...key].map((column) => raw[column]));
123
+ const group = groups.get(id) ?? { rule: rule.rule, space: raw.space as Space, rows: [] };
124
+ group.rows.push(conflictRow(rule, raw));
125
+ groups.set(id, group);
126
+ }
127
+ return [...groups.values()];
128
+ }
129
+
130
+ /** The current rows of one rule whose address ranges overlap, in groups of rows that overlap in a chain. */
131
+ function overlapping(db: DatabaseSync, rule: TableRule): Conflict[] {
132
+ const rows = db
133
+ .prepare(`SELECT ${rule.columns}, id FROM ${sqlName(rule.table)} WHERE valid_to_revision IS NULL${rule.where === undefined ? "" : ` AND ${rule.where}`} ORDER BY space, start_address, end_address`)
134
+ .all() as Raw[];
135
+ const conflicts: Conflict[] = [];
136
+ let group: Conflict | undefined;
137
+ let end = -1;
138
+ for (const raw of rows) {
139
+ if (group !== undefined && raw.space === group.space && (raw.start_address as number) <= end) {
140
+ group.rows.push(conflictRow(rule, raw));
141
+ end = Math.max(end, raw.end_address as number);
142
+ continue;
143
+ }
144
+ if (group !== undefined && group.rows.length > 1) conflicts.push(group);
145
+ group = { rule: rule.rule, space: raw.space as Space, rows: [conflictRow(rule, raw)] };
146
+ end = raw.end_address as number;
147
+ }
148
+ if (group !== undefined && group.rows.length > 1) conflicts.push(group);
149
+ return conflicts;
150
+ }
151
+
152
+ /** Every conflict of the knowledge, rule by rule; the rows of each, oldest first. */
153
+ export function conflicts(db: DatabaseSync | undefined): Conflict[] {
154
+ if (db === undefined) return [];
155
+ return RULES.flatMap((rule) => (rule.key === "overlap" ? overlapping(db, rule) : sameKey(db, rule, rule.key))).map((conflict) => ({ ...conflict, rows: conflict.rows.sort(byAge) }));
156
+ }
157
+
158
+ /** The table of a conflict rule. */
159
+ export function conflictTable(rule: ConflictRule): string {
160
+ return RULES.find((item) => item.rule === rule)!.table;
161
+ }