@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
package/r2000-enum-gen.ts
DELETED
|
@@ -1,574 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// r2000-enum-gen.ts -- the ONE authoritative place in this repo for value ->
|
|
3
|
-
// variant naming, the adjacent-pair pass, identifier sanitization, enum
|
|
4
|
-
// installation and the coverage report (D-20/D-22/D-23, R2000-13, criterion
|
|
5
|
-
// 3 -- the phase's most distinctive deliverable: neither this project nor
|
|
6
|
-
// regenerator2000 can produce it alone).
|
|
7
|
-
//
|
|
8
|
-
// MEASURED MECHANISM FACTS (all confirmed by direct live calls against a
|
|
9
|
-
// real regenerator2000 0.9.20 child on this host, not merely paraphrased
|
|
10
|
-
// from RESEARCH.md):
|
|
11
|
-
// - `EnumDefinition.variants` is a flat `BTreeMap<u16, String>` -- a plain
|
|
12
|
-
// value-to-name map. There is NO bit-OR composition anywhere in 0.9.20.
|
|
13
|
-
// - `r2000_apply_enum_usage` binds to the INSTRUCTION ADDRESS holding the
|
|
14
|
-
// immediate operand (the `lda`, never the `sta`) -- confirmed both by
|
|
15
|
-
// direct call and by `handler.rs:1236-1264`'s own description text.
|
|
16
|
-
// - Applying an enum emits its WHOLE variant list into the exported ACME
|
|
17
|
-
// header; an unmatched value falls back to bare `#$xx` while the dead
|
|
18
|
-
// definitions are still emitted. This is exactly why D-20 generates one
|
|
19
|
-
// variant per value the program actually writes, never a full
|
|
20
|
-
// 256-values-per-register table.
|
|
21
|
-
// - `r2000_create_project_enum` FAILS with "Enum '<name>' already exists"
|
|
22
|
-
// (`app_state.rs:443-457`'s `validate_new_enum_name`) if the name is
|
|
23
|
-
// already taken -- there is no upsert. This module's own precedence
|
|
24
|
-
// (documented at `createOrUpdateEnum()` below): try create first, and
|
|
25
|
-
// ONLY on an "already exists" failure fall back to
|
|
26
|
-
// `r2000_update_project_enum`, which replaces the variant map wholesale
|
|
27
|
-
// (R2000-13's own "re-runnable" requirement).
|
|
28
|
-
// - `r2000_search_disassembly` matches its `query` regex against the
|
|
29
|
-
// `mnemonic` and `operand` fields INDEPENDENTLY (`state/search.rs:
|
|
30
|
-
// 309-313`, `text_matches(&line.mnemonic, ...)` OR
|
|
31
|
-
// `text_matches(&line.operand, ...)`) -- they are NEVER concatenated
|
|
32
|
-
// into one searchable string. This corrects RESEARCH.md's own Pattern 2
|
|
33
|
-
// code example, which assumed a combined `"^sta \$(...)"`-shaped query
|
|
34
|
-
// would match a "mnemonic + operand" string; measured live, it does not
|
|
35
|
-
// (a query is applied to `mnemonic` OR `operand`, so a combined pattern
|
|
36
|
-
// never matches either field alone). This module instead queries the
|
|
37
|
-
// MNEMONIC exactly (`"^lda$"` / `"^sta$"`, case-insensitive per the
|
|
38
|
-
// server's own `(?i)` prefix) and does the register/immediate-mode
|
|
39
|
-
// narrowing CLIENT-SIDE against this project's own curated register set
|
|
40
|
-
// -- still derived from `r2000-regbits.json`'s own keys, never a second
|
|
41
|
-
// hardcoded list, exactly as D-23 requires; only the MECHANISM by which
|
|
42
|
-
// that narrowing happens changed from "one combined regex" to "two exact
|
|
43
|
-
// mnemonic queries plus a client-side operand filter".
|
|
44
|
-
// - `r2000_search_disassembly`'s `max_results` server-side default is 50
|
|
45
|
-
// (`handler.rs:1074-1077`) -- always pass an explicit value on this
|
|
46
|
-
// surface (D-23's "no silent caps": the returned count is compared
|
|
47
|
-
// against the requested ceiling and a truncation signal is reported in
|
|
48
|
-
// words, never left to be inferred).
|
|
49
|
-
//
|
|
50
|
-
// WHAT NOT TO DO, named concretely:
|
|
51
|
-
// - Never assert criterion 3 against `r2000_search_disassembly`'s own
|
|
52
|
-
// rendered operand text. Measured discrepancy (RESEARCH.md, confirmed
|
|
53
|
-
// unchanged this session): the live query view renders an applied enum
|
|
54
|
-
// reference as `EnumName.VARIANT` (a dot), while the ACME export
|
|
55
|
-
// (`--export_asm`) renders `EnumName_VARIANT` (an underscore) -- exactly
|
|
56
|
-
// what criterion 3's own wording quotes. This finding is VERSION-SCOPED
|
|
57
|
-
// to regenerator2000 0.9.20 (RESEARCH.md Assumption A2) -- re-verify
|
|
58
|
-
// against `--export_asm` at execution time rather than trusting it as
|
|
59
|
-
// permanent; Task 3's acceptance test records what it observes on this
|
|
60
|
-
// run rather than assuming the historical finding still holds.
|
|
61
|
-
// - Never omit `max_results` on a `r2000_search_disassembly` call in this
|
|
62
|
-
// module. Every call site below passes it explicitly.
|
|
63
|
-
// - Never call `r2000_create_project_enum` (or `_update_`) with an
|
|
64
|
-
// unsanitized identifier. `assertLegalAcmeIdentifier()` (now defined in
|
|
65
|
-
// `r2000-acme-ident.ts`, re-exported here) runs on the enum name AND
|
|
66
|
-
// every variant name inside `sanitizeVariantMap()`, which is called
|
|
67
|
-
// BEFORE `createOrUpdateEnum()` ever reaches `runR2000Tool()` -- proven
|
|
68
|
-
// zero-spawn in `r2000-enum-gen.test.ts` via a spy binary.
|
|
69
|
-
// - Never write a machine-global enum. Every call in this module goes
|
|
70
|
-
// through `runR2000Tool()` (`r2000-tools.ts`, plan 11-05), which only
|
|
71
|
-
// knows `r2000_create_project_enum`/`r2000_update_project_enum` -- the
|
|
72
|
-
// machine-wide config-dir save route named in D-21 is never referenced
|
|
73
|
-
// anywhere in this file (asserted mechanically by
|
|
74
|
-
// `r2000-enum-gen.test.ts`'s own zero-count grep).
|
|
75
|
-
// - Never call `r2000-mcp-client.ts` directly. Every child interaction in
|
|
76
|
-
// this module goes through `r2000-tools.ts`'s `runR2000Tool()`, so the
|
|
77
|
-
// curated allow-list gate and the per-call auto-save both apply for
|
|
78
|
-
// free, exactly as plan 11-05's own "Next Phase Readiness" note
|
|
79
|
-
// instructs.
|
|
80
|
-
import { readFileSync } from "node:fs";
|
|
81
|
-
import { dirname, join } from "node:path";
|
|
82
|
-
import { fileURLToPath } from "node:url";
|
|
83
|
-
|
|
84
|
-
import { runR2000Tool } from "./r2000-tools.ts";
|
|
85
|
-
import type { RegBitsField, RegBitsTable } from "./r2000-regbits-gen.ts";
|
|
86
|
-
import { MAX_ACME_IDENTIFIER_LENGTH, assertLegalAcmeIdentifier } from "./r2000-acme-ident.ts";
|
|
87
|
-
|
|
88
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
89
|
-
const REGBITS_PATH = join(HERE, "r2000-regbits.json");
|
|
90
|
-
|
|
91
|
-
/** The server-side default (`handler.rs:1074-1077`) this surface's own
|
|
92
|
-
* `runR2000Tool()` wrapper REQUIRES an explicit override for -- named here
|
|
93
|
-
* so every call site in this module states its ceiling instead of trusting
|
|
94
|
-
* the child's own default. */
|
|
95
|
-
export const DEFAULT_MAX_RESULTS = 10_000;
|
|
96
|
-
|
|
97
|
-
// MAX_ACME_IDENTIFIER_LENGTH / assertLegalAcmeIdentifier() now live in
|
|
98
|
-
// r2000-acme-ident.ts (plan 260821-a86, T-11-NAME-INJECT) -- that module is
|
|
99
|
-
// the ONE authoritative place for the ACME identifier policy, consumed by
|
|
100
|
-
// THIS file's createOrUpdateEnum()/sanitizeVariantMap() below plus two more
|
|
101
|
-
// entry routes (r2000-tools.ts's r2000_set_label_name, r2000-symbols.ts's
|
|
102
|
-
// importLabels()) that could not import it from here without forming a
|
|
103
|
-
// cycle (this file statically imports runR2000Tool FROM r2000-tools.ts).
|
|
104
|
-
// Re-exported here (imported above) so this file's own existing
|
|
105
|
-
// consumers/tests keep their current import path.
|
|
106
|
-
export { MAX_ACME_IDENTIFIER_LENGTH, assertLegalAcmeIdentifier };
|
|
107
|
-
|
|
108
|
-
// ---------------------------------------------------------------------------
|
|
109
|
-
// The bit-name table (Task 1) -- loaded once, from the committed generated
|
|
110
|
-
// artifact, never re-derived from memmap.json at runtime.
|
|
111
|
-
// ---------------------------------------------------------------------------
|
|
112
|
-
|
|
113
|
-
let cachedTable: RegBitsTable | undefined;
|
|
114
|
-
|
|
115
|
-
function loadRegBits(): RegBitsTable {
|
|
116
|
-
if (cachedTable) return cachedTable;
|
|
117
|
-
const doc = JSON.parse(readFileSync(REGBITS_PATH, "utf8")) as Record<string, unknown>;
|
|
118
|
-
const { _generated, ...table } = doc;
|
|
119
|
-
cachedTable = table as unknown as RegBitsTable;
|
|
120
|
-
return cachedTable;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** Test-only reset, so a test can install a synthetic table without this
|
|
124
|
-
* module's cache surviving across cases. Not exported for production use. */
|
|
125
|
-
export function __resetRegBitsCacheForTests(table?: RegBitsTable): void {
|
|
126
|
-
cachedTable = table;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export function registerKeyFor(address: number): string {
|
|
130
|
-
return `$${address.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Decodes `value` against `register`'s fields (from the loaded bit-name
|
|
135
|
-
* table), in ascending bit order, emitting one token per field:
|
|
136
|
-
* - a "numeric" field ALWAYS emits `NAME` concatenated with the decoded
|
|
137
|
-
* number (e.g. `YSCROLL` + `3` = `YSCROLL3`) -- total by construction,
|
|
138
|
-
* nothing to look up;
|
|
139
|
-
* - a "flag"/"enum" field emits its own `tokens[decoded]` string. This
|
|
140
|
-
* table's own fields (Task 1) give EVERY flag/enum field an EXPLICIT
|
|
141
|
-
* token for every value it can take -- including an explicit EMPTY
|
|
142
|
-
* STRING for a state that is silent by design (e.g. `$D011`'s ECM/RST8,
|
|
143
|
-
* silent when clear) -- so "no token defined" is a genuine data error,
|
|
144
|
-
* never an expected shape. When it happens anyway, this function
|
|
145
|
-
* REFUSES (throws), naming the register/field/value, rather than
|
|
146
|
-
* silently dropping the field: a dropped token could make two distinct
|
|
147
|
-
* register values decode to the identical name, which is exactly the
|
|
148
|
-
* property `r2000-enum-gen.test.ts`'s 256-value check exists to catch.
|
|
149
|
-
* - an empty-string token contributes NOTHING to the joined name (it is
|
|
150
|
-
* filtered out before the final `_`-join) -- this is what makes the
|
|
151
|
-
* silent-by-design case above actually silent in the output.
|
|
152
|
-
*
|
|
153
|
-
* The measured target this function is pinned against:
|
|
154
|
-
* `variantNameFor(0xd011, 0x1b) === "YSCROLL3_ROW25_SCREENON_TEXT"`.
|
|
155
|
-
*/
|
|
156
|
-
export function variantNameFor(register: number, value: number): string {
|
|
157
|
-
const table = loadRegBits();
|
|
158
|
-
const key = registerKeyFor(register);
|
|
159
|
-
const entry = table[key];
|
|
160
|
-
if (!entry) {
|
|
161
|
-
throw new Error(
|
|
162
|
-
`variantNameFor: no bit-name table entry for register ${key} -- r2000-regbits.json has no fields ` +
|
|
163
|
-
"for this address (add an OVERRIDES entry in r2000-regbits-gen.ts, or exclude it from generation).",
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const tokens: string[] = [];
|
|
168
|
-
for (const field of entry.fields as RegBitsField[]) {
|
|
169
|
-
const decoded = (value & field.mask) >>> field.shift;
|
|
170
|
-
if (field.kind === "numeric") {
|
|
171
|
-
tokens.push(`${field.name}${decoded}`);
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
const token = field.tokens?.[decoded];
|
|
175
|
-
if (token === undefined) {
|
|
176
|
-
throw new Error(
|
|
177
|
-
`variantNameFor: register ${key} field "${field.name}" (kind ${field.kind}) has no token for decoded ` +
|
|
178
|
-
`value ${decoded} (full register value 0x${value.toString(16)}) -- refusing rather than silently ` +
|
|
179
|
-
"dropping a field, which could make two distinct register values decode to the same name.",
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
if (token !== "") tokens.push(token);
|
|
183
|
-
}
|
|
184
|
-
if (tokens.length === 0) {
|
|
185
|
-
// Every field decoded to an explicitly-silent token (e.g. all eight
|
|
186
|
-
// sprite-plane flags clear at once) -- the only way this can happen is a
|
|
187
|
-
// register whose EVERY field is a flag/enum with a silent-by-design
|
|
188
|
-
// state, at the one value where every field lands on that state. An
|
|
189
|
-
// empty string is not a legal ACME identifier, so this is not "no
|
|
190
|
-
// change needed", it is the single degenerate case this table's design
|
|
191
|
-
// creates -- named explicitly (`V<value>`) rather than left empty. Since
|
|
192
|
-
// a numeric field always emits a non-empty token, this fallback can only
|
|
193
|
-
// ever fire for AT MOST one value per register (the all-fields-silent
|
|
194
|
-
// one), so it can never collide with a genuine multi-token name.
|
|
195
|
-
return `V${value}`;
|
|
196
|
-
}
|
|
197
|
-
return tokens.join("_");
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// ---------------------------------------------------------------------------
|
|
201
|
-
// The two-pass search + adjacent-pair (D-23).
|
|
202
|
-
// ---------------------------------------------------------------------------
|
|
203
|
-
|
|
204
|
-
export interface DisasmSearchRow {
|
|
205
|
-
address: string;
|
|
206
|
-
address_decimal: number;
|
|
207
|
-
label: string;
|
|
208
|
-
mnemonic: string;
|
|
209
|
-
operand: string;
|
|
210
|
-
comment: string;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/** Parses one `r2000_search_disassembly` `ToolCallResult` into its rows,
|
|
214
|
-
* throwing (naming `what`) on a reported `isError` or an unparsable body --
|
|
215
|
-
* never silently treating either as "no rows". */
|
|
216
|
-
async function parseSearchRows(
|
|
217
|
-
resultPromise: ReturnType<typeof runR2000Tool>,
|
|
218
|
-
what: string,
|
|
219
|
-
): Promise<DisasmSearchRow[]> {
|
|
220
|
-
const result = await resultPromise;
|
|
221
|
-
const text = result.content.map((c) => c.text).join("");
|
|
222
|
-
if (result.isError) {
|
|
223
|
-
throw new Error(`${what} failed: ${text}`);
|
|
224
|
-
}
|
|
225
|
-
let rows: unknown;
|
|
226
|
-
try {
|
|
227
|
-
rows = JSON.parse(text);
|
|
228
|
-
} catch (err) {
|
|
229
|
-
throw new Error(`${what}: could not parse response as JSON (${err instanceof Error ? err.message : String(err)}): ${text}`);
|
|
230
|
-
}
|
|
231
|
-
if (!Array.isArray(rows)) {
|
|
232
|
-
throw new Error(`${what}: expected an array of rows, got ${typeof rows}`);
|
|
233
|
-
}
|
|
234
|
-
return rows as DisasmSearchRow[];
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/** Parses an ACME-style immediate operand string (`"#$1b"`, `"#42"`,
|
|
238
|
-
* `"#%00011011"`) into its numeric value. Throws on anything else, naming
|
|
239
|
-
* the offending operand text -- never silently returns 0 for an
|
|
240
|
-
* unparsable operand, which would misname a variant. */
|
|
241
|
-
export function parseImmediateOperand(operand: string): number {
|
|
242
|
-
if (!operand.startsWith("#")) {
|
|
243
|
-
throw new Error(`parseImmediateOperand: "${operand}" is not an immediate operand (does not start with "#")`);
|
|
244
|
-
}
|
|
245
|
-
const body = operand.slice(1);
|
|
246
|
-
let value: number;
|
|
247
|
-
if (body.startsWith("$")) {
|
|
248
|
-
value = Number.parseInt(body.slice(1), 16);
|
|
249
|
-
} else if (body.startsWith("%")) {
|
|
250
|
-
value = Number.parseInt(body.slice(1), 2);
|
|
251
|
-
} else {
|
|
252
|
-
value = Number.parseInt(body, 10);
|
|
253
|
-
}
|
|
254
|
-
if (!Number.isInteger(value) || Number.isNaN(value)) {
|
|
255
|
-
throw new Error(`parseImmediateOperand: could not parse "${operand}" as a numeric immediate value`);
|
|
256
|
-
}
|
|
257
|
-
return value;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/** Normalises a store's operand text (`"$d011"`) into the same `$xxxx`
|
|
261
|
-
* (uppercase, no padding assumptions beyond what the server itself emits)
|
|
262
|
-
* shape used as this module's own register-lookup key, so the two never
|
|
263
|
-
* silently fail to match on case alone. */
|
|
264
|
-
function normalizeOperandAsKey(operand: string): string | null {
|
|
265
|
-
if (!operand.startsWith("$")) return null;
|
|
266
|
-
const hex = operand.slice(1);
|
|
267
|
-
if (!/^[0-9a-fA-F]+$/.test(hex)) return null;
|
|
268
|
-
return `$${hex.toUpperCase().padStart(4, "0")}`;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
export interface PairOccurrence {
|
|
272
|
-
regKey: string;
|
|
273
|
-
value: number;
|
|
274
|
-
ldaAddr: number;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
export interface PairingResult {
|
|
278
|
-
occurrences: PairOccurrence[];
|
|
279
|
-
totalRegisterStores: number;
|
|
280
|
-
pairedStores: number;
|
|
281
|
-
unpairedStores: number;
|
|
282
|
-
pass1Truncated: boolean;
|
|
283
|
-
pass2Truncated: boolean;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Runs the two-pass search (all `lda` instructions, all `sta` instructions --
|
|
288
|
-
* queried by EXACT mnemonic match, per this module's own measured correction
|
|
289
|
-
* to RESEARCH.md's combined-regex assumption, see header comment) and pairs
|
|
290
|
-
* each store to a register this module's bit-name table knows with an
|
|
291
|
-
* immediate load exactly 2 bytes earlier (D-23: adjacent-only, no
|
|
292
|
-
* dataflow -- `lda #imm` is always 2 bytes in immediate mode, so the
|
|
293
|
-
* following store begins at `ldaAddr + 2` regardless of the store's own
|
|
294
|
-
* addressing mode).
|
|
295
|
-
*/
|
|
296
|
-
export async function pairImmediateLoadsToStores(
|
|
297
|
-
projectPath: string,
|
|
298
|
-
maxResults: number = DEFAULT_MAX_RESULTS,
|
|
299
|
-
): Promise<PairingResult> {
|
|
300
|
-
const table = loadRegBits();
|
|
301
|
-
const knownRegisters = new Set(Object.keys(table));
|
|
302
|
-
|
|
303
|
-
const ldaRows = await parseSearchRows(
|
|
304
|
-
runR2000Tool("r2000_search_disassembly", {
|
|
305
|
-
project: projectPath,
|
|
306
|
-
query: "^lda$",
|
|
307
|
-
use_regex: true,
|
|
308
|
-
max_results: maxResults,
|
|
309
|
-
search_labels: false,
|
|
310
|
-
search_comments: false,
|
|
311
|
-
search_instructions: true,
|
|
312
|
-
}),
|
|
313
|
-
"r2000_search_disassembly (pass 1: lda)",
|
|
314
|
-
);
|
|
315
|
-
const pass1Truncated = ldaRows.length === maxResults;
|
|
316
|
-
|
|
317
|
-
const staRows = await parseSearchRows(
|
|
318
|
-
runR2000Tool("r2000_search_disassembly", {
|
|
319
|
-
project: projectPath,
|
|
320
|
-
query: "^sta$",
|
|
321
|
-
use_regex: true,
|
|
322
|
-
max_results: maxResults,
|
|
323
|
-
search_labels: false,
|
|
324
|
-
search_comments: false,
|
|
325
|
-
search_instructions: true,
|
|
326
|
-
}),
|
|
327
|
-
"r2000_search_disassembly (pass 2: sta)",
|
|
328
|
-
);
|
|
329
|
-
const pass2Truncated = staRows.length === maxResults;
|
|
330
|
-
|
|
331
|
-
const immByAddr = new Map<number, number>();
|
|
332
|
-
for (const row of ldaRows) {
|
|
333
|
-
if (!row.operand.startsWith("#")) continue; // not an immediate load
|
|
334
|
-
try {
|
|
335
|
-
immByAddr.set(row.address_decimal, parseImmediateOperand(row.operand));
|
|
336
|
-
} catch {
|
|
337
|
-
// An unparsable immediate operand is skipped (never paired), not fatal
|
|
338
|
-
// to the whole pass -- D-23's "a miss costs nothing" posture.
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const knownStores = staRows.filter((row) => {
|
|
343
|
-
const key = normalizeOperandAsKey(row.operand);
|
|
344
|
-
return key !== null && knownRegisters.has(key);
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
const occurrences: PairOccurrence[] = [];
|
|
348
|
-
for (const store of knownStores) {
|
|
349
|
-
const regKey = normalizeOperandAsKey(store.operand)!;
|
|
350
|
-
const ldaAddr = store.address_decimal - 2;
|
|
351
|
-
const imm = immByAddr.get(ldaAddr);
|
|
352
|
-
if (imm === undefined) continue; // D-23: adjacent-only -- a miss costs nothing
|
|
353
|
-
occurrences.push({ regKey, value: imm, ldaAddr });
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
return {
|
|
357
|
-
occurrences,
|
|
358
|
-
totalRegisterStores: knownStores.length,
|
|
359
|
-
pairedStores: occurrences.length,
|
|
360
|
-
unpairedStores: knownStores.length - occurrences.length,
|
|
361
|
-
pass1Truncated,
|
|
362
|
-
pass2Truncated,
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// ---------------------------------------------------------------------------
|
|
367
|
-
// Enum installation (D-20/D-21/D-33-adjacent: only through runR2000Tool()).
|
|
368
|
-
// ---------------------------------------------------------------------------
|
|
369
|
-
|
|
370
|
-
/** Formats a numeric value the way `r2000_create_project_enum`'s own
|
|
371
|
-
* `EnumDefinition::parse_variants` accepts (`$`-prefixed lowercase hex),
|
|
372
|
-
* matching the measured example in this phase's own RESEARCH.md exactly. */
|
|
373
|
-
function formatVariantKey(value: number): string {
|
|
374
|
-
return `$${value.toString(16)}`;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* Builds the `{ "$1b": "YSCROLL3_..." }`-shaped variants object for
|
|
379
|
-
* `r2000_create_project_enum`/`_update_`, calling `assertLegalAcmeIdentifier`
|
|
380
|
-
* on every variant name FIRST -- this is the whole reason
|
|
381
|
-
* `createOrUpdateEnum()` below can prove zero child spawns for a rejected
|
|
382
|
-
* name: sanitization happens entirely client-side, before any I/O.
|
|
383
|
-
*/
|
|
384
|
-
export function sanitizeVariantMap(regKey: string, variants: ReadonlyMap<number, string>): Record<string, string> {
|
|
385
|
-
const out: Record<string, string> = {};
|
|
386
|
-
for (const [value, name] of variants) {
|
|
387
|
-
assertLegalAcmeIdentifier(name, `variant name for ${regKey} value 0x${value.toString(16)}`);
|
|
388
|
-
out[formatVariantKey(value)] = name;
|
|
389
|
-
}
|
|
390
|
-
return out;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
export type EnumInstallAction = "created" | "updated";
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
* Creates (or, on an "already exists" failure, updates) the project-level
|
|
397
|
-
* enum named after `regKey` (e.g. `$D011` -> enum name `D011`) with
|
|
398
|
-
* `variants`. Precedence, decided and documented here: CREATE is tried
|
|
399
|
-
* first; only when regenerator2000 itself reports the name already exists
|
|
400
|
-
* (`validate_new_enum_name`'s own message, `app_state.rs:455`) does this
|
|
401
|
-
* function fall back to UPDATE, which replaces the variant map wholesale --
|
|
402
|
-
* this is what makes a re-run of `generateEnums()` idempotent (R2000-13's
|
|
403
|
-
* "re-runnable" requirement) rather than failing on every run after the
|
|
404
|
-
* first.
|
|
405
|
-
*
|
|
406
|
-
* `assertLegalAcmeIdentifier` runs on the enum name and (via
|
|
407
|
-
* `sanitizeVariantMap`) every variant name BEFORE either child call --
|
|
408
|
-
* proven zero-spawn in `r2000-enum-gen.test.ts` via a spy binary.
|
|
409
|
-
*/
|
|
410
|
-
export async function createOrUpdateEnum(
|
|
411
|
-
projectPath: string,
|
|
412
|
-
regKey: string,
|
|
413
|
-
variants: ReadonlyMap<number, string>,
|
|
414
|
-
): Promise<EnumInstallAction> {
|
|
415
|
-
const enumName = regKey.slice(1); // "$D011" -> "D011"
|
|
416
|
-
assertLegalAcmeIdentifier(enumName, `enum name for ${regKey}`);
|
|
417
|
-
const variantsObj = sanitizeVariantMap(regKey, variants);
|
|
418
|
-
|
|
419
|
-
const createResult = await runR2000Tool("r2000_create_project_enum", {
|
|
420
|
-
project: projectPath,
|
|
421
|
-
name: enumName,
|
|
422
|
-
variants: variantsObj,
|
|
423
|
-
});
|
|
424
|
-
if (!createResult.isError) return "created";
|
|
425
|
-
|
|
426
|
-
const createText = createResult.content.map((c) => c.text).join(" ");
|
|
427
|
-
if (!/already exists/i.test(createText)) {
|
|
428
|
-
throw new Error(`r2000_create_project_enum failed for "${enumName}": ${createText}`);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const updateResult = await runR2000Tool("r2000_update_project_enum", {
|
|
432
|
-
project: projectPath,
|
|
433
|
-
name: enumName,
|
|
434
|
-
variants: variantsObj,
|
|
435
|
-
});
|
|
436
|
-
if (updateResult.isError) {
|
|
437
|
-
throw new Error(
|
|
438
|
-
`r2000_update_project_enum failed for "${enumName}" (after create reported already-exists): ` +
|
|
439
|
-
`${updateResult.content.map((c) => c.text).join(" ")}`,
|
|
440
|
-
);
|
|
441
|
-
}
|
|
442
|
-
return "updated";
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
async function applyUsage(projectPath: string, address: number, enumName: string): Promise<void> {
|
|
446
|
-
const result = await runR2000Tool("r2000_apply_enum_usage", {
|
|
447
|
-
project: projectPath,
|
|
448
|
-
address,
|
|
449
|
-
name: enumName,
|
|
450
|
-
});
|
|
451
|
-
if (result.isError) {
|
|
452
|
-
throw new Error(
|
|
453
|
-
`r2000_apply_enum_usage failed at address ${address} for enum "${enumName}": ` +
|
|
454
|
-
`${result.content.map((c) => c.text).join(" ")}`,
|
|
455
|
-
);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// ---------------------------------------------------------------------------
|
|
460
|
-
// generateEnums() -- the whole pass, and its coverage report.
|
|
461
|
-
// ---------------------------------------------------------------------------
|
|
462
|
-
|
|
463
|
-
export interface EnumInstallSummary {
|
|
464
|
-
regKey: string;
|
|
465
|
-
enumName: string;
|
|
466
|
-
variantCount: number;
|
|
467
|
-
action: EnumInstallAction;
|
|
468
|
-
usagesApplied: number;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
export interface EnumGenerationReport {
|
|
472
|
-
totalRegisterStores: number;
|
|
473
|
-
pairedStores: number;
|
|
474
|
-
unpairedStores: number;
|
|
475
|
-
pass1Truncated: boolean;
|
|
476
|
-
pass2Truncated: boolean;
|
|
477
|
-
enums: EnumInstallSummary[];
|
|
478
|
-
/** Human-readable summary lines, always including the word "truncat..." if
|
|
479
|
-
* either pass hit its own `max_results` ceiling (D-23: "no silent caps" --
|
|
480
|
-
* a possible truncation is stated in words, never left to be inferred). */
|
|
481
|
-
summaryLines: string[];
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
export interface GenerateEnumsOptions {
|
|
485
|
-
projectPath: string;
|
|
486
|
-
maxResults?: number;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
/**
|
|
490
|
-
* The whole D-20/D-22/D-23 pass: two exact-mnemonic searches (explicit
|
|
491
|
-
* `max_results`, never the server's own 50-row default), adjacent-only
|
|
492
|
-
* pairing, one variant per DISTINCT value observed per register (D-20),
|
|
493
|
-
* create-or-update per register (this module's own documented precedence),
|
|
494
|
-
* apply-usage at every paired `lda` address (never the store address --
|
|
495
|
-
* measured, `handler.rs:1236-1264`), and a coverage report naming totals,
|
|
496
|
-
* pairing counts and any possible truncation explicitly.
|
|
497
|
-
*
|
|
498
|
-
* Persistence: every mutating call here goes through `runR2000Tool()`
|
|
499
|
-
* (`r2000-tools.ts`), whose own per-call auto-save (D-17) already persists
|
|
500
|
-
* each create/update/apply the instant its own session closes -- see that
|
|
501
|
-
* module's header for why a SECOND, standalone `r2000_save_project` call
|
|
502
|
-
* after a sequence of already-auto-saving calls is not issued here: it would
|
|
503
|
-
* only ever observe an unchanged hash (nothing pending) and report a
|
|
504
|
-
* spurious `R2000SaveNotPersistedError`, which `r2000-tools.ts`'s own header
|
|
505
|
-
* documents as the expected (not buggy) shape of that specific call
|
|
506
|
-
* sequence. Persistence itself is independently proven at the ACME EXPORT
|
|
507
|
-
* layer (Task 3), not re-asserted here.
|
|
508
|
-
*/
|
|
509
|
-
export async function generateEnums({ projectPath, maxResults = DEFAULT_MAX_RESULTS }: GenerateEnumsOptions): Promise<EnumGenerationReport> {
|
|
510
|
-
const pairing = await pairImmediateLoadsToStores(projectPath, maxResults);
|
|
511
|
-
|
|
512
|
-
const byRegister = new Map<string, Map<number, number>>(); // regKey -> value -> representative ldaAddr (first seen)
|
|
513
|
-
const occurrencesByRegister = new Map<string, PairOccurrence[]>();
|
|
514
|
-
for (const occ of pairing.occurrences) {
|
|
515
|
-
if (!byRegister.has(occ.regKey)) byRegister.set(occ.regKey, new Map());
|
|
516
|
-
if (!occurrencesByRegister.has(occ.regKey)) occurrencesByRegister.set(occ.regKey, []);
|
|
517
|
-
byRegister.get(occ.regKey)!.set(occ.value, occ.ldaAddr);
|
|
518
|
-
occurrencesByRegister.get(occ.regKey)!.push(occ);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
const enums: EnumInstallSummary[] = [];
|
|
522
|
-
for (const [regKey, valuesToLdaAddr] of byRegister) {
|
|
523
|
-
const address = Number.parseInt(regKey.slice(1), 16);
|
|
524
|
-
const variants = new Map<number, string>();
|
|
525
|
-
for (const value of valuesToLdaAddr.keys()) {
|
|
526
|
-
variants.set(value, variantNameFor(address, value));
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
const action = await createOrUpdateEnum(projectPath, regKey, variants);
|
|
530
|
-
|
|
531
|
-
const occurrences = occurrencesByRegister.get(regKey) ?? [];
|
|
532
|
-
for (const occ of occurrences) {
|
|
533
|
-
const enumName = regKey.slice(1);
|
|
534
|
-
await applyUsage(projectPath, occ.ldaAddr, enumName);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
enums.push({
|
|
538
|
-
regKey,
|
|
539
|
-
enumName: regKey.slice(1),
|
|
540
|
-
variantCount: variants.size,
|
|
541
|
-
action,
|
|
542
|
-
usagesApplied: occurrences.length,
|
|
543
|
-
});
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
const summaryLines: string[] = [
|
|
547
|
-
`total register stores seen: ${pairing.totalRegisterStores}`,
|
|
548
|
-
`paired (adjacent lda #imm found): ${pairing.pairedStores}`,
|
|
549
|
-
`unpaired (no adjacent immediate load): ${pairing.unpairedStores}`,
|
|
550
|
-
];
|
|
551
|
-
if (pairing.pass1Truncated) {
|
|
552
|
-
summaryLines.push(
|
|
553
|
-
`TRUNCATION WARNING: pass 1 (lda search) returned exactly max_results=${maxResults} rows -- coverage may be incomplete`,
|
|
554
|
-
);
|
|
555
|
-
}
|
|
556
|
-
if (pairing.pass2Truncated) {
|
|
557
|
-
summaryLines.push(
|
|
558
|
-
`TRUNCATION WARNING: pass 2 (sta search) returned exactly max_results=${maxResults} rows -- coverage may be incomplete`,
|
|
559
|
-
);
|
|
560
|
-
}
|
|
561
|
-
for (const e of enums) {
|
|
562
|
-
summaryLines.push(`enum ${e.enumName}: ${e.action}, ${e.variantCount} variant(s), ${e.usagesApplied} usage(s) applied`);
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
return {
|
|
566
|
-
totalRegisterStores: pairing.totalRegisterStores,
|
|
567
|
-
pairedStores: pairing.pairedStores,
|
|
568
|
-
unpairedStores: pairing.unpairedStores,
|
|
569
|
-
pass1Truncated: pairing.pass1Truncated,
|
|
570
|
-
pass2Truncated: pairing.pass2Truncated,
|
|
571
|
-
enums,
|
|
572
|
-
summaryLines,
|
|
573
|
-
};
|
|
574
|
-
}
|