code wiki / (root) / nx_riscv.nx

nx_riscv.nx source

↩ module page · 2891 lines · 110424 B

1// riscv.nx -- RV64 codegen backend, in NishiLang. 2// 3// Takes a Function + its ValueLoc table and emits RV64 assembly 4// text to an output buffer. Pairs with regalloc.nx and ir.nx. 5// 6// Scope: the opcodes our benchmarks emit. Full backend parity is 7// mechanical extension of the switch statement. 8// 9// Output format: raw asm text, same as riscv.c emits today, so the 10// output of this NishiLang codegen is byte-compatible with gcc `as` 11// (verified at compile time by reading its IR output). 12 13// ---- shared IR shapes (must match ir.nx / opt.nx / regalloc.nx) ---- 14 15// nx_safety_envelope: (schema: nishi-library/seeds/safety-critical-standards.toml) 16// intended_use: "RV64IMA(V) codegen -- lowers IR to RISC-V 17// assembly. Consumes ValueLoc table from 18// nx_regalloc; emits via OutBuf to .s or via 19// nxasm to ELF." 20// sil_target: SIL3 (codegen correctness; bugs produce 21// wrong code that may APPEAR to run 22// -- silent miscompilation is worse 23// than a crash) 24// asil_target: QM 25// dal_target: DAL B 26// iec_62304_class: NONE 27// evidence: [no_floating_point_in_codegen_logic, 28// per_instruction_emit_dispatch, 29// materialise_handles_REGISTER_SPILLED_REMAT, 30// compute_alloca_offsets_pre_pass, 31// rv64_calling_convention_documented, 32// cross_arch_smokes_x86_64_aarch64_compile_clean] 33// hazard_register: [bug-tape-F14-emit_sd_sp-register-collision, 34// bug-tape-F16-self-compile-via-this-file, 35// bug-tape-spilled-load-with-12-bit-overflow, 36// bug-tape-callee-saved-not-restored-in-epilogue] 37// residual_risk: "VAL_ALLOCA rematerialisation gap (queued). 38// Materialise() signature would need alloca_off 39// threading per the deferred comment in 40// nx_regalloc.nx lines 612-617. ANY change 41// to this file MUST run bench/self_host_gauntlet 42// before commit (F16 cardinal)." 43// verdict: NOT_YET_EVALUATED 44 45import "nx_syscalls.nx" 46import "nx_types.nx" 47import "nx_ir.nx" 48// ---- runtime helpers ---- 49 50func loc_at(locs: *ValueLoc, id: i64) -> *ValueLoc { 51 let base: i64 = locs as i64 52 return (base + id * 16) as *ValueLoc 53} 54 55// ---- output buffer (growing byte sink) ---- 56// 57// Codegen writes text one piece at a time; we accumulate into a 58// pre-allocated buffer. Caller decides capacity (big enough for 59// the whole function body). 60 61// OutBuf + out_str/out_char/out_i64 live in outbuf.nx so wasm.nx 62// (and any future backend) can share one copy. 63import "nx_outbuf.nx" 64 65// ---- register name table ---- 66// 67// Regalloc hands us integer indices 0..15. Map back to RV64 ABI 68// names (t0..t3, s0..s11) for the emitted asm. 69 70func reg_name(o: *OutBuf, idx: i64) -> i64 { 71 // Vector registers -- RVV v0..v31 at indices 300..331. Check 72 // before f-regs since 300 > 200. 73 if idx >= 300 { 74 if idx < 332 { 75 out_str(o, "v") 76 out_i64(o, idx - 300) 77 return 0 78 } 79 } 80 // Floating-point registers -- RV64F + RV64D. Indices mirror 81 // the pool in regalloc.nx: 100..111 = ft0..ft11, 200..211 = 82 // fs0..fs11. See commit c2a06ef for the pool scheme. 83 if idx >= 200 { 84 out_str(o, "fs") 85 out_i64(o, idx - 200) 86 return 0 87 } 88 if idx >= 100 { 89 if idx < 108 { 90 out_str(o, "ft") 91 out_i64(o, idx - 100) 92 return 0 93 } 94 // ft8..ft11 (indices 108..111) 95 out_str(o, "ft") 96 out_i64(o, idx - 100) 97 return 0 98 } 99 // Integer GPRs: 100 if idx == 0 { out_str(o, "t0"); return 0 } 101 if idx == 1 { out_str(o, "t1"); return 0 } 102 if idx == 2 { out_str(o, "t2"); return 0 } 103 if idx == 3 { out_str(o, "t3"); return 0 } 104 if idx == 4 { out_str(o, "s0"); return 0 } 105 if idx == 5 { out_str(o, "s1"); return 0 } 106 if idx == 6 { out_str(o, "s2"); return 0 } 107 if idx == 7 { out_str(o, "s3"); return 0 } 108 if idx == 8 { out_str(o, "s4"); return 0 } 109 if idx == 9 { out_str(o, "s5"); return 0 } 110 if idx == 10 { out_str(o, "s6"); return 0 } 111 if idx == 11 { out_str(o, "s7"); return 0 } 112 if idx == 12 { out_str(o, "s8"); return 0 } 113 if idx == 13 { out_str(o, "s9"); return 0 } 114 if idx == 14 { out_str(o, "s10"); return 0 } 115 if idx == 15 { out_str(o, "s11"); return 0 } 116 out_str(o, "t6") // scratch 117 return 0 118} 119 120// ---- sp-relative emitter helpers (12-bit immediate expansion) ----- 121// 122// RV64's I-type and S-type encodings reserve only a signed 12-bit 123// field for the immediate offset: [-2048, 2047]. Spill + alloca 124// frames in a self-hosting compiler (nxc.nx compiling itself) easily 125// exceed 2 KiB, so a raw `ld dst, 3000(sp)` would emit an illegal 126// encoding. These helpers expand any out-of-range offset into the 127// two-instruction form, gating the failure mode at emit time. All 128// sp-relative ld/sd/addi sites in this file MUST go through them 129// (closed 2026-04-23 via commit a9ab7cb). 130// 131// These helpers check range + emit the two-step expansion when out: 132// 133// ld dst, N(sp) -- N in [-2048, 2047] 134// li t6, N ; add t6, sp, t6 ; ld dst, 0(t6) -- otherwise 135// 136// Mirror of emit_ld_sp/emit_sd_sp/emit_addi_sp/emit_addi_sp_sp in 137// nxc2/riscv.c. Moving these into runtime/riscv.nx closes the last 138// major codegen-correctness gap between the C bootstrap compiler and 139// the NishiLang self-host (per docs/SOVEREIGNTY_DISCIPLINE.md). 140// 141// Scratch-register discipline: 142// emit_sp_adjust : always uses t6 (safe -- prologue/epilogue 143// has nothing else live). 144// emit_sp_ld : uses t6 for the address. Safe even when the 145// destination is t6 itself (the final `ld t6, 146// 0(t6)` overwrites the address with the loaded 147// value, which is what the caller wanted). 148// emit_sp_sd : uses t4 for the address (NOT t6, because `sd 149// t6, off(sp)` with big off would otherwise 150// clobber src with the address before the store). 151// Callers at spill-store sites must ensure t4 is 152// free; in rv_emit_binop this is guaranteed 153// because the binop has already consumed t4/t5. 154// emit_sp_addi : uses the destination register as its own 155// scratch (self-contained, always safe). 156 157func fits_imm12(off: i64) -> i64 { 158 if off < -2048 { return 0 } 159 if off > 2047 { return 0 } 160 return 1 161} 162 163// emit `addi sp, sp, off` with expansion when out of range. 164func emit_sp_adjust(o: *OutBuf, off: i64) -> i64 { 165 if fits_imm12(off) == 1 { 166 out_str(o, " addi sp, sp, ") 167 out_i64(o, off) 168 out_char(o, 0x0A) 169 return 0 170 } 171 out_str(o, " li t6, ") 172 out_i64(o, off) 173 out_char(o, 0x0A) 174 out_str(o, " add sp, sp, t6\n") 175 return 0 176} 177 178// emit `ld dst, off(sp)` with expansion when out of range. 179// Uses t6 internally as address holder. 180func emit_sp_ld(o: *OutBuf, dst: *u8, off: i64) -> i64 { 181 if fits_imm12(off) == 1 { 182 out_str(o, " ld ") 183 out_str(o, dst) 184 out_str(o, ", ") 185 out_i64(o, off) 186 out_str(o, "(sp)\n") 187 return 0 188 } 189 out_str(o, " li t6, ") 190 out_i64(o, off) 191 out_char(o, 0x0A) 192 out_str(o, " add t6, sp, t6\n") 193 out_str(o, " ld ") 194 out_str(o, dst) 195 out_str(o, ", 0(t6)\n") 196 return 0 197} 198 199// Return 1 if the two-byte strings a[0..2] and b[0..2] match (used 200// to detect "t4" / "t5" name collisions inside emit_sp_sd). Compares 201// exactly the first 2 characters -- callers pass short register 202// names so this is sufficient for the scratch-selection heuristic. 203func sp_is_reg(name: *u8, a: i64, b: i64) -> i64 { 204 if name[0] != a { return 0 } 205 if name[1] != b { return 0 } 206 return 1 207} 208 209// emit `sd src, off(sp)` with expansion when out of range. 210// 211// Scratch-register selection: 212// default: t4 (matches the "binop consumes t4/t5" convention that 213// makes t4/t5 dead at every spill-store site in this 214// backend) 215// if src=="t4" (call-result or csrr stores src there): use t5 216// instead, to avoid clobbering src with the address 217// if src=="t5" (not used today but future-proof): fall back to t4 218// 219// The alternative of always using t6 fails because 6 of the 10 220// current callers pass src=="t6" (binop spill-stores); using t6 as 221// the address holder would clobber src before the store. See the 222// scratch-register discipline block above this helper for the full 223// derivation. 224func emit_sp_sd(o: *OutBuf, src: *u8, off: i64) -> i64 { 225 if fits_imm12(off) == 1 { 226 out_str(o, " sd ") 227 out_str(o, src) 228 out_str(o, ", ") 229 out_i64(o, off) 230 out_str(o, "(sp)\n") 231 return 0 232 } 233 // Pick a scratch that differs from src. 234 var scratch: *u8 = "t4" as *u8 235 if sp_is_reg(src, 0x74, 0x34) == 1 { scratch = "t5" as *u8 } 236 out_str(o, " li ") 237 out_str(o, scratch) 238 out_str(o, ", ") 239 out_i64(o, off) 240 out_char(o, 0x0A) 241 out_str(o, " add ") 242 out_str(o, scratch) 243 out_str(o, ", sp, ") 244 out_str(o, scratch) 245 out_char(o, 0x0A) 246 out_str(o, " sd ") 247 out_str(o, src) 248 out_str(o, ", 0(") 249 out_str(o, scratch) 250 out_str(o, ")\n") 251 return 0 252} 253 254// emit `addi dst, sp, off` with expansion when out of range. 255// Uses the destination register as its own scratch (self-contained). 256func emit_sp_addi(o: *OutBuf, dst: *u8, off: i64) -> i64 { 257 if fits_imm12(off) == 1 { 258 out_str(o, " addi ") 259 out_str(o, dst) 260 out_str(o, ", sp, ") 261 out_i64(o, off) 262 out_char(o, 0x0A) 263 return 0 264 } 265 out_str(o, " li ") 266 out_str(o, dst) 267 out_str(o, ", ") 268 out_i64(o, off) 269 out_char(o, 0x0A) 270 out_str(o, " add ") 271 out_str(o, dst) 272 out_str(o, ", sp, ") 273 out_str(o, dst) 274 out_char(o, 0x0A) 275 return 0 276} 277 278// ---- sp-relative flw / fsw (single-precision fp load/store) ------- 279// 280// Parallel to emit_sp_ld / emit_sp_sd but for the F-extension's 281// flw (load word float) / fsw (store word float) instructions. 282// 283// Address-holder scratch is always a GPR (t4 / t6), never an 284// f-register -- the integer regfile is disjoint from the float 285// regfile, so the src/dst f-register can't alias the address GPR. 286// This makes the scratch-picking logic simpler than emit_sp_sd 287// (no src==scratch collision case). 288// 289// These helpers activate when the fp-regalloc commit lands and 290// starts assigning spill slots to f-values. Dead code on the 291// self-host today. 292 293func emit_sp_flw(o: *OutBuf, dst: *u8, off: i64) -> i64 { 294 if fits_imm12(off) == 1 { 295 out_str(o, " flw ") 296 out_str(o, dst) 297 out_str(o, ", ") 298 out_i64(o, off) 299 out_str(o, "(sp)\n") 300 return 0 301 } 302 out_str(o, " li t6, ") 303 out_i64(o, off) 304 out_char(o, 0x0A) 305 out_str(o, " add t6, sp, t6\n") 306 out_str(o, " flw ") 307 out_str(o, dst) 308 out_str(o, ", 0(t6)\n") 309 return 0 310} 311 312func emit_sp_fsw(o: *OutBuf, src: *u8, off: i64) -> i64 { 313 if fits_imm12(off) == 1 { 314 out_str(o, " fsw ") 315 out_str(o, src) 316 out_str(o, ", ") 317 out_i64(o, off) 318 out_str(o, "(sp)\n") 319 return 0 320 } 321 out_str(o, " li t4, ") 322 out_i64(o, off) 323 out_char(o, 0x0A) 324 out_str(o, " add t4, sp, t4\n") 325 out_str(o, " fsw ") 326 out_str(o, src) 327 out_str(o, ", 0(t4)\n") 328 return 0 329} 330 331// 64-bit FP load/store from sp+off. Mirrors emit_sp_flw / emit_sp_fsw 332// shape; just swaps `flw`/`fsw` for `fld`/`fsd` (D-extension). The 333// 8-byte spill slots regalloc hands out are already 8-aligned so `fld` 334// is safe. 335 336func emit_sp_fld(o: *OutBuf, dst: *u8, off: i64) -> i64 { 337 if fits_imm12(off) == 1 { 338 out_str(o, " fld ") 339 out_str(o, dst) 340 out_str(o, ", ") 341 out_i64(o, off) 342 out_str(o, "(sp)\n") 343 return 0 344 } 345 out_str(o, " li t6, ") 346 out_i64(o, off) 347 out_char(o, 0x0A) 348 out_str(o, " add t6, sp, t6\n") 349 out_str(o, " fld ") 350 out_str(o, dst) 351 out_str(o, ", 0(t6)\n") 352 return 0 353} 354 355func emit_sp_fsd(o: *OutBuf, src: *u8, off: i64) -> i64 { 356 if fits_imm12(off) == 1 { 357 out_str(o, " fsd ") 358 out_str(o, src) 359 out_str(o, ", ") 360 out_i64(o, off) 361 out_str(o, "(sp)\n") 362 return 0 363 } 364 out_str(o, " li t4, ") 365 out_i64(o, off) 366 out_char(o, 0x0A) 367 out_str(o, " add t4, sp, t4\n") 368 out_str(o, " fsd ") 369 out_str(o, src) 370 out_str(o, ", 0(t4)\n") 371 return 0 372} 373 374// ---- F-extension binop mnemonics ---- 375// 376// Single-precision uses `.s` suffix, double `.d`. Mirrors RV64F 377// ISA encoding. Called by emit_fbinop (future commit) once the 378// OP_F* opcodes are lowered. 379 380func emit_fbinop_mnem(o: *OutBuf, op: i64, is_double: i64) -> i64 { 381 var suffix: *u8 = "s" as *u8 382 if is_double == 1 { suffix = "d" as *u8 } 383 // OP_FADD=50, OP_FSUB=51, OP_FMUL=52, OP_FDIV=53 (types.nx) 384 if op == 50 { 385 out_str(o, "fadd.") 386 out_str(o, suffix) 387 return 0 388 } 389 if op == 51 { 390 out_str(o, "fsub.") 391 out_str(o, suffix) 392 return 0 393 } 394 if op == 52 { 395 out_str(o, "fmul.") 396 out_str(o, suffix) 397 return 0 398 } 399 if op == 53 { 400 out_str(o, "fdiv.") 401 out_str(o, suffix) 402 return 0 403 } 404 out_str(o, "fadd.") // fallback 405 out_str(o, suffix) 406 return 0 407} 408 409// ---- binop mnemonic table ---- 410 411func emit_binop_mnem(o: *OutBuf, op: i64) -> i64 { 412 if op == 1 { out_str(o, "add"); return 0 } 413 if op == 2 { out_str(o, "sub"); return 0 } 414 if op == 3 { out_str(o, "mul"); return 0 } 415 if op == 4 { out_str(o, "div"); return 0 } 416 if op == 6 { out_str(o, "rem"); return 0 } 417 if op == 10 { out_str(o, "and"); return 0 } 418 if op == 11 { out_str(o, "or"); return 0 } 419 if op == 12 { out_str(o, "xor"); return 0 } 420 if op == 13 { out_str(o, "sll"); return 0 } 421 if op == 14 { out_str(o, "sra"); return 0 } 422 if op == 15 { out_str(o, "srl"); return 0 } 423 out_str(o, "nop") 424 return 0 425} 426 427// ---- materialise ---- 428// 429// Given a Value id, produce a register name that holds its value 430// at the current point. If it's a constant, emit `li scratch, N` 431// and return "scratch". If it's in a register, return that reg. 432// If it's spilled, emit `ld scratch, offset(sp)` and return scratch. 433// 434// `scratch` is whichever temp name the caller passes in. Writes 435// directly to the output buffer. 436 437func materialise(f: *Function, locs: *ValueLoc, o: *OutBuf, 438 id: i64, scratch: *u8) -> i64 { 439 let v: *Value = val_at(f, id) 440 if v.kind == 0 { 441 // Emit `li <scratch>, <const>` 442 out_str(o, " li ") 443 out_str(o, scratch) 444 out_str(o, ", ") 445 out_i64(o, v.const_int) 446 out_char(o, 0x0A) 447 return 0 448 } 449 if v.kind == VK_GLOBAL { 450 // Emit `la <scratch>, .Lg<gid>` -- nxasm expands to 451 // auipc + addi at assemble time so the runtime address is 452 // the loaded virtual address of the global, not the id. 453 out_str(o, " la ") 454 out_str(o, scratch) 455 out_str(o, ", .Lg") 456 out_i64(o, v.const_int) 457 out_char(o, 0x0A) 458 return 0 459 } 460 if v.kind == VK_FUNC_ADDR { 461 // `&fn` / bare function name: const_int holds a *Function; emit 462 // `la <scratch>, <fnname>` (nxasm expands to auipc+addi = the 463 // function's PC-relative address). Mirrors x86 `leaq name(%rip)`. 464 // Without this a VK_FUNC_ADDR fell through to the ValueLoc path and 465 // read an UNSET register -> fn-ptrs passed as args were garbage. 466 let fnp: *Function = v.const_int as *Function 467 out_str(o, " la ") 468 out_str(o, scratch) 469 out_str(o, ", ") 470 out_str(o, fnp.name_start as *u8) 471 out_char(o, 0x0A) 472 return 0 473 } 474 let l: *ValueLoc = loc_at(locs, id) 475 if l.kind == 0 { 476 // Register; caller uses reg_name(idx) -- we write into a 477 // user-supplied name buffer. Here we just emit a `mv` so 478 // the rest of the code can uniformly use `scratch`. 479 out_str(o, " mv ") 480 out_str(o, scratch) 481 out_str(o, ", ") 482 reg_name(o, l.idx) 483 out_char(o, 0x0A) 484 return 0 485 } 486 if l.kind == 3 { 487 // VL_ALLOCA: rematerialise alloca address inline. l.idx is 488 // the sp-relative byte offset of the alloca's stack slot. 489 // Mirrors C anchor behaviour; closes F14. See cardinal 490 // feedback-self-compile-reentry-prevention-pillar. 491 out_str(o, " addi ") 492 out_str(o, scratch) 493 out_str(o, ", sp, ") 494 out_i64(o, l.idx) 495 out_char(o, 0x0A) 496 return 0 497 } 498 // Spilled -- emit `ld scratch, offset(sp)` (with 12-bit expansion). 499 emit_sp_ld(o, scratch, l.idx) 500 return 0 501} 502 503// As-VALUE materialisation. Identical to materialise() for EVERY value/loc kind EXCEPT 504// VL_ALLOCA (kind 3): here the alloca's STORED VALUE is LOADED (`ld off(sp)`) instead of 505// its address being rematerialised (`addi sp, off`). Mirrors x86 load_value_v: nx_parse 506// does NOT insert the implicit "load pointer from alloca" before a value-USE (binop / cmp 507// / call-arg / return / indirect-call target), so the backend must -- else e.g. `s[i]` 508// (frontend: OP_ADD(s, off)) with a pointer PARAM `s` used `&slot_s + i` instead of the 509// loaded pointer + i, reading the pointer's own bytes (garbled `fc("A")`, uart_puts). 510// A binop/call/return operand is never an aggregate (arrays subscript via GEP, not OP_ADD), 511// so loading is always correct here; for every NON-alloca kind this is byte-identical to 512// materialise(), so switching a value-site to it only fixes the previously-wrong case. 513func materialise_value(f: *Function, locs: *ValueLoc, o: *OutBuf, 514 id: i64, scratch: *u8) -> i64 { 515 let v: *Value = val_at(f, id) 516 if v.kind == 0 { 517 out_str(o, " li ") 518 out_str(o, scratch) 519 out_str(o, ", ") 520 out_i64(o, v.const_int) 521 out_char(o, 0x0A) 522 return 0 523 } 524 if v.kind == VK_GLOBAL { 525 out_str(o, " la ") 526 out_str(o, scratch) 527 out_str(o, ", .Lg") 528 out_i64(o, v.const_int) 529 out_char(o, 0x0A) 530 return 0 531 } 532 if v.kind == VK_FUNC_ADDR { 533 let fnp: *Function = v.const_int as *Function 534 out_str(o, " la ") 535 out_str(o, scratch) 536 out_str(o, ", ") 537 out_str(o, fnp.name_start as *u8) 538 out_char(o, 0x0A) 539 return 0 540 } 541 let l: *ValueLoc = loc_at(locs, id) 542 if l.kind == 0 { 543 out_str(o, " mv ") 544 out_str(o, scratch) 545 out_str(o, ", ") 546 reg_name(o, l.idx) 547 out_char(o, 0x0A) 548 return 0 549 } 550 if l.kind == 3 { 551 // VL_ALLOCA. An AGGREGATE (array/struct) IS its address -- its "value" passed to 552 // a call/return is the pointer to its storage -> rematerialise the address (as 553 // materialise() does). A SCALAR/POINTER alloca HOLDS its value -> LOAD it. This 554 // aggregate guard makes materialise_value safe at EVERY value position, incl. call 555 // args where a struct/array may be passed by reference. 556 if v.ty != (0 as *Type) { 557 if v.ty.kind == TY_ARRAY { 558 out_str(o, " addi ") 559 out_str(o, scratch) 560 out_str(o, ", sp, ") 561 out_i64(o, l.idx) 562 out_char(o, 0x0A) 563 return 0 564 } 565 if v.ty.kind == TY_STRUCT { 566 out_str(o, " addi ") 567 out_str(o, scratch) 568 out_str(o, ", sp, ") 569 out_i64(o, l.idx) 570 out_char(o, 0x0A) 571 return 0 572 } 573 } 574 emit_sp_ld(o, scratch, l.idx) 575 return 0 576 } 577 // Spilled. 578 emit_sp_ld(o, scratch, l.idx) 579 return 0 580} 581 582// ---- fmaterialise ------------------------------------------------- 583// 584// Float analog of materialise(): resolve a TY_F32/F64 Value `id` 585// into a named f-register. Dispatches on v.ty.kind: 586// 587// TY_F32 (single): 588// VK_CONST -> 'li t6, <bits> ; fmv.w.x scratch, t6' 589// (parse.nx fp32_from_parts stores 32-bit IEEE 754 590// bit pattern in const_int's low half. fmv.w.x 591// moves those 32 bits into f-reg low half.) 592// REGISTER -> 'fmv.s scratch, <src_reg>' 593// SPILLED -> 'flw scratch, off(sp)' via 12-bit expansion 594// 595// TY_F64 (double): 596// VK_CONST -> 'li t6, <bits> ; fmv.d.x scratch, t6' 597// (parse.nx fp64_from_parts stores all 64 IEEE 754 598// binary64 bits in const_int. fmv.d.x transfers 599// all 64 bits into the f-register.) 600// REGISTER -> 'fmv.d scratch, <src_reg>' 601// SPILLED -> 'fld scratch, off(sp)' via 12-bit expansion 602 603func fmaterialise(f: *Function, locs: *ValueLoc, o: *OutBuf, 604 id: i64, scratch: *u8) -> i64 { 605 let v: *Value = val_at(f, id) 606 var is_d: i64 = 0 607 if v.ty != (0 as *Type) { 608 if v.ty.kind == TY_F64 { is_d = 1 } 609 } 610 if v.kind == VAL_CONST { 611 out_str(o, " li t6, ") 612 out_i64(o, v.const_int) 613 out_char(o, 0x0A) 614 if is_d == 1 { 615 out_str(o, " fmv.d.x ") 616 } else { 617 out_str(o, " fmv.w.x ") 618 } 619 out_str(o, scratch) 620 out_str(o, ", t6\n") 621 return 0 622 } 623 let l: *ValueLoc = loc_at(locs, id) 624 if l.kind == 0 { 625 if is_d == 1 { 626 out_str(o, " fmv.d ") 627 } else { 628 out_str(o, " fmv.s ") 629 } 630 out_str(o, scratch) 631 out_str(o, ", ") 632 reg_name(o, l.idx) 633 out_char(o, 0x0A) 634 return 0 635 } 636 // Spilled -- 64-bit fld for f64, 32-bit flw for f32. 637 if is_d == 1 { 638 emit_sp_fld(o, scratch, l.idx) 639 } else { 640 emit_sp_flw(o, scratch, l.idx) 641 } 642 return 0 643} 644 645// ---- emit an F-extension binop (fadd/fsub/fmul/fdiv) -------------- 646// 647// Now supports full register + spill mix. Operands materialise into 648// reserved scratches ft4 / ft5; result lands in the regalloc home or 649// ft6 if spilled. 650// 651// v0.0.1 still single-precision only (is_double hardwired 0). Bumps 652// to F64 once TY_F64 plumbs through emit_fbinop_mnem and parse.nx 653// accepts f64 literals. 654// 655// Spill discipline: ft4/ft5/ft6 are reserved scratches in the f-reg 656// allocator (same convention as GPR t4/t5/t6). The allocator pool 657// formally excludes them once the f-reg regalloc commit lands. 658// Today the allocator is GPR-only so nothing homes ft4/ft5/ft6 659// anyway -- the reservation cost is zero. 660 661func rv_emit_fbinop(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 662 // Determine precision from the instruction's result type. Binops 663 // are uniform-typed (both operands == result), so checking i.ty 664 // suffices. 665 var is_d: i64 = 0 666 if i.ty != (0 as *Type) { 667 if i.ty.kind == TY_F64 { is_d = 1 } 668 } 669 670 // Bring operands into scratch f-regs. fmaterialise dispatches 671 // single vs double via each Value's own ty.kind so a mixed-prec 672 // operand (should never happen for a valid IR, but harmless) is 673 // still loaded with the correct mnemonic. 674 fmaterialise(f, locs, o, i.op0, "ft4" as *u8) 675 fmaterialise(f, locs, o, i.op1, "ft5" as *u8) 676 677 let dl: *ValueLoc = loc_at(locs, i.result) 678 out_str(o, " ") 679 emit_fbinop_mnem(o, i.op, is_d) 680 out_str(o, " ") 681 if dl.kind == 0 { 682 reg_name(o, dl.idx) 683 } 684 if dl.kind == 1 { 685 out_str(o, "ft6") 686 } 687 out_str(o, ", ft4, ft5\n") 688 689 // Spill result back if needed: 64-bit fsd for double, 32-bit fsw 690 // for single. 691 if dl.kind == 1 { 692 if is_d == 1 { 693 emit_sp_fsd(o, "ft6" as *u8, dl.idx) 694 } else { 695 emit_sp_fsw(o, "ft6" as *u8, dl.idx) 696 } 697 } 698 return 0 699} 700 701// ---- RVV vector binop mnemonic table ---- 702// 703// Maps OP_V* / OP_VF* to the corresponding RVV mnemonic. All use 704// the .vv (vector-vector) variant for the scaffold; .vx / .vi 705// variants (mixed vector-scalar, vector-immediate) land when the 706// IR gets expressivity for scalar-in-vector ops. 707 708func emit_vbinop_mnem(o: *OutBuf, op: i64) -> i64 { 709 if op == 80 { out_str(o, "vadd.vv"); return 0 } // OP_VADD 710 if op == 81 { out_str(o, "vsub.vv"); return 0 } // OP_VSUB 711 if op == 82 { out_str(o, "vmul.vv"); return 0 } // OP_VMUL 712 if op == 83 { out_str(o, "vdiv.vv"); return 0 } // OP_VDIV (signed) 713 if op == 84 { out_str(o, "vfadd.vv"); return 0 } // OP_VFADD 714 if op == 85 { out_str(o, "vfsub.vv"); return 0 } // OP_VFSUB 715 if op == 86 { out_str(o, "vfmul.vv"); return 0 } // OP_VFMUL 716 if op == 87 { out_str(o, "vfdiv.vv"); return 0 } // OP_VFDIV 717 // Fallback -- should never fire once dispatch guards are tight. 718 out_str(o, "vadd.vv") 719 return 0 720} 721 722// ---- emit an RVV vector binop (vadd.vv / vfadd.vv / ...) ---- 723// 724// v0.0.1 assumptions: 725// * Both operands + result are vector-register-homed. The v-reg 726// allocator isn't wired yet; hand-built IR exercises this path. 727// * Vector length is set once at function entry via vsetvli 728// (future: per-block or per-loop). For the scaffold, callers 729// must emit a vsetvli themselves before the vbinop runs. 730// * Mask disabled (unmasked op). 731// 732// Matches rv_emit_fbinop's shape: mnemonic, dst, lhs, rhs. 733 734func rv_emit_vbinop(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 735 let dl: *ValueLoc = loc_at(locs, i.result) 736 let ll: *ValueLoc = loc_at(locs, i.op0) 737 let rl: *ValueLoc = loc_at(locs, i.op1) 738 out_str(o, " ") 739 emit_vbinop_mnem(o, i.op) 740 out_str(o, " ") 741 reg_name(o, dl.idx) 742 out_str(o, ", ") 743 reg_name(o, ll.idx) 744 out_str(o, ", ") 745 reg_name(o, rl.idx) 746 out_char(o, 0x0A) 747 return 0 748} 749 750// ---- emit a binop ---- 751// 752// Materialise lhs and rhs into t4 and t5 (reserved scratches), 753// perform the op into the destination reg / slot, store back if 754// spilled. This is the simplified form; the C backend folds 755// register-already-assigned operands to avoid the extra mv. 756 757func rv_emit_binop(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 758 // Operands are VALUE uses -> materialise_value so an alloca'd scalar/pointer (e.g. a 759 // pointer PARAM in `s[i]` = OP_ADD(s, off)) is LOADED, not addressed. (F14 remat gave 760 // the slot address -> pointer arithmetic on &slot instead of the pointer.) 761 materialise_value(f, locs, o, i.op0, "t4") 762 materialise_value(f, locs, o, i.op1, "t5") 763 764 let dl: *ValueLoc = loc_at(locs, i.result) 765 766 // 32-bit (i32/u32) result -> WRAP mod 2^32 after the op. RV64 `add`/`mul`/`sll`/... 767 // compute in 64 bits, so a u32 add never overflowed away its high bits (0xEE6B2800 + 768 // 0x3B9ACA00 stayed 5e9 instead of 705032704). The sovereign assembler has no `.W` 769 // forms, so canonicalise with a shift pair: slli 32 then srli 32 (unsigned -> zero- 770 // extend low 32) or srai 32 (signed i32 -> sign-extend). Gated on TY_I32, so i64 771 // arithmetic emits byte-identically to before (zero extra instructions / no regression). 772 var is_w: i64 = 0 773 var w_signed: i64 = 0 774 if i.ty != (0 as *Type) { 775 if i.ty.kind == TY_I32 { 776 is_w = 1 777 if i.ty.sext == 1 { w_signed = 1 } 778 } 779 } 780 781 out_str(o, " ") 782 emit_binop_mnem(o, i.op) 783 out_str(o, " ") 784 if dl.kind == 0 { 785 reg_name(o, dl.idx) 786 } 787 if dl.kind == 1 { 788 out_str(o, "t6") 789 } 790 out_str(o, ", t4, t5\n") 791 792 if is_w == 1 { 793 out_str(o, " slli ") 794 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 795 out_str(o, ", ") 796 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 797 out_str(o, ", 32\n") 798 if w_signed == 1 { out_str(o, " srai ") } else { out_str(o, " srli ") } 799 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 800 out_str(o, ", ") 801 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 802 out_str(o, ", 32\n") 803 } 804 805 if dl.kind == 1 { 806 emit_sp_sd(o, "t6" as *u8, dl.idx) 807 } 808 return 0 809} 810 811// ---- callee-save helpers ------------------------------------------ 812// 813// popcount(x): count set bits. Used to size the save area from a 814// regalloc used_cs_mask where bits 4..15 flag s0..s11 assignments. 815func popcount(x: i64) -> i64 { 816 var n: i64 = 0 817 var v: i64 = x 818 while v != 0 { 819 n = n + (v & 1) 820 v = v >> 1 821 } 822 return n 823} 824 825// Emit `sd s<n>, off(sp)` for each set bit in mask. Bit k (4..15) 826// corresponds to regalloc index k = s<k-4>. Offsets start at 827// save_base and step by 8 for each saved register. emit_sp_sd 828// handles 12-bit imm expansion so large frames work. 829// emit_save_gpr: store each saved s-reg + emit .cfi_offset so GDB 830// can locate the saved value in an unwound frame. The CFA is the 831// caller's sp, which equals our sp + actual_frame. Offset from CFA 832// of a save at sp+N is therefore (N - actual_frame). 833func emit_save_gpr(o: *OutBuf, mask: i64, save_base: i64, 834 actual_frame: i64) -> i64 { 835 var k: i64 = 4 836 var slot: i64 = 0 837 while k < 16 { 838 let bit: i64 = (mask >> k) & 1 839 if bit == 1 { 840 let name_raw: *u8 = sys_mmap(8) 841 name_raw[0] = 0x73 // 's' 842 let n: i64 = k - 4 843 if n < 10 { 844 name_raw[1] = 0x30 + n 845 name_raw[2] = 0 846 } 847 if n >= 10 { 848 name_raw[1] = 0x31 // '1' 849 name_raw[2] = 0x30 + (n - 10) 850 name_raw[3] = 0 851 } 852 let sp_off: i64 = save_base + slot * 8 853 emit_sp_sd(o, name_raw, sp_off) 854 out_str(o, " .cfi_offset ") 855 out_str(o, name_raw) 856 out_str(o, ", ") 857 out_i64(o, sp_off - actual_frame) 858 out_char(o, 0x0A) 859 slot = slot + 1 860 } 861 k = k + 1 862 } 863 return 0 864} 865 866// Symmetric restore via emit_sp_ld. 867func emit_restore_gpr(o: *OutBuf, mask: i64, save_base: i64) -> i64 { 868 var k: i64 = 4 869 var slot: i64 = 0 870 while k < 16 { 871 let bit: i64 = (mask >> k) & 1 872 if bit == 1 { 873 let name_raw: *u8 = sys_mmap(8) 874 name_raw[0] = 0x73 // 's' 875 let n: i64 = k - 4 876 if n < 10 { 877 name_raw[1] = 0x30 + n 878 name_raw[2] = 0 879 } 880 if n >= 10 { 881 name_raw[1] = 0x31 882 name_raw[2] = 0x30 + (n - 10) 883 name_raw[3] = 0 884 } 885 emit_sp_ld(o, name_raw, save_base + slot * 8) 886 slot = slot + 1 887 } 888 k = k + 1 889 } 890 return 0 891} 892 893// FPR callee-save: fs0..fs11 mapped to bits 0..11 of 894// used_cs_mask_fpr (written by linear_scan_fpr using 895// `picked - FREG_BASE_S`, so bit k = fs<k>). Uses fsd for 64-bit 896// preservation so f64 calling convention is correct; the low 32 897// bits are what f32 values care about. Stores go via emit_sp_sd 898// -- wait, those write integer gprs. Need dedicated fsd variants. 899// 900// We emit raw fsd/fld here rather than add generic fsd/fld helpers 901// because the offset is always within our save area which we've 902// sized to fit in a 12-bit imm (max 12 saves * 8 bytes + ra_slot 903// headroom = ~112 bytes). Keeps the helper tight. 904// emit_save_fpr: fsd each saved fs-reg + emit .cfi_offset. RISC-V 905// DWARF register numbers: f0..f31 are 32..63, so fs0 (ABI) = f8 906// arch register = DWARF 32+8 = 40, fs1 = 41, etc. GDB uses these 907// numbers to symbolically reference the register by ABI name. 908func emit_save_fpr(o: *OutBuf, mask: i64, save_base: i64, 909 actual_frame: i64) -> i64 { 910 var k: i64 = 0 911 var slot: i64 = 0 912 while k < 12 { 913 let bit: i64 = (mask >> k) & 1 914 if bit == 1 { 915 out_str(o, " fsd fs") 916 if k < 10 { 917 out_char(o, 0x30 + k) 918 } 919 if k >= 10 { 920 out_char(o, 0x31) 921 out_char(o, 0x30 + (k - 10)) 922 } 923 let sp_off: i64 = save_base + slot * 8 924 out_str(o, ", ") 925 out_i64(o, sp_off) 926 out_str(o, "(sp)\n") 927 out_str(o, " .cfi_offset fs") 928 if k < 10 { 929 out_char(o, 0x30 + k) 930 } 931 if k >= 10 { 932 out_char(o, 0x31) 933 out_char(o, 0x30 + (k - 10)) 934 } 935 out_str(o, ", ") 936 out_i64(o, sp_off - actual_frame) 937 out_char(o, 0x0A) 938 slot = slot + 1 939 } 940 k = k + 1 941 } 942 return 0 943} 944 945func emit_restore_fpr(o: *OutBuf, mask: i64, save_base: i64) -> i64 { 946 var k: i64 = 0 947 var slot: i64 = 0 948 while k < 12 { 949 let bit: i64 = (mask >> k) & 1 950 if bit == 1 { 951 out_str(o, " fld fs") 952 if k < 10 { 953 out_char(o, 0x30 + k) 954 } 955 if k >= 10 { 956 out_char(o, 0x31) 957 out_char(o, 0x30 + (k - 10)) 958 } 959 out_str(o, ", ") 960 out_i64(o, save_base + slot * 8) 961 out_str(o, "(sp)\n") 962 slot = slot + 1 963 } 964 k = k + 1 965 } 966 return 0 967} 968 969// ---- emit return ---- 970// 971// Put the return value in a0, unwind frame, `ret`. 972// 973// When save_mask > 0, the function has callee-saved s-regs that 974// need restoring before the final `ret`. Rather than inlining 975// restores at every RETURN instruction (which may be called many 976// times), rv_emit_return emits a `j .L<fn>_epi` and the single 977// epilogue block (emitted by emit_function after the last block) 978// performs the restores + frame pop + ret. When save_mask == 0 979// the inline path is preserved byte-identically, so simple 980// functions keep their existing asm and F6 stays byte-stable. 981 982func rv_emit_return(f: *Function, locs: *ValueLoc, o: *OutBuf, 983 frame_size: i64, ra_slot: i64, i: *Instr, 984 fn_name: *u8) -> i64 { 985 if i.n_operands > 0 { 986 let v: *Value = val_at(f, i.op0) 987 var is_fp: i64 = 0 988 var is_d: i64 = 0 989 if v.ty != (0 as *Type) { 990 let k: i64 = v.ty.kind 991 if k == TY_F32 { is_fp = 1 } 992 if k == TY_F64 { is_fp = 1; is_d = 1 } 993 } 994 if is_fp == 1 { 995 // RV64GD: FP return goes in fa0. 996 if v.kind == 0 { 997 out_str(o, " li t6, ") 998 out_i64(o, v.const_int) 999 out_char(o, 0x0A) 1000 if is_d == 1 { 1001 out_str(o, " fmv.d.x fa0, t6\n") 1002 } else { 1003 out_str(o, " fmv.w.x fa0, t6\n") 1004 } 1005 } 1006 if v.kind != 0 { 1007 fmaterialise(f, locs, o, i.op0, "fa0" as *u8) 1008 } 1009 } else { 1010 if v.kind == 0 { 1011 out_str(o, " li a0, ") 1012 out_i64(o, v.const_int) 1013 out_char(o, 0x0A) 1014 } 1015 if v.kind != 0 { 1016 materialise_value(f, locs, o, i.op0, "a0") // returned VALUE (load an alloca'd pointer/scalar) 1017 } 1018 } 1019 } 1020 // Always jump to the function's shared epilogue block. It holds 1021 // the s-reg restores (when save_mask > 0) plus ld ra / addi sp / 1022 // ret. Unifying return control flow simplifies emit_function and 1023 // avoids plumbing save_mask through emit_instr (which otherwise 1024 // would hit the nxc2/riscv.c 8-arg ABI cap). 1025 out_str(o, " j .L") 1026 out_str(o, fn_name) 1027 out_str(o, "_epi\n") 1028 return 0 1029} 1030 1031// ---- emit branch ---- 1032 1033func emit_branch(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr, 1034 fn_name: *u8) -> i64 { 1035 if i.op == 31 { 1036 out_str(o, " j .L") 1037 out_str(o, fn_name) 1038 out_str(o, "_bb") 1039 out_i64(o, i.op0) 1040 out_char(o, 0x0A) 1041 return 0 1042 } 1043 // BR_COND: cond in op0 (Value id), targets are block ids op1/op2 1044 materialise(f, locs, o, i.op0, "t4") 1045 out_str(o, " bnez t4, .L") 1046 out_str(o, fn_name) 1047 out_str(o, "_bb") 1048 out_i64(o, i.op1) 1049 out_char(o, 0x0A) 1050 out_str(o, " j .L") 1051 out_str(o, fn_name) 1052 out_str(o, "_bb") 1053 out_i64(o, i.op2) 1054 out_char(o, 0x0A) 1055 return 0 1056} 1057 1058// ---- comparisons (port from riscv.c's emit_cmp) ---- 1059// 1060// RV64 has slt (signed less-than) that writes 0/1. Other compares 1061// compose via slt + xori: 1062// lt slt dst, lhs, rhs 1063// gt slt dst, rhs, lhs (operand swap) 1064// le slt dst, rhs, lhs; xori dst, dst, 1 1065// ge slt dst, lhs, rhs; xori dst, dst, 1 1066// eq sub dst, lhs, rhs; seqz dst, dst 1067// ne sub dst, lhs, rhs; snez dst, dst 1068// 1069// All produce 0 (false) or 1 (true) in a GPR, matching the i64 1070// truthy convention opt.nx / parse.nx expect. 1071 1072// FP comparison emitter. RV64FD ISA: feq.d / flt.d / fle.d (and .s 1073// counterparts) take two F-regs and write a 0/1 result to an INTEGER 1074// register, so the dst-register handling matches the integer cmp path. 1075// NE / GT / GE are synthesised: NE = !EQ via xori, GT = flt swap, 1076// GE = fle swap. 1077 1078func rv_emit_fcmp(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr, is_d: i64) -> i64 { 1079 fmaterialise(f, locs, o, i.op0, "ft4" as *u8) 1080 fmaterialise(f, locs, o, i.op1, "ft5" as *u8) 1081 let dl: *ValueLoc = loc_at(locs, i.result) 1082 let op: i64 = i.op 1083 var suffix: *u8 = "s" as *u8 1084 if is_d == 1 { suffix = "d" as *u8 } 1085 1086 if op == OP_EQ { 1087 out_str(o, " feq.") 1088 out_str(o, suffix) 1089 out_str(o, " ") 1090 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 1091 out_str(o, ", ft4, ft5\n") 1092 } 1093 if op == OP_NE { 1094 out_str(o, " feq.") 1095 out_str(o, suffix) 1096 out_str(o, " t6, ft4, ft5\n") 1097 out_str(o, " xori ") 1098 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 1099 out_str(o, ", t6, 1\n") 1100 } 1101 if op == OP_LT_S { 1102 out_str(o, " flt.") 1103 out_str(o, suffix) 1104 out_str(o, " ") 1105 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 1106 out_str(o, ", ft4, ft5\n") 1107 } 1108 if op == OP_LE_S { 1109 out_str(o, " fle.") 1110 out_str(o, suffix) 1111 out_str(o, " ") 1112 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 1113 out_str(o, ", ft4, ft5\n") 1114 } 1115 if op == OP_GT_S { 1116 // Swap operands: flt.d dst, ft5, ft4 1117 out_str(o, " flt.") 1118 out_str(o, suffix) 1119 out_str(o, " ") 1120 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 1121 out_str(o, ", ft5, ft4\n") 1122 } 1123 if op == OP_GE_S { 1124 // Swap operands: fle.d dst, ft5, ft4 1125 out_str(o, " fle.") 1126 out_str(o, suffix) 1127 out_str(o, " ") 1128 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 1129 out_str(o, ", ft5, ft4\n") 1130 } 1131 1132 if dl.kind == 1 { 1133 emit_sp_sd(o, "t6" as *u8, dl.idx) 1134 } 1135 return 0 1136} 1137 1138func rv_emit_cmp(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 1139 // Dispatch to FP cmp when operand 0's type is f32 / f64. Both 1140 // operands of a binary cmp share the same type (parser-enforced), 1141 // so checking op0 suffices. 1142 let v0: *Value = val_at(f, i.op0) 1143 if v0.ty != (0 as *Type) { 1144 let k: i64 = v0.ty.kind 1145 if k == TY_F32 { rv_emit_fcmp(f, locs, o, i, 0); return 0 } 1146 if k == TY_F64 { rv_emit_fcmp(f, locs, o, i, 1); return 0 } 1147 } 1148 materialise_value(f, locs, o, i.op0, "t4") // cmp operands are VALUE uses (load alloca'd pointers/scalars) 1149 materialise_value(f, locs, o, i.op1, "t5") 1150 1151 let dl: *ValueLoc = loc_at(locs, i.result) 1152 let op: i64 = i.op 1153 1154 // Helper: emit " <mnemonic> <dst>, <a>, <b>\n" 1155 // where dst is either the allocated reg name or "t6" scratch. 1156 if op == OP_LT_S { 1157 out_str(o, " slt ") 1158 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 1159 out_str(o, ", t4, t5\n") 1160 } 1161 if op == OP_GT_S { 1162 // Swap operands: slt dst, rhs, lhs 1163 out_str(o, " slt ") 1164 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 1165 out_str(o, ", t5, t4\n") 1166 } 1167 if op == OP_LE_S { 1168 out_str(o, " slt ") 1169 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 1170 out_str(o, ", t5, t4\n") 1171 out_str(o, " xori ") 1172 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 1173 out_str(o, ", ") 1174 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 1175 out_str(o, ", 1\n") 1176 } 1177 if op == OP_GE_S { 1178 out_str(o, " slt ") 1179 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 1180 out_str(o, ", t4, t5\n") 1181 out_str(o, " xori ") 1182 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 1183 out_str(o, ", ") 1184 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 1185 out_str(o, ", 1\n") 1186 } 1187 if op == OP_EQ { 1188 out_str(o, " sub ") 1189 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 1190 out_str(o, ", t4, t5\n") 1191 out_str(o, " seqz ") 1192 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 1193 out_str(o, ", ") 1194 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 1195 out_char(o, 0x0A) 1196 } 1197 if op == OP_NE { 1198 out_str(o, " sub ") 1199 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 1200 out_str(o, ", t4, t5\n") 1201 out_str(o, " snez ") 1202 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 1203 out_str(o, ", ") 1204 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 1205 out_char(o, 0x0A) 1206 } 1207 1208 // Spill back if result is on the stack. 1209 if dl.kind == 1 { 1210 emit_sp_sd(o, "t6" as *u8, dl.idx) 1211 } 1212 return 0 1213} 1214 1215// ---- call dispatch (port from riscv.c's emit_call) ---- 1216// 1217// Marshal up to 8 operands into a0..a7, emit `call <name>`, 1218// move a0 back to the result's location. 1219// 1220// Limitation: riscv.nx's Instr has op0..op7 inline slots covering 1221// up to 8 operands without heap fallback. Matches the 8-arg ABI 1222// cap on RV64. 1223 1224func target_reg_name(o: *OutBuf, k: i64) -> i64 { 1225 out_str(o, "a") 1226 out_i64(o, k) 1227 return 0 1228} 1229 1230// "fa0".."fa7" for FP arg/return regs. RV64GD ABI keeps a SEPARATE 1231// counter for FP args (independent of a0..a7), so a function with a 1232// mixed signature like f(int, f64, int) places those at a0, fa0, a1. 1233func target_freg_name(o: *OutBuf, k: i64) -> i64 { 1234 out_str(o, "fa") 1235 out_i64(o, k) 1236 return 0 1237} 1238 1239func rv_emit_call(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 1240 let n: i64 = i.n_operands 1241 var k: i64 = 0 1242 var int_k: i64 = 0 // RV64GD int-arg counter (a0..a7) 1243 var fp_k: i64 = 0 // RV64GD fp-arg counter (fa0..fa7) 1244 while k < n { 1245 var v: i64 = i.op0 1246 if k == 1 { v = i.op1 } 1247 if k == 2 { v = i.op2 } 1248 if k == 3 { v = i.op3 } 1249 if k == 4 { v = i.op4 } 1250 if k == 5 { v = i.op5 } 1251 if k == 6 { v = i.op6 } 1252 if k == 7 { v = i.op7 } 1253 let val: *Value = val_at(f, v) 1254 var is_fp: i64 = 0 1255 var is_d: i64 = 0 1256 if val.ty != (0 as *Type) { 1257 let kt: i64 = val.ty.kind 1258 if kt == TY_F32 { is_fp = 1 } 1259 if kt == TY_F64 { is_fp = 1; is_d = 1 } 1260 } 1261 if is_fp == 1 { 1262 if val.kind == 0 { 1263 // FP literal -- IEEE bits stored in const_int. 1264 out_str(o, " li t6, ") 1265 out_i64(o, val.const_int) 1266 out_char(o, 0x0A) 1267 if is_d == 1 { 1268 out_str(o, " fmv.d.x ") 1269 } else { 1270 out_str(o, " fmv.w.x ") 1271 } 1272 target_freg_name(o, fp_k) 1273 out_str(o, ", t6\n") 1274 } else { 1275 // Materialise into fa<fp_k>. fmaterialise dispatches on 1276 // the value's own type for fmv.d / fmv.s / fld / flw. 1277 let fscratch: *u8 = sys_mmap(8) 1278 fscratch[0] = 0x66 // 'f' 1279 fscratch[1] = 0x61 // 'a' 1280 fscratch[2] = 0x30 + fp_k // '0'..'7' 1281 fscratch[3] = 0 1282 fmaterialise(f, locs, o, v, fscratch) 1283 } 1284 fp_k = fp_k + 1 1285 } else { 1286 if val.kind == 0 { 1287 // CONST_INT -- `li a<int_k>, N`. 1288 out_str(o, " li ") 1289 target_reg_name(o, int_k) 1290 out_str(o, ", ") 1291 out_i64(o, val.const_int) 1292 out_char(o, 0x0A) 1293 } else { 1294 let scratch: *u8 = sys_mmap(4) 1295 scratch[0] = 0x61 // 'a' 1296 scratch[1] = 0x30 + int_k // '0'..'7' 1297 scratch[2] = 0 1298 materialise_value(f, locs, o, v, scratch) 1299 } 1300 int_k = int_k + 1 1301 } 1302 k = k + 1 1303 } 1304 out_str(o, " call ") 1305 if i.callee != (0 as *Function) { 1306 let callee_name: *u8 = i.callee.name_start as *u8 1307 out_str(o, callee_name) 1308 } else { 1309 out_str(o, "_unknown_callee") 1310 } 1311 out_char(o, 0x0A) 1312 1313 // Save return value to result's location. fa0 if return type is 1314 // FP (per RV64GD ABI), else a0. 1315 let dl: *ValueLoc = loc_at(locs, i.result) 1316 var ret_is_fp: i64 = 0 1317 var ret_is_d: i64 = 0 1318 if i.ty != (0 as *Type) { 1319 let kr: i64 = i.ty.kind 1320 if kr == TY_F32 { ret_is_fp = 1 } 1321 if kr == TY_F64 { ret_is_fp = 1; ret_is_d = 1 } 1322 } 1323 if ret_is_fp == 1 { 1324 if dl.kind == 0 { 1325 if ret_is_d == 1 { 1326 out_str(o, " fmv.d ") 1327 } else { 1328 out_str(o, " fmv.s ") 1329 } 1330 reg_name(o, dl.idx) 1331 out_str(o, ", fa0\n") 1332 } 1333 if dl.kind == 1 { 1334 if ret_is_d == 1 { 1335 emit_sp_fsd(o, "fa0" as *u8, dl.idx) 1336 } else { 1337 emit_sp_fsw(o, "fa0" as *u8, dl.idx) 1338 } 1339 } 1340 } else { 1341 if dl.kind == 0 { 1342 out_str(o, " mv ") 1343 reg_name(o, dl.idx) 1344 out_str(o, ", a0\n") 1345 } 1346 if dl.kind == 1 { 1347 emit_sp_sd(o, "a0" as *u8, dl.idx) 1348 } 1349 } 1350 return 0 1351} 1352 1353// OP_CALL_INDIRECT: `f(args...)` where f is a func-typed VALUE (fn-ptr), not a named 1354// function. op0 = the target; args are op1..op(n_operands-1) (n_args = n-1). Mirrors 1355// x86ctx_emit_call_indirect. The target is materialised into t0 FIRST -- arg loading 1356// overwrites a0..a7, so a fn-ptr living in an a-reg would be clobbered before use (x86 1357// pushes rax / pops r11 for the same reason). t0 is caller-saved scratch that arg 1358// materialise never touches (emit_sp_ld uses t6, emit_sp_sd t4/t5), so it survives to 1359// the `jalr ra, t0, 0`. Return-value handling is identical to rv_emit_call. 1360func rv_emit_call_indirect(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 1361 let n: i64 = i.n_operands 1362 // LOUD-FAIL GUARD (2026-07-25, seq715 family): the operand fan-out below only 1363 // resolves k=1..7. At k>=8 NONE of the `if k ==` arms match and `v` keeps its 1364 // initialiser i.op1 -- so arg 8 would be passed the value of arg 1, silently. 1365 // x86 grew real stack args in this same pass; RISC-V stack args are a separate 1366 // arc, so until then this fails the BUILD rather than miscompiling the call. 1367 if n > 8 { 1368 out_str(o, " .error \"nx riscv: indirect call with >7 args (a1..a7 cap; stack args not implemented)\"\n") 1369 return 0 1370 } 1371 materialise_value(f, locs, o, i.op0, "t0") // fn-ptr target VALUE -> t0 (load if alloca'd), before any a-reg is touched 1372 var k: i64 = 1 1373 var int_k: i64 = 0 1374 var fp_k: i64 = 0 1375 while k < n { 1376 var v: i64 = i.op1 1377 if k == 2 { v = i.op2 } 1378 if k == 3 { v = i.op3 } 1379 if k == 4 { v = i.op4 } 1380 if k == 5 { v = i.op5 } 1381 if k == 6 { v = i.op6 } 1382 if k == 7 { v = i.op7 } 1383 let val: *Value = val_at(f, v) 1384 var is_fp: i64 = 0 1385 var is_d: i64 = 0 1386 if val.ty != (0 as *Type) { 1387 let kt: i64 = val.ty.kind 1388 if kt == TY_F32 { is_fp = 1 } 1389 if kt == TY_F64 { is_fp = 1; is_d = 1 } 1390 } 1391 if is_fp == 1 { 1392 if val.kind == 0 { 1393 out_str(o, " li t6, ") 1394 out_i64(o, val.const_int) 1395 out_char(o, 0x0A) 1396 if is_d == 1 { 1397 out_str(o, " fmv.d.x ") 1398 } else { 1399 out_str(o, " fmv.w.x ") 1400 } 1401 target_freg_name(o, fp_k) 1402 out_str(o, ", t6\n") 1403 } else { 1404 let fscratch: *u8 = sys_mmap(8) 1405 fscratch[0] = 0x66 // 'f' 1406 fscratch[1] = 0x61 // 'a' 1407 fscratch[2] = 0x30 + fp_k // '0'..'7' 1408 fscratch[3] = 0 1409 fmaterialise(f, locs, o, v, fscratch) 1410 } 1411 fp_k = fp_k + 1 1412 } else { 1413 if val.kind == 0 { 1414 out_str(o, " li ") 1415 target_reg_name(o, int_k) 1416 out_str(o, ", ") 1417 out_i64(o, val.const_int) 1418 out_char(o, 0x0A) 1419 } else { 1420 let scratch: *u8 = sys_mmap(4) 1421 scratch[0] = 0x61 // 'a' 1422 scratch[1] = 0x30 + int_k // '0'..'7' 1423 scratch[2] = 0 1424 materialise_value(f, locs, o, v, scratch) 1425 } 1426 int_k = int_k + 1 1427 } 1428 k = k + 1 1429 } 1430 out_str(o, " jalr ra, t0, 0\n") 1431 1432 // Save return value (a0/fa0) into result's location -- same as rv_emit_call. 1433 let dl: *ValueLoc = loc_at(locs, i.result) 1434 var ret_is_fp: i64 = 0 1435 var ret_is_d: i64 = 0 1436 if i.ty != (0 as *Type) { 1437 let kr: i64 = i.ty.kind 1438 if kr == TY_F32 { ret_is_fp = 1 } 1439 if kr == TY_F64 { ret_is_fp = 1; ret_is_d = 1 } 1440 } 1441 if ret_is_fp == 1 { 1442 if dl.kind == 0 { 1443 if ret_is_d == 1 { 1444 out_str(o, " fmv.d ") 1445 } else { 1446 out_str(o, " fmv.s ") 1447 } 1448 reg_name(o, dl.idx) 1449 out_str(o, ", fa0\n") 1450 } 1451 if dl.kind == 1 { 1452 if ret_is_d == 1 { 1453 emit_sp_fsd(o, "fa0" as *u8, dl.idx) 1454 } else { 1455 emit_sp_fsw(o, "fa0" as *u8, dl.idx) 1456 } 1457 } 1458 } else { 1459 if dl.kind == 0 { 1460 out_str(o, " mv ") 1461 reg_name(o, dl.idx) 1462 out_str(o, ", a0\n") 1463 } 1464 if dl.kind == 1 { 1465 emit_sp_sd(o, "a0" as *u8, dl.idx) 1466 } 1467 } 1468 return 0 1469} 1470 1471// ---- load / store (port from riscv.c's emit_load / emit_store) ---- 1472// 1473// Width-specific load: lb/lh/lw/ld (signed); lbu/lhu/lwu (unsigned). 1474// Width-specific store: sb/sh/sw/sd. Size comes from instruction's 1475// result type (for loads) or operand type (for stores). Fallback is 1476// 8-byte (ld/sd) when type info is missing. 1477 1478func load_mnem_name(o: *OutBuf, sz: i64, is_signed: i64) -> i64 { 1479 if sz == 1 { 1480 if is_signed == 1 { out_str(o, "lb") } else { out_str(o, "lbu") } 1481 return 0 1482 } 1483 if sz == 2 { 1484 if is_signed == 1 { out_str(o, "lh") } else { out_str(o, "lhu") } 1485 return 0 1486 } 1487 if sz == 4 { 1488 if is_signed == 1 { out_str(o, "lw") } else { out_str(o, "lwu") } 1489 return 0 1490 } 1491 out_str(o, "ld") 1492 return 0 1493} 1494 1495func store_mnem_name(o: *OutBuf, sz: i64) -> i64 { 1496 if sz == 1 { out_str(o, "sb"); return 0 } 1497 if sz == 2 { out_str(o, "sh"); return 0 } 1498 if sz == 4 { out_str(o, "sw"); return 0 } 1499 out_str(o, "sd") 1500 return 0 1501} 1502 1503// Are the low bits of a type kind the signed integer class? 1504func type_kind_is_signed(k: i64) -> i64 { 1505 if k == TY_I8 { return 1 } 1506 if k == TY_I16 { return 1 } 1507 if k == TY_I32 { return 1 } 1508 if k == TY_I64 { return 1 } 1509 return 0 1510} 1511 1512// Home the value sitting in t6 into a result's location (register -> mv, spill slot -> sd off(sp)). The 1513// atomic lowerings (LN34) all deliver their result in t6, so one homing helper keeps their four call sites 1514// from each re-typing the rv_emit_load tail. 1515func rv_home_t6(locs: *ValueLoc, o: *OutBuf, id: i64) -> i64 { 1516 let dl: *ValueLoc = loc_at(locs, id) 1517 if dl.kind == 0 { 1518 out_str(o, " mv ") 1519 reg_name(o, dl.idx) 1520 out_str(o, ", t6\n") 1521 } 1522 if dl.kind == 1 { 1523 emit_sp_sd(o, "t6" as *u8, dl.idx) 1524 } 1525 return 0 1526} 1527 1528func rv_emit_load(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 1529 materialise(f, locs, o, i.op0, "t4") 1530 let dl: *ValueLoc = loc_at(locs, i.result) 1531 var sz: i64 = 8 1532 var is_signed: i64 = 0 1533 if i.ty != (0 as *Type) { 1534 if i.ty.size > 0 { sz = i.ty.size } 1535 // 2026-07-10 debt fix: signedness now comes from the sext bit (set only for i8/i16/i32 1536 // annotations), NOT from the kind. Kind-based was WRONG for *u8 (u8 also mints TY_I8 -> lb 1537 // sign-extended bytes >= 0x80, the x509 0xA0-must-stay-160 class). Aligns RV64 with x86. 1538 is_signed = i.ty.sext 1539 } 1540 out_str(o, " ") 1541 load_mnem_name(o, sz, is_signed) 1542 out_str(o, " ") 1543 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 1544 out_str(o, ", 0(t4)\n") 1545 if dl.kind == 1 { 1546 emit_sp_sd(o, "t6" as *u8, dl.idx) 1547 } 1548 return 0 1549} 1550 1551// OP_ADDR_OF (&x): materialise op0's ADDRESS into t4 (op0 is the alloca -> materialise emits `addi t4, sp, off` via the 1552// F14 VL_ALLOCA path = the address of x), then store t4 into the result's home. Mirrors x86_64_ctx OP_ADDR_OF (load 1553// op0's addr into rax, store to result). Neither RISC-V backend had this -> `let p = &x` never set p -> wild deref 1554// (caught by the nxc-vs-QEMU oracle: ptr-to-local gave a wild address). Fixes address-of-local codegen. 1555func rv_emit_addr_of(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 1556 materialise(f, locs, o, i.op0, "t4") // op0 is the alloca; VL_ALLOCA -> `addi t4, sp, off` = &op0 1557 let dl: *ValueLoc = loc_at(locs, i.result) 1558 if dl.kind == 0 { 1559 out_str(o, " mv ") 1560 reg_name(o, dl.idx) 1561 out_str(o, ", t4\n") 1562 } 1563 if dl.kind == 1 { 1564 emit_sp_sd(o, "t4" as *u8, dl.idx) 1565 } 1566 return 0 1567} 1568 1569func rv_emit_store(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 1570 materialise(f, locs, o, i.op0, "t4") // address 1571 materialise(f, locs, o, i.op1, "t5") // value 1572 var sz: i64 = 8 1573 if i.ty != (0 as *Type) { 1574 if i.ty.kind != TY_VOID { 1575 if i.ty.size > 0 { sz = i.ty.size } 1576 } 1577 } 1578 out_str(o, " ") 1579 store_mnem_name(o, sz) 1580 out_str(o, " t5, 0(t4)\n") 1581 return 0 1582} 1583 1584// ---- GEP address arithmetic (port from riscv.c's emit_gep) ---- 1585// 1586// GEP = base-pointer + offset. When offset is a constant in the 1587// [-2048, 2047] range it fits in an `addi` immediate; otherwise 1588// fall back to materialising offset into a reg and using `add`. 1589 1590func rv_emit_gep(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 1591 materialise(f, locs, o, i.op0, "t4") 1592 let off_v: *Value = val_at(f, i.op1) 1593 let dl: *ValueLoc = loc_at(locs, i.result) 1594 1595 if off_v.kind == 0 { 1596 // CONST_INT. Check immediate range for addi. 1597 let c: i64 = off_v.const_int 1598 if c >= -2048 { 1599 if c <= 2047 { 1600 out_str(o, " addi ") 1601 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 1602 out_str(o, ", t4, ") 1603 out_i64(o, c) 1604 out_char(o, 0x0A) 1605 if dl.kind == 1 { 1606 emit_sp_sd(o, "t6" as *u8, dl.idx) 1607 } 1608 return 0 1609 } 1610 } 1611 } 1612 1613 // Non-immediate offset: materialise into t5, emit `add`. 1614 materialise(f, locs, o, i.op1, "t5") 1615 out_str(o, " add ") 1616 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 1617 out_str(o, ", t4, t5\n") 1618 if dl.kind == 1 { 1619 emit_sp_sd(o, "t6" as *u8, dl.idx) 1620 } 1621 return 0 1622} 1623 1624// ---- tail call (port from riscv.c's emit_tail_call) ---- 1625// 1626// Like emit_call but unwinds our frame first, then jumps to the 1627// callee via `tail` (no ra push). Callee's `ret` returns 1628// directly to our caller: O(1) stack regardless of recursion 1629// depth. Used when the parser detects `return foo(...)` in tail 1630// position. 1631 1632func rv_emit_tail_call(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr, 1633 frame_size: i64, ra_slot: i64) -> i64 { 1634 let n: i64 = i.n_operands 1635 var k: i64 = 0 1636 while k < n { 1637 var v: i64 = i.op0 1638 if k == 1 { v = i.op1 } 1639 if k == 2 { v = i.op2 } 1640 if k == 3 { v = i.op3 } 1641 if k == 4 { v = i.op4 } 1642 if k == 5 { v = i.op5 } 1643 if k == 6 { v = i.op6 } 1644 if k == 7 { v = i.op7 } 1645 let val: *Value = val_at(f, v) 1646 if val.kind == 0 { 1647 out_str(o, " li ") 1648 target_reg_name(o, k) 1649 out_str(o, ", ") 1650 out_i64(o, val.const_int) 1651 out_char(o, 0x0A) 1652 } else { 1653 let scratch: *u8 = sys_mmap(4) 1654 scratch[0] = 0x61 1655 scratch[1] = 0x30 + k 1656 scratch[2] = 0 1657 materialise(f, locs, o, v, scratch) 1658 } 1659 k = k + 1 1660 } 1661 // Tear down frame: reload ra, pop frame, then `tail`. 1662 emit_sp_ld(o, "ra" as *u8, ra_slot) 1663 out_str(o, " addi sp, sp, ") 1664 out_i64(o, frame_size) 1665 out_char(o, 0x0A) 1666 out_str(o, " tail ") 1667 if i.callee != (0 as *Function) { 1668 let callee_name: *u8 = i.callee.name_start as *u8 1669 out_str(o, callee_name) 1670 } else { 1671 out_str(o, "_unknown_callee") 1672 } 1673 out_char(o, 0x0A) 1674 return 0 1675} 1676 1677// ---- F-extension casts (int <-> float) ---------------------------- 1678// 1679// OP_FCAST_I_TO_F (55): convert i64 in op0 to FP value of result type. 1680// Emit `fcvt.d.l fdst, src_int` for f64 result, `fcvt.s.l` for f32. 1681// OP_FCAST_F_TO_I (56): convert FP in op0 (f32 or f64) to i64 result. 1682// Emit `fcvt.l.d dst, fsrc, rtz` for f64 source (round-to-zero = 1683// C-style truncation), `fcvt.l.s` for f32 source. 1684// 1685// rtz matches the IEEE 754 truncation semantics most languages use 1686// for explicit float-to-int casts. Round-to-nearest-even is the 1687// default if no mode is given but produces surprises on .5 inputs. 1688 1689func rv_emit_fcast(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 1690 let op: i64 = i.op 1691 if op == OP_FCAST_I_TO_F { 1692 materialise(f, locs, o, i.op0, "t4" as *u8) 1693 let dl: *ValueLoc = loc_at(locs, i.result) 1694 var is_d: i64 = 0 1695 if i.ty != (0 as *Type) { 1696 if i.ty.kind == TY_F64 { is_d = 1 } 1697 } 1698 if is_d == 1 { 1699 out_str(o, " fcvt.d.l ") 1700 } else { 1701 out_str(o, " fcvt.s.l ") 1702 } 1703 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "ft6") } 1704 out_str(o, ", t4\n") 1705 if dl.kind == 1 { 1706 if is_d == 1 { 1707 emit_sp_fsd(o, "ft6" as *u8, dl.idx) 1708 } else { 1709 emit_sp_fsw(o, "ft6" as *u8, dl.idx) 1710 } 1711 } 1712 return 0 1713 } 1714 if op == OP_FCAST_F_TO_I { 1715 let v0: *Value = val_at(f, i.op0) 1716 var is_d: i64 = 0 1717 if v0.ty != (0 as *Type) { 1718 if v0.ty.kind == TY_F64 { is_d = 1 } 1719 } 1720 fmaterialise(f, locs, o, i.op0, "ft4" as *u8) 1721 let dl: *ValueLoc = loc_at(locs, i.result) 1722 if is_d == 1 { 1723 out_str(o, " fcvt.l.d ") 1724 } else { 1725 out_str(o, " fcvt.l.s ") 1726 } 1727 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 1728 out_str(o, ", ft4, rtz\n") 1729 if dl.kind == 1 { 1730 emit_sp_sd(o, "t6" as *u8, dl.idx) 1731 } 1732 return 0 1733 } 1734 return 0 1735} 1736 1737// ---- alloca prep + emit (port from riscv.c's emit_alloca) ---- 1738// 1739// Each OP_ALLOCA reserves 8 bytes (rounded up) in the function's 1740// stack frame and returns the address as addi dst, sp, <offset>. 1741// Since riscv.nx doesn't track alloca offsets in a Ctx struct, 1742// we compute them in a preliminary pass over the function before 1743// emitting any instruction. 1744// 1745// Layout: allocas live ABOVE the spill area (which occupies 0.. 1746// spill_bytes) and BELOW the saved ra slot. Simple assignment: 1747// allocated in encounter order, 8-byte aligned. 1748 1749func compute_alloca_offsets(f: *Function, spill_bytes: i64, 1750 alloca_off: *i64) -> i64 { 1751 // Initialise all to -1. 1752 var i: i64 = 0 1753 while i < f.n_values { 1754 alloca_off[i] = -1 1755 i = i + 1 1756 } 1757 // Walk blocks; assign offsets to each OP_ALLOCA's result. 1758 var cur_off: i64 = spill_bytes 1759 var bi: i64 = 0 1760 while bi < f.n_blocks { 1761 let bb: *BasicBlock = block_at(f, bi) 1762 var inst: *Instr = bb.head 1763 while inst != (0 as *Instr) { 1764 if inst.op == OP_ALLOCA { 1765 if inst.result < f.n_values { 1766 alloca_off[inst.result] = cur_off 1767 // Size: use type.size if available, else 8. 1768 var sz: i64 = 8 1769 if inst.ty != (0 as *Type) { 1770 if inst.ty.size > 0 { 1771 sz = inst.ty.size 1772 } 1773 } 1774 // Round up to 8. 1775 sz = (sz + 7) & (0 - 8) 1776 cur_off = cur_off + sz 1777 } 1778 } 1779 inst = inst.next 1780 } 1781 bi = bi + 1 1782 } 1783 return cur_off // total alloca area bytes 1784} 1785 1786func rv_emit_alloca(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr, 1787 alloca_off: *i64) -> i64 { 1788 let off: i64 = alloca_off[i.result] 1789 if off < 0 { 1790 out_str(o, " # alloca with no slot?!\n") 1791 return 0 1792 } 1793 let dl: *ValueLoc = loc_at(locs, i.result) 1794 // F14 fix: VL_ALLOCA values are rematerialised by materialise() 1795 // at each use; the alloca slot itself is reserved in the prologue 1796 // by compute_alloca_offsets. Nothing to emit here in that case. 1797 if dl.kind == 3 { return 0 } 1798 out_str(o, " addi ") 1799 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 1800 out_str(o, ", sp, ") 1801 out_i64(o, off) 1802 out_char(o, 0x0A) 1803 if dl.kind == 1 { 1804 emit_sp_sd(o, "t6" as *u8, dl.idx) 1805 } 1806 return 0 1807} 1808 1809// ---- per-instruction dispatch ---- 1810 1811// ---- emit widening SIMD dot product i16x16 -> i64 ---- 1812// 1813// Lowers OP_SIMD_VDOT_I16_X16 to the RV-V chain: 1814// vsetvli e16 m1 avl=16 1815// vle16.v v1, (a_ptr) 1816// vle16.v v2, (b_ptr) 1817// vwmul.vv v4, v1, v2 ; widens i16*i16 -> i32, group m2 1818// vsetvli e64 m1 avl=1 ; switch to e64 to init accumulator 1819// vmv.v.i v6, 0 1820// vsetvli e32 m2 avl=16 ; back to e32 m2 for the reduce 1821// vwredsum.vs v6, v4, v6 ; widens i32 -> i64 scalar in v6[0] 1822// vsetvli e64 m1 avl=1 1823// vmv.x.s <dst>, v6 1824// 1825// Same shape as the C-side x86_64.c emit_simd_vdot_i16_x16 (which 1826// uses vpmaddwd) and riscv.c OP_SIMD_VDOT_I16_X16 (same RVV chain 1827// being ported here from C to NishiLang). 1828func rv_emit_simd_vdot_i16(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 1829 // Materialise the two pointer operands into t4 / t5. 1830 materialise(f, locs, o, i.op0, "t4") 1831 materialise(f, locs, o, i.op1, "t5") 1832 // Set vector length scalar (t6 = 16) -- this is a regular scalar 1833 // ADDI, which the existing nxasm `li` mnemonic handles. 1834 out_str(o, " li t6, 16\n") 1835 // The RVV instructions are emitted as raw 32-bit words via the 1836 // .word directive. Encodings pre-computed against GNU as 1837 // (riscv64-linux-gnu-as -march=rv64imav) -- saves bringing up a 1838 // full RVV mnemonic parser in nxasm. Each word is the exact 1839 // bytes GNU as emits for the corresponding mnemonic. See 1840 // bench/_offc/rvv_enc_probe.s for the source mapping; should 1841 // any encoding change, regen by rerunning that probe. 1842 // 1843 // vsetvli t6, t6, e16, m1, ta, ma -> 0x0c8fffd7 1844 out_str(o, " .word 0x0c8fffd7\n") 1845 // vle16.v v1, (t4) -> 0x020ed087 1846 out_str(o, " .word 0x020ed087\n") 1847 // vle16.v v2, (t5) -> 0x020f5107 1848 out_str(o, " .word 0x020f5107\n") 1849 // vwmul.vv v4, v1, v2 -> 0xee112257 1850 out_str(o, " .word 0xee112257\n") 1851 // Init e64 m1 accumulator vector v6 = 0. AVL=1 since we only 1852 // need lane 0 of v6 zeroed for the reduce; widening reduce reads 1853 // the full source lanes but only writes scalar lane 0 of v6. 1854 out_str(o, " li t6, 1\n") 1855 // vsetvli t6, t6, e64, m1, ta, ma -> 0x0d8fffd7 1856 out_str(o, " .word 0x0d8fffd7\n") 1857 // vmv.v.i v6, 0 -> 0x5e003357 1858 out_str(o, " .word 0x5e003357\n") 1859 // Switch to e32 m2 to read the widened products + reduce. Reset 1860 // AVL=16 -- the vsetvli before this one zeroed it via t6=1. 1861 out_str(o, " li t6, 16\n") 1862 // vsetvli t6, t6, e32, m2, ta, ma -> 0x0d1fffd7 1863 out_str(o, " .word 0x0d1fffd7\n") 1864 // vwredsum.vs v6, v4, v6 -> 0xc6430357 1865 out_str(o, " .word 0xc6430357\n") 1866 // Restore e64 m1 + AVL=1 for the scalar extract. 1867 out_str(o, " li t6, 1\n") 1868 // vsetvli t6, t6, e64, m1, ta, ma -> 0x0d8fffd7 1869 out_str(o, " .word 0x0d8fffd7\n") 1870 // Extract v6[0] as i64 scalar into t6 (x31), then either mv to 1871 // the destination register or spill it. Always landing in t6 1872 // avoids needing to map regalloc pool indices (0=t0, 3=t3, 1873 // 6=s2, ...) to physical x-reg numbers for the encoding -- a 1874 // separate concern that lives in reg_name's name table. 1875 // 1876 // vmv.x.s t6, v6 -> 0x42602fd7 (rd=x31, vs2=v6) 1877 out_str(o, " .word 0x42602fd7\n") 1878 let dl: *ValueLoc = loc_at(locs, i.result) 1879 if dl.kind == 0 { 1880 out_str(o, " mv ") 1881 reg_name(o, dl.idx) 1882 out_str(o, ", t6\n") 1883 } 1884 if dl.kind == 1 { 1885 emit_sp_sd(o, "t6" as *u8, dl.idx) 1886 } 1887 return 0 1888} 1889 1890// ---- emit horizontal min/max reduce for i16x16 ---- 1891// Source: *i64 pointer to 4 packed-i16 i64 words (16 lanes). 1892// Lowers to: 1893// li t6, 16 1894// vsetvli t6, t6, e16, m1, ta, ma .word 0x0c8fffd7 1895// vle16.v v1, (t4) .word 0x020ed087 1896// vmv.x.s t6, v1 .word 0x42102fd7 (seed) 1897// vmv.v.x v0, t6 .word 0x5e0fc057 (broadcast seed) 1898// vred{min,max}.vs v0, v1, v0 1899// vmv.x.s t6, v0 .word 0x42002fd7 1900// slli t6, t6, 48 (sign-extend i16 -> i64) 1901// srai t6, t6, 48 1902// mv dst, t6 (or spill) 1903func rv_emit_simd_vreduce_minmax_i16(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr, is_max: i64) -> i64 { 1904 materialise(f, locs, o, i.op0, "t4") 1905 out_str(o, " li t6, 16\n") 1906 out_str(o, " .word 0x0c8fffd7\n") // vsetvli e16 m1 1907 out_str(o, " .word 0x020ed087\n") // vle16.v v1, (t4) 1908 out_str(o, " .word 0x42102fd7\n") // vmv.x.s t6, v1 (seed from lane 0) 1909 out_str(o, " .word 0x5e0fc057\n") // vmv.v.x v0, t6 (broadcast seed) 1910 if is_max == 0 { 1911 out_str(o, " .word 0x16102057\n") // vredmin.vs v0, v1, v0 1912 } 1913 if is_max == 1 { 1914 out_str(o, " .word 0x1e102057\n") // vredmax.vs v0, v1, v0 1915 } 1916 out_str(o, " .word 0x42002fd7\n") // vmv.x.s t6, v0 1917 // Sign-extend i16 -> i64 via shift pair. 1918 out_str(o, " slli t6, t6, 48\n") 1919 out_str(o, " srai t6, t6, 48\n") 1920 let dl: *ValueLoc = loc_at(locs, i.result) 1921 if dl.kind == 0 { 1922 out_str(o, " mv ") 1923 reg_name(o, dl.idx) 1924 out_str(o, ", t6\n") 1925 } 1926 if dl.kind == 1 { 1927 emit_sp_sd(o, "t6" as *u8, dl.idx) 1928 } 1929 return 0 1930} 1931 1932// ---- emit i16x16 generic per-lane vbinop ---- 1933// Args: op0 = *i64 a, op1 = *i64 b, op2 = *i64 out. 1934// enc_word = the pre-computed RVV encoding for `<op>.vv v3, v1, v2` 1935// (vsadd=0x861101d7, vssub=0x8e1101d7, vsaddu=0x821101d7, 1936// vssubu=0x8a1101d7, vmin=0x161101d7, vmax=0x1e1101d7, 1937// vadd=0x021101d7, vsub=0x0a1101d7, vmul=0x961121d7). 1938// Loads both vectors into v1/v2, applies enc_word, stores to *out. 1939func rv_emit_simd_vbinop_i16(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr, enc_word: i64) -> i64 { 1940 materialise(f, locs, o, i.op0, "t4") 1941 materialise(f, locs, o, i.op1, "t5") 1942 out_str(o, " li t6, 16\n") 1943 out_str(o, " .word 0x0c8fffd7\n") // vsetvli e16 m1 1944 out_str(o, " .word 0x020ed087\n") // vle16.v v1, (t4) 1945 out_str(o, " .word 0x020f5107\n") // vle16.v v2, (t5) 1946 out_str(o, " .word ") 1947 out_i64(o, enc_word) // <op>.vv v3, v1, v2 1948 out_char(o, 0x0A) 1949 materialise(f, locs, o, i.op2, "t4") // out pointer 1950 out_str(o, " .word 0x020ed1a7\n") // vse16.v v3, (t4) 1951 let dl: *ValueLoc = loc_at(locs, i.result) 1952 if dl.kind == 0 { 1953 out_str(o, " li ") 1954 reg_name(o, dl.idx) 1955 out_str(o, ", 0\n") 1956 } 1957 if dl.kind == 1 { 1958 out_str(o, " li t6, 0\n") 1959 emit_sp_sd(o, "t6" as *u8, dl.idx) 1960 } 1961 return 0 1962} 1963// vsadd retained as a named wrapper for the existing dispatch. 1964func rv_emit_simd_vsadd_i16(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 1965 return rv_emit_simd_vbinop_i16(f, locs, o, i, 0x861101d7) 1966} 1967 1968// ---- emit i16x16 per-lane shift (vsll/vsrl/vsra .vx form) ---- 1969// Args: op0 = *i64 src, op1 = i64 count (scalar), op2 = *i64 out. 1970// enc_word is the RVV encoding for `<op>.vx v3, v1, t4` where the 1971// scalar count register is fixed at t4 (= x29, encoded in rs1 of 1972// the shift instruction). 1973// 1974// Register sequencing: 1975// 1. t4 = src ptr (vle16.v v1, (t4) requires src in t4=rs1) 1976// 2. load v1 from (t4) 1977// 3. t4 = count (shift's rs1 is t4 -- overwrite, src ptr no longer needed) 1978// 4. <shift>.vx v3, v1, t4 1979// 5. t4 = out ptr (vse16.v v3, (t4)) 1980// 6. store v3 1981func rv_emit_simd_vshift_i16(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr, enc_word: i64) -> i64 { 1982 materialise(f, locs, o, i.op0, "t4") 1983 out_str(o, " li t6, 16\n") 1984 out_str(o, " .word 0x0c8fffd7\n") // vsetvli e16 m1 1985 out_str(o, " .word 0x020ed087\n") // vle16.v v1, (t4) 1986 materialise(f, locs, o, i.op1, "t4") // count into t4 (rs1) 1987 out_str(o, " .word ") 1988 out_i64(o, enc_word) // <op>.vx v3, v1, t4 1989 out_char(o, 0x0A) 1990 materialise(f, locs, o, i.op2, "t4") // out ptr 1991 out_str(o, " .word 0x020ed1a7\n") // vse16.v v3, (t4) 1992 let dl: *ValueLoc = loc_at(locs, i.result) 1993 if dl.kind == 0 { 1994 out_str(o, " li ") 1995 reg_name(o, dl.idx) 1996 out_str(o, ", 0\n") 1997 } 1998 if dl.kind == 1 { 1999 out_str(o, " li t6, 0\n") 2000 emit_sp_sd(o, "t6" as *u8, dl.idx) 2001 } 2002 return 0 2003} 2004 2005// ---- emit horizontal i16 sum -> i64 scalar ---- 2006// Lowers via vwredsum.vs (widening: e16 source -> e32 accumulator), 2007// then sign-extend the 32-bit scalar to 64-bit. 2008// 1. li t6, 16; vsetvli e16 m1 2009// 2. t4 = src; vle16.v v1, (t4) 2010// 3. li t6, 1; vsetvli e32 m1; vmv.v.i v6, 0 (init accumulator) 2011// 4. li t6, 16; vsetvli e16 m1 (back to source SEW for the reduce) 2012// 5. vwredsum.vs v6, v1, v6 -- widening sum into v6[0] as i32 2013// 6. li t6, 1; vsetvli e32 m1; vmv.x.s t6, v6 (extract, sign-ext to i64) 2014func rv_emit_simd_vreduce_sum_i16(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 2015 materialise(f, locs, o, i.op0, "t4") 2016 out_str(o, " li t6, 16\n") 2017 out_str(o, " .word 0x0c8fffd7\n") // vsetvli e16 m1 2018 out_str(o, " .word 0x020ed087\n") // vle16.v v1, (t4) 2019 // Init e32 m1 accumulator v6 = 0. 2020 out_str(o, " li t6, 1\n") 2021 out_str(o, " .word 0x0d0fffd7\n") // vsetvli e32 m1 2022 out_str(o, " .word 0x5e003357\n") // vmv.v.i v6, 0 2023 // Back to e16 m1 for the source spec; vwredsum widens internally. 2024 out_str(o, " li t6, 16\n") 2025 out_str(o, " .word 0x0c8fffd7\n") // vsetvli e16 m1 2026 out_str(o, " .word 0xc6130357\n") // vwredsum.vs v6, v1, v6 2027 // Extract i32 scalar from v6[0], vmv.x.s sign-extends to XLEN. 2028 out_str(o, " li t6, 1\n") 2029 out_str(o, " .word 0x0d0fffd7\n") // vsetvli e32 m1 2030 out_str(o, " .word 0x42602fd7\n") // vmv.x.s t6, v6 2031 let dl: *ValueLoc = loc_at(locs, i.result) 2032 if dl.kind == 0 { 2033 out_str(o, " mv ") 2034 reg_name(o, dl.idx) 2035 out_str(o, ", t6\n") 2036 } 2037 if dl.kind == 1 { 2038 emit_sp_sd(o, "t6" as *u8, dl.idx) 2039 } 2040 return 0 2041} 2042 2043// ---- emit i16x16 scalar broadcast ---- 2044// Args: op0 = i64 scalar value, op1 = *i64 out. 2045// Lowers to vmv.v.x v3, t4 with the scalar in t4. 2046func rv_emit_simd_vbroadcast_i16(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 2047 materialise(f, locs, o, i.op0, "t4") // scalar into t4 (= x29, rs1) 2048 out_str(o, " li t6, 16\n") 2049 out_str(o, " .word 0x0c8fffd7\n") // vsetvli e16 m1 2050 out_str(o, " .word 0x5e0ec1d7\n") // vmv.v.x v3, t4 2051 materialise(f, locs, o, i.op1, "t4") // out ptr 2052 out_str(o, " .word 0x020ed1a7\n") // vse16.v v3, (t4) 2053 let dl: *ValueLoc = loc_at(locs, i.result) 2054 if dl.kind == 0 { 2055 out_str(o, " li ") 2056 reg_name(o, dl.idx) 2057 out_str(o, ", 0\n") 2058 } 2059 if dl.kind == 1 { 2060 out_str(o, " li t6, 0\n") 2061 emit_sp_sd(o, "t6" as *u8, dl.idx) 2062 } 2063 return 0 2064} 2065 2066// ---- i8x32 generic per-lane vbinop (vadd/vsub/vsadd/vssub) ---- 2067// Same template as rv_emit_simd_vbinop_i16 but with e8 SEW. 2068// vsetvli e8 m1 avl=32 -> VL=32 (8-bit lanes in 256-bit vector). 2069// vle8.v / vse8.v for load/store; arith encodings are SEW-agnostic 2070// (same op-bytes used for i16x16 binops, just different vsetvli). 2071func rv_emit_simd_vbinop_i8(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr, enc_word: i64) -> i64 { 2072 materialise(f, locs, o, i.op0, "t4") 2073 materialise(f, locs, o, i.op1, "t5") 2074 out_str(o, " li t6, 32\n") 2075 out_str(o, " .word 0x0c0fffd7\n") // vsetvli e8 m1 2076 out_str(o, " .word 0x020e8087\n") // vle8.v v1, (t4) 2077 out_str(o, " .word 0x020f0107\n") // vle8.v v2, (t5) 2078 out_str(o, " .word ") 2079 out_i64(o, enc_word) // <op>.vv v3, v1, v2 2080 out_char(o, 0x0A) 2081 materialise(f, locs, o, i.op2, "t4") 2082 out_str(o, " .word 0x020e81a7\n") // vse8.v v3, (t4) 2083 let dl: *ValueLoc = loc_at(locs, i.result) 2084 if dl.kind == 0 { 2085 out_str(o, " li ") 2086 reg_name(o, dl.idx) 2087 out_str(o, ", 0\n") 2088 } 2089 if dl.kind == 1 { 2090 out_str(o, " li t6, 0\n") 2091 emit_sp_sd(o, "t6" as *u8, dl.idx) 2092 } 2093 return 0 2094} 2095 2096// ---- i8x32 horizontal sum -> i64 (widening reduce) ---- 2097// vwredsum.vs widens i8 -> i16 lanes during accumulation, ensures 2098// 32-lane sum can't overflow (max |sum| = 32 * 127 = 4064, fits 2099// in i16 range -32768..32767). Sign-extend i16 -> i64 on extract. 2100func rv_emit_simd_vreduce_sum_i8(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 2101 materialise(f, locs, o, i.op0, "t4") 2102 out_str(o, " li t6, 32\n") 2103 out_str(o, " .word 0x0c0fffd7\n") // vsetvli e8 m1 2104 out_str(o, " .word 0x020e8087\n") // vle8.v v1, (t4) 2105 // Init e16 m1 accumulator v6 = 0. 2106 out_str(o, " li t6, 1\n") 2107 out_str(o, " .word 0x0c8fffd7\n") // vsetvli e16 m1 2108 out_str(o, " .word 0x5e003357\n") // vmv.v.i v6, 0 2109 // Back to e8 m1 for widening reduce. 2110 out_str(o, " li t6, 32\n") 2111 out_str(o, " .word 0x0c0fffd7\n") // vsetvli e8 m1 2112 out_str(o, " .word 0xc6130357\n") // vwredsum.vs v6, v1, v6 2113 // Extract i16 scalar in e16 m1, sign-extend i16 -> i64. 2114 out_str(o, " li t6, 1\n") 2115 out_str(o, " .word 0x0c8fffd7\n") // vsetvli e16 m1 2116 out_str(o, " .word 0x42602fd7\n") // vmv.x.s t6, v6 2117 out_str(o, " slli t6, t6, 48\n") 2118 out_str(o, " srai t6, t6, 48\n") 2119 let dl: *ValueLoc = loc_at(locs, i.result) 2120 if dl.kind == 0 { 2121 out_str(o, " mv ") 2122 reg_name(o, dl.idx) 2123 out_str(o, ", t6\n") 2124 } 2125 if dl.kind == 1 { 2126 emit_sp_sd(o, "t6" as *u8, dl.idx) 2127 } 2128 return 0 2129} 2130 2131// ---- i8x32 scalar broadcast ---- 2132// vmv.v.x v3, t4 in e8 SEW writes low 8 bits of t4 into all 32 lanes. 2133func rv_emit_simd_vbroadcast_i8(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 2134 materialise(f, locs, o, i.op0, "t4") // scalar into t4 (rs1) 2135 out_str(o, " li t6, 32\n") 2136 out_str(o, " .word 0x0c0fffd7\n") // vsetvli e8 m1 2137 out_str(o, " .word 0x5e0ec1d7\n") // vmv.v.x v3, t4 2138 materialise(f, locs, o, i.op1, "t4") // out ptr 2139 out_str(o, " .word 0x020e81a7\n") // vse8.v v3, (t4) 2140 let dl: *ValueLoc = loc_at(locs, i.result) 2141 if dl.kind == 0 { 2142 out_str(o, " li ") 2143 reg_name(o, dl.idx) 2144 out_str(o, ", 0\n") 2145 } 2146 if dl.kind == 1 { 2147 out_str(o, " li t6, 0\n") 2148 emit_sp_sd(o, "t6" as *u8, dl.idx) 2149 } 2150 return 0 2151} 2152 2153// ---- i32x8 generic per-lane vbinop ---- 2154// vsetvli e32 m1 avl=8 -> VL=8 (32-bit lanes in 256-bit vector). 2155// vle32.v / vse32.v for load/store; arith encodings are SEW-agnostic. 2156func rv_emit_simd_vbinop_i32(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr, enc_word: i64) -> i64 { 2157 materialise(f, locs, o, i.op0, "t4") 2158 materialise(f, locs, o, i.op1, "t5") 2159 out_str(o, " li t6, 8\n") 2160 out_str(o, " .word 0x0d0fffd7\n") // vsetvli e32 m1 2161 out_str(o, " .word 0x020ee087\n") // vle32.v v1, (t4) 2162 out_str(o, " .word 0x020f6107\n") // vle32.v v2, (t5) 2163 out_str(o, " .word ") 2164 out_i64(o, enc_word) // <op>.vv v3, v1, v2 2165 out_char(o, 0x0A) 2166 materialise(f, locs, o, i.op2, "t4") 2167 out_str(o, " .word 0x020ee1a7\n") // vse32.v v3, (t4) 2168 let dl: *ValueLoc = loc_at(locs, i.result) 2169 if dl.kind == 0 { 2170 out_str(o, " li ") 2171 reg_name(o, dl.idx) 2172 out_str(o, ", 0\n") 2173 } 2174 if dl.kind == 1 { 2175 out_str(o, " li t6, 0\n") 2176 emit_sp_sd(o, "t6" as *u8, dl.idx) 2177 } 2178 return 0 2179} 2180 2181// ---- i32x8 horizontal sum -> i64 (widening reduce) ---- 2182// vwredsum.vs widens e32 -> e64 accumulator. No overflow for 2183// any 8-lane i32 sum (max |sum| = 8 * 2^31 ~ 2^34, fits i64). 2184func rv_emit_simd_vreduce_sum_i32(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 2185 materialise(f, locs, o, i.op0, "t4") 2186 out_str(o, " li t6, 8\n") 2187 out_str(o, " .word 0x0d0fffd7\n") // vsetvli e32 m1 2188 out_str(o, " .word 0x020ee087\n") // vle32.v v1, (t4) 2189 // Init e64 m1 accumulator v6 = 0. 2190 out_str(o, " li t6, 1\n") 2191 out_str(o, " .word 0x0d8fffd7\n") // vsetvli e64 m1 2192 out_str(o, " .word 0x5e003357\n") // vmv.v.i v6, 0 2193 // Back to e32 m1 for widening reduce. 2194 out_str(o, " li t6, 8\n") 2195 out_str(o, " .word 0x0d0fffd7\n") // vsetvli e32 m1 2196 out_str(o, " .word 0xc6130357\n") // vwredsum.vs v6, v1, v6 2197 // Extract i64 scalar in e64 m1 -- already correct width, no sign-ext needed. 2198 out_str(o, " li t6, 1\n") 2199 out_str(o, " .word 0x0d8fffd7\n") // vsetvli e64 m1 2200 out_str(o, " .word 0x42602fd7\n") // vmv.x.s t6, v6 2201 let dl: *ValueLoc = loc_at(locs, i.result) 2202 if dl.kind == 0 { 2203 out_str(o, " mv ") 2204 reg_name(o, dl.idx) 2205 out_str(o, ", t6\n") 2206 } 2207 if dl.kind == 1 { 2208 emit_sp_sd(o, "t6" as *u8, dl.idx) 2209 } 2210 return 0 2211} 2212 2213// ---- i32x8 scalar broadcast ---- 2214func rv_emit_simd_vbroadcast_i32(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 2215 materialise(f, locs, o, i.op0, "t4") 2216 out_str(o, " li t6, 8\n") 2217 out_str(o, " .word 0x0d0fffd7\n") // vsetvli e32 m1 2218 out_str(o, " .word 0x5e0ec1d7\n") // vmv.v.x v3, t4 2219 materialise(f, locs, o, i.op1, "t4") 2220 out_str(o, " .word 0x020ee1a7\n") // vse32.v v3, (t4) 2221 let dl: *ValueLoc = loc_at(locs, i.result) 2222 if dl.kind == 0 { 2223 out_str(o, " li ") 2224 reg_name(o, dl.idx) 2225 out_str(o, ", 0\n") 2226 } 2227 if dl.kind == 1 { 2228 out_str(o, " li t6, 0\n") 2229 emit_sp_sd(o, "t6" as *u8, dl.idx) 2230 } 2231 return 0 2232} 2233 2234// ---- i64x4 generic per-lane vbinop ---- 2235func rv_emit_simd_vbinop_i64(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr, enc_word: i64) -> i64 { 2236 materialise(f, locs, o, i.op0, "t4") 2237 materialise(f, locs, o, i.op1, "t5") 2238 out_str(o, " li t6, 4\n") 2239 out_str(o, " .word 0x0d8fffd7\n") // vsetvli e64 m1 2240 out_str(o, " .word 0x020ef087\n") // vle64.v v1, (t4) 2241 out_str(o, " .word 0x020f7107\n") // vle64.v v2, (t5) 2242 out_str(o, " .word ") 2243 out_i64(o, enc_word) 2244 out_char(o, 0x0A) 2245 materialise(f, locs, o, i.op2, "t4") 2246 out_str(o, " .word 0x020ef1a7\n") // vse64.v v3, (t4) 2247 let dl: *ValueLoc = loc_at(locs, i.result) 2248 if dl.kind == 0 { 2249 out_str(o, " li ") 2250 reg_name(o, dl.idx) 2251 out_str(o, ", 0\n") 2252 } 2253 if dl.kind == 1 { 2254 out_str(o, " li t6, 0\n") 2255 emit_sp_sd(o, "t6" as *u8, dl.idx) 2256 } 2257 return 0 2258} 2259 2260// ---- i64x4 horizontal sum -> i64 (non-widening) ---- 2261func rv_emit_simd_vreduce_sum_i64(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 2262 materialise(f, locs, o, i.op0, "t4") 2263 out_str(o, " li t6, 4\n") 2264 out_str(o, " .word 0x0d8fffd7\n") // vsetvli e64 m1 2265 out_str(o, " .word 0x020ef087\n") // vle64.v v1, (t4) 2266 out_str(o, " .word 0x5e003357\n") // vmv.v.i v6, 0 2267 out_str(o, " .word 0x02132357\n") // vredsum.vs v6, v1, v6 2268 out_str(o, " .word 0x42602fd7\n") // vmv.x.s t6, v6 2269 let dl: *ValueLoc = loc_at(locs, i.result) 2270 if dl.kind == 0 { 2271 out_str(o, " mv ") 2272 reg_name(o, dl.idx) 2273 out_str(o, ", t6\n") 2274 } 2275 if dl.kind == 1 { 2276 emit_sp_sd(o, "t6" as *u8, dl.idx) 2277 } 2278 return 0 2279} 2280 2281// ---- i64x4 scalar broadcast ---- 2282func rv_emit_simd_vbroadcast_i64(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 2283 materialise(f, locs, o, i.op0, "t4") 2284 out_str(o, " li t6, 4\n") 2285 out_str(o, " .word 0x0d8fffd7\n") // vsetvli e64 m1 2286 out_str(o, " .word 0x5e0ec1d7\n") // vmv.v.x v3, t4 2287 materialise(f, locs, o, i.op1, "t4") 2288 out_str(o, " .word 0x020ef1a7\n") // vse64.v v3, (t4) 2289 let dl: *ValueLoc = loc_at(locs, i.result) 2290 if dl.kind == 0 { 2291 out_str(o, " li ") 2292 reg_name(o, dl.idx) 2293 out_str(o, ", 0\n") 2294 } 2295 if dl.kind == 1 { 2296 out_str(o, " li t6, 0\n") 2297 emit_sp_sd(o, "t6" as *u8, dl.idx) 2298 } 2299 return 0 2300} 2301 2302func emit_instr(f: *Function, locs: *ValueLoc, o: *OutBuf, 2303 i: *Instr, frame_size: i64, ra_slot: i64, fn_name: *u8, 2304 alloca_off: *i64) -> i64 { 2305 let op: i64 = i.op 2306 // arithmetic / logic binops (1..15 minus 9 = NEG) 2307 if op >= 1 { if op <= 15 { if op != 9 { rv_emit_binop(f, locs, o, i); return 0 } } } 2308 // F-extension binops (OP_FADD=50 .. OP_FDIV=53). Register-homed 2309 // operands only in v0.0.1 -- f-reg spill support lands with the 2310 // f-reg allocator. 2311 if op >= OP_FADD { if op <= OP_FDIV { rv_emit_fbinop(f, locs, o, i); return 0 } } 2312 // F-extension casts (int<->float). OP_FCAST_I_TO_F=55, OP_FCAST_F_TO_I=56. 2313 if op == OP_FCAST_I_TO_F { rv_emit_fcast(f, locs, o, i); return 0 } 2314 if op == OP_FCAST_F_TO_I { rv_emit_fcast(f, locs, o, i); return 0 } 2315 // RVV vector binops (OP_VADD=80 .. OP_VFDIV=87). All operands 2316 // + result must be v-reg-homed; allocator pass is future work. 2317 if op >= OP_VADD { if op <= OP_VFDIV { rv_emit_vbinop(f, locs, o, i); return 0 } } 2318 // Width-specific SIMD: widening dot product i16x16 -> i64. 2319 // v0.0.1 shape: op0 = *i64 to packed 16 i16 lanes (4 words), 2320 // op1 = *i64 same. Lowers to vsetvli + vle16 + vwmul + vwredsum 2321 // chain, returns i64 scalar. 2322 if op == OP_SIMD_VDOT_I16_X16 { 2323 rv_emit_simd_vdot_i16(f, locs, o, i) 2324 return 0 2325 } 2326 if op == OP_SIMD_VREDUCE_MIN_I16_X16 { 2327 rv_emit_simd_vreduce_minmax_i16(f, locs, o, i, 0) 2328 return 0 2329 } 2330 if op == OP_SIMD_VREDUCE_MAX_I16_X16 { 2331 rv_emit_simd_vreduce_minmax_i16(f, locs, o, i, 1) 2332 return 0 2333 } 2334 if op == OP_SIMD_VSADD_I16_X16 { 2335 rv_emit_simd_vbinop_i16(f, locs, o, i, 0x861101d7) // vsadd.vv 2336 return 0 2337 } 2338 if op == OP_SIMD_VSSUB_I16_X16 { 2339 rv_emit_simd_vbinop_i16(f, locs, o, i, 0x8e1101d7) // vssub.vv 2340 return 0 2341 } 2342 if op == OP_SIMD_VSADDU_I16_X16 { 2343 rv_emit_simd_vbinop_i16(f, locs, o, i, 0x821101d7) // vsaddu.vv 2344 return 0 2345 } 2346 if op == OP_SIMD_VSSUBU_I16_X16 { 2347 rv_emit_simd_vbinop_i16(f, locs, o, i, 0x8a1101d7) // vssubu.vv 2348 return 0 2349 } 2350 if op == OP_SIMD_VMIN_LANE_I16_X16 { 2351 rv_emit_simd_vbinop_i16(f, locs, o, i, 0x161101d7) // vmin.vv 2352 return 0 2353 } 2354 if op == OP_SIMD_VMAX_LANE_I16_X16 { 2355 rv_emit_simd_vbinop_i16(f, locs, o, i, 0x1e1101d7) // vmax.vv 2356 return 0 2357 } 2358 if op == OP_SIMD_VADD_LANE_I16_X16 { 2359 rv_emit_simd_vbinop_i16(f, locs, o, i, 0x021101d7) // vadd.vv 2360 return 0 2361 } 2362 if op == OP_SIMD_VSUB_LANE_I16_X16 { 2363 rv_emit_simd_vbinop_i16(f, locs, o, i, 0x0a1101d7) // vsub.vv 2364 return 0 2365 } 2366 if op == OP_SIMD_VMUL_LANE_I16_X16 { 2367 rv_emit_simd_vbinop_i16(f, locs, o, i, 0x961121d7) // vmul.vv 2368 return 0 2369 } 2370 if op == OP_SIMD_VSLL_I16_X16 { 2371 rv_emit_simd_vshift_i16(f, locs, o, i, 0x961ec1d7) // vsll.vx 2372 return 0 2373 } 2374 if op == OP_SIMD_VSRL_I16_X16 { 2375 rv_emit_simd_vshift_i16(f, locs, o, i, 0xa21ec1d7) // vsrl.vx 2376 return 0 2377 } 2378 if op == OP_SIMD_VSRA_I16_X16 { 2379 rv_emit_simd_vshift_i16(f, locs, o, i, 0xa61ec1d7) // vsra.vx 2380 return 0 2381 } 2382 if op == OP_SIMD_VREDUCE_SUM_I16_X16 { 2383 rv_emit_simd_vreduce_sum_i16(f, locs, o, i) 2384 return 0 2385 } 2386 if op == OP_SIMD_VBROADCAST_I16_X16 { 2387 rv_emit_simd_vbroadcast_i16(f, locs, o, i) 2388 return 0 2389 } 2390 // i8x32 dispatch 2391 if op == OP_SIMD_VADD_I8_X32 { 2392 rv_emit_simd_vbinop_i8(f, locs, o, i, 0x021101d7) // vadd.vv 2393 return 0 2394 } 2395 if op == OP_SIMD_VSUB_I8_X32 { 2396 rv_emit_simd_vbinop_i8(f, locs, o, i, 0x0a1101d7) // vsub.vv 2397 return 0 2398 } 2399 if op == OP_SIMD_VSADD_I8_X32 { 2400 rv_emit_simd_vbinop_i8(f, locs, o, i, 0x861101d7) // vsadd.vv 2401 return 0 2402 } 2403 if op == OP_SIMD_VSSUB_I8_X32 { 2404 rv_emit_simd_vbinop_i8(f, locs, o, i, 0x8e1101d7) // vssub.vv 2405 return 0 2406 } 2407 if op == OP_SIMD_VREDUCE_SUM_I8_X32 { 2408 rv_emit_simd_vreduce_sum_i8(f, locs, o, i) 2409 return 0 2410 } 2411 if op == OP_SIMD_VBROADCAST_I8_X32 { 2412 rv_emit_simd_vbroadcast_i8(f, locs, o, i) 2413 return 0 2414 } 2415 // i32x8 dispatch 2416 if op == OP_SIMD_VADD_I32_X8 { 2417 rv_emit_simd_vbinop_i32(f, locs, o, i, 0x021101d7) // vadd.vv 2418 return 0 2419 } 2420 if op == OP_SIMD_VSUB_I32_X8 { 2421 rv_emit_simd_vbinop_i32(f, locs, o, i, 0x0a1101d7) // vsub.vv 2422 return 0 2423 } 2424 if op == OP_SIMD_VMUL_I32_X8 { 2425 rv_emit_simd_vbinop_i32(f, locs, o, i, 0x961121d7) // vmul.vv 2426 return 0 2427 } 2428 if op == OP_SIMD_VSADD_I32_X8 { 2429 rv_emit_simd_vbinop_i32(f, locs, o, i, 0x861101d7) // vsadd.vv 2430 return 0 2431 } 2432 if op == OP_SIMD_VSSUB_I32_X8 { 2433 rv_emit_simd_vbinop_i32(f, locs, o, i, 0x8e1101d7) // vssub.vv 2434 return 0 2435 } 2436 if op == OP_SIMD_VREDUCE_SUM_I32_X8 { 2437 rv_emit_simd_vreduce_sum_i32(f, locs, o, i) 2438 return 0 2439 } 2440 if op == OP_SIMD_VBROADCAST_I32_X8 { 2441 rv_emit_simd_vbroadcast_i32(f, locs, o, i) 2442 return 0 2443 } 2444 // i64x4 dispatch 2445 if op == OP_SIMD_VADD_I64_X4 { 2446 rv_emit_simd_vbinop_i64(f, locs, o, i, 0x021101d7); return 0 2447 } 2448 if op == OP_SIMD_VSUB_I64_X4 { 2449 rv_emit_simd_vbinop_i64(f, locs, o, i, 0x0a1101d7); return 0 2450 } 2451 if op == OP_SIMD_VMUL_I64_X4 { 2452 rv_emit_simd_vbinop_i64(f, locs, o, i, 0x961121d7); return 0 2453 } 2454 if op == OP_SIMD_VSADD_I64_X4 { 2455 rv_emit_simd_vbinop_i64(f, locs, o, i, 0x861101d7); return 0 2456 } 2457 if op == OP_SIMD_VSSUB_I64_X4 { 2458 rv_emit_simd_vbinop_i64(f, locs, o, i, 0x8e1101d7); return 0 2459 } 2460 if op == OP_SIMD_VREDUCE_SUM_I64_X4 { 2461 rv_emit_simd_vreduce_sum_i64(f, locs, o, i); return 0 2462 } 2463 if op == OP_SIMD_VBROADCAST_I64_X4 { 2464 rv_emit_simd_vbroadcast_i64(f, locs, o, i); return 0 2465 } 2466 if op == OP_ALLOCA { rv_emit_alloca(f, locs, o, i, alloca_off); return 0 } 2467 // comparisons: EQ=20, NE=21, LT_S=22, LE_S=23, GT_S=24, GE_S=25 2468 if op >= 20 { if op <= 25 { rv_emit_cmp(f, locs, o, i); return 0 } } 2469 if op == 30 { rv_emit_return(f, locs, o, frame_size, ra_slot, i, fn_name); return 0 } 2470 if op == OP_CALL { rv_emit_call(f, locs, o, i); return 0 } 2471 if op == OP_CALL_INDIRECT { rv_emit_call_indirect(f, locs, o, i); return 0 } 2472 if op == OP_LOAD { rv_emit_load(f, locs, o, i); return 0 } 2473 if op == OP_STORE { rv_emit_store(f, locs, o, i); return 0 } 2474 if op == OP_GEP { rv_emit_gep(f, locs, o, i); return 0 } 2475 if op == OP_ADDR_OF { rv_emit_addr_of(f, locs, o, i); return 0 } 2476 if op == OP_TAIL_CALL { rv_emit_tail_call(f, locs, o, i, frame_size, ra_slot); return 0 } 2477 if op == 31 { emit_branch(f, locs, o, i, fn_name); return 0 } 2478 if op == 32 { emit_branch(f, locs, o, i, fn_name); return 0 } 2479 if op == 40 { 2480 // COPY: just materialise into dst. 2481 materialise(f, locs, o, i.op0, "t4") 2482 let dl: *ValueLoc = loc_at(locs, i.result) 2483 if dl.kind == 0 { 2484 out_str(o, " mv ") 2485 reg_name(o, dl.idx) 2486 out_str(o, ", t4\n") 2487 } 2488 if dl.kind == 1 { 2489 emit_sp_sd(o, "t4" as *u8, dl.idx) 2490 } 2491 return 0 2492 } 2493 // OP_NOT (bitwise one's complement, `~x`). RV64 has no native NOT; 2494 // it is `xori rd, rs, -1` (the 12-bit -1 sign-extends to all-ones). 2495 // Two-is-one cross-arch parity: parse_unary emits OP_NOT for `~` and 2496 // the x86_64 backend lowers it to `notq` -- before this, OP_NOT fell 2497 // through emit_instr unhandled, silently dropping `~` on the RV64 2498 // target (the same silent-codegen class the x86 fix closed). 2499 if op == OP_NOT { 2500 materialise(f, locs, o, i.op0, "t4") 2501 let dl_not: *ValueLoc = loc_at(locs, i.result) 2502 if dl_not.kind == 0 { 2503 out_str(o, " xori ") 2504 reg_name(o, dl_not.idx) 2505 out_str(o, ", t4, -1\n") 2506 } 2507 if dl_not.kind == 1 { 2508 out_str(o, " xori t4, t4, -1\n") 2509 emit_sp_sd(o, "t4" as *u8, dl_not.idx) 2510 } 2511 return 0 2512 } 2513 // OP_RDTSC (`__rdtsc()`): read the cycle counter. Two-is-one parity 2514 // with x86's rdtsc -- RV64 has the `rdcycle rd` pseudo (csrr rd,cycle) 2515 // reading the full 64-bit cycle CSR. The dummy op0 is ignored. 2516 if op == OP_RDTSC { 2517 let dl_tsc: *ValueLoc = loc_at(locs, i.result) 2518 if dl_tsc.kind == 0 { 2519 out_str(o, " rdcycle ") 2520 reg_name(o, dl_tsc.idx) 2521 out_str(o, "\n") 2522 } 2523 if dl_tsc.kind == 1 { 2524 out_str(o, " rdcycle t4\n") 2525 emit_sp_sd(o, "t4" as *u8, dl_tsc.idx) 2526 } 2527 return 0 2528 } 2529 // Kernel intrinsics. Each lowers to a single RV64 instruction. 2530 if op == OP_WFI { 2531 out_str(o, " wfi\n") 2532 return 0 2533 } 2534 if op == OP_FENCE { 2535 out_str(o, " fence rw, rw\n") 2536 return 0 2537 } 2538 // LN34 (2026-09-03): the IR atomic family (__atomic_* builtins / nx_atom.nx) lowered to RV64A. Mirrors 2539 // x86ctx_emit_atomic (LOCK cmpxchg / xadd / mfence) with the ISA manual's sequentially-consistent mappings: 2540 // load = fence rw,rw; ld; fence r,rw · store = fence rw,w; sd · faa = amoadd.d.aqrl · cas = lr/sc.aqrl loop 2541 // · fence = fence rw,rw. The memory-order operand is honoured by being STRENGTHENED to seq_cst (the same 2542 // choice x86 makes), never weakened. Scratch: t4 address, t5 value/status, t6 loaded/result, and a7 for 2543 // the CAS desired value -- a7 is an ABI argument register the prologue has already homed and the call 2544 // marshaller only writes inside its own emit, so it is dead at every instruction boundary; regalloc hands 2545 // out t0-t3/s0-s11 only (reg_name). The lr/sc loop keeps to the constrained-sequence rules (no loads, 2546 // stores or backward jumps between lr and sc) so forward progress is guaranteed on conforming hardware. 2547 if op == OP_ATOMIC_LOAD_I64 { 2548 materialise(f, locs, o, i.op0, "t4") 2549 out_str(o, " fence rw, rw\n ld t6, 0(t4)\n fence r, rw\n") 2550 rv_home_t6(locs, o, i.result) 2551 return 0 2552 } 2553 if op == OP_ATOMIC_STORE_I64 { 2554 materialise(f, locs, o, i.op0, "t4") 2555 materialise(f, locs, o, i.op1, "t5") 2556 out_str(o, " fence rw, w\n sd t5, 0(t4)\n") 2557 return 0 2558 } 2559 if op == OP_ATOMIC_FAA_I64 { 2560 materialise(f, locs, o, i.op0, "t4") 2561 materialise(f, locs, o, i.op1, "t5") 2562 out_str(o, " amoadd.d.aqrl t6, t5, (t4)\n") 2563 rv_home_t6(locs, o, i.result) 2564 return 0 2565 } 2566 if op == OP_ATOMIC_CAS_I64 { 2567 materialise(f, locs, o, i.op0, "t4") 2568 materialise(f, locs, o, i.op1, "t5") 2569 materialise(f, locs, o, i.op2, "a7") 2570 out_str(o, ".L"); out_str(o, fn_name); out_str(o, "_cas"); out_i64(o, i.result); out_str(o, ":\n") 2571 out_str(o, " lr.d.aqrl t6, (t4)\n bne t6, t5, .L"); out_str(o, fn_name); out_str(o, "_casf"); out_i64(o, i.result); out_char(o, 0x0A) 2572 out_str(o, " sc.d.aqrl t6, a7, (t4)\n bne t6, zero, .L"); out_str(o, fn_name); out_str(o, "_cas"); out_i64(o, i.result); out_char(o, 0x0A) 2573 out_str(o, " li t6, 1\n j .L"); out_str(o, fn_name); out_str(o, "_casd"); out_i64(o, i.result); out_char(o, 0x0A) 2574 out_str(o, ".L"); out_str(o, fn_name); out_str(o, "_casf"); out_i64(o, i.result); out_str(o, ":\n li t6, 0\n") 2575 out_str(o, ".L"); out_str(o, fn_name); out_str(o, "_casd"); out_i64(o, i.result); out_str(o, ":\n") 2576 rv_home_t6(locs, o, i.result) 2577 return 0 2578 } 2579 if op == OP_ATOMIC_FENCE { 2580 out_str(o, " fence rw, rw\n") 2581 return 0 2582 } 2583 if op == OP_MRET { 2584 out_str(o, " mret\n") 2585 return 0 2586 } 2587 if op == OP_CSR_READ { 2588 // op0 = csr number. Result goes into the allocated dst reg 2589 // or spill slot. Emit `csrr <dst>, <csr>`. 2590 let dl_cr: *ValueLoc = loc_at(locs, i.result) 2591 if dl_cr.kind == 0 { 2592 out_str(o, " csrr ") 2593 reg_name(o, dl_cr.idx) 2594 out_str(o, ", ") 2595 out_i64(o, i.op0) 2596 out_char(o, 0x0A) 2597 } 2598 if dl_cr.kind == 1 { 2599 out_str(o, " csrr t4, ") 2600 out_i64(o, i.op0) 2601 out_char(o, 0x0A) 2602 emit_sp_sd(o, "t4" as *u8, dl_cr.idx) 2603 } 2604 return 0 2605 } 2606 if op == OP_CSR_WRITE { 2607 // op0 = csr number (literal), op1 = src value id. 2608 materialise(f, locs, o, i.op1, "t4") 2609 out_str(o, " csrw ") 2610 out_i64(o, i.op0) 2611 out_str(o, ", t4\n") 2612 return 0 2613 } 2614 if op == OP_SYSCALL { 2615 // ECALL: syscall number in a7, args in a0..a5, return in a0. 2616 // op0 = number, op1..op6 = args (up to 6). 2617 materialise(f, locs, o, i.op0, "a7") 2618 if i.n_operands > 1 { materialise(f, locs, o, i.op1, "a0") } 2619 if i.n_operands > 2 { materialise(f, locs, o, i.op2, "a1") } 2620 if i.n_operands > 3 { materialise(f, locs, o, i.op3, "a2") } 2621 if i.n_operands > 4 { materialise(f, locs, o, i.op4, "a3") } 2622 if i.n_operands > 5 { materialise(f, locs, o, i.op5, "a4") } 2623 if i.n_operands > 6 { materialise(f, locs, o, i.op6, "a5") } 2624 out_str(o, " ecall\n") 2625 // Result: a0 -> result location. 2626 let dl_sc: *ValueLoc = loc_at(locs, i.result) 2627 if dl_sc.kind == 0 { 2628 out_str(o, " mv ") 2629 reg_name(o, dl_sc.idx) 2630 out_str(o, ", a0\n") 2631 } 2632 if dl_sc.kind == 1 { 2633 emit_sp_sd(o, "a0" as *u8, dl_sc.idx) 2634 } 2635 return 0 2636 } 2637 out_str(o, " # unhandled op ") 2638 out_i64(o, op) 2639 out_char(o, 0x0A) 2640 return 0 2641} 2642 2643// ---- emit function ---- 2644// 2645// Prologue, per-block label + body, epilogue. Takes a name string 2646// (null-terminated), frame info, locs. 2647 2648func emit_function(f: *Function, locs: *ValueLoc, o: *OutBuf, 2649 fn_name: *u8, 2650 frame_size: i64, ra_slot: i64, 2651 save_mask: i64, save_mask_fpr: i64) -> i64 { 2652 // Frame layout when any saves are present: 2653 // [0 .. spill_bytes) spill slots (regalloc) 2654 // [ra_slot .. ra_slot+8) ra 2655 // [ra_slot+8 .. +N_gpr*8) N_gpr saved s-regs 2656 // [ra_slot+8+N_gpr*8 .. +N_fpr*8) N_fpr saved fs-regs 2657 // frame_size bumped by (N_gpr + N_fpr) * 8 2658 // When both masks are zero, layout is unchanged (frame_size=16, 2659 // ra_slot=8). 2660 // Pre-pass: total alloca bytes. Allocas live below ra at sp+0.., 2661 // so the ra slot must be moved past them or storing param `b` at 2662 // sp+8 (the second alloca's home) clobbers ra. Was a real bug 2663 // on callmin: 16-byte frame with 2 allocas overwrote ra and the 2664 // epilogue's `ld ra; ret` jumped to the param value (4) and 2665 // SIGSEGV'd at addr 4. Hoist the alloca walk here so frame 2666 // layout below sees the correct total. 2667 let alloca_off_raw: *u8 = sys_mmap(f.n_values * 8 + 16) 2668 let alloca_off: *i64 = alloca_off_raw as *i64 2669 // ★BUG 8 ROOT FIX: allocas must sit ABOVE the spill region, not at sp+0. regalloc spills SSA values to 2670 // slots [0..spill_bytes); compute_alloca_offsets was called with spill_bytes=0, so alloca #0 (`acc`) also 2671 // landed at sp+0 and a spilled value at slot 0 overwrote it (TRIPLE-NESTED gave 4 not 24 once a value 2672 // spilled to slot 0). The frame-layout comment above always intended "[0..spill_bytes) spill slots" then 2673 // allocas -- it just passed 0. Recover the true spill_bytes as the max spilled ValueLoc offset + 8 (loc 2674 // kind 1 = VL_SPILLED; idx = slot; idx<0 = never-live, skip) and base the allocas above it. 2675 var spill_bytes: i64 = 0 2676 var sv: i64 = 0 2677 while sv < f.n_values { 2678 let sl: *ValueLoc = loc_at(locs, sv) 2679 if sl.kind == 1 { 2680 if sl.idx >= 0 { 2681 if sl.idx + 8 > spill_bytes { spill_bytes = sl.idx + 8 } 2682 } 2683 } 2684 sv = sv + 1 2685 } 2686 let alloca_total: i64 = compute_alloca_offsets(f, spill_bytes, alloca_off) 2687 2688 // F14 fix: mark every alloca's loc as VL_ALLOCA so materialise() 2689 // rematerialises `addi reg, sp, off` at each use rather than 2690 // relying on a real register home that intermediate compute can 2691 // clobber. compute_alloca_offsets sets alloca_off[v] >= 0 for 2692 // any v that is an OP_ALLOCA result; other values stay at -1. 2693 var av: i64 = 0 2694 while av < f.n_values { 2695 if alloca_off[av] >= 0 { 2696 let al: *ValueLoc = loc_at(locs, av) 2697 al.kind = 3 2698 al.idx = alloca_off[av] 2699 } 2700 av = av + 1 2701 } 2702 2703 let save_count_gpr: i64 = popcount(save_mask) 2704 let save_count_fpr: i64 = popcount(save_mask_fpr) 2705 let save_count_total: i64 = save_count_gpr + save_count_fpr 2706 // Bump ra_slot up if the alloca area would land on top of it. 2707 // Round to 8 for sd alignment. 2708 var ra_slot_actual: i64 = ra_slot 2709 if alloca_total > ra_slot_actual { 2710 ra_slot_actual = (alloca_total + 7) & (0 - 8) 2711 } 2712 let save_base_gpr: i64 = ra_slot_actual + 8 2713 let save_base_fpr: i64 = ra_slot_actual + 8 + save_count_gpr * 8 2714 var actual_frame: i64 = ra_slot_actual + 8 2715 if save_count_total > 0 { 2716 actual_frame = ra_slot_actual + 8 + save_count_total * 8 2717 } 2718 2719 out_str(o, "\n .text\n") 2720 out_str(o, " .globl ") 2721 out_str(o, fn_name) 2722 out_char(o, 0x0A) 2723 // Mark function symbol so GDB classifies + sizes it correctly. 2724 out_str(o, " .type ") 2725 out_str(o, fn_name) 2726 out_str(o, ", @function\n") 2727 out_str(o, fn_name) 2728 out_str(o, ":\n") 2729 // DWARF Call-Frame Info (CFI) directives: tell GDB how to unwind 2730 // our frames. Without these the 'bt' backtrace stops at the 2731 // first frame because GDB doesn't know where we saved ra. 2732 // .cfi_startproc: begin CFI record for this function 2733 // .cfi_def_cfa_offset N: sp+N is the canonical frame address 2734 // (i.e. the caller's sp, which equals the incoming sp before 2735 // our addi sp, sp, -N) 2736 // .cfi_offset ra, -8: ra was spilled at CFA-8 (our sp+ra_slot) 2737 // .cfi_endproc: close the record (emitted at function end) 2738 out_str(o, " .cfi_startproc\n") 2739 emit_sp_adjust(o, 0 - actual_frame) 2740 out_str(o, " .cfi_def_cfa_offset ") 2741 out_i64(o, actual_frame) 2742 out_char(o, 0x0A) 2743 emit_sp_sd(o, "ra" as *u8, ra_slot_actual) 2744 out_str(o, " .cfi_offset ra, ") 2745 out_i64(o, ra_slot_actual - actual_frame) 2746 out_char(o, 0x0A) 2747 if save_count_gpr > 0 { 2748 emit_save_gpr(o, save_mask, save_base_gpr, actual_frame) 2749 } 2750 if save_count_fpr > 0 { 2751 emit_save_fpr(o, save_mask_fpr, save_base_fpr, actual_frame) 2752 } 2753 2754 // Param prologue: RV64 ABI passes params in a0..a7. Regalloc 2755 // assigned each VK_PARAM Value to a register / spill slot from 2756 // the function pool (t0..t6, s0..s11) -- but the function entry 2757 // has the params sitting in a0..a7. Emit a copy from a<idx> 2758 // to wherever regalloc placed the param. Without this, the 2759 // body reads from an uninitialised register and gets garbage 2760 // (the param ABI bug -- previously hit on `add(3,4)` returning 2761 // 3 and on `sys_mmap(8)` calling `mmap(0)`). 2762 // RV64GD ABI: int args go to a0..a7, FP args to fa0..fa7, with 2763 // SEPARATE counters per bank. Walk VAL_PARAMs in pi order 2764 // (parse.nx creates them in source order so pi monotonically 2765 // matches param_index for the prologue). Track int_abi / fp_abi 2766 // separately so a mixed signature like f(int, f64, int) is 2767 // unpacked correctly: a0 -> p0, fa0 -> p1, a1 -> p2. 2768 let lbase_pp: i64 = locs as i64 2769 var int_abi: i64 = 0 2770 var fp_abi: i64 = 0 2771 var pi: i64 = 0 2772 while pi < f.n_values { 2773 let v: *Value = val_at(f, pi) 2774 if v.kind == VK_PARAM { 2775 let pl: *ValueLoc = (lbase_pp + pi * 16) as *ValueLoc 2776 let pidx: i64 = v.param_index 2777 var p_is_fp: i64 = 0 2778 var p_is_d: i64 = 0 2779 if v.ty != (0 as *Type) { 2780 let kt: i64 = v.ty.kind 2781 if kt == TY_F32 { p_is_fp = 1 } 2782 if kt == TY_F64 { p_is_fp = 1; p_is_d = 1 } 2783 } 2784 if pidx >= 0 { 2785 if p_is_fp == 1 { 2786 if fp_abi < 8 { 2787 if pl.kind == VL_REGISTER { 2788 if p_is_d == 1 { 2789 out_str(o, " fmv.d ") 2790 } else { 2791 out_str(o, " fmv.s ") 2792 } 2793 reg_name(o, pl.idx) 2794 out_str(o, ", fa") 2795 out_i64(o, fp_abi) 2796 out_char(o, 0x0A) 2797 } 2798 if pl.kind == VL_SPILLED { 2799 if p_is_d == 1 { 2800 out_str(o, " fsd fa") 2801 } else { 2802 out_str(o, " fsw fa") 2803 } 2804 out_i64(o, fp_abi) 2805 out_str(o, ", ") 2806 out_i64(o, pl.idx) 2807 out_str(o, "(sp)\n") 2808 } 2809 } 2810 fp_abi = fp_abi + 1 2811 } else { 2812 if int_abi < 8 { 2813 if pl.kind == VL_REGISTER { 2814 out_str(o, " mv ") 2815 reg_name(o, pl.idx) 2816 out_str(o, ", a") 2817 out_i64(o, int_abi) 2818 out_char(o, 0x0A) 2819 } 2820 if pl.kind == VL_SPILLED { 2821 out_str(o, " sd a") 2822 out_i64(o, int_abi) 2823 out_str(o, ", ") 2824 out_i64(o, pl.idx) 2825 out_str(o, "(sp)\n") 2826 } 2827 } 2828 int_abi = int_abi + 1 2829 } 2830 } 2831 } 2832 pi = pi + 1 2833 } 2834 2835 // alloca_off was computed above (hoisted so frame layout knew 2836 // the total). No second walk needed here. 2837 var bi: i64 = 0 2838 while bi < f.n_blocks { 2839 let base: i64 = f.blocks as i64 2840 let b: *BasicBlock = (base + bi * 96) as *BasicBlock 2841 out_str(o, ".L") 2842 out_str(o, fn_name) 2843 out_str(o, "_bb") 2844 out_i64(o, b.id) 2845 out_str(o, ":\n") 2846 var inst: *Instr = b.head 2847 while inst != (0 as *Instr) { 2848 emit_instr(f, locs, o, inst, actual_frame, ra_slot_actual, 2849 fn_name, alloca_off) 2850 inst = inst.next 2851 } 2852 bi = bi + 1 2853 } 2854 2855 // Shared epilogue block. All RETURNs in the body jump here via 2856 // `j .L<fn>_epi`. Always emitted (even when save_mask == 0) so 2857 // rv_emit_return has a single uniform codepath; this keeps 2858 // emit_instr's argument list under the nxc2/riscv.c 8-arg ABI 2859 // cap (threading save_mask through would need 9 args). 2860 out_str(o, ".L") 2861 out_str(o, fn_name) 2862 out_str(o, "_epi:\n") 2863 if save_count_fpr > 0 { 2864 emit_restore_fpr(o, save_mask_fpr, save_base_fpr) 2865 } 2866 if save_count_gpr > 0 { 2867 emit_restore_gpr(o, save_mask, save_base_gpr) 2868 } 2869 emit_sp_ld(o, "ra" as *u8, ra_slot_actual) 2870 emit_sp_adjust(o, actual_frame) 2871 out_str(o, " ret\n") 2872 out_str(o, " .cfi_endproc\n") 2873 return 0 2874} 2875 2876// ===== self-test ==================================================== 2877// 2878// Build `func main() -> i64 { return 42 }`, fake a ValueLoc (const 2879// doesn't need one), emit the function, check the output buffer 2880// contains a recognisable snippet. 2881 2882func emit_return_instr(bb: *BasicBlock, v: i64) -> i64 { 2883 let f: *Function = bb.parent 2884 let i: *Instr = alloc_instr(f, 30, ir_type_i64()) 2885 i.n_operands = 1 2886 i.op0 = v 2887 append_instr(bb, i) 2888 return 0 2889} 2890 2891// Library only; self-test lives in riscv_test.nx.