@henols/vice-mcp 0.1.10 → 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,248 @@
|
|
|
1
|
+
// disasm-decoder.ts
|
|
2
|
+
//
|
|
3
|
+
// The pure `decode(bytes, startAddress, opts) -> Instruction[]` function --
|
|
4
|
+
// D-05's standalone module. Phase 5's backtrace (DERIV-02) and Phase 6's
|
|
5
|
+
// CPU-history decode (GAIN-01) import THIS file directly, never a tool
|
|
6
|
+
// module, so a protocol import here would force those consumers to pull in
|
|
7
|
+
// transport code they do not need. This module has no emulator, no
|
|
8
|
+
// protocol, no network -- its only input is a byte array.
|
|
9
|
+
//
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// WHY THIS FILE EXISTS RATHER THAN LIVING INSIDE THE TOOL HANDLER
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// `stock-disassemble.ts` (04-05) is the tool-facing consumer, but three other
|
|
14
|
+
// consumers need decoding without a socket in the picture at all: the
|
|
15
|
+
// renderer (04-04), Phase 5's backtrace and Phase 6's CPU-history decode.
|
|
16
|
+
// Keeping decode() import-free of `stock-*.ts`/`vice*.ts`/any `node:`
|
|
17
|
+
// builtin means all four can depend on this one file without dragging in
|
|
18
|
+
// transport code.
|
|
19
|
+
//
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// WHAT NOT TO DO
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// - Never import `stock-*.ts`, `vice*.ts` or any `node:` builtin -- the
|
|
24
|
+
// only import is `./disasm-opcodes.ts`.
|
|
25
|
+
// - Never throw on malformed input; return `notes: ["truncated"]` for a
|
|
26
|
+
// partial instruction or `[]` for a malformed top-level argument.
|
|
27
|
+
// - Never fabricate operand bytes that were not in `bytes` (DISASM-05 is
|
|
28
|
+
// precisely the requirement that a partial instruction is reported, not
|
|
29
|
+
// invented).
|
|
30
|
+
// - Never add recursion or an unbounded loop -- the bound is what makes an
|
|
31
|
+
// attacker-controlled memory image safe to decode (04-RESEARCH.md
|
|
32
|
+
// Security Domain, T-04-03-01).
|
|
33
|
+
|
|
34
|
+
import { OPCODES, type AddressingMode } from "./disasm-opcodes.ts";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* D-10's structured note vocabulary. A closed union, not a free string, so
|
|
38
|
+
* the renderer (04-04) and Phase 5's backtrace can switch on it
|
|
39
|
+
* exhaustively.
|
|
40
|
+
*/
|
|
41
|
+
export type DisasmNote = "nmos-page-wrap" | "truncated" | "acme-unassemblable" | "illegal-opcode";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A decoded instruction's operand. `role` and `width` together are what
|
|
45
|
+
* DISASM-06's substitution rule (D-11) reads to decide whether a symbol can
|
|
46
|
+
* safely replace a literal. `value` is the operand as encoded (for
|
|
47
|
+
* `relative`, the raw signed offset -- the resolved address lives in
|
|
48
|
+
* `Instruction.resolvedTarget`). `width` is the operand's byte width (1 or
|
|
49
|
+
* 2), always equal to `entry.length - 1`.
|
|
50
|
+
*/
|
|
51
|
+
export interface DecodedOperand {
|
|
52
|
+
role: "immediate" | "zeropage" | "absolute" | "relative" | "indirect";
|
|
53
|
+
value: number;
|
|
54
|
+
width: 1 | 2;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* One decoded 6502/6510 instruction. `notes` is always present (empty array
|
|
59
|
+
* when there is nothing to say), never `undefined`. `bytes` holds every
|
|
60
|
+
* byte the instruction consumed, including a partial instruction's bytes.
|
|
61
|
+
*/
|
|
62
|
+
export interface Instruction {
|
|
63
|
+
address: number;
|
|
64
|
+
bytes: number[];
|
|
65
|
+
opcode: number;
|
|
66
|
+
mnemonic: string;
|
|
67
|
+
mode: AddressingMode;
|
|
68
|
+
illegal: boolean;
|
|
69
|
+
acmeExpressible: boolean;
|
|
70
|
+
operand?: DecodedOperand;
|
|
71
|
+
resolvedTarget?: number;
|
|
72
|
+
notes: DisasmNote[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface DecodeOptions {
|
|
76
|
+
count?: number;
|
|
77
|
+
end?: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** True iff `value` is a well-formed, generic JSON object -- not null, not
|
|
81
|
+
* an array. Matches this module tree's own isPlainObject() convention
|
|
82
|
+
* (stock-checkpoints.ts, stock-schema-check.ts et al. -- the one-line
|
|
83
|
+
* predicate is repeated per file, never centrally imported). */
|
|
84
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
85
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** True iff `value` is a representable, non-negative whole number -- the
|
|
89
|
+
* shared narrowing for `startAddress`, `opts.count` and `opts.end`. A
|
|
90
|
+
* non-integer, negative or non-safe-integer ("absurd") value is treated as
|
|
91
|
+
* absent rather than thrown on; argument-validation refusal text is the
|
|
92
|
+
* tool's job (04-05), not this module's. */
|
|
93
|
+
function isNonNegativeSafeInteger(value: unknown): value is number {
|
|
94
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Interprets `b` as an 8-bit two's-complement signed byte, per DISASM-04's
|
|
98
|
+
* branch resolution rule: `signed8(b) = b < 0x80 ? b : b - 0x100`. */
|
|
99
|
+
function signed8(b: number): number {
|
|
100
|
+
return b < 0x80 ? b : b - 0x100;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Builds the `notes` array in the plan's mandated, deterministic order:
|
|
104
|
+
* `"truncated"` first (rule 3), then `"nmos-page-wrap"` (rule 7), then
|
|
105
|
+
* `"illegal-opcode"` and `"acme-unassemblable"` (rule 8) -- so the
|
|
106
|
+
* renderer's output and the round-trip test are stable. */
|
|
107
|
+
function buildNotes(flags: { truncated: boolean; pageWrap: boolean; illegal: boolean; acmeExpressible: boolean }): DisasmNote[] {
|
|
108
|
+
const notes: DisasmNote[] = [];
|
|
109
|
+
if (flags.truncated) notes.push("truncated");
|
|
110
|
+
if (flags.pageWrap) notes.push("nmos-page-wrap");
|
|
111
|
+
if (flags.illegal) notes.push("illegal-opcode");
|
|
112
|
+
if (!flags.acmeExpressible) notes.push("acme-unassemblable");
|
|
113
|
+
return notes;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Decodes `bytes` as a stream of 6502/6510 instructions starting at
|
|
118
|
+
* `startAddress`. Bounded by construction (T-04-03-01): a single `while`
|
|
119
|
+
* loop over a byte cursor, every iteration consumes at least one byte, no
|
|
120
|
+
* recursion anywhere in this file. Never throws -- malformed input (a
|
|
121
|
+
* non-`Uint8Array` `bytes`, a negative or non-integer `startAddress`)
|
|
122
|
+
* returns `[]`.
|
|
123
|
+
*/
|
|
124
|
+
export function decode(bytes: Uint8Array, startAddress: number, opts: DecodeOptions = {}): Instruction[] {
|
|
125
|
+
if (!(bytes instanceof Uint8Array)) return [];
|
|
126
|
+
if (!isNonNegativeSafeInteger(startAddress)) return [];
|
|
127
|
+
|
|
128
|
+
const options = isPlainObject(opts) ? opts : {};
|
|
129
|
+
const count = isNonNegativeSafeInteger(options.count) ? options.count : undefined;
|
|
130
|
+
const end = isNonNegativeSafeInteger(options.end) ? options.end : undefined;
|
|
131
|
+
|
|
132
|
+
const instructions: Instruction[] = [];
|
|
133
|
+
let offset = 0;
|
|
134
|
+
|
|
135
|
+
while (offset < bytes.length) {
|
|
136
|
+
if (count !== undefined && instructions.length >= count) break;
|
|
137
|
+
|
|
138
|
+
// Rule 2: address arithmetic wraps at 16 bits.
|
|
139
|
+
const address = (startAddress + offset) & 0xffff;
|
|
140
|
+
|
|
141
|
+
// Rule 9: an instruction starting past `end` is dropped entirely, not
|
|
142
|
+
// emitted. Checked against the START address, before the opcode byte is
|
|
143
|
+
// even read -- an instruction starting at or before `end` is emitted in
|
|
144
|
+
// full even if its last byte lies past `end`.
|
|
145
|
+
if (end !== undefined && address > end) break;
|
|
146
|
+
|
|
147
|
+
const opcodeByte = bytes[offset]!;
|
|
148
|
+
const entry = OPCODES[opcodeByte]!;
|
|
149
|
+
const available = bytes.length - offset;
|
|
150
|
+
|
|
151
|
+
if (available < entry.length) {
|
|
152
|
+
// Rule 3 / DISASM-05: truncated instruction. Only the bytes that
|
|
153
|
+
// actually exist are recorded; operand/resolvedTarget are omitted;
|
|
154
|
+
// never fabricate the missing bytes. The loop stops here.
|
|
155
|
+
const rawBytes: number[] = [];
|
|
156
|
+
for (let i = 0; i < available; i++) rawBytes.push(bytes[offset + i]!);
|
|
157
|
+
|
|
158
|
+
instructions.push({
|
|
159
|
+
address,
|
|
160
|
+
bytes: rawBytes,
|
|
161
|
+
opcode: opcodeByte,
|
|
162
|
+
mnemonic: entry.mnemonic,
|
|
163
|
+
mode: entry.mode,
|
|
164
|
+
illegal: entry.illegal,
|
|
165
|
+
acmeExpressible: entry.acmeExpressible,
|
|
166
|
+
notes: buildNotes({ truncated: true, pageWrap: false, illegal: entry.illegal, acmeExpressible: entry.acmeExpressible }),
|
|
167
|
+
});
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const rawBytes: number[] = [];
|
|
172
|
+
for (let i = 0; i < entry.length; i++) rawBytes.push(bytes[offset + i]!);
|
|
173
|
+
const b1 = entry.length >= 2 ? rawBytes[1]! : undefined;
|
|
174
|
+
const b2 = entry.length >= 3 ? rawBytes[2]! : undefined;
|
|
175
|
+
|
|
176
|
+
let operand: DecodedOperand | undefined;
|
|
177
|
+
let resolvedTarget: number | undefined;
|
|
178
|
+
let pageWrap = false;
|
|
179
|
+
|
|
180
|
+
// Rule 4: operand extraction by mode.
|
|
181
|
+
switch (entry.mode) {
|
|
182
|
+
case "implicit":
|
|
183
|
+
case "accumulator":
|
|
184
|
+
break;
|
|
185
|
+
|
|
186
|
+
case "immediate":
|
|
187
|
+
operand = { role: "immediate", value: b1!, width: 1 };
|
|
188
|
+
break;
|
|
189
|
+
|
|
190
|
+
case "zeropage":
|
|
191
|
+
case "zeropage_x":
|
|
192
|
+
case "zeropage_y":
|
|
193
|
+
case "indirect_x":
|
|
194
|
+
case "indirect_y":
|
|
195
|
+
operand = { role: "zeropage", value: b1!, width: 1 };
|
|
196
|
+
break;
|
|
197
|
+
|
|
198
|
+
case "relative":
|
|
199
|
+
// Rule 5, DISASM-04: raw signed offset stays in `operand.value`;
|
|
200
|
+
// the resolved absolute target (wrapped at 16 bits) is separate.
|
|
201
|
+
operand = { role: "relative", value: b1!, width: 1 };
|
|
202
|
+
resolvedTarget = (address + 2 + signed8(b1!)) & 0xffff;
|
|
203
|
+
break;
|
|
204
|
+
|
|
205
|
+
case "absolute":
|
|
206
|
+
case "absolute_x":
|
|
207
|
+
case "absolute_y": {
|
|
208
|
+
const value = b1! | (b2! << 8);
|
|
209
|
+
operand = { role: "absolute", value, width: 2 };
|
|
210
|
+
// Rule 6: jmp absolute ($4C) and jsr absolute ($20) also resolve a
|
|
211
|
+
// control-flow target, so a consumer never has to special-case
|
|
212
|
+
// which operand is the target.
|
|
213
|
+
if (entry.mode === "absolute" && (opcodeByte === 0x4c || opcodeByte === 0x20)) {
|
|
214
|
+
resolvedTarget = value;
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
case "indirect": {
|
|
220
|
+
const value = b1! | (b2! << 8);
|
|
221
|
+
operand = { role: "indirect", value, width: 2 };
|
|
222
|
+
// Rule 7, D-10: NMOS page-wrap bug, jmp ($xxFF) only ($6C is the
|
|
223
|
+
// only opcode using this mode).
|
|
224
|
+
if (opcodeByte === 0x6c && (value & 0x00ff) === 0x00ff) {
|
|
225
|
+
pageWrap = true;
|
|
226
|
+
}
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
instructions.push({
|
|
232
|
+
address,
|
|
233
|
+
bytes: rawBytes,
|
|
234
|
+
opcode: opcodeByte,
|
|
235
|
+
mnemonic: entry.mnemonic,
|
|
236
|
+
mode: entry.mode,
|
|
237
|
+
illegal: entry.illegal,
|
|
238
|
+
acmeExpressible: entry.acmeExpressible,
|
|
239
|
+
...(operand !== undefined ? { operand } : {}),
|
|
240
|
+
...(resolvedTarget !== undefined ? { resolvedTarget } : {}),
|
|
241
|
+
notes: buildNotes({ truncated: false, pageWrap, illegal: entry.illegal, acmeExpressible: entry.acmeExpressible }),
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
offset += entry.length;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return instructions;
|
|
248
|
+
}
|