@specy/x86 2.4.0 → 2.6.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/dist/index.mjs CHANGED
@@ -1112,6 +1112,17 @@ function emptyFpuStateBlock() {
1112
1112
  }
1113
1113
  //#endregion
1114
1114
  //#region src/blink-runtime.ts
1115
+ /** The machine's page size: a page is contiguous in the host heap, the next page need not be. */
1116
+ const BLINK_PAGE_SIZE = 4096;
1117
+ /**
1118
+ * The shadow-memory range blink keeps for its own bookkeeping, which the
1119
+ * memory bridge refuses to read. Kept in step with `IsShadow` in
1120
+ * `blink/blinkenlib.c`, so the fast read path refuses exactly what the bridge
1121
+ * refuses.
1122
+ */
1123
+ function isShadowAddress(address) {
1124
+ return address >= 2147450880n && address < 17594333528064n;
1125
+ }
1115
1126
  const SIGNALS = {
1116
1127
  SIGTRAP: 5,
1117
1128
  SIGXCPU: 24
@@ -1191,6 +1202,8 @@ const SIGNAL_INFO = {
1191
1202
  };
1192
1203
  var BlinkRuntime = class BlinkRuntime {
1193
1204
  module;
1205
+ /** Cached view over the wasm heap, for `spyMemoryBytes`; growth invalidates it. */
1206
+ heapView = null;
1194
1207
  mode;
1195
1208
  state = "NOT_READY";
1196
1209
  stopReason = null;
@@ -1485,6 +1498,47 @@ var BlinkRuntime = class BlinkRuntime {
1485
1498
  if (!result.ok) throw new Error(`${result.error}: 0x${address.toString(16)}`);
1486
1499
  return Uint8Array.from(result.bytes);
1487
1500
  }
1501
+ /**
1502
+ * The bytes at `address` read straight out of the machine's own pages,
1503
+ * or null when the machine cannot answer for the whole range.
1504
+ *
1505
+ * `blinkenlibReadMemoryBytes` looks each byte up and then builds a
1506
+ * JavaScript array inside the bridge, which costs about four times as much
1507
+ * as this does; recording a step reads only addresses the step just wrote,
1508
+ * so it takes this path and falls back to the bridge whenever this one
1509
+ * declines. The two agree byte for byte because they do the same lookup:
1510
+ * `blinkenlib_spy_address` IS the lookup `blinkenlib_read_memory_byte`
1511
+ * makes, and the shadow-memory range the bridge refuses is refused here
1512
+ * too. A page is contiguous in the host heap and the next page need not
1513
+ * be, so the read stops at every page boundary and looks the next one up.
1514
+ */
1515
+ spyMemoryBytes(address, length) {
1516
+ const spy = this.module._blinkenlib_spy_address;
1517
+ if (!spy || !this.module.wasmExports?.memory) return null;
1518
+ if (!Number.isSafeInteger(length) || length <= 0) return null;
1519
+ if (this.state === "NOT_READY" || this.state === "READY") return null;
1520
+ const bytes = new Uint8Array(length);
1521
+ let offset = 0;
1522
+ while (offset < length) {
1523
+ const target = address + BigInt(offset);
1524
+ if (isShadowAddress(target)) return null;
1525
+ const pointer = spy.call(this.module, target) >>> 0;
1526
+ if (pointer === 0) return null;
1527
+ const take = Math.min(BLINK_PAGE_SIZE - Number(target & 4095n), length - offset);
1528
+ bytes.set(this.heapBytes().subarray(pointer, pointer + take), offset);
1529
+ offset += take;
1530
+ }
1531
+ return bytes;
1532
+ }
1533
+ /**
1534
+ * A view over the wasm heap, rebuilt only when growth has replaced the
1535
+ * buffer the last one was taken from.
1536
+ */
1537
+ heapBytes() {
1538
+ const buffer = this.module.wasmExports.memory.buffer;
1539
+ if (!this.heapView || this.heapView.buffer !== buffer) this.heapView = new Uint8Array(buffer);
1540
+ return this.heapView;
1541
+ }
1488
1542
  writeMemoryBytes(address, data) {
1489
1543
  const result = this.module.blinkenlibWriteMemoryBytes(address, data);
1490
1544
  if (!result.ok) throw new Error(`${result.error}: 0x${address.toString(16)}`);
@@ -1874,6 +1928,14 @@ var BaseEmulator = class {
1874
1928
  };
1875
1929
  //#endregion
1876
1930
  //#region src/x86-emulator-utils.ts
1931
+ /**
1932
+ * Whether a native memory write is one a history entry can report in full:
1933
+ * the machine captured every byte it replaced. A write that overflowed the
1934
+ * native journal keeps its `Other` shape instead and stays out of undo.
1935
+ */
1936
+ function isRecordableWrite(write) {
1937
+ return !write.truncated && write.old.length === write.size;
1938
+ }
1877
1939
  var CircularHistory = class {
1878
1940
  capacity;
1879
1941
  entries;
@@ -1965,12 +2027,14 @@ function cloneCallStack(stack) {
1965
2027
  }
1966
2028
  function stripPrivateHistory(entry) {
1967
2029
  return {
2030
+ kind: entry.kind,
1968
2031
  mutations: entry.mutations,
1969
2032
  pc: entry.pc,
1970
2033
  old_ccr: entry.old_ccr,
1971
2034
  new_ccr: entry.new_ccr,
1972
2035
  line: entry.line,
1973
- file: entry.file
2036
+ file: entry.file,
2037
+ ...entry.writes ? { writes: entry.writes } : {}
1974
2038
  };
1975
2039
  }
1976
2040
  function toHistoryPc(address) {
@@ -2003,6 +2067,9 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
2003
2067
  lastSourceCode = "";
2004
2068
  history = new CircularHistory(0);
2005
2069
  callStack = [];
2070
+ openPoke = null;
2071
+ /** True while an instruction is running, so a Poke cannot open on top of one. */
2072
+ executing = false;
2006
2073
  constructor(runtime) {
2007
2074
  super({
2008
2075
  systemSize: 8,
@@ -2086,8 +2153,15 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
2086
2153
  this.runtime.loadElf(data);
2087
2154
  }
2088
2155
  async runUntilBlocked() {
2156
+ this.assertNoOpenPoke("run");
2089
2157
  if (this.isTracingEnabled()) return this.run();
2090
- await this.runtime.runUntilBlocked();
2158
+ const wasExecuting = this.executing;
2159
+ this.executing = true;
2160
+ try {
2161
+ await this.runtime.runUntilBlocked();
2162
+ } finally {
2163
+ this.executing = wasExecuting;
2164
+ }
2091
2165
  return this.getStatus();
2092
2166
  }
2093
2167
  provideInput(line) {
@@ -2172,11 +2246,115 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
2172
2246
  const entry = this.history.peekNewest();
2173
2247
  return Boolean(entry?.reversible);
2174
2248
  }
2249
+ /**
2250
+ * Opens a Poke: a register or memory value the HOST changes between two
2251
+ * instructions, recorded as one step of this history and undone like an
2252
+ * instruction.
2253
+ *
2254
+ * Everything `setRegisterValue`, `setFpuState` and `writeMemoryBytes`
2255
+ * change until `endPoke()` becomes one entry, however many calls it took.
2256
+ * Outside a transaction those setters stay exactly as direct as they were:
2257
+ * a Testcase presetting its starting values records nothing.
2258
+ *
2259
+ * Throws when a poke is already open, and when an instruction is running -
2260
+ * a step inside a transaction would hand the program's own writes to the
2261
+ * poke, and undoing it would then revert the instruction too.
2262
+ */
2263
+ beginPoke() {
2264
+ if (this.openPoke) throw new Error("A poke is already open: end it before beginning another");
2265
+ if (this.executing) throw new Error("Cannot begin a poke while an instruction is executing");
2266
+ const snapshot = this.runtime.getRegisterSnapshot();
2267
+ this.openPoke = {
2268
+ registersBefore: cloneRegisterValues(snapshot),
2269
+ flagsBefore: snapshot.flags,
2270
+ fpuBefore: this.runtime.getFpuStateRaw(),
2271
+ callStackBefore: cloneCallStack(this.callStack),
2272
+ memoryWrites: []
2273
+ };
2274
+ }
2275
+ /**
2276
+ * Closes the open Poke and records it, returning whether anything was
2277
+ * recorded: a transaction that changed no value - because it wrote
2278
+ * nothing, or wrote the value that was already there - records nothing and
2279
+ * answers false, so the caller never shows an Undo step that reverts
2280
+ * nothing.
2281
+ *
2282
+ * Nothing here resumes the machine: the poke changed state, not why the
2283
+ * program stopped, and the next step resumes a paused machine itself.
2284
+ *
2285
+ * A poke takes one slot of the history, exactly as an instruction does, so
2286
+ * with `initialize(0)` it applies and answers true while the zero-capacity
2287
+ * history keeps it no more than it keeps an instruction.
2288
+ *
2289
+ * Throws when no poke is open.
2290
+ */
2291
+ endPoke() {
2292
+ const poke = this.openPoke;
2293
+ if (!poke) throw new Error("No poke is open: begin one before ending it");
2294
+ this.openPoke = null;
2295
+ const after = this.runtime.getRegisterSnapshot();
2296
+ const registersAfter = cloneRegisterValues(after);
2297
+ const mutations = [];
2298
+ const writes = [];
2299
+ for (const register of X86_REGISTER_NAMES) {
2300
+ const old = poke.registersBefore[register];
2301
+ const value = registersAfter[register];
2302
+ if (old === value) continue;
2303
+ mutations.push({
2304
+ type: "WriteRegister",
2305
+ value: {
2306
+ register,
2307
+ old,
2308
+ new: value,
2309
+ size: 8
2310
+ }
2311
+ });
2312
+ writes.push({
2313
+ type: "register",
2314
+ name: register,
2315
+ old,
2316
+ new: value
2317
+ });
2318
+ }
2319
+ this.recordFpuMutations(poke.fpuBefore, this.runtime.getFpuStateRaw(), mutations, writes);
2320
+ const memoryWrites = this.recordPokeMemoryMutations(poke.memoryWrites, mutations, writes);
2321
+ if (writes.length === 0) return false;
2322
+ const location = this.runtime.getSourceLocationForAddress(after.pc);
2323
+ const entry = {
2324
+ kind: "poke",
2325
+ mutations,
2326
+ writes,
2327
+ pc: toHistoryPc(after.pc),
2328
+ old_ccr: { bits: poke.flagsBefore },
2329
+ new_ccr: { bits: after.flags },
2330
+ line: location?.line ?? -1,
2331
+ file: location?.path,
2332
+ registersBefore: poke.registersBefore,
2333
+ flagsBefore: poke.flagsBefore,
2334
+ fpuBefore: poke.fpuBefore,
2335
+ callStackBefore: cloneCallStack(this.callStack),
2336
+ memoryWrites,
2337
+ reversible: true
2338
+ };
2339
+ this.history.push(entry);
2340
+ return true;
2341
+ }
2342
+ /** True between `beginPoke()` and `endPoke()`. */
2343
+ isPokeOpen() {
2344
+ return this.openPoke !== null;
2345
+ }
2175
2346
  async step() {
2176
- if (this.isTracingEnabled()) this.captureStep();
2177
- else {
2178
- this.prepareOneInstructionRun();
2179
- this.runtime.step();
2347
+ this.assertNoOpenPoke("step");
2348
+ const wasExecuting = this.executing;
2349
+ this.executing = true;
2350
+ try {
2351
+ if (this.isTracingEnabled()) this.captureStep();
2352
+ else {
2353
+ this.prepareOneInstructionRun();
2354
+ this.runtime.step();
2355
+ }
2356
+ } finally {
2357
+ this.executing = wasExecuting;
2180
2358
  }
2181
2359
  return { terminated: this.hasTerminated() };
2182
2360
  }
@@ -2186,8 +2364,23 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
2186
2364
  if (this.runtime.state === "PROGRAM_STOPPED") return 0;
2187
2365
  return 1;
2188
2366
  }
2367
+ /**
2368
+ * Inside an open Poke the bytes this overwrites are journaled first, so
2369
+ * the transaction can put them back; outside one the write goes straight
2370
+ * into the machine and records nothing, as it always has.
2371
+ */
2189
2372
  writeMemoryBytes(address, data) {
2373
+ const poke = this.openPoke;
2374
+ if (!poke || data.length === 0) {
2375
+ this.runtime.writeMemoryBytes(address, data);
2376
+ return;
2377
+ }
2378
+ const old = [...this.runtime.readMemoryBytes(address, BigInt(data.length))];
2190
2379
  this.runtime.writeMemoryBytes(address, data);
2380
+ poke.memoryWrites.push({
2381
+ address,
2382
+ old
2383
+ });
2191
2384
  }
2192
2385
  readMemoryBytes(address, length) {
2193
2386
  return this.runtime.readMemoryBytes(address, length);
@@ -2286,15 +2479,25 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
2286
2479
  return this.runtime.state === "PROGRAM_STOPPED";
2287
2480
  }
2288
2481
  async run(limit, breakpoints = []) {
2482
+ this.assertNoOpenPoke("run");
2289
2483
  this.validateRunLimit(limit);
2290
2484
  const breakpointAddresses = this.resolveBreakpointAddresses(breakpoints);
2291
- if (this.isTracingEnabled()) return this.runWithHistory(limit, breakpointAddresses);
2292
- if (breakpointAddresses.length) return this.runWithBreakpoints(limit, breakpointAddresses);
2293
- await this.runtime.runUntilBlocked({
2294
- limit,
2295
- breakpointAddresses
2296
- });
2297
- return this.getStatus();
2485
+ const wasExecuting = this.executing;
2486
+ this.executing = true;
2487
+ try {
2488
+ if (this.isTracingEnabled()) return await this.runWithHistory(limit, breakpointAddresses);
2489
+ if (breakpointAddresses.length) return await this.runWithBreakpoints(limit, breakpointAddresses);
2490
+ await this.runtime.runUntilBlocked({
2491
+ limit,
2492
+ breakpointAddresses
2493
+ });
2494
+ return this.getStatus();
2495
+ } finally {
2496
+ this.executing = wasExecuting;
2497
+ }
2498
+ }
2499
+ assertNoOpenPoke(what) {
2500
+ if (this.openPoke) throw new Error(`Cannot ${what} while a poke is open: end it first`);
2298
2501
  }
2299
2502
  validateRunLimit(limit) {
2300
2503
  if (limit === void 0) return;
@@ -2358,6 +2561,7 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
2358
2561
  clearExecutionTrace() {
2359
2562
  this.history.clear();
2360
2563
  this.callStack = [];
2564
+ this.openPoke = null;
2361
2565
  }
2362
2566
  prepareOneInstructionRun() {
2363
2567
  if (this.runtime.state === "PROGRAM_LOADED" || this.runtime.state === "PROGRAM_STOPPED") {
@@ -2393,6 +2597,7 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
2393
2597
  value: {
2394
2598
  register,
2395
2599
  old: registersBefore[register],
2600
+ new: registersAfter[register],
2396
2601
  size: 8
2397
2602
  }
2398
2603
  });
@@ -2402,6 +2607,7 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
2402
2607
  if (nativeStep.valid) this.recordControlFlowMutation(nativeStep, instruction, mutations);
2403
2608
  const location = this.runtime.getSourceLocationForAddress(pcBefore);
2404
2609
  const entry = {
2610
+ kind: "instruction",
2405
2611
  mutations,
2406
2612
  pc: toHistoryPc(pcBefore),
2407
2613
  old_ccr: { bits: flagsBefore },
@@ -2428,7 +2634,7 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
2428
2634
  * exact either way, because it restores the whole block rather than
2429
2635
  * replaying the mutations.
2430
2636
  */
2431
- recordFpuMutations(fpuBefore, fpuAfter, mutations) {
2637
+ recordFpuMutations(fpuBefore, fpuAfter, mutations, writes) {
2432
2638
  if (fpuStateBlocksEqual(fpuBefore, fpuAfter)) return;
2433
2639
  const stateBefore = decodeFpuState(fpuBefore);
2434
2640
  const stateAfter = decodeFpuState(fpuAfter);
@@ -2439,18 +2645,34 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
2439
2645
  value: {
2440
2646
  register: X86_SSE_REGISTERS[index],
2441
2647
  old: stateBefore.xmm[index],
2648
+ new: stateAfter.xmm[index],
2442
2649
  size: 16
2443
2650
  }
2444
2651
  });
2652
+ writes?.push({
2653
+ type: "register",
2654
+ name: X86_SSE_REGISTERS[index],
2655
+ old: stateBefore.xmm[index],
2656
+ new: stateAfter.xmm[index]
2657
+ });
2445
2658
  }
2446
- if (stateBefore.mxcsr !== stateAfter.mxcsr) mutations.push({
2447
- type: "WriteRegister",
2448
- value: {
2449
- register: "mxcsr",
2659
+ if (stateBefore.mxcsr !== stateAfter.mxcsr) {
2660
+ mutations.push({
2661
+ type: "WriteRegister",
2662
+ value: {
2663
+ register: "mxcsr",
2664
+ old: BigInt(stateBefore.mxcsr),
2665
+ new: BigInt(stateAfter.mxcsr),
2666
+ size: 4
2667
+ }
2668
+ });
2669
+ writes?.push({
2670
+ type: "register",
2671
+ name: "mxcsr",
2450
2672
  old: BigInt(stateBefore.mxcsr),
2451
- size: 4
2452
- }
2453
- });
2673
+ new: BigInt(stateAfter.mxcsr)
2674
+ });
2675
+ }
2454
2676
  const stBitsBefore = readLogicalStBits(fpuBefore);
2455
2677
  const stBitsAfter = readLogicalStBits(fpuAfter);
2456
2678
  for (let index = 0; index < stBitsBefore.length; index += 1) {
@@ -2460,9 +2682,16 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
2460
2682
  value: {
2461
2683
  register: X86_X87_REGISTERS[index],
2462
2684
  old: stBitsBefore[index],
2685
+ new: stBitsAfter[index],
2463
2686
  size: 8
2464
2687
  }
2465
2688
  });
2689
+ writes?.push({
2690
+ type: "register",
2691
+ name: X86_X87_REGISTERS[index],
2692
+ old: stBitsBefore[index],
2693
+ new: stBitsAfter[index]
2694
+ });
2466
2695
  }
2467
2696
  for (const word of [
2468
2697
  "fctrl",
@@ -2475,15 +2704,106 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
2475
2704
  value: {
2476
2705
  register: word,
2477
2706
  old: BigInt(stateBefore[word]),
2707
+ new: BigInt(stateAfter[word]),
2478
2708
  size: 2
2479
2709
  }
2480
2710
  });
2711
+ writes?.push({
2712
+ type: "register",
2713
+ name: word,
2714
+ old: BigInt(stateBefore[word]),
2715
+ new: BigInt(stateAfter[word])
2716
+ });
2481
2717
  }
2482
2718
  }
2719
+ /**
2720
+ * The memory a Poke actually changed. The journal is collapsed per byte
2721
+ * first - the EARLIEST `old` of every address, from the first write that
2722
+ * touched it - and only then compared against what memory holds now, so a
2723
+ * range written twice inside one transaction is diffed against what the
2724
+ * machine held before the poke and not against the intermediate value the
2725
+ * first write left. A range written away and back therefore leaves no
2726
+ * trace at all, and two differing writes to one range leave one entry
2727
+ * carrying the value the machine really had. What survives keeps the old
2728
+ * bytes the way an instruction's writes do, for undo to put back in
2729
+ * reverse order.
2730
+ */
2731
+ recordPokeMemoryMutations(journaled, mutations, writes) {
2732
+ const earliest = /* @__PURE__ */ new Map();
2733
+ for (const write of journaled) write.old.forEach((byte, index) => {
2734
+ const address = write.address + BigInt(index);
2735
+ if (!earliest.has(address)) earliest.set(address, byte);
2736
+ });
2737
+ const addresses = [...earliest.keys()].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
2738
+ const ranges = [];
2739
+ for (const address of addresses) {
2740
+ const open = ranges[ranges.length - 1];
2741
+ const byte = earliest.get(address);
2742
+ if (open && open.address + BigInt(open.old.length) === address) {
2743
+ open.old.push(byte);
2744
+ continue;
2745
+ }
2746
+ ranges.push({
2747
+ address,
2748
+ old: [byte]
2749
+ });
2750
+ }
2751
+ const undoWrites = [];
2752
+ for (const range of ranges) {
2753
+ const current = [...this.runtime.readMemoryBytes(range.address, BigInt(range.old.length))];
2754
+ if (current.every((byte, index) => byte === range.old[index])) continue;
2755
+ undoWrites.push(range);
2756
+ mutations.push({
2757
+ type: "WriteMemoryBytes",
2758
+ value: {
2759
+ address: range.address,
2760
+ old: [...range.old],
2761
+ new: [...current]
2762
+ }
2763
+ });
2764
+ writes.push({
2765
+ type: "memory",
2766
+ address: range.address,
2767
+ old: [...range.old],
2768
+ new: current
2769
+ });
2770
+ }
2771
+ return undoWrites;
2772
+ }
2773
+ /**
2774
+ * The memory a step wrote. The native journal hands over the bytes each
2775
+ * store REPLACED, captured in the machine at that store; it records
2776
+ * nothing about what a store PUT there. So `old` is the machine's own
2777
+ * capture, taken at the write, while `new` is read out of the machine as
2778
+ * the entry is recorded - the step has finished and nothing has run since,
2779
+ * so those addresses still hold what the step left.
2780
+ *
2781
+ * The read costs one page lookup per store: one for the single store
2782
+ * almost every store-bearing instruction journals, and none at all for the
2783
+ * majority of instructions, which write no memory. It goes through the
2784
+ * machine's own page lookup rather than the byte-by-byte bridge (see
2785
+ * `BlinkRuntime.spyMemoryBytes`), which keeps it far below the cost of
2786
+ * tracing the step around it.
2787
+ *
2788
+ * What reading rather than journaling costs: were one step to store twice
2789
+ * over the same address, both entries would report the bytes the step
2790
+ * ENDED with rather than the bytes each store left. No x86 instruction
2791
+ * reachable here does that - blink coalesces a `rep` into one record, and
2792
+ * push, call, ret, enter, leave, `xchg` with memory and read-modify-write
2793
+ * arithmetic each journal a single store - so nothing observable turns on
2794
+ * it today. Only a post-image in the native journal would close it, and
2795
+ * that needs the wasm rebuilt, which this package does not do.
2796
+ *
2797
+ * A range the machine refuses to read back - a step can unmap what it
2798
+ * wrote - leaves that entry with no new bytes rather than a guess, and the
2799
+ * read never throws out of recording. Truncated writes keep their existing
2800
+ * `Other` shape and stay out of the undo journal.
2801
+ */
2483
2802
  recordMemoryMutations(writes, mutations) {
2484
2803
  const undoWrites = [];
2485
- for (const write of writes) {
2486
- if (write.truncated || write.old.length !== write.size) {
2804
+ const written = this.writtenBytes(writes);
2805
+ for (const [index, write] of writes.entries()) {
2806
+ if (!isRecordableWrite(write)) {
2487
2807
  mutations.push({
2488
2808
  type: "Other",
2489
2809
  value: `Wrote ${write.size} bytes to 0x${write.address.toString(16)}`
@@ -2498,12 +2818,37 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
2498
2818
  type: "WriteMemoryBytes",
2499
2819
  value: {
2500
2820
  address: write.address,
2501
- old: [...write.old]
2821
+ old: [...write.old],
2822
+ new: written[index] ?? []
2502
2823
  }
2503
2824
  });
2504
2825
  }
2505
2826
  return undoWrites;
2506
2827
  }
2828
+ /**
2829
+ * The bytes each of a step's stores left, by the index of that store: what
2830
+ * the addresses that store wrote hold now, at the width of the write. One
2831
+ * lookup per store it can account for, and none for a store it cannot.
2832
+ */
2833
+ writtenBytes(writes) {
2834
+ return writes.map((write) => isRecordableWrite(write) ? this.readBytes(write.address, write.size) : []);
2835
+ }
2836
+ /**
2837
+ * One read of the machine that answers with nothing rather than throwing,
2838
+ * so recording a step can never fail after the instruction already ran.
2839
+ * It takes the machine's own page lookup when that can answer for the
2840
+ * whole range and the byte-by-byte bridge otherwise; the two read the same
2841
+ * memory the same way.
2842
+ */
2843
+ readBytes(address, length) {
2844
+ const spied = this.runtime.spyMemoryBytes(address, length);
2845
+ if (spied) return Array.from(spied);
2846
+ try {
2847
+ return [...this.runtime.readMemoryBytes(address, BigInt(length))];
2848
+ } catch {
2849
+ return [];
2850
+ }
2851
+ }
2507
2852
  recordControlFlowMutation(step, instruction, mutations) {
2508
2853
  if (step.controlFlow === "call") {
2509
2854
  const returnAddress = instruction ? instruction.address + BigInt(instruction.size) : step.pcBefore;
@@ -2546,6 +2891,6 @@ async function createX86Emulator(options = {}) {
2546
2891
  return X86Emulator.create(options);
2547
2892
  }
2548
2893
  //#endregion
2549
- export { BaseEmulator, BlinkRuntime, BlinkState, DEFAULT_ASSEMBLER_ID, EmulatorStatus, RegisterSize, X86Emulator, X86_FPU_STATE_CW_OFFSET, X86_FPU_STATE_DP_OFFSET, X86_FPU_STATE_IP_OFFSET, X86_FPU_STATE_MXCSR_OFFSET, X86_FPU_STATE_OP_OFFSET, X86_FPU_STATE_SIZE, X86_FPU_STATE_ST_OFFSET, X86_FPU_STATE_SW_OFFSET, X86_FPU_STATE_TW_OFFSET, X86_FPU_STATE_XMM_OFFSET, X86_PROJECT_ROOT, X86_REGISTER_NAMES, X86_SSE_REGISTERS, X86_X87_REGISTERS, assemblers, createX86Emulator, decodeFpuState, defaultResourceUrl, emptyFpuStateBlock, encodeFpuState, fasmDiagnostics, fpuStateBlocksEqual, gnuDiagnostics, isNodeRuntime, ldDiagnostics, locateDiagnosticColumn, locateDiagnosticSpan, nasmDiagnostics, observeCallbackResult, readLogicalStBits, readLogicalStTags, readResourceBytes, selectX86Source, stageX86Project, toResourceUrl, validateX86Project, x86ProjectSourcePath, x86ProjectText };
2894
+ export { BaseEmulator, BlinkRuntime, BlinkState, DEFAULT_ASSEMBLER_ID, EmulatorStatus, RegisterSize, X86Emulator, X86_FPU_STATE_CW_OFFSET, X86_FPU_STATE_DP_OFFSET, X86_FPU_STATE_IP_OFFSET, X86_FPU_STATE_MXCSR_OFFSET, X86_FPU_STATE_OP_OFFSET, X86_FPU_STATE_SIZE, X86_FPU_STATE_ST_OFFSET, X86_FPU_STATE_SW_OFFSET, X86_FPU_STATE_TW_OFFSET, X86_FPU_STATE_XMM_OFFSET, X86_PROJECT_ROOT, X86_REGISTER_NAMES, X86_SSE_REGISTERS, X86_X87_REGISTERS, assemblers, createX86Emulator, decodeFpuState, defaultResourceUrl, emptyFpuStateBlock, encodeFpuState, fasmDiagnostics, fpuStateBlocksEqual, gnuDiagnostics, isNodeRuntime, isShadowAddress, ldDiagnostics, locateDiagnosticColumn, locateDiagnosticSpan, nasmDiagnostics, observeCallbackResult, readLogicalStBits, readLogicalStTags, readResourceBytes, selectX86Source, stageX86Project, toResourceUrl, validateX86Project, x86ProjectSourcePath, x86ProjectText };
2550
2895
 
2551
2896
  //# sourceMappingURL=index.mjs.map