@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/Dockerfile +36 -16
- package/README.md +1 -1
- package/docker.config +1 -1
- package/docs/architecture.md +4 -4
- package/docs/e2ughengine.md +4 -4
- package/modalities.ts +99 -22
- package/package.json +55 -56
- package/vmcore.ts +581 -0
- package/workloadlaw.ts +337 -38
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
|
+
}
|