@henols/vice-mcp 0.2.0 → 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 -1
- package/anno-acme-ident.ts +97 -0
- package/anno-cli.ts +1465 -0
- package/anno-confidence.ts +233 -0
- package/anno-coverage.ts +2465 -0
- package/anno-d64.ts +310 -0
- 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/anno-memmap-render.ts +672 -0
- package/anno-regbits-gen.ts +421 -0
- package/anno-regbits.json +1370 -0
- 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 -3
- 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 +33 -9
- package/stock-timing.ts +1 -1
- package/stock-vicii.ts +1 -1
- package/version.ts +1 -1
- package/vice-proxy.ts +168 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// anno-regbits-gen.ts -- the ONE authoritative place in this repo that turns
|
|
3
|
+
// c64-memory-mapping's memmap.json into the curated address->bit-name table
|
|
4
|
+
// anno-enum-gen.ts decodes register values against (D-22, ANNO-13).
|
|
5
|
+
//
|
|
6
|
+
// WHY THIS EXISTS (D-22): neither register the phase's own pinned criterion-3
|
|
7
|
+
// target needs ($D011) nor the registers a real game writes to constantly
|
|
8
|
+
// ($D015/$D017/$D01A-$D01D) can be named from memmap.json's own `bits` prose
|
|
9
|
+
// alone -- some of that prose is OCR-damaged ("O = Blank" uses a letter O for
|
|
10
|
+
// the digit 0; "Read NMls" uses a lowercase L for an uppercase I), and five
|
|
11
|
+
// addresses have NO `bits` entry in memmap.json at all (its `io` parser never
|
|
12
|
+
// produced one for them; widening memmap.json itself is separate work
|
|
13
|
+
// belonging to `c64-memory-mapping`, not this phase). `OVERRIDES` below is
|
|
14
|
+
// the curated fix for both problems, carrying a WHY comment on every entry
|
|
15
|
+
// so a future reader never has to guess why a bit was hand-named instead of
|
|
16
|
+
// mechanically derived.
|
|
17
|
+
//
|
|
18
|
+
// WHAT THIS IS THE ONE AUTHORITATIVE PLACE FOR: reading memmap.json's 29
|
|
19
|
+
// structured `bits` entries, normalising each `bits[].bit` range string into
|
|
20
|
+
// `{mask, shift}`, deriving (or overriding) a legal ACME identifier for every
|
|
21
|
+
// field, and emitting the committed, banner-marked `anno-regbits.json`
|
|
22
|
+
// artifact `anno-enum-gen.ts` decodes against. No other module may read
|
|
23
|
+
// memmap.json for this purpose or hand-maintain a second bit-name table.
|
|
24
|
+
//
|
|
25
|
+
// KEY-SHAPE DECISION: table keys are `$XXXX` (uppercase, 4-hex-digit,
|
|
26
|
+
// dollar-prefixed) strings, not decimal numbers -- this matches how every
|
|
27
|
+
// register in this project's own assembly/documentation conventions is
|
|
28
|
+
// named (`$D011`, not `53265`), and because every key is the same fixed
|
|
29
|
+
// width, `Object.keys(table).sort()` on the STRING keys already produces the
|
|
30
|
+
// same order as sorting the underlying addresses numerically, so no separate
|
|
31
|
+
// numeric-sort step is needed to keep the emitted JSON diff-stable.
|
|
32
|
+
//
|
|
33
|
+
// WHAT NOT TO DO, named concretely:
|
|
34
|
+
// - Never hand-edit anno-regbits.json. It is a generated-but-committed
|
|
35
|
+
// artifact (ENGINEERING_RULES.md Sec 11), the same shape
|
|
36
|
+
// `resources-sync.test.ts` already established for compiled `.mjs`
|
|
37
|
+
// build output -- re-run `node anno-regbits-gen.ts` and let the drift
|
|
38
|
+
// guard in anno-regbits.test.ts confirm the result matches.
|
|
39
|
+
// - Never silently skip or placeholder an unmappable bit description.
|
|
40
|
+
// `buildRegBits()` THROWS, naming the address, the bit range and the
|
|
41
|
+
// offending description, when mechanical derivation fails AND no
|
|
42
|
+
// OVERRIDES entry covers it -- the difference between a curated table
|
|
43
|
+
// and an implicit one is exactly this refusal.
|
|
44
|
+
// - Never emit a timestamp into the banner. The drift guard's whole
|
|
45
|
+
// comparison (`buildRegBits()` re-run in memory vs. the committed file)
|
|
46
|
+
// is only TOTAL because nothing in the emitted document changes between
|
|
47
|
+
// two runs against the same memmap.json bytes.
|
|
48
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
49
|
+
import { createHash } from "node:crypto";
|
|
50
|
+
import { dirname, join } from "node:path";
|
|
51
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
52
|
+
|
|
53
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
54
|
+
|
|
55
|
+
/** The sole read of c64-memory-mapping's own memmap.json -- this generator is
|
|
56
|
+
* its only consumer for this purpose (per this plan's key_links entry).
|
|
57
|
+
* 2026-08-22 (plan 16-01): the skills tree moved from `.claude/skills/`
|
|
58
|
+
* (two levels up from `.claude/mcp/vice`) to `src/skills/` (three levels up,
|
|
59
|
+
* since `src/` sits directly under the repo root rather than under `.claude/`).
|
|
60
|
+
* This literal was not in plan 16-01's own enumerated consumer list and its
|
|
61
|
+
* test failures caught the gap live -- see 16-01-SUMMARY.md deviations. */
|
|
62
|
+
const MEMMAP_PATH = join(HERE, "..", "..", "..", "src", "skills", "c64-memory-mapping", "memmap.json");
|
|
63
|
+
|
|
64
|
+
/** Where the generated, committed artifact lives -- always a sibling of this
|
|
65
|
+
* generator, never a caller-supplied path. */
|
|
66
|
+
const OUTPUT_PATH = join(HERE, "anno-regbits.json");
|
|
67
|
+
|
|
68
|
+
export type FieldKind = "flag" | "numeric" | "enum";
|
|
69
|
+
|
|
70
|
+
export interface RegBitsField {
|
|
71
|
+
mask: number;
|
|
72
|
+
shift: number;
|
|
73
|
+
name: string;
|
|
74
|
+
kind: FieldKind;
|
|
75
|
+
/** Present only for "flag"/"enum" fields whose decoded value maps to a
|
|
76
|
+
* specific token string. A field silent-by-design in one state (e.g. a
|
|
77
|
+
* "the bit only matters when set" flag like ECM/RST8 below) still carries
|
|
78
|
+
* an EXPLICIT entry for that state -- an empty string, never an absent
|
|
79
|
+
* key -- so decoding never has to guess whether an omission was
|
|
80
|
+
* deliberate. */
|
|
81
|
+
tokens?: Record<number, string>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface RegBitsEntry {
|
|
85
|
+
label: string;
|
|
86
|
+
fields: RegBitsField[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type RegBitsTable = Record<string, RegBitsEntry>;
|
|
90
|
+
|
|
91
|
+
const ACME_IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
92
|
+
|
|
93
|
+
interface MemmapBit {
|
|
94
|
+
bit: string;
|
|
95
|
+
desc: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface MemmapEntry {
|
|
99
|
+
start: number;
|
|
100
|
+
end: number;
|
|
101
|
+
label?: string | null;
|
|
102
|
+
desc?: string;
|
|
103
|
+
bits?: MemmapBit[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface MemmapFile {
|
|
107
|
+
sources: unknown[];
|
|
108
|
+
entries: MemmapEntry[];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Parses a memmap `bits[].bit` string into `{mask, shift}`. Handles a single
|
|
113
|
+
* index ("7"), a descending range ("2-0") and an ascending range ("6-7") --
|
|
114
|
+
* both range forms appear in memmap.json's real data, and the result is
|
|
115
|
+
* identical either way since a bit RANGE has no inherent direction; only the
|
|
116
|
+
* high/low bounds matter for the mask this produces.
|
|
117
|
+
*/
|
|
118
|
+
export function parseBitRange(bitStr: string): { mask: number; shift: number } {
|
|
119
|
+
const parts = bitStr.split("-").map((s) => Number.parseInt(s.trim(), 10));
|
|
120
|
+
if (parts.length === 1) {
|
|
121
|
+
const bit = parts[0]!;
|
|
122
|
+
return { mask: 1 << bit, shift: bit };
|
|
123
|
+
}
|
|
124
|
+
const [a, b] = parts as [number, number];
|
|
125
|
+
const hi = Math.max(a, b);
|
|
126
|
+
const lo = Math.min(a, b);
|
|
127
|
+
const width = hi - lo + 1;
|
|
128
|
+
return { mask: ((1 << width) - 1) << lo, shift: lo };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Mechanically derives an ACME-legal identifier from a memmap bit
|
|
133
|
+
* description: drops parenthetical asides, uppercases, replaces every
|
|
134
|
+
* non-alphanumeric run with a single underscore, and trims leading/trailing
|
|
135
|
+
* underscores. Returns `null` (never throws) when the result is empty or
|
|
136
|
+
* still not a legal identifier -- the caller decides whether to consult
|
|
137
|
+
* `OVERRIDES` or throw; this function's job is only the mechanical half of
|
|
138
|
+
* that decision.
|
|
139
|
+
*/
|
|
140
|
+
export function deriveIdentifier(desc: string): string | null {
|
|
141
|
+
let s = desc.replace(/\([^)]*\)/g, " ");
|
|
142
|
+
s = s.toUpperCase();
|
|
143
|
+
s = s.replace(/[^A-Z0-9]+/g, "_");
|
|
144
|
+
s = s.replace(/^_+|_+$/g, "");
|
|
145
|
+
if (s === "") return null;
|
|
146
|
+
if (!ACME_IDENT_RE.test(s)) return null;
|
|
147
|
+
return s;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface RegbitsFieldOverride {
|
|
151
|
+
/** Must match a memmap `bits[].bit` string EXACTLY (e.g. "2-0", "4"). */
|
|
152
|
+
bit: string;
|
|
153
|
+
name: string;
|
|
154
|
+
kind: FieldKind;
|
|
155
|
+
tokens?: Record<number, string>;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface RegbitsRegisterOverride {
|
|
159
|
+
address: number;
|
|
160
|
+
/** Overrides memmap's own `label` for this address (OCR-damage fix). */
|
|
161
|
+
label?: string;
|
|
162
|
+
/** Per-bit overrides, matched against this address's own memmap `bits`
|
|
163
|
+
* entries by their `bit` string. */
|
|
164
|
+
fields?: readonly RegbitsFieldOverride[];
|
|
165
|
+
/** A COMPLETE field list for an address memmap.json's `io` parser produced
|
|
166
|
+
* no `bits` entry for at all (D-22's known gap) -- used only when no
|
|
167
|
+
* memmap entry exists for this address, never to replace one that does. */
|
|
168
|
+
synthetic?: readonly RegBitsField[];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Builds the 8 independent "bit N = sprite N" flag fields the five VIC
|
|
172
|
+
* sprite-plane registers ($D015/$D017/$D01B/$D01C/$D01D) all share the same
|
|
173
|
+
* shape for (D-22's named gap) -- each bit is silent (empty token) when
|
|
174
|
+
* clear and names the specific sprite when set, so a typical enum (most
|
|
175
|
+
* sprites off, one or two on) renders as a short, readable name instead of
|
|
176
|
+
* naming all eight sprites' negative state every time.
|
|
177
|
+
*/
|
|
178
|
+
function spriteBitFields(suffix: string): RegBitsField[] {
|
|
179
|
+
const fields: RegBitsField[] = [];
|
|
180
|
+
for (let n = 0; n < 8; n++) {
|
|
181
|
+
fields.push({
|
|
182
|
+
mask: 1 << n,
|
|
183
|
+
shift: n,
|
|
184
|
+
name: `SPR${n}${suffix}`,
|
|
185
|
+
kind: "flag",
|
|
186
|
+
tokens: { 0: "", 1: `SPR${n}${suffix}` },
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return fields;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// OVERRIDES -- every entry carries its own WHY comment immediately above it
|
|
194
|
+
// (anno-regbits.test.ts's own non-vacuity check counts these two things
|
|
195
|
+
// against each other, so removing a comment without removing its entry, or
|
|
196
|
+
// vice versa, fails a test rather than silently drifting).
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
export const OVERRIDES: readonly RegbitsRegisterOverride[] = [
|
|
200
|
+
{
|
|
201
|
+
address: 53265, // $D011 -- VIC Control Register 1, this phase's pinned criterion-3 target
|
|
202
|
+
fields: [
|
|
203
|
+
// WHY: memmap's own prose ("Smooth Scroll to Y Dot-Position (0-7)") reads as a sentence, not
|
|
204
|
+
// a compact field name; mechanical derivation would bake the whole sentence into the
|
|
205
|
+
// identifier instead of the short numeric field the pinned criterion-3 target needs
|
|
206
|
+
// (YSCROLL3, not SMOOTH_SCROLL_TO_Y_DOT_POSITION3).
|
|
207
|
+
{ bit: "2-0", name: "YSCROLL", kind: "numeric" },
|
|
208
|
+
// WHY: OCR damage -- "O = Blank" uses a letter O for the digit 0, and the raw prose reads as
|
|
209
|
+
// a NEGATIVE ("0 = Blank" the screen, i.e. bit=0 blanks it) rather than the positive
|
|
210
|
+
// "is the screen on" framing this table wants. This override fixes both problems in one
|
|
211
|
+
// place: the OCR-damaged letter, and the polarity, landing on the pinned SCREENON name.
|
|
212
|
+
{ bit: "4", name: "SCREENON", kind: "flag", tokens: { 0: "SCREENOFF", 1: "SCREENON" } },
|
|
213
|
+
// WHY: memmap's prose ("Select 24/25 Row Text Display: 1 = 25 Rows") mixes the field name
|
|
214
|
+
// with its own value table; this override extracts the clean ROW24/ROW25 pair the pinned
|
|
215
|
+
// criterion-3 target needs instead of a mechanical transcription of the whole sentence.
|
|
216
|
+
{ bit: "3", name: "ROWS", kind: "flag", tokens: { 0: "ROW24", 1: "ROW25" } },
|
|
217
|
+
// WHY: same shape as ROWS above -- "Bit Map Mode. 1 = Enable" is this register's other
|
|
218
|
+
// genuinely-binary choice (text vs. bitmap), and both states are equally meaningful, so both
|
|
219
|
+
// get a name rather than one state being silent.
|
|
220
|
+
{ bit: "5", name: "MODE", kind: "flag", tokens: { 0: "TEXT", 1: "BITMAP" } },
|
|
221
|
+
// WHY: Extended Color Mode is off in the overwhelming majority of real programs' writes to
|
|
222
|
+
// this register; naming only the "on" state (silent when clear, via an explicit empty-string
|
|
223
|
+
// token for 0 -- never an absent key) keeps a typical generated variant name short rather
|
|
224
|
+
// than always appending "_ECMOFF" to every single value.
|
|
225
|
+
{ bit: "6", name: "ECM", kind: "flag", tokens: { 0: "", 1: "ECM" } },
|
|
226
|
+
// WHY: same reasoning as ECM -- bit 8 of the raster compare value is only interesting when
|
|
227
|
+
// set (a compare line past 255), so the clear state is silent by design, not a dropped token.
|
|
228
|
+
{ bit: "7", name: "RST8", kind: "flag", tokens: { 0: "", 1: "RST8" } },
|
|
229
|
+
],
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
// WHY: OCR damage -- memmap's own label reads "Read NMls" (a lowercase L in place of an
|
|
233
|
+
// uppercase I). This is the register's LABEL, not a field name, so it is fixed here rather
|
|
234
|
+
// than via a per-bit override.
|
|
235
|
+
address: 56589, // $DD0D -- CIA2 Interrupt Control Register
|
|
236
|
+
label: "CIA Interrupt Control Register (Read NMIs/Write Mask)",
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
// WHY: memmap.json's `io` parser produced no `bits` entry at all for this address (D-22's
|
|
240
|
+
// named gap) even though a real game writes to it constantly -- sprite enable, one flag bit
|
|
241
|
+
// per sprite, is trivially regular and does not need memmap's own prose to describe correctly.
|
|
242
|
+
address: 53269, // $D015 -- Sprite Enable
|
|
243
|
+
label: "Sprite Enable",
|
|
244
|
+
synthetic: spriteBitFields("EN"),
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
// WHY: same D-22 gap as $D015 -- Sprite Y-Expand, one flag bit per sprite.
|
|
248
|
+
address: 53271, // $D017 -- Sprite Y-Expand
|
|
249
|
+
label: "Sprite Y-Expand",
|
|
250
|
+
synthetic: spriteBitFields("YEXP"),
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
// WHY: memmap.json's `io` parser produced no `bits` entry for the VIC IRQ mask register at
|
|
254
|
+
// all -- distinct from the VIC Interrupt FLAG register at $D019, which memmap.json does cover.
|
|
255
|
+
// Named per-bit since each bit enables a DIFFERENT interrupt source, not a repeated pattern.
|
|
256
|
+
address: 53274, // $D01A -- VIC Interrupt Enable (mask) Register
|
|
257
|
+
label: "VIC Interrupt Enable Register",
|
|
258
|
+
synthetic: [
|
|
259
|
+
{ mask: 0x01, shift: 0, name: "RSTIRQEN", kind: "numeric" },
|
|
260
|
+
{ mask: 0x02, shift: 1, name: "SPRBGIRQEN", kind: "numeric" },
|
|
261
|
+
{ mask: 0x04, shift: 2, name: "SPRSPRIRQEN", kind: "numeric" },
|
|
262
|
+
{ mask: 0x08, shift: 3, name: "LPIRQEN", kind: "numeric" },
|
|
263
|
+
{ mask: 0xf0, shift: 4, name: "IRQENUNUSED", kind: "numeric" },
|
|
264
|
+
],
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
// WHY: same D-22 gap as $D015 -- Sprite Priority (behind/in-front of background), one flag bit
|
|
268
|
+
// per sprite.
|
|
269
|
+
address: 53275, // $D01B -- Sprite Data Priority
|
|
270
|
+
label: "Sprite Data Priority",
|
|
271
|
+
synthetic: spriteBitFields("BG"),
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
// WHY: same D-22 gap as $D015 -- Sprite Multicolor, one flag bit per sprite.
|
|
275
|
+
address: 53276, // $D01C -- Sprite Multicolor
|
|
276
|
+
label: "Sprite Multicolor",
|
|
277
|
+
synthetic: spriteBitFields("MC"),
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
// WHY: same D-22 gap as $D015 -- Sprite X-Expand, one flag bit per sprite.
|
|
281
|
+
address: 53277, // $D01D -- Sprite X-Expand
|
|
282
|
+
label: "Sprite X-Expand",
|
|
283
|
+
synthetic: spriteBitFields("XEXP"),
|
|
284
|
+
},
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
function findOverride(address: number): RegbitsRegisterOverride | undefined {
|
|
288
|
+
return OVERRIDES.find((o) => o.address === address);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Builds the field list for one memmap entry, consulting `override` for
|
|
293
|
+
* per-bit name/kind/tokens, falling back to mechanical derivation, and
|
|
294
|
+
* THROWING when neither succeeds (see this module's header). Also
|
|
295
|
+
* deduplicates overlapping bit ranges within a single entry -- two memmap
|
|
296
|
+
* "io" registers (`$DC00`/`$DC01`, the joystick/keyboard data ports) list
|
|
297
|
+
* multiple ALTERNATE readings of the very same bits (e.g. "7-0: keyboard
|
|
298
|
+
* column" and, separately, "4: joystick fire" -- the same physical bits
|
|
299
|
+
* read two different ways depending on what is plugged in), which would
|
|
300
|
+
* otherwise double-count those bits into two overlapping fields. First
|
|
301
|
+
* claim wins (array order); a later entry whose mask intersects an
|
|
302
|
+
* already-claimed one is skipped, never merged or thrown on.
|
|
303
|
+
*/
|
|
304
|
+
function buildFieldsForEntry(entry: MemmapEntry, override: RegbitsRegisterOverride | undefined): RegBitsField[] {
|
|
305
|
+
const claimed: RegBitsField[] = [];
|
|
306
|
+
for (const b of entry.bits ?? []) {
|
|
307
|
+
const { mask, shift } = parseBitRange(b.bit);
|
|
308
|
+
if (claimed.some((f) => (f.mask & mask) !== 0)) continue;
|
|
309
|
+
|
|
310
|
+
const fo = override?.fields?.find((f) => f.bit === b.bit);
|
|
311
|
+
if (fo) {
|
|
312
|
+
const field: RegBitsField = { mask, shift, name: fo.name, kind: fo.kind };
|
|
313
|
+
if (fo.tokens) field.tokens = fo.tokens;
|
|
314
|
+
claimed.push(field);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const name = deriveIdentifier(b.desc);
|
|
319
|
+
if (name === null) {
|
|
320
|
+
throw new Error(
|
|
321
|
+
`buildRegBits: address $${entry.start.toString(16).toUpperCase().padStart(4, "0")} bit "${b.bit}" ` +
|
|
322
|
+
`desc "${b.desc}" did not mechanically derive a legal ACME identifier, and no OVERRIDES entry ` +
|
|
323
|
+
`covers it -- add a fieldOverride for { address: ${entry.start}, bit: "${b.bit}" }.`,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
claimed.push({ mask, shift, name, kind: "numeric" });
|
|
327
|
+
}
|
|
328
|
+
claimed.sort((a, b) => a.shift - b.shift);
|
|
329
|
+
return claimed;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function formatAddressKey(address: number): string {
|
|
333
|
+
return `$${address.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Reads memmap.json, takes every entry with a `bits` array (29 as of this
|
|
338
|
+
* writing), normalises each field, and returns the curated table keyed by
|
|
339
|
+
* `$XXXX` address string, sorted so the result -- and the JSON this module
|
|
340
|
+
* emits from it -- is diff-stable. Synthetic (memmap-absent) registers named
|
|
341
|
+
* in `OVERRIDES` are added afterward, only when memmap produced no entry for
|
|
342
|
+
* that address.
|
|
343
|
+
*/
|
|
344
|
+
export function buildRegBits(): RegBitsTable {
|
|
345
|
+
const memmap = JSON.parse(readFileSync(MEMMAP_PATH, "utf8")) as MemmapFile;
|
|
346
|
+
const table: RegBitsTable = {};
|
|
347
|
+
|
|
348
|
+
for (const entry of memmap.entries) {
|
|
349
|
+
if (!Array.isArray(entry.bits) || entry.bits.length === 0) continue;
|
|
350
|
+
const address = entry.start;
|
|
351
|
+
const override = findOverride(address);
|
|
352
|
+
const key = formatAddressKey(address);
|
|
353
|
+
const label = override?.label ?? entry.label ?? entry.desc ?? key;
|
|
354
|
+
const fields = buildFieldsForEntry(entry, override);
|
|
355
|
+
table[key] = { label, fields };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
for (const override of OVERRIDES) {
|
|
359
|
+
if (!override.synthetic) continue;
|
|
360
|
+
const key = formatAddressKey(override.address);
|
|
361
|
+
if (table[key]) continue; // memmap already produced this address -- synthetic is a fallback only
|
|
362
|
+
table[key] = {
|
|
363
|
+
label: override.label ?? key,
|
|
364
|
+
fields: [...override.synthetic].sort((a, b) => a.shift - b.shift),
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const sorted: RegBitsTable = {};
|
|
369
|
+
for (const key of Object.keys(table).sort()) {
|
|
370
|
+
sorted[key] = table[key]!;
|
|
371
|
+
}
|
|
372
|
+
return sorted;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** SHA-256 hex digest of memmap.json's current bytes -- the banner's own
|
|
376
|
+
* drift-pin (T-11-GEN-DRIFT). */
|
|
377
|
+
export function memmapSha256(): string {
|
|
378
|
+
return createHash("sha256").update(readFileSync(MEMMAP_PATH)).digest("hex");
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export interface RegBitsBanner {
|
|
382
|
+
generator: string;
|
|
383
|
+
memmapSha256: string;
|
|
384
|
+
warning: string;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export interface RegBitsDocument {
|
|
388
|
+
_generated: RegBitsBanner;
|
|
389
|
+
[key: string]: unknown;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Wraps `buildRegBits()`'s table with the first-key banner (generator
|
|
394
|
+
* filename, memmap.json's own digest, a do-not-hand-edit warning) --
|
|
395
|
+
* deliberately no timestamp, so two runs against the same memmap.json bytes
|
|
396
|
+
* produce byte-identical output and the drift guard's comparison stays
|
|
397
|
+
* total.
|
|
398
|
+
*/
|
|
399
|
+
export function buildRegBitsDocument(): RegBitsDocument {
|
|
400
|
+
const table = buildRegBits();
|
|
401
|
+
const doc: RegBitsDocument = {
|
|
402
|
+
_generated: {
|
|
403
|
+
generator: "anno-regbits-gen.ts",
|
|
404
|
+
memmapSha256: memmapSha256(),
|
|
405
|
+
warning: "GENERATED FILE -- do not hand-edit. Regenerate via `node anno-regbits-gen.ts` from src/mcp/vice.",
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
for (const key of Object.keys(table).sort()) {
|
|
409
|
+
doc[key] = table[key];
|
|
410
|
+
}
|
|
411
|
+
return doc;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Run-as-script: regenerate the committed artifact. Guarded so importing this
|
|
415
|
+
// module (e.g. from anno-regbits.test.ts) never has a write side effect.
|
|
416
|
+
const isMain = process.argv[1] !== undefined && pathToFileURL(process.argv[1]).href === import.meta.url;
|
|
417
|
+
if (isMain) {
|
|
418
|
+
const doc = buildRegBitsDocument();
|
|
419
|
+
writeFileSync(OUTPUT_PATH, `${JSON.stringify(doc, null, 2)}\n`);
|
|
420
|
+
console.log(`anno-regbits-gen: wrote ${OUTPUT_PATH} (${Object.keys(doc).length - 1} registers)`);
|
|
421
|
+
}
|