@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.
- package/README.md +28 -62
- package/distribution/package.ts +1 -16
- package/distribution/plugin.ts +5 -3
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +25 -6
- package/skills/c64-assembler/scripts/assemble.ts +45 -10
- package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
- package/skills/c64-basic/SKILL.md +15 -7
- package/skills/c64-basic/scripts/basic.ts +4 -4
- package/skills/c64-disk/SKILL.md +34 -11
- package/skills/c64-disk/scripts/disk.ts +65 -10
- package/skills/c64-disk/scripts/lib/containers.ts +124 -0
- package/skills/c64-docs/SKILL.md +138 -0
- package/skills/c64-docs/references/page-types.md +141 -0
- package/skills/c64-docs/references/style.md +43 -0
- package/skills/c64-docs/scripts/generate.ts +102 -0
- package/skills/c64-docs/scripts/lib/links.ts +155 -0
- package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
- package/skills/c64-docs/scripts/lib/reference.ts +406 -0
- package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
- package/skills/c64-docs/scripts/link.ts +207 -0
- package/skills/c64-emulator/SKILL.md +29 -8
- package/skills/c64-knowledge/SKILL.md +201 -15
- package/skills/c64-knowledge/scripts/knowledge.ts +927 -78
- package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
- package/skills/c64-memory-map/SKILL.md +14 -2
- package/skills/c64-memory-map/scripts/memmap.ts +15 -4
- package/skills/c64-provenance/SKILL.md +100 -11
- package/skills/c64-provenance/scripts/lib/compare.ts +152 -0
- package/skills/c64-provenance/scripts/lib/library.ts +584 -0
- package/skills/c64-provenance/scripts/lib/runtime.ts +191 -0
- package/skills/c64-provenance/scripts/lib/separate.ts +140 -0
- package/skills/c64-provenance/scripts/provenance.ts +477 -24
- package/skills/c64-reverse-engineering/SKILL.md +90 -11
- package/skills/c64-source/SKILL.md +174 -0
- package/skills/c64-source/scripts/generate.ts +788 -0
- package/skills/c64-source/scripts/lib/loader.ts +630 -0
- package/skills/c64-source/scripts/lib/source.ts +1121 -0
- package/skills/c64-static-analysis/SKILL.md +53 -13
- package/skills/c64-static-analysis/scripts/analyze.ts +260 -31
- package/skills/c64-static-analysis/scripts/lib/coverage.ts +72 -0
- package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +11 -5
- package/skills/c64-static-analysis/scripts/lib/idioms.ts +158 -0
- package/skills/c64-static-analysis/scripts/lib/instructions.ts +105 -0
- package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
- package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
- package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
- package/skills/c64-testing/SKILL.md +57 -4
- package/skills/c64-testing/references/scenario-format.md +50 -6
- package/skills/c64-testing/scripts/checklist.ts +3 -3
- package/skills/c64-testing/scripts/diff.ts +167 -0
- package/skills/c64-testing/scripts/lib/diff.ts +70 -0
- package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +183 -152
- package/skills/c64-testing/scripts/run.ts +241 -0
- package/skills/c64-testing/scripts/test.ts +20 -10
- package/skills/c64-unpacker/SKILL.md +88 -21
- package/skills/c64-unpacker/scripts/lib/evidence.ts +207 -0
- package/skills/c64-unpacker/scripts/lib/loads.ts +209 -0
- package/skills/c64-unpacker/scripts/lib/machine.ts +80 -0
- package/skills/c64-unpacker/scripts/lib/stages.ts +142 -0
- package/skills/c64-unpacker/scripts/unpack.ts +545 -61
- package/src/c64.ts +240 -12
- package/src/cli/cleanup.ts +1 -1
- package/src/cli/main.ts +14 -8
- package/src/cli/status.ts +105 -11
- package/src/graphics.ts +158 -0
- package/src/host/main.ts +57 -31
- package/src/host/server.ts +30 -47
- package/src/host/tools/c1541.ts +34 -5
- package/src/host/tools/index.ts +6 -2
- package/src/host/tools/petcat.ts +10 -3
- package/src/host/tools/status.ts +23 -6
- package/src/host/vice/adapter.ts +64 -20
- package/src/host/vice/chips.ts +11 -1
- package/src/host/vice/process.ts +19 -8
- package/src/host/vice/screen.ts +4 -44
- package/src/host/vice/session.ts +343 -60
- package/src/host-client/autostart.ts +55 -0
- package/src/host-client/connect.ts +14 -14
- package/src/host-client/coverage.ts +36 -0
- package/src/host-client/image.ts +63 -0
- package/src/host-client/steps.ts +175 -0
- package/src/host-client/tools.ts +7 -6
- package/src/host-client/transfer.ts +60 -10
- package/src/host-client/vice-session.ts +107 -28
- package/src/knowledge/conflicts.ts +161 -0
- package/src/knowledge/database.ts +150 -38
- package/src/knowledge/expression.ts +209 -0
- package/src/knowledge/graph.ts +104 -0
- package/src/knowledge/history.ts +105 -27
- package/src/knowledge/import.ts +141 -85
- package/src/knowledge/join.ts +211 -0
- package/src/knowledge/program.ts +258 -0
- package/src/knowledge/read.ts +445 -63
- package/src/knowledge/schema.ts +277 -65
- package/src/knowledge/text.ts +415 -0
- package/src/knowledge/write.ts +1145 -150
- package/src/mcp/main.ts +44 -19
- package/src/mcp/output.ts +39 -0
- package/src/mcp/server.ts +15 -4
- package/src/mcp/tools/debug.ts +202 -31
- package/src/mcp/tools/execution.ts +39 -5
- package/src/mcp/tools/input.ts +7 -4
- package/src/mcp/tools/machine.ts +14 -2
- package/src/mcp/tools/media.ts +44 -16
- package/src/mcp/tools/memory.ts +68 -12
- package/src/mcp/tools/video.ts +20 -9
- package/src/native/discover.ts +44 -6
- package/src/native/ghidra/analyze.ts +2 -2
- package/src/native/ghidra/index.ts +19 -3
- package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
- package/src/native/ghidra/scripts/C64Export.java +33 -1
- package/src/native/ghidra/scripts/C64Prepare.java +7 -1
- package/src/native/status.ts +15 -3
- package/src/native/types.ts +43 -8
- package/src/opcodes.ts +133 -0
- package/src/package.ts +21 -0
- package/{skills/c64-memory-map/scripts → src}/platform.ts +87 -7
- package/src/project.ts +166 -3
- package/src/protocol/messages.ts +17 -10
- package/src/protocol/tools.ts +27 -22
- package/src/protocol/vice.ts +255 -33
- package/src/script.ts +49 -9
- package/src/trace.ts +55 -23
- package/skills/c64-provenance/scripts/compare.ts +0 -92
- 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
|
|
11
|
-
"
|
|
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
|
|
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:
|
|
144
|
-
breakpoint(params: BreakpointParams
|
|
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
|
|
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:
|
|
151
|
-
watchpoint(params: WatchpointParams
|
|
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<
|
|
168
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
+
}
|