@fugloy/emu 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/NOTICE.md +27 -0
- package/README.md +119 -2
- package/package.json +30 -3
- package/src/cpu/z80cpu.js +3343 -0
- package/src/debug/debugger.js +777 -0
- package/src/debug/instr.js +90 -0
- package/src/debug/label-file.js +423 -0
- package/src/debug/state.js +214 -0
- package/src/devices/cassette-tape.js +254 -0
- package/src/devices/cassette.js +259 -0
- package/src/devices/disk-image.js +578 -0
- package/src/devices/fdc-wd1793.js +466 -0
- package/src/devices/key-queue.js +261 -0
- package/src/devices/keyboard.js +176 -0
- package/src/devices/printer.js +44 -0
- package/src/devices/sound.js +53 -0
- package/src/devices/video.js +66 -0
- package/src/index.js +22 -0
- package/src/loaders/bas.js +107 -0
- package/src/loaders/cas-format.js +213 -0
- package/src/loaders/cmd-format.js +88 -0
- package/src/loaders/handoff.js +29 -0
- package/src/loaders/load.js +135 -0
- package/src/machine/io.js +290 -0
- package/src/machine/machine.js +295 -0
- package/src/machine/memory.js +207 -0
- package/src/machine/rom-hashes.js +14 -0
- package/src/machine/sha256.js +74 -0
- package/src/machine/trs80-system.js +232 -0
|
@@ -0,0 +1,777 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The spec §7 debugger, over a Machine. Task by task it grows breakpoints, watchpoints, coverage,
|
|
3
|
+
* and a trace; the Machine consults it at each instruction boundary only while `active` is true,
|
|
4
|
+
* so a machine with nothing set pays one boolean check per instruction.
|
|
5
|
+
*
|
|
6
|
+
* Memory and port accesses are seen by swapping the CPU's accessor functions (`readMemory`,
|
|
7
|
+
* `writeMemory`, `readPort`, `writePort`) for instrumented ones only while something listens,
|
|
8
|
+
* and putting the original function objects back when nothing does. An instruction fetch is not
|
|
9
|
+
* a read (phase 3 Ruling 2): `startInstruction` records the instruction's start and length, and
|
|
10
|
+
* read k of the instruction is a fetch when k < len and its address is pc + k (the core fetches
|
|
11
|
+
* every instruction's bytes first, in order; a test checks that for every opcode).
|
|
12
|
+
*/
|
|
13
|
+
import { instructionInfo } from './instr.js';
|
|
14
|
+
import { serializeState, restoreState } from './state.js';
|
|
15
|
+
import { writeLabelFile } from './label-file.js';
|
|
16
|
+
import { sha256Hex } from '../machine/sha256.js';
|
|
17
|
+
|
|
18
|
+
/** The hit log keeps the newest this many hits. */
|
|
19
|
+
const RING = 65536;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Coverage bits, one byte per address (spec §7): `EXEC` is the first byte of an executed
|
|
23
|
+
* instruction, prefix included, and `OPERAND` its other bytes (phase 3 Ruling 1); `READ` and
|
|
24
|
+
* `WRITE` are data accesses, never fetches (Ruling 2); `JUMP_TARGET` is where a taken jump, jump
|
|
25
|
+
* relative, `DJNZ`, or `JP (HL)` landed, and `CALL_TARGET` where a taken call or `RST` did. Where a
|
|
26
|
+
* return lands is not marked (the callers map still records it as `ret-to`).
|
|
27
|
+
*/
|
|
28
|
+
export const COVERAGE = Object.freeze({ EXEC: 1, OPERAND: 2, READ: 4, WRITE: 8, JUMP_TARGET: 16, CALL_TARGET: 32 });
|
|
29
|
+
|
|
30
|
+
/** Callers kinds by index; an edge is keyed `from * 8 + index` within its target. */
|
|
31
|
+
const EDGE_KINDS = /** @type {const} */ (['call', 'jp', 'jr', 'rst', 'ret-to']);
|
|
32
|
+
/** @type {Record<string, number>} An `instructionInfo` kind → its EDGE_KINDS index. */
|
|
33
|
+
const EDGE_OF = { call: 0, jp: 1, 'jp-ind': 1, jr: 2, djnz: 2, rst: 3, ret: 4 };
|
|
34
|
+
const { EXEC, OPERAND, READ, WRITE, JUMP_TARGET, CALL_TARGET } = COVERAGE;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {{ id: number, addr: number, once: boolean, hits: number, when?: (m: any) => boolean }} BreakEntry
|
|
38
|
+
* @typedef {{ id: number, addr: number, len: number, read: boolean, write: boolean, stop: boolean, hits: number }} WatchEntry
|
|
39
|
+
* @typedef {{ id: number, kind: 'read' | 'write', cycle: number, pc: number, addr: number, value: number, prev: number }} Hit
|
|
40
|
+
* @typedef {((addr: number, value: number, pc: number) => void) | null} ReadHook
|
|
41
|
+
* @typedef {((addr: number, value: number, prev: number, pc: number) => void) | null} WriteHook
|
|
42
|
+
* @typedef {((port: number, value: number, dir: 'in' | 'out', pc: number) => void) | null} PortHook
|
|
43
|
+
* @typedef {{ pc: number, bytes: number[], cycles: number, regs?: any }} TraceEntry
|
|
44
|
+
* @typedef {{ from: number, kind: 'call' | 'jp' | 'jr' | 'rst' | 'ret-to', count: number }} CallerEdge
|
|
45
|
+
* @typedef {{ addr: number, name: string, kind: 'code' | 'data', size?: number, source: 'user', note?: string }} UserLabel
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/** A label name: an identifier of up to 32 characters, as the disassembler's hint file takes. */
|
|
49
|
+
const LABEL_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,31}$/;
|
|
50
|
+
|
|
51
|
+
export class Debugger {
|
|
52
|
+
/** @type {boolean} True while anything needs per-instruction work. */
|
|
53
|
+
active = false;
|
|
54
|
+
/** @type {number | null} The id of the breakpoint that made the last `beforeInstruction` stop. */
|
|
55
|
+
stopId = null;
|
|
56
|
+
/** @type {boolean} True while memory reads or writes are instrumented (coverage, a watch, or a memory hook listens). */
|
|
57
|
+
tracksAccess = false;
|
|
58
|
+
/** @type {boolean} True while the Machine must call `startInstruction` before each instruction: memory accesses are instrumented or the trace is on. */
|
|
59
|
+
startsInstruction = false;
|
|
60
|
+
/** @type {boolean} True when the hit log dropped its oldest hits to stay at 65,536. */
|
|
61
|
+
hitsOverflowed = false;
|
|
62
|
+
/**
|
|
63
|
+
* @type {{ onMemRead: ReadHook, onMemWrite: WriteHook, onPort: PortHook }}
|
|
64
|
+
* Called on every data read (fetches excluded), every memory write (with the byte it replaced),
|
|
65
|
+
* and every port access. The last argument, `pc`, is the start of the instruction making the
|
|
66
|
+
* access, as a hit's `pc` (the CPU's PC outside one, or for a port access while no memory
|
|
67
|
+
* access is instrumented). Setting one to a function or null re-instruments the CPU.
|
|
68
|
+
*/
|
|
69
|
+
hooks;
|
|
70
|
+
/**
|
|
71
|
+
* @type {{ enable(): void, disable(): void, clear(): void, readonly enabled: boolean, readonly bits: Uint8Array }}
|
|
72
|
+
* Per-byte coverage and the callers map, behind one switch (phase 3 Ruling 6). `bits` is live;
|
|
73
|
+
* `disable` keeps what was recorded and `clear` empties both.
|
|
74
|
+
*/
|
|
75
|
+
coverage;
|
|
76
|
+
/**
|
|
77
|
+
* @type {{ enable(opts: { depth: number, regs?: boolean }): void, disable(): void, clear(): void,
|
|
78
|
+
* last(n: number): TraceEntry[], collapsed(n: number): { entries: TraceEntry[], repeat: number }[], readonly enabled: boolean }}
|
|
79
|
+
* The instruction trace (spec §7): a preallocated ring of the last `depth` executed instructions.
|
|
80
|
+
* `enable` allocates a fresh, empty ring; `disable` keeps what was recorded; `clear` empties it.
|
|
81
|
+
*/
|
|
82
|
+
trace;
|
|
83
|
+
|
|
84
|
+
#machine;
|
|
85
|
+
/** @type {any} The Z80CPU whose accessors are swapped. */
|
|
86
|
+
#cpu;
|
|
87
|
+
/** @type {{ readMemory: (a: number) => number, writeMemory: (a: number, v: number) => void, readPort: (p: number) => number, writePort: (p: number, v: number) => void }} The CPU's own accessors, restored when nothing listens. */
|
|
88
|
+
#plain;
|
|
89
|
+
/** @type {Map<number, BreakEntry[]>} */
|
|
90
|
+
#breaks = new Map();
|
|
91
|
+
/** @type {Map<number, WatchEntry>} In id order. */
|
|
92
|
+
#watches = new Map();
|
|
93
|
+
/** Per address: bit 0 set when a read watch covers it, bit 1 for a write watch. */
|
|
94
|
+
#watchMask = new Uint8Array(0x10000);
|
|
95
|
+
#nextId = 1;
|
|
96
|
+
/** @type {ReadHook} */
|
|
97
|
+
#onMemRead = null;
|
|
98
|
+
/** @type {WriteHook} */
|
|
99
|
+
#onMemWrite = null;
|
|
100
|
+
/** @type {PortHook} */
|
|
101
|
+
#onPort = null;
|
|
102
|
+
|
|
103
|
+
/** True between `startInstruction` and `afterInstruction`: accesses then belong to the instruction at `#pc`. */
|
|
104
|
+
#inInstr = false;
|
|
105
|
+
#pc = 0;
|
|
106
|
+
/** Bytes the current instruction fetches (0 when reads are not instrumented). */
|
|
107
|
+
#len = 0;
|
|
108
|
+
/** Reads the current instruction has made so far. */
|
|
109
|
+
#reads = 0;
|
|
110
|
+
/** True while `readMemory` is instrumented, so `startInstruction` needs the length. */
|
|
111
|
+
#readsOn = false;
|
|
112
|
+
/** @type {number[]} The ids of the stop watches hit since the last `afterInstruction` (or pending from a breakpoint stop), unsorted. */
|
|
113
|
+
#stopHits = [];
|
|
114
|
+
|
|
115
|
+
/** True while coverage is enabled. */
|
|
116
|
+
#covOn = false;
|
|
117
|
+
/** True while coverage records: enabled, and not inside the keyboard hook's stand-in RET. */
|
|
118
|
+
#marking = false;
|
|
119
|
+
/** True during the keyboard hook's stand-in RET. */
|
|
120
|
+
#inStandIn = false;
|
|
121
|
+
#bits = new Uint8Array(0x10000);
|
|
122
|
+
/** @type {Map<number, Map<number, CallerEdge>>} target → (from * 8 + kind index) → edge */
|
|
123
|
+
#callers = new Map();
|
|
124
|
+
/** @type {string | null} The current instruction's `instructionInfo` kind (null unless coverage looked it up). */
|
|
125
|
+
#kind = null;
|
|
126
|
+
/** @type {number | null} The current instruction's static target. */
|
|
127
|
+
#target = null;
|
|
128
|
+
|
|
129
|
+
/** True while the trace records. */
|
|
130
|
+
#traceOn = false;
|
|
131
|
+
#traceRegs = false;
|
|
132
|
+
#depth = 0;
|
|
133
|
+
/** The slot the next entry fills, and how many slots hold entries. */
|
|
134
|
+
#tHead = 0;
|
|
135
|
+
#tCount = 0;
|
|
136
|
+
#tPcs = new Uint16Array(0);
|
|
137
|
+
#tLens = new Uint8Array(0);
|
|
138
|
+
#tBytes = new Uint8Array(0);
|
|
139
|
+
#tCycles = new Uint16Array(0);
|
|
140
|
+
/** @type {any[]} */
|
|
141
|
+
#tRegs = [];
|
|
142
|
+
/** True between `startInstruction` and `afterInstruction` when an entry is open. */
|
|
143
|
+
#tOpen = false;
|
|
144
|
+
#tStart = 0;
|
|
145
|
+
|
|
146
|
+
/** @type {Hit[]} The hit log, a ring once full: `#ringHead` is the oldest entry. */
|
|
147
|
+
#ring = [];
|
|
148
|
+
#ringHead = 0;
|
|
149
|
+
|
|
150
|
+
/** @type {Map<number, UserLabel>} User labels by address. */
|
|
151
|
+
#labels = new Map();
|
|
152
|
+
/** @type {{ manifest: any, sha256: string } | null} The last `m.load.*` call's manifest and the SHA-256 of the bytes it was given. */
|
|
153
|
+
#loaded = null;
|
|
154
|
+
/** @type {{ addr: number, bytes: Uint8Array }[]} What the last load placed in memory: each landed manifest block's bytes, read back once the loader finished. */
|
|
155
|
+
#placed = [];
|
|
156
|
+
|
|
157
|
+
/** @param {any} machine */
|
|
158
|
+
constructor(machine) {
|
|
159
|
+
this.#machine = machine;
|
|
160
|
+
const cpu = machine.system.cpu;
|
|
161
|
+
this.#cpu = cpu;
|
|
162
|
+
this.#plain = { readMemory: cpu.readMemory, writeMemory: cpu.writeMemory, readPort: cpu.readPort, writePort: cpu.writePort };
|
|
163
|
+
const dbg = this;
|
|
164
|
+
/** @param {string} name @param {unknown} f */
|
|
165
|
+
const check = (name, f) => {
|
|
166
|
+
if (f !== null && typeof f !== 'function') throw new Error(`hooks.${name} must be a function or null, not ${String(f)}`);
|
|
167
|
+
};
|
|
168
|
+
this.hooks = {
|
|
169
|
+
get onMemRead() {
|
|
170
|
+
return dbg.#onMemRead;
|
|
171
|
+
},
|
|
172
|
+
set onMemRead(f) {
|
|
173
|
+
check('onMemRead', f);
|
|
174
|
+
dbg.#onMemRead = f;
|
|
175
|
+
dbg.#refresh();
|
|
176
|
+
},
|
|
177
|
+
get onMemWrite() {
|
|
178
|
+
return dbg.#onMemWrite;
|
|
179
|
+
},
|
|
180
|
+
set onMemWrite(f) {
|
|
181
|
+
check('onMemWrite', f);
|
|
182
|
+
dbg.#onMemWrite = f;
|
|
183
|
+
dbg.#refresh();
|
|
184
|
+
},
|
|
185
|
+
get onPort() {
|
|
186
|
+
return dbg.#onPort;
|
|
187
|
+
},
|
|
188
|
+
set onPort(f) {
|
|
189
|
+
check('onPort', f);
|
|
190
|
+
dbg.#onPort = f;
|
|
191
|
+
dbg.#refresh();
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
this.coverage = {
|
|
195
|
+
enable() {
|
|
196
|
+
dbg.#covOn = true;
|
|
197
|
+
dbg.#refresh();
|
|
198
|
+
},
|
|
199
|
+
disable() {
|
|
200
|
+
dbg.#covOn = false;
|
|
201
|
+
dbg.#refresh();
|
|
202
|
+
},
|
|
203
|
+
clear() {
|
|
204
|
+
dbg.#bits.fill(0);
|
|
205
|
+
dbg.#callers.clear();
|
|
206
|
+
},
|
|
207
|
+
get enabled() {
|
|
208
|
+
return dbg.#covOn;
|
|
209
|
+
},
|
|
210
|
+
get bits() {
|
|
211
|
+
return dbg.#bits;
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
this.trace = {
|
|
215
|
+
enable({ depth, regs = false } = /** @type {any} */ ({})) {
|
|
216
|
+
if (!Number.isInteger(depth) || depth < 1 || depth > 1000000) throw new Error(`trace: depth must be an integer 1..1000000 (the trace is never unlimited), not ${String(depth)}`);
|
|
217
|
+
dbg.#depth = depth;
|
|
218
|
+
dbg.#traceRegs = !!regs;
|
|
219
|
+
dbg.#tPcs = new Uint16Array(depth);
|
|
220
|
+
dbg.#tLens = new Uint8Array(depth);
|
|
221
|
+
dbg.#tBytes = new Uint8Array(depth * 4);
|
|
222
|
+
dbg.#tCycles = new Uint16Array(depth);
|
|
223
|
+
dbg.#tRegs = regs ? new Array(depth) : [];
|
|
224
|
+
dbg.#tHead = 0;
|
|
225
|
+
dbg.#tCount = 0;
|
|
226
|
+
dbg.#tOpen = false;
|
|
227
|
+
dbg.#traceOn = true;
|
|
228
|
+
dbg.#refresh();
|
|
229
|
+
},
|
|
230
|
+
disable() {
|
|
231
|
+
dbg.#traceOn = false;
|
|
232
|
+
dbg.#tOpen = false;
|
|
233
|
+
dbg.#refresh();
|
|
234
|
+
},
|
|
235
|
+
clear() {
|
|
236
|
+
dbg.#tHead = 0;
|
|
237
|
+
dbg.#tCount = 0;
|
|
238
|
+
dbg.#tOpen = false;
|
|
239
|
+
if (dbg.#traceRegs) dbg.#tRegs = new Array(dbg.#depth);
|
|
240
|
+
},
|
|
241
|
+
last: (n) => dbg.#last(n),
|
|
242
|
+
collapsed: (n) => collapse(dbg.#last(n)),
|
|
243
|
+
get enabled() {
|
|
244
|
+
return dbg.#traceOn;
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* A serializable snapshot of the machine (spec §7): everything `serializeState` carries. It
|
|
251
|
+
* survives `JSON.stringify` and `parse`.
|
|
252
|
+
* @returns {any}
|
|
253
|
+
*/
|
|
254
|
+
snapshot() {
|
|
255
|
+
return serializeState(this.#machine.system);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Restore the machine exactly from a snapshot, cycles included, then start a fresh timeline:
|
|
260
|
+
* the key queue and the sound ring are cleared, and the Machine forgets where the last run
|
|
261
|
+
* stopped and any watch stop pending. Breakpoints, watches, hooks, the hit log, coverage,
|
|
262
|
+
* callers, the trace, and labels stay as they were.
|
|
263
|
+
* @param {any} snap
|
|
264
|
+
*/
|
|
265
|
+
restore(snap) {
|
|
266
|
+
restoreState(this.#machine.system, snap);
|
|
267
|
+
this.#stopHits = [];
|
|
268
|
+
this.#inInstr = false;
|
|
269
|
+
this.#tOpen = false;
|
|
270
|
+
this.#machine.newTimeline();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Name an address (a `source: 'user'` label in the label file, which wins over an observed one
|
|
275
|
+
* there). A second label at the same address replaces the first.
|
|
276
|
+
* @param {number} addr 0..$FFFF
|
|
277
|
+
* @param {string} name an identifier: a letter or `_`, then letters, digits, or `_`, up to 32 characters
|
|
278
|
+
* @param {{ kind?: 'code' | 'data', size?: number, note?: string }} [opts] `size` is how many bytes the label covers (1..65536); `note` is a remark for a human, with no control character
|
|
279
|
+
*/
|
|
280
|
+
label(addr, name, { kind = 'code', size, note } = {}) {
|
|
281
|
+
if (!Number.isInteger(addr) || addr < 0 || addr > 0xffff) throw new Error(`label: address must be an integer 0..65535 ($0000..$FFFF), not ${String(addr)}`);
|
|
282
|
+
if (typeof name !== 'string' || !LABEL_NAME.test(name)) throw new Error(`label: name must be an identifier (a letter or _, then letters, digits, or _, up to 32 characters), not ${JSON.stringify(name)}`);
|
|
283
|
+
if (kind !== 'code' && kind !== 'data') throw new Error(`label: kind must be 'code' or 'data', not ${String(kind)}`);
|
|
284
|
+
if (size !== undefined && (!Number.isInteger(size) || size < 1 || size > 0x10000)) throw new Error(`label: size must be an integer 1..65536, not ${String(size)}`);
|
|
285
|
+
if (note !== undefined && (typeof note !== 'string' || /[\u0000-\u001f\u007f]/.test(note))) throw new Error(`label: note must be a string with no control character, not ${JSON.stringify(note)}`);
|
|
286
|
+
/** @type {UserLabel} */
|
|
287
|
+
const l = size === undefined ? { addr, name, kind, source: 'user' } : { addr, name, kind, size, source: 'user' };
|
|
288
|
+
if (note !== undefined) l.note = note;
|
|
289
|
+
this.#labels.set(addr, l);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** @returns {UserLabel[]} the user labels, copies, sorted by address */
|
|
293
|
+
get labels() {
|
|
294
|
+
return [...this.#labels.values()].sort((a, b) => a.addr - b.addr).map((l) => ({ ...l }));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** @returns {{ manifest: any, sha256: string } | null} the last `m.load.*` call's manifest and the SHA-256 (hex) of the bytes it was given, or null */
|
|
298
|
+
get loaded() {
|
|
299
|
+
return this.#loaded === null ? null : JSON.parse(JSON.stringify(this.#loaded));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Internal: called by each `m.load.*` after it succeeds.
|
|
304
|
+
* @param {any} manifest
|
|
305
|
+
* @param {Uint8Array} bytes the bytes the loader was given
|
|
306
|
+
*/
|
|
307
|
+
recordLoad(manifest, bytes) {
|
|
308
|
+
this.#loaded = { manifest: JSON.parse(JSON.stringify(manifest)), sha256: sha256Hex(bytes) };
|
|
309
|
+
// A copy of what landed, so the label file can tell which loaded bytes changed since (a
|
|
310
|
+
// block still `predicted` has not landed). Read after the loader, so a later block wins.
|
|
311
|
+
const read = this.#plain.readMemory;
|
|
312
|
+
this.#placed = manifest.blocks
|
|
313
|
+
.filter((/** @type {any} */ b) => !b.predicted)
|
|
314
|
+
.map((/** @type {any} */ b) => {
|
|
315
|
+
const placed = new Uint8Array(b.len);
|
|
316
|
+
for (let i = 0; i < b.len; i++) placed[i] = read((b.addr + i) & 0xffff);
|
|
317
|
+
return { addr: b.addr, bytes: placed };
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* The spec §8 label and coverage file for this session: the program (named by `name`, else by
|
|
323
|
+
* the last load's manifest), its loaded blocks and entry, labels (`ENTRY`, observed call
|
|
324
|
+
* targets in the loaded blocks, user labels), the six coverage fields, the callers map, and one
|
|
325
|
+
* session. The core reads no clock: `started` is recorded only when the caller passes it.
|
|
326
|
+
* `write` marks a byte written while coverage was on, or a loaded byte that differs now from
|
|
327
|
+
* what the load placed there (so a program that rewrote itself before coverage started still
|
|
328
|
+
* says so); the live `coverage.bits` are not changed.
|
|
329
|
+
* @param {{ name?: string, started?: string, input?: string }} [opts]
|
|
330
|
+
* @returns {import('./label-file.js').LabelFile}
|
|
331
|
+
*/
|
|
332
|
+
labelFile({ name, started, input } = {}) {
|
|
333
|
+
const bits = this.#bits.slice();
|
|
334
|
+
const read = this.#plain.readMemory;
|
|
335
|
+
for (const { addr, bytes } of this.#placed) {
|
|
336
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
337
|
+
const a = (addr + i) & 0xffff;
|
|
338
|
+
if (read(a) !== bytes[i]) bits[a] |= WRITE;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return writeLabelFile({
|
|
342
|
+
name,
|
|
343
|
+
started,
|
|
344
|
+
input,
|
|
345
|
+
loaded: this.#loaded,
|
|
346
|
+
userLabels: [...this.#labels.values()],
|
|
347
|
+
bits,
|
|
348
|
+
callerMap: this.callerMap(),
|
|
349
|
+
cycles: this.#cpu.cycles,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** @param {number} n @returns {TraceEntry[]} the newest `n` entries, oldest first */
|
|
354
|
+
#last(n) {
|
|
355
|
+
const count = Math.min(Math.max(0, Math.floor(n) || 0), this.#tCount);
|
|
356
|
+
const out = [];
|
|
357
|
+
const depth = this.#depth;
|
|
358
|
+
for (let k = count; k > 0; k--) {
|
|
359
|
+
const i = (this.#tHead - k + depth) % depth;
|
|
360
|
+
const len = this.#tLens[i];
|
|
361
|
+
/** @type {TraceEntry} */
|
|
362
|
+
const e = { pc: this.#tPcs[i], bytes: Array.from(this.#tBytes.subarray(i * 4, i * 4 + len)), cycles: this.#tCycles[i] };
|
|
363
|
+
if (this.#traceRegs) e.regs = { ...this.#tRegs[i] };
|
|
364
|
+
out.push(e);
|
|
365
|
+
}
|
|
366
|
+
return out;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Stop before the instruction at `addr` runs.
|
|
371
|
+
* @param {number} addr 0..$FFFF
|
|
372
|
+
* @param {{ once?: boolean, when?: (m: any) => boolean }} [opts] `once` removes it after its first hit; `when` gates each hit
|
|
373
|
+
* @returns {number} the breakpoint id (ids count up from 1 across breakpoints and watches)
|
|
374
|
+
*/
|
|
375
|
+
break(addr, { once = false, when } = {}) {
|
|
376
|
+
if (!Number.isInteger(addr) || addr < 0 || addr > 0xffff) throw new Error(`break: address must be an integer 0..65535 ($0000..$FFFF), not ${String(addr)}`);
|
|
377
|
+
if (when !== undefined && typeof when !== 'function') throw new Error(`break: when must be a function, not ${String(when)}`);
|
|
378
|
+
const id = this.#nextId++;
|
|
379
|
+
const list = this.#breaks.get(addr);
|
|
380
|
+
const entry = { id, addr, once, hits: 0, when };
|
|
381
|
+
if (list) list.push(entry);
|
|
382
|
+
else this.#breaks.set(addr, [entry]);
|
|
383
|
+
this.#refresh();
|
|
384
|
+
return id;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Remove a breakpoint.
|
|
389
|
+
* @param {number} id
|
|
390
|
+
* @returns {boolean} false when no breakpoint has that id
|
|
391
|
+
*/
|
|
392
|
+
unbreak(id) {
|
|
393
|
+
for (const [addr, list] of this.#breaks) {
|
|
394
|
+
const k = list.findIndex((e) => e.id === id);
|
|
395
|
+
if (k < 0) continue;
|
|
396
|
+
list.splice(k, 1);
|
|
397
|
+
if (list.length === 0) this.#breaks.delete(addr);
|
|
398
|
+
this.#refresh();
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** @returns {{ id: number, addr: number, once: boolean, hits: number }[]} the breakpoints, in id order */
|
|
405
|
+
get breaks() {
|
|
406
|
+
const out = [];
|
|
407
|
+
for (const list of this.#breaks.values()) for (const e of list) out.push({ id: e.id, addr: e.addr, once: e.once, hits: e.hits });
|
|
408
|
+
return out.sort((a, b) => a.id - b.id);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Watch memory accesses to `len` bytes from `addr` (wrapping at $FFFF). Each matching access is
|
|
413
|
+
* logged (`hits`); with `stop`, `run` returns `{ reason: 'watch', id }` after the instruction
|
|
414
|
+
* that made it. Instruction fetches are not reads.
|
|
415
|
+
* @param {number} addr 0..$FFFF
|
|
416
|
+
* @param {number} len 1..65536
|
|
417
|
+
* @param {{ read?: boolean, write?: boolean, stop?: boolean }} [opts] at least one of `read` and `write`
|
|
418
|
+
* @returns {number} the watch id (ids count up from 1 across breakpoints and watches)
|
|
419
|
+
*/
|
|
420
|
+
watch(addr, len, { read = false, write = true, stop = false } = {}) {
|
|
421
|
+
if (!Number.isInteger(addr) || addr < 0 || addr > 0xffff) throw new Error(`watch: address must be an integer 0..65535 ($0000..$FFFF), not ${String(addr)}`);
|
|
422
|
+
if (!Number.isInteger(len) || len < 1 || len > 0x10000) throw new Error(`watch: len must be an integer 1..65536, not ${String(len)}`);
|
|
423
|
+
if (!read && !write) throw new Error('watch: it must watch read or write (or both); with neither it would never hit');
|
|
424
|
+
const id = this.#nextId++;
|
|
425
|
+
this.#watches.set(id, { id, addr, len, read: !!read, write: !!write, stop: !!stop, hits: 0 });
|
|
426
|
+
this.#rebuildMask();
|
|
427
|
+
this.#refresh();
|
|
428
|
+
return id;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Remove a watchpoint. Its hits stay in the log.
|
|
433
|
+
* @param {number} id
|
|
434
|
+
* @returns {boolean} false when no watch has that id
|
|
435
|
+
*/
|
|
436
|
+
unwatch(id) {
|
|
437
|
+
if (!this.#watches.delete(id)) return false;
|
|
438
|
+
// A stop this watch made and `run` has not yet returned goes with it.
|
|
439
|
+
if (this.#stopHits.length > 0) this.#stopHits = this.#stopHits.filter((x) => x !== id);
|
|
440
|
+
this.#rebuildMask();
|
|
441
|
+
this.#refresh();
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** @returns {WatchEntry[]} the watchpoints, in id order */
|
|
446
|
+
get watches() {
|
|
447
|
+
return [...this.#watches.values()].map((w) => ({ ...w }));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* The hit log, oldest first: the newest 65,536 hits (`hitsOverflowed` tells whether older ones
|
|
452
|
+
* were dropped). `pc` is the start of the instruction making the access, or the CPU's PC for an
|
|
453
|
+
* access outside one (an interrupt response, the keyboard hook's stand-in RET); `cycle` is
|
|
454
|
+
* `m.cycles` at the access; `prev` is the byte a write replaced, and equals `value` for a read.
|
|
455
|
+
* @param {number} [id] only this watch's hits
|
|
456
|
+
* @returns {Hit[]}
|
|
457
|
+
*/
|
|
458
|
+
hits(id) {
|
|
459
|
+
const ring = this.#ring;
|
|
460
|
+
const out = ring.length < RING ? ring.slice() : [...ring.slice(this.#ringHead), ...ring.slice(0, this.#ringHead)];
|
|
461
|
+
return id === undefined ? out : out.filter((h) => h.id === id);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Who transferred control to `addr`, with how many times each edge was taken: calls, jumps
|
|
466
|
+
* (`JP (HL)` included), relative jumps (`DJNZ` included), `RST`s, and returns (`ret-to`).
|
|
467
|
+
* Recorded while coverage is enabled; an interrupt response and the keyboard hook's stand-in
|
|
468
|
+
* RET are not instructions and record nothing.
|
|
469
|
+
* @param {number} addr
|
|
470
|
+
* @returns {CallerEdge[]} fresh objects, sorted by `from`, then `kind`
|
|
471
|
+
*/
|
|
472
|
+
callers(addr) {
|
|
473
|
+
const edges = this.#callers.get(addr & 0xffff);
|
|
474
|
+
return edges === undefined ? [] : sortEdges(edges);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** @returns {Map<number, CallerEdge[]>} every target's callers, as `callers` gives them, in the order targets were first reached */
|
|
478
|
+
callerMap() {
|
|
479
|
+
const out = new Map();
|
|
480
|
+
for (const [target, edges] of this.#callers) out.set(target, sortEdges(edges));
|
|
481
|
+
return out;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** Empty the hit log and clear `hitsOverflowed`. */
|
|
485
|
+
clearHits() {
|
|
486
|
+
this.#ring = [];
|
|
487
|
+
this.#ringHead = 0;
|
|
488
|
+
this.hitsOverflowed = false;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Internal: called at an instruction boundary before the instruction runs.
|
|
493
|
+
* @param {number} pc
|
|
494
|
+
* @param {boolean} first true for a run's first boundary, which never stops, so a run resumes past the breakpoint it stopped at
|
|
495
|
+
* @returns {boolean} true to stop; `stopId` then names the breakpoint (the lowest id when several hit)
|
|
496
|
+
*/
|
|
497
|
+
beforeInstruction(pc, first) {
|
|
498
|
+
if (first) return false;
|
|
499
|
+
const list = this.#breaks.get(pc);
|
|
500
|
+
if (list === undefined) return false;
|
|
501
|
+
let stop = null;
|
|
502
|
+
for (const e of list.slice()) {
|
|
503
|
+
if (e.when !== undefined && !e.when(this.#machine)) continue;
|
|
504
|
+
e.hits++;
|
|
505
|
+
if (stop === null) stop = e.id;
|
|
506
|
+
if (e.once) this.unbreak(e.id);
|
|
507
|
+
}
|
|
508
|
+
this.stopId = stop;
|
|
509
|
+
return stop !== null;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Internal: called while `tracksAccess`, once the boundary is certain to execute an instruction
|
|
514
|
+
* at `pc` (not for a breakpoint stop, a halted CPU's idle, or the keyboard hook's stand-in RET).
|
|
515
|
+
* @param {number} pc
|
|
516
|
+
*/
|
|
517
|
+
startInstruction(pc) {
|
|
518
|
+
this.#pc = pc;
|
|
519
|
+
this.#reads = 0;
|
|
520
|
+
this.#inInstr = true;
|
|
521
|
+
// Reset each instruction, so coverage enabled by a hook mid-instruction records no stale edge.
|
|
522
|
+
this.#kind = null;
|
|
523
|
+
if (this.#traceOn) this.#traceStart(pc);
|
|
524
|
+
if (!this.#readsOn) {
|
|
525
|
+
this.#len = 0;
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
// The look-ahead assumes reads have no side effects: true on the Model III; the Model I's $37E0
|
|
529
|
+
// interrupt latch (phase 6) clears on read and will need a side-effect-free peek.
|
|
530
|
+
const info = instructionInfo(this.#plain.readMemory, pc);
|
|
531
|
+
const len = info.len;
|
|
532
|
+
this.#len = len;
|
|
533
|
+
if (this.#covOn) {
|
|
534
|
+
this.#kind = info.kind;
|
|
535
|
+
this.#target = info.target;
|
|
536
|
+
const bits = this.#bits;
|
|
537
|
+
bits[pc] |= EXEC;
|
|
538
|
+
for (let k = 1; k < len; k++) bits[(pc + k) & 0xffff] |= OPERAND;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Internal: on a breakpoint stop, close any open instruction but keep a pending watch stop (one
|
|
544
|
+
* the boundary's interrupt response hit), so the next run returns it after the held instruction.
|
|
545
|
+
*/
|
|
546
|
+
endInstruction() {
|
|
547
|
+
this.#inInstr = false;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Internal: called after each boundary while `active`. Ends the instruction `startInstruction` began.
|
|
552
|
+
* @returns {number | null} the lowest id of a stop watch hit since the last call, or null
|
|
553
|
+
*/
|
|
554
|
+
afterInstruction() {
|
|
555
|
+
if (this.#tOpen) {
|
|
556
|
+
this.#tCycles[(this.#tHead - 1 + this.#depth) % this.#depth] = this.#cpu.cycles - this.#tStart;
|
|
557
|
+
this.#tOpen = false;
|
|
558
|
+
}
|
|
559
|
+
if (this.#inInstr && this.#covOn && this.#kind !== null) this.#transfer();
|
|
560
|
+
this.#inInstr = false;
|
|
561
|
+
const hits = this.#stopHits;
|
|
562
|
+
if (hits.length === 0) return null;
|
|
563
|
+
this.#stopHits = [];
|
|
564
|
+
return Math.min(...hits);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Internal: around the keyboard hook's stand-in RET, which executes no instruction and so marks
|
|
569
|
+
* no coverage (its stack reads are still logged by watches and hooks).
|
|
570
|
+
* @param {boolean} on
|
|
571
|
+
*/
|
|
572
|
+
standIn(on) {
|
|
573
|
+
this.#inStandIn = on;
|
|
574
|
+
this.#marking = this.#covOn && !on;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** Open the next trace entry for the instruction at `pc`: its bytes and registers before it runs; `afterInstruction` adds its T-states. */
|
|
578
|
+
#traceStart(/** @type {number} */ pc) {
|
|
579
|
+
const i = this.#tHead;
|
|
580
|
+
const read = this.#plain.readMemory;
|
|
581
|
+
const len = instructionInfo(read, pc).len;
|
|
582
|
+
this.#tPcs[i] = pc;
|
|
583
|
+
this.#tLens[i] = len;
|
|
584
|
+
for (let k = 0; k < len; k++) this.#tBytes[i * 4 + k] = read((pc + k) & 0xffff);
|
|
585
|
+
if (this.#traceRegs) this.#tRegs[i] = this.#machine.cpu.regs;
|
|
586
|
+
this.#tHead = (i + 1) % this.#depth;
|
|
587
|
+
if (this.#tCount < this.#depth) this.#tCount++;
|
|
588
|
+
this.#tStart = this.#cpu.cycles;
|
|
589
|
+
this.#tOpen = true;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** After a control-transfer instruction: when it was taken, mark where it landed (not for a return) and count the edge. */
|
|
593
|
+
#transfer() {
|
|
594
|
+
const kind = /** @type {string} */ (this.#kind);
|
|
595
|
+
const pc = this.#pc;
|
|
596
|
+
const to = this.#cpu.registers.PC & 0xffff;
|
|
597
|
+
if (kind === 'ret') {
|
|
598
|
+
if (to === ((pc + this.#len) & 0xffff)) return;
|
|
599
|
+
} else if (kind !== 'jp-ind' && to !== this.#target) return;
|
|
600
|
+
const e = EDGE_OF[kind];
|
|
601
|
+
if (e === 0 || e === 3) this.#bits[to] |= CALL_TARGET;
|
|
602
|
+
else if (e !== 4) this.#bits[to] |= JUMP_TARGET;
|
|
603
|
+
let edges = this.#callers.get(to);
|
|
604
|
+
if (edges === undefined) {
|
|
605
|
+
edges = new Map();
|
|
606
|
+
this.#callers.set(to, edges);
|
|
607
|
+
}
|
|
608
|
+
const key = pc * 8 + e;
|
|
609
|
+
const edge = edges.get(key);
|
|
610
|
+
if (edge !== undefined) edge.count++;
|
|
611
|
+
else edges.set(key, { from: pc, kind: EDGE_KINDS[e], count: 1 });
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/** The instrumented `readMemory`: fetches pass straight through. */
|
|
615
|
+
#read = (/** @type {number} */ addr) => {
|
|
616
|
+
const value = this.#plain.readMemory(addr);
|
|
617
|
+
if (this.#inInstr) {
|
|
618
|
+
const k = this.#reads++;
|
|
619
|
+
if (k < this.#len && addr === ((this.#pc + k) & 0xffff)) return value;
|
|
620
|
+
}
|
|
621
|
+
addr &= 0xffff;
|
|
622
|
+
if (this.#marking) this.#bits[addr] |= READ;
|
|
623
|
+
if (this.#watchMask[addr] & 1) this.#hit('read', addr, value, value);
|
|
624
|
+
if (this.#onMemRead !== null) this.#onMemRead(addr, value, this.#accessPc());
|
|
625
|
+
return value;
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
/** The instrumented `writeMemory`. */
|
|
629
|
+
#write = (/** @type {number} */ addr, /** @type {number} */ value) => {
|
|
630
|
+
addr &= 0xffff;
|
|
631
|
+
value &= 0xff;
|
|
632
|
+
const watched = (this.#watchMask[addr] & 2) !== 0;
|
|
633
|
+
const prev = watched || this.#onMemWrite !== null ? this.#plain.readMemory(addr) : 0;
|
|
634
|
+
this.#plain.writeMemory(addr, value);
|
|
635
|
+
if (this.#marking) this.#bits[addr] |= WRITE;
|
|
636
|
+
if (watched) this.#hit('write', addr, value, prev);
|
|
637
|
+
if (this.#onMemWrite !== null) this.#onMemWrite(addr, value, prev, this.#accessPc());
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
/** The instrumented `readPort`. */
|
|
641
|
+
#readPort = (/** @type {number} */ port) => {
|
|
642
|
+
const value = this.#plain.readPort(port);
|
|
643
|
+
if (this.#onPort !== null) this.#onPort(port, value & 0xff, 'in', this.#accessPc());
|
|
644
|
+
return value;
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
/** The instrumented `writePort`. */
|
|
648
|
+
#writePort = (/** @type {number} */ port, /** @type {number} */ value) => {
|
|
649
|
+
this.#plain.writePort(port, value);
|
|
650
|
+
if (this.#onPort !== null) this.#onPort(port, value & 0xff, 'out', this.#accessPc());
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
/** The instruction making the current access: its start inside one, else the CPU's PC. */
|
|
654
|
+
#accessPc() {
|
|
655
|
+
return this.#inInstr ? this.#pc : this.#cpu.registers.PC;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Log an access every matching watch sees, in id order.
|
|
660
|
+
* @param {'read' | 'write'} kind
|
|
661
|
+
* @param {number} addr
|
|
662
|
+
* @param {number} value
|
|
663
|
+
* @param {number} prev
|
|
664
|
+
*/
|
|
665
|
+
#hit(kind, addr, value, prev) {
|
|
666
|
+
const pc = this.#accessPc();
|
|
667
|
+
const cycle = this.#cpu.cycles;
|
|
668
|
+
for (const w of this.#watches.values()) {
|
|
669
|
+
if (!(kind === 'read' ? w.read : w.write) || ((addr - w.addr) & 0xffff) >= w.len) continue;
|
|
670
|
+
w.hits++;
|
|
671
|
+
const h = Object.freeze({ id: w.id, kind, cycle, pc, addr, value, prev });
|
|
672
|
+
if (this.#ring.length < RING) this.#ring.push(h);
|
|
673
|
+
else {
|
|
674
|
+
this.#ring[this.#ringHead] = h;
|
|
675
|
+
this.#ringHead = (this.#ringHead + 1) % RING;
|
|
676
|
+
this.hitsOverflowed = true;
|
|
677
|
+
}
|
|
678
|
+
if (w.stop && !this.#stopHits.includes(w.id)) this.#stopHits.push(w.id);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
#rebuildMask() {
|
|
683
|
+
const mask = this.#watchMask;
|
|
684
|
+
mask.fill(0);
|
|
685
|
+
for (const w of this.#watches.values()) {
|
|
686
|
+
const bits = (w.read ? 1 : 0) | (w.write ? 2 : 0);
|
|
687
|
+
for (let k = 0; k < w.len; k++) mask[(w.addr + k) & 0xffff] |= bits;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Install the instrumented CPU accessors that something listens to, and the plain ones for
|
|
693
|
+
* the rest. Coverage needs the instrumented reads and writes.
|
|
694
|
+
*/
|
|
695
|
+
#instrument() {
|
|
696
|
+
let reads = this.#onMemRead !== null || this.#covOn;
|
|
697
|
+
let writes = this.#onMemWrite !== null || this.#covOn;
|
|
698
|
+
for (const w of this.#watches.values()) {
|
|
699
|
+
reads ||= w.read;
|
|
700
|
+
writes ||= w.write;
|
|
701
|
+
}
|
|
702
|
+
const ports = this.#onPort !== null;
|
|
703
|
+
const cpu = this.#cpu;
|
|
704
|
+
const plain = this.#plain;
|
|
705
|
+
cpu.readMemory = reads ? this.#read : plain.readMemory;
|
|
706
|
+
cpu.writeMemory = writes ? this.#write : plain.writeMemory;
|
|
707
|
+
cpu.readPort = ports ? this.#readPort : plain.readPort;
|
|
708
|
+
cpu.writePort = ports ? this.#writePort : plain.writePort;
|
|
709
|
+
this.#readsOn = reads;
|
|
710
|
+
this.#marking = this.#covOn && !this.#inStandIn;
|
|
711
|
+
this.tracksAccess = reads || writes;
|
|
712
|
+
this.startsInstruction = this.tracksAccess || this.#traceOn;
|
|
713
|
+
if (!this.startsInstruction) this.#inInstr = false;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
#refresh() {
|
|
717
|
+
this.#instrument();
|
|
718
|
+
this.active = this.#covOn || this.#traceOn || this.#breaks.size > 0 || this.#watches.size > 0 || this.#onMemRead !== null || this.#onMemWrite !== null || this.#onPort !== null;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* @param {Map<number, CallerEdge>} edges
|
|
724
|
+
* @returns {CallerEdge[]} copies sorted by `from`, then `kind`
|
|
725
|
+
*/
|
|
726
|
+
function sortEdges(edges) {
|
|
727
|
+
return [...edges.values()]
|
|
728
|
+
.map((e) => ({ from: e.from, kind: e.kind, count: e.count }))
|
|
729
|
+
.sort((a, b) => a.from - b.from || (a.kind < b.kind ? -1 : a.kind > b.kind ? 1 : 0));
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Merge consecutive repetitions of one PC sequence. Greedy from the oldest entry: at each
|
|
734
|
+
* position try periods 1..64 and take the one covering the most entries with at least 2
|
|
735
|
+
* repeats (the shortest on a tie); otherwise the entry stands alone with `repeat: 1`.
|
|
736
|
+
* @param {TraceEntry[]} list
|
|
737
|
+
* @returns {{ entries: TraceEntry[], repeat: number }[]}
|
|
738
|
+
*/
|
|
739
|
+
function collapse(list) {
|
|
740
|
+
const out = [];
|
|
741
|
+
const n = list.length;
|
|
742
|
+
let i = 0;
|
|
743
|
+
while (i < n) {
|
|
744
|
+
let bestP = 0;
|
|
745
|
+
let bestR = 0;
|
|
746
|
+
let bestCover = 0;
|
|
747
|
+
for (let p = 1; p <= 64 && i + 2 * p <= n; p++) {
|
|
748
|
+
let r = 1;
|
|
749
|
+
for (;;) {
|
|
750
|
+
const at = i + r * p;
|
|
751
|
+
if (at + p > n) break;
|
|
752
|
+
let same = true;
|
|
753
|
+
for (let k = 0; k < p; k++) {
|
|
754
|
+
if (list[at + k].pc !== list[i + k].pc) {
|
|
755
|
+
same = false;
|
|
756
|
+
break;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
if (!same) break;
|
|
760
|
+
r++;
|
|
761
|
+
}
|
|
762
|
+
if (r >= 2 && r * p > bestCover) {
|
|
763
|
+
bestP = p;
|
|
764
|
+
bestR = r;
|
|
765
|
+
bestCover = r * p;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
if (bestP === 0) {
|
|
769
|
+
out.push({ entries: [list[i]], repeat: 1 });
|
|
770
|
+
i++;
|
|
771
|
+
} else {
|
|
772
|
+
out.push({ entries: list.slice(i, i + bestP), repeat: bestR });
|
|
773
|
+
i += bestCover;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
return out;
|
|
777
|
+
}
|