@henols/vice-mcp 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/THIRD-PARTY-NOTICES.md +422 -1
- package/anno-bank.ts +171 -0
- package/anno-cli.ts +1674 -99
- package/anno-enum-gen.ts +416 -30
- package/anno-export-asm.ts +1175 -89
- package/anno-graphics.ts +338 -0
- package/anno-hazard-report.ts +1367 -0
- package/anno-import.ts +495 -0
- package/anno-join.ts +480 -0
- package/anno-provenance-ledger.ts +472 -0
- package/anno-register.ts +159 -0
- package/anno-store-export.ts +661 -0
- package/anno-store.ts +518 -2
- package/anno-tools.ts +1169 -16
- package/anno-types.ts +275 -2
- package/backend-detect.mts +124 -312
- package/build.ts +3 -1
- package/capture-predicate.ts +597 -0
- package/channel-lock.ts +349 -0
- package/evid-ingest.ts +217 -0
- package/evid-reconcile.ts +316 -0
- package/host-tool-client.ts +430 -0
- package/incident-record.ts +23 -12
- package/install-resources.ts +29 -13
- package/memmap-lookup.ts +285 -0
- package/package.json +27 -8
- package/prg-image.ts +1 -2
- package/repo-root.ts +87 -3
- package/resources/backend-detect.mjs +98 -236
- package/resources/broker-control.mjs +189 -16
- package/resources/broker-epoch.mjs +1 -1
- package/resources/broker-kill.mjs +8 -2
- package/resources/broker-launch.mjs +365 -210
- package/resources/broker-state.mjs +64 -18
- package/resources/container-guard.mjs +1 -1
- package/resources/ghidra-project.mjs +790 -0
- package/resources/host-tool.mjs +2561 -0
- package/resources/vice-broker.mjs +330 -184
- package/resources/vice-launcher.sh +127 -9
- package/stock-address.ts +1 -1
- package/stock-condition.ts +1 -1
- package/stock-connect.ts +9 -5
- package/stock-derived.ts +29 -37
- package/stock-diagnose.ts +200 -36
- package/stock-dispatch.ts +179 -77
- package/stock-handler.ts +1 -1
- package/stock-paths.ts +18 -14
- package/stock-petscii.ts +1 -1
- package/stock-protocol.ts +1 -1
- package/stock-recycle.ts +83 -2
- package/stock-reproducible-run.ts +811 -0
- package/stock-run-until.ts +100 -1
- package/stock-symbols.ts +4 -4
- package/stock-timing.ts +1 -1
- package/stop-oracle.ts +167 -0
- package/text-capability-probe.ts +660 -0
- package/text-connect.ts +157 -0
- package/text-protocol.ts +810 -0
- package/text-tools.ts +778 -0
- package/textmon-backtrace.ts +385 -0
- package/textmon-cpuhistory.ts +335 -0
- package/textmon-memmap.ts +494 -0
- package/textmon-profile.ts +458 -0
- package/textmon-registers.ts +748 -0
- package/tools-manifest.stock.json +864 -3
- package/vice-broker-client.ts +189 -42
- package/vice-errors.ts +268 -0
- package/vice-proxy.ts +339 -2144
- package/vsf-slice.ts +640 -0
- package/anno-d64.ts +0 -310
- package/capability-registry.ts +0 -390
- package/refresh-manifest.ts +0 -124
- package/tools-manifest.json +0 -1223
- package/vice-probe.ts +0 -278
- package/vice-sync.ts +0 -336
- package/vice.ts +0 -772
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
// textmon-profile.ts
|
|
2
|
+
//
|
|
3
|
+
// THE ONE owning module for VICE's `prof flat` text-monitor output
|
|
4
|
+
// (PARSE-02, PARSE-03). Nothing else in this tree reads or interprets a
|
|
5
|
+
// `prof flat` reply -- a handler dials the wire, this module is the only
|
|
6
|
+
// place that turns the framed text into structured data.
|
|
7
|
+
//
|
|
8
|
+
// WHY THIS FILE EXISTS RATHER THAN LIVING INSIDE THE TOOL HANDLER: the same
|
|
9
|
+
// reasoning `textmon-memmap.ts` and `disasm-decoder.ts` state for themselves
|
|
10
|
+
// applies here unchanged -- keeping the parser import-free of transport code
|
|
11
|
+
// means any future non-tool consumer can depend on this one file without
|
|
12
|
+
// dragging in a socket, and PARSE-03's own never-throw discipline (D-42-3,
|
|
13
|
+
// decided once in plan 42-01 for all five text parsers this phase adds)
|
|
14
|
+
// means this module can never be the thing that silently launders a wire
|
|
15
|
+
// error into a parse result, because it never touches the wire at all.
|
|
16
|
+
//
|
|
17
|
+
// THE ONE HAZARD THAT DEFINES THIS MODULE. VICE's flat profiler writes its
|
|
18
|
+
// cycle counts with U+202F (NARROW NO-BREAK SPACE, UTF-8 bytes e2 80 af) as
|
|
19
|
+
// the thousands separator -- MEASURED directly against both committed real
|
|
20
|
+
// captures (`xxd fixtures/textmon/flat-profile-stock.txt`) and recorded in
|
|
21
|
+
// `fixtures/textmon/README.md`'s own "Encoding" section. In JavaScript, the
|
|
22
|
+
// regular-expression whitespace class (`\s`) MATCHES U+202F. So the obvious
|
|
23
|
+
// implementation -- split each row on a run of `\s` -- silently cuts every
|
|
24
|
+
// cycle count into pieces and yields only the first piece: the stock
|
|
25
|
+
// capture's leading row would read `2` instead of `2326151`, an inverted
|
|
26
|
+
// answer with no error anywhere. THIS MODULE THEREFORE NEVER SPLITS ON `\s`.
|
|
27
|
+
// Every row is tokenized on runs of the ASCII space character (U+0020) ONLY
|
|
28
|
+
// (`/ +/`, never `/\s+/`), so a thousands-separated number -- whose internal
|
|
29
|
+
// separator is U+202F, not U+0020 -- survives tokenization as ONE token.
|
|
30
|
+
// Each numeric token is then parsed by first stripping U+202F and requiring
|
|
31
|
+
// the remainder to be digits only. Do not "simplify" this back to a
|
|
32
|
+
// whitespace-class split; that is the defect this module exists to avoid.
|
|
33
|
+
//
|
|
34
|
+
// WHAT NOT TO DO:
|
|
35
|
+
// - Never import anything -- not `node:` anything, not `text-protocol.ts`,
|
|
36
|
+
// not `textmon-fixtures.ts`, not even a type-only import of a sibling
|
|
37
|
+
// module. Purity is asserted mechanically by this file's own test.
|
|
38
|
+
// - Never throw on a malformed or drifted input (D-42-3). Return the
|
|
39
|
+
// discriminated `FlatProfileParseResult` instead.
|
|
40
|
+
// - Never sort or re-rank `entries`. VICE emits rows in its own rank
|
|
41
|
+
// order (by cycles descending); this module performs NO array-sorting
|
|
42
|
+
// or array-reversing call anywhere, asserted mechanically by this
|
|
43
|
+
// file's own test scanning the source for one. Re-ranking, including
|
|
44
|
+
// "stabilizing" a tie by re-sorting on a secondary key, would restate
|
|
45
|
+
// which address dominates the profile -- the whole point of the format
|
|
46
|
+
// -- and is never this module's decision to make.
|
|
47
|
+
// - Never use `\s`, `String.prototype.trim()` on a whole row before
|
|
48
|
+
// tokenizing, or any other general-whitespace-class operation on a row
|
|
49
|
+
// that still contains its numeric fields -- see the hazard note above.
|
|
50
|
+
// - Never silently accept an ASCII space in place of the thousands
|
|
51
|
+
// separator by concatenating split fragments back together. A row using
|
|
52
|
+
// the wrong separator character is drift and must refuse by name
|
|
53
|
+
// (`unrecognised-separator`), never be silently repaired.
|
|
54
|
+
// - Never assume the percentage's decimal separator is a comma. The
|
|
55
|
+
// captures render it as a comma (the capturing host's locale), but a
|
|
56
|
+
// period is an equally legitimate reading from a different host; both
|
|
57
|
+
// are accepted and the OBSERVED separator is recorded on the result
|
|
58
|
+
// rather than assumed or discarded.
|
|
59
|
+
// - Never fold a never-started profiler into `missing-header`, and never
|
|
60
|
+
// move `PROFILING_NOT_STARTED_TEXT` outside this module. `text-tools.ts`
|
|
61
|
+
// branches on the `profiling-not-started` CODE this module exports,
|
|
62
|
+
// never on VICE's own text -- the single-owner rule PARSE-03's
|
|
63
|
+
// structural guard enforces (textmon-seam.test.ts) is not weakened to
|
|
64
|
+
// fix a message.
|
|
65
|
+
|
|
66
|
+
/** One decoded `prof flat` row, in VICE's own emitted order (rank order by
|
|
67
|
+
* cycles, descending) -- this module performs no sort of its own, ever.
|
|
68
|
+
* `address` is a 16-bit machine address for an ordinary call-tree entry, or
|
|
69
|
+
* the literal string `"ROOT"` for VICE's own synthetic top-level pseudo-frame
|
|
70
|
+
* -- MEASURED live (plan 42-09) against genuine stock VICE: cycles that
|
|
71
|
+
* elapsed outside any traced call (e.g. idle-loop time before any subroutine
|
|
72
|
+
* call happened during the profiled window) are attributed to a row whose
|
|
73
|
+
* address FIELD is the literal text `ROOT`, not a hex address, and neither
|
|
74
|
+
* committed fixture under `fixtures/textmon/` happened to capture this row
|
|
75
|
+
* shape (both captures ran long enough that no cycles landed there). This is
|
|
76
|
+
* a real, closed two-member shape -- never a third string, and never
|
|
77
|
+
* silently coerced to a number. */
|
|
78
|
+
export interface FlatProfileEntry {
|
|
79
|
+
readonly totalCycles: number;
|
|
80
|
+
readonly totalPercent: number;
|
|
81
|
+
readonly selfCycles: number;
|
|
82
|
+
readonly selfPercent: number;
|
|
83
|
+
readonly address: number | "ROOT";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The full decoded flat profile. `decimalSeparator` records the FIRST data
|
|
87
|
+
* row's observed decimal-point character (comma or period) -- a real
|
|
88
|
+
* environmental fact, never assumed -- and is NOT validated against any
|
|
89
|
+
* later row. A caller must not read it as a whole-payload guarantee that
|
|
90
|
+
* every row shares the same separator; it names only what the first row
|
|
91
|
+
* showed. */
|
|
92
|
+
export interface FlatProfile {
|
|
93
|
+
readonly entries: readonly FlatProfileEntry[];
|
|
94
|
+
readonly decimalSeparator: "," | ".";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The closed refusal-code union (D-42-3): an empty response, a missing or
|
|
98
|
+
* unrecognised header/rule pair, a structurally malformed row (wrong field
|
|
99
|
+
* count, non-hex address), an unrecognised thousands separator (a numeric
|
|
100
|
+
* group broken apart by an ASCII space rather than joined by U+202F), an
|
|
101
|
+
* unrecognised percentage shape, and a profiler that was never started
|
|
102
|
+
* (`profiling-not-started`) -- the profiler subsystem is present and the
|
|
103
|
+
* command is fine, it simply has nothing recorded yet, which is a
|
|
104
|
+
* different fact from a header that failed to match. */
|
|
105
|
+
export type FlatProfileRefusalCode =
|
|
106
|
+
| "empty-response"
|
|
107
|
+
| "missing-header"
|
|
108
|
+
| "malformed-row"
|
|
109
|
+
| "unrecognised-separator"
|
|
110
|
+
| "unrecognised-percentage"
|
|
111
|
+
| "profiling-not-started";
|
|
112
|
+
|
|
113
|
+
/** A parser refusal (D-42-3): returned, never thrown. `line` and
|
|
114
|
+
* `lineNumber` name the offending content whenever one exists -- for the
|
|
115
|
+
* whole-payload `empty-response` refusal, `line` is empty and `lineNumber`
|
|
116
|
+
* is 0. */
|
|
117
|
+
export interface TextParseRefusal {
|
|
118
|
+
readonly code: FlatProfileRefusalCode;
|
|
119
|
+
readonly message: string;
|
|
120
|
+
readonly line: string;
|
|
121
|
+
readonly lineNumber: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** D-42-3's discriminated shape: a parser never throws, it returns one of
|
|
125
|
+
* these two arms. */
|
|
126
|
+
export type FlatProfileParseResult = { ok: true; value: FlatProfile } | { ok: false; refusal: TextParseRefusal };
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Framing constants -- this module's OWN copies, never imported from
|
|
130
|
+
// text-protocol.ts (purity rule above). `prof flat` carries no leading
|
|
131
|
+
// entry-echo prompt (unlike `memmapshow`) per fixtures/textmon/README.md's
|
|
132
|
+
// own "Framing" section -- only a TRAILING exit-prompt is ever stripped.
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
const TRAILING_PROMPT_RE = /\(C:\$[0-9A-Fa-f]{4}\)\s*$/;
|
|
136
|
+
|
|
137
|
+
/** VICE's own two-line header, byte-for-byte -- MEASURED identical across
|
|
138
|
+
* both the stock and fork real captures. Neither line's content depends on
|
|
139
|
+
* the reported row count. */
|
|
140
|
+
const HEADER_LINE = " Total % Self %";
|
|
141
|
+
const RULES_LINE = "------------- ------ ------------- ------";
|
|
142
|
+
|
|
143
|
+
/** VICE's own cold-profiler sentence, quoted byte-for-byte -- including its
|
|
144
|
+
* embedded double quotes and its trailing period -- from
|
|
145
|
+
* `text-protocol.ts`'s `TEXT_COMMAND_ALLOWLIST` doc comment and
|
|
146
|
+
* `docs/phase42-text-format-drift-citations.md`'s Block 9. Unlike this
|
|
147
|
+
* module's siblings' source-traced strings, this one is MEASURED: observed
|
|
148
|
+
* live against genuine stock `x64sc (VICE 3.9)`, 2026-09-09. `prof flat`
|
|
149
|
+
* alone, on a freshly connected session that has never issued `prof on`,
|
|
150
|
+
* returns exactly this sentence -- the profiler subsystem is compiled in
|
|
151
|
+
* and the command itself is fine, it simply has nothing recorded yet. */
|
|
152
|
+
export const PROFILING_NOT_STARTED_TEXT = 'No profiling data available. Start profiling with "prof on".';
|
|
153
|
+
|
|
154
|
+
/** The narrow no-break space (U+202F) VICE uses as its thousands separator
|
|
155
|
+
* -- see this module's header comment for the full hazard explanation. */
|
|
156
|
+
const THOUSANDS_SEPARATOR = "\u202f";
|
|
157
|
+
|
|
158
|
+
/** A numeric field: one or more digits, optionally interleaved with the
|
|
159
|
+
* narrow no-break space thousands separator -- NEVER an ASCII space, which
|
|
160
|
+
* is exclusively the inter-field tokenizer's own delimiter (see
|
|
161
|
+
* `tokenizeRow` below). */
|
|
162
|
+
const NUMERIC_FIELD_RE = new RegExp(`^[0-9${THOUSANDS_SEPARATOR}]+$`);
|
|
163
|
+
|
|
164
|
+
/** A percentage field: one or more digits, a decimal separator (comma or
|
|
165
|
+
* period, both accepted -- see this module's header comment), one or more
|
|
166
|
+
* digits, then a trailing `%`. */
|
|
167
|
+
const PERCENT_FIELD_RE = /^(\d+)([,.])(\d+)%$/;
|
|
168
|
+
|
|
169
|
+
function makeRefusal(code: FlatProfileRefusalCode, message: string, line: string, lineNumber: number): TextParseRefusal {
|
|
170
|
+
return { code, message, line, lineNumber };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Tokenizes one data row on runs of the ASCII space character ONLY (never
|
|
174
|
+
* `\s`, never any other Unicode space) -- the one hazard this module exists
|
|
175
|
+
* to defend against. A thousands-separated numeric field survives this
|
|
176
|
+
* split as a single token because its internal separator is U+202F, not
|
|
177
|
+
* U+0020. */
|
|
178
|
+
function tokenizeRow(row: string): string[] {
|
|
179
|
+
return row.split(/ +/).filter((token) => token.length > 0);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
type NumericFieldResult =
|
|
183
|
+
| { ok: true; value: number }
|
|
184
|
+
| { ok: false; code: "unrecognised-separator" | "malformed-row"; detail: string };
|
|
185
|
+
|
|
186
|
+
/** Parses the totalCycles/selfCycles field from its (possibly split-apart,
|
|
187
|
+
* if malformed) token run. A well-formed field is exactly ONE token
|
|
188
|
+
* matching `NUMERIC_FIELD_RE`. When tokenization instead produced more than
|
|
189
|
+
* one token, the field is malformed -- but WHY it split apart matters: if
|
|
190
|
+
* every fragment is digits-only, an ASCII space stood where the narrow
|
|
191
|
+
* no-break space belongs (`unrecognised-separator`, the specific, named
|
|
192
|
+
* hazard this module exists to catch); anything else is a generic
|
|
193
|
+
* structural malformation (`malformed-row`). */
|
|
194
|
+
function parseNumericField(tokens: readonly string[]): NumericFieldResult {
|
|
195
|
+
if (tokens.length !== 1) {
|
|
196
|
+
const detail = tokens.join(" ");
|
|
197
|
+
if (tokens.length > 0 && tokens.every((t) => /^\d+$/.test(t))) {
|
|
198
|
+
return { ok: false, code: "unrecognised-separator", detail };
|
|
199
|
+
}
|
|
200
|
+
return { ok: false, code: "malformed-row", detail };
|
|
201
|
+
}
|
|
202
|
+
const raw = tokens[0]!;
|
|
203
|
+
if (!NUMERIC_FIELD_RE.test(raw)) {
|
|
204
|
+
return { ok: false, code: "malformed-row", detail: raw };
|
|
205
|
+
}
|
|
206
|
+
const digitsOnly = raw.split(THOUSANDS_SEPARATOR).join("");
|
|
207
|
+
return { ok: true, value: Number(digitsOnly) };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
type PercentFieldResult = { ok: true; value: number; separator: "," | "." } | { ok: false };
|
|
211
|
+
|
|
212
|
+
/** Parses a percentage field. Both a comma and a period decimal separator
|
|
213
|
+
* are accepted (the capturing host's locale, not a VICE constant); the
|
|
214
|
+
* OBSERVED separator is returned alongside the numeric value rather than
|
|
215
|
+
* assumed. A token that carries no recognisable decimal shape at all
|
|
216
|
+
* (including one with the wrong separator character, e.g. a semicolon)
|
|
217
|
+
* fails here. */
|
|
218
|
+
function parsePercentField(token: string): PercentFieldResult {
|
|
219
|
+
const match = PERCENT_FIELD_RE.exec(token);
|
|
220
|
+
if (!match) return { ok: false };
|
|
221
|
+
const separator = match[2] as "," | ".";
|
|
222
|
+
return { ok: true, value: Number(`${match[1]}.${match[3]}`), separator };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Parses `prof flat`'s framed text-monitor reply into a structured
|
|
227
|
+
* {@link FlatProfile}. Input is the string `TextMonitorClient.command()`
|
|
228
|
+
* resolves to, or a fixture's `text` -- both must parse identically. Never
|
|
229
|
+
* throws (D-42-3): every failure mode returns `{ ok: false, refusal }`
|
|
230
|
+
* naming exactly what was wrong and where.
|
|
231
|
+
*
|
|
232
|
+
* Bounded by construction: one pass over the payload's lines, each
|
|
233
|
+
* iteration consumes exactly one line, no recursion anywhere in this
|
|
234
|
+
* function. Performs NO sort -- rows are returned in VICE's own emitted
|
|
235
|
+
* order, entry by entry, as encountered.
|
|
236
|
+
*/
|
|
237
|
+
export function parseFlatProfile(text: string): FlatProfileParseResult {
|
|
238
|
+
if (typeof text !== "string" || text.trim() === "") {
|
|
239
|
+
return {
|
|
240
|
+
ok: false,
|
|
241
|
+
refusal: makeRefusal(
|
|
242
|
+
"empty-response",
|
|
243
|
+
"prof flat: the response was empty or whitespace-only -- never decoded as a zero-row profile, because an " +
|
|
244
|
+
"empty response and a real capture that recorded no ranked cycles are two different facts",
|
|
245
|
+
"",
|
|
246
|
+
0,
|
|
247
|
+
),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let body = text.replace(TRAILING_PROMPT_RE, "");
|
|
252
|
+
if (body.trim() === "") {
|
|
253
|
+
return {
|
|
254
|
+
ok: false,
|
|
255
|
+
refusal: makeRefusal(
|
|
256
|
+
"empty-response",
|
|
257
|
+
"prof flat: the response contained only prompt text and no header -- never decoded as a zero-row profile",
|
|
258
|
+
"",
|
|
259
|
+
0,
|
|
260
|
+
),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const trimmedBody = body.trim();
|
|
265
|
+
if (trimmedBody === PROFILING_NOT_STARTED_TEXT) {
|
|
266
|
+
return {
|
|
267
|
+
ok: false,
|
|
268
|
+
refusal: makeRefusal(
|
|
269
|
+
"profiling-not-started",
|
|
270
|
+
`prof flat: the connected machine replied: ${PROFILING_NOT_STARTED_TEXT} -- profiling is not currently ` +
|
|
271
|
+
"running there, this is not a missing build capability, and it is not a failure to read the reply",
|
|
272
|
+
trimmedBody,
|
|
273
|
+
1,
|
|
274
|
+
),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const lines = body.split("\n");
|
|
279
|
+
// A trailing "\n" before the (already-stripped) prompt splits into one
|
|
280
|
+
// trailing empty element -- drop exactly that split artifact, never any
|
|
281
|
+
// other blank line, so a genuinely blank data line still reaches the row
|
|
282
|
+
// parser below and refuses structurally rather than being silently
|
|
283
|
+
// swallowed here.
|
|
284
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
285
|
+
lines.pop();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const headerLine = lines[0] ?? "";
|
|
289
|
+
const rulesLine = lines[1] ?? "";
|
|
290
|
+
if (headerLine !== HEADER_LINE || rulesLine !== RULES_LINE) {
|
|
291
|
+
const badLine = headerLine !== HEADER_LINE ? headerLine : rulesLine;
|
|
292
|
+
const badLineNumber = headerLine !== HEADER_LINE ? 1 : 2;
|
|
293
|
+
return {
|
|
294
|
+
ok: false,
|
|
295
|
+
refusal: makeRefusal(
|
|
296
|
+
"missing-header",
|
|
297
|
+
`prof flat: expected the two-line header ${JSON.stringify(HEADER_LINE)} / ${JSON.stringify(RULES_LINE)}, ` +
|
|
298
|
+
`found ${JSON.stringify(badLine)} at line ${badLineNumber}`,
|
|
299
|
+
badLine,
|
|
300
|
+
badLineNumber,
|
|
301
|
+
),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const dataLines = lines.slice(2);
|
|
306
|
+
if (dataLines.length === 0) {
|
|
307
|
+
return {
|
|
308
|
+
ok: false,
|
|
309
|
+
refusal: makeRefusal(
|
|
310
|
+
"malformed-row",
|
|
311
|
+
"prof flat: the header was present but no data rows followed it -- never decoded as a zero-row profile",
|
|
312
|
+
rulesLine,
|
|
313
|
+
2,
|
|
314
|
+
),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const entries: FlatProfileEntry[] = [];
|
|
319
|
+
let decimalSeparator: "," | "." | undefined;
|
|
320
|
+
|
|
321
|
+
for (let i = 0; i < dataLines.length; i++) {
|
|
322
|
+
const line = dataLines[i]!;
|
|
323
|
+
const lineNumber = i + 3; // two header lines occupy lines 1-2
|
|
324
|
+
|
|
325
|
+
const tokens = tokenizeRow(line);
|
|
326
|
+
if (tokens.length === 0) {
|
|
327
|
+
return {
|
|
328
|
+
ok: false,
|
|
329
|
+
refusal: makeRefusal("malformed-row", `prof flat: line ${lineNumber} is blank where a data row was expected`, line, lineNumber),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const percentIdxs: number[] = [];
|
|
334
|
+
for (let t = 0; t < tokens.length; t++) {
|
|
335
|
+
if (tokens[t]!.endsWith("%")) percentIdxs.push(t);
|
|
336
|
+
}
|
|
337
|
+
if (percentIdxs.length !== 2) {
|
|
338
|
+
return {
|
|
339
|
+
ok: false,
|
|
340
|
+
refusal: makeRefusal(
|
|
341
|
+
"malformed-row",
|
|
342
|
+
`prof flat: line ${lineNumber} does not carry exactly two percentage fields: ${JSON.stringify(line)}`,
|
|
343
|
+
line,
|
|
344
|
+
lineNumber,
|
|
345
|
+
),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
const [p1, p2] = percentIdxs as [number, number];
|
|
349
|
+
|
|
350
|
+
const totalCyclesTokens = tokens.slice(0, p1);
|
|
351
|
+
const selfCyclesTokens = tokens.slice(p1 + 1, p2);
|
|
352
|
+
const addressTokens = tokens.slice(p2 + 1);
|
|
353
|
+
|
|
354
|
+
if (addressTokens.length !== 1) {
|
|
355
|
+
return {
|
|
356
|
+
ok: false,
|
|
357
|
+
refusal: makeRefusal(
|
|
358
|
+
"malformed-row",
|
|
359
|
+
`prof flat: line ${lineNumber} does not carry exactly one address field after the second percentage: ${JSON.stringify(line)}`,
|
|
360
|
+
line,
|
|
361
|
+
lineNumber,
|
|
362
|
+
),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
const addressToken = addressTokens[0]!;
|
|
366
|
+
// "ROOT" (exact, case-sensitive) is VICE's own synthetic top-level
|
|
367
|
+
// pseudo-frame -- MEASURED live (plan 42-09), see FlatProfileEntry's own
|
|
368
|
+
// doc comment. Checked before the hex-digit shape so a genuine ROOT row
|
|
369
|
+
// is never misrouted through the hex-address refusal below.
|
|
370
|
+
const isRootPseudoFrame = addressToken === "ROOT";
|
|
371
|
+
if (!isRootPseudoFrame && !/^[0-9a-fA-F]{4}$/.test(addressToken)) {
|
|
372
|
+
return {
|
|
373
|
+
ok: false,
|
|
374
|
+
refusal: makeRefusal(
|
|
375
|
+
"malformed-row",
|
|
376
|
+
`prof flat: line ${lineNumber} has an address field that is not four hex digits (and is not the literal ` +
|
|
377
|
+
`"ROOT" pseudo-frame): ${JSON.stringify(addressToken)}`,
|
|
378
|
+
line,
|
|
379
|
+
lineNumber,
|
|
380
|
+
),
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const totalCyclesResult = parseNumericField(totalCyclesTokens);
|
|
385
|
+
if (!totalCyclesResult.ok) {
|
|
386
|
+
return {
|
|
387
|
+
ok: false,
|
|
388
|
+
refusal: makeRefusal(
|
|
389
|
+
totalCyclesResult.code,
|
|
390
|
+
`prof flat: line ${lineNumber}'s total-cycles field ${JSON.stringify(totalCyclesResult.detail)} is ` +
|
|
391
|
+
(totalCyclesResult.code === "unrecognised-separator"
|
|
392
|
+
? "split by an ASCII space rather than the narrow no-break space (U+202F) thousands separator"
|
|
393
|
+
: "not a recognised numeric field"),
|
|
394
|
+
line,
|
|
395
|
+
lineNumber,
|
|
396
|
+
),
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
const selfCyclesResult = parseNumericField(selfCyclesTokens);
|
|
400
|
+
if (!selfCyclesResult.ok) {
|
|
401
|
+
return {
|
|
402
|
+
ok: false,
|
|
403
|
+
refusal: makeRefusal(
|
|
404
|
+
selfCyclesResult.code,
|
|
405
|
+
`prof flat: line ${lineNumber}'s self-cycles field ${JSON.stringify(selfCyclesResult.detail)} is ` +
|
|
406
|
+
(selfCyclesResult.code === "unrecognised-separator"
|
|
407
|
+
? "split by an ASCII space rather than the narrow no-break space (U+202F) thousands separator"
|
|
408
|
+
: "not a recognised numeric field"),
|
|
409
|
+
line,
|
|
410
|
+
lineNumber,
|
|
411
|
+
),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const totalPercentToken = tokens[p1]!;
|
|
416
|
+
const totalPercentResult = parsePercentField(totalPercentToken);
|
|
417
|
+
if (!totalPercentResult.ok) {
|
|
418
|
+
return {
|
|
419
|
+
ok: false,
|
|
420
|
+
refusal: makeRefusal(
|
|
421
|
+
"unrecognised-percentage",
|
|
422
|
+
`prof flat: line ${lineNumber}'s total-percent field ${JSON.stringify(totalPercentToken)} does not match ` +
|
|
423
|
+
"digits, a comma or period decimal separator, digits, then a trailing %",
|
|
424
|
+
line,
|
|
425
|
+
lineNumber,
|
|
426
|
+
),
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
const selfPercentToken = tokens[p2]!;
|
|
430
|
+
const selfPercentResult = parsePercentField(selfPercentToken);
|
|
431
|
+
if (!selfPercentResult.ok) {
|
|
432
|
+
return {
|
|
433
|
+
ok: false,
|
|
434
|
+
refusal: makeRefusal(
|
|
435
|
+
"unrecognised-percentage",
|
|
436
|
+
`prof flat: line ${lineNumber}'s self-percent field ${JSON.stringify(selfPercentToken)} does not match ` +
|
|
437
|
+
"digits, a comma or period decimal separator, digits, then a trailing %",
|
|
438
|
+
line,
|
|
439
|
+
lineNumber,
|
|
440
|
+
),
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (decimalSeparator === undefined) {
|
|
445
|
+
decimalSeparator = totalPercentResult.separator;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
entries.push({
|
|
449
|
+
totalCycles: totalCyclesResult.value,
|
|
450
|
+
totalPercent: totalPercentResult.value,
|
|
451
|
+
selfCycles: selfCyclesResult.value,
|
|
452
|
+
selfPercent: selfPercentResult.value,
|
|
453
|
+
address: isRootPseudoFrame ? "ROOT" : parseInt(addressToken, 16),
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return { ok: true, value: { entries, decimalSeparator: decimalSeparator ?? "," } };
|
|
458
|
+
}
|