@henols/vice-mcp 0.2.1 → 0.2.2
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 +2 -1
- package/THIRD-PARTY-NOTICES.md +1 -24
- package/{r2000-acme-ident.ts → anno-acme-ident.ts} +13 -13
- package/anno-cli.ts +1465 -0
- package/{r2000-confidence.ts → anno-confidence.ts} +22 -22
- package/anno-coverage.ts +2465 -0
- package/{r2000-d64.ts → anno-d64.ts} +5 -5
- package/anno-derive.ts +590 -0
- package/anno-details.ts +169 -0
- package/anno-enum-gen.ts +533 -0
- package/anno-export-asm.ts +1310 -0
- package/anno-index.ts +150 -0
- package/{r2000-memmap-render.ts → anno-memmap-render.ts} +236 -95
- package/{r2000-regbits-gen.ts → anno-regbits-gen.ts} +20 -15
- package/{r2000-regbits.json → anno-regbits.json} +2 -2
- package/anno-register.ts +240 -0
- package/anno-store.ts +3486 -0
- package/anno-symbols.ts +266 -0
- package/anno-tools.ts +2111 -0
- package/anno-types.ts +1636 -0
- package/block-class.ts +201 -0
- package/build.ts +1 -1
- package/capability-registry.ts +3 -1
- package/disasm-decoder.ts +14 -14
- package/disasm-opcodes.ts +4 -4
- package/disasm-renderer.ts +2 -2
- package/hostpath.ts +1 -1
- package/install-resources.ts +1 -1
- package/package.json +23 -17
- package/prg-image.ts +119 -0
- package/repo-root.ts +20 -5
- package/resources/broker-launch.mjs +8 -4
- package/resources/vice-launcher.sh +3 -3
- package/stock-address.ts +5 -5
- package/stock-cia.ts +2 -2
- package/stock-condition.ts +7 -7
- package/stock-connect.ts +1 -1
- package/stock-dispatch.ts +35 -5
- package/stock-execution.ts +5 -3
- package/stock-input.ts +9 -9
- package/stock-machine.ts +17 -6
- package/stock-protocol.ts +16 -11
- package/stock-registers.ts +54 -29
- package/stock-sprites.ts +3 -3
- package/stock-symbols.ts +9 -9
- package/stock-timing.ts +1 -1
- package/stock-vicii.ts +1 -1
- package/version.ts +1 -1
- package/vice-proxy.ts +68 -46
- package/r2000-cli.ts +0 -1103
- package/r2000-enum-gen.ts +0 -574
- package/r2000-launch.ts +0 -357
- package/r2000-mcp-client.ts +0 -596
- package/r2000-project.ts +0 -190
- package/r2000-symbols.ts +0 -388
- package/r2000-tools.ts +0 -914
- package/r2000-verify.ts +0 -184
|
@@ -4,19 +4,19 @@
|
|
|
4
4
|
// requirement.
|
|
5
5
|
//
|
|
6
6
|
// WHY THIS FILE EXISTS HERE, AND NOT AS AN EXTENSION OF
|
|
7
|
-
//
|
|
7
|
+
// `src/skills/c64-ram-capture/scripts/d64-parse.mjs`: the researcher's
|
|
8
8
|
// own recommendation (RESEARCH.md Open Question #2) was to extend
|
|
9
9
|
// `d64-parse.mjs` in place, since it already walks the directory chain. That
|
|
10
10
|
// is not reachable in practice: this MCP server ships as `@henols/vice-mcp`,
|
|
11
|
-
// whose `files[]` in `package.json` lists only
|
|
12
|
-
// while
|
|
11
|
+
// whose `files[]` in `package.json` lists only `src/mcp/vice/` contents,
|
|
12
|
+
// while `src/skills/**` ships in the *other* package
|
|
13
13
|
// (`@henols/c64-re-tools`). An import from this seam into a skill script
|
|
14
14
|
// cannot resolve on either npm-installer route (neither copies the sibling
|
|
15
15
|
// package's source tree onto disk next to it), and
|
|
16
16
|
// `scripts/check-npm-packages.mjs`'s transitive-closure walk over `files[]`
|
|
17
17
|
// would fail the pack the moment a reachable module sat outside the listed
|
|
18
18
|
// set. So this is a SECOND, independent copy of the sector-chain-walk
|
|
19
|
-
// algorithm, container-side, scoped to exactly what the
|
|
19
|
+
// algorithm, container-side, scoped to exactly what the anno bootstrap
|
|
20
20
|
// needs -- not a shared library and not an import of the skill-side module.
|
|
21
21
|
//
|
|
22
22
|
// `d64-parse.mjs` REMAINS the skill-side owner of the algorithm and is left
|
|
@@ -215,7 +215,7 @@ export function listEntries(image: Uint8Array): D64Entry[] {
|
|
|
215
215
|
* The returned bytes are the file's RAW content INCLUDING its leading 2-byte
|
|
216
216
|
* PRG load address, unmodified -- that is deliberate, since the whole point
|
|
217
217
|
* of this module is to hand bytes straight to `parsePrg()` in
|
|
218
|
-
* `
|
|
218
|
+
* `prg-image.ts`, which expects that same 2-byte header.
|
|
219
219
|
*/
|
|
220
220
|
export function extractEntry(image: Uint8Array, entryName: string): Uint8Array {
|
|
221
221
|
const entries = listEntries(image);
|
package/anno-derive.ts
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// anno-derive.ts
|
|
3
|
+
//
|
|
4
|
+
// WHAT THIS IS THE ONE AUTHORITATIVE PLACE FOR: answers DERIVED from the
|
|
5
|
+
// program bytes on every query -- the cross-reference set for an address, and
|
|
6
|
+
// the search over labels, comments and rendered instruction text. Every answer
|
|
7
|
+
// here is recomputed from the caller's own bytes and the store's own range
|
|
8
|
+
// table each time it is asked for. Nothing this module computes is retained
|
|
9
|
+
// between calls, in memory or anywhere else.
|
|
10
|
+
//
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// WHY THIS FILE EXISTS
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// A cached derivation would be a SECOND ON-DISK TRUTH that can disagree with
|
|
15
|
+
// the range table it came from, and the disagreement would be invisible because
|
|
16
|
+
// both answers look authoritative. That is not this module's opinion -- three
|
|
17
|
+
// places in this tree state the rule independently:
|
|
18
|
+
//
|
|
19
|
+
// * `anno-store.ts`'s `putXref` doc block, in as many words: "nothing
|
|
20
|
+
// derivable is ever written here. A cached derivation would be a SECOND
|
|
21
|
+
// ON-DISK TRUTH that can disagree with the range table it came from".
|
|
22
|
+
// * `anno-types.ts`'s `XrefRow` type doc: "Only NON-DERIVABLE references live
|
|
23
|
+
// here".
|
|
24
|
+
// * `COV-01`'s derived-from-bytes byte-coverage census, whose whole
|
|
25
|
+
// discipline is that coverage is computed from bytes rather than recorded
|
|
26
|
+
// beside them.
|
|
27
|
+
//
|
|
28
|
+
// Splitting the derivation into its own module is what makes that rule
|
|
29
|
+
// ENFORCEABLE rather than merely stated: `anno-derive.test.ts`'s structural
|
|
30
|
+
// control reads THIS FILE's comment-stripped source and asserts it carries no
|
|
31
|
+
// SQL write verb, no filesystem write call and no persistence binding. A rule
|
|
32
|
+
// living in a file whose whole subject is derivation is a rule a reader trips
|
|
33
|
+
// over before breaking.
|
|
34
|
+
//
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// THE MATCHING RULE, stated as a decision rather than left to be inferred
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Search matching is a BYTE-EXACT SUBSTRING test over the corpus text, CASE
|
|
39
|
+
// SENSITIVE, applied identically to all three corpora. No Unicode
|
|
40
|
+
// normalisation is applied and none is assumed.
|
|
41
|
+
//
|
|
42
|
+
// Case sensitivity is not a default that fell out of `String.prototype`
|
|
43
|
+
// includes: `anno-store.ts`'s `setLabel` doc block records that the store's own
|
|
44
|
+
// name-versus-name comparison is "EXACT BYTE EQUALITY -- the SQL `=` on a text
|
|
45
|
+
// column with the default (binary) collation [...] No case folding, no Unicode
|
|
46
|
+
// normalisation, no trimming." A search that case-folded would report a hit on
|
|
47
|
+
// a label the store itself considers a DIFFERENT name, so the search and the
|
|
48
|
+
// store would disagree about identity. `anno_search_disassembly`'s
|
|
49
|
+
// case-insensitive default is the shape this deliberately does not copy, and
|
|
50
|
+
// the divergence is named in the tool description rather than left for a caller
|
|
51
|
+
// to discover from a missing hit.
|
|
52
|
+
//
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// WHAT NOT TO DO
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// - Never write anything, anywhere, on any path in this file. No cache table,
|
|
57
|
+
// no index file, no memoised store column, no snapshot. That means: no SQL
|
|
58
|
+
// write statement, no `node:fs` write call, and no naming of the
|
|
59
|
+
// persistence builtin `node:sqlite` -- the store is reached ONLY through
|
|
60
|
+
// `anno-store.ts`'s own read entry points (STORE-07).
|
|
61
|
+
// - Never import `hostpath.ts`, `containerpath.ts` or `container-guard.mts`.
|
|
62
|
+
// This module is proxy-local; a host/container-translated path would point
|
|
63
|
+
// the derivation at bytes on the wrong side of the container boundary
|
|
64
|
+
// (MCP-02, and `hostpath-consumers.test.ts` names this module as forbidden).
|
|
65
|
+
// - Never add a second address parser, a second range validator or a second
|
|
66
|
+
// data-type vocabulary. `parseStoreAddress`, `assertRangeShape` and
|
|
67
|
+
// `assertDataType` are imported from `anno-types.ts` for exactly that
|
|
68
|
+
// reason; a divergent second rule would accept an address the store refuses.
|
|
69
|
+
// - Never re-derive a split table's partner arithmetic. `splitEntryAddressPairs`
|
|
70
|
+
// is the ONE place that knows an entry's partner; a second copy could
|
|
71
|
+
// disagree about what an entry IS, silently, because both copies produce
|
|
72
|
+
// legal answers.
|
|
73
|
+
// - Never give `max_results` a default. An implicit ceiling makes a truncated
|
|
74
|
+
// answer indistinguishable from a complete one; the ceiling is the caller's
|
|
75
|
+
// and the true total comes back beside the truncated list.
|
|
76
|
+
// - Never return a plausible zero for something this surface cannot answer.
|
|
77
|
+
// An unsupported corpus comes back as `{available:false, reason}` in the
|
|
78
|
+
// register style of `stock-cia.ts`'s unavailability table -- what was asked
|
|
79
|
+
// for, why it cannot be answered, and where the nearest answerable thing
|
|
80
|
+
// lives.
|
|
81
|
+
import { listComments, listLabels, listRanges, listXrefs } from "./anno-store.ts";
|
|
82
|
+
import type { AnnoStoreHandle } from "./anno-store.ts";
|
|
83
|
+
import {
|
|
84
|
+
ADDRESS_MAX,
|
|
85
|
+
ADDRESS_MIN,
|
|
86
|
+
AnnoStoreError,
|
|
87
|
+
assertDataType,
|
|
88
|
+
assertRangeShape,
|
|
89
|
+
isSplitDataType,
|
|
90
|
+
parseStoreAddress,
|
|
91
|
+
producesXrefsFor,
|
|
92
|
+
resolveSplitTargets,
|
|
93
|
+
splitEntryAddressPairs,
|
|
94
|
+
} from "./anno-types.ts";
|
|
95
|
+
import type { AnnoStoreErrorOptions, DataType, RangeRow } from "./anno-types.ts";
|
|
96
|
+
import { decode } from "./disasm-decoder.ts";
|
|
97
|
+
import type { Instruction } from "./disasm-decoder.ts";
|
|
98
|
+
import { renderLine } from "./disasm-renderer.ts";
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Refusals. Both are `AnnoStoreError` subclasses and therefore `ViceError`s --
|
|
102
|
+
// never a bare `Error` -- so one `catch` takes the whole family and the caller
|
|
103
|
+
// can name which member fired.
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
export interface AnnoDeriveArgumentErrorOptions extends AnnoStoreErrorOptions {
|
|
107
|
+
argument?: string;
|
|
108
|
+
value?: unknown;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** An argument this module cannot derive from: an absent or non-positive
|
|
112
|
+
* `max_results`, a query that is not a string, or an image larger than the
|
|
113
|
+
* address space it is supposed to describe. The MCP transport validates
|
|
114
|
+
* NOTHING (`vice-proxy.ts:3230`'s `validate: (value) => ({ value })`), so every
|
|
115
|
+
* such argument is re-checked at the boundary that actually runs. */
|
|
116
|
+
export class AnnoDeriveArgumentError extends AnnoStoreError {
|
|
117
|
+
argument?: string;
|
|
118
|
+
value?: unknown;
|
|
119
|
+
|
|
120
|
+
constructor(message: string, { argument, value, ...rest }: AnnoDeriveArgumentErrorOptions = {}) {
|
|
121
|
+
super(message, rest);
|
|
122
|
+
this.name = "AnnoDeriveArgumentError";
|
|
123
|
+
this.argument = argument;
|
|
124
|
+
this.value = value;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface AnnoDerivedTargetErrorOptions extends AnnoStoreErrorOptions {
|
|
129
|
+
fromAddress?: number;
|
|
130
|
+
target?: number;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A decoded edge whose computed target left the 6510's address space.
|
|
134
|
+
*
|
|
135
|
+
* UNREACHABLE TODAY, AND DELIBERATELY KEPT: `disasm-decoder.ts`'s rule 2 masks
|
|
136
|
+
* every address it produces with `& 0xffff`, and its relative branch resolves
|
|
137
|
+
* `(address + 2 + signed8(offset)) & 0xffff`, so no `Instruction` this module
|
|
138
|
+
* can be handed carries an out-of-space target. This is defence in depth on a
|
|
139
|
+
* currently unreachable path -- the same posture `disasm-decoder.ts` records for
|
|
140
|
+
* its own `startAddress` bound -- and it exists so that a future decoder change
|
|
141
|
+
* that stopped wrapping is REPORTED BY NAME rather than silently wrapped or
|
|
142
|
+
* truncated here, where the wrap would look like a legitimate address. */
|
|
143
|
+
export class AnnoDerivedTargetError extends AnnoStoreError {
|
|
144
|
+
fromAddress?: number;
|
|
145
|
+
target?: number;
|
|
146
|
+
|
|
147
|
+
constructor(message: string, { fromAddress, target, ...rest }: AnnoDerivedTargetErrorOptions = {}) {
|
|
148
|
+
super(message, rest);
|
|
149
|
+
this.name = "AnnoDerivedTargetError";
|
|
150
|
+
this.fromAddress = fromAddress;
|
|
151
|
+
this.target = target;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Bounds (T-29-15). Both are caller-facing refusals rather than silent
|
|
157
|
+
// truncation: an answer quietly computed over less than it was asked about is
|
|
158
|
+
// the failure the whole `max_results` convention exists against.
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
/** The largest `image` this module will look at: the 6510's whole address
|
|
162
|
+
* space. A longer buffer cannot be a C64 program image -- every range the store
|
|
163
|
+
* can hold is bounded to `$0000..$ffff` by `assertRangeShape` -- so it is a
|
|
164
|
+
* caller mistake (a whole `.d64`, an unsplit archive) and is refused by name
|
|
165
|
+
* rather than walked. */
|
|
166
|
+
export const ANNO_DERIVE_MAX_IMAGE_BYTES = ADDRESS_MAX - ADDRESS_MIN + 1;
|
|
167
|
+
|
|
168
|
+
/** The default ceiling on how many bytes `searchAnnotations` will decode to
|
|
169
|
+
* build its instruction corpus. Overridable through the environment variable of
|
|
170
|
+
* the same name, READ AT CALL TIME (never frozen at module load) -- the same
|
|
171
|
+
* read-at-call-time convention `anno-tools.ts`'s `ANNO_READ_REGION_MAX_BYTES`
|
|
172
|
+
* override uses, so one `node --test` process can point several different caps
|
|
173
|
+
* at this code within a single run. */
|
|
174
|
+
export const ANNO_SEARCH_MAX_CORPUS_BYTES = ADDRESS_MAX - ADDRESS_MIN + 1;
|
|
175
|
+
|
|
176
|
+
function currentCorpusByteCap(): number {
|
|
177
|
+
const raw = process.env.ANNO_SEARCH_MAX_CORPUS_BYTES;
|
|
178
|
+
if (raw === undefined) return ANNO_SEARCH_MAX_CORPUS_BYTES;
|
|
179
|
+
const n = Number(raw);
|
|
180
|
+
return Number.isFinite(n) && n > 0 ? n : ANNO_SEARCH_MAX_CORPUS_BYTES;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// Shared byte plumbing. Every slice below is bounded by a `RangeRow`'s OWN
|
|
185
|
+
// `start`/`endInclusive` and by the image's own length (T-29-17): a derived
|
|
186
|
+
// answer can never read a byte outside a range a human typed.
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
function assertImage(image: unknown): Uint8Array {
|
|
190
|
+
if (!(image instanceof Uint8Array)) {
|
|
191
|
+
throw new AnnoDeriveArgumentError(
|
|
192
|
+
'the program "image" must be a Uint8Array of the bytes to derive from -- the store holds NO program image (D-07), so every ' +
|
|
193
|
+
"derived answer is computed from bytes the caller names.",
|
|
194
|
+
{ argument: "image", value: typeof image },
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
if (image.length > ANNO_DERIVE_MAX_IMAGE_BYTES) {
|
|
198
|
+
throw new AnnoDeriveArgumentError(
|
|
199
|
+
`the program "image" is ${image.length} bytes, which exceeds ANNO_DERIVE_MAX_IMAGE_BYTES (${ANNO_DERIVE_MAX_IMAGE_BYTES}, the ` +
|
|
200
|
+
"6510's whole address space). Every range this store can hold is bounded to $0000..$ffff, so a longer buffer is not a program " +
|
|
201
|
+
"image -- refused by name rather than walked.",
|
|
202
|
+
{ argument: "image", value: image.length },
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
return image;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function assertOrigin(origin: unknown): number {
|
|
209
|
+
return parseStoreAddress(origin, { what: "origin" });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The bytes of `range` as they sit in `image`, or `null` when the range is not
|
|
214
|
+
* reachable in this image at all.
|
|
215
|
+
*
|
|
216
|
+
* `range.start - origin` is computed and CHECKED before it is used: a negative
|
|
217
|
+
* offset handed to `subarray()` counts from the END of the buffer, which would
|
|
218
|
+
* silently return a plausible-looking slice of the wrong bytes. The tail is
|
|
219
|
+
* clamped to the image's own length so a range that runs past the last byte
|
|
220
|
+
* yields the bytes that exist (which `decode()` reports as `truncated`) rather
|
|
221
|
+
* than a fabricated remainder.
|
|
222
|
+
*/
|
|
223
|
+
function sliceForRange(image: Uint8Array, origin: number, range: RangeRow): Uint8Array | null {
|
|
224
|
+
const begin = range.start - origin;
|
|
225
|
+
if (begin < 0 || begin >= image.length) return null;
|
|
226
|
+
const end = Math.min(range.endInclusive - origin + 1, image.length);
|
|
227
|
+
if (end <= begin) return null;
|
|
228
|
+
return image.subarray(begin, end);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Re-narrows a row's `data_type` text through the ONE vocabulary. `listRanges`
|
|
232
|
+
* casts the column (`row.data_type as DataType`) without validating it, so this
|
|
233
|
+
* is where a store written by something else stops being trusted. */
|
|
234
|
+
function rangeDataType(range: RangeRow): DataType {
|
|
235
|
+
return assertDataType(range.dataType);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Every range this store types `code`, with its bytes and its own shape
|
|
239
|
+
* re-validated. */
|
|
240
|
+
function codeRanges(handle: AnnoStoreHandle): RangeRow[] {
|
|
241
|
+
return listRanges(handle).filter((range) => rangeDataType(range) === "code");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Decodes one `code` range fresh. There is deliberately no memoisation here,
|
|
245
|
+
* not even within a single call: see this file's header. */
|
|
246
|
+
function decodeRange(image: Uint8Array, origin: number, range: RangeRow): Instruction[] {
|
|
247
|
+
const bytes = sliceForRange(image, origin, range);
|
|
248
|
+
if (bytes === null) return [];
|
|
249
|
+
assertRangeShape(range.start, range.endInclusive, rangeDataType(range));
|
|
250
|
+
return decode(bytes, range.start, { end: range.endInclusive });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* The address an instruction REFERENCES, or `undefined` when it references
|
|
255
|
+
* none.
|
|
256
|
+
*
|
|
257
|
+
* * No operand at all (`rts`, `nop`) -- nothing to reference.
|
|
258
|
+
* * `immediate` -- the operand IS the value. `lda #$c0` encodes the byte
|
|
259
|
+
* `$c0`; reporting it as a reference to `$00c0` would invent an edge.
|
|
260
|
+
* * `indirect` -- `jmp ($0314)` transfers to whatever the two bytes AT
|
|
261
|
+
* `$0314` hold. That target is not derivable from this instruction, and
|
|
262
|
+
* attributing an edge to the vector's own address would name the wrong one.
|
|
263
|
+
* * Everything else (`absolute`, `zeropage`, `relative`) references an
|
|
264
|
+
* address: `resolvedTarget` when the decoder resolved one (a branch, a
|
|
265
|
+
* `jmp`/`jsr` absolute), `operand.value` otherwise.
|
|
266
|
+
*/
|
|
267
|
+
function referencedAddress(instruction: Instruction): number | undefined {
|
|
268
|
+
const operand = instruction.operand;
|
|
269
|
+
if (operand === undefined) return undefined;
|
|
270
|
+
if (operand.role === "immediate" || operand.role === "indirect") return undefined;
|
|
271
|
+
const target = instruction.resolvedTarget ?? operand.value;
|
|
272
|
+
if (!Number.isInteger(target) || target < ADDRESS_MIN || target > ADDRESS_MAX) {
|
|
273
|
+
throw new AnnoDerivedTargetError(
|
|
274
|
+
`the instruction at $${instruction.address.toString(16).padStart(4, "0")} computed the target ${String(target)}, which is outside ` +
|
|
275
|
+
`${ADDRESS_MIN}..${ADDRESS_MAX} ($0000-$ffff). Reported by name rather than wrapped or truncated: a wrapped target is a ` +
|
|
276
|
+
"plausible-looking address pointing at the wrong place, and nothing downstream could tell.",
|
|
277
|
+
{ fromAddress: instruction.address, target: typeof target === "number" ? target : undefined },
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
return target;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
// Derived cross-references (STORE-06)
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
/** What `crossReferencesTo()` returns: the target, every address that reaches
|
|
288
|
+
* it (ascending, de-duplicated), and the count restated as a field so a caller
|
|
289
|
+
* can assert it without walking the array -- the same shape convention
|
|
290
|
+
* `SplitTargets` uses. */
|
|
291
|
+
export interface CrossReferencesResult {
|
|
292
|
+
to: number;
|
|
293
|
+
callers: readonly number[];
|
|
294
|
+
count: number;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Every address that references `to`, unioned from THREE sources and returned
|
|
299
|
+
* as one ascending, de-duplicated list.
|
|
300
|
+
*
|
|
301
|
+
* 1. THE DECODED CODE. Every range `listRanges()` types `code`, sliced out of
|
|
302
|
+
* `image` and decoded FRESH, keeping the instructions whose operand role
|
|
303
|
+
* references an address (see `referencedAddress`).
|
|
304
|
+
* 2. THE TYPED SPLIT TABLES. Every range whose type is a split layout AND for
|
|
305
|
+
* which `producesXrefsFor()` is true -- the `_address` forms produce
|
|
306
|
+
* cross-references and the `_word` forms do not, which is the schema's own
|
|
307
|
+
* distinction and not a judgement made here. Each resolved target is
|
|
308
|
+
* attributed to its entry's own first-half address, taken from
|
|
309
|
+
* `splitEntryAddressPairs()` rather than recomputed.
|
|
310
|
+
* 3. THE STORED ROWS. `listXrefs()` -- the ONLY half that lives on disk, and
|
|
311
|
+
* only because those references (a computed dispatch, a hand-asserted
|
|
312
|
+
* edge) cannot be recovered from the bytes at all.
|
|
313
|
+
*
|
|
314
|
+
* The union is a `Set`, so an address reached by two sources appears once; the
|
|
315
|
+
* sort makes the answer stable regardless of the order the three sources are
|
|
316
|
+
* walked. NOTHING IS WRITTEN.
|
|
317
|
+
*/
|
|
318
|
+
export function crossReferencesTo(
|
|
319
|
+
handle: AnnoStoreHandle,
|
|
320
|
+
image: Uint8Array,
|
|
321
|
+
origin: number | string,
|
|
322
|
+
to: number | string,
|
|
323
|
+
): CrossReferencesResult {
|
|
324
|
+
const bytes = assertImage(image);
|
|
325
|
+
const base = assertOrigin(origin);
|
|
326
|
+
const target = parseStoreAddress(to, { what: "to" });
|
|
327
|
+
|
|
328
|
+
const callers = new Set<number>();
|
|
329
|
+
|
|
330
|
+
// 1. Derived from the bytes: every code range, decoded fresh.
|
|
331
|
+
for (const range of codeRanges(handle)) {
|
|
332
|
+
for (const instruction of decodeRange(bytes, base, range)) {
|
|
333
|
+
if (referencedAddress(instruction) === target) callers.add(instruction.address);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// 2. Derived from typed split tables (also bytes, also never stored).
|
|
338
|
+
for (const range of listRanges(handle)) {
|
|
339
|
+
const dataType = rangeDataType(range);
|
|
340
|
+
if (!isSplitDataType(dataType) || !producesXrefsFor(dataType)) continue;
|
|
341
|
+
const tableBytes = sliceForRange(bytes, base, range);
|
|
342
|
+
// A PARTIAL split table is skipped rather than resolved. `anno-types.ts`'s
|
|
343
|
+
// `SplitTableReinterpretation` records why: an entry's partner is a
|
|
344
|
+
// function of the row's start AND its length, so a fragment re-pairs every
|
|
345
|
+
// entry and decodes to DIFFERENT 16-bit values than the ones a human
|
|
346
|
+
// recorded. Resolving a fragment would produce legal, plausible, wrong
|
|
347
|
+
// targets.
|
|
348
|
+
if (tableBytes === null || tableBytes.length !== range.endInclusive - range.start + 1) continue;
|
|
349
|
+
const { targets } = resolveSplitTargets(tableBytes, dataType);
|
|
350
|
+
const { pairs } = splitEntryAddressPairs(range.start, range.endInclusive, dataType);
|
|
351
|
+
targets.forEach((resolved, i) => {
|
|
352
|
+
if (resolved === target) callers.add(pairs[i]![0]);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// 3. The ONLY stored half: references that cannot be recovered from bytes.
|
|
357
|
+
for (const row of listXrefs(handle)) {
|
|
358
|
+
if (row.toAddress === target) callers.add(row.fromAddress);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const sorted = [...callers].sort((a, b) => a - b);
|
|
362
|
+
return { to: target, callers: sorted, count: sorted.length };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
// Search (STORE-06)
|
|
367
|
+
// ---------------------------------------------------------------------------
|
|
368
|
+
|
|
369
|
+
/** The three corpora this surface has. Frozen and derived from, never
|
|
370
|
+
* re-typed: `CorpusName` is `Extract`ed from it below. */
|
|
371
|
+
export const SEARCH_CORPORA = Object.freeze(["labels", "comments", "instructions"] as const);
|
|
372
|
+
|
|
373
|
+
/** One member of the frozen three. */
|
|
374
|
+
export type CorpusName = (typeof SEARCH_CORPORA)[number];
|
|
375
|
+
|
|
376
|
+
/** One search hit. `corpus` is carried on EVERY hit so a caller can tell a
|
|
377
|
+
* label match from an instruction match without re-deriving which corpus could
|
|
378
|
+
* have produced the text. */
|
|
379
|
+
export interface SearchHit {
|
|
380
|
+
corpus: CorpusName;
|
|
381
|
+
address: number;
|
|
382
|
+
text: string;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** What a corpus this surface does not have reports. Structurally identical to
|
|
386
|
+
* `stock-cia.ts`'s unavailability entries, and for the same reason: an
|
|
387
|
+
* unanswerable question must never come back as a plausible zero. */
|
|
388
|
+
export interface UnavailableCorpus {
|
|
389
|
+
available: false;
|
|
390
|
+
reason: string;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export interface SearchRequest {
|
|
394
|
+
query: string;
|
|
395
|
+
max_results: number;
|
|
396
|
+
search_labels?: boolean;
|
|
397
|
+
search_comments?: boolean;
|
|
398
|
+
search_instructions?: boolean;
|
|
399
|
+
[key: string]: unknown;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export interface SearchResult {
|
|
403
|
+
query: string;
|
|
404
|
+
caseSensitive: true;
|
|
405
|
+
corpora: Record<CorpusName, { searched: boolean; entries: number }>;
|
|
406
|
+
unavailable: Record<string, UnavailableCorpus>;
|
|
407
|
+
results: SearchHit[];
|
|
408
|
+
returned: number;
|
|
409
|
+
total: number;
|
|
410
|
+
truncated: boolean;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** Where each corpus's own data actually lives, named in the unavailability
|
|
414
|
+
* reason so a refusal points at the answerable thing instead of stopping at
|
|
415
|
+
* "no". */
|
|
416
|
+
const NEAREST_ANSWERABLE =
|
|
417
|
+
"labels (anno-store.ts's listLabels), comments (listComments) and the instruction text rendered from the ranges typed `code`";
|
|
418
|
+
|
|
419
|
+
function unavailableCorpus(name: string): UnavailableCorpus {
|
|
420
|
+
return {
|
|
421
|
+
available: false,
|
|
422
|
+
reason:
|
|
423
|
+
`search_${name} names the "${name}" corpus, which this surface does not search. The three corpora it does search are ` +
|
|
424
|
+
`${NEAREST_ANSWERABLE}. Every other annotation kind is read directly through its own anno-store.ts list entry point rather ` +
|
|
425
|
+
"than through this search, because a corpus with no rendered text has nothing for a substring rule to match.",
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function assertQuery(query: unknown): string {
|
|
430
|
+
if (typeof query !== "string" || query === "") {
|
|
431
|
+
throw new AnnoDeriveArgumentError(
|
|
432
|
+
`"query" must be a non-empty string, got ${JSON.stringify(query)} -- an empty query would match every entry of every corpus, ` +
|
|
433
|
+
"which is a listing rather than a search and is what the list entry points are for.",
|
|
434
|
+
{ argument: "query", value: query },
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
return query;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** `max_results` is REQUIRED with no default, and `0` is refused by name. An
|
|
441
|
+
* implicit ceiling makes a truncated answer indistinguishable from a complete
|
|
442
|
+
* one; a ceiling of zero asks for an answer that cannot carry information. */
|
|
443
|
+
function assertMaxResults(request: SearchRequest | undefined): number {
|
|
444
|
+
const raw = request?.max_results;
|
|
445
|
+
if (typeof raw !== "number" || !Number.isInteger(raw) || raw <= 0) {
|
|
446
|
+
throw new AnnoDeriveArgumentError(
|
|
447
|
+
`"max_results" must be a positive integer, got ${JSON.stringify(raw)} -- it is REQUIRED and has no default on this surface. ` +
|
|
448
|
+
"Pass an explicit ceiling and compare the returned count against it to detect truncation; the true total comes back beside " +
|
|
449
|
+
"the truncated list.",
|
|
450
|
+
{ argument: "max_results", value: raw },
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
return raw;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** True when the request did not explicitly disable this corpus. Enabled is the
|
|
457
|
+
* default for all three, matching `anno_search_disassembly`'s advertised
|
|
458
|
+
* shape. */
|
|
459
|
+
function corpusEnabled(request: SearchRequest, corpus: CorpusName): boolean {
|
|
460
|
+
const flag = request[`search_${corpus}`];
|
|
461
|
+
return flag !== false;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** Every `search_*` key naming something outside the frozen three. */
|
|
465
|
+
function unsupportedCorpora(request: SearchRequest): string[] {
|
|
466
|
+
const known = new Set<string>(SEARCH_CORPORA);
|
|
467
|
+
const found: string[] = [];
|
|
468
|
+
for (const key of Object.keys(request)) {
|
|
469
|
+
const match = /^search_(.+)$/.exec(key);
|
|
470
|
+
if (match === null) continue;
|
|
471
|
+
const name = match[1]!;
|
|
472
|
+
if (!known.has(name)) found.push(name);
|
|
473
|
+
}
|
|
474
|
+
return found.sort();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** The instruction corpus: one rendered line per decoded instruction, carrying
|
|
478
|
+
* the address it was decoded at.
|
|
479
|
+
*
|
|
480
|
+
* `renderLine()` rather than `render()` over the whole range: a hit has to
|
|
481
|
+
* carry the address it was found at, and `render()`'s output is one blob with a
|
|
482
|
+
* `!cpu 6510` header and a symbol block. Both go through the same
|
|
483
|
+
* `renderInstructionLine()` inside `disasm-renderer.ts`, so the text a search
|
|
484
|
+
* matches is byte-identical to the text a listing shows.
|
|
485
|
+
*
|
|
486
|
+
* BOUNDED (T-29-15): the total number of bytes decoded for the corpus is capped
|
|
487
|
+
* and exceeding the cap is REFUSED BY NAME. A silently truncated corpus would
|
|
488
|
+
* report a genuine-looking zero for a term that is really there. */
|
|
489
|
+
function instructionCorpus(handle: AnnoStoreHandle, image: Uint8Array, origin: number): SearchHit[] {
|
|
490
|
+
const cap = currentCorpusByteCap();
|
|
491
|
+
const ranges = codeRanges(handle);
|
|
492
|
+
let budget = 0;
|
|
493
|
+
for (const range of ranges) {
|
|
494
|
+
const bytes = sliceForRange(image, origin, range);
|
|
495
|
+
if (bytes !== null) budget += bytes.length;
|
|
496
|
+
}
|
|
497
|
+
if (budget > cap) {
|
|
498
|
+
throw new AnnoDeriveArgumentError(
|
|
499
|
+
`the ranges typed \`code\` cover ${budget} byte(s) of this image, which exceeds the ANNO_SEARCH_MAX_CORPUS_BYTES cap of ${cap}. ` +
|
|
500
|
+
"Refused rather than silently truncated: a corpus cut short reports a genuine-looking zero for a term that is really there. " +
|
|
501
|
+
"Narrow the typed ranges, or raise ANNO_SEARCH_MAX_CORPUS_BYTES.",
|
|
502
|
+
{ argument: "image", value: budget },
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const hits: SearchHit[] = [];
|
|
507
|
+
for (const range of ranges) {
|
|
508
|
+
for (const instruction of decodeRange(image, origin, range)) {
|
|
509
|
+
hits.push({ corpus: "instructions", address: instruction.address, text: renderLine(instruction) });
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return hits;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Searches the three corpora for `request.query`.
|
|
517
|
+
*
|
|
518
|
+
* MATCHING IS BYTE-EXACT AND CASE-SENSITIVE over the corpus text, applied
|
|
519
|
+
* identically to all three -- see the module header for why case folding would
|
|
520
|
+
* put this search and the store's own name comparison into disagreement. The
|
|
521
|
+
* rule is restated in the result body as `caseSensitive`, so a caller reading
|
|
522
|
+
* an empty answer is told which rule produced it.
|
|
523
|
+
*
|
|
524
|
+
* EVERY CORPUS IS NAMED IN THE RESULT with the number of entries it held, so an
|
|
525
|
+
* EMPTY RESULT over a real, non-empty corpus (a genuine zero) is distinguishable
|
|
526
|
+
* from a corpus this surface does not have (`unavailable`, carrying a reason).
|
|
527
|
+
* The two are separate fields; neither is inferable from the other.
|
|
528
|
+
*/
|
|
529
|
+
export function searchAnnotations(
|
|
530
|
+
handle: AnnoStoreHandle,
|
|
531
|
+
image: Uint8Array,
|
|
532
|
+
origin: number | string,
|
|
533
|
+
request: SearchRequest,
|
|
534
|
+
): SearchResult {
|
|
535
|
+
const bytes = assertImage(image);
|
|
536
|
+
const base = assertOrigin(origin);
|
|
537
|
+
const bag: SearchRequest = (typeof request === "object" && request !== null ? request : {}) as SearchRequest;
|
|
538
|
+
const query = assertQuery(bag.query);
|
|
539
|
+
const maxResults = assertMaxResults(bag);
|
|
540
|
+
|
|
541
|
+
const unavailable: Record<string, UnavailableCorpus> = {};
|
|
542
|
+
for (const name of unsupportedCorpora(bag)) unavailable[name] = unavailableCorpus(name);
|
|
543
|
+
|
|
544
|
+
const labelHits: SearchHit[] = listLabels(handle).map((row) => ({
|
|
545
|
+
corpus: "labels" as const,
|
|
546
|
+
address: row.address,
|
|
547
|
+
text: row.name,
|
|
548
|
+
}));
|
|
549
|
+
const commentHits: SearchHit[] = listComments(handle).map((row) => ({
|
|
550
|
+
corpus: "comments" as const,
|
|
551
|
+
address: row.address,
|
|
552
|
+
text: row.text,
|
|
553
|
+
}));
|
|
554
|
+
|
|
555
|
+
const enabled: Record<CorpusName, boolean> = {
|
|
556
|
+
labels: corpusEnabled(bag, "labels"),
|
|
557
|
+
comments: corpusEnabled(bag, "comments"),
|
|
558
|
+
instructions: corpusEnabled(bag, "instructions"),
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
// The instruction corpus is only BUILT when it is going to be searched:
|
|
562
|
+
// decoding every code range to answer a request that disabled the corpus
|
|
563
|
+
// would pay the whole cost for nothing, and would raise the byte-cap refusal
|
|
564
|
+
// for a corpus the caller did not ask about.
|
|
565
|
+
const instructionHits: SearchHit[] = enabled.instructions ? instructionCorpus(handle, bytes, base) : [];
|
|
566
|
+
|
|
567
|
+
const corpora: Record<CorpusName, { searched: boolean; entries: number }> = {
|
|
568
|
+
labels: { searched: enabled.labels, entries: labelHits.length },
|
|
569
|
+
comments: { searched: enabled.comments, entries: commentHits.length },
|
|
570
|
+
instructions: { searched: enabled.instructions, entries: instructionHits.length },
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
const matched: SearchHit[] = [];
|
|
574
|
+
for (const hit of [...labelHits, ...commentHits, ...instructionHits]) {
|
|
575
|
+
if (!enabled[hit.corpus]) continue;
|
|
576
|
+
if (hit.text.includes(query)) matched.push(hit);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const results = matched.slice(0, maxResults);
|
|
580
|
+
return {
|
|
581
|
+
query,
|
|
582
|
+
caseSensitive: true,
|
|
583
|
+
corpora,
|
|
584
|
+
unavailable,
|
|
585
|
+
results,
|
|
586
|
+
returned: results.length,
|
|
587
|
+
total: matched.length,
|
|
588
|
+
truncated: matched.length > results.length,
|
|
589
|
+
};
|
|
590
|
+
}
|