@henols/vice-mcp 0.2.2 → 0.2.4
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 +1736 -163
- package/anno-confidence.ts +2 -2
- package/anno-derive.ts +6 -6
- package/anno-details.ts +4 -4
- package/anno-enum-gen.ts +416 -30
- package/anno-export-asm.ts +1211 -126
- package/anno-graphics.ts +338 -0
- package/anno-hazard-report.ts +1367 -0
- package/anno-import.ts +495 -0
- package/anno-index.ts +8 -8
- package/anno-join.ts +480 -0
- package/anno-memmap-render.ts +22 -21
- package/anno-provenance-ledger.ts +472 -0
- package/anno-regbits-gen.ts +13 -13
- package/anno-register.ts +159 -0
- package/anno-store-export.ts +661 -0
- package/anno-store.ts +635 -124
- package/anno-symbols.ts +7 -7
- package/anno-tools.ts +1169 -16
- package/anno-types.ts +313 -40
- 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 +220 -54
- package/resources/broker-epoch.mjs +7 -8
- package/resources/broker-kill.mjs +36 -31
- package/resources/broker-launch.mjs +511 -374
- package/resources/broker-state.mjs +69 -24
- package/resources/container-guard.mjs +1 -1
- package/resources/ghidra-project.mjs +790 -0
- package/resources/host-tool.mjs +2533 -0
- package/resources/vice-broker.mjs +434 -290
- 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 +253 -108
- 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
package/anno-confidence.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// anno-confidence.ts -- the ONE authoritative place in this repo for
|
|
2
|
+
// anno-confidence.ts -- the ONE authoritative place in this repo for the
|
|
3
3
|
// confidence-grade convention: a machine-readable bracket-token prefix inside
|
|
4
4
|
// an anno line comment (e.g. `[confirmed-code] observed executing at $0810`).
|
|
5
5
|
//
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// "reachable via a JSR, never run" -- that distinction is
|
|
11
11
|
// `memory-map.template.md`'s most deliberate feature, and its own text
|
|
12
12
|
// forbids promoting a row by editing its grade (re-verify and restate the
|
|
13
|
-
// evidence instead). Measured
|
|
13
|
+
// evidence instead). Measured: anno line comments persist through
|
|
14
14
|
// save/reload (`user_line_comments`), and both `anno_get_comments` and
|
|
15
15
|
// `anno_search_disassembly` (which searches comments by default) can filter
|
|
16
16
|
// on a leading token -- so "show me everything still [unknown]" is a real
|
package/anno-derive.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// ON-DISK TRUTH that can disagree with the range table it came from".
|
|
22
22
|
// * `anno-types.ts`'s `XrefRow` type doc: "Only NON-DERIVABLE references live
|
|
23
23
|
// here".
|
|
24
|
-
// *
|
|
24
|
+
// * the derived-from-bytes byte-coverage census, whose whole
|
|
25
25
|
// discipline is that coverage is computed from bytes rather than recorded
|
|
26
26
|
// beside them.
|
|
27
27
|
//
|
|
@@ -57,11 +57,11 @@
|
|
|
57
57
|
// no index file, no memoised store column, no snapshot. That means: no SQL
|
|
58
58
|
// write statement, no `node:fs` write call, and no naming of the
|
|
59
59
|
// persistence builtin `node:sqlite` -- the store is reached ONLY through
|
|
60
|
-
// `anno-store.ts`'s own read entry points
|
|
60
|
+
// `anno-store.ts`'s own read entry points.
|
|
61
61
|
// - Never import `hostpath.ts`, `containerpath.ts` or `container-guard.mts`.
|
|
62
62
|
// This module is proxy-local; a host/container-translated path would point
|
|
63
63
|
// the derivation at bytes on the wrong side of the container boundary
|
|
64
|
-
// (
|
|
64
|
+
// (`hostpath-consumers.test.ts` names this module as forbidden).
|
|
65
65
|
// - Never add a second address parser, a second range validator or a second
|
|
66
66
|
// data-type vocabulary. `parseStoreAddress`, `assertRangeShape` and
|
|
67
67
|
// `assertDataType` are imported from `anno-types.ts` for exactly that
|
|
@@ -189,7 +189,7 @@ function currentCorpusByteCap(): number {
|
|
|
189
189
|
function assertImage(image: unknown): Uint8Array {
|
|
190
190
|
if (!(image instanceof Uint8Array)) {
|
|
191
191
|
throw new AnnoDeriveArgumentError(
|
|
192
|
-
'the program "image" must be a Uint8Array of the bytes to derive from -- the store holds NO program image
|
|
192
|
+
'the program "image" must be a Uint8Array of the bytes to derive from -- the store holds NO program image, so every ' +
|
|
193
193
|
"derived answer is computed from bytes the caller names.",
|
|
194
194
|
{ argument: "image", value: typeof image },
|
|
195
195
|
);
|
|
@@ -281,7 +281,7 @@ function referencedAddress(instruction: Instruction): number | undefined {
|
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
// ---------------------------------------------------------------------------
|
|
284
|
-
// Derived cross-references
|
|
284
|
+
// Derived cross-references
|
|
285
285
|
// ---------------------------------------------------------------------------
|
|
286
286
|
|
|
287
287
|
/** What `crossReferencesTo()` returns: the target, every address that reaches
|
|
@@ -363,7 +363,7 @@ export function crossReferencesTo(
|
|
|
363
363
|
}
|
|
364
364
|
|
|
365
365
|
// ---------------------------------------------------------------------------
|
|
366
|
-
// Search
|
|
366
|
+
// Search
|
|
367
367
|
// ---------------------------------------------------------------------------
|
|
368
368
|
|
|
369
369
|
/** The three corpora this surface has. Frozen and derived from, never
|
package/anno-details.ts
CHANGED
|
@@ -32,15 +32,15 @@
|
|
|
32
32
|
// - Never scan for the containing range with a start/end bracket comparison
|
|
33
33
|
// loop. `resolveAt()` over `paintIndexOf()` is the single arbiter --
|
|
34
34
|
// narrowest range wins, and among equally short ones the later-inserted one
|
|
35
|
-
// --
|
|
35
|
+
// -- cross-validated at all 65,536 addresses against an
|
|
36
36
|
// independently written implementation. A second lookup rule here would be
|
|
37
37
|
// a second answer, and the disagreement would be invisible because both
|
|
38
38
|
// look authoritative.
|
|
39
39
|
// - Never write anything. This module is on a read path and is held to
|
|
40
40
|
// `anno-derive.ts`'s never-cache rule by the same structural control: no
|
|
41
41
|
// SQL write verb, no filesystem write call, no persistence binding.
|
|
42
|
-
// - Never import `hostpath.ts`, `containerpath.ts` or `container-guard.mts`
|
|
43
|
-
//
|
|
42
|
+
// - Never import `hostpath.ts`, `containerpath.ts` or `container-guard.mts` --
|
|
43
|
+
// this composition is proxy-local.
|
|
44
44
|
// - Never return an empty array for something that could not be answered. A
|
|
45
45
|
// component with no answer comes back as `{available:false, reason}`, so an
|
|
46
46
|
// address that genuinely has no comments is distinguishable from a question
|
|
@@ -135,7 +135,7 @@ export function composeAddressDetails(
|
|
|
135
135
|
available: false,
|
|
136
136
|
reason:
|
|
137
137
|
`cross-references to ${hex4(at)} are DERIVED from program bytes on every query and no bytes were supplied: the store ` +
|
|
138
|
-
"holds no program image
|
|
138
|
+
"holds no program image, so this component is answerable only when the caller names the image it wants derived " +
|
|
139
139
|
"from. The stored non-derivable rows alone are readable through anno-store.ts's listXrefs.",
|
|
140
140
|
}
|
|
141
141
|
: { available: true, value: crossReferencesTo(handle, image, origin, at) };
|
package/anno-enum-gen.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// anno-enum-gen.ts -- the ONE authoritative place in this repo for value ->
|
|
3
3
|
// variant naming, the adjacent-pair rule, identifier sanitization, the
|
|
4
|
-
// per-register enum plan
|
|
5
|
-
//
|
|
4
|
+
// per-register enum plan, the coverage report's wording contract, and (as of
|
|
5
|
+
// phase 45 plan 45-03) the multi-bit register DECOMPOSITION into named,
|
|
6
|
+
// OR-able terms (D-15/D-16/D-17/D-20/D-22/D-23, ANNO-13).
|
|
6
7
|
//
|
|
7
8
|
// WHAT LEFT, WHAT STAYED, AND WHERE THE ROUTE RETURNS (plan 29-10, D-01,
|
|
8
9
|
// 2026-08-30). Read this paragraph before looking for a function that is not
|
|
@@ -39,17 +40,34 @@
|
|
|
39
40
|
// leaving it to be inferred from a row count.
|
|
40
41
|
// - `sanitizeVariantMap()` and the identifier gate it runs, unchanged.
|
|
41
42
|
//
|
|
42
|
-
// WHERE THE ROUTE RETURNS:
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
// the
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
// that
|
|
52
|
-
//
|
|
43
|
+
// WHERE THE ROUTE RETURNS: this line used to say "NO PHASE CURRENTLY OWNS
|
|
44
|
+
// ITS RETURN", and that too is now CORRECTED rather than deleted, for the
|
|
45
|
+
// same reason the paragraph above was: deleting a withdrawal notice erases
|
|
46
|
+
// the record that a capability went missing, and deleting a return notice
|
|
47
|
+
// would erase the record of when and why it came back. The first
|
|
48
|
+
// correction (recorded here, kept for the history): it forecast a rebuild
|
|
49
|
+
// of the fetch and the install over this project's own annotation store,
|
|
50
|
+
// rendering the enums into the ACME export. The ACME export route itself
|
|
51
|
+
// did come back on 2026-08-31, as the `anno export-asm` CLI verb -- but the
|
|
52
|
+
// work that rebuilt it covered that route ONLY: no requirement and no
|
|
53
|
+
// success criterion of it mentioned `gen-enums`, and at that time no phase
|
|
54
|
+
// owned rebuilding it.
|
|
55
|
+
//
|
|
56
|
+
// THE SECOND CORRECTION, dated 2026-09-11 (phase 45 plan 45-03, D-15):
|
|
57
|
+
// Phase 45 owns it now, and has returned it -- the ENUM half of `ANNO-13`
|
|
58
|
+
// only. `fetchRegisterSearchRows()` walks a store's own `code`-typed ranges
|
|
59
|
+
// through the same `disasm-decoder.ts` `decode()` `anno_disassemble` uses,
|
|
60
|
+
// `generateEnumsFromStore()` strings fetch -> `pairSearchRows()` ->
|
|
61
|
+
// `planEnumsForPairing()` -> `sanitizeVariantMap()` -> `installPlannedEnums()`
|
|
62
|
+
// -> `buildEnumGenerationReport()`, and `installPlannedEnums()` installs
|
|
63
|
+
// through the same `createProjectEnum()`/`updateProjectEnum()`/
|
|
64
|
+
// `applyEnumUsage()` write path the by-hand route already used. The symbol
|
|
65
|
+
// round trip (`ANNO-14`/`ANNO-15`, `export-lbl`/`import-lbl`) is a SEPARATE
|
|
66
|
+
// capability this phase does not touch and remains unowned -- see
|
|
67
|
+
// `.planning/PROJECT.md`'s own withdrawal notice, corrected in the same
|
|
68
|
+
// plan. Everything above this paragraph is the specification this rebuild
|
|
69
|
+
// was built against, and it needed no changes to build against: every
|
|
70
|
+
// surviving heuristic is called here unmodified.
|
|
53
71
|
//
|
|
54
72
|
// MEASURED MECHANISM FACTS, PAST TENSE -- kept because they are WHY the
|
|
55
73
|
// heuristics have the shape they have, not because anything still calls the
|
|
@@ -115,6 +133,11 @@ import { fileURLToPath } from "node:url";
|
|
|
115
133
|
|
|
116
134
|
import type { RegBitsField, RegBitsTable } from "./anno-regbits-gen.ts";
|
|
117
135
|
import { MAX_ACME_IDENTIFIER_LENGTH, assertLegalAcmeIdentifier } from "./anno-acme-ident.ts";
|
|
136
|
+
import { decode } from "./disasm-decoder.ts";
|
|
137
|
+
import type { Instruction } from "./disasm-decoder.ts";
|
|
138
|
+
import { applyEnumUsage, createProjectEnum, listRanges, updateProjectEnum } from "./anno-store.ts";
|
|
139
|
+
import type { AnnoStoreHandle } from "./anno-store.ts";
|
|
140
|
+
import { AnnoLabelError } from "./anno-types.ts";
|
|
118
141
|
|
|
119
142
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
120
143
|
const REGBITS_PATH = join(HERE, "anno-regbits.json");
|
|
@@ -182,32 +205,85 @@ export function registerKeyFor(address: number): string {
|
|
|
182
205
|
* The measured target this function is pinned against:
|
|
183
206
|
* `variantNameFor(0xd011, 0x1b) === "YSCROLL3_ROW25_SCREENON_TEXT"`.
|
|
184
207
|
*/
|
|
185
|
-
|
|
208
|
+
/** Looks up `register`'s bit-name table entry, or throws naming the register
|
|
209
|
+
* and the remedy -- the ONE lookup+refusal both `variantNameFor()` and
|
|
210
|
+
* `decomposeRegisterValue()` share, so the two can never disagree about
|
|
211
|
+
* which registers are decodable at all. */
|
|
212
|
+
function requireRegBitsEntry(key: string, callerName: string): RegBitsField[] {
|
|
186
213
|
const table = loadRegBits();
|
|
187
|
-
const key = registerKeyFor(register);
|
|
188
214
|
const entry = table[key];
|
|
189
215
|
if (!entry) {
|
|
190
216
|
throw new Error(
|
|
191
|
-
|
|
217
|
+
`${callerName}: no bit-name table entry for register ${key} -- anno-regbits.json has no fields ` +
|
|
192
218
|
"for this address (add an OVERRIDES entry in anno-regbits-gen.ts, or exclude it from generation).",
|
|
193
219
|
);
|
|
194
220
|
}
|
|
221
|
+
return entry.fields as RegBitsField[];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* THE ONE MEMBERSHIP-TEST PREDICATE (45-REVIEW CR-01): answers "does
|
|
226
|
+
* `anno-regbits.json` have an entry for this register at all", independent
|
|
227
|
+
* of whether that entry, once found, can fully decompose any particular
|
|
228
|
+
* value. `pairSearchRows()` above already narrows candidate `sta` targets
|
|
229
|
+
* this same way (`knownRegisters.has(key)`, built from this table's own
|
|
230
|
+
* keys) before ever treating one as a register; this export gives the two
|
|
231
|
+
* D-16 render surfaces (`anno-export-asm.ts`, `anno-tools.ts`'s
|
|
232
|
+
* `renderDisassembleListing()`) the identical membership check so a
|
|
233
|
+
* register-SHAPED enum name for a register the table simply does not cover
|
|
234
|
+
* (e.g. `D020`, `D021` -- confirmed absent, `docs/phase45-closure-dxa-family.md`)
|
|
235
|
+
* falls through to the plain single-symbol path instead of being attempted,
|
|
236
|
+
* and failing, through `decomposeRegisterValue()`.
|
|
237
|
+
*
|
|
238
|
+
* `key` is the SAME `$`-prefixed shape `registerKeyFor()` produces and the
|
|
239
|
+
* table's own keys use (e.g. `"$D011"`) -- callers holding only the bare
|
|
240
|
+
* `regKey.slice(1)` enum name (e.g. `"D011"`) must prefix it with `$` before
|
|
241
|
+
* calling this, exactly as `requireRegBitsEntry()`'s own callers already do
|
|
242
|
+
* via `registerKeyFor()`.
|
|
243
|
+
*
|
|
244
|
+
* Deliberately NOT folded into `requireRegBitsEntry()`: that function's job
|
|
245
|
+
* is "fetch or throw naming the remedy" for a caller that already believes
|
|
246
|
+
* the register IS decodable; this function's job is "may I even ask" for a
|
|
247
|
+
* caller that does not yet know. Collapsing them would force every
|
|
248
|
+
* membership check to pay for (and catch) a thrown error it does not want.
|
|
249
|
+
*/
|
|
250
|
+
export function hasRegBitsEntry(key: string): boolean {
|
|
251
|
+
return loadRegBits()[key] !== undefined;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Decodes ONE field of `register`'s value against `field` -- the single
|
|
256
|
+
* decode step `variantNameFor()` and `decomposeRegisterValue()` BOTH walk in
|
|
257
|
+
* the same ascending bit order (Task 1's own rule: "do not write a second
|
|
258
|
+
* decode"). A numeric field ALWAYS returns a non-empty token (total by
|
|
259
|
+
* construction). A flag/enum field's token is looked up in `field.tokens`;
|
|
260
|
+
* an explicitly-silent state (empty string) is returned as `""`, never
|
|
261
|
+
* treated as absent; a genuinely missing token throws, naming the register,
|
|
262
|
+
* the field and the decoded value, exactly as before this extraction.
|
|
263
|
+
*/
|
|
264
|
+
function decodeField(key: string, field: RegBitsField, value: number): { decoded: number; token: string } {
|
|
265
|
+
const decoded = (value & field.mask) >>> field.shift;
|
|
266
|
+
if (field.kind === "numeric") {
|
|
267
|
+
return { decoded, token: `${field.name}${decoded}` };
|
|
268
|
+
}
|
|
269
|
+
const token = field.tokens?.[decoded];
|
|
270
|
+
if (token === undefined) {
|
|
271
|
+
throw new Error(
|
|
272
|
+
`variantNameFor: register ${key} field "${field.name}" (kind ${field.kind}) has no token for decoded ` +
|
|
273
|
+
`value ${decoded} (full register value 0x${value.toString(16)}) -- refusing rather than silently ` +
|
|
274
|
+
"dropping a field, which could make two distinct register values decode to the same name.",
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
return { decoded, token };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function variantNameFor(register: number, value: number): string {
|
|
281
|
+
const key = registerKeyFor(register);
|
|
282
|
+
const fields = requireRegBitsEntry(key, "variantNameFor");
|
|
195
283
|
|
|
196
284
|
const tokens: string[] = [];
|
|
197
|
-
for (const field of
|
|
198
|
-
const
|
|
199
|
-
if (field.kind === "numeric") {
|
|
200
|
-
tokens.push(`${field.name}${decoded}`);
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
203
|
-
const token = field.tokens?.[decoded];
|
|
204
|
-
if (token === undefined) {
|
|
205
|
-
throw new Error(
|
|
206
|
-
`variantNameFor: register ${key} field "${field.name}" (kind ${field.kind}) has no token for decoded ` +
|
|
207
|
-
`value ${decoded} (full register value 0x${value.toString(16)}) -- refusing rather than silently ` +
|
|
208
|
-
"dropping a field, which could make two distinct register values decode to the same name.",
|
|
209
|
-
);
|
|
210
|
-
}
|
|
285
|
+
for (const field of fields) {
|
|
286
|
+
const { token } = decodeField(key, field, value);
|
|
211
287
|
if (token !== "") tokens.push(token);
|
|
212
288
|
}
|
|
213
289
|
if (tokens.length === 0) {
|
|
@@ -226,6 +302,150 @@ export function variantNameFor(register: number, value: number): string {
|
|
|
226
302
|
return tokens.join("_");
|
|
227
303
|
}
|
|
228
304
|
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
// The ONE owning multi-bit decoder (Task 1, D-16/D-17). `variantNameFor()`
|
|
307
|
+
// above already decodes a value into per-field tokens and joins them with
|
|
308
|
+
// `_` into ONE total name; this is that SAME token list, unjoined, each term
|
|
309
|
+
// carrying its own masked value -- so the OR-ed decomposition and the
|
|
310
|
+
// whole-value enum member are provably one vocabulary, never two.
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
/** One named, OR-able term of a decomposed register write. `name` is the
|
|
314
|
+
* emitted ACME identifier (`<enumName>_<field token>`, the same
|
|
315
|
+
* `regKey.slice(1)` prefix `planEnumsForPairing()` already uses for its own
|
|
316
|
+
* `enumName`). `value` is this term's own masked contribution
|
|
317
|
+
* (`value & field.mask`) -- the bitwise OR of every term's `value` in a
|
|
318
|
+
* `RegisterDecomposition` reconstructs the original byte exactly, or
|
|
319
|
+
* `decomposeRegisterValue()` refuses rather than return the lossy result. */
|
|
320
|
+
export interface RegisterTerm {
|
|
321
|
+
name: string;
|
|
322
|
+
value: number;
|
|
323
|
+
fieldName: string;
|
|
324
|
+
decoded: number;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** The full decomposition of one register write. `comment` is the
|
|
328
|
+
* mechanical decode text (`<REGKEY>: <FIELD>=<decoded>`, comma-separated, in
|
|
329
|
+
* ascending bit order) -- D-17's readability half; `terms` is the OR-able
|
|
330
|
+
* half. `multiField` is true when the register's own table entry has two or
|
|
331
|
+
* more fields, regardless of how many terms a particular value happened to
|
|
332
|
+
* produce (a single-field register, or a value that silenced every
|
|
333
|
+
* flag/enum field but one, is not "multi-bit" in the sense D-17 cares
|
|
334
|
+
* about). */
|
|
335
|
+
export interface RegisterDecomposition {
|
|
336
|
+
terms: RegisterTerm[];
|
|
337
|
+
comment: string;
|
|
338
|
+
multiField: boolean;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* THE ONE OWNING DECODER (D-16): splits `value` into one named term per
|
|
343
|
+
* bit-field of `register`, arithmetically exact. Both render surfaces
|
|
344
|
+
* (`anno-export-asm.ts`'s OR-ed constants, plan 45-05; `anno_disassemble`'s
|
|
345
|
+
* readable comment, plan 45-05) consume THIS function rather than decoding
|
|
346
|
+
* independently -- see this module's own header for why that is the whole
|
|
347
|
+
* point.
|
|
348
|
+
*
|
|
349
|
+
* Rules, each pinned by a test in `anno-enum-gen.test.ts`:
|
|
350
|
+
* - Walks the SAME field loop, in the SAME ascending bit order, and the
|
|
351
|
+
* SAME per-field decode (`decodeField()` above) that `variantNameFor()`
|
|
352
|
+
* walks -- never a second decode.
|
|
353
|
+
* - A field whose token is the explicit empty string AND whose masked
|
|
354
|
+
* contribution is zero is OMITTED: it contributes nothing to the OR and
|
|
355
|
+
* nothing to the name (the silent-by-design case).
|
|
356
|
+
* - A field whose token is the empty string but whose masked contribution
|
|
357
|
+
* is NON-zero is a DATA ERROR in `anno-regbits.json`'s own OVERRIDES
|
|
358
|
+
* table -- refused by name, exactly like the missing-token case
|
|
359
|
+
* `decodeField()` already refuses.
|
|
360
|
+
* - After building the terms, the OR of every term's `value` MUST equal
|
|
361
|
+
* the input `value`. When it does not -- the table's fields do not cover
|
|
362
|
+
* every set bit of `value` -- this REFUSES, naming the register, the
|
|
363
|
+
* value and the uncovered bits in hex, with the remedy. Never emits a
|
|
364
|
+
* residual hex literal into the term list: a magic number in the OR
|
|
365
|
+
* expression is exactly what criterion 5 forbids.
|
|
366
|
+
* - The degenerate all-silent case (every field decodes to a silent
|
|
367
|
+
* token) returns the single `V<value>` term rather than an empty list,
|
|
368
|
+
* mirroring `variantNameFor()`'s own fallback so the two never disagree
|
|
369
|
+
* about what that value is called. This can only happen when `value`
|
|
370
|
+
* itself is `0` for a register whose fields fully cover the byte (every
|
|
371
|
+
* silent field's masked contribution is, by the rule above, zero) --
|
|
372
|
+
* the OR-reconstruction check above still runs FIRST, so a value this
|
|
373
|
+
* branch would otherwise mis-accept as "all silent" but that actually
|
|
374
|
+
* has uncovered bits is refused there instead, never silently treated
|
|
375
|
+
* as degenerate.
|
|
376
|
+
*/
|
|
377
|
+
export function decomposeRegisterValue(register: number, value: number): RegisterDecomposition {
|
|
378
|
+
const key = registerKeyFor(register);
|
|
379
|
+
const fields = requireRegBitsEntry(key, "decomposeRegisterValue");
|
|
380
|
+
const enumName = key.slice(1); // "$D011" -> "D011", the SAME prefix planEnumsForPairing() derives.
|
|
381
|
+
|
|
382
|
+
const terms: RegisterTerm[] = [];
|
|
383
|
+
const commentParts: string[] = [];
|
|
384
|
+
let orAccumulator = 0;
|
|
385
|
+
|
|
386
|
+
for (const field of fields) {
|
|
387
|
+
const { decoded, token } = decodeField(key, field, value);
|
|
388
|
+
const masked = value & field.mask;
|
|
389
|
+
commentParts.push(`${field.name}=${decoded}`);
|
|
390
|
+
|
|
391
|
+
if (token === "") {
|
|
392
|
+
if (masked !== 0) {
|
|
393
|
+
throw new Error(
|
|
394
|
+
`decomposeRegisterValue: register ${key} field "${field.name}" decoded a NON-ZERO contribution ` +
|
|
395
|
+
`(0x${masked.toString(16)}) from an explicitly-silent token at decoded value ${decoded} (full register ` +
|
|
396
|
+
`value 0x${value.toString(16)}) -- a silent token must correspond to a zero masked contribution, or the ` +
|
|
397
|
+
"OR-reconstruction below would silently drop a real bit. This is a data error in anno-regbits.json's " +
|
|
398
|
+
"OVERRIDES table (anno-regbits-gen.ts), not a value this function can decompose.",
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
continue; // silent-by-design, zero contribution -- omitted from both the OR and the name.
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
terms.push({ name: `${enumName}_${token}`, value: masked, fieldName: field.name, decoded });
|
|
405
|
+
orAccumulator |= masked;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (orAccumulator !== value) {
|
|
409
|
+
const uncovered = value & ~orAccumulator & 0xff;
|
|
410
|
+
throw new Error(
|
|
411
|
+
`decomposeRegisterValue: register ${key} value 0x${value.toString(16)} is not fully covered by its fields -- the ` +
|
|
412
|
+
`OR of the decomposed terms is 0x${orAccumulator.toString(16)}, leaving bits 0x${uncovered.toString(16)} unaccounted ` +
|
|
413
|
+
"for. Refusing to emit a lossy decomposition rather than a residual hex literal (add an OVERRIDES entry in " +
|
|
414
|
+
"anno-regbits-gen.ts and regenerate anno-regbits.json).",
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (terms.length === 0) {
|
|
419
|
+
// Every field decoded to an explicitly-silent, zero-contribution token --
|
|
420
|
+
// the invariant check above already proved value === 0 in this branch
|
|
421
|
+
// (orAccumulator is the OR of zeros), so this mirrors variantNameFor()'s
|
|
422
|
+
// own V<value> fallback exactly, never disagreeing about what value 0
|
|
423
|
+
// (or any all-silent value) is called.
|
|
424
|
+
terms.push({ name: `${enumName}_V${value}`, value, fieldName: "", decoded: value });
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// T-45-10's mitigation, run here rather than left to a downstream caller:
|
|
428
|
+
// MEASURED against the real committed table (register $0001, "MOS 6510
|
|
429
|
+
// Micro-Processor On-Chip I/O Port") that a numeric-leading `enumName`
|
|
430
|
+
// (`registerKeyFor(1).slice(1)` is the all-digit string "0001") produces
|
|
431
|
+
// an illegal ACME identifier for EVERY term of that register, regardless
|
|
432
|
+
// of value -- `sanitizeVariantMap()` never catches this because it only
|
|
433
|
+
// validates a bare variant name, never the enum-name prefix this
|
|
434
|
+
// function's own OR-emission shape adds. Refusing here, before returning
|
|
435
|
+
// anything, is the same client-side-before-I/O property
|
|
436
|
+
// `sanitizeVariantMap()` already holds -- an illegal name provably never
|
|
437
|
+
// reaches a render surface.
|
|
438
|
+
for (const term of terms) {
|
|
439
|
+
assertLegalAcmeIdentifier(term.name, `decomposeRegisterValue term for register ${key} value 0x${value.toString(16)}`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return {
|
|
443
|
+
terms,
|
|
444
|
+
comment: `${key}: ${commentParts.join(", ")}`,
|
|
445
|
+
multiField: fields.length >= 2,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
229
449
|
// ---------------------------------------------------------------------------
|
|
230
450
|
// The two-pass search + adjacent-pair (D-23).
|
|
231
451
|
// ---------------------------------------------------------------------------
|
|
@@ -531,3 +751,169 @@ export function buildEnumGenerationReport(
|
|
|
531
751
|
};
|
|
532
752
|
}
|
|
533
753
|
|
|
754
|
+
// ---------------------------------------------------------------------------
|
|
755
|
+
// Task 2 (D-15): THE REBUILT FETCH AND INSTALL, over this project's own
|
|
756
|
+
// disassembler and store. Everything above this line is a surviving
|
|
757
|
+
// heuristic, called here but never edited (`variantNameFor()`,
|
|
758
|
+
// `pairSearchRows()`, `planEnumsForPairing()`, `sanitizeVariantMap()`,
|
|
759
|
+
// `buildEnumGenerationReport()`).
|
|
760
|
+
// ---------------------------------------------------------------------------
|
|
761
|
+
|
|
762
|
+
/** The minimal shape this module's fetch needs from a loaded image: the
|
|
763
|
+
* origin address and the raw body bytes. Deliberately NOT importing
|
|
764
|
+
* `anno-tools.ts`'s own `LoadedImage` (a private, tool-layer interface) --
|
|
765
|
+
* that would pull the tool-dispatch module into this one, and all this fetch
|
|
766
|
+
* needs from it is these two fields. */
|
|
767
|
+
interface EnumSourceImage {
|
|
768
|
+
origin: number;
|
|
769
|
+
body: Uint8Array;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const IMMEDIATE_LOAD_MNEMONICS: ReadonlySet<string> = new Set(["lda", "ldx", "ldy"]);
|
|
773
|
+
const ABSOLUTE_STORE_MNEMONICS: ReadonlySet<string> = new Set(["sta", "stx", "sty"]);
|
|
774
|
+
|
|
775
|
+
/** The slice of `image` covering `[start, endInclusive]`, or `null` when the
|
|
776
|
+
* span is not entirely inside the image -- mirrors `anno-tools.ts`'s own
|
|
777
|
+
* `sliceSpan()` bounds discipline (never a short slice, never a fabricated
|
|
778
|
+
* byte for a range this image does not cover) without importing that
|
|
779
|
+
* private function. */
|
|
780
|
+
function sliceImageRange(image: EnumSourceImage, start: number, endInclusive: number): Uint8Array | null {
|
|
781
|
+
const from = start - image.origin;
|
|
782
|
+
const to = endInclusive - image.origin;
|
|
783
|
+
if (from < 0 || to >= image.body.length || from > to) return null;
|
|
784
|
+
return image.body.subarray(from, to + 1);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function searchRowAddress(instr: Instruction): string {
|
|
788
|
+
return `$${instr.address.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function toSearchRow(instr: Instruction, operand: string): DisasmSearchRow {
|
|
792
|
+
return { address: searchRowAddress(instr), address_decimal: instr.address, label: "", mnemonic: instr.mnemonic, operand, comment: "" };
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
export interface FetchRegisterSearchRowsOptions {
|
|
796
|
+
maxResults?: number;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
export interface FetchRegisterSearchRowsResult {
|
|
800
|
+
ldaRows: DisasmSearchRow[];
|
|
801
|
+
staRows: DisasmSearchRow[];
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* THE REBUILT FETCH (D-15). Walks `handle`'s own `code`-typed ranges,
|
|
806
|
+
* decoding each through the SAME `disasm-decoder.ts` `decode()` function
|
|
807
|
+
* `anno_disassemble` uses -- never a second decoder, never a regex over
|
|
808
|
+
* rendered text. Returns two plain row arrays in the exact `DisasmSearchRow`
|
|
809
|
+
* shape `pairSearchRows()` already consumes: pass 1, immediate loads
|
|
810
|
+
* (`lda`/`ldx`/`ldy`); pass 2, absolute stores (`sta`/`stx`/`sty`) whose
|
|
811
|
+
* target is a register `anno-regbits.json` knows.
|
|
812
|
+
*
|
|
813
|
+
* `maxResults` bounds EACH pass independently AS IT IS FETCHED, not merely
|
|
814
|
+
* reported afterwards -- that is what makes `pairSearchRows()`'s own
|
|
815
|
+
* truncation signal (a returned row count equal to the ceiling) a true
|
|
816
|
+
* measurement rather than a coincidence: capping here is the only way a
|
|
817
|
+
* caller comparing the returned length against the same ceiling can trust
|
|
818
|
+
* what it sees (D-23's "no silent caps").
|
|
819
|
+
*/
|
|
820
|
+
export function fetchRegisterSearchRows(
|
|
821
|
+
handle: AnnoStoreHandle,
|
|
822
|
+
image: EnumSourceImage,
|
|
823
|
+
opts: FetchRegisterSearchRowsOptions = {},
|
|
824
|
+
): FetchRegisterSearchRowsResult {
|
|
825
|
+
const maxResults = opts.maxResults ?? DEFAULT_MAX_RESULTS;
|
|
826
|
+
const knownRegisters = new Set(Object.keys(loadRegBits()));
|
|
827
|
+
|
|
828
|
+
const ldaRows: DisasmSearchRow[] = [];
|
|
829
|
+
const staRows: DisasmSearchRow[] = [];
|
|
830
|
+
|
|
831
|
+
for (const range of listRanges(handle)) {
|
|
832
|
+
if (range.dataType !== "code") continue;
|
|
833
|
+
const bytes = sliceImageRange(image, range.start, range.endInclusive);
|
|
834
|
+
if (bytes === null) continue; // this image does not cover the range -- never fabricate bytes for it
|
|
835
|
+
const instructions = decode(bytes, range.start, { end: range.endInclusive });
|
|
836
|
+
for (const instr of instructions) {
|
|
837
|
+
if (ldaRows.length < maxResults && instr.mode === "immediate" && instr.operand && IMMEDIATE_LOAD_MNEMONICS.has(instr.mnemonic)) {
|
|
838
|
+
ldaRows.push(toSearchRow(instr, `#$${instr.operand.value.toString(16).padStart(2, "0")}`));
|
|
839
|
+
} else if (staRows.length < maxResults && instr.mode === "absolute" && instr.operand && ABSOLUTE_STORE_MNEMONICS.has(instr.mnemonic)) {
|
|
840
|
+
const key = registerKeyFor(instr.operand.value);
|
|
841
|
+
if (knownRegisters.has(key)) {
|
|
842
|
+
staRows.push(toSearchRow(instr, `$${instr.operand.value.toString(16).padStart(4, "0")}`));
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
return { ldaRows, staRows };
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* THE REBUILT INSTALL (D-15): create-or-update each planned enum through the
|
|
853
|
+
* shipped `createProjectEnum()`/`updateProjectEnum()` write path -- the SAME
|
|
854
|
+
* functions `anno_create_project_enum`/`anno_update_project_enum` dispatch
|
|
855
|
+
* to, never a second install path -- and bind every occurrence through
|
|
856
|
+
* `applyEnumUsage()`, at the `lda` address (never the store address -- the
|
|
857
|
+
* measured binding fact in this module's header). `sanitizeVariantMap()`
|
|
858
|
+
* runs FIRST, before any I/O, so an illegal identifier provably never
|
|
859
|
+
* reaches the store (the same client-side-first property the deleted
|
|
860
|
+
* installer proved with a spy binary).
|
|
861
|
+
*
|
|
862
|
+
* CREATE-THEN-UPDATE, never a delete: `createProjectEnum()` no-ops on a
|
|
863
|
+
* byte-identical repeat and THROWS `AnnoLabelError` when the same name
|
|
864
|
+
* already holds DIFFERENT content -- caught here and retried through
|
|
865
|
+
* `updateProjectEnum()`, which replaces the variant map wholesale. This is
|
|
866
|
+
* `EnumInstallAction`'s own documented re-runnability precedent (see its
|
|
867
|
+
* comment above); an installer that could only ever report "created" would
|
|
868
|
+
* have quietly dropped ANNO-13's re-runnability requirement.
|
|
869
|
+
*/
|
|
870
|
+
export function installPlannedEnums(handle: AnnoStoreHandle, planned: readonly PlannedEnum[]): EnumInstallSummary[] {
|
|
871
|
+
const summaries: EnumInstallSummary[] = [];
|
|
872
|
+
for (const plan of planned) {
|
|
873
|
+
const sanitized = sanitizeVariantMap(plan.regKey, plan.variants);
|
|
874
|
+
const description = `Generated by anno-enum-gen.ts (D-15) from ${plan.occurrences.length} observed write(s) to ${plan.regKey}.`;
|
|
875
|
+
|
|
876
|
+
let action: EnumInstallAction;
|
|
877
|
+
try {
|
|
878
|
+
createProjectEnum(handle, { name: plan.enumName, variants: sanitized, description });
|
|
879
|
+
action = "created";
|
|
880
|
+
} catch (err) {
|
|
881
|
+
if (!(err instanceof AnnoLabelError)) throw err;
|
|
882
|
+
updateProjectEnum(handle, { name: plan.enumName, variants: sanitized, description });
|
|
883
|
+
action = "updated";
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
let usagesApplied = 0;
|
|
887
|
+
for (const occ of plan.occurrences) {
|
|
888
|
+
applyEnumUsage(handle, { address: occ.ldaAddr, name: plan.enumName });
|
|
889
|
+
usagesApplied += 1;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
summaries.push({ regKey: plan.regKey, enumName: plan.enumName, variantCount: plan.variants.size, action, usagesApplied });
|
|
893
|
+
}
|
|
894
|
+
return summaries;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
export interface GenerateEnumsFromStoreOptions {
|
|
898
|
+
maxResults?: number;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* THE REBUILT PASS (D-15): fetch -> `pairSearchRows()` -> `planEnumsForPairing()`
|
|
903
|
+
* -> `installPlannedEnums()` (which itself calls `sanitizeVariantMap()`) ->
|
|
904
|
+
* `buildEnumGenerationReport()`. The three middle heuristics are called,
|
|
905
|
+
* never edited, exactly per this module's own header specification.
|
|
906
|
+
*/
|
|
907
|
+
export function generateEnumsFromStore(
|
|
908
|
+
handle: AnnoStoreHandle,
|
|
909
|
+
image: EnumSourceImage,
|
|
910
|
+
opts: GenerateEnumsFromStoreOptions = {},
|
|
911
|
+
): EnumGenerationReport {
|
|
912
|
+
const maxResults = opts.maxResults ?? DEFAULT_MAX_RESULTS;
|
|
913
|
+
const { ldaRows, staRows } = fetchRegisterSearchRows(handle, image, { maxResults });
|
|
914
|
+
const pairing = pairSearchRows(ldaRows, staRows, maxResults);
|
|
915
|
+
const planned = planEnumsForPairing(pairing);
|
|
916
|
+
const installed = installPlannedEnums(handle, planned);
|
|
917
|
+
return buildEnumGenerationReport(pairing, installed, maxResults);
|
|
918
|
+
}
|
|
919
|
+
|