nx_rv64_sim.nx source
↩ module page · 832 lines · 28883 B
1// nx_rv64_sim.nx -- minimal RV64I interpreter (sovereign qemu).
2//
3// MAJOR off-C deliverable: removes qemu-riscv64-static dependency
4// from bootstrap_proof.sh stage 2. An RV64I program loaded into
5// memory can be executed by THIS module's instruction loop.
6//
7// v0.0.1 scope (RV64I + RV64M):
8// * 31 general registers (x0 = always zero, x1..x31 = mutable)
9// * pc register
10// * Linear flat memory (caller-supplied buffer)
11// * Decode + execute every RV64I op the codegen emits:
12// - R-type: add/sub/mul/div/rem/and/or/xor/sll/srl/sra/slt/sltu
13// - I-type: addi/andi/ori/xori/slli/srli/srai/slti/sltiu, jalr,
14// ld/lw/lh/lb/lwu/lhu/lbu
15// - S-type: sd/sw/sh/sb
16// - B-type: beq/bne/blt/bge/bltu/bgeu
17// - U-type: lui/auipc
18// - J-type: jal
19// - System: ecall (-> caller-supplied syscall handler)
20//
21// What we DEFER to follow-up commits:
22// * F/D/V extensions (A is supported as nop-AMO since SMP off)
23// * privileged mode (M-mode, S-mode)
24// * memory management / page tables
25// * proper trap delivery
26//
27// COMPRESSED (C) extension is now wired:
28// * nx_rv64_step inspects low 2 bits of the instruction halfword.
29// * If they're not 11, the instruction is 16-bit compressed; we
30// expand it to a 32-bit RV64I/M equivalent via nx_rv64c_decode
31// and dispatch on that, advancing pc by 2.
32// * Otherwise pc advances by 4 as before.
33// * This is enough to run binaries from stock gcc / clang +c.
34//
35// Why now:
36// * Stage 2 of bootstrap_proof currently runs through Docker +
37// qemu-riscv64-static. When Docker is unavailable, we cannot
38// verify the off-C path. Native execution closes that gap.
39// * Pairs with nx_dis.nx (decode) and nx_elf_read.nx (load) to
40// produce a complete sovereign run-loop: load ELF, decode each
41// instruction, simulate, repeat until exit syscall.
42//
43// Note: this is NOT meant to be FAST. qemu-riscv64-static does
44// JIT-compilation; we walk one instruction at a time interpretively.
45// For CORRECTNESS verification + sovereignty, that's the right
46// trade-off.
47
48// nx_safety_envelope:
49// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
50// sil_target: SIL1
51// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
52// verdict: NOT_YET_EVALUATED
53
54import "syscalls.nx"
55import "nx_dis.nx"
56import "nx_rv64c.nx"
57const NX_MAGIC_4096: i64 = 4096
58const NX_MAGIC_4095: i64 = 4095
59
60// ---- machine state ------------------------------------------------
61
62struct NxRv64Cpu {
63 regs: *i64, // x0..x31 (32 slots, x0 always reads 0)
64 pc: i64,
65 mem_base: *u8,
66 mem_size: i64,
67 halted: i64, // 1 = ecall delivered + handled, stop
68 exit_code: i64, // last syscall arg passed to exit()
69 insn_count: i64, // instructions executed (observability)
70}
71
72const NX_RV64_CPU_BYTES: i64 = 56
73
74// ---- construction -------------------------------------------------
75
76func nx_rv64_cpu_new(mem_base: *u8, mem_size: i64, entry_pc: i64) -> *NxRv64Cpu {
77 let raw: *u8 = sys_mmap(NX_RV64_CPU_BYTES)
78 let c: *NxRv64Cpu = raw as *NxRv64Cpu
79 c.regs = sys_mmap(32 * 8) as *i64
80 var i: i64 = 0
81 while i < 32 { c.regs[i] = 0; i = i + 1 }
82 c.pc = entry_pc
83 c.mem_base = mem_base
84 c.mem_size = mem_size
85 c.halted = 0
86 c.exit_code = 0
87 c.insn_count = 0
88 return c
89}
90
91// Set sp to a sensible default (top of memory minus a frame).
92func nx_rv64_cpu_set_sp(c: *NxRv64Cpu, sp: i64) -> i64 {
93 c.regs[2] = sp
94 return 0
95}
96
97// ---- register helpers --------------------------------------------
98
99// Read register; x0 always returns 0.
100func nx_rv64_rd(c: *NxRv64Cpu, idx: i64) -> i64 {
101 if idx == 0 { return 0 }
102 return c.regs[idx]
103}
104
105// Write register; x0 silently ignored.
106func nx_rv64_wr(c: *NxRv64Cpu, idx: i64, v: i64) -> i64 {
107 if idx == 0 { return 0 }
108 c.regs[idx] = v
109 return 0
110}
111
112// ---- memory access (relative to mem_base; bounds-checked) --------
113
114func nx_rv64_load_u8(c: *NxRv64Cpu, addr: i64) -> i64 {
115 if addr < 0 { return 0 }
116 if addr >= c.mem_size { return 0 }
117 return c.mem_base[addr]
118}
119
120func nx_rv64_store_u8(c: *NxRv64Cpu, addr: i64, v: i64) -> i64 {
121 if addr < 0 { return -1 }
122 if addr >= c.mem_size { return -1 }
123 c.mem_base[addr] = v & 0xFF
124 return 0
125}
126
127func nx_rv64_load_u64(c: *NxRv64Cpu, addr: i64) -> i64 {
128 var v: i64 = 0
129 var i: i64 = 0
130 while i < 8 {
131 v = v | (nx_rv64_load_u8(c, addr + i) << (i * 8))
132 i = i + 1
133 }
134 return v
135}
136
137func nx_rv64_store_u64(c: *NxRv64Cpu, addr: i64, v: i64) -> i64 {
138 var i: i64 = 0
139 while i < 8 {
140 nx_rv64_store_u8(c, addr + i, (v >> (i * 8)) & 0xFF)
141 i = i + 1
142 }
143 return 0
144}
145
146func nx_rv64_load_u32(c: *NxRv64Cpu, addr: i64) -> i64 {
147 let b0: i64 = nx_rv64_load_u8(c, addr + 0)
148 let b1: i64 = nx_rv64_load_u8(c, addr + 1)
149 let b2: i64 = nx_rv64_load_u8(c, addr + 2)
150 let b3: i64 = nx_rv64_load_u8(c, addr + 3)
151 return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)
152}
153
154func nx_rv64_store_u32(c: *NxRv64Cpu, addr: i64, v: i64) -> i64 {
155 nx_rv64_store_u8(c, addr + 0, (v ) & 0xFF)
156 nx_rv64_store_u8(c, addr + 1, (v >> 8) & 0xFF)
157 nx_rv64_store_u8(c, addr + 2, (v >> 16) & 0xFF)
158 nx_rv64_store_u8(c, addr + 3, (v >> 24) & 0xFF)
159 return 0
160}
161
162// ---- syscall handler hook ----------------------------------------
163//
164// The simulator delivers ecalls to a caller-supplied handler. This
165// table covers the syscalls nxc.elf actually uses (per syscalls.nx):
166//
167// sys_write (#64) write(fd, buf, len)
168// sys_read (#63) read(fd, buf, len)
169// sys_close (#57) close(fd)
170// sys_exit (#93) exit(code)
171// sys_mmap (#222) mmap(addr, size, prot, flags, fd, off)
172// sys_openat (#56) openat(dirfd, path, flags, mode)
173// sys_getpid (#172) getpid()
174// sys_pipe2 (#59) pipe2(fds, flags)
175// sys_dup3 (#24) dup3(old, new, flags)
176// sys_fork (#220) fork()
177// sys_execve (#221) execve(path, argv, envp)
178// sys_wait4 (#260) wait4(pid, status, options, rusage)
179// sys_getdents (#61) getdents64(fd, buf, len)
180//
181// For pass-through syscalls (write/read/close/openat etc.) we
182// translate sim memory addresses to host pointers + invoke the
183// host syscall, then write the result back to the sim's a0.
184//
185// sys_mmap is special: the sim has its own flat memory; we
186// implement mmap as a bump allocator inside the sim's address
187// space (above a high-water mark). Returned vaddr is the next
188// aligned slot.
189
190// Per-CPU bump-allocator state for sys_mmap inside the sim.
191// We carve mmap regions from the high end of the sim's mem
192// region downward, leaving low addresses for the program's
193// loaded segments + stack.
194const NX_RV64_MMAP_BASE: i64 = 0x800000 // start at 8 MB into sim
195struct NxRv64Mmap {
196 cur_off: i64,
197}
198
199// Initialise the mmap bump pointer in the cpu (idempotent).
200func nx_rv64_mmap_init(c: *NxRv64Cpu) -> i64 {
201 // Stash the high-water mark in a hidden cpu field via a side
202 // mmap (the cpu struct doesn't have a slot for it; tag onto
203 // mem_size for now -- a follow-up should add a proper field).
204 return 0
205}
206
207// Helper: copy `n` bytes from sim_addr to host buffer.
208func nx_rv64_sim_to_host(c: *NxRv64Cpu, sim_addr: i64, host: *u8, n: i64) -> i64 {
209 var i: i64 = 0
210 while i < n {
211 host[i] = nx_rv64_load_u8(c, sim_addr + i) & 0xFF
212 i = i + 1
213 }
214 return 0
215}
216
217// Helper: copy `n` bytes from host buffer to sim_addr.
218func nx_rv64_host_to_sim(c: *NxRv64Cpu, sim_addr: i64, host: *u8, n: i64) -> i64 {
219 var i: i64 = 0
220 while i < n {
221 nx_rv64_store_u8(c, sim_addr + i, host[i])
222 i = i + 1
223 }
224 return 0
225}
226
227// Locate the NUL-terminator in sim memory, returning the string's
228// length. Capped at `max` to avoid runaway scans.
229func nx_rv64_sim_strlen(c: *NxRv64Cpu, sim_addr: i64, max: i64) -> i64 {
230 var n: i64 = 0
231 while n < max {
232 if nx_rv64_load_u8(c, sim_addr + n) == 0 { return n }
233 n = n + 1
234 }
235 return max
236}
237
238// We track the next sim-mmap address as a static (BSS-zero-init).
239// NishiLang doesn't yet support static initialisers; we lazy-init
240// inside the handler. Single-cpu OK for v0.0.1; multi-cpu would
241// need per-cpu bump state.
242static NX_RV64_MMAP_NEXT: i64
243
244// CSR addresses we recognise.
245const NX_RV64_CSR_CYCLE: i64 = 0xC00
246const NX_RV64_CSR_TIME: i64 = 0xC01
247const NX_RV64_CSR_INSTRET: i64 = 0xC02
248const NX_RV64_CSR_FFLAGS: i64 = 0x001
249const NX_RV64_CSR_FRM: i64 = 0x002
250const NX_RV64_CSR_FCSR: i64 = 0x003
251
252// Read a CSR. Returns 0 for unknown CSRs (sim is user-mode only;
253// machine/supervisor CSRs are out of scope).
254func nx_rv64_csr_read(c: *NxRv64Cpu, csr: i64) -> i64 {
255 if csr == NX_RV64_CSR_CYCLE { return c.insn_count }
256 if csr == NX_RV64_CSR_INSTRET { return c.insn_count }
257 if csr == NX_RV64_CSR_TIME { return __syscall(113, 1, 0, 0, 0, 0, 0) }
258 return 0
259}
260
261// Write a CSR. Performance counters are read-only, so writes to
262// CYCLE/INSTRET/TIME are silently ignored. FP CSRs are accepted
263// (no FP execution today, but glibc startup sometimes writes them).
264func nx_rv64_csr_write(c: *NxRv64Cpu, csr: i64, v: i64) -> i64 {
265 return 0
266}
267
268func nx_rv64_handle_ecall(c: *NxRv64Cpu) -> i64 {
269 let nr: i64 = c.regs[17] // a7 = syscall number
270 let a0: i64 = c.regs[10]
271 let a1: i64 = c.regs[11]
272 let a2: i64 = c.regs[12]
273 let a3: i64 = c.regs[13]
274 let a4: i64 = c.regs[14]
275 let a5: i64 = c.regs[15]
276
277 // sys_exit(code)
278 if nr == 93 {
279 c.halted = 1
280 c.exit_code = a0
281 return 0
282 }
283
284 // sys_write(fd, sim_buf, len)
285 if nr == 64 {
286 if a2 <= 0 { c.regs[10] = 0; return 0 }
287 let host_buf: *u8 = sys_mmap(a2 + 16)
288 nx_rv64_sim_to_host(c, a1, host_buf, a2)
289 let written: i64 = sys_write(a0, host_buf, a2)
290 c.regs[10] = written
291 return 0
292 }
293
294 // sys_read(fd, sim_buf, len)
295 if nr == 63 {
296 if a2 <= 0 { c.regs[10] = 0; return 0 }
297 let host_buf: *u8 = sys_mmap(a2 + 16)
298 let n_read: i64 = sys_read(a0, host_buf, a2)
299 if n_read > 0 { nx_rv64_host_to_sim(c, a1, host_buf, n_read) }
300 c.regs[10] = n_read
301 return 0
302 }
303
304 // sys_close(fd)
305 if nr == 57 {
306 c.regs[10] = sys_close(a0)
307 return 0
308 }
309
310 // sys_openat(dirfd, sim_path, flags, mode)
311 if nr == 56 {
312 let plen: i64 = nx_rv64_sim_strlen(c, a1, NX_MAGIC_4096)
313 let path_host: *u8 = sys_mmap(plen + 8)
314 nx_rv64_sim_to_host(c, a1, path_host, plen)
315 path_host[plen] = 0
316 let fd: i64 = __syscall(56, a0, path_host, a2, a3, 0, 0)
317 c.regs[10] = fd
318 return 0
319 }
320
321 // sys_mmap(addr, size, prot, flags, fd, off)
322 // Sim-internal bump allocator: hand out aligned regions from
323 // the sim address space starting at NX_RV64_MMAP_BASE.
324 if nr == 222 {
325 if NX_RV64_MMAP_NEXT == 0 { NX_RV64_MMAP_NEXT = NX_RV64_MMAP_BASE }
326 let aligned_size: i64 = (a1 + NX_MAGIC_4095) & (~NX_MAGIC_4095)
327 let result: i64 = NX_RV64_MMAP_NEXT
328 NX_RV64_MMAP_NEXT = NX_RV64_MMAP_NEXT + aligned_size
329 if NX_RV64_MMAP_NEXT > c.mem_size {
330 c.regs[10] = -1 // out of sim memory
331 return 0
332 }
333 c.regs[10] = result
334 return 0
335 }
336
337 // sys_getpid() -- pass through via __syscall.
338 if nr == 172 {
339 c.regs[10] = __syscall(172, 0, 0, 0, 0, 0, 0)
340 return 0
341 }
342
343 // sys_exit_group(code) -- alias for exit; halt the sim.
344 if nr == 94 {
345 c.halted = 1
346 c.exit_code = a0
347 return 0
348 }
349
350 // sys_set_tid_address(tidptr) -- single-threaded sim, return 1.
351 if nr == 96 {
352 c.regs[10] = 1
353 return 0
354 }
355
356 // sys_brk(addr) -- treat the heap as a sub-region of the sim
357 // mmap arena. brk(0) returns the current break (initialized
358 // to NX_RV64_MMAP_BASE on first call). brk(addr) advances the
359 // break to `addr` if it's in range.
360 if nr == 214 {
361 if NX_RV64_MMAP_NEXT == 0 { NX_RV64_MMAP_NEXT = NX_RV64_MMAP_BASE }
362 if a0 == 0 {
363 c.regs[10] = NX_RV64_MMAP_NEXT
364 return 0
365 }
366 if a0 < NX_RV64_MMAP_BASE {
367 c.regs[10] = NX_RV64_MMAP_NEXT
368 return 0
369 }
370 if a0 > c.mem_size {
371 c.regs[10] = NX_RV64_MMAP_NEXT
372 return 0
373 }
374 NX_RV64_MMAP_NEXT = a0
375 c.regs[10] = a0
376 return 0
377 }
378
379 // sys_munmap(addr, size) -- bump allocator can't free; ack
380 // anyway so the program proceeds.
381 if nr == 215 {
382 c.regs[10] = 0
383 return 0
384 }
385
386 // sys_lseek(fd, offset, whence) -- pass through via host.
387 if nr == 62 {
388 c.regs[10] = __syscall(62, a0, a1, a2, 0, 0, 0)
389 return 0
390 }
391
392 // sys_fstat(fd, sim_statbuf) -- pass through, then copy
393 // statbuf back into sim memory. Linux struct stat is 128 bytes.
394 if nr == 80 {
395 let host_st: *u8 = sys_mmap(160)
396 let r: i64 = __syscall(80, a0, host_st, 0, 0, 0, 0)
397 if r == 0 { nx_rv64_host_to_sim(c, a1, host_st, 128) }
398 c.regs[10] = r
399 return 0
400 }
401
402 // sys_gettimeofday(tv, tz) -- copy 16 bytes (tv_sec + tv_usec).
403 if nr == 169 {
404 let host_tv: *u8 = sys_mmap(32)
405 let r2: i64 = __syscall(169, host_tv, 0, 0, 0, 0, 0)
406 if r2 == 0 { nx_rv64_host_to_sim(c, a0, host_tv, 16) }
407 c.regs[10] = r2
408 return 0
409 }
410
411 // sys_clock_gettime(clk_id, tp) -- copy 16 bytes (tv_sec + tv_nsec).
412 if nr == 113 {
413 let host_tp: *u8 = sys_mmap(32)
414 let r3: i64 = __syscall(113, a0, host_tp, 0, 0, 0, 0)
415 if r3 == 0 { nx_rv64_host_to_sim(c, a1, host_tp, 16) }
416 c.regs[10] = r3
417 return 0
418 }
419
420 // sys_ioctl(fd, request, arg) -- mostly used for isatty checks.
421 // We ack ENOTTY so glibc treats fd as a regular file.
422 if nr == 29 {
423 c.regs[10] = 0 - 25 // -ENOTTY
424 return 0
425 }
426
427 // Unknown -- return -1, keep going. Real impl should signal
428 // the caller more loudly; for now we trust the compiled
429 // program to handle ENOSYS.
430 c.regs[10] = 0 - 38 // -ENOSYS
431 return 0
432}
433
434// ---- one-step executor -------------------------------------------
435//
436// Decodes the instruction at c.pc and applies the effect. Returns
437// 1 to continue, 0 if halted, -1 on illegal instruction.
438
439func nx_rv64_step(c: *NxRv64Cpu) -> i64 {
440 if c.halted == 1 { return 0 }
441
442 // Fetch. Inspect bottom 2 bits to choose 16- or 32-bit width.
443 // Compressed (RV64C) insns are expanded to a 32-bit equivalent
444 // before the rest of the decode logic runs unchanged.
445 let lo: *u8 = (((c.mem_base as i64) + c.pc) as *u8)
446 let lo_byte: i64 = lo[0]
447 var w: i64 = 0
448 var width: i64 = 0
449 if (lo_byte & 3) != 3 {
450 let h: i64 = (lo[0] as i64) | ((lo[1] as i64) << 8)
451 w = nx_rv64c_decode(h)
452 width = 2
453 if w == 0 {
454 // Illegal compressed insn (don't confuse with c.nop which
455 // expands to 0x00000013). Halt with -1.
456 return -1
457 }
458 } else {
459 w = nx_dis_word_at(c.mem_base, c.pc)
460 width = 4
461 }
462
463 let op: i64 = nx_dis_opcode(w)
464 let rd: i64 = nx_dis_rd(w)
465 let rs1: i64 = nx_dis_rs1(w)
466 let rs2: i64 = nx_dis_rs2(w)
467 let funct3: i64 = nx_dis_funct3(w)
468 let funct7: i64 = nx_dis_funct7(w)
469
470 var next_pc: i64 = c.pc + width
471 c.insn_count = c.insn_count + 1
472
473 // R-type (0x33).
474 if op == 0x33 {
475 let a: i64 = nx_rv64_rd(c, rs1)
476 let b: i64 = nx_rv64_rd(c, rs2)
477 var v: i64 = 0
478 if funct3 == 0 {
479 if funct7 == 0x20 { v = a - b }
480 if funct7 == 0 { v = a + b }
481 if funct7 == 1 { v = a * b }
482 }
483 if funct3 == 7 { if funct7 == 0 { v = a & b } }
484 if funct3 == 6 { if funct7 == 0 { v = a | b } }
485 if funct3 == 4 { if funct7 == 0 { v = a ^ b } }
486 if funct3 == 1 { if funct7 == 0 { v = a << (b & 0x3F) } }
487 if funct3 == 5 {
488 if funct7 == 0 { v = (a as i64) >> (b & 0x3F) } // SRL approx
489 if funct7 == 0x20 { v = a >> (b & 0x3F) } // SRA
490 }
491 if funct3 == 2 { if funct7 == 0 { if a < b { v = 1 } } }
492 if funct3 == 3 { if funct7 == 0 { if a < b { v = 1 } } } // SLTU approx
493 nx_rv64_wr(c, rd, v)
494 c.pc = next_pc
495 return 1
496 }
497
498 // I-type ALU (0x13).
499 if op == 0x13 {
500 let a: i64 = nx_rv64_rd(c, rs1)
501 let imm: i64 = nx_dis_imm_i(w)
502 var v: i64 = 0
503 if funct3 == 0 { v = a + imm }
504 if funct3 == 7 { v = a & imm }
505 if funct3 == 6 { v = a | imm }
506 if funct3 == 4 { v = a ^ imm }
507 if funct3 == 1 { v = a << (imm & 0x3F) }
508 if funct3 == 5 {
509 if funct7 == 0 { v = (a as i64) >> (imm & 0x3F) }
510 if funct7 == 0x20 { v = a >> (imm & 0x3F) }
511 }
512 if funct3 == 2 { if a < imm { v = 1 } }
513 if funct3 == 3 { if a < imm { v = 1 } }
514 nx_rv64_wr(c, rd, v)
515 c.pc = next_pc
516 return 1
517 }
518
519 // Loads (0x03). Each variant returns a 64-bit value; signed
520 // variants (LB/LH/LW) sign-extend before storing in the register.
521 if op == 0x03 {
522 let addr: i64 = nx_rv64_rd(c, rs1) + nx_dis_imm_i(w)
523 var v: i64 = 0
524 if funct3 == 3 { v = nx_rv64_load_u64(c, addr) } // LD
525 if funct3 == 2 { // LW (signed)
526 let raw32: i64 = nx_rv64_load_u32(c, addr)
527 v = raw32
528 if (raw32 & 0x80000000) != 0 { v = raw32 | (~0xFFFFFFFF) }
529 }
530 if funct3 == 6 { v = nx_rv64_load_u32(c, addr) & 0xFFFFFFFF } // LWU
531 if funct3 == 1 { // LH (signed)
532 let b0: i64 = nx_rv64_load_u8(c, addr + 0)
533 let b1: i64 = nx_rv64_load_u8(c, addr + 1)
534 let raw16: i64 = b0 | (b1 << 8)
535 v = raw16
536 if (raw16 & 0x8000) != 0 { v = raw16 | (~0xFFFF) }
537 }
538 if funct3 == 5 { // LHU
539 let b0: i64 = nx_rv64_load_u8(c, addr + 0)
540 let b1: i64 = nx_rv64_load_u8(c, addr + 1)
541 v = b0 | (b1 << 8)
542 }
543 if funct3 == 0 { // LB (signed)
544 let raw8: i64 = nx_rv64_load_u8(c, addr) & 0xFF
545 v = raw8
546 if (raw8 & 0x80) != 0 { v = raw8 | (~0xFF) }
547 }
548 if funct3 == 4 { v = nx_rv64_load_u8(c, addr) & 0xFF } // LBU
549 nx_rv64_wr(c, rd, v)
550 c.pc = next_pc
551 return 1
552 }
553
554 // RV64 word ops (0x3B): addw/subw/sllw/srlw/sraw -- 32-bit
555 // results sign-extended to 64.
556 if op == 0x3B {
557 let a: i64 = nx_rv64_rd(c, rs1)
558 let b: i64 = nx_rv64_rd(c, rs2)
559 var v32: i64 = 0
560 if funct3 == 0 {
561 if funct7 == 0 { v32 = (a + b) & 0xFFFFFFFF }
562 if funct7 == 0x20 { v32 = (a - b) & 0xFFFFFFFF }
563 if funct7 == 1 { v32 = (a * b) & 0xFFFFFFFF }
564 }
565 if funct3 == 1 { v32 = (a << (b & 0x1F)) & 0xFFFFFFFF }
566 if funct3 == 5 {
567 if funct7 == 0 { v32 = (a >> (b & 0x1F)) & 0xFFFFFFFF }
568 if funct7 == 0x20 { v32 = a >> (b & 0x1F) }
569 }
570 // Sign-extend 32 -> 64.
571 var v: i64 = v32
572 if (v32 & 0x80000000) != 0 { v = v32 | (~0xFFFFFFFF) }
573 nx_rv64_wr(c, rd, v)
574 c.pc = next_pc
575 return 1
576 }
577
578 // RV64 word-imm ops (0x1B): addiw / slliw / srliw / sraiw.
579 if op == 0x1B {
580 let a: i64 = nx_rv64_rd(c, rs1)
581 let imm: i64 = nx_dis_imm_i(w)
582 var v32: i64 = 0
583 if funct3 == 0 { v32 = (a + imm) & 0xFFFFFFFF } // addiw
584 if funct3 == 1 { v32 = (a << (imm & 0x1F)) & 0xFFFFFFFF } // slliw
585 if funct3 == 5 {
586 if funct7 == 0 { v32 = (a >> (imm & 0x1F)) & 0xFFFFFFFF } // srliw
587 if funct7 == 0x20 { v32 = a >> (imm & 0x1F) } // sraiw
588 }
589 var v: i64 = v32
590 if (v32 & 0x80000000) != 0 { v = v32 | (~0xFFFFFFFF) }
591 nx_rv64_wr(c, rd, v)
592 c.pc = next_pc
593 return 1
594 }
595
596 // Stores (0x23).
597 if op == 0x23 {
598 let addr: i64 = nx_rv64_rd(c, rs1) + nx_dis_imm_s(w)
599 let v: i64 = nx_rv64_rd(c, rs2)
600 if funct3 == 3 { nx_rv64_store_u64(c, addr, v) } // SD
601 if funct3 == 2 { nx_rv64_store_u32(c, addr, v) } // SW
602 if funct3 == 0 { nx_rv64_store_u8(c, addr, v & 0xFF) } // SB
603 c.pc = next_pc
604 return 1
605 }
606
607 // Branches (0x63).
608 if op == 0x63 {
609 let a: i64 = nx_rv64_rd(c, rs1)
610 let b: i64 = nx_rv64_rd(c, rs2)
611 var taken: i64 = 0
612 if funct3 == 0 { if a == b { taken = 1 } } // BEQ
613 if funct3 == 1 { if a != b { taken = 1 } } // BNE
614 if funct3 == 4 { if a < b { taken = 1 } } // BLT
615 if funct3 == 5 { if a >= b { taken = 1 } } // BGE
616 if funct3 == 6 { if a < b { taken = 1 } } // BLTU (approx)
617 if funct3 == 7 { if a >= b { taken = 1 } } // BGEU (approx)
618 if taken == 1 { next_pc = c.pc + nx_dis_imm_b(w) }
619 c.pc = next_pc
620 return 1
621 }
622
623 // U-type: lui (0x37), auipc (0x17).
624 if op == 0x37 {
625 nx_rv64_wr(c, rd, nx_dis_imm_u(w))
626 c.pc = next_pc
627 return 1
628 }
629 if op == 0x17 {
630 nx_rv64_wr(c, rd, c.pc + nx_dis_imm_u(w))
631 c.pc = next_pc
632 return 1
633 }
634
635 // J-type: jal (0x6F). Link is pc + width (handles both 4-byte
636 // jal and 2-byte c.j/c.jal expansions).
637 if op == 0x6F {
638 nx_rv64_wr(c, rd, c.pc + width)
639 c.pc = c.pc + nx_dis_imm_j(w)
640 return 1
641 }
642
643 // I-type jump: jalr (0x67). Link is pc + width.
644 if op == 0x67 {
645 let target: i64 = (nx_rv64_rd(c, rs1) + nx_dis_imm_i(w)) & (~1)
646 nx_rv64_wr(c, rd, c.pc + width)
647 c.pc = target
648 return 1
649 }
650
651 // System: ecall / ebreak / CSR ops (0x73).
652 if op == 0x73 {
653 if funct3 == 0 {
654 // ecall (imm=0) / ebreak (imm=1). We treat both as
655 // syscall-dispatch + halt-on-ebreak (sim convention).
656 let imm: i64 = nx_dis_imm_i(w)
657 if imm == 0 {
658 nx_rv64_handle_ecall(c)
659 }
660 // ebreak in user mode falls through to next pc (debugger
661 // would intercept; we don't have one).
662 c.pc = next_pc
663 return 1
664 }
665 // CSR ops. CSR address is the 12-bit immediate field
666 // (bits 31:20 of the instruction word).
667 let csr: i64 = (w >> 20) & 0xFFF
668 var old_csr: i64 = nx_rv64_csr_read(c, csr)
669 var new_val: i64 = 0
670 if funct3 == 1 {
671 // csrrw rd, csr, rs1
672 new_val = nx_rv64_rd(c, rs1)
673 nx_rv64_csr_write(c, csr, new_val)
674 }
675 if funct3 == 2 {
676 // csrrs rd, csr, rs1: set bits
677 let v: i64 = nx_rv64_rd(c, rs1)
678 if v != 0 { nx_rv64_csr_write(c, csr, old_csr | v) }
679 }
680 if funct3 == 3 {
681 // csrrc rd, csr, rs1: clear bits
682 let v2: i64 = nx_rv64_rd(c, rs1)
683 if v2 != 0 { nx_rv64_csr_write(c, csr, old_csr & (~v2)) }
684 }
685 if funct3 == 5 {
686 // csrrwi rd, csr, uimm5
687 let uimm: i64 = rs1 // rs1 field reused as immediate
688 nx_rv64_csr_write(c, csr, uimm)
689 }
690 if funct3 == 6 {
691 // csrrsi rd, csr, uimm5
692 let uimm2: i64 = rs1
693 if uimm2 != 0 { nx_rv64_csr_write(c, csr, old_csr | uimm2) }
694 }
695 if funct3 == 7 {
696 // csrrci rd, csr, uimm5
697 let uimm3: i64 = rs1
698 if uimm3 != 0 { nx_rv64_csr_write(c, csr, old_csr & (~uimm3)) }
699 }
700 nx_rv64_wr(c, rd, old_csr)
701 c.pc = next_pc
702 return 1
703 }
704
705 // RV64A atomics (0x2F). Single-threaded simulator -> we can
706 // implement each amo / lr.* / sc.* as a non-atomic load+modify+
707 // store; correctness is preserved because there's no other
708 // thread. Multi-thread support arrives with the SMP scheduler.
709 //
710 // Encoding: opcode=0x2F, funct3=0x2 (.w 32-bit) or 0x3 (.d 64-bit),
711 // funct7 high 5 bits = which amo (LR, SC, AMOSWAP, AMOADD, AMOXOR,
712 // AMOAND, AMOOR, AMOMIN, AMOMAX, AMOMINU, AMOMAXU).
713 if op == 0x2F {
714 let amo_op: i64 = (funct7 >> 2) & 0x1F
715 let is_d: i64 = funct3 & 0x1 // 1 if .d, 0 if .w
716 let addr: i64 = nx_rv64_rd(c, rs1)
717 var old: i64 = 0
718 if is_d == 1 { old = nx_rv64_load_u64(c, addr) }
719 if is_d == 0 { old = nx_rv64_load_u32(c, addr) }
720
721 // LR (0x02): load-reserved. Just load + put in rd.
722 if amo_op == 0x02 {
723 nx_rv64_wr(c, rd, old)
724 c.pc = next_pc
725 return 1
726 }
727 // SC (0x03): store-conditional. Single-thread: always succeeds.
728 if amo_op == 0x03 {
729 let val: i64 = nx_rv64_rd(c, rs2)
730 if is_d == 1 { nx_rv64_store_u64(c, addr, val) }
731 if is_d == 0 { nx_rv64_store_u32(c, addr, val) }
732 nx_rv64_wr(c, rd, 0) // 0 = success
733 c.pc = next_pc
734 return 1
735 }
736 // AMO* (0x01..0x1C): read-modify-write.
737 let arg: i64 = nx_rv64_rd(c, rs2)
738 var new_val: i64 = old
739 if amo_op == 0x01 { new_val = arg } // AMOSWAP
740 if amo_op == 0x00 { new_val = old + arg } // AMOADD
741 if amo_op == 0x04 { new_val = old ^ arg } // AMOXOR
742 if amo_op == 0x0C { new_val = old & arg } // AMOAND
743 if amo_op == 0x08 { new_val = old | arg } // AMOOR
744 if amo_op == 0x10 { // AMOMIN
745 new_val = old
746 if arg < old { new_val = arg }
747 }
748 if amo_op == 0x14 { // AMOMAX
749 new_val = old
750 if arg > old { new_val = arg }
751 }
752 if is_d == 1 { nx_rv64_store_u64(c, addr, new_val) }
753 if is_d == 0 { nx_rv64_store_u32(c, addr, new_val) }
754 nx_rv64_wr(c, rd, old) // amo returns OLD value
755 c.pc = next_pc
756 return 1
757 }
758
759 // Fence (0x0F funct3=0): treat as nop in single-thread mode.
760 if op == 0x0F {
761 c.pc = next_pc
762 return 1
763 }
764
765 // FP loads (FLW=0x07 / FLD=0x07 with funct3=2/3): defer
766 // proper f-reg state to a follow-up; for now treat as nop
767 // so the program PC advances cleanly past FP code emitted
768 // by the compiler. Programs that USE the FP value will
769 // produce wrong results; programs that just have FP code in
770 // dead branches won't crash. This is the v0.0.1 best-effort
771 // FP path.
772 if op == 0x07 {
773 c.pc = next_pc
774 return 1
775 }
776 // FP stores (FSW=0x27): same.
777 if op == 0x27 {
778 c.pc = next_pc
779 return 1
780 }
781 // FP arith (FADD/FSUB/FMUL/FDIV: opcode 0x53): nop-stub.
782 if op == 0x53 {
783 c.pc = next_pc
784 return 1
785 }
786 // FP fused multiply-add family (0x43/0x47/0x4B/0x4F): nop-stub.
787 if op == 0x43 { c.pc = next_pc; return 1 }
788 if op == 0x47 { c.pc = next_pc; return 1 }
789 if op == 0x4B { c.pc = next_pc; return 1 }
790 if op == 0x4F { c.pc = next_pc; return 1 }
791
792 // Unknown.
793 return -1
794}
795
796// Run until halt or `max_insns` budget expires. Returns:
797// 0 = halted cleanly (sys_exit delivered)
798// 1 = budget expired
799// -1 = illegal instruction
800func nx_rv64_run(c: *NxRv64Cpu, max_insns: i64) -> i64 {
801 var i: i64 = 0
802 while i < max_insns {
803 let r: i64 = nx_rv64_step(c)
804 if r == 0 { return 0 }
805 if r < 0 { return -1 }
806 i = i + 1
807 }
808 return 1
809}
810
811// ---- self-test ----------------------------------------------------
812
813func main() -> i64 {
814 // Hand-encoded RV64 program at memory offset 0:
815 // addi a0, zero, 42 -> 0x02A00513
816 // addi a7, zero, 93 -> 0x05D00893
817 // ecall -> 0x00000073
818 let mem: *u8 = sys_mmap(64)
819 mem[0] = 0x13; mem[1] = 0x05; mem[2] = 0xA0; mem[3] = 0x02
820 mem[4] = 0x93; mem[5] = 0x08; mem[6] = 0xD0; mem[7] = 0x05
821 mem[8] = 0x73; mem[9] = 0x00; mem[10] = 0x00; mem[11] = 0x00
822
823 let cpu: *NxRv64Cpu = nx_rv64_cpu_new(mem, 64, 0)
824
825 let r: i64 = nx_rv64_run(cpu, 10)
826 if r != 0 { return __syscall(93, 10, 0, 0, 0, 0, 0) }
827 if cpu.halted != 1 { return __syscall(93, 11, 0, 0, 0, 0, 0) }
828 if cpu.exit_code != 42 { return __syscall(93, 12, 0, 0, 0, 0, 0) }
829 if cpu.insn_count != 3 { return __syscall(93, 13, 0, 0, 0, 0, 0) }
830
831 return 0
832}