code wiki / (root) / nx_rv64_sim.nx

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}