@henols/vice-mcp 0.2.2 → 0.2.3
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 -2
- package/THIRD-PARTY-NOTICES.md +422 -1
- package/anno-bank.ts +171 -0
- package/anno-cli.ts +1674 -99
- package/anno-enum-gen.ts +416 -30
- package/anno-export-asm.ts +1175 -89
- package/anno-graphics.ts +338 -0
- package/anno-hazard-report.ts +1367 -0
- package/anno-import.ts +495 -0
- package/anno-join.ts +480 -0
- package/anno-provenance-ledger.ts +472 -0
- package/anno-register.ts +159 -0
- package/anno-store-export.ts +661 -0
- package/anno-store.ts +518 -2
- package/anno-tools.ts +1169 -16
- package/anno-types.ts +275 -2
- package/backend-detect.mts +124 -312
- package/build.ts +3 -1
- package/capture-predicate.ts +597 -0
- package/channel-lock.ts +349 -0
- package/evid-ingest.ts +217 -0
- package/evid-reconcile.ts +316 -0
- package/host-tool-client.ts +430 -0
- package/incident-record.ts +23 -12
- package/install-resources.ts +29 -13
- package/memmap-lookup.ts +285 -0
- package/package.json +27 -8
- package/prg-image.ts +1 -2
- package/repo-root.ts +87 -3
- package/resources/backend-detect.mjs +98 -236
- package/resources/broker-control.mjs +189 -16
- package/resources/broker-epoch.mjs +1 -1
- package/resources/broker-kill.mjs +8 -2
- package/resources/broker-launch.mjs +365 -210
- package/resources/broker-state.mjs +64 -18
- package/resources/container-guard.mjs +1 -1
- package/resources/ghidra-project.mjs +790 -0
- package/resources/host-tool.mjs +2561 -0
- package/resources/vice-broker.mjs +330 -184
- package/resources/vice-launcher.sh +127 -9
- package/stock-address.ts +1 -1
- package/stock-condition.ts +1 -1
- package/stock-connect.ts +9 -5
- package/stock-derived.ts +29 -37
- package/stock-diagnose.ts +200 -36
- package/stock-dispatch.ts +179 -77
- package/stock-handler.ts +1 -1
- package/stock-paths.ts +18 -14
- package/stock-petscii.ts +1 -1
- package/stock-protocol.ts +1 -1
- package/stock-recycle.ts +83 -2
- package/stock-reproducible-run.ts +811 -0
- package/stock-run-until.ts +100 -1
- package/stock-symbols.ts +4 -4
- package/stock-timing.ts +1 -1
- package/stop-oracle.ts +167 -0
- package/text-capability-probe.ts +660 -0
- package/text-connect.ts +157 -0
- package/text-protocol.ts +810 -0
- package/text-tools.ts +778 -0
- package/textmon-backtrace.ts +385 -0
- package/textmon-cpuhistory.ts +335 -0
- package/textmon-memmap.ts +494 -0
- package/textmon-profile.ts +458 -0
- package/textmon-registers.ts +748 -0
- package/tools-manifest.stock.json +864 -3
- package/vice-broker-client.ts +189 -42
- package/vice-errors.ts +268 -0
- package/vice-proxy.ts +339 -2144
- package/vsf-slice.ts +640 -0
- package/anno-d64.ts +0 -310
- package/capability-registry.ts +0 -390
- package/refresh-manifest.ts +0 -124
- package/tools-manifest.json +0 -1223
- package/vice-probe.ts +0 -278
- package/vice-sync.ts +0 -336
- package/vice.ts +0 -772
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
// textmon-memmap.ts
|
|
2
|
+
//
|
|
3
|
+
// THE ONE owning module for VICE's `memmapshow` text-monitor output
|
|
4
|
+
// (PARSE-01, PARSE-03). Nothing else in this tree reads or interprets a
|
|
5
|
+
// `memmapshow` reply -- a handler dials the wire, this module is the only
|
|
6
|
+
// place that turns the framed text into structured data.
|
|
7
|
+
//
|
|
8
|
+
// WHY THIS FILE EXISTS RATHER THAN LIVING INSIDE THE TOOL HANDLER: the exact
|
|
9
|
+
// reasoning `disasm-decoder.ts` states for itself applies here unchanged --
|
|
10
|
+
// keeping the parser import-free of transport code means any future
|
|
11
|
+
// non-tool consumer (an offline diff over two captured maps, say) can depend
|
|
12
|
+
// on this one file without dragging in a socket. It is also PARSE-03's own
|
|
13
|
+
// requirement: an owning module that is pure by construction cannot be the
|
|
14
|
+
// thing that silently launders a wire error into a parse result, because it
|
|
15
|
+
// never touches the wire at all.
|
|
16
|
+
//
|
|
17
|
+
// WHAT NOT TO DO:
|
|
18
|
+
// - Never import anything -- not `node:` anything, not `text-protocol.ts`,
|
|
19
|
+
// not `textmon-fixtures.ts`, not even a type-only import of a sibling
|
|
20
|
+
// module. Purity is asserted mechanically by this file's own test
|
|
21
|
+
// (reads this module's source, greps for a top-level `import`) -- it is
|
|
22
|
+
// not a style preference to be relaxed later.
|
|
23
|
+
// - Never throw on a malformed or drifted input (D-42-3, decided once for
|
|
24
|
+
// all five text parsers this phase adds). Return the discriminated
|
|
25
|
+
// `AccessMapParseResult` instead. `withTextTool()`'s own `try/catch`
|
|
26
|
+
// converts anything THROWN into a transport-error message -- a thrown
|
|
27
|
+
// refusal would be reported to the caller as a WIRE failure, laundering
|
|
28
|
+
// a format-drift refusal into exactly the "absorbed into a
|
|
29
|
+
// plausible-looking wrong answer" failure mode PARSE-03 exists to
|
|
30
|
+
// prevent.
|
|
31
|
+
// - Never fold `execute` into `read`, default it from `read`, or treat the
|
|
32
|
+
// absence of a read glyph as evidence about execute. VICE emits
|
|
33
|
+
// `MEMMAP_*_X` as a bit distinct from `MEMMAP_*_R`/`_W` and prints each
|
|
34
|
+
// as its own glyph -- an entry with `--x` (execute recorded, no read) is
|
|
35
|
+
// real, measured output, not an edge case to normalize away.
|
|
36
|
+
// - Never treat an empty or whitespace-only payload as a zero-entry
|
|
37
|
+
// access map. Those are two different facts: "nothing was captured"
|
|
38
|
+
// and "the capture recorded no access anywhere" are not the same
|
|
39
|
+
// sentence, and collapsing them is exactly the "absence read as a
|
|
40
|
+
// claim" failure T-42-05 exists to close.
|
|
41
|
+
// - Never add a field, key, label or enum member anywhere in this module
|
|
42
|
+
// or its answer types that classifies an address as DATA on the
|
|
43
|
+
// strength of never having been observed. An address absent from
|
|
44
|
+
// `AccessMap.entries` is reported only as a count against
|
|
45
|
+
// `addressesQueried` in `accessMapRanges()`'s result -- never rendered,
|
|
46
|
+
// named, or implied as anything else.
|
|
47
|
+
// - Never add a second buffer or a second length cap here. The transport
|
|
48
|
+
// (`text-protocol.ts`'s `TEXT_MAX_BUFFERED_LEN`) is the one place that
|
|
49
|
+
// bound lives; this module receives an already-framed string and its
|
|
50
|
+
// own `maxRanges` budget bounds only what is EMITTED to a caller, never
|
|
51
|
+
// what is read.
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* One access-permission triple, exactly as `memmapshow` prints one column
|
|
55
|
+
* (IO, ROM or RAM) of a data line. `execute` is a field of its own, never
|
|
56
|
+
* derived from `read` -- see the module header's "WHAT NOT TO DO".
|
|
57
|
+
*/
|
|
58
|
+
export interface AccessFlags {
|
|
59
|
+
readonly read: boolean;
|
|
60
|
+
readonly write: boolean;
|
|
61
|
+
readonly execute: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The closed set of parenthesised trailers VICE's `mon_memmap.c` can print
|
|
66
|
+
* after a data line's three glyph groups. `dummy` is the only one either
|
|
67
|
+
* committed real capture actually contains; `uninitialized-read` and
|
|
68
|
+
* `uninitialized-exec` are the fork build's remaining two arms (see this
|
|
69
|
+
* module's test file for where each is exercised from).
|
|
70
|
+
*/
|
|
71
|
+
export type AccessAnnotation = "dummy" | "uninitialized-read" | "uninitialized-exec";
|
|
72
|
+
|
|
73
|
+
/** One decoded `memmapshow` data line. */
|
|
74
|
+
export interface AccessMapEntry {
|
|
75
|
+
readonly address: number;
|
|
76
|
+
readonly io: AccessFlags;
|
|
77
|
+
readonly rom: AccessFlags;
|
|
78
|
+
readonly ram: AccessFlags;
|
|
79
|
+
readonly annotations: readonly AccessAnnotation[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The full decoded access map. `entries` is SPARSE by design -- VICE skips
|
|
84
|
+
* an address with no recorded access with a bare `continue` in its own
|
|
85
|
+
* print loop, so a shorter `entries` array is evidence, not an error, and
|
|
86
|
+
* this module never pads it back out to 65536 rows.
|
|
87
|
+
*/
|
|
88
|
+
export interface AccessMap {
|
|
89
|
+
readonly entries: readonly AccessMapEntry[];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The closed refusal-code union (D-42-3). `malformed-line` covers a data
|
|
94
|
+
* line whose overall layout does not match (a non-hex-digit or
|
|
95
|
+
* wrong-length address field, a missing separator, a short or long glyph
|
|
96
|
+
* group) -- a STRUCTURAL mismatch, distinct from `unrecognised-glyph`,
|
|
97
|
+
* which covers a line that IS laid out correctly but carries a character
|
|
98
|
+
* this parser has never seen at a glyph position.
|
|
99
|
+
*/
|
|
100
|
+
export type AccessMapRefusalCode =
|
|
101
|
+
| "empty-response"
|
|
102
|
+
| "missing-header"
|
|
103
|
+
| "no-data-lines"
|
|
104
|
+
| "malformed-line"
|
|
105
|
+
| "unrecognised-glyph"
|
|
106
|
+
| "unrecognised-annotation";
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* A parser refusal (D-42-3): returned, never thrown. `line` and
|
|
110
|
+
* `lineNumber` name the offending content whenever one exists -- for the
|
|
111
|
+
* two whole-payload refusals (`empty-response`, and `no-data-lines`'s
|
|
112
|
+
* absence of any content past the header) `lineNumber` points at the last
|
|
113
|
+
* real content seen (0 when there was none at all).
|
|
114
|
+
*/
|
|
115
|
+
export interface TextParseRefusal {
|
|
116
|
+
readonly code: AccessMapRefusalCode;
|
|
117
|
+
readonly message: string;
|
|
118
|
+
readonly line: string;
|
|
119
|
+
readonly lineNumber: number;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** D-42-3's discriminated shape: a parser never throws, it returns one of
|
|
123
|
+
* these two arms. */
|
|
124
|
+
export type AccessMapParseResult = { ok: true; value: AccessMap } | { ok: false; refusal: TextParseRefusal };
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// Framing constants -- this module's OWN copies, never imported from
|
|
128
|
+
// text-protocol.ts (purity rule above). `text-protocol.ts`'s `PROMPT_RE` is
|
|
129
|
+
// tail-anchored only and is applied by the transport to strip the TRAILING
|
|
130
|
+
// prompt before this module ever sees the string; the LEADING entry-echo
|
|
131
|
+
// prompt `memmapshow` alone emits is untouched by the transport and must be
|
|
132
|
+
// tolerated here.
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
const LEADING_PROMPT_RE = /^\(C:\$[0-9A-Fa-f]{4}\)\s*/;
|
|
136
|
+
const TRAILING_PROMPT_RE = /\(C:\$[0-9A-Fa-f]{4}\)\s*$/;
|
|
137
|
+
|
|
138
|
+
/** VICE's own header line, byte-for-byte -- two spaces after `IO`, one
|
|
139
|
+
* after `ROM`, no trailing space (`mon_memmap.c`'s own `mon_out()` call,
|
|
140
|
+
* confirmed against both committed real captures). */
|
|
141
|
+
const HEADER_LINE = "addr: IO ROM RAM";
|
|
142
|
+
|
|
143
|
+
/** A data line's overall layout: a 4-hex-digit address, `: `, then three
|
|
144
|
+
* space-separated 3-character glyph groups (IO, ROM, RAM, in that fixed
|
|
145
|
+
* order), then whatever trailer text follows (validated separately by
|
|
146
|
+
* `parseTrailer()`). A line that does not match this AT ALL is a
|
|
147
|
+
* STRUCTURAL malformation (`malformed-line`) -- the per-character glyph
|
|
148
|
+
* and per-suffix trailer checks below only run once this much has already
|
|
149
|
+
* matched. */
|
|
150
|
+
const DATA_LINE_RE = /^([0-9a-fA-F]{4}): (.{3}) (.{3}) (.{3})(.*)$/;
|
|
151
|
+
|
|
152
|
+
/** The closed, ordered set of recognised trailer suffixes. Order matters:
|
|
153
|
+
* `parseTrailer()` strips them in VICE's own emission order, per the plan's
|
|
154
|
+
* own framing note -- never a set membership test that would accept the
|
|
155
|
+
* three suffixes in any order. */
|
|
156
|
+
const ANNOTATION_SUFFIXES: ReadonlyArray<{ suffix: string; annotation: AccessAnnotation }> = [
|
|
157
|
+
{ suffix: " (dummy)", annotation: "dummy" },
|
|
158
|
+
{ suffix: " (uninitialized read)", annotation: "uninitialized-read" },
|
|
159
|
+
{ suffix: " (uninitialized exec)", annotation: "uninitialized-exec" },
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
function makeRefusal(code: AccessMapRefusalCode, message: string, line: string, lineNumber: number): TextParseRefusal {
|
|
163
|
+
return { code, message, line, lineNumber };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Decodes one 3-character glyph group. Recognised characters only:
|
|
167
|
+
* position 0 is `r` or `-`, position 1 is `w` or `-`, position 2 is `x` or
|
|
168
|
+
* `-` -- exact match, case-sensitive (VICE has never emitted an uppercase
|
|
169
|
+
* glyph; recognition is exact, not case-insensitive). The group's WIDTH is
|
|
170
|
+
* already guaranteed to be exactly 3 by `DATA_LINE_RE`'s own capture group,
|
|
171
|
+
* so only content is checked here. */
|
|
172
|
+
function parseGlyphGroup(group: string): { ok: true; flags: AccessFlags } | { ok: false; badChar: string } {
|
|
173
|
+
const r = group[0]!;
|
|
174
|
+
const w = group[1]!;
|
|
175
|
+
const x = group[2]!;
|
|
176
|
+
if (r !== "r" && r !== "-") return { ok: false, badChar: r };
|
|
177
|
+
if (w !== "w" && w !== "-") return { ok: false, badChar: w };
|
|
178
|
+
if (x !== "x" && x !== "-") return { ok: false, badChar: x };
|
|
179
|
+
return { ok: true, flags: { read: r === "r", write: w === "w", execute: x === "x" } };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Decodes the trailer following a data line's three glyph groups: zero or
|
|
183
|
+
* more of the three recognised suffixes, in VICE's own emission order
|
|
184
|
+
* (`ANNOTATION_SUFFIXES`'s own order). Any leftover text that is not a
|
|
185
|
+
* clean, ordered concatenation of recognised suffixes refuses. Bounded by
|
|
186
|
+
* construction -- at most three iterations over a fixed array, no
|
|
187
|
+
* recursion. */
|
|
188
|
+
function parseTrailer(trailer: string): { ok: true; annotations: AccessAnnotation[] } | { ok: false } {
|
|
189
|
+
let rest = trailer;
|
|
190
|
+
const annotations: AccessAnnotation[] = [];
|
|
191
|
+
for (const { suffix, annotation } of ANNOTATION_SUFFIXES) {
|
|
192
|
+
if (rest.startsWith(suffix)) {
|
|
193
|
+
annotations.push(annotation);
|
|
194
|
+
rest = rest.slice(suffix.length);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (rest !== "") return { ok: false };
|
|
198
|
+
return { ok: true, annotations };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Parses `memmapshow`'s framed text-monitor reply into a structured
|
|
203
|
+
* {@link AccessMap}. Input is the string `TextMonitorClient.command()`
|
|
204
|
+
* resolves to, or a fixture's `text` -- both must parse identically (see
|
|
205
|
+
* the module header). Never throws (D-42-3): every failure mode returns
|
|
206
|
+
* `{ ok: false, refusal }` naming exactly what was wrong and where.
|
|
207
|
+
*
|
|
208
|
+
* Bounded by construction: one pass over the payload's lines, each
|
|
209
|
+
* iteration consumes exactly one line, no recursion anywhere in this
|
|
210
|
+
* function.
|
|
211
|
+
*/
|
|
212
|
+
export function parseAccessMap(text: string): AccessMapParseResult {
|
|
213
|
+
if (typeof text !== "string" || text.trim() === "") {
|
|
214
|
+
return {
|
|
215
|
+
ok: false,
|
|
216
|
+
refusal: makeRefusal(
|
|
217
|
+
"empty-response",
|
|
218
|
+
"memmapshow: the response was empty or whitespace-only -- never decoded as a zero-entry access map, " +
|
|
219
|
+
"because an empty response and a real capture that recorded no access anywhere are two different facts",
|
|
220
|
+
"",
|
|
221
|
+
0,
|
|
222
|
+
),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let body = text;
|
|
227
|
+
const leadingMatch = LEADING_PROMPT_RE.exec(body);
|
|
228
|
+
if (leadingMatch) {
|
|
229
|
+
body = body.slice(leadingMatch[0]!.length);
|
|
230
|
+
}
|
|
231
|
+
body = body.replace(TRAILING_PROMPT_RE, "");
|
|
232
|
+
|
|
233
|
+
if (body.trim() === "") {
|
|
234
|
+
return {
|
|
235
|
+
ok: false,
|
|
236
|
+
refusal: makeRefusal(
|
|
237
|
+
"empty-response",
|
|
238
|
+
"memmapshow: the response contained only prompt text and no header -- never decoded as a zero-entry access map",
|
|
239
|
+
"",
|
|
240
|
+
0,
|
|
241
|
+
),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const lines = body.split("\n");
|
|
246
|
+
// A trailing "\n" before the (already-stripped) prompt splits into one
|
|
247
|
+
// trailing empty element -- drop exactly that split artifact, never any
|
|
248
|
+
// other blank line, so a genuinely blank line inside the payload still
|
|
249
|
+
// reaches the data-line parser below and refuses structurally rather
|
|
250
|
+
// than being silently swallowed here.
|
|
251
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
252
|
+
lines.pop();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const headerLine = lines[0] ?? "";
|
|
256
|
+
if (headerLine !== HEADER_LINE) {
|
|
257
|
+
return {
|
|
258
|
+
ok: false,
|
|
259
|
+
refusal: makeRefusal(
|
|
260
|
+
"missing-header",
|
|
261
|
+
`memmapshow: expected the header line ${JSON.stringify(HEADER_LINE)}, found ${JSON.stringify(headerLine)}`,
|
|
262
|
+
headerLine,
|
|
263
|
+
1,
|
|
264
|
+
),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const dataLines = lines.slice(1);
|
|
269
|
+
if (dataLines.length === 0) {
|
|
270
|
+
return {
|
|
271
|
+
ok: false,
|
|
272
|
+
refusal: makeRefusal(
|
|
273
|
+
"no-data-lines",
|
|
274
|
+
"memmapshow: the header was present but no data lines followed it -- never decoded as a zero-entry access map",
|
|
275
|
+
headerLine,
|
|
276
|
+
1,
|
|
277
|
+
),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const entries: AccessMapEntry[] = [];
|
|
282
|
+
|
|
283
|
+
for (let i = 0; i < dataLines.length; i++) {
|
|
284
|
+
const line = dataLines[i]!;
|
|
285
|
+
const lineNumber = i + 2; // the header occupies line 1
|
|
286
|
+
|
|
287
|
+
const match = DATA_LINE_RE.exec(line);
|
|
288
|
+
if (!match) {
|
|
289
|
+
return {
|
|
290
|
+
ok: false,
|
|
291
|
+
refusal: makeRefusal(
|
|
292
|
+
"malformed-line",
|
|
293
|
+
`memmapshow: line ${lineNumber} does not match the expected "aaaa: xxx xxx xxx" layout: ${JSON.stringify(line)}`,
|
|
294
|
+
line,
|
|
295
|
+
lineNumber,
|
|
296
|
+
),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
const addrHex = match[1]!;
|
|
300
|
+
const ioStr = match[2]!;
|
|
301
|
+
const romStr = match[3]!;
|
|
302
|
+
const ramStr = match[4]!;
|
|
303
|
+
const trailer = match[5] ?? "";
|
|
304
|
+
|
|
305
|
+
const ioResult = parseGlyphGroup(ioStr);
|
|
306
|
+
if (!ioResult.ok) {
|
|
307
|
+
return {
|
|
308
|
+
ok: false,
|
|
309
|
+
refusal: makeRefusal(
|
|
310
|
+
"unrecognised-glyph",
|
|
311
|
+
`memmapshow: line ${lineNumber} has an unrecognised IO glyph character ${JSON.stringify(ioResult.badChar)}: ${JSON.stringify(line)}`,
|
|
312
|
+
line,
|
|
313
|
+
lineNumber,
|
|
314
|
+
),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
const romResult = parseGlyphGroup(romStr);
|
|
318
|
+
if (!romResult.ok) {
|
|
319
|
+
return {
|
|
320
|
+
ok: false,
|
|
321
|
+
refusal: makeRefusal(
|
|
322
|
+
"unrecognised-glyph",
|
|
323
|
+
`memmapshow: line ${lineNumber} has an unrecognised ROM glyph character ${JSON.stringify(romResult.badChar)}: ${JSON.stringify(line)}`,
|
|
324
|
+
line,
|
|
325
|
+
lineNumber,
|
|
326
|
+
),
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
const ramResult = parseGlyphGroup(ramStr);
|
|
330
|
+
if (!ramResult.ok) {
|
|
331
|
+
return {
|
|
332
|
+
ok: false,
|
|
333
|
+
refusal: makeRefusal(
|
|
334
|
+
"unrecognised-glyph",
|
|
335
|
+
`memmapshow: line ${lineNumber} has an unrecognised RAM glyph character ${JSON.stringify(ramResult.badChar)}: ${JSON.stringify(line)}`,
|
|
336
|
+
line,
|
|
337
|
+
lineNumber,
|
|
338
|
+
),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const trailerResult = parseTrailer(trailer);
|
|
343
|
+
if (!trailerResult.ok) {
|
|
344
|
+
return {
|
|
345
|
+
ok: false,
|
|
346
|
+
refusal: makeRefusal(
|
|
347
|
+
"unrecognised-annotation",
|
|
348
|
+
`memmapshow: line ${lineNumber} has an unrecognised trailer ${JSON.stringify(trailer)}: ${JSON.stringify(line)}`,
|
|
349
|
+
line,
|
|
350
|
+
lineNumber,
|
|
351
|
+
),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
entries.push({
|
|
356
|
+
address: parseInt(addrHex, 16),
|
|
357
|
+
io: ioResult.flags,
|
|
358
|
+
rom: romResult.flags,
|
|
359
|
+
ram: ramResult.flags,
|
|
360
|
+
annotations: trailerResult.annotations,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return { ok: true, value: { entries } };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ---------------------------------------------------------------------------
|
|
368
|
+
// accessMapRanges() -- the adjacency-merged, budgeted range projection.
|
|
369
|
+
// ---------------------------------------------------------------------------
|
|
370
|
+
|
|
371
|
+
export interface AccessMapRangesOptions {
|
|
372
|
+
/** Inclusive lower bound, 0-65535. Defaults to 0. */
|
|
373
|
+
startAddress?: number;
|
|
374
|
+
/** Inclusive upper bound, 0-65535. Defaults to 65535. */
|
|
375
|
+
endAddress?: number;
|
|
376
|
+
/** Hard ceiling on emitted ranges. Defaults to 256, clamped to a hard
|
|
377
|
+
* ceiling of 4096 regardless of what is requested. */
|
|
378
|
+
maxRanges?: number;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** One merged, adjacency-collapsed run of addresses sharing identical
|
|
382
|
+
* access flags and annotation sets. */
|
|
383
|
+
export interface AccessMapRange {
|
|
384
|
+
readonly start: number;
|
|
385
|
+
readonly end: number;
|
|
386
|
+
readonly io: AccessFlags;
|
|
387
|
+
readonly rom: AccessFlags;
|
|
388
|
+
readonly ram: AccessFlags;
|
|
389
|
+
readonly annotations: readonly AccessAnnotation[];
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export interface AccessMapRangesResult {
|
|
393
|
+
readonly ranges: readonly AccessMapRange[];
|
|
394
|
+
/** The full merged range count, BEFORE the `maxRanges` budget is applied
|
|
395
|
+
* -- always equal to `ranges.length` when `truncated` is false. */
|
|
396
|
+
readonly rangeCount: number;
|
|
397
|
+
/** True when `rangeCount` exceeds the effective `maxRanges` budget. */
|
|
398
|
+
readonly truncated: boolean;
|
|
399
|
+
/** How many addresses in `[startAddress, endAddress]` actually appear in
|
|
400
|
+
* `map.entries` -- the numerator every count in this result is a
|
|
401
|
+
* fraction of. */
|
|
402
|
+
readonly addressesWithRecordedAccess: number;
|
|
403
|
+
/** `endAddress - startAddress + 1` (0 if the range is empty/inverted) --
|
|
404
|
+
* the denominator `addressesWithRecordedAccess` is a fraction of. An
|
|
405
|
+
* address counted here but absent from `addressesWithRecordedAccess`
|
|
406
|
+
* proves nothing about that address; it is never reported as data. */
|
|
407
|
+
readonly addressesQueried: number;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const DEFAULT_MAX_RANGES = 256;
|
|
411
|
+
const MAX_RANGES_CEILING = 4096;
|
|
412
|
+
|
|
413
|
+
function isValidAddress(value: unknown): value is number {
|
|
414
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= 0xffff;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function isValidMaxRanges(value: unknown): value is number {
|
|
418
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 1;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function sameFlags(a: AccessFlags, b: AccessFlags): boolean {
|
|
422
|
+
return a.read === b.read && a.write === b.write && a.execute === b.execute;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function sameAnnotationSet(a: readonly AccessAnnotation[], b: readonly AccessAnnotation[]): boolean {
|
|
426
|
+
if (a.length !== b.length) return false;
|
|
427
|
+
const sortedA = [...a].sort();
|
|
428
|
+
const sortedB = [...b].sort();
|
|
429
|
+
return sortedA.every((value, index) => value === sortedB[index]);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Walks `map.entries` (already in address order -- `parseAccessMap()`
|
|
434
|
+
* always emits them that way, but this function re-sorts defensively
|
|
435
|
+
* rather than trusting the caller) and merges a run of ADJACENT addresses
|
|
436
|
+
* (address n+1 immediately following n) whose nine access booleans and
|
|
437
|
+
* whose annotation set are all identical into one range. A single
|
|
438
|
+
* differing bit, a differing annotation set, or an address gap starts a
|
|
439
|
+
* new range. Bounded by construction: one pass over `map.entries`, no
|
|
440
|
+
* recursion.
|
|
441
|
+
*
|
|
442
|
+
* There is no field, key or label anywhere in this function's return value
|
|
443
|
+
* that classifies an address as data -- an address absent from
|
|
444
|
+
* `map.entries` is reported only through the gap between
|
|
445
|
+
* `addressesWithRecordedAccess` and `addressesQueried` (see this module's
|
|
446
|
+
* header comment).
|
|
447
|
+
*/
|
|
448
|
+
export function accessMapRanges(map: AccessMap, opts: AccessMapRangesOptions = {}): AccessMapRangesResult {
|
|
449
|
+
const options = opts && typeof opts === "object" ? opts : {};
|
|
450
|
+
const startAddress = isValidAddress(options.startAddress) ? options.startAddress : 0;
|
|
451
|
+
const endAddress = isValidAddress(options.endAddress) ? options.endAddress : 0xffff;
|
|
452
|
+
const requestedMaxRanges = isValidMaxRanges(options.maxRanges) ? options.maxRanges : DEFAULT_MAX_RANGES;
|
|
453
|
+
const maxRanges = Math.min(requestedMaxRanges, MAX_RANGES_CEILING);
|
|
454
|
+
|
|
455
|
+
const sorted = [...map.entries].sort((a, b) => a.address - b.address);
|
|
456
|
+
const filtered = sorted.filter((entry) => entry.address >= startAddress && entry.address <= endAddress);
|
|
457
|
+
|
|
458
|
+
const merged: AccessMapRange[] = [];
|
|
459
|
+
for (const entry of filtered) {
|
|
460
|
+
const last = merged[merged.length - 1];
|
|
461
|
+
if (
|
|
462
|
+
last !== undefined &&
|
|
463
|
+
last.end + 1 === entry.address &&
|
|
464
|
+
sameFlags(last.io, entry.io) &&
|
|
465
|
+
sameFlags(last.rom, entry.rom) &&
|
|
466
|
+
sameFlags(last.ram, entry.ram) &&
|
|
467
|
+
sameAnnotationSet(last.annotations, entry.annotations)
|
|
468
|
+
) {
|
|
469
|
+
merged[merged.length - 1] = { ...last, end: entry.address };
|
|
470
|
+
} else {
|
|
471
|
+
merged.push({
|
|
472
|
+
start: entry.address,
|
|
473
|
+
end: entry.address,
|
|
474
|
+
io: entry.io,
|
|
475
|
+
rom: entry.rom,
|
|
476
|
+
ram: entry.ram,
|
|
477
|
+
annotations: entry.annotations,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const rangeCount = merged.length;
|
|
483
|
+
const truncated = rangeCount > maxRanges;
|
|
484
|
+
const ranges = truncated ? merged.slice(0, maxRanges) : merged;
|
|
485
|
+
const addressesQueried = endAddress >= startAddress ? endAddress - startAddress + 1 : 0;
|
|
486
|
+
|
|
487
|
+
return {
|
|
488
|
+
ranges,
|
|
489
|
+
rangeCount,
|
|
490
|
+
truncated,
|
|
491
|
+
addressesWithRecordedAccess: filtered.length,
|
|
492
|
+
addressesQueried,
|
|
493
|
+
};
|
|
494
|
+
}
|