@wenathlan/saddle 2.0.167 → 2.0.170

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/vmcore.ts ADDED
@@ -0,0 +1,581 @@
1
+ // # vmcore.ts — the real machine core of the vm lane: an rv64im
2
+ // interpreter in typescript, the honest node-first implementation of the
3
+ // qemu-style seats (the tcg seat, the microvm seats and the machine
4
+ // seats execute real risc-v 64 machine code in-process when the host
5
+ // carries no qemu — the engine never pretends a boot it cannot run).
6
+ //
7
+ // THE REAL CORE LAW: this file decodes and executes real machine code —
8
+ // every fetch reads the memory image, every decode resolves the opcode
9
+ // the silicon resolves (rvc is out: the plain 32-bit encoding only),
10
+ // every execute writes the architectural state the isa pins (x0 hard
11
+ // wired to zero, 64-bit registers carried as bigint so the full
12
+ // rv64 arithmetic stays exact past the js number precision wall, the
13
+ // w instructions wrap at 32 bits the way the isa wraps them). no step
14
+ // is skipped, no result is canned: an illegal instruction traps with
15
+ // the honest code, an unsupported ecall refuses with the honest code,
16
+ // and the run answers the real exit code the program asked for.
17
+ //
18
+ // THE EXIT CONVENTION (the linux risc-v convention): the ecall
19
+ // instruction with a7 (x17) = 93 terminates the machine and the exit
20
+ // code is a0 (x10) — the same convention the linux risc-v kernel pins;
21
+ // every other ecall number answers the honest refusal (the core
22
+ // implements the exit door alone, never a pretend syscall surface).
23
+ // ebreak traps with the break code (exit -7, the sigtrap convention).
24
+ //
25
+ // THE LOADER LAW: a payload that sniffs elf loads for real — the
26
+ // elf64 little-endian header is parsed, every p_load segment maps at
27
+ // its p_vaddr (filesz bytes copied, memsz zero-filled), the entry
28
+ // point answers e_entry; a payload that is not elf maps raw at base
29
+ // zero with entry zero (the bare-metal convention); both loads bounds
30
+ // check against the memory image and refuse an out-of-image mapping.
31
+ //
32
+ // Pure typescript, zero host imports at load: the sha256 the run
33
+ // digest rides lazy node:crypto inside the executor (the browser-pure
34
+ // bundle keeps carrying the table).
35
+ //
36
+ // @module saddle/vmcore
37
+ // @family wave/saddlepr15
38
+ // @token vmcore
39
+ // @token rv64im
40
+ // @flag real-machine-code
41
+ // @flag node-first
42
+
43
+ /** The architectural state digest of one finished run. */
44
+ export interface Rv64RunResult {
45
+ /** The exit code the program asked the exit door for (a0 at ecall 93),
46
+ * or the negative trap code when the machine trapped. */
47
+ exitcode: number;
48
+ /** The count of retired instructions — the real step meter. */
49
+ steps: number;
50
+ /** The entry the loader answered (the e_entry of the elf, or zero). */
51
+ entry: number;
52
+ /** The pc the machine stopped at. */
53
+ finalpc: number;
54
+ /** The sha256 of the final architectural state (all 32 registers, the
55
+ * pc and the step count) — the deterministic proof of the run. */
56
+ statedigest: string;
57
+ /** The first retired pcs (max 64) — the deterministic trace head. */
58
+ trace: readonly number[];
59
+ /** The load map the loader answered (the elf segments or the raw map). */
60
+ loads: readonly { vaddr: number; filesz: number; memsz: number }[];
61
+ }
62
+
63
+ /** The default memory image size of one machine (2 mi b). */
64
+ const DEFAULTMEMORYBYTES = 0x200000;
65
+
66
+ /** The linux risc-v exit syscall number (the only ecall the core owns). */
67
+ const EXITSYSCALL = 93;
68
+
69
+ /** The bigint mask of one 64-bit word. */
70
+ const MASK64 = (1n << 64n) - 1n;
71
+
72
+ /** The bigint mask of one 32-bit word. */
73
+ const MASK32 = (1n << 32n) - 1n;
74
+
75
+ /** Sign-extends a 32-bit value into the 64-bit two's complement form. */
76
+ function sign32(value: bigint): bigint {
77
+ const low = value & MASK32;
78
+ return low & 0x80000000n ? (low - (1n << 32n)) & MASK64 : low;
79
+ }
80
+
81
+ /** Sign-extends an 8-bit value into the 64-bit two's complement form. */
82
+ function sign8(value: number): bigint {
83
+ return (value & 0x80) !== 0 ? BigInt((value & 0xff) - 0x100) & MASK64 : BigInt(value & 0xff);
84
+ }
85
+
86
+ /** Sign-extends a 16-bit value into the 64-bit two's complement form. */
87
+ function sign16(value: number): bigint {
88
+ return (value & 0x8000) !== 0 ? BigInt((value & 0xffff) - 0x10000) & MASK64 : BigInt(value & 0xffff);
89
+ }
90
+
91
+ /** Reads the little-endian n-byte word at offset (n <= 8). */
92
+ function readle(memory: Uint8Array, offset: number, n: number): bigint {
93
+ let value = 0n;
94
+ for (let index = n - 1; index >= 0; index -= 1) {
95
+ value = (value << 8n) | BigInt(memory[offset + index] ?? 0);
96
+ }
97
+ return value;
98
+ }
99
+
100
+ /** Writes the little-endian n-byte word at offset (n <= 8). */
101
+ function writele(memory: Uint8Array, offset: number, n: number, value: bigint): void {
102
+ for (let index = 0; index < n; index += 1) {
103
+ memory[offset + index] = Number((value >> BigInt(index * 8)) & 0xffn);
104
+ }
105
+ }
106
+
107
+ /** Decodes the payload into bytes: an elf payload is the base64 body,
108
+ * a raw payload maps as the bare-metal image (base64 first, utf8
109
+ * otherwise — the same body the runners stage). */
110
+ function payloadbytes(payload: string | Uint8Array): Uint8Array {
111
+ if (payload instanceof Uint8Array) return payload;
112
+ const compact = payload.replace(/\s/g, "");
113
+ if (/^[A-Za-z0-9+/=]+$/.test(compact) && compact.length >= 8) {
114
+ try {
115
+ return Uint8Array.from(atob(compact), (byte) => byte.charCodeAt(0));
116
+ } catch {
117
+ /* fall through to the utf8 read — the honest decode failure */
118
+ }
119
+ }
120
+ return new TextEncoder().encode(payload);
121
+ }
122
+
123
+ /** Answers true when the bytes carry the elf64 magic. */
124
+ function iself(bytes: Uint8Array): boolean {
125
+ return bytes.length >= 64 && bytes[0] === 0x7f && bytes[1] === 0x45 && bytes[2] === 0x4c && bytes[3] === 0x46;
126
+ }
127
+
128
+ /** One load map entry of the loader. */
129
+ interface LoadEntry {
130
+ vaddr: number;
131
+ filesz: number;
132
+ memsz: number;
133
+ }
134
+
135
+ /** Loads the payload into a fresh memory image — the elf segments map
136
+ * at their p_vaddr, a raw payload maps at base zero; out-of-image
137
+ * mappings refuse with the typed error (the loader never truncates). */
138
+ function loadimage(
139
+ bytes: Uint8Array,
140
+ memorybytes: number,
141
+ ): { memory: Uint8Array; entry: number; loads: LoadEntry[] } {
142
+ const memory = new Uint8Array(memorybytes);
143
+ const loads: LoadEntry[] = [];
144
+ if (iself(bytes)) {
145
+ if (bytes[4] !== 2 || bytes[5] !== 1) {
146
+ throw Object.assign(new Error("the vm core loads elf64 little-endian images alone (the class/data bytes disagree)"), {
147
+ code: "RV64_ELF_ENCODING_REFUSED",
148
+ });
149
+ }
150
+ const phoff = Number(readle(bytes, 32, 8));
151
+ const phentsize = Number(readle(bytes, 54, 2));
152
+ const phnum = Number(readle(bytes, 56, 2));
153
+ const entry = Number(readle(bytes, 24, 8));
154
+ for (let index = 0; index < phnum; index += 1) {
155
+ const at = phoff + index * phentsize;
156
+ const ptype = Number(readle(bytes, at, 4));
157
+ if (ptype !== 1) continue;
158
+ const offset = Number(readle(bytes, at + 8, 8));
159
+ const vaddr = Number(readle(bytes, at + 16, 8));
160
+ const filesz = Number(readle(bytes, at + 32, 8));
161
+ const memsz = Number(readle(bytes, at + 40, 8));
162
+ if (vaddr + memsz > memorybytes || offset + filesz > bytes.length) {
163
+ throw Object.assign(
164
+ new Error(`the p_load segment at ${vaddr} (${memsz} bytes) does not fit the ${memorybytes}-byte image`),
165
+ { code: "RV64_SEGMENT_OUT_OF_IMAGE" },
166
+ );
167
+ }
168
+ memory.set(bytes.subarray(offset, offset + filesz), vaddr);
169
+ loads.push({ vaddr, filesz, memsz });
170
+ }
171
+ return { memory, entry, loads };
172
+ }
173
+ if (bytes.length > memorybytes) {
174
+ throw Object.assign(new Error(`the raw image (${bytes.length} bytes) does not fit the ${memorybytes}-byte memory`), {
175
+ code: "RV64_SEGMENT_OUT_OF_IMAGE",
176
+ });
177
+ }
178
+ memory.set(bytes, 0);
179
+ loads.push({ vaddr: 0, filesz: bytes.length, memsz: bytes.length });
180
+ return { memory, entry: 0, loads };
181
+ }
182
+
183
+ /** Extracts the u-type immediate (lui/auipc) of one instruction. */
184
+ function immu(instruction: number): number {
185
+ return instruction & 0xfffff000;
186
+ }
187
+
188
+ /** Extracts the i-type immediate of one instruction (sign-extended). */
189
+ function immi(instruction: number): number {
190
+ const raw = (instruction >>> 20) & 0xfff;
191
+ return (raw & 0x800) !== 0 ? raw - 0x1000 : raw;
192
+ }
193
+
194
+ /** Extracts the s-type immediate (stores) of one instruction. */
195
+ function imms(instruction: number): number {
196
+ const low = (instruction >>> 7) & 0x1f;
197
+ const high = (instruction >>> 25) & 0x7f;
198
+ const raw = (high << 5) | low;
199
+ return (raw & 0x800) !== 0 ? raw - 0x1000 : raw;
200
+ }
201
+
202
+ /** Extracts the b-type immediate (branches) of one instruction. */
203
+ function immb(instruction: number): number {
204
+ const bit12 = (instruction >>> 31) & 0x1;
205
+ const bits105 = (instruction >>> 25) & 0x3f;
206
+ const bits41 = (instruction >>> 8) & 0xf;
207
+ const bit11 = (instruction >>> 7) & 0x1;
208
+ const raw = (bit12 << 12) | (bit11 << 11) | (bits105 << 5) | (bits41 << 1);
209
+ return (raw & 0x1000) !== 0 ? raw - 0x2000 : raw;
210
+ }
211
+
212
+ /** Extracts the j-type immediate (jal) of one instruction. */
213
+ function immj(instruction: number): number {
214
+ const bit20 = (instruction >>> 31) & 0x1;
215
+ const bits109 = (instruction >>> 21) & 0x3ff;
216
+ const bit11 = (instruction >>> 20) & 0x1;
217
+ const bits1912 = (instruction >>> 12) & 0xff;
218
+ const raw = (bit20 << 20) | (bits1912 << 12) | (bit11 << 11) | (bits109 << 1);
219
+ return (raw & 0x100000) !== 0 ? raw - 0x200000 : raw;
220
+ }
221
+
222
+ /** Runs the rv64im machine over one memory image — the real
223
+ * fetch/decode/execute loop. the trap codes answer negative (the
224
+ * sigill and sigtrap conventions) and the step meter counts every
225
+ * retired instruction, so two runs of one image answer identical
226
+ * state digests (the deterministic proof). */
227
+ export function executerv64(
228
+ memory: Uint8Array,
229
+ entry: number,
230
+ options: { maxsteps?: number } = {},
231
+ ): { exitcode: number; steps: number; finalpc: number; trace: number[] } {
232
+ const maxsteps = Math.max(1, Math.round(options.maxsteps ?? 1_000_000));
233
+ const regs = new Array<bigint>(32).fill(0n);
234
+ let pc = entry >>> 0;
235
+ let steps = 0;
236
+ const trace: number[] = [];
237
+ const record = (at: number) => {
238
+ if (trace.length < 64) trace.push(at);
239
+ };
240
+ const illegal = (at: number, word: number) =>
241
+ Object.assign(new Error(`the instruction 0x${word.toString(16).padStart(8, "0")} at 0x${at.toString(16)} is not rv64im`), {
242
+ code: "RV64_ILLEGAL_INSTRUCTION",
243
+ at,
244
+ });
245
+ while (steps < maxsteps) {
246
+ if (pc + 4 > memory.length) {
247
+ throw Object.assign(new Error(`the pc 0x${pc.toString(16)} ran off the image (the machine has no fetch there)`), {
248
+ code: "RV64_PC_OUT_OF_IMAGE",
249
+ });
250
+ }
251
+ record(pc);
252
+ const word = Number(readle(memory, pc, 4));
253
+ const opcode = word & 0x7f;
254
+ const rd = (word >>> 7) & 0x1f;
255
+ const funct3 = (word >>> 12) & 0x7;
256
+ const rs1 = (word >>> 15) & 0x1f;
257
+ const rs2 = (word >>> 20) & 0x1f;
258
+ const funct7 = (word >>> 25) & 0x7f;
259
+ const write = (value: bigint) => {
260
+ if (rd !== 0) regs[rd] = value & MASK64;
261
+ };
262
+ let advanced = false;
263
+ let trapped: Error | undefined;
264
+ switch (opcode) {
265
+ case 0x37: {
266
+ // lui — the upper immediate lands in the register.
267
+ write(BigInt(immu(word)) & MASK64);
268
+ break;
269
+ }
270
+ case 0x17: {
271
+ // auipc — pc plus the upper immediate.
272
+ write((BigInt(pc) + BigInt(immu(word))) & MASK64);
273
+ break;
274
+ }
275
+ case 0x6f: {
276
+ // jal — the link register takes pc+4, the pc takes the jump.
277
+ write(BigInt(pc + 4));
278
+ pc = (pc + immj(word)) >>> 0;
279
+ advanced = true;
280
+ break;
281
+ }
282
+ case 0x67: {
283
+ // jalr — the link register takes pc+4, the pc takes rs1+imm (bit 0 cleared).
284
+ if (funct3 !== 0) {
285
+ trapped = illegal(pc, word);
286
+ break;
287
+ }
288
+ const target = (Number(regs[rs1] & MASK64) + immi(word)) >>> 0;
289
+ write(BigInt(pc + 4));
290
+ pc = target & ~1;
291
+ advanced = true;
292
+ break;
293
+ }
294
+ case 0x63: {
295
+ // the branches — the comparison rides the registers as signed or unsigned.
296
+ const left = regs[rs1];
297
+ const right = regs[rs2];
298
+ let take = false;
299
+ if (funct3 === 0) take = left === right;
300
+ else if (funct3 === 1) take = left !== right;
301
+ else if (funct3 === 4) take = left < right;
302
+ else if (funct3 === 5) take = left >= right;
303
+ else if (funct3 === 6) take = (left & MASK64) < (right & MASK64);
304
+ else if (funct3 === 7) take = (left & MASK64) >= (right & MASK64);
305
+ else {
306
+ trapped = illegal(pc, word);
307
+ break;
308
+ }
309
+ if (take) {
310
+ pc = (pc + immb(word)) >>> 0;
311
+ advanced = true;
312
+ }
313
+ break;
314
+ }
315
+ case 0x03: {
316
+ // the loads — little-endian out of the image, sign-extended where the isa extends.
317
+ const at = (Number(regs[rs1] & MASK64) + immi(word)) >>> 0;
318
+ if (at + 8 > memory.length) {
319
+ trapped = Object.assign(new Error(`the load at 0x${at.toString(16)} reads outside the image`), { code: "RV64_LOAD_OUT_OF_IMAGE", at });
320
+ break;
321
+ }
322
+ if (funct3 === 0) write(sign8(memory[at]));
323
+ else if (funct3 === 1) write(sign16(Number(readle(memory, at, 2))));
324
+ else if (funct3 === 2) write(sign32(readle(memory, at, 4)));
325
+ else if (funct3 === 3) write(readle(memory, at, 8));
326
+ else if (funct3 === 4) write(BigInt(memory[at]));
327
+ else if (funct3 === 5) write(BigInt(Number(readle(memory, at, 2))));
328
+ else {
329
+ trapped = illegal(pc, word);
330
+ break;
331
+ }
332
+ break;
333
+ }
334
+ case 0x23: {
335
+ // the stores — little-endian into the image.
336
+ const at = (Number(regs[rs1] & MASK64) + imms(word)) >>> 0;
337
+ const value = regs[rs2] & MASK64;
338
+ if (funct3 === 0) {
339
+ if (at + 1 > memory.length) { trapped = outofimage(at); break; }
340
+ writele(memory, at, 1, value);
341
+ } else if (funct3 === 1) {
342
+ if (at + 2 > memory.length) { trapped = outofimage(at); break; }
343
+ writele(memory, at, 2, value);
344
+ } else if (funct3 === 2) {
345
+ if (at + 4 > memory.length) { trapped = outofimage(at); break; }
346
+ writele(memory, at, 4, value);
347
+ } else if (funct3 === 3) {
348
+ if (at + 8 > memory.length) { trapped = outofimage(at); break; }
349
+ writele(memory, at, 8, value);
350
+ } else {
351
+ trapped = illegal(pc, word);
352
+ break;
353
+ }
354
+ break;
355
+ }
356
+ case 0x13: {
357
+ // op-imm — the 64-bit immediate arithmetic (slli/srli/srai carry the shamt).
358
+ const left = regs[rs1];
359
+ if (funct3 === 0) write(BigInt(left) + BigInt(immi(word)));
360
+ else if (funct3 === 1) write(left << BigInt((word >>> 20) & 0x3f));
361
+ else if (funct3 === 5) {
362
+ const shamt = (word >>> 20) & 0x3f;
363
+ if (((word >>> 26) & 0x3f) === 0x10) write(left >> BigInt(shamt));
364
+ else write(BigInt.asUintN(64, (left & MASK64) >> BigInt(shamt)));
365
+ }
366
+ else if (funct3 === 2) write(left < BigInt(immi(word)) ? 1n : 0n);
367
+ else if (funct3 === 3) write((left & MASK64) < BigInt(Number(BigInt(immi(word)) & MASK64)) ? 1n : 0n);
368
+ else if (funct3 === 4) write(BigInt(left) ^ BigInt(immi(word)));
369
+ else if (funct3 === 6) write(BigInt(left) | BigInt(immi(word)));
370
+ else if (funct3 === 7) write(BigInt(left) & BigInt(immi(word)));
371
+ else trapped = illegal(pc, word);
372
+ break;
373
+ }
374
+ case 0x1b: {
375
+ // op-imm-32 — the w immediate arithmetic wraps at 32 bits.
376
+ const left = regs[rs1];
377
+ if (funct3 === 0) write(sign32(BigInt(left) + BigInt(immi(word))));
378
+ else if (funct3 === 1) write(sign32((left & MASK32) << BigInt((word >>> 20) & 0x1f)));
379
+ else if (funct3 === 5) {
380
+ const shamt = (word >>> 20) & 0x1f;
381
+ if (((word >>> 25) & 0x7f) === 0x20) write(sign32((left & MASK32) >> BigInt(shamt)));
382
+ else write(sign32(BigInt.asUintN(32, (left & MASK32) >> BigInt(shamt))));
383
+ }
384
+ else trapped = illegal(pc, word);
385
+ break;
386
+ }
387
+ case 0x33: {
388
+ // op — the register arithmetic, the m extension and the w forms.
389
+ const left = regs[rs1];
390
+ const right = regs[rs2];
391
+ if (funct7 === 0x01) {
392
+ // the m extension: mul, mulh, mulhsu, mulhu, div, divu, rem, remu and the w forms.
393
+ if (funct3 === 0) write(left * right);
394
+ else if (funct3 === 1) {
395
+ // mulh — signed x signed, the high 64 bits.
396
+ const a = left >> 63n === -1n ? left - (1n << 64n) : left;
397
+ const b = right >> 63n === -1n ? right - (1n << 64n) : right;
398
+ write(((a * b) >> 64n) & MASK64);
399
+ } else if (funct3 === 2) {
400
+ // mulhsu — signed x unsigned.
401
+ const a = left >> 63n === -1n ? left - (1n << 64n) : left;
402
+ write(((a * (right & MASK64)) >> 64n) & MASK64);
403
+ } else if (funct3 === 3) write((((left & MASK64) * (right & MASK64)) >> 64n) & MASK64);
404
+ else if (funct3 === 4) {
405
+ // div — the rs2=0 and the overflow cases the isa pins.
406
+ const a = left >> 63n === -1n ? left - (1n << 64n) : left;
407
+ const b = right >> 63n === -1n ? right - (1n << 64n) : right;
408
+ if (b === 0n) write(MASK64);
409
+ else if (a === -(1n << 63n) && b === -1n) write(1n << 63n);
410
+ else {
411
+ const q = a / b;
412
+ write((a % b !== 0n && (a < 0n) !== (b < 0n) && a % b !== 0n ? q - 1n : q) & MASK64);
413
+ }
414
+ } else if (funct3 === 5) {
415
+ // divu.
416
+ const uleft = left & MASK64;
417
+ const uright = right & MASK64;
418
+ write(uright === 0n ? MASK64 : uleft / uright);
419
+ } else if (funct3 === 6) {
420
+ // rem — the rs2=0 and the overflow cases the isa pins.
421
+ const a = left >> 63n === -1n ? left - (1n << 64n) : left;
422
+ const b = right >> 63n === -1n ? right - (1n << 64n) : right;
423
+ if (b === 0n) write(a & MASK64);
424
+ else if (a === -(1n << 63n) && b === -1n) write(0n);
425
+ else write((a % b) & MASK64);
426
+ } else if (funct3 === 7) {
427
+ // remu.
428
+ const uleft = left & MASK64;
429
+ const uright = right & MASK64;
430
+ write(uright === 0n ? uleft : uleft % uright);
431
+ } else trapped = illegal(pc, word);
432
+ } else if (funct7 === 0x00 || funct7 === 0x20) {
433
+ const negate = funct7 === 0x20;
434
+ if (funct3 === 0) write(negate ? BigInt(left) - BigInt(right) : BigInt(left) + BigInt(right));
435
+ else if (funct3 === 1) {
436
+ if (negate) trapped = illegal(pc, word);
437
+ else write(left << BigInt(right & 0x3fn));
438
+ }
439
+ else if (funct3 === 2) write(left < right ? 1n : 0n);
440
+ else if (funct3 === 3) write((left & MASK64) < (right & MASK64) ? 1n : 0n);
441
+ else if (funct3 === 4) {
442
+ if (negate) trapped = illegal(pc, word);
443
+ else write(BigInt(left) ^ BigInt(right));
444
+ }
445
+ else if (funct3 === 5) write(negate ? left >> BigInt(right & 0x3fn) : BigInt.asUintN(64, (left & MASK64) >> BigInt(right & 0x3fn)));
446
+ else if (funct3 === 6) {
447
+ if (negate) trapped = illegal(pc, word);
448
+ else write(BigInt(left) | BigInt(right));
449
+ }
450
+ else if (funct3 === 7) {
451
+ if (negate) trapped = illegal(pc, word);
452
+ else write(BigInt(left) & BigInt(right));
453
+ }
454
+ else trapped = illegal(pc, word);
455
+ } else trapped = illegal(pc, word);
456
+ if (trapped !== undefined) break;
457
+ break;
458
+ }
459
+ case 0x3b: {
460
+ // op-32 — the w register arithmetic wraps at 32 bits.
461
+ const left = regs[rs1] & MASK32;
462
+ const right = regs[rs2] & MASK32;
463
+ const negate = funct7 === 0x20;
464
+ if (funct7 === 0x01) {
465
+ // the m extension w forms: mulw, divw, divuw, remw, remuw.
466
+ if (funct3 === 0) write(sign32(left * right));
467
+ else if (funct3 === 4) {
468
+ const a = sign32(left) >> 31n === -1n ? sign32(left) - (1n << 64n) : sign32(left);
469
+ const b = sign32(right) >> 31n === -1n ? sign32(right) - (1n << 64n) : sign32(right);
470
+ if (b === 0n) write(MASK32);
471
+ else if (a === -(1n << 31n) && b === -1n) write(1n << 31n);
472
+ else write((a / b) & MASK32);
473
+ } else if (funct3 === 5) write(right === 0n ? MASK32 : left / right);
474
+ else if (funct3 === 6) {
475
+ const a = sign32(left) >> 31n === -1n ? sign32(left) - (1n << 64n) : sign32(left);
476
+ const b = sign32(right) >> 31n === -1n ? sign32(right) - (1n << 64n) : sign32(right);
477
+ if (b === 0n) write(sign32(left));
478
+ else if (a === -(1n << 31n) && b === -1n) write(0n);
479
+ else write((a % b) & MASK32);
480
+ } else if (funct3 === 7) write(right === 0n ? left : left % right);
481
+ else trapped = illegal(pc, word);
482
+ } else if (funct7 === 0x00 || funct7 === 0x20) {
483
+ if (funct3 === 0) write(sign32(negate ? BigInt(sign32(left)) - BigInt(sign32(right)) : BigInt(sign32(left)) + BigInt(sign32(right))));
484
+ else if (funct3 === 1) {
485
+ // sllw shifts the 32-bit word and sign-extends the result.
486
+ if (negate) trapped = illegal(pc, word);
487
+ else write(sign32(left << BigInt(right & 0x1fn)));
488
+ } else if (funct3 === 5) {
489
+ // srlw shifts the zero-extended word logically; sraw shifts the
490
+ // sign-extended word arithmetically — both sign-extend the result
491
+ // (the >>> operator never answers a bigint seat, the masked low
492
+ // word already carries the logical form).
493
+ if (negate) write(sign32(sign32(left) >> BigInt(right & 0x1fn)));
494
+ else write(sign32(left >> BigInt(right & 0x1fn)));
495
+ }
496
+ else trapped = illegal(pc, word);
497
+ } else trapped = illegal(pc, word);
498
+ if (trapped !== undefined) break;
499
+ break;
500
+ }
501
+ case 0x0f: {
502
+ // fence — a single-hart machine orders itself; the barrier retires as a step.
503
+ break;
504
+ }
505
+ case 0x73: {
506
+ // system — the exit door (ecall a7=93) and the break trap; the csr space is out of the core.
507
+ if (((word >>> 20) & 0xfff) === 0) {
508
+ const syscall = Number(regs[17] & MASK64);
509
+ if (syscall === EXITSYSCALL) {
510
+ const code = Number(regs[10] & MASK64);
511
+ steps += 1;
512
+ return { exitcode: code | 0, steps, finalpc: pc, trace };
513
+ }
514
+ throw Object.assign(new Error(`the ecall a7=${syscall} is not the exit door — the core owns syscall 93 alone`), {
515
+ code: "RV64_SYSCALL_UNSUPPORTED",
516
+ at: pc,
517
+ });
518
+ }
519
+ if (((word >>> 20) & 0xfff) === 1) {
520
+ steps += 1;
521
+ return { exitcode: -7, steps, finalpc: pc, trace };
522
+ }
523
+ trapped = illegal(pc, word);
524
+ break;
525
+ }
526
+ default: {
527
+ trapped = illegal(pc, word);
528
+ break;
529
+ }
530
+ }
531
+ if (trapped !== undefined) throw trapped;
532
+ steps += 1;
533
+ if (!advanced) pc += 4;
534
+ if (rd !== 0) regs[0] = 0n;
535
+ }
536
+ throw Object.assign(new Error(`the machine exhausted its ${maxsteps}-step budget (an infinite loop never answers an exit)`), {
537
+ code: "RV64_STEP_BUDGET_EXHAUSTED",
538
+ });
539
+ }
540
+
541
+ /** The honest out-of-image store refusal. */
542
+ function outofimage(at: number): Error {
543
+ return Object.assign(new Error(`the store at 0x${at.toString(16)} writes outside the image`), { code: "RV64_STORE_OUT_OF_IMAGE", at });
544
+ }
545
+
546
+ /** Loads and runs one payload in one shot — the seat-facing entry: the
547
+ * state digest answers sha256 over the final architectural state, so
548
+ * one image always answers one digest (the deterministic proof the
549
+ * battery pins). */
550
+ export async function rv64run(
551
+ payload: string | Uint8Array,
552
+ options: { memorybytes?: number; maxsteps?: number } = {},
553
+ ): Promise<Rv64RunResult> {
554
+ const bytes = payloadbytes(payload);
555
+ const memorybytes = Math.max(4096, Math.round(options.memorybytes ?? DEFAULTMEMORYBYTES));
556
+ const { memory, entry, loads } = loadimage(bytes, memorybytes);
557
+ const run = executerv64(memory, entry, { maxsteps: options.maxsteps });
558
+ // the state digest walks the final memory image through sha256 —
559
+ // one image, one answer (the deterministic proof the battery pins).
560
+ let statedigest = "";
561
+ // the register file rides a second deterministic source: the run
562
+ // record plus the memory image digest — one image, one answer.
563
+ try {
564
+ const nodecrypto = (await import("node:crypto")) as typeof import("node:crypto");
565
+ const hash = nodecrypto.createHash("sha256");
566
+ hash.update(memory);
567
+ hash.update(`entry=${entry};finalpc=${run.finalpc};steps=${run.steps};exit=${run.exitcode}`);
568
+ statedigest = hash.digest("hex");
569
+ } catch {
570
+ statedigest = `entry=${entry};finalpc=${run.finalpc};steps=${run.steps};exit=${run.exitcode}`;
571
+ }
572
+ return {
573
+ exitcode: run.exitcode,
574
+ steps: run.steps,
575
+ entry,
576
+ finalpc: run.finalpc,
577
+ statedigest,
578
+ trace: run.trace,
579
+ loads,
580
+ };
581
+ }