@henols/vice-mcp 0.1.9 → 0.1.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/README.md +15 -0
- package/THIRD-PARTY-NOTICES.md +113 -0
- package/backend-detect.mts +595 -0
- package/build.ts +1 -0
- package/disasm-decoder.ts +248 -0
- package/disasm-opcodes.ts +464 -0
- package/disasm-renderer.ts +306 -0
- package/package.json +27 -2
- package/refresh-manifest.ts +23 -2
- package/resources/backend-detect.mjs +396 -0
- package/resources/broker-control.mjs +110 -8
- package/resources/broker-kill.mjs +114 -103
- package/resources/broker-launch.mjs +334 -19
- package/resources/broker-state.mjs +11 -0
- package/resources/vice-broker.mjs +185 -14
- package/stock-address.ts +219 -0
- package/stock-checkpoints.ts +794 -0
- package/stock-condition.ts +636 -0
- package/stock-connect.ts +427 -0
- package/stock-derived.ts +122 -0
- package/stock-disassemble.ts +252 -0
- package/stock-dispatch.ts +640 -0
- package/stock-execution.ts +327 -0
- package/stock-handler.ts +175 -0
- package/stock-input.ts +274 -0
- package/stock-machine.ts +357 -0
- package/stock-memory.ts +323 -0
- package/stock-paths.ts +191 -0
- package/stock-petscii.ts +143 -0
- package/stock-protocol.ts +2057 -0
- package/stock-registers.ts +324 -0
- package/stock-runstate.ts +104 -0
- package/tools-manifest.json +1 -9
- package/tools-manifest.stock.json +841 -0
- package/vice-broker-client.ts +233 -7
- package/vice-proxy.ts +202 -17
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// stock-disassemble.ts
|
|
3
|
+
//
|
|
4
|
+
// vice_disassemble -- a DERIVED tool (DERIV-07, DISASM-01): its answer is
|
|
5
|
+
// computed CLIENT-SIDE from bytes MEM_GET returned (disasm-decoder.ts's
|
|
6
|
+
// decode() + disasm-renderer.ts's render()), never answered by one
|
|
7
|
+
// binary-monitor opcode the way a direct tool's answer is. Registered
|
|
8
|
+
// through withDerivedTool() in stock-dispatch.ts, never withStockSession()
|
|
9
|
+
// (D-01/D-03) -- this is the first and largest consumer of the derived-tool
|
|
10
|
+
// seam 04-02 built.
|
|
11
|
+
//
|
|
12
|
+
// WHY THIS FILE EXISTS: DISASM-01 is criterion 2's own sentence -- "a user
|
|
13
|
+
// can disassemble a memory range on the stock backend" -- and the binary
|
|
14
|
+
// monitor has no disassemble opcode at all. `address`/`count`/`show_symbols`
|
|
15
|
+
// keep the fork's own names, types and defaults (Phase 3 D-03); `end` is a
|
|
16
|
+
// stock-only optional extra, mutually exclusive with `count` (D-12, never
|
|
17
|
+
// silently resolved -- a caller that gets a silently-different range than it
|
|
18
|
+
// asked for reads the wrong code).
|
|
19
|
+
//
|
|
20
|
+
// WHAT NOT TO DO:
|
|
21
|
+
// - Never import hostpath.ts or vice-proxy.ts, and never call the
|
|
22
|
+
// fork-forwarding function's rewriteArguments() -- hostpath-consumers.test.ts
|
|
23
|
+
// gates this file's absence from the closed host-path consumer set
|
|
24
|
+
// (D-02). This tool takes no path argument at all; the surface is empty
|
|
25
|
+
// by construction.
|
|
26
|
+
// - Never issue an unrequested resume (Phase 3 D-05) -- this handler sends
|
|
27
|
+
// MEM_GET and nothing else. `runState` on the answer (via stockAnswer())
|
|
28
|
+
// reports the halt honestly.
|
|
29
|
+
// - Never turn the MEM_GET body's side-effect flag on -- disassembling
|
|
30
|
+
// $D000-$DFFF must never clear a pending VIC-II IRQ flag or otherwise
|
|
31
|
+
// mutate emulator state as a side effect of reading it. `sidefx` is
|
|
32
|
+
// hardcoded `false` below with no argument to override it.
|
|
33
|
+
// - Never build the answer outside stockAnswer() (D-06) -- that is exactly
|
|
34
|
+
// how an answer ships without `runState`.
|
|
35
|
+
// - Never re-derive address/byte-count parsing locally (D-04) --
|
|
36
|
+
// stock-address.ts's parseAddress()/parseByteCount() are the only seam.
|
|
37
|
+
import { CommandType, memGetBody } from "./stock-protocol.ts";
|
|
38
|
+
import { parseAddress, parseByteCount, symbolNameFor, hasSymbolStore } from "./stock-address.ts";
|
|
39
|
+
import { convertWireError, isErrorText, stockAnswer, type StockSessionHandler } from "./stock-handler.ts";
|
|
40
|
+
import { decode, type Instruction } from "./disasm-decoder.ts";
|
|
41
|
+
import { render } from "./disasm-renderer.ts";
|
|
42
|
+
|
|
43
|
+
/** True iff `value` is a well-formed, generic JSON object -- not null, not
|
|
44
|
+
* an array. Matches this module tree's own isPlainObject() convention
|
|
45
|
+
* (stock-memory.ts, disasm-decoder.ts et al.). */
|
|
46
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
47
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** D-13's answer bound: at most this many instructions are ever returned in
|
|
51
|
+
* one answer, regardless of which form (`count` or `end`) requested the
|
|
52
|
+
* range. `count` itself is already capped at this same value (the fork's own
|
|
53
|
+
* documented max), but the `end` form has no natural cap of its own -- an
|
|
54
|
+
* unbounded answer is the DoS surface (T-04-05-03). */
|
|
55
|
+
const MAX_INSTRUCTIONS = 100;
|
|
56
|
+
|
|
57
|
+
/** Renders an 8-bit value as ACME hex syntax, e.g. `$0f`. Duplicated from
|
|
58
|
+
* disasm-renderer.ts's own private helper of the same shape -- that module
|
|
59
|
+
* exports no per-operand text primitive, and this file's `instructions[]`
|
|
60
|
+
* answer field is a distinct concern from `listing` (D-13's structured
|
|
61
|
+
* per-instruction fields are plain numeric text, never symbol-substituted;
|
|
62
|
+
* `listing` is the one place a substituted symbol name appears). */
|
|
63
|
+
function hex2(value: number): string {
|
|
64
|
+
return `$${(value & 0xff).toString(16).padStart(2, "0")}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Renders a 16-bit value as ACME hex syntax, e.g. `$d020`. */
|
|
68
|
+
function hex4(value: number): string {
|
|
69
|
+
return `$${(value & 0xffff).toString(16).padStart(4, "0")}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Renders just the operand text for one decoded, non-truncated instruction
|
|
74
|
+
* -- the per-instruction `operand` field (D-13), always numeric (never
|
|
75
|
+
* symbol-substituted; that substitution belongs to `listing` alone).
|
|
76
|
+
* Returns `""` for a mode with no operand at all. Must never be called on a
|
|
77
|
+
* truncated instruction (its `operand`/`resolvedTarget` keys are absent by
|
|
78
|
+
* construction, DISASM-05) -- callers guard on `notes.includes("truncated")`
|
|
79
|
+
* first and use `""` directly instead.
|
|
80
|
+
*/
|
|
81
|
+
function operandTextFor(instr: Instruction): string {
|
|
82
|
+
switch (instr.mode) {
|
|
83
|
+
case "implicit":
|
|
84
|
+
case "accumulator":
|
|
85
|
+
return "";
|
|
86
|
+
case "immediate":
|
|
87
|
+
return `#${hex2(instr.operand!.value)}`;
|
|
88
|
+
case "zeropage":
|
|
89
|
+
return hex2(instr.operand!.value);
|
|
90
|
+
case "zeropage_x":
|
|
91
|
+
return `${hex2(instr.operand!.value)},x`;
|
|
92
|
+
case "zeropage_y":
|
|
93
|
+
return `${hex2(instr.operand!.value)},y`;
|
|
94
|
+
case "indirect_x":
|
|
95
|
+
return `(${hex2(instr.operand!.value)},x)`;
|
|
96
|
+
case "indirect_y":
|
|
97
|
+
return `(${hex2(instr.operand!.value)}),y`;
|
|
98
|
+
case "indirect":
|
|
99
|
+
return `(${hex4(instr.operand!.value)})`;
|
|
100
|
+
case "relative":
|
|
101
|
+
return hex4(instr.resolvedTarget!);
|
|
102
|
+
case "absolute":
|
|
103
|
+
return hex4(instr.operand!.value);
|
|
104
|
+
case "absolute_x":
|
|
105
|
+
return `${hex4(instr.operand!.value)},x`;
|
|
106
|
+
case "absolute_y":
|
|
107
|
+
return `${hex4(instr.operand!.value)},y`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export const handleDisassemble: StockSessionHandler = async (args, session, _deps) => {
|
|
112
|
+
if (!isPlainObject(args)) {
|
|
113
|
+
return isErrorText("vice_disassemble: arguments must be an object");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// --------------------------------------------------------- address (required)
|
|
117
|
+
|
|
118
|
+
let address: number;
|
|
119
|
+
try {
|
|
120
|
+
address = parseAddress(args.address, { what: "address" });
|
|
121
|
+
} catch (err) {
|
|
122
|
+
return isErrorText(`vice_disassemble: ${err instanceof Error ? err.message : String(err)}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// --------------------------------------------------------- D-12: mutual exclusion, refused outright
|
|
126
|
+
//
|
|
127
|
+
// Gated on args.count !== undefined -- explicitly supplied -- never on the
|
|
128
|
+
// resolved default, so an `end`-only call is never refused because of
|
|
129
|
+
// count's own default of 10.
|
|
130
|
+
if (args.count !== undefined && args.end !== undefined) {
|
|
131
|
+
return isErrorText(
|
|
132
|
+
`vice_disassemble: count and end are mutually exclusive -- supply one or the other, not both ` +
|
|
133
|
+
`(got count=${JSON.stringify(args.count)}, end=${JSON.stringify(args.end)})`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// --------------------------------------------------------- count (optional, default 10, max 100)
|
|
138
|
+
|
|
139
|
+
let count: number | undefined;
|
|
140
|
+
if (args.count !== undefined) {
|
|
141
|
+
try {
|
|
142
|
+
count = parseByteCount(args.count, { max: MAX_INSTRUCTIONS, what: "count" });
|
|
143
|
+
} catch (err) {
|
|
144
|
+
return isErrorText(`vice_disassemble: ${err instanceof Error ? err.message : String(err)}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const effectiveCount = count ?? 10;
|
|
148
|
+
|
|
149
|
+
// --------------------------------------------------------- end (optional, stock-only extra)
|
|
150
|
+
|
|
151
|
+
let end: number | undefined;
|
|
152
|
+
if (args.end !== undefined) {
|
|
153
|
+
try {
|
|
154
|
+
end = parseAddress(args.end, { what: "end" });
|
|
155
|
+
} catch (err) {
|
|
156
|
+
return isErrorText(`vice_disassemble: ${err instanceof Error ? err.message : String(err)}`);
|
|
157
|
+
}
|
|
158
|
+
if (end < address) {
|
|
159
|
+
return isErrorText(
|
|
160
|
+
`vice_disassemble: end (0x${end.toString(16)}) must be >= address (0x${address.toString(16)})`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// --------------------------------------------------------- show_symbols (optional, default true)
|
|
166
|
+
|
|
167
|
+
let showSymbols = true;
|
|
168
|
+
if (args.show_symbols !== undefined) {
|
|
169
|
+
if (typeof args.show_symbols !== "boolean") {
|
|
170
|
+
return isErrorText(`vice_disassemble: show_symbols must be a boolean, got ${typeof args.show_symbols}`);
|
|
171
|
+
}
|
|
172
|
+
showSymbols = args.show_symbols;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// --------------------------------------------------------- bounded memory read (Phase 3 D-05: halts, never resumes)
|
|
176
|
+
//
|
|
177
|
+
// `end` form: over-read by two bytes so the last instruction that STARTS
|
|
178
|
+
// at or before `end` has its full length available; `count` form:
|
|
179
|
+
// over-read by up to two extra bytes per instruction (three is the
|
|
180
|
+
// maximum instruction length) so `count` instructions can always be
|
|
181
|
+
// decoded. Both clamped at $ffff -- a genuine memspace boundary, not a
|
|
182
|
+
// client bug.
|
|
183
|
+
const readEnd = end !== undefined ? Math.min(end + 2, 0xffff) : Math.min(address + effectiveCount * 3 - 1, 0xffff);
|
|
184
|
+
|
|
185
|
+
const body = memGetBody({ sidefx: false, start: address, end: readEnd, memspace: 0x00, bank: 0x0000 });
|
|
186
|
+
|
|
187
|
+
let response;
|
|
188
|
+
try {
|
|
189
|
+
response = await session.client.send(CommandType.MemoryGet, body);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
return convertWireError("vice_disassemble", err);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (response.type !== "memory_get") {
|
|
195
|
+
return isErrorText(
|
|
196
|
+
`vice_disassemble: the binary monitor replied with an unexpected response type ("${response.type}"), expected "memory_get"`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const expectedLength = readEnd - address + 1;
|
|
201
|
+
if (response.bytes.length !== expectedLength) {
|
|
202
|
+
return isErrorText(
|
|
203
|
+
`vice_disassemble: expected ${expectedLength} byte(s), got ${response.bytes.length} -- a short read is a wrong answer, not a partial success`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// --------------------------------------------------------- decode and render
|
|
208
|
+
|
|
209
|
+
const decoded = decode(response.bytes, address, end !== undefined ? { end } : { count: effectiveCount });
|
|
210
|
+
|
|
211
|
+
let limitReached = false;
|
|
212
|
+
let nextAddress: number | undefined;
|
|
213
|
+
let kept = decoded;
|
|
214
|
+
if (decoded.length > MAX_INSTRUCTIONS) {
|
|
215
|
+
kept = decoded.slice(0, MAX_INSTRUCTIONS);
|
|
216
|
+
limitReached = true;
|
|
217
|
+
nextAddress = decoded[MAX_INSTRUCTIONS]!.address;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// D-14: show_symbols with no store installed is a successful no-op that
|
|
221
|
+
// SAYS SO -- never an error.
|
|
222
|
+
const symbolsApplied = showSymbols && hasSymbolStore();
|
|
223
|
+
const listing = render(kept, { showSymbols: symbolsApplied, symbolFor: symbolNameFor, origin: address });
|
|
224
|
+
|
|
225
|
+
const instructions = kept.map((instr) => {
|
|
226
|
+
const truncated = instr.notes.includes("truncated");
|
|
227
|
+
return {
|
|
228
|
+
address: instr.address,
|
|
229
|
+
bytes: instr.bytes,
|
|
230
|
+
mnemonic: truncated ? "" : instr.mnemonic,
|
|
231
|
+
operand: truncated ? "" : operandTextFor(instr),
|
|
232
|
+
...(instr.resolvedTarget !== undefined ? { resolvedTarget: instr.resolvedTarget } : {}),
|
|
233
|
+
notes: instr.notes,
|
|
234
|
+
};
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const payload: Record<string, unknown> = {
|
|
238
|
+
address,
|
|
239
|
+
...(end !== undefined ? { end } : {}),
|
|
240
|
+
count: kept.length,
|
|
241
|
+
instructions,
|
|
242
|
+
listing,
|
|
243
|
+
symbolsApplied,
|
|
244
|
+
...(showSymbols && !symbolsApplied
|
|
245
|
+
? { symbolNote: "no symbol table is loaded -- addresses are rendered numerically. Load one to see symbol names." }
|
|
246
|
+
: {}),
|
|
247
|
+
limitReached,
|
|
248
|
+
...(limitReached ? { nextAddress } : {}),
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
return stockAnswer(session.client, payload);
|
|
252
|
+
};
|