@henols/vice-mcp 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/THIRD-PARTY-NOTICES.md +423 -25
- package/{r2000-acme-ident.ts → anno-acme-ident.ts} +13 -13
- package/anno-bank.ts +171 -0
- package/anno-cli.ts +3040 -0
- package/{r2000-confidence.ts → anno-confidence.ts} +22 -22
- package/anno-coverage.ts +2465 -0
- package/anno-derive.ts +590 -0
- package/anno-details.ts +169 -0
- package/anno-enum-gen.ts +919 -0
- package/anno-export-asm.ts +2396 -0
- package/anno-graphics.ts +338 -0
- package/anno-hazard-report.ts +1367 -0
- package/anno-import.ts +495 -0
- package/anno-index.ts +150 -0
- package/anno-join.ts +480 -0
- package/{r2000-memmap-render.ts → anno-memmap-render.ts} +236 -95
- package/anno-provenance-ledger.ts +472 -0
- package/{r2000-regbits-gen.ts → anno-regbits-gen.ts} +20 -15
- package/{r2000-regbits.json → anno-regbits.json} +2 -2
- package/anno-register.ts +399 -0
- package/anno-store-export.ts +661 -0
- package/anno-store.ts +4002 -0
- package/anno-symbols.ts +266 -0
- package/anno-tools.ts +3264 -0
- package/anno-types.ts +1909 -0
- package/backend-detect.mts +124 -312
- package/block-class.ts +201 -0
- package/build.ts +4 -2
- package/capture-predicate.ts +597 -0
- package/channel-lock.ts +349 -0
- package/disasm-decoder.ts +14 -14
- package/disasm-opcodes.ts +4 -4
- package/disasm-renderer.ts +2 -2
- package/evid-ingest.ts +217 -0
- package/evid-reconcile.ts +316 -0
- package/host-tool-client.ts +430 -0
- package/hostpath.ts +1 -1
- package/incident-record.ts +23 -12
- package/install-resources.ts +30 -14
- package/memmap-lookup.ts +285 -0
- package/package.json +48 -23
- package/prg-image.ts +118 -0
- package/repo-root.ts +107 -8
- package/resources/backend-detect.mjs +98 -236
- package/resources/broker-control.mjs +189 -16
- package/resources/broker-epoch.mjs +1 -1
- package/resources/broker-kill.mjs +8 -2
- package/resources/broker-launch.mjs +373 -214
- package/resources/broker-state.mjs +64 -18
- package/resources/container-guard.mjs +1 -1
- package/resources/ghidra-project.mjs +790 -0
- package/resources/host-tool.mjs +2561 -0
- package/resources/vice-broker.mjs +330 -184
- package/resources/vice-launcher.sh +130 -12
- package/stock-address.ts +6 -6
- package/stock-cia.ts +2 -2
- package/stock-condition.ts +8 -8
- package/stock-connect.ts +10 -6
- package/stock-derived.ts +29 -37
- package/stock-diagnose.ts +200 -36
- package/stock-dispatch.ts +200 -68
- package/stock-execution.ts +5 -3
- package/stock-handler.ts +1 -1
- package/stock-input.ts +9 -9
- package/stock-machine.ts +17 -6
- package/stock-paths.ts +18 -14
- package/stock-petscii.ts +1 -1
- package/stock-protocol.ts +17 -12
- package/stock-recycle.ts +83 -2
- package/stock-registers.ts +54 -29
- package/stock-reproducible-run.ts +811 -0
- package/stock-run-until.ts +100 -1
- package/stock-sprites.ts +3 -3
- package/stock-symbols.ts +13 -13
- package/stock-timing.ts +2 -2
- package/stock-vicii.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/version.ts +1 -1
- package/vice-broker-client.ts +189 -42
- package/vice-errors.ts +268 -0
- package/vice-proxy.ts +392 -2175
- package/vsf-slice.ts +640 -0
- package/capability-registry.ts +0 -388
- package/r2000-cli.ts +0 -1103
- package/r2000-d64.ts +0 -310
- 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/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-join.ts
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// anno-join.ts
|
|
3
|
+
//
|
|
4
|
+
// Phase 37, plan 37-01 (AUTO-01): the mechanical join between stored
|
|
5
|
+
// cross-references and `memmap.json`, with no agent call, no queue walk and
|
|
6
|
+
// no skill invocation anywhere in the loop.
|
|
7
|
+
//
|
|
8
|
+
// THIS MODULE RECEIVES AN ALREADY-OPEN STORE HANDLE, exactly like
|
|
9
|
+
// `anno-import.ts` -- there is no second store session anywhere in this
|
|
10
|
+
// file. It never names `node:sqlite`, never calls `openStore()`/`closeStore()`
|
|
11
|
+
// itself, and never imports `hostpath.ts`/`containerpath.ts`: the image range
|
|
12
|
+
// it needs arrives as plain numbers (`imageOrigin`, `imageByteLength`) that
|
|
13
|
+
// the caller has already derived from a loaded image, mirroring
|
|
14
|
+
// `dxa-blocks.ts`'s own header posture of reading only already-fetched facts
|
|
15
|
+
// a caller passes in.
|
|
16
|
+
//
|
|
17
|
+
// WHAT THIS IS THE ONE AUTHORITATIVE PLACE FOR: reading `listXrefs()`'s
|
|
18
|
+
// distinct target addresses, classifying each one (inside the loaded image,
|
|
19
|
+
// no `memmap.json` entry, or annotated) and writing the resulting comment
|
|
20
|
+
// through `setComment()`. `AUTO-01`'s own criterion -- no agent, no queue
|
|
21
|
+
// walk, no skill invocation -- is checked STRUCTURALLY over this module's own
|
|
22
|
+
// source in `anno-join.test.ts` (plan 37 task 3), not merely asserted here in
|
|
23
|
+
// prose.
|
|
24
|
+
//
|
|
25
|
+
// WHAT NOT TO DO:
|
|
26
|
+
// - Never import anything under `src/skills/`. This function's entire
|
|
27
|
+
// point is that annotating an address costs one mechanical call, not an
|
|
28
|
+
// agent turn.
|
|
29
|
+
// - Never spawn a child process from this module.
|
|
30
|
+
// - Never carry a bank value forward past a point where two paths
|
|
31
|
+
// disagree, and never default an unresolved bank state to the power-on
|
|
32
|
+
// value (T-37-26). A reaching-values set of size other than one, or one
|
|
33
|
+
// that decodes to more than one region, DECLINES -- see the bank-state
|
|
34
|
+
// block below.
|
|
35
|
+
//
|
|
36
|
+
// Phase 37, plan 37-06 (AUTO-04/AUTO-05): `runMemmapJoin()` gains an
|
|
37
|
+
// OPTIONAL `constWrites` argument. When `undefined` (every pre-37-06 call
|
|
38
|
+
// site, and every existing test in this file), the bank-state machinery
|
|
39
|
+
// below is a complete no-op and every address resolves EXACTLY as it did
|
|
40
|
+
// before this plan -- the candidate-constraint argument must not change any
|
|
41
|
+
// unconstrained selection's answer. Only when a caller explicitly supplies
|
|
42
|
+
// an array (even an empty one) does the reaching-values/decline logic
|
|
43
|
+
// activate for addresses inside `BANK_CONDITIONAL_RANGES`. D-37-24: the
|
|
44
|
+
// reaching-values computation is deliberately conservative -- it is NOT a
|
|
45
|
+
// dataflow analysis. It uses only what the export gives: the recovered
|
|
46
|
+
// constant stores to the processor port (each with its own address), and
|
|
47
|
+
// the cross-reference graph the importer already stored via `putXref()`.
|
|
48
|
+
// The set of recovered values reaching a given address is the set of
|
|
49
|
+
// `constWrites` whose OWN store address can reach that address, walked
|
|
50
|
+
// forward over that same graph. Where the graph does not connect a store to
|
|
51
|
+
// the address at all, that value is simply not in the reaching set --
|
|
52
|
+
// D-37-23's decline-on-empty-or-disagreement rule (below) is what turns
|
|
53
|
+
// "nothing reaches this point" into a stated absence rather than a silent
|
|
54
|
+
// default.
|
|
55
|
+
|
|
56
|
+
import { listXrefs, setComment, setDataType } from "./anno-store.ts";
|
|
57
|
+
import type { AnnoStoreHandle } from "./anno-store.ts";
|
|
58
|
+
import {
|
|
59
|
+
decodeBankState,
|
|
60
|
+
isBankConditionalAddress,
|
|
61
|
+
regionAdmitsEntry,
|
|
62
|
+
resolveBankedRegion,
|
|
63
|
+
} from "./anno-bank.ts";
|
|
64
|
+
import type { BankedRegion } from "./anno-bank.ts";
|
|
65
|
+
import type { ConstWriteFact } from "./anno-import.ts";
|
|
66
|
+
import type { ContradictedComment, SplitTableReinterpretation, XrefRow } from "./anno-types.ts";
|
|
67
|
+
import { loadMemmap, memmapDigest, PROVENANCE_TOKEN_PREFIX, selectMemmapEntry } from "./memmap-lookup.ts";
|
|
68
|
+
import type { MemmapEntry, MemmapSelection } from "./memmap-lookup.ts";
|
|
69
|
+
// Phase 37, plan 37-08 (AUTO-07): the graphics write-back. deriveGraphicsRanges()
|
|
70
|
+
// is structurally typed against ConstWriteFact -- GraphicsConstWriteFact's own
|
|
71
|
+
// shape is identical ({storeAddress, targetAddress, value}) -- so THIS module's
|
|
72
|
+
// own constWrites argument, already threaded for D-37-24's bank-state block, is
|
|
73
|
+
// handed straight through with no translation layer (37-07-SUMMARY.md's own
|
|
74
|
+
// "Next Phase Readiness" note names this directly).
|
|
75
|
+
import { deriveGraphicsRanges } from "./anno-graphics.ts";
|
|
76
|
+
import type { GraphicsMap } from "./anno-graphics.ts";
|
|
77
|
+
|
|
78
|
+
/** D-37-25: the axis-qualified provenance marker an annotated bank-conditional
|
|
79
|
+
* comment carries, ALWAYS before `PROVENANCE_TOKEN_PREFIX`'s own digest
|
|
80
|
+
* token (which stays last -- D-37-13). Names the axis ("processor-port")
|
|
81
|
+
* explicitly, so a later phase adding the VIC banking axis is additive
|
|
82
|
+
* rather than ambiguous about which axis a given token names. */
|
|
83
|
+
const BANK_PROVENANCE_PREFIX = "[processor-port:";
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The reaching-values computation's own result shape (D-37-24): exactly one
|
|
87
|
+
* recovered value reaches the address; several do (an array -- length 0
|
|
88
|
+
* means none reach at all, length 2+ means genuine disagreement-or-agreement
|
|
89
|
+
* to resolve); or the computation cannot decide at all. Nothing in this
|
|
90
|
+
* project's current data model (a plain directed graph of already-resolved
|
|
91
|
+
* `XrefRow`s) can currently produce `"unknown"` -- there is no signal here
|
|
92
|
+
* for a dropped or unresolved reference (D-37-24's own stated limit) -- but
|
|
93
|
+
* the shape is kept complete for a future importer that DOES emit such a
|
|
94
|
+
* signal, rather than silently folding that case into "empty".
|
|
95
|
+
*/
|
|
96
|
+
type ReachingValues = { kind: "one"; value: number } | { kind: "several"; values: number[] } | { kind: "unknown" };
|
|
97
|
+
|
|
98
|
+
/** Builds a forward adjacency map from `xrefs`, ONCE per join run, reused for
|
|
99
|
+
* every address's own reachability walk below. */
|
|
100
|
+
function buildAdjacency(xrefs: readonly XrefRow[]): Map<number, number[]> {
|
|
101
|
+
const adjacency = new Map<number, number[]>();
|
|
102
|
+
for (const xref of xrefs) {
|
|
103
|
+
const list = adjacency.get(xref.fromAddress);
|
|
104
|
+
if (list) list.push(xref.toAddress);
|
|
105
|
+
else adjacency.set(xref.fromAddress, [xref.toAddress]);
|
|
106
|
+
}
|
|
107
|
+
return adjacency;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Plain forward reachability (DFS, visited-set guarded against cycles):
|
|
111
|
+
* can `from` reach `target` by following zero or more `adjacency` edges? */
|
|
112
|
+
function canReach(from: number, target: number, adjacency: Map<number, number[]>): boolean {
|
|
113
|
+
if (from === target) return true;
|
|
114
|
+
const visited = new Set<number>([from]);
|
|
115
|
+
const stack = [from];
|
|
116
|
+
while (stack.length > 0) {
|
|
117
|
+
const current = stack.pop()!;
|
|
118
|
+
for (const next of adjacency.get(current) ?? []) {
|
|
119
|
+
if (next === target) return true;
|
|
120
|
+
if (!visited.has(next)) {
|
|
121
|
+
visited.add(next);
|
|
122
|
+
stack.push(next);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** D-37-24: the set of recovered processor-port values whose OWN store
|
|
130
|
+
* address can reach `targetAddress`, over `adjacency`. Never a dataflow
|
|
131
|
+
* analysis -- purely "does the stored cross-reference graph connect this
|
|
132
|
+
* store to this address". */
|
|
133
|
+
function computeReachingValues(
|
|
134
|
+
targetAddress: number,
|
|
135
|
+
constWrites: readonly ConstWriteFact[],
|
|
136
|
+
adjacency: Map<number, number[]>,
|
|
137
|
+
): ReachingValues {
|
|
138
|
+
const reaching: number[] = [];
|
|
139
|
+
for (const write of constWrites) {
|
|
140
|
+
if (canReach(write.storeAddress, targetAddress, adjacency)) reaching.push(write.value);
|
|
141
|
+
}
|
|
142
|
+
if (reaching.length === 1) return { kind: "one", value: reaching[0]! };
|
|
143
|
+
return { kind: "several", values: reaching };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Raised when `runMemmapJoin()`'s own inputs cannot support a join at all --
|
|
147
|
+
* currently only the zero-byte-image refusal below. Follows `anno-import.ts`'s
|
|
148
|
+
* `AnnoImportError` construction idiom (a bare `Error` subclass, not
|
|
149
|
+
* `AnnoStoreError`, since this module never touches the store's own
|
|
150
|
+
* persistence). */
|
|
151
|
+
export class AnnoJoinError extends Error {
|
|
152
|
+
constructor(message: string) {
|
|
153
|
+
super(message);
|
|
154
|
+
this.name = "AnnoJoinError";
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** What one `runMemmapJoin()` call reports. `addressesConsidered` is always
|
|
159
|
+
* the sum of the next four fields. The three `graphics*` fields (plan 37-08,
|
|
160
|
+
* AUTO-07) are always present and `0` when `constWrites` is omitted or when
|
|
161
|
+
* the selected map derives zero ranges -- never absent, so a caller reads
|
|
162
|
+
* them unconditionally instead of guarding on them, mirroring
|
|
163
|
+
* `SetDataTypeResult`'s own "always present, often empty" convention for
|
|
164
|
+
* `contradictedComments`/`reinterpretedSplitTables`. */
|
|
165
|
+
export interface JoinCounts {
|
|
166
|
+
addressesConsidered: number;
|
|
167
|
+
annotated: number;
|
|
168
|
+
skippedInImage: number;
|
|
169
|
+
skippedNoMapEntry: number;
|
|
170
|
+
declined: number;
|
|
171
|
+
commentsChanged: number;
|
|
172
|
+
graphicsRangesWritten: number;
|
|
173
|
+
graphicsContradictedComments: number;
|
|
174
|
+
graphicsReinterpretedSplitTables: number;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* What the graphics write-back (plan 37-08, AUTO-07) reports, in full --
|
|
179
|
+
* `JoinCounts`'s own `graphics*` fields are the COUNTS of these same
|
|
180
|
+
* `contradictedComments`/`reinterpretedSplitTables` arrays; this record
|
|
181
|
+
* carries the disclosures themselves so neither is dropped (must_haves.truths:
|
|
182
|
+
* "the join's returned counts include the contradicted-comment and
|
|
183
|
+
* fragmented-split-table disclosures the range write reported; neither is
|
|
184
|
+
* dropped"). `mapIndex` records WHICH of `deriveGraphicsRanges()`'s several
|
|
185
|
+
* maps was written -- D-37-27's own rule (several valid combinations are
|
|
186
|
+
* several maps, never one merged map) means writing more than one would
|
|
187
|
+
* write mutually-contradicting ranges into the SAME store, so exactly one is
|
|
188
|
+
* ever written and this field is the record of which. */
|
|
189
|
+
export interface GraphicsWriteBack {
|
|
190
|
+
mapIndex: number;
|
|
191
|
+
rangesWritten: number;
|
|
192
|
+
contradictedComments: readonly ContradictedComment[];
|
|
193
|
+
reinterpretedSplitTables: readonly SplitTableReinterpretation[];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** One address's own outcome. A skip or a decline always carries a non-empty
|
|
197
|
+
* `reason` naming WHY in the join's own vocabulary; an annotation carries the
|
|
198
|
+
* `label` it wrote. */
|
|
199
|
+
export interface JoinDecision {
|
|
200
|
+
address: number;
|
|
201
|
+
outcome: "annotated" | "skipped-in-image" | "skipped-no-entry" | "declined";
|
|
202
|
+
reason?: string;
|
|
203
|
+
label?: string;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface RunMemmapJoinArgs {
|
|
207
|
+
imageOrigin: number;
|
|
208
|
+
imageByteLength: number;
|
|
209
|
+
/** D-37-24/AUTO-04/AUTO-05: the recovered `$01` const-write facts this run
|
|
210
|
+
* has evidence for, typically `parseConstWrites()`'s own output over one
|
|
211
|
+
* imported export. `undefined` (every pre-37-06 call site) means "this run
|
|
212
|
+
* carries no bank-state evidence at all" -- the bank-state machinery is a
|
|
213
|
+
* complete no-op and every address resolves EXACTLY as before this plan.
|
|
214
|
+
* An explicit array (even `[]`) activates it for addresses inside
|
|
215
|
+
* `BANK_CONDITIONAL_RANGES`. */
|
|
216
|
+
constWrites?: readonly ConstWriteFact[];
|
|
217
|
+
/** Plan 37-08 (AUTO-07, D-37-27): which of `deriveGraphicsRanges()`'s
|
|
218
|
+
* several maps to write back, when `constWrites` derives more than one
|
|
219
|
+
* distinct register-value combination. Defaults to `0`. Consulted ONLY
|
|
220
|
+
* when `constWrites` is supplied AT ALL (the SAME gate that activates the
|
|
221
|
+
* bank-state block above) -- omitting `constWrites` entirely skips the
|
|
222
|
+
* graphics write-back completely, exactly like the bank-state block. Out
|
|
223
|
+
* of range for the derived map count refuses BY NAME (`AnnoJoinError`)
|
|
224
|
+
* rather than silently clamping or picking a default. */
|
|
225
|
+
graphicsMapIndex?: number;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Joins every DISTINCT `toAddress` `listXrefs()` holds against `memmap.json`,
|
|
230
|
+
* in ascending address order. An address inside the caller's own loaded
|
|
231
|
+
* image range is a program address, not a hardware/memory-map feature, and
|
|
232
|
+
* is skipped WITHOUT a `memmap.json` lookup -- the membership test runs
|
|
233
|
+
* BEFORE `selectEntry()` is called at all (D-37-12), so the guard is a
|
|
234
|
+
* control-flow fact rather than a result-filtering one: `AUTO-03` says an
|
|
235
|
+
* in-image address is "never looked up in memmap.json", a claim about what
|
|
236
|
+
* runs, not merely about what the caller sees back. An address with no
|
|
237
|
+
* containing `memmap.json` entry is skipped for that reason instead;
|
|
238
|
+
* everything else is annotated with the selected entry's label via
|
|
239
|
+
* `setComment()`. Running this twice over an unchanged store re-classifies
|
|
240
|
+
* every address identically and reports `commentsChanged: 0` on the second
|
|
241
|
+
* run, because `setComment()` itself reports `changed: false` for a
|
|
242
|
+
* byte-identical repeat.
|
|
243
|
+
*
|
|
244
|
+
* `selectEntry` defaults to the real `selectMemmapEntry` and exists as an
|
|
245
|
+
* injection point for exactly one reason: it is the seam that makes "never
|
|
246
|
+
* looked up" checkable at all. Without it, the only available assertion is
|
|
247
|
+
* on the RESULT, and a result-filtering implementation (compute the
|
|
248
|
+
* selection, then discard it for an in-image address) would pass that
|
|
249
|
+
* assertion while still violating the requirement. A test passes a counting
|
|
250
|
+
* spy in its place and asserts zero calls for an all-in-image store.
|
|
251
|
+
*/
|
|
252
|
+
export function runMemmapJoin(
|
|
253
|
+
handle: AnnoStoreHandle,
|
|
254
|
+
args: RunMemmapJoinArgs,
|
|
255
|
+
entries: readonly MemmapEntry[] = loadMemmap(),
|
|
256
|
+
selectEntry: (address: number, entries: readonly MemmapEntry[]) => MemmapSelection | undefined = selectMemmapEntry,
|
|
257
|
+
): { counts: JoinCounts; decisions: JoinDecision[]; graphics?: GraphicsWriteBack } {
|
|
258
|
+
if (args.imageByteLength === 0) {
|
|
259
|
+
throw new AnnoJoinError(
|
|
260
|
+
"runMemmapJoin refused: the image's own body length is 0 -- an image with no bytes has no range, and treating " +
|
|
261
|
+
"it as a zero-width range at its own origin would silently make one address (the origin) read as in-image.",
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// D-37-13: computed ONCE per join run and reused for every annotated row,
|
|
266
|
+
// never recomputed per row -- two comments written in the same run are
|
|
267
|
+
// therefore GUARANTEED to carry byte-identical tokens, not merely likely
|
|
268
|
+
// to (the file cannot change mid-run, but a per-row recompute would still
|
|
269
|
+
// be wasted work re-reading and re-hashing the same bytes for nothing).
|
|
270
|
+
const digest = memmapDigest();
|
|
271
|
+
|
|
272
|
+
const xrefs = listXrefs(handle);
|
|
273
|
+
const targets = [...new Set(xrefs.map((xref) => xref.toAddress))].sort((a, b) => a - b);
|
|
274
|
+
|
|
275
|
+
// D-37-24: built ONCE per run, over the SAME xref graph the unconstrained
|
|
276
|
+
// path already reads via `listXrefs()` above -- reused for every address's
|
|
277
|
+
// own reachability walk below. `undefined` `args.constWrites` means the
|
|
278
|
+
// bank-state block is never entered at all, so this adjacency map is built
|
|
279
|
+
// but simply never consulted -- negligible cost, and keeps the "no
|
|
280
|
+
// constWrites -> no-op" contract a single conditional rather than two
|
|
281
|
+
// divergent code paths.
|
|
282
|
+
const bankAdjacency = buildAdjacency(xrefs);
|
|
283
|
+
|
|
284
|
+
// The inclusive image range, computed ONCE from the LoadedImage's own body
|
|
285
|
+
// bytes (D-37-12) -- never from `totalBytes` (the file's own byte count,
|
|
286
|
+
// which on the .prg route includes the two-byte load-address header and
|
|
287
|
+
// would shift this whole range by two bytes) and never a caller-supplied
|
|
288
|
+
// number pair that could silently widen or narrow the program's own
|
|
289
|
+
// extent.
|
|
290
|
+
const imageStart = args.imageOrigin;
|
|
291
|
+
const imageEnd = args.imageOrigin + args.imageByteLength - 1;
|
|
292
|
+
|
|
293
|
+
const decisions: JoinDecision[] = [];
|
|
294
|
+
let annotated = 0;
|
|
295
|
+
let skippedInImage = 0;
|
|
296
|
+
let skippedNoMapEntry = 0;
|
|
297
|
+
let declined = 0;
|
|
298
|
+
let commentsChanged = 0;
|
|
299
|
+
|
|
300
|
+
for (const address of targets) {
|
|
301
|
+
// THE GUARD: one early-return, before selectEntry() is ever called. A
|
|
302
|
+
// single textual deletion of this block removes it cleanly -- that
|
|
303
|
+
// deletion is plan 37-05's own observed-red control (row 4).
|
|
304
|
+
if (address >= imageStart && address <= imageEnd) {
|
|
305
|
+
skippedInImage += 1;
|
|
306
|
+
decisions.push({
|
|
307
|
+
address,
|
|
308
|
+
outcome: "skipped-in-image",
|
|
309
|
+
reason:
|
|
310
|
+
`address $${address.toString(16)} lies inside the loaded image's own range ($${imageStart.toString(16)}-` +
|
|
311
|
+
`$${imageEnd.toString(16)}) and is therefore a program address, never looked up in memmap.json`,
|
|
312
|
+
});
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// THE BANK-STATE BLOCK (D-37-22/D-37-23/D-37-24, AUTO-04/AUTO-05). Only
|
|
317
|
+
// entered when the caller supplied `constWrites` AT ALL (`undefined`
|
|
318
|
+
// skips this whole block, falling through to the unconstrained path
|
|
319
|
+
// below exactly as pre-37-06) AND the address is inside one of the
|
|
320
|
+
// three bank-conditional ranges -- outside them, bank state is
|
|
321
|
+
// irrelevant and the candidate set stays unconstrained regardless of
|
|
322
|
+
// `constWrites` (D-37-22).
|
|
323
|
+
if (args.constWrites !== undefined && isBankConditionalAddress(address)) {
|
|
324
|
+
const reaching = computeReachingValues(address, args.constWrites, bankAdjacency);
|
|
325
|
+
|
|
326
|
+
if (reaching.kind === "unknown") {
|
|
327
|
+
declined += 1;
|
|
328
|
+
decisions.push({
|
|
329
|
+
address,
|
|
330
|
+
outcome: "declined",
|
|
331
|
+
reason: `bank state at $${address.toString(16)} could not be determined -- the reaching-values computation could not decide`,
|
|
332
|
+
});
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Resolves ONE region (or refuses) for a single reaching value, applies
|
|
337
|
+
// it as a candidate constraint BEFORE selection runs (never a
|
|
338
|
+
// post-filter -- D-37-22), and pushes the matching decision. Shared by
|
|
339
|
+
// both the single-value and the several-values-same-region branches
|
|
340
|
+
// below, so the annotate path is written exactly once.
|
|
341
|
+
const annotateUnderRegion = (region: Exclude<BankedRegion, "not_applicable">, bankNote: string): void => {
|
|
342
|
+
const constrained = entries.filter((entry) => regionAdmitsEntry(entry, region));
|
|
343
|
+
const selection = selectEntry(address, constrained);
|
|
344
|
+
if (selection === undefined) {
|
|
345
|
+
skippedNoMapEntry += 1;
|
|
346
|
+
decisions.push({
|
|
347
|
+
address,
|
|
348
|
+
outcome: "skipped-no-entry",
|
|
349
|
+
reason: `no memmap.json entry admits the resolved ${region} region for $${address.toString(16)}`,
|
|
350
|
+
});
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const commentText = `${selection.entry.label} ${BANK_PROVENANCE_PREFIX}${bankNote}] ${PROVENANCE_TOKEN_PREFIX}${digest}`;
|
|
354
|
+
const write = setComment(handle, { address, commentType: "line", text: commentText });
|
|
355
|
+
annotated += 1;
|
|
356
|
+
if (write.changed) commentsChanged += 1;
|
|
357
|
+
decisions.push({
|
|
358
|
+
address,
|
|
359
|
+
outcome: "annotated",
|
|
360
|
+
label: selection.entry.label,
|
|
361
|
+
...(reaching.kind === "several" ? { reason: `reached under differing processor-port values (${bankNote}) that all resolve to the same region (${region})` } : {}),
|
|
362
|
+
});
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
if (reaching.kind === "one") {
|
|
366
|
+
const region = resolveBankedRegion(address, decodeBankState(reaching.value));
|
|
367
|
+
annotateUnderRegion(region as Exclude<BankedRegion, "not_applicable">, `$${reaching.value.toString(16)}`);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// reaching.kind === "several"
|
|
372
|
+
if (reaching.values.length === 0) {
|
|
373
|
+
declined += 1;
|
|
374
|
+
decisions.push({
|
|
375
|
+
address,
|
|
376
|
+
outcome: "declined",
|
|
377
|
+
reason: `no recovered processor-port value reaches $${address.toString(16)} -- declining rather than defaulting to the power-on state`,
|
|
378
|
+
});
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const uniqueValues = [...new Set(reaching.values)].sort((a, b) => a - b);
|
|
383
|
+
const regionsByValue = uniqueValues.map((value) => resolveBankedRegion(address, decodeBankState(value)));
|
|
384
|
+
const uniqueRegions = new Set(regionsByValue);
|
|
385
|
+
if (uniqueRegions.size > 1) {
|
|
386
|
+
declined += 1;
|
|
387
|
+
const named = uniqueValues.map((value, i) => `$${value.toString(16)}(${regionsByValue[i]})`).join(", ");
|
|
388
|
+
decisions.push({
|
|
389
|
+
address,
|
|
390
|
+
outcome: "declined",
|
|
391
|
+
reason: `$${address.toString(16)} is reached under disagreeing processor-port values: ${named}`,
|
|
392
|
+
});
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const region = regionsByValue[0]! as Exclude<BankedRegion, "not_applicable">;
|
|
397
|
+
const valuesNote = uniqueValues.map((value) => `$${value.toString(16)}`).join(",");
|
|
398
|
+
annotateUnderRegion(region, valuesNote);
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const selection = selectEntry(address, entries);
|
|
403
|
+
if (selection === undefined) {
|
|
404
|
+
skippedNoMapEntry += 1;
|
|
405
|
+
decisions.push({
|
|
406
|
+
address,
|
|
407
|
+
outcome: "skipped-no-entry",
|
|
408
|
+
reason: "no memmap.json entry contains this address",
|
|
409
|
+
});
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// The full comment text: the selected entry's label, one space, the
|
|
414
|
+
// provenance prefix, then the full 64-character digest -- always LAST,
|
|
415
|
+
// never truncated (D-37-13). setComment() -> assertCommentText() refuses
|
|
416
|
+
// (never truncates) a text that overflows MAX_COMMENT_BYTES; that
|
|
417
|
+
// refusal is left to propagate here rather than being pre-checked and
|
|
418
|
+
// silently worked around, because a truncated provenance token would be
|
|
419
|
+
// a wrong answer that reports success.
|
|
420
|
+
const commentText = `${selection.entry.label} ${PROVENANCE_TOKEN_PREFIX}${digest}`;
|
|
421
|
+
const write = setComment(handle, { address, commentType: "line", text: commentText });
|
|
422
|
+
annotated += 1;
|
|
423
|
+
if (write.changed) commentsChanged += 1;
|
|
424
|
+
decisions.push({ address, outcome: "annotated", label: selection.entry.label });
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// THE GRAPHICS WRITE-BACK (D-37-32/D-37-33, AUTO-07). Runs AFTER the main
|
|
428
|
+
// per-address loop above, as its own step -- graphics ranges are derived
|
|
429
|
+
// from register VALUES, never from the cross-reference targets the loop
|
|
430
|
+
// above walks, so there is no reason to interleave the two. Gated on the
|
|
431
|
+
// SAME `constWrites !== undefined` condition the bank-state block uses:
|
|
432
|
+
// omitting `constWrites` entirely is a complete no-op here too, exactly
|
|
433
|
+
// like the bank-state block above.
|
|
434
|
+
let graphics: GraphicsWriteBack | undefined;
|
|
435
|
+
if (args.constWrites !== undefined) {
|
|
436
|
+
const maps = deriveGraphicsRanges(args.constWrites);
|
|
437
|
+
const mapIndex = args.graphicsMapIndex ?? 0;
|
|
438
|
+
const selectedMap: GraphicsMap | undefined = maps[mapIndex];
|
|
439
|
+
if (selectedMap === undefined) {
|
|
440
|
+
throw new AnnoJoinError(
|
|
441
|
+
`runMemmapJoin refused: graphicsMapIndex ${mapIndex} is out of range -- deriveGraphicsRanges() produced ` +
|
|
442
|
+
`${maps.length} map(s) for this run's own constWrites`,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// D-37-27: write ONLY the selected map's own ranges -- never every map
|
|
447
|
+
// deriveGraphicsRanges() returned. Several distinct register-value
|
|
448
|
+
// combinations describe MUTUALLY CONTRADICTING layouts (D-37-27's own
|
|
449
|
+
// reason several maps exist at all); writing more than one into the
|
|
450
|
+
// same store would write ranges that disagree with each other by
|
|
451
|
+
// construction.
|
|
452
|
+
let rangesWritten = 0;
|
|
453
|
+
const contradictedComments: ContradictedComment[] = [];
|
|
454
|
+
const reinterpretedSplitTables: SplitTableReinterpretation[] = [];
|
|
455
|
+
for (const range of selectedMap.ranges) {
|
|
456
|
+
const write = setDataType(handle, { start: range.start, endInclusive: range.endInclusive, dataType: range.dataType });
|
|
457
|
+
rangesWritten += 1;
|
|
458
|
+
// Disclosures a successful range write can carry -- surfaced, never
|
|
459
|
+
// dropped (must_haves.truths): a comment whose recorded confidence now
|
|
460
|
+
// contradicts the type this write just assigned, or a split table this
|
|
461
|
+
// write fragmented.
|
|
462
|
+
contradictedComments.push(...write.contradictedComments);
|
|
463
|
+
reinterpretedSplitTables.push(...write.reinterpretedSplitTables);
|
|
464
|
+
}
|
|
465
|
+
graphics = { mapIndex, rangesWritten, contradictedComments, reinterpretedSplitTables };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const counts: JoinCounts = {
|
|
469
|
+
addressesConsidered: targets.length,
|
|
470
|
+
annotated,
|
|
471
|
+
skippedInImage,
|
|
472
|
+
skippedNoMapEntry,
|
|
473
|
+
declined,
|
|
474
|
+
commentsChanged,
|
|
475
|
+
graphicsRangesWritten: graphics?.rangesWritten ?? 0,
|
|
476
|
+
graphicsContradictedComments: graphics?.contradictedComments.length ?? 0,
|
|
477
|
+
graphicsReinterpretedSplitTables: graphics?.reinterpretedSplitTables.length ?? 0,
|
|
478
|
+
};
|
|
479
|
+
return graphics === undefined ? { counts, decisions } : { counts, decisions, graphics };
|
|
480
|
+
}
|