@henols/vice-mcp 0.1.12 → 0.2.0
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/backend-detect.mts +58 -8
- package/capability-registry.ts +388 -0
- package/package.json +12 -1
- package/resources/backend-detect.mjs +30 -2
- package/resources/broker-launch.mjs +166 -34
- package/resources/vice-broker.mjs +25 -4
- package/stock-cia.ts +598 -0
- package/stock-connect.ts +137 -20
- package/stock-derived.ts +67 -14
- package/stock-diagnose.ts +994 -0
- package/stock-dispatch.ts +105 -4
- package/stock-handler.ts +29 -0
- package/stock-memory-search.ts +441 -0
- package/stock-memory.ts +92 -13
- package/stock-protocol.ts +328 -0
- package/stock-recycle.ts +500 -0
- package/stock-run-until.ts +400 -0
- package/stock-runstate.ts +46 -2
- package/stock-sprites.ts +712 -0
- package/stock-symbols.ts +431 -0
- package/stock-timing.ts +562 -0
- package/stock-vicii.ts +318 -0
- package/tools-manifest.stock.json +3031 -223
- package/version.ts +279 -0
- package/vice-proxy.ts +49 -6
package/stock-symbols.ts
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// stock-symbols.ts
|
|
3
|
+
//
|
|
4
|
+
// DERIV-04's store: `vice_symbols_load` and `vice_symbols_lookup`, the
|
|
5
|
+
// client-side symbol table this codebase carries no other implementation of.
|
|
6
|
+
// This is the ONLY module that may call stock-address.ts's
|
|
7
|
+
// setSymbolResolver() -- that file's ONE holder is the single seam, and a
|
|
8
|
+
// second holder or a family-local address->name map is that file's own
|
|
9
|
+
// named anti-pattern ("Never add a second resolver holder").
|
|
10
|
+
//
|
|
11
|
+
// Both tools are `needsSession: false` (D-04 of Phase 4): loading or
|
|
12
|
+
// looking up a symbol never opens a monitor connection and therefore never
|
|
13
|
+
// halts the user's running program -- a genuine ergonomic win over the
|
|
14
|
+
// fork, whose implementation lives inside the emulator process.
|
|
15
|
+
//
|
|
16
|
+
// WHY hostpath.ts IS NEVER IMPORTED HERE, spelled out: hostpath.ts
|
|
17
|
+
// translates a container path into a HOST path for a filename stock VICE
|
|
18
|
+
// ITSELF OPENS ACROSS THE WIRE. `vice_symbols_load` reads the file with
|
|
19
|
+
// Node's `fs` inside the MCP server's OWN process; there is no wire
|
|
20
|
+
// filename argument at all, so the translation does not apply and applying
|
|
21
|
+
// it would read the wrong file (or nothing). hostpath-consumers.test.ts's
|
|
22
|
+
// closed five-member production consumer set (containerpath.ts,
|
|
23
|
+
// install-resources.ts, stock-paths.ts, vice-proxy.ts, vice-sync.ts) must
|
|
24
|
+
// stay exactly five -- this module joining it would fail that test outright.
|
|
25
|
+
//
|
|
26
|
+
// The confirmed input format is a VICE label file, one `al C:xxxx .Name`
|
|
27
|
+
// line per symbol, verified against ACME's `--vicelabels` output via
|
|
28
|
+
// acme-build/scripts/acme.mjs's own parser (curateLabels(),
|
|
29
|
+
// `/^al\s+C:[0-9a-f]+\s+\.(\S+)/i`). STATED ASSUMPTION, NOT A VERIFIED FACT:
|
|
30
|
+
// regenerator2000's `--export_lbl` is *expected* to emit the same syntax,
|
|
31
|
+
// but R2000-16(c) has never been run -- hence the parser below SKIPS
|
|
32
|
+
// unrecognised lines rather than refusing the whole file, and no comment or
|
|
33
|
+
// doc here may claim "regenerator2000-compatible" as verified.
|
|
34
|
+
//
|
|
35
|
+
// WHAT NOT TO DO:
|
|
36
|
+
// - Never add a second resolver holder or call setSymbolResolver() from
|
|
37
|
+
// any other new Phase 5 module -- this file is the one seam.
|
|
38
|
+
// - Never import hostpath.ts, stock-paths.ts, containerpath.ts or
|
|
39
|
+
// vice-proxy.ts from this file (see above).
|
|
40
|
+
// - Never build a success-result object literal by hand (an "isError"
|
|
41
|
+
// field set to the negative literal) outside derivedAnswer() -- every
|
|
42
|
+
// success on this module's two handlers goes through it, exactly as
|
|
43
|
+
// stock-handler.ts's own header requires.
|
|
44
|
+
// - Never merge a newly-loaded table into the previous one -- a load is a
|
|
45
|
+
// REPLACE, matching the fork's own single-active-symbol-table framing
|
|
46
|
+
// (T-05-02-05).
|
|
47
|
+
import { readFileSync, realpathSync, statSync } from "node:fs";
|
|
48
|
+
import { resolve, sep } from "node:path";
|
|
49
|
+
|
|
50
|
+
import { ViceError, type ViceErrorOptions } from "./vice.ts";
|
|
51
|
+
import { repoRoot } from "./repo-root.ts";
|
|
52
|
+
import { parseAddress, setSymbolResolver, type SymbolResolver } from "./stock-address.ts";
|
|
53
|
+
import { derivedAnswer, isErrorText } from "./stock-handler.ts";
|
|
54
|
+
import type { DerivedPureHandler } from "./stock-derived.ts";
|
|
55
|
+
|
|
56
|
+
/** True iff `value` is a well-formed, generic JSON object -- not null, not
|
|
57
|
+
* an array. Matches this module tree's own isPlainObject() convention
|
|
58
|
+
* (stock-memory.ts, stock-disassemble.ts et al. each carry a private copy
|
|
59
|
+
* rather than a shared import). */
|
|
60
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
61
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Module constants.
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
/** The confirmed VICE label-file line shape: `al C:xxxx .Name`. Group 1 is
|
|
69
|
+
* the hex address (1-4 digits, either case); group 2 is the symbol name.
|
|
70
|
+
* Anchored at line start -- leading whitespace is trimmed off each line
|
|
71
|
+
* before matching. Deliberately case-sensitive on the literal `al`/`C:`
|
|
72
|
+
* text (unlike acme.mjs's own `/i` parser) since every producer this repo
|
|
73
|
+
* has verified emits exactly that casing; only the hex digits themselves
|
|
74
|
+
* accept either case. */
|
|
75
|
+
const VICE_LABEL_LINE_RE = /^al\s+C:([0-9a-fA-F]{1,4})\s+\.(\S+)/;
|
|
76
|
+
|
|
77
|
+
/** T-05-02-03: three independent resource ceilings, each refusing with both
|
|
78
|
+
* the observed value and the limit named. */
|
|
79
|
+
const MAX_LABEL_FILE_BYTES = 2 * 1024 * 1024;
|
|
80
|
+
const MAX_LABEL_FILE_LINES = 50000;
|
|
81
|
+
const MAX_SYMBOLS = 20000;
|
|
82
|
+
|
|
83
|
+
/** D-05-02: `'auto'` does no format sniffing -- it parses the `al C:xxxx
|
|
84
|
+
* .Name` pattern only and reports the count actually loaded, whether that
|
|
85
|
+
* is `0` or not. `'kickasm'`/`'simple'` are refused by name (no skill or
|
|
86
|
+
* script in this repo produces either). */
|
|
87
|
+
const SUPPORTED_FORMATS = ["auto", "vice"];
|
|
88
|
+
const REFUSED_FORMATS = ["kickasm", "simple"];
|
|
89
|
+
|
|
90
|
+
/** The one address/byte-count error type this module ever throws -- never a
|
|
91
|
+
* bare Error, matching vice.ts's established ViceError hierarchy
|
|
92
|
+
* (stock-address.ts's StockAddressError, stock-paths.ts's StockPathError
|
|
93
|
+
* are the sibling precedents). */
|
|
94
|
+
export class StockSymbolsError extends ViceError {
|
|
95
|
+
constructor(message: string, options: ViceErrorOptions = {}) {
|
|
96
|
+
super(message, options);
|
|
97
|
+
this.name = "StockSymbolsError";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface SymbolTable {
|
|
102
|
+
byName: Map<string, number>;
|
|
103
|
+
byAddress: Map<number, string>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Path containment (T-05-02-01/02) -- resolve `path` against repoRoot() and
|
|
108
|
+
// refuse anything whose resolved absolute path is neither the root itself
|
|
109
|
+
// nor prefixed by `root + sep`, including via a symlink. Never calls
|
|
110
|
+
// hostpath.ts (see this file's header).
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
function isContained(candidate: string, root: string): boolean {
|
|
114
|
+
return candidate === root || candidate.startsWith(root + sep);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Resolves `pathArg` against `repoRoot()`, refusing anything that escapes
|
|
118
|
+
* the workspace either directly or via a symlink, and returns the ONE
|
|
119
|
+
* canonical path that is checked, opened and reported. The rule (WR-08): the
|
|
120
|
+
* path that is containment-checked is the path that is opened and the path
|
|
121
|
+
* that is reported -- returning the pre-`realpathSync` string made the
|
|
122
|
+
* check advisory, because `statSync`/`readFileSync` re-traverse symlinks
|
|
123
|
+
* independently of this function's own check. */
|
|
124
|
+
function resolveLabelFilePath(pathArg: unknown): string {
|
|
125
|
+
if (typeof pathArg !== "string" || pathArg.trim() === "") {
|
|
126
|
+
throw new StockSymbolsError(`path must be a non-empty string, got ${typeof pathArg === "string" ? "an empty/whitespace-only string" : typeof pathArg}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const root = repoRoot();
|
|
130
|
+
const resolved = resolve(root, pathArg.trim());
|
|
131
|
+
|
|
132
|
+
if (!isContained(resolved, root)) {
|
|
133
|
+
throw new StockSymbolsError(`"${resolved}" is outside the workspace root (${root}) -- a symbol file must live inside the workspace`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let real: string;
|
|
137
|
+
try {
|
|
138
|
+
real = realpathSync(resolved);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
141
|
+
throw new StockSymbolsError(`"${resolved}" was not found`);
|
|
142
|
+
}
|
|
143
|
+
throw new StockSymbolsError(`could not resolve "${resolved}" (${err instanceof Error ? err.message : String(err)})`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// WR-05 (2026-08-17): the second check must compare CANONICAL against
|
|
147
|
+
// CANONICAL. `repoRoot()` returns `resolve(...)`, never `realpathSync(...)`,
|
|
148
|
+
// so comparing the fully-canonicalised `real` against a possibly-symlinked
|
|
149
|
+
// `root` refused EVERY file in a workspace whose own path contains a
|
|
150
|
+
// symlinked component -- a bind-mounted or symlinked project directory,
|
|
151
|
+
// `/tmp` on macOS, a `~ -> /mnt/...` home. The file was inside the
|
|
152
|
+
// workspace and the refusal said it was not. Canonicalising the root is the
|
|
153
|
+
// fix; the check itself, and the `real` that is returned (WR-08), stay.
|
|
154
|
+
let realRoot: string;
|
|
155
|
+
try {
|
|
156
|
+
realRoot = realpathSync(root);
|
|
157
|
+
} catch {
|
|
158
|
+
// An unresolvable root cannot be canonicalised, so fall back to the
|
|
159
|
+
// resolved spelling rather than refusing every path -- the check below
|
|
160
|
+
// still runs, just against the less canonical of the two.
|
|
161
|
+
realRoot = root;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!isContained(real, realRoot)) {
|
|
165
|
+
throw new StockSymbolsError(
|
|
166
|
+
`"${resolved}" resolves (via symlink) to "${real}", which is outside the workspace root ` +
|
|
167
|
+
`(${realRoot === root ? realRoot : `${root}, canonically ${realRoot}`}) -- a symbol file must live inside the workspace`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// WR-08: the path that is checked is the path that is opened and the path
|
|
172
|
+
// that is reported -- `real` (the fully-resolved, containment-checked
|
|
173
|
+
// path), never `resolved` (the pre-canonicalisation string). Returning
|
|
174
|
+
// `resolved` made the containment check advisory: statSync()/readFileSync()
|
|
175
|
+
// re-traverse any symlink in `resolved`, so a component swapped after the
|
|
176
|
+
// check on `real` but before those calls could read a file outside the
|
|
177
|
+
// workspace while the check above had passed on a different, already-gone
|
|
178
|
+
// resolution of the same string. Both checks above stay (the pre-realpath
|
|
179
|
+
// check on `resolved` gives the clearer error for an obviously out-of-tree
|
|
180
|
+
// argument); only the returned path changes.
|
|
181
|
+
return real;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Parsing -- defensive per Pitfall 5: an unrecognised line is skipped and
|
|
186
|
+
// counted, never a whole-file refusal.
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
function parseViceLabelFile(text: string): {
|
|
190
|
+
table: SymbolTable;
|
|
191
|
+
symbolCount: number;
|
|
192
|
+
skippedLines: number;
|
|
193
|
+
duplicateNames: number;
|
|
194
|
+
lineCount: number;
|
|
195
|
+
} {
|
|
196
|
+
const lines = text.split("\n");
|
|
197
|
+
const lineCount = lines.length;
|
|
198
|
+
if (lineCount > MAX_LABEL_FILE_LINES) {
|
|
199
|
+
throw new StockSymbolsError(`the label file has ${lineCount} lines, which exceeds the ${MAX_LABEL_FILE_LINES}-line ceiling`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const byName = new Map<string, number>();
|
|
203
|
+
const byAddress = new Map<number, string>();
|
|
204
|
+
let skippedLines = 0;
|
|
205
|
+
let duplicateNames = 0;
|
|
206
|
+
|
|
207
|
+
for (const rawLine of lines) {
|
|
208
|
+
const line = rawLine.trim();
|
|
209
|
+
if (line === "") {
|
|
210
|
+
skippedLines += 1;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const match = VICE_LABEL_LINE_RE.exec(line);
|
|
214
|
+
if (!match) {
|
|
215
|
+
skippedLines += 1;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const address = parseInt(match[1]!, 16);
|
|
219
|
+
// Defensively unreachable given the {1,4} hex bound above, but never
|
|
220
|
+
// trust a parsed value blindly -- an out-of-range address is counted as
|
|
221
|
+
// skipped rather than thrown.
|
|
222
|
+
if (!Number.isInteger(address) || address < 0 || address > 0xffff) {
|
|
223
|
+
skippedLines += 1;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const name = match[2]!;
|
|
227
|
+
if (byName.has(name)) {
|
|
228
|
+
duplicateNames += 1;
|
|
229
|
+
}
|
|
230
|
+
byName.set(name, address); // last definition wins
|
|
231
|
+
if (!byAddress.has(address)) {
|
|
232
|
+
byAddress.set(address, name); // first name for an address wins
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (byName.size > MAX_SYMBOLS) {
|
|
237
|
+
throw new StockSymbolsError(`the label file defines ${byName.size} distinct symbol names, which exceeds the ${MAX_SYMBOLS}-symbol ceiling`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return { table: { byName, byAddress }, symbolCount: byName.size, skippedLines, duplicateNames, lineCount };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// Module state about THIS module's own load -- not a second resolver
|
|
245
|
+
// holder. loadedTable/loadedSymbolCount let handleSymbolsLookup answer
|
|
246
|
+
// without re-reading the file. (WR-11: a third field tracking the last-
|
|
247
|
+
// loaded path was write-only -- assigned on every load and cleared on
|
|
248
|
+
// reset, but read nowhere in the codebase. Deleted rather than replaced
|
|
249
|
+
// with an answer field: adding a key to either answer would require a
|
|
250
|
+
// tools-manifest.stock.json change this plan deliberately excludes.)
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
let loadedTable: SymbolTable | null = null;
|
|
254
|
+
let loadedSymbolCount = 0;
|
|
255
|
+
|
|
256
|
+
/** Builds one SymbolResolver implementing BOTH directions and installs it
|
|
257
|
+
* into stock-address.ts's existing holder via setSymbolResolver() -- a load
|
|
258
|
+
* is a REPLACE, never a merge: whatever was installed before is discarded. */
|
|
259
|
+
function installSymbolTable(table: SymbolTable): void {
|
|
260
|
+
const resolver: SymbolResolver = {
|
|
261
|
+
resolve: (name) => table.byName.get(name),
|
|
262
|
+
nameFor: (address) => table.byAddress.get(address),
|
|
263
|
+
};
|
|
264
|
+
setSymbolResolver(resolver);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Test-only reset, following stock-paths.ts's setIsInsideContainerForTest()
|
|
268
|
+
* / stock-runstate.ts's resetRunStateTrackersForTest() precedent: a
|
|
269
|
+
* module-level reset exported from the module that owns the state. Also
|
|
270
|
+
* clears stock-address.ts's holder so no test leaks a loaded table into
|
|
271
|
+
* another file's run. */
|
|
272
|
+
export function resetSymbolStoreForTest(): void {
|
|
273
|
+
loadedTable = null;
|
|
274
|
+
loadedSymbolCount = 0;
|
|
275
|
+
setSymbolResolver(null);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
// vice_symbols_load
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
export const handleSymbolsLoad: DerivedPureHandler = async (args, _deps) => {
|
|
283
|
+
if (!isPlainObject(args)) {
|
|
284
|
+
return isErrorText("vice_symbols_load: arguments must be an object");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
let format = "auto";
|
|
288
|
+
if (args.format !== undefined) {
|
|
289
|
+
if (typeof args.format !== "string") {
|
|
290
|
+
return isErrorText(`vice_symbols_load: format must be a string, got ${typeof args.format}`);
|
|
291
|
+
}
|
|
292
|
+
if (REFUSED_FORMATS.includes(args.format)) {
|
|
293
|
+
return isErrorText(
|
|
294
|
+
`vice_symbols_load: format "${args.format}" is not supported on the stock backend -- only VICE-format label files ` +
|
|
295
|
+
`("al C:xxxx .Name" lines, as produced by ACME's --vicelabels) are supported. format must be "auto" or "vice".`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
if (!SUPPORTED_FORMATS.includes(args.format)) {
|
|
299
|
+
return isErrorText(
|
|
300
|
+
`vice_symbols_load: format "${args.format}" is not one of the fork's declared values (auto, kickasm, vice, simple) -- ` +
|
|
301
|
+
`only "auto" and "vice" are supported on the stock backend.`,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
format = args.format;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
let resolvedPath: string;
|
|
308
|
+
try {
|
|
309
|
+
resolvedPath = resolveLabelFilePath(args.path);
|
|
310
|
+
} catch (err) {
|
|
311
|
+
return isErrorText(`vice_symbols_load: ${err instanceof Error ? err.message : String(err)}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
let size: number;
|
|
315
|
+
try {
|
|
316
|
+
size = statSync(resolvedPath).size;
|
|
317
|
+
} catch (err) {
|
|
318
|
+
return isErrorText(`vice_symbols_load: could not stat "${resolvedPath}" (${err instanceof Error ? err.message : String(err)})`);
|
|
319
|
+
}
|
|
320
|
+
if (size > MAX_LABEL_FILE_BYTES) {
|
|
321
|
+
return isErrorText(`vice_symbols_load: "${resolvedPath}" is ${size} bytes, which exceeds the ${MAX_LABEL_FILE_BYTES}-byte ceiling`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
let text: string;
|
|
325
|
+
try {
|
|
326
|
+
text = readFileSync(resolvedPath, "utf8");
|
|
327
|
+
} catch (err) {
|
|
328
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
329
|
+
if (code === "ENOENT") {
|
|
330
|
+
return isErrorText(`vice_symbols_load: "${resolvedPath}" was not found`);
|
|
331
|
+
}
|
|
332
|
+
if (code === "EACCES") {
|
|
333
|
+
return isErrorText(`vice_symbols_load: permission denied reading "${resolvedPath}"`);
|
|
334
|
+
}
|
|
335
|
+
if (code === "EISDIR") {
|
|
336
|
+
return isErrorText(`vice_symbols_load: "${resolvedPath}" is a directory, not a file`);
|
|
337
|
+
}
|
|
338
|
+
return isErrorText(`vice_symbols_load: could not read "${resolvedPath}" (${err instanceof Error ? err.message : String(err)})`);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
let parsed: ReturnType<typeof parseViceLabelFile>;
|
|
342
|
+
try {
|
|
343
|
+
parsed = parseViceLabelFile(text);
|
|
344
|
+
} catch (err) {
|
|
345
|
+
return isErrorText(`vice_symbols_load: ${err instanceof Error ? err.message : String(err)}`);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const replaced = loadedTable !== null;
|
|
349
|
+
installSymbolTable(parsed.table);
|
|
350
|
+
loadedTable = parsed.table;
|
|
351
|
+
loadedSymbolCount = parsed.symbolCount;
|
|
352
|
+
|
|
353
|
+
const payload: Record<string, unknown> = {
|
|
354
|
+
path: args.path,
|
|
355
|
+
resolvedPath,
|
|
356
|
+
format: "vice",
|
|
357
|
+
symbolCount: parsed.symbolCount,
|
|
358
|
+
skippedLines: parsed.skippedLines,
|
|
359
|
+
duplicateNames: parsed.duplicateNames,
|
|
360
|
+
lineCount: parsed.lineCount,
|
|
361
|
+
replaced,
|
|
362
|
+
};
|
|
363
|
+
if (parsed.symbolCount === 0) {
|
|
364
|
+
payload.note =
|
|
365
|
+
`no "al C:xxxx .Name" lines were found in this file -- this is not an error. If the file was produced by ` +
|
|
366
|
+
`KickAssembler or another non-VICE format, it is not supported on the stock backend (format: "${format}").`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return derivedAnswer(payload);
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// ---------------------------------------------------------------------------
|
|
373
|
+
// vice_symbols_lookup
|
|
374
|
+
// ---------------------------------------------------------------------------
|
|
375
|
+
|
|
376
|
+
export const handleSymbolsLookup: DerivedPureHandler = async (args, _deps) => {
|
|
377
|
+
if (!isPlainObject(args)) {
|
|
378
|
+
return isErrorText("vice_symbols_lookup: arguments must be an object");
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const hasName = args.name !== undefined;
|
|
382
|
+
const hasAddress = args.address !== undefined;
|
|
383
|
+
|
|
384
|
+
if (!hasName && !hasAddress) {
|
|
385
|
+
return isErrorText("vice_symbols_lookup: exactly one of name or address is required");
|
|
386
|
+
}
|
|
387
|
+
if (hasName && hasAddress) {
|
|
388
|
+
return isErrorText("vice_symbols_lookup: name and address are mutually exclusive -- supply exactly one");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const noTableNote = loadedTable === null ? "no symbol table is loaded -- call vice_symbols_load first" : undefined;
|
|
392
|
+
|
|
393
|
+
if (hasName) {
|
|
394
|
+
if (typeof args.name !== "string") {
|
|
395
|
+
return isErrorText(`vice_symbols_lookup: name must be a string, got ${typeof args.name}`);
|
|
396
|
+
}
|
|
397
|
+
const address = loadedTable?.byName.get(args.name);
|
|
398
|
+
const payload: Record<string, unknown> = { query: { name: args.name }, found: address !== undefined, symbolCount: loadedSymbolCount };
|
|
399
|
+
if (address !== undefined) {
|
|
400
|
+
payload.name = args.name;
|
|
401
|
+
payload.address = address;
|
|
402
|
+
}
|
|
403
|
+
if (noTableNote) {
|
|
404
|
+
payload.note = noTableNote;
|
|
405
|
+
}
|
|
406
|
+
return derivedAnswer(payload);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
let address: number;
|
|
410
|
+
try {
|
|
411
|
+
address = parseAddress(args.address, { what: "address" });
|
|
412
|
+
} catch (err) {
|
|
413
|
+
return isErrorText(`vice_symbols_lookup: ${err instanceof Error ? err.message : String(err)}`);
|
|
414
|
+
}
|
|
415
|
+
const name = loadedTable?.byAddress.get(address);
|
|
416
|
+
// `query` echoes the value the lookup was PERFORMED AGAINST -- the parsed
|
|
417
|
+
// `address` local -- never the caller's raw `args.address`. parseAddress()
|
|
418
|
+
// accepts "$d020"/"0xd020" strings as well as numbers, but this tool's
|
|
419
|
+
// declared outputSchema pins `query.address` to `type: "number"`; echoing
|
|
420
|
+
// the raw argument would make the answer's own shape depend on the
|
|
421
|
+
// caller's formatting choice and violate that schema (WR-01, D-05-18).
|
|
422
|
+
const payload: Record<string, unknown> = { query: { address }, found: name !== undefined, symbolCount: loadedSymbolCount };
|
|
423
|
+
if (name !== undefined) {
|
|
424
|
+
payload.name = name;
|
|
425
|
+
payload.address = address;
|
|
426
|
+
}
|
|
427
|
+
if (noTableNote) {
|
|
428
|
+
payload.note = noTableNote;
|
|
429
|
+
}
|
|
430
|
+
return derivedAnswer(payload);
|
|
431
|
+
};
|