@henols/c64-re-tools 1.1.0 → 2.0.0-rc.11
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/LICENSE +21 -0
- package/README.md +154 -37
- package/distribution/package.ts +21 -0
- package/distribution/plugin.ts +122 -0
- package/package.json +34 -40
- package/skills/c64-assembler/SKILL.md +76 -0
- package/skills/c64-assembler/scripts/assemble.ts +110 -0
- package/skills/c64-assembler/scripts/compare.ts +84 -0
- package/skills/c64-basic/SKILL.md +66 -0
- package/skills/c64-basic/scripts/basic.ts +46 -0
- package/skills/c64-disk/SKILL.md +93 -0
- package/skills/c64-disk/scripts/containers.ts +124 -0
- package/skills/c64-disk/scripts/disk.ts +120 -0
- package/skills/c64-emulator/SKILL.md +134 -0
- package/skills/c64-knowledge/SKILL.md +192 -0
- package/skills/c64-knowledge/scripts/knowledge.ts +613 -0
- package/skills/c64-memory-map/SKILL.md +79 -0
- package/skills/c64-memory-map/scripts/memmap.ts +92 -0
- package/skills/c64-provenance/SKILL.md +110 -0
- package/skills/c64-provenance/scripts/compare.ts +152 -0
- package/skills/c64-provenance/scripts/provenance.ts +334 -0
- package/skills/c64-provenance/scripts/runtime.ts +191 -0
- package/skills/c64-provenance/scripts/separate.ts +140 -0
- package/skills/c64-reverse-engineering/SKILL.md +144 -0
- package/skills/c64-source/SKILL.md +133 -0
- package/skills/c64-source/scripts/generate.ts +503 -0
- package/skills/c64-source/scripts/loader.ts +589 -0
- package/skills/c64-source/scripts/opcodes.ts +61 -0
- package/skills/c64-source/scripts/source.ts +673 -0
- package/skills/c64-static-analysis/SKILL.md +120 -0
- package/skills/c64-static-analysis/scripts/analyze.ts +293 -0
- package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
- package/skills/c64-static-analysis/scripts/findings.ts +75 -0
- package/skills/c64-static-analysis/scripts/idioms.ts +149 -0
- package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
- package/skills/c64-static-analysis/scripts/pointers.ts +108 -0
- package/skills/c64-testing/SKILL.md +103 -0
- package/skills/c64-testing/references/scenario-format.md +116 -0
- package/skills/c64-testing/scripts/checklist.ts +51 -0
- package/skills/c64-testing/scripts/run.ts +189 -0
- package/skills/c64-testing/scripts/scenario.ts +493 -0
- package/skills/c64-testing/scripts/test.ts +65 -0
- package/skills/c64-unpacker/SKILL.md +135 -0
- package/skills/c64-unpacker/scripts/evidence.ts +207 -0
- package/skills/c64-unpacker/scripts/loads.ts +209 -0
- package/skills/c64-unpacker/scripts/machine.ts +80 -0
- package/skills/c64-unpacker/scripts/stages.ts +130 -0
- package/skills/c64-unpacker/scripts/unpack.ts +683 -0
- package/src/c64.ts +411 -0
- package/src/cli/ap-sdk.ts +44 -0
- package/src/cli/cleanup.ts +194 -0
- package/src/cli/main.ts +125 -0
- package/src/cli/status.ts +61 -0
- package/src/host/main.ts +134 -0
- package/src/host/server.ts +392 -0
- package/src/host/tools/c1541.ts +284 -0
- package/src/host/tools/index.ts +35 -0
- package/src/host/tools/petcat.ts +77 -0
- package/src/host/tools/status.ts +47 -0
- package/src/host/tools/version.ts +41 -0
- package/src/host/vice/adapter.ts +804 -0
- package/src/host/vice/binary-monitor.ts +389 -0
- package/src/host/vice/chips.ts +118 -0
- package/src/host/vice/connect.ts +34 -0
- package/src/host/vice/process.ts +413 -0
- package/src/host/vice/screen.ts +184 -0
- package/src/host/vice/session.ts +1426 -0
- package/src/host/vice/text-monitor.ts +223 -0
- package/src/host-client/autostart.ts +55 -0
- package/src/host-client/connect.ts +241 -0
- package/src/host-client/coverage.ts +63 -0
- package/src/host-client/image.ts +63 -0
- package/src/host-client/steps.ts +170 -0
- package/src/host-client/tools.ts +89 -0
- package/src/host-client/transfer.ts +126 -0
- package/src/host-client/vice-session.ts +316 -0
- package/src/knowledge/database.ts +132 -0
- package/src/knowledge/expression.ts +209 -0
- package/src/knowledge/history.ts +169 -0
- package/src/knowledge/import.ts +392 -0
- package/src/knowledge/read.ts +392 -0
- package/src/knowledge/schema.ts +250 -0
- package/src/knowledge/write.ts +837 -0
- package/src/mcp/main.ts +134 -0
- package/src/mcp/output.ts +32 -0
- package/src/mcp/server.ts +316 -0
- package/src/mcp/tools/debug.ts +327 -0
- package/src/mcp/tools/execution.ts +117 -0
- package/src/mcp/tools/input.ts +72 -0
- package/src/mcp/tools/machine.ts +100 -0
- package/src/mcp/tools/media.ts +129 -0
- package/src/mcp/tools/memory.ts +250 -0
- package/src/mcp/tools/video.ts +271 -0
- package/src/native/acme.ts +116 -0
- package/src/native/discover.ts +179 -0
- package/src/native/dxa.ts +166 -0
- package/src/native/ghidra/analyze.ts +86 -0
- package/src/native/ghidra/index.ts +173 -0
- package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
- package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
- package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
- package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
- package/src/native/ghidra/scripts/C64Export.java +274 -0
- package/src/native/ghidra/scripts/C64Prepare.java +60 -0
- package/src/native/local.ts +34 -0
- package/src/native/processes.ts +350 -0
- package/src/native/run.ts +172 -0
- package/src/native/staging.ts +83 -0
- package/src/native/status.ts +113 -0
- package/src/native/types.ts +257 -0
- package/src/native/watchdog.ts +78 -0
- package/src/package.ts +21 -0
- package/src/platform.ts +441 -0
- package/src/project.ts +39 -0
- package/src/protocol/framing.ts +132 -0
- package/src/protocol/messages.ts +259 -0
- package/src/protocol/tools.ts +266 -0
- package/src/protocol/vice.ts +1348 -0
- package/src/script.ts +60 -0
- package/src/time.ts +15 -0
- package/src/trace.ts +194 -0
- package/THIRD-PARTY-NOTICES.md +0 -26
- package/bin/cli.mjs +0 -283
- package/skills/acme-build/SKILL.md +0 -260
- package/skills/acme-build/scripts/acme.mjs +0 -341
- package/skills/acme-build/template.a +0 -53
- package/skills/c64-disk-access/SKILL.md +0 -159
- package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
- package/skills/c64-memory-mapping/SKILL.md +0 -631
- package/skills/c64-memory-mapping/memmap.json +0 -8800
- package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
- package/skills/c64-petcat/SKILL.md +0 -87
- package/skills/c64-petcat/scripts/petcat.mjs +0 -221
- package/skills/c64-program-recon/SKILL.md +0 -706
- package/skills/c64-program-recon/references/control-flow.md +0 -180
- package/skills/c64-program-recon/references/graphics.md +0 -79
- package/skills/c64-program-recon/references/observation-hazards.md +0 -143
- package/skills/c64-program-recon/references/reconstruction.md +0 -133
- package/skills/c64-program-recon/references/sound-and-input.md +0 -74
- package/skills/c64-program-recon/references/tool-selection.md +0 -78
- package/skills/c64-program-recon/scripts/derive.mjs +0 -364
- package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
- package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
- package/skills/c64-provenance-diff/SKILL.md +0 -292
- package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
- package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
- package/skills/c64-ram-capture/RELEASES.json.example +0 -17
- package/skills/c64-ram-capture/SKILL.md +0 -474
- package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
- package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
- package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
- package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
- package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
- package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
- package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
- package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
- package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
- package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
- package/skills/c64-ram-capture/transients/README.md +0 -137
- package/skills/routine-queue-walker/SKILL.md +0 -394
- package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
- package/skills/vice-wedge-triage/SKILL.md +0 -269
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
// The long-lived Host Runtime connection an MCP process owns: one connection,
|
|
2
|
+
// one VICE. Exposes C64-domain operations only.
|
|
3
|
+
|
|
4
|
+
import { ProtocolError, WireFailure, type VideoStandard } from "../protocol/messages.ts";
|
|
5
|
+
import { validateViceResult, type Breakpoint, type BreakpointParams, type PointRemoved, 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";
|
|
6
|
+
import { HostConnection } from "./connect.ts";
|
|
7
|
+
import { readProjectFile } from "./transfer.ts";
|
|
8
|
+
|
|
9
|
+
const HOST_LOST =
|
|
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
|
+
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
|
+
}
|
|
24
|
+
|
|
25
|
+
interface Pending {
|
|
26
|
+
op: ViceOperation;
|
|
27
|
+
resolve: (value: unknown, attachments: Buffer[]) => void;
|
|
28
|
+
reject: (error: Error) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class ViceSessionClient {
|
|
32
|
+
/** Settles when the session is gone, whether we closed it or the host went away. */
|
|
33
|
+
readonly closed: Promise<void>;
|
|
34
|
+
readonly #connection: HostConnection;
|
|
35
|
+
readonly #pending = new Map<number, Pending>();
|
|
36
|
+
#nextId = 1;
|
|
37
|
+
#lost: WireFailure | undefined;
|
|
38
|
+
#answered = false;
|
|
39
|
+
#lossReported = false;
|
|
40
|
+
|
|
41
|
+
private constructor(connection: HostConnection) {
|
|
42
|
+
this.#connection = connection;
|
|
43
|
+
connection.onMessage((message, attachments) => {
|
|
44
|
+
// Anything but a reply after the handshake breaks the contract.
|
|
45
|
+
if (message.type !== "reply") return this.#fail();
|
|
46
|
+
const pending = this.#pending.get(message.id);
|
|
47
|
+
if (pending === undefined) return; // not ours; ignore
|
|
48
|
+
this.#pending.delete(message.id);
|
|
49
|
+
this.#answered = true;
|
|
50
|
+
if ("error" in message) {
|
|
51
|
+
pending.reject(new WireFailure(message.error.code, message.error.message));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
pending.resolve(validateViceResult(pending.op, message.result), attachments);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
pending.reject(
|
|
58
|
+
new WireFailure(
|
|
59
|
+
"operation-failed",
|
|
60
|
+
error instanceof ProtocolError ? "The c64-re-tools host runtime sent an invalid reply." : "The c64-re-tools MCP server could not read the reply of the host runtime.",
|
|
61
|
+
),
|
|
62
|
+
);
|
|
63
|
+
this.#fail();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
this.closed = connection.closed.then(() => {
|
|
67
|
+
this.#lost ??= this.#answered ? new WireFailure("machine-state-lost", HOST_LOST) : new LostBeforeAnswer();
|
|
68
|
+
this.#lossReported = this.#pending.size > 0;
|
|
69
|
+
for (const pending of this.#pending.values()) pending.reject(this.#lost);
|
|
70
|
+
this.#pending.clear();
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Connects and waits until the host has started this session's emulator; `signal` stops the wait at once. */
|
|
75
|
+
static async open(options: { videoStandard: VideoStandard; env?: NodeJS.ProcessEnv; signal?: AbortSignal }): Promise<ViceSessionClient> {
|
|
76
|
+
const connection = await HostConnection.open({
|
|
77
|
+
role: "vice-session",
|
|
78
|
+
videoStandard: options.videoStandard,
|
|
79
|
+
...(options.env === undefined ? {} : { env: options.env }),
|
|
80
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
81
|
+
});
|
|
82
|
+
return new ViceSessionClient(connection);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
status(): Promise<MachineStatus> {
|
|
86
|
+
return this.#request("status", {});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
memoryRead(params: MemoryReadParams): Promise<MemoryReadResult> {
|
|
90
|
+
return this.#request("memoryRead", params);
|
|
91
|
+
}
|
|
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
|
+
|
|
98
|
+
registersGet(space: Space): Promise<Registers> {
|
|
99
|
+
return this.#request("registersGet", { space });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
memoryWrite(params: MemoryWriteParams): Promise<{ address: number; bytesWritten: number }> {
|
|
103
|
+
return this.#request("memoryWrite", params);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
memorySearch(params: ViceOperations["memorySearch"]["params"]): Promise<{ matches: number[] }> {
|
|
107
|
+
return this.#request("memorySearch", params);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
memoryCompare(params: ViceOperations["memoryCompare"]["params"]): Promise<ViceOperations["memoryCompare"]["result"]> {
|
|
111
|
+
return this.#request("memoryCompare", params);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
disassemble(params: ViceOperations["disassemble"]["params"]): Promise<ViceOperations["disassemble"]["result"]> {
|
|
115
|
+
return this.#request("disassemble", params);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
registersSet(space: Space, values: RegisterValues): Promise<Registers> {
|
|
119
|
+
return this.#request("registersSet", { space, values });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
execution(params: ExecutionParams): Promise<ExecutionResult> {
|
|
123
|
+
return this.#request("execution", params);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
runUntil(params: { target: RunTarget; timeoutFrames: number }): Promise<RunUntilResult> {
|
|
127
|
+
return this.#request("runUntil", params);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Loads a project PRG into memory without reset or start; finishes stopped. */
|
|
131
|
+
async programLoad(params: { path: string; address?: number }): Promise<{ state: RunState; loadAddress: number; size: number }> {
|
|
132
|
+
const file = readProjectFile(params.path);
|
|
133
|
+
return this.#request("programLoad", params.address === undefined ? {} : { address: params.address }, [file.bytes]);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Autostarts a project program or image, as VICE's autostart does. */
|
|
137
|
+
async autostart(params: { path: string; index: number; run: boolean }): Promise<{ state: RunState; emulatorFlaw?: string }> {
|
|
138
|
+
const file = readProjectFile(params.path);
|
|
139
|
+
return this.#request("autostart", { type: file.type, index: params.index, run: params.run }, [file.bytes]);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Attaches a project disk image to drive 8. */
|
|
143
|
+
async diskAttach(params: { path: string }): Promise<{ attached: boolean }> {
|
|
144
|
+
const file = readProjectFile(params.path);
|
|
145
|
+
return this.#request("diskAttach", { type: file.type }, [file.bytes]);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
reset(params: { mode: ResetMode; run: boolean }): Promise<{ state: RunState; pc?: number }> {
|
|
149
|
+
return this.#request("reset", params);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Queues PETSCII bytes for the C64 keyboard. */
|
|
153
|
+
keyboard(petscii: Uint8Array): Promise<{ queuedBytes: number }> {
|
|
154
|
+
return this.#request("keyboard", { data: Buffer.from(petscii).toString("hex") });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
joystick(state: JoystickState): Promise<JoystickState> {
|
|
158
|
+
return this.#request("joystick", state);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Action list gives all breakpoints, remove gives `{removed, id}`; add, enable and disable give the one breakpoint. */
|
|
162
|
+
breakpoint(params: Extract<BreakpointParams, { action: "list" }>): Promise<{ 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[] }> {
|
|
166
|
+
return this.#request("breakpoint", params);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Action list gives all watchpoints, remove gives `{removed, id}`; add, enable and disable give the one watchpoint. */
|
|
170
|
+
watchpoint(params: Extract<WatchpointParams, { action: "list" }>): Promise<{ 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[] }> {
|
|
174
|
+
return this.#request("watchpoint", params);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
cpuHistory(params: ViceOperations["cpuHistory"]["params"]): Promise<ViceOperations["cpuHistory"]["result"]> {
|
|
178
|
+
return this.#request("cpuHistory", params);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
backtrace(params: ViceOperations["backtrace"]["params"]): Promise<ViceOperations["backtrace"]["result"]> {
|
|
182
|
+
return this.#request("backtrace", params);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
timing(action: ViceOperations["timing"]["params"]["action"]): Promise<ViceOperations["timing"]["result"]> {
|
|
186
|
+
return this.#request("timing", { action });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
profile(limit: number): Promise<ViceOperations["profile"]["result"]> {
|
|
190
|
+
return this.#request("profile", { limit });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Action read gives the ranges; action clear gives cleared. */
|
|
194
|
+
memmap(params: Extract<ViceOperations["memmap"]["params"], { action: "read" }>): Promise<{ ranges: MemmapRange[]; next?: number }>;
|
|
195
|
+
memmap(params: { action: "clear" }): Promise<{ cleared: boolean }>;
|
|
196
|
+
memmap(params: ViceOperations["memmap"]["params"]): Promise<ViceOperations["memmap"]["result"]> {
|
|
197
|
+
return this.#request("memmap", params);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
vicii(): Promise<ViceOperations["vicii"]["result"]> {
|
|
201
|
+
return this.#request("vicii", {});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Empty `indexes` means all eight sprites. */
|
|
205
|
+
sprites(indexes: number[]): Promise<ViceOperations["sprites"]["result"]> {
|
|
206
|
+
return this.#request("sprites", { indexes });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
cia(which: ViceOperations["cia"]["params"]["which"]): Promise<ViceOperations["cia"]["result"]> {
|
|
210
|
+
return this.#request("cia", { which });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
sid(): Promise<ViceOperations["sid"]["result"]> {
|
|
214
|
+
return this.#request("sid", {});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
observe(params: ViceOperations["observe"]["params"]): Promise<ViceOperations["observe"]["result"]> {
|
|
218
|
+
return this.#request("observe", params);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** The last frame the VIC-II drew, as a base64 PNG with its size. */
|
|
222
|
+
screenCapture(baseline?: string): Promise<ViceOperations["screenCapture"]["result"]> {
|
|
223
|
+
return this.#request("screenCapture", baseline === undefined ? {} : { baseline });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
screenCompare(params: ViceOperations["screenCompare"]["params"]): Promise<ViceOperations["screenCompare"]["result"]> {
|
|
227
|
+
return this.#request("screenCompare", params);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
snapshot(params: ViceOperations["snapshot"]["params"]): Promise<ViceOperations["snapshot"]["result"]> {
|
|
231
|
+
return this.#request("snapshot", params);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** The bytes of a named snapshot, as a .vsf file holds them (D41). */
|
|
235
|
+
async snapshotExport(name: string): Promise<Buffer> {
|
|
236
|
+
const { attachments } = await this.#requestWithAttachments("snapshotFile", { action: "export", name });
|
|
237
|
+
const [bytes] = attachments;
|
|
238
|
+
if (bytes === undefined || attachments.length !== 1) throw new WireFailure("operation-failed", "The c64-re-tools host runtime sent no snapshot file.");
|
|
239
|
+
return bytes;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Restores the machine from the bytes of a .vsf file; the CPU stays stopped. */
|
|
243
|
+
snapshotRestore(bytes: Uint8Array): Promise<ViceOperations["snapshotFile"]["result"]> {
|
|
244
|
+
return this.#request("snapshotFile", { action: "restore" }, [bytes]);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
screenBaselines(): Promise<{ baselines: string[] }> {
|
|
248
|
+
return this.#request("screenBaselines", {});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
screenDiscard(baseline: string): Promise<{ discarded: boolean }> {
|
|
252
|
+
return this.#request("screenDiscard", { baseline });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
warp(enabled: boolean): Promise<{ enabled: boolean }> {
|
|
256
|
+
return this.#request("warp", { enabled });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Moves the machine into a VICE with a window (open) or back into a headless one (close). */
|
|
260
|
+
window(action: ViceOperations["window"]["params"]["action"]): Promise<ViceOperations["window"]["result"]> {
|
|
261
|
+
return this.#request("window", { action });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** True when the connection to the host is gone, whatever the reason. */
|
|
265
|
+
get lost(): boolean {
|
|
266
|
+
return this.#lost !== undefined;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** True when the emulator answered a request: then the session had machine state to lose (D57). */
|
|
270
|
+
get answered(): boolean {
|
|
271
|
+
return this.#answered;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** The loss of a lost session, when no request got it; undefined while the session lives (D57). */
|
|
275
|
+
get unreportedLoss(): WireFailure | undefined {
|
|
276
|
+
return this.#lost?.code === "machine-state-lost" && !this.#lossReported ? this.#lost : undefined;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Ends the session; the host stops its emulator. Open and later requests fail as closed. */
|
|
280
|
+
async close(): Promise<void> {
|
|
281
|
+
this.#lost ??= new WireFailure("machine-unavailable", SESSION_CLOSED);
|
|
282
|
+
await this.#connection.close();
|
|
283
|
+
await this.closed;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async #request<O extends ViceOperation>(
|
|
287
|
+
op: O,
|
|
288
|
+
params: ViceOperations[O]["params"],
|
|
289
|
+
attachments: readonly Uint8Array[] = [],
|
|
290
|
+
): Promise<ViceOperations[O]["result"]> {
|
|
291
|
+
return (await this.#requestWithAttachments(op, params, attachments)).result;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** One request; the reply's result and the attachments that came with it. */
|
|
295
|
+
#requestWithAttachments<O extends ViceOperation>(
|
|
296
|
+
op: O,
|
|
297
|
+
params: ViceOperations[O]["params"],
|
|
298
|
+
attachments: readonly Uint8Array[] = [],
|
|
299
|
+
): Promise<{ result: ViceOperations[O]["result"]; attachments: Buffer[] }> {
|
|
300
|
+
if (this.#lost !== undefined) {
|
|
301
|
+
this.#lossReported = true;
|
|
302
|
+
return Promise.reject(this.#lost);
|
|
303
|
+
}
|
|
304
|
+
const id = this.#nextId;
|
|
305
|
+
this.#nextId = this.#nextId >= 0xffff_ffff ? 1 : this.#nextId + 1;
|
|
306
|
+
return new Promise((resolve, reject) => {
|
|
307
|
+
this.#pending.set(id, { op, resolve: (result, received) => resolve({ result: result as ViceOperations[O]["result"], attachments: received }), reject });
|
|
308
|
+
this.#connection.send({ type: "request", id, op, params }, attachments);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Drops a connection whose host broke the protocol; pending work fails as lost. */
|
|
313
|
+
#fail(): void {
|
|
314
|
+
void this.#connection.close();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// The one place that opens .c64-re-tools/knowledge.db.
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
4
|
+
import { DatabaseSync } from "node:sqlite";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
import { projectRoot } from "../project.ts";
|
|
8
|
+
import { checkVersion, ensureSchema, schemaVersion } from "./schema.ts";
|
|
9
|
+
|
|
10
|
+
export const KNOWLEDGE_DIRECTORY = ".c64-re-tools";
|
|
11
|
+
export const KNOWLEDGE_FILE = "knowledge.db";
|
|
12
|
+
|
|
13
|
+
export const KNOWLEDGE_ERROR_CODES = [
|
|
14
|
+
"invalid-input",
|
|
15
|
+
"not-found",
|
|
16
|
+
"conflict",
|
|
17
|
+
"stale-revision",
|
|
18
|
+
"invalid-database",
|
|
19
|
+
"unsupported-schema",
|
|
20
|
+
] as const;
|
|
21
|
+
export type KnowledgeErrorCode = (typeof KNOWLEDGE_ERROR_CODES)[number];
|
|
22
|
+
|
|
23
|
+
/** A refused knowledge operation, with a stable code and an actionable message. */
|
|
24
|
+
export class KnowledgeError extends Error {
|
|
25
|
+
override name = "KnowledgeError";
|
|
26
|
+
readonly code: KnowledgeErrorCode;
|
|
27
|
+
|
|
28
|
+
constructor(code: KnowledgeErrorCode, message: string) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.code = code;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function knowledgePath(root: string = projectRoot()): string {
|
|
35
|
+
return join(root, KNOWLEDGE_DIRECTORY, KNOWLEDGE_FILE);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function configure(db: DatabaseSync): void {
|
|
39
|
+
// A committed write must be complete in knowledge.db itself: rollback journal, never WAL.
|
|
40
|
+
db.exec("PRAGMA journal_mode = DELETE; PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 5000;");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function open(path: string, readOnly: boolean): DatabaseSync {
|
|
44
|
+
let db: DatabaseSync;
|
|
45
|
+
try {
|
|
46
|
+
db = new DatabaseSync(path, { readOnly });
|
|
47
|
+
configure(db);
|
|
48
|
+
db.prepare("SELECT count(*) FROM sqlite_master").get();
|
|
49
|
+
} catch (error) {
|
|
50
|
+
throw new KnowledgeError("invalid-database", `${KNOWLEDGE_DIRECTORY}/${KNOWLEDGE_FILE} is not a readable SQLite database: ${(error as Error).message}`);
|
|
51
|
+
}
|
|
52
|
+
return db;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Opens the project's knowledge database read-only, as it is: a read never
|
|
57
|
+
* migrates and never writes. Returns undefined when the database does not
|
|
58
|
+
* exist or has no schema yet: that reads as empty knowledge, and a read
|
|
59
|
+
* creates nothing. Refuses a schema version other than this build's.
|
|
60
|
+
*/
|
|
61
|
+
export function openForRead(root: string = projectRoot()): DatabaseSync | undefined {
|
|
62
|
+
const path = knowledgePath(root);
|
|
63
|
+
if (!existsSync(path)) return undefined;
|
|
64
|
+
const db = open(path, true);
|
|
65
|
+
try {
|
|
66
|
+
const version = schemaVersion(db);
|
|
67
|
+
if (version === 0) {
|
|
68
|
+
db.close();
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
checkVersion(version);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
db.close();
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
return db;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Opens the project's knowledge database for a write, creating
|
|
81
|
+
* .c64-re-tools/knowledge.db and its schema when needed. Refuses a schema
|
|
82
|
+
* version other than this build's.
|
|
83
|
+
*/
|
|
84
|
+
export function openForWrite(root: string = projectRoot()): DatabaseSync {
|
|
85
|
+
mkdirSync(join(root, KNOWLEDGE_DIRECTORY), { recursive: true });
|
|
86
|
+
const db = open(knowledgePath(root), false);
|
|
87
|
+
try {
|
|
88
|
+
ensureSchema(db);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
db.close();
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
return db;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Runs one write on the project's knowledge database and closes it. When the
|
|
98
|
+
* project has no knowledge.db yet, `work` first runs on an empty database in
|
|
99
|
+
* memory: a write that is refused there creates no .c64-re-tools/. `work`
|
|
100
|
+
* must change nothing outside the database it gets.
|
|
101
|
+
*/
|
|
102
|
+
export function withWrite<T>(work: (db: DatabaseSync) => T, root: string = projectRoot()): T {
|
|
103
|
+
if (!existsSync(knowledgePath(root))) {
|
|
104
|
+
const trial = new DatabaseSync(":memory:");
|
|
105
|
+
try {
|
|
106
|
+
configure(trial);
|
|
107
|
+
ensureSchema(trial);
|
|
108
|
+
work(trial);
|
|
109
|
+
} finally {
|
|
110
|
+
trial.close();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const db = openForWrite(root);
|
|
114
|
+
try {
|
|
115
|
+
return work(db);
|
|
116
|
+
} finally {
|
|
117
|
+
db.close();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Runs `work` in one immediate transaction: all of it commits, or none of it does. */
|
|
122
|
+
export function transaction<T>(db: DatabaseSync, work: () => T): T {
|
|
123
|
+
db.exec("BEGIN IMMEDIATE");
|
|
124
|
+
try {
|
|
125
|
+
const result = work();
|
|
126
|
+
db.exec("COMMIT");
|
|
127
|
+
return result;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
db.exec("ROLLBACK");
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// Operand expressions (D65): the part of ACME's expression syntax that names
|
|
2
|
+
// an operand by symbols and numbers. Knowledge checks an expression against
|
|
3
|
+
// the image with it, and c64-source writes it with the names of its layout.
|
|
4
|
+
//
|
|
5
|
+
// Priorities are ACME's, from high to low: unary - and !, then * / %, + -,
|
|
6
|
+
// << >> >>>, unary < and >, &, XOR, |. A unary < or > takes everything of a
|
|
7
|
+
// higher priority after it, so <a+1 is the low byte of a+1.
|
|
8
|
+
|
|
9
|
+
/** An expression that does not parse, or that names a symbol with no value. */
|
|
10
|
+
export class ExpressionError extends Error {
|
|
11
|
+
override name = "ExpressionError";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The selectors of an operand expression: a byte of a symbol, its value, or free expression text. */
|
|
15
|
+
export const OPERAND_SELECTS = ["lo", "hi", "value", "expr"] as const;
|
|
16
|
+
export type OperandSelect = (typeof OPERAND_SELECTS)[number];
|
|
17
|
+
|
|
18
|
+
/** An operand expression as knowledge stores it: a symbol with an offset, or text for expr. */
|
|
19
|
+
export interface OperandExpression {
|
|
20
|
+
select: OperandSelect;
|
|
21
|
+
symbol?: string;
|
|
22
|
+
offset?: number;
|
|
23
|
+
expression?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type Node =
|
|
27
|
+
| { kind: "number"; value: number }
|
|
28
|
+
| { kind: "name"; name: string }
|
|
29
|
+
| { kind: "unary"; operator: string; operand: Node }
|
|
30
|
+
| { kind: "binary"; operator: string; left: Node; right: Node };
|
|
31
|
+
|
|
32
|
+
/** Binary operators by priority (higher binds tighter); unary < and > sit at LOW_HIGH. */
|
|
33
|
+
const BINARY: Record<string, number> = { "*": 10, "/": 10, "%": 10, "+": 9, "-": 9, "<<": 8, ">>": 8, ">>>": 8, "&": 4, XOR: 3, "|": 2 };
|
|
34
|
+
const LOW_HIGH = 7;
|
|
35
|
+
const NAME = /^[A-Za-z_.][A-Za-z0-9_]*/;
|
|
36
|
+
const MAX_TEXT = 200;
|
|
37
|
+
|
|
38
|
+
interface Token {
|
|
39
|
+
text: string;
|
|
40
|
+
at: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The expression as tokens with their offsets; context decides % (binary literal or remainder). */
|
|
44
|
+
function tokenize(text: string): Token[] {
|
|
45
|
+
const tokens: Token[] = [];
|
|
46
|
+
let at = 0;
|
|
47
|
+
let operandNext = true;
|
|
48
|
+
while (at < text.length) {
|
|
49
|
+
const rest = text.slice(at);
|
|
50
|
+
const space = /^\s+/.exec(rest);
|
|
51
|
+
if (space) {
|
|
52
|
+
at += space[0].length;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const number: RegExpExecArray | null = operandNext ? /^(\$[0-9A-Fa-f]+|%[01]+|[0-9]+)/.exec(rest) : null;
|
|
56
|
+
const match: RegExpExecArray | null = number ?? /^(>>>|<<|>>|XOR\b)/.exec(rest) ?? NAME.exec(rest) ?? /^[-+*/%&|!<>()]/.exec(rest);
|
|
57
|
+
if (!match) throw new ExpressionError(`"${text}" has an unknown character at position ${at + 1}: "${rest[0]}".`);
|
|
58
|
+
const token: string = match[0];
|
|
59
|
+
tokens.push({ text: token, at });
|
|
60
|
+
at += token.length;
|
|
61
|
+
// After an operand or a ), an operator comes next; after anything else, an operand.
|
|
62
|
+
operandNext = !(number !== null || (NAME.test(token) && token !== "XOR") || token === ")");
|
|
63
|
+
}
|
|
64
|
+
return tokens;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parse(text: string): Node {
|
|
68
|
+
if (text.trim().length === 0) throw new ExpressionError("An expression is empty.");
|
|
69
|
+
if (text.length > MAX_TEXT) throw new ExpressionError(`An expression has at most ${MAX_TEXT} characters.`);
|
|
70
|
+
const tokens = tokenize(text);
|
|
71
|
+
let index = 0;
|
|
72
|
+
const peek = () => tokens[index]?.text;
|
|
73
|
+
const fail = (what: string): never => {
|
|
74
|
+
throw new ExpressionError(`"${text}" ${what}.`);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const operand = (): Node => {
|
|
78
|
+
const token = tokens[index++]?.text ?? fail("ends where an operand is missing");
|
|
79
|
+
if (token === "(") {
|
|
80
|
+
const inner = binary(0);
|
|
81
|
+
if (tokens[index++]?.text !== ")") fail("has a ( with no )");
|
|
82
|
+
return inner;
|
|
83
|
+
}
|
|
84
|
+
if (token === "-" || token === "!") return { kind: "unary", operator: token, operand: operand() };
|
|
85
|
+
if (token === "<" || token === ">") return { kind: "unary", operator: token, operand: binary(LOW_HIGH + 1) };
|
|
86
|
+
if (token.startsWith("$")) return { kind: "number", value: Number.parseInt(token.slice(1), 16) };
|
|
87
|
+
if (token.startsWith("%")) return { kind: "number", value: Number.parseInt(token.slice(1), 2) };
|
|
88
|
+
if (/^[0-9]/.test(token)) return { kind: "number", value: Number.parseInt(token, 10) };
|
|
89
|
+
if (NAME.test(token) && token !== "XOR") return { kind: "name", name: token };
|
|
90
|
+
return fail(`has "${token}" where an operand must be`);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const binary = (minimum: number): Node => {
|
|
94
|
+
let left = operand();
|
|
95
|
+
for (;;) {
|
|
96
|
+
const operator = peek();
|
|
97
|
+
const priority = operator === undefined ? undefined : BINARY[operator];
|
|
98
|
+
if (priority === undefined || priority < minimum) return left;
|
|
99
|
+
index++;
|
|
100
|
+
left = { kind: "binary", operator: operator!, left, right: binary(priority + 1) };
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const tree = binary(0);
|
|
105
|
+
if (index < tokens.length) fail(`has "${tokens[index]!.text}" after the end of the expression`);
|
|
106
|
+
return tree;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function names(node: Node, into: string[]): string[] {
|
|
110
|
+
if (node.kind === "name" && !into.includes(node.name)) into.push(node.name);
|
|
111
|
+
if (node.kind === "unary") names(node.operand, into);
|
|
112
|
+
if (node.kind === "binary") {
|
|
113
|
+
names(node.left, into);
|
|
114
|
+
names(node.right, into);
|
|
115
|
+
}
|
|
116
|
+
return into;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function value(node: Node, resolve: (name: string) => number | undefined): number {
|
|
120
|
+
switch (node.kind) {
|
|
121
|
+
case "number":
|
|
122
|
+
return node.value;
|
|
123
|
+
case "name": {
|
|
124
|
+
const found = resolve(node.name);
|
|
125
|
+
if (found === undefined) throw new ExpressionError(`"${node.name}" is no symbol of the knowledge and no platform name.`);
|
|
126
|
+
return found;
|
|
127
|
+
}
|
|
128
|
+
case "unary": {
|
|
129
|
+
const inner = value(node.operand, resolve);
|
|
130
|
+
if (node.operator === "-") return -inner;
|
|
131
|
+
if (node.operator === "!") return ~inner;
|
|
132
|
+
return node.operator === "<" ? inner & 0xff : (inner >> 8) & 0xff;
|
|
133
|
+
}
|
|
134
|
+
case "binary": {
|
|
135
|
+
const left = value(node.left, resolve);
|
|
136
|
+
const right = value(node.right, resolve);
|
|
137
|
+
switch (node.operator) {
|
|
138
|
+
case "*":
|
|
139
|
+
return left * right;
|
|
140
|
+
case "/":
|
|
141
|
+
case "%":
|
|
142
|
+
if (right === 0) throw new ExpressionError("An expression divides by zero.");
|
|
143
|
+
return node.operator === "/" ? Math.trunc(left / right) : left % right;
|
|
144
|
+
case "+":
|
|
145
|
+
return left + right;
|
|
146
|
+
case "-":
|
|
147
|
+
return left - right;
|
|
148
|
+
case "<<":
|
|
149
|
+
return left << right;
|
|
150
|
+
case ">>":
|
|
151
|
+
return left >> right;
|
|
152
|
+
case ">>>":
|
|
153
|
+
return left >>> right;
|
|
154
|
+
case "&":
|
|
155
|
+
return left & right;
|
|
156
|
+
case "XOR":
|
|
157
|
+
return left ^ right;
|
|
158
|
+
default:
|
|
159
|
+
return left | right;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Throws ExpressionError when the text is no expression of this syntax. */
|
|
166
|
+
export function checkExpression(text: string): void {
|
|
167
|
+
parse(text);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The value of the expression; `resolve` gives the value of a name, or undefined. */
|
|
171
|
+
export function evaluate(text: string, resolve: (name: string) => number | undefined): number {
|
|
172
|
+
return value(parse(text), resolve);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The names the expression uses, each once, in their order. */
|
|
176
|
+
export function identifiers(text: string): string[] {
|
|
177
|
+
return names(parse(text), []);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The text with each name replaced by `replace(name)` (a name it returns
|
|
182
|
+
* unchanged stays). Spaces and other tokens stay as written.
|
|
183
|
+
*/
|
|
184
|
+
export function replaceIdentifiers(text: string, replace: (name: string) => string): string {
|
|
185
|
+
parse(text);
|
|
186
|
+
let out = "";
|
|
187
|
+
let from = 0;
|
|
188
|
+
for (const token of tokenize(text)) {
|
|
189
|
+
if (!NAME.test(token.text) || token.text === "XOR") continue;
|
|
190
|
+
out += text.slice(from, token.at) + replace(token.text);
|
|
191
|
+
from = token.at + token.text.length;
|
|
192
|
+
}
|
|
193
|
+
return out + text.slice(from);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function offsetText(offset: number): string {
|
|
197
|
+
const size = Math.abs(offset);
|
|
198
|
+
return `${offset < 0 ? "-" : "+"}${size < 10 ? String(size) : `$${size.toString(16)}`}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** ACME text of an operand expression, for example <irq_handler, >(sprite_bank+$1000) or bar_colours-1. */
|
|
202
|
+
export function operandText(operand: OperandExpression): string {
|
|
203
|
+
if (operand.select === "expr") return operand.expression!;
|
|
204
|
+
const offset = operand.offset ?? 0;
|
|
205
|
+
const sum = offset === 0 ? operand.symbol! : `${operand.symbol!}${offsetText(offset)}`;
|
|
206
|
+
if (operand.select === "value") return sum;
|
|
207
|
+
const selector = operand.select === "lo" ? "<" : ">";
|
|
208
|
+
return offset === 0 ? `${selector}${sum}` : `${selector}(${sum})`;
|
|
209
|
+
}
|