code wiki / (root) / nx_riscv.nx

nx_riscv.nx source

↩ module page · 2821 lines · 106471 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 materialise_value(f, locs, o, i.op0, "t0") // fn-ptr target VALUE -> t0 (load if alloca'd), before any a-reg is touched 1363 var k: i64 = 1 1364 var int_k: i64 = 0 1365 var fp_k: i64 = 0 1366 while k < n { 1367 var v: i64 = i.op1 1368 if k == 2 { v = i.op2 } 1369 if k == 3 { v = i.op3 } 1370 if k == 4 { v = i.op4 } 1371 if k == 5 { v = i.op5 } 1372 if k == 6 { v = i.op6 } 1373 if k == 7 { v = i.op7 } 1374 let val: *Value = val_at(f, v) 1375 var is_fp: i64 = 0 1376 var is_d: i64 = 0 1377 if val.ty != (0 as *Type) { 1378 let kt: i64 = val.ty.kind 1379 if kt == TY_F32 { is_fp = 1 } 1380 if kt == TY_F64 { is_fp = 1; is_d = 1 } 1381 } 1382 if is_fp == 1 { 1383 if val.kind == 0 { 1384 out_str(o, " li t6, ") 1385 out_i64(o, val.const_int) 1386 out_char(o, 0x0A) 1387 if is_d == 1 { 1388 out_str(o, " fmv.d.x ") 1389 } else { 1390 out_str(o, " fmv.w.x ") 1391 } 1392 target_freg_name(o, fp_k) 1393 out_str(o, ", t6\n") 1394 } else { 1395 let fscratch: *u8 = sys_mmap(8) 1396 fscratch[0] = 0x66 // 'f' 1397 fscratch[1] = 0x61 // 'a' 1398 fscratch[2] = 0x30 + fp_k // '0'..'7' 1399 fscratch[3] = 0 1400 fmaterialise(f, locs, o, v, fscratch) 1401 } 1402 fp_k = fp_k + 1 1403 } else { 1404 if val.kind == 0 { 1405 out_str(o, " li ") 1406 target_reg_name(o, int_k) 1407 out_str(o, ", ") 1408 out_i64(o, val.const_int) 1409 out_char(o, 0x0A) 1410 } else { 1411 let scratch: *u8 = sys_mmap(4) 1412 scratch[0] = 0x61 // 'a' 1413 scratch[1] = 0x30 + int_k // '0'..'7' 1414 scratch[2] = 0 1415 materialise_value(f, locs, o, v, scratch) 1416 } 1417 int_k = int_k + 1 1418 } 1419 k = k + 1 1420 } 1421 out_str(o, " jalr ra, t0, 0\n") 1422 1423 // Save return value (a0/fa0) into result's location -- same as rv_emit_call. 1424 let dl: *ValueLoc = loc_at(locs, i.result) 1425 var ret_is_fp: i64 = 0 1426 var ret_is_d: i64 = 0 1427 if i.ty != (0 as *Type) { 1428 let kr: i64 = i.ty.kind 1429 if kr == TY_F32 { ret_is_fp = 1 } 1430 if kr == TY_F64 { ret_is_fp = 1; ret_is_d = 1 } 1431 } 1432 if ret_is_fp == 1 { 1433 if dl.kind == 0 { 1434 if ret_is_d == 1 { 1435 out_str(o, " fmv.d ") 1436 } else { 1437 out_str(o, " fmv.s ") 1438 } 1439 reg_name(o, dl.idx) 1440 out_str(o, ", fa0\n") 1441 } 1442 if dl.kind == 1 { 1443 if ret_is_d == 1 { 1444 emit_sp_fsd(o, "fa0" as *u8, dl.idx) 1445 } else { 1446 emit_sp_fsw(o, "fa0" as *u8, dl.idx) 1447 } 1448 } 1449 } else { 1450 if dl.kind == 0 { 1451 out_str(o, " mv ") 1452 reg_name(o, dl.idx) 1453 out_str(o, ", a0\n") 1454 } 1455 if dl.kind == 1 { 1456 emit_sp_sd(o, "a0" as *u8, dl.idx) 1457 } 1458 } 1459 return 0 1460} 1461 1462// ---- load / store (port from riscv.c's emit_load / emit_store) ---- 1463// 1464// Width-specific load: lb/lh/lw/ld (signed); lbu/lhu/lwu (unsigned). 1465// Width-specific store: sb/sh/sw/sd. Size comes from instruction's 1466// result type (for loads) or operand type (for stores). Fallback is 1467// 8-byte (ld/sd) when type info is missing. 1468 1469func load_mnem_name(o: *OutBuf, sz: i64, is_signed: i64) -> i64 { 1470 if sz == 1 { 1471 if is_signed == 1 { out_str(o, "lb") } else { out_str(o, "lbu") } 1472 return 0 1473 } 1474 if sz == 2 { 1475 if is_signed == 1 { out_str(o, "lh") } else { out_str(o, "lhu") } 1476 return 0 1477 } 1478 if sz == 4 { 1479 if is_signed == 1 { out_str(o, "lw") } else { out_str(o, "lwu") } 1480 return 0 1481 } 1482 out_str(o, "ld") 1483 return 0 1484} 1485 1486func store_mnem_name(o: *OutBuf, sz: i64) -> i64 { 1487 if sz == 1 { out_str(o, "sb"); return 0 } 1488 if sz == 2 { out_str(o, "sh"); return 0 } 1489 if sz == 4 { out_str(o, "sw"); return 0 } 1490 out_str(o, "sd") 1491 return 0 1492} 1493 1494// Are the low bits of a type kind the signed integer class? 1495func type_kind_is_signed(k: i64) -> i64 { 1496 if k == TY_I8 { return 1 } 1497 if k == TY_I16 { return 1 } 1498 if k == TY_I32 { return 1 } 1499 if k == TY_I64 { return 1 } 1500 return 0 1501} 1502 1503func rv_emit_load(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 1504 materialise(f, locs, o, i.op0, "t4") 1505 let dl: *ValueLoc = loc_at(locs, i.result) 1506 var sz: i64 = 8 1507 var is_signed: i64 = 0 1508 if i.ty != (0 as *Type) { 1509 if i.ty.size > 0 { sz = i.ty.size } 1510 // 2026-07-10 debt fix: signedness now comes from the sext bit (set only for i8/i16/i32 1511 // annotations), NOT from the kind. Kind-based was WRONG for *u8 (u8 also mints TY_I8 -> lb 1512 // sign-extended bytes >= 0x80, the x509 0xA0-must-stay-160 class). Aligns RV64 with x86. 1513 is_signed = i.ty.sext 1514 } 1515 out_str(o, " ") 1516 load_mnem_name(o, sz, is_signed) 1517 out_str(o, " ") 1518 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 1519 out_str(o, ", 0(t4)\n") 1520 if dl.kind == 1 { 1521 emit_sp_sd(o, "t6" as *u8, dl.idx) 1522 } 1523 return 0 1524} 1525 1526// OP_ADDR_OF (&x): materialise op0's ADDRESS into t4 (op0 is the alloca -> materialise emits `addi t4, sp, off` via the 1527// F14 VL_ALLOCA path = the address of x), then store t4 into the result's home. Mirrors x86_64_ctx OP_ADDR_OF (load 1528// op0's addr into rax, store to result). Neither RISC-V backend had this -> `let p = &x` never set p -> wild deref 1529// (caught by the nxc-vs-QEMU oracle: ptr-to-local gave a wild address). Fixes address-of-local codegen. 1530func rv_emit_addr_of(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 1531 materialise(f, locs, o, i.op0, "t4") // op0 is the alloca; VL_ALLOCA -> `addi t4, sp, off` = &op0 1532 let dl: *ValueLoc = loc_at(locs, i.result) 1533 if dl.kind == 0 { 1534 out_str(o, " mv ") 1535 reg_name(o, dl.idx) 1536 out_str(o, ", t4\n") 1537 } 1538 if dl.kind == 1 { 1539 emit_sp_sd(o, "t4" as *u8, dl.idx) 1540 } 1541 return 0 1542} 1543 1544func rv_emit_store(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 1545 materialise(f, locs, o, i.op0, "t4") // address 1546 materialise(f, locs, o, i.op1, "t5") // value 1547 var sz: i64 = 8 1548 if i.ty != (0 as *Type) { 1549 if i.ty.kind != TY_VOID { 1550 if i.ty.size > 0 { sz = i.ty.size } 1551 } 1552 } 1553 out_str(o, " ") 1554 store_mnem_name(o, sz) 1555 out_str(o, " t5, 0(t4)\n") 1556 return 0 1557} 1558 1559// ---- GEP address arithmetic (port from riscv.c's emit_gep) ---- 1560// 1561// GEP = base-pointer + offset. When offset is a constant in the 1562// [-2048, 2047] range it fits in an `addi` immediate; otherwise 1563// fall back to materialising offset into a reg and using `add`. 1564 1565func rv_emit_gep(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 1566 materialise(f, locs, o, i.op0, "t4") 1567 let off_v: *Value = val_at(f, i.op1) 1568 let dl: *ValueLoc = loc_at(locs, i.result) 1569 1570 if off_v.kind == 0 { 1571 // CONST_INT. Check immediate range for addi. 1572 let c: i64 = off_v.const_int 1573 if c >= -2048 { 1574 if c <= 2047 { 1575 out_str(o, " addi ") 1576 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 1577 out_str(o, ", t4, ") 1578 out_i64(o, c) 1579 out_char(o, 0x0A) 1580 if dl.kind == 1 { 1581 emit_sp_sd(o, "t6" as *u8, dl.idx) 1582 } 1583 return 0 1584 } 1585 } 1586 } 1587 1588 // Non-immediate offset: materialise into t5, emit `add`. 1589 materialise(f, locs, o, i.op1, "t5") 1590 out_str(o, " add ") 1591 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 1592 out_str(o, ", t4, t5\n") 1593 if dl.kind == 1 { 1594 emit_sp_sd(o, "t6" as *u8, dl.idx) 1595 } 1596 return 0 1597} 1598 1599// ---- tail call (port from riscv.c's emit_tail_call) ---- 1600// 1601// Like emit_call but unwinds our frame first, then jumps to the 1602// callee via `tail` (no ra push). Callee's `ret` returns 1603// directly to our caller: O(1) stack regardless of recursion 1604// depth. Used when the parser detects `return foo(...)` in tail 1605// position. 1606 1607func rv_emit_tail_call(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr, 1608 frame_size: i64, ra_slot: i64) -> i64 { 1609 let n: i64 = i.n_operands 1610 var k: i64 = 0 1611 while k < n { 1612 var v: i64 = i.op0 1613 if k == 1 { v = i.op1 } 1614 if k == 2 { v = i.op2 } 1615 if k == 3 { v = i.op3 } 1616 if k == 4 { v = i.op4 } 1617 if k == 5 { v = i.op5 } 1618 if k == 6 { v = i.op6 } 1619 if k == 7 { v = i.op7 } 1620 let val: *Value = val_at(f, v) 1621 if val.kind == 0 { 1622 out_str(o, " li ") 1623 target_reg_name(o, k) 1624 out_str(o, ", ") 1625 out_i64(o, val.const_int) 1626 out_char(o, 0x0A) 1627 } else { 1628 let scratch: *u8 = sys_mmap(4) 1629 scratch[0] = 0x61 1630 scratch[1] = 0x30 + k 1631 scratch[2] = 0 1632 materialise(f, locs, o, v, scratch) 1633 } 1634 k = k + 1 1635 } 1636 // Tear down frame: reload ra, pop frame, then `tail`. 1637 emit_sp_ld(o, "ra" as *u8, ra_slot) 1638 out_str(o, " addi sp, sp, ") 1639 out_i64(o, frame_size) 1640 out_char(o, 0x0A) 1641 out_str(o, " tail ") 1642 if i.callee != (0 as *Function) { 1643 let callee_name: *u8 = i.callee.name_start as *u8 1644 out_str(o, callee_name) 1645 } else { 1646 out_str(o, "_unknown_callee") 1647 } 1648 out_char(o, 0x0A) 1649 return 0 1650} 1651 1652// ---- F-extension casts (int <-> float) ---------------------------- 1653// 1654// OP_FCAST_I_TO_F (55): convert i64 in op0 to FP value of result type. 1655// Emit `fcvt.d.l fdst, src_int` for f64 result, `fcvt.s.l` for f32. 1656// OP_FCAST_F_TO_I (56): convert FP in op0 (f32 or f64) to i64 result. 1657// Emit `fcvt.l.d dst, fsrc, rtz` for f64 source (round-to-zero = 1658// C-style truncation), `fcvt.l.s` for f32 source. 1659// 1660// rtz matches the IEEE 754 truncation semantics most languages use 1661// for explicit float-to-int casts. Round-to-nearest-even is the 1662// default if no mode is given but produces surprises on .5 inputs. 1663 1664func rv_emit_fcast(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 1665 let op: i64 = i.op 1666 if op == OP_FCAST_I_TO_F { 1667 materialise(f, locs, o, i.op0, "t4" as *u8) 1668 let dl: *ValueLoc = loc_at(locs, i.result) 1669 var is_d: i64 = 0 1670 if i.ty != (0 as *Type) { 1671 if i.ty.kind == TY_F64 { is_d = 1 } 1672 } 1673 if is_d == 1 { 1674 out_str(o, " fcvt.d.l ") 1675 } else { 1676 out_str(o, " fcvt.s.l ") 1677 } 1678 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "ft6") } 1679 out_str(o, ", t4\n") 1680 if dl.kind == 1 { 1681 if is_d == 1 { 1682 emit_sp_fsd(o, "ft6" as *u8, dl.idx) 1683 } else { 1684 emit_sp_fsw(o, "ft6" as *u8, dl.idx) 1685 } 1686 } 1687 return 0 1688 } 1689 if op == OP_FCAST_F_TO_I { 1690 let v0: *Value = val_at(f, i.op0) 1691 var is_d: i64 = 0 1692 if v0.ty != (0 as *Type) { 1693 if v0.ty.kind == TY_F64 { is_d = 1 } 1694 } 1695 fmaterialise(f, locs, o, i.op0, "ft4" as *u8) 1696 let dl: *ValueLoc = loc_at(locs, i.result) 1697 if is_d == 1 { 1698 out_str(o, " fcvt.l.d ") 1699 } else { 1700 out_str(o, " fcvt.l.s ") 1701 } 1702 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 1703 out_str(o, ", ft4, rtz\n") 1704 if dl.kind == 1 { 1705 emit_sp_sd(o, "t6" as *u8, dl.idx) 1706 } 1707 return 0 1708 } 1709 return 0 1710} 1711 1712// ---- alloca prep + emit (port from riscv.c's emit_alloca) ---- 1713// 1714// Each OP_ALLOCA reserves 8 bytes (rounded up) in the function's 1715// stack frame and returns the address as addi dst, sp, <offset>. 1716// Since riscv.nx doesn't track alloca offsets in a Ctx struct, 1717// we compute them in a preliminary pass over the function before 1718// emitting any instruction. 1719// 1720// Layout: allocas live ABOVE the spill area (which occupies 0.. 1721// spill_bytes) and BELOW the saved ra slot. Simple assignment: 1722// allocated in encounter order, 8-byte aligned. 1723 1724func compute_alloca_offsets(f: *Function, spill_bytes: i64, 1725 alloca_off: *i64) -> i64 { 1726 // Initialise all to -1. 1727 var i: i64 = 0 1728 while i < f.n_values { 1729 alloca_off[i] = -1 1730 i = i + 1 1731 } 1732 // Walk blocks; assign offsets to each OP_ALLOCA's result. 1733 var cur_off: i64 = spill_bytes 1734 var bi: i64 = 0 1735 while bi < f.n_blocks { 1736 let bb: *BasicBlock = block_at(f, bi) 1737 var inst: *Instr = bb.head 1738 while inst != (0 as *Instr) { 1739 if inst.op == OP_ALLOCA { 1740 if inst.result < f.n_values { 1741 alloca_off[inst.result] = cur_off 1742 // Size: use type.size if available, else 8. 1743 var sz: i64 = 8 1744 if inst.ty != (0 as *Type) { 1745 if inst.ty.size > 0 { 1746 sz = inst.ty.size 1747 } 1748 } 1749 // Round up to 8. 1750 sz = (sz + 7) & (0 - 8) 1751 cur_off = cur_off + sz 1752 } 1753 } 1754 inst = inst.next 1755 } 1756 bi = bi + 1 1757 } 1758 return cur_off // total alloca area bytes 1759} 1760 1761func rv_emit_alloca(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr, 1762 alloca_off: *i64) -> i64 { 1763 let off: i64 = alloca_off[i.result] 1764 if off < 0 { 1765 out_str(o, " # alloca with no slot?!\n") 1766 return 0 1767 } 1768 let dl: *ValueLoc = loc_at(locs, i.result) 1769 // F14 fix: VL_ALLOCA values are rematerialised by materialise() 1770 // at each use; the alloca slot itself is reserved in the prologue 1771 // by compute_alloca_offsets. Nothing to emit here in that case. 1772 if dl.kind == 3 { return 0 } 1773 out_str(o, " addi ") 1774 if dl.kind == 0 { reg_name(o, dl.idx) } else { out_str(o, "t6") } 1775 out_str(o, ", sp, ") 1776 out_i64(o, off) 1777 out_char(o, 0x0A) 1778 if dl.kind == 1 { 1779 emit_sp_sd(o, "t6" as *u8, dl.idx) 1780 } 1781 return 0 1782} 1783 1784// ---- per-instruction dispatch ---- 1785 1786// ---- emit widening SIMD dot product i16x16 -> i64 ---- 1787// 1788// Lowers OP_SIMD_VDOT_I16_X16 to the RV-V chain: 1789// vsetvli e16 m1 avl=16 1790// vle16.v v1, (a_ptr) 1791// vle16.v v2, (b_ptr) 1792// vwmul.vv v4, v1, v2 ; widens i16*i16 -> i32, group m2 1793// vsetvli e64 m1 avl=1 ; switch to e64 to init accumulator 1794// vmv.v.i v6, 0 1795// vsetvli e32 m2 avl=16 ; back to e32 m2 for the reduce 1796// vwredsum.vs v6, v4, v6 ; widens i32 -> i64 scalar in v6[0] 1797// vsetvli e64 m1 avl=1 1798// vmv.x.s <dst>, v6 1799// 1800// Same shape as the C-side x86_64.c emit_simd_vdot_i16_x16 (which 1801// uses vpmaddwd) and riscv.c OP_SIMD_VDOT_I16_X16 (same RVV chain 1802// being ported here from C to NishiLang). 1803func rv_emit_simd_vdot_i16(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 1804 // Materialise the two pointer operands into t4 / t5. 1805 materialise(f, locs, o, i.op0, "t4") 1806 materialise(f, locs, o, i.op1, "t5") 1807 // Set vector length scalar (t6 = 16) -- this is a regular scalar 1808 // ADDI, which the existing nxasm `li` mnemonic handles. 1809 out_str(o, " li t6, 16\n") 1810 // The RVV instructions are emitted as raw 32-bit words via the 1811 // .word directive. Encodings pre-computed against GNU as 1812 // (riscv64-linux-gnu-as -march=rv64imav) -- saves bringing up a 1813 // full RVV mnemonic parser in nxasm. Each word is the exact 1814 // bytes GNU as emits for the corresponding mnemonic. See 1815 // bench/_offc/rvv_enc_probe.s for the source mapping; should 1816 // any encoding change, regen by rerunning that probe. 1817 // 1818 // vsetvli t6, t6, e16, m1, ta, ma -> 0x0c8fffd7 1819 out_str(o, " .word 0x0c8fffd7\n") 1820 // vle16.v v1, (t4) -> 0x020ed087 1821 out_str(o, " .word 0x020ed087\n") 1822 // vle16.v v2, (t5) -> 0x020f5107 1823 out_str(o, " .word 0x020f5107\n") 1824 // vwmul.vv v4, v1, v2 -> 0xee112257 1825 out_str(o, " .word 0xee112257\n") 1826 // Init e64 m1 accumulator vector v6 = 0. AVL=1 since we only 1827 // need lane 0 of v6 zeroed for the reduce; widening reduce reads 1828 // the full source lanes but only writes scalar lane 0 of v6. 1829 out_str(o, " li t6, 1\n") 1830 // vsetvli t6, t6, e64, m1, ta, ma -> 0x0d8fffd7 1831 out_str(o, " .word 0x0d8fffd7\n") 1832 // vmv.v.i v6, 0 -> 0x5e003357 1833 out_str(o, " .word 0x5e003357\n") 1834 // Switch to e32 m2 to read the widened products + reduce. Reset 1835 // AVL=16 -- the vsetvli before this one zeroed it via t6=1. 1836 out_str(o, " li t6, 16\n") 1837 // vsetvli t6, t6, e32, m2, ta, ma -> 0x0d1fffd7 1838 out_str(o, " .word 0x0d1fffd7\n") 1839 // vwredsum.vs v6, v4, v6 -> 0xc6430357 1840 out_str(o, " .word 0xc6430357\n") 1841 // Restore e64 m1 + AVL=1 for the scalar extract. 1842 out_str(o, " li t6, 1\n") 1843 // vsetvli t6, t6, e64, m1, ta, ma -> 0x0d8fffd7 1844 out_str(o, " .word 0x0d8fffd7\n") 1845 // Extract v6[0] as i64 scalar into t6 (x31), then either mv to 1846 // the destination register or spill it. Always landing in t6 1847 // avoids needing to map regalloc pool indices (0=t0, 3=t3, 1848 // 6=s2, ...) to physical x-reg numbers for the encoding -- a 1849 // separate concern that lives in reg_name's name table. 1850 // 1851 // vmv.x.s t6, v6 -> 0x42602fd7 (rd=x31, vs2=v6) 1852 out_str(o, " .word 0x42602fd7\n") 1853 let dl: *ValueLoc = loc_at(locs, i.result) 1854 if dl.kind == 0 { 1855 out_str(o, " mv ") 1856 reg_name(o, dl.idx) 1857 out_str(o, ", t6\n") 1858 } 1859 if dl.kind == 1 { 1860 emit_sp_sd(o, "t6" as *u8, dl.idx) 1861 } 1862 return 0 1863} 1864 1865// ---- emit horizontal min/max reduce for i16x16 ---- 1866// Source: *i64 pointer to 4 packed-i16 i64 words (16 lanes). 1867// Lowers to: 1868// li t6, 16 1869// vsetvli t6, t6, e16, m1, ta, ma .word 0x0c8fffd7 1870// vle16.v v1, (t4) .word 0x020ed087 1871// vmv.x.s t6, v1 .word 0x42102fd7 (seed) 1872// vmv.v.x v0, t6 .word 0x5e0fc057 (broadcast seed) 1873// vred{min,max}.vs v0, v1, v0 1874// vmv.x.s t6, v0 .word 0x42002fd7 1875// slli t6, t6, 48 (sign-extend i16 -> i64) 1876// srai t6, t6, 48 1877// mv dst, t6 (or spill) 1878func rv_emit_simd_vreduce_minmax_i16(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr, is_max: i64) -> i64 { 1879 materialise(f, locs, o, i.op0, "t4") 1880 out_str(o, " li t6, 16\n") 1881 out_str(o, " .word 0x0c8fffd7\n") // vsetvli e16 m1 1882 out_str(o, " .word 0x020ed087\n") // vle16.v v1, (t4) 1883 out_str(o, " .word 0x42102fd7\n") // vmv.x.s t6, v1 (seed from lane 0) 1884 out_str(o, " .word 0x5e0fc057\n") // vmv.v.x v0, t6 (broadcast seed) 1885 if is_max == 0 { 1886 out_str(o, " .word 0x16102057\n") // vredmin.vs v0, v1, v0 1887 } 1888 if is_max == 1 { 1889 out_str(o, " .word 0x1e102057\n") // vredmax.vs v0, v1, v0 1890 } 1891 out_str(o, " .word 0x42002fd7\n") // vmv.x.s t6, v0 1892 // Sign-extend i16 -> i64 via shift pair. 1893 out_str(o, " slli t6, t6, 48\n") 1894 out_str(o, " srai t6, t6, 48\n") 1895 let dl: *ValueLoc = loc_at(locs, i.result) 1896 if dl.kind == 0 { 1897 out_str(o, " mv ") 1898 reg_name(o, dl.idx) 1899 out_str(o, ", t6\n") 1900 } 1901 if dl.kind == 1 { 1902 emit_sp_sd(o, "t6" as *u8, dl.idx) 1903 } 1904 return 0 1905} 1906 1907// ---- emit i16x16 generic per-lane vbinop ---- 1908// Args: op0 = *i64 a, op1 = *i64 b, op2 = *i64 out. 1909// enc_word = the pre-computed RVV encoding for `<op>.vv v3, v1, v2` 1910// (vsadd=0x861101d7, vssub=0x8e1101d7, vsaddu=0x821101d7, 1911// vssubu=0x8a1101d7, vmin=0x161101d7, vmax=0x1e1101d7, 1912// vadd=0x021101d7, vsub=0x0a1101d7, vmul=0x961121d7). 1913// Loads both vectors into v1/v2, applies enc_word, stores to *out. 1914func rv_emit_simd_vbinop_i16(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr, enc_word: i64) -> i64 { 1915 materialise(f, locs, o, i.op0, "t4") 1916 materialise(f, locs, o, i.op1, "t5") 1917 out_str(o, " li t6, 16\n") 1918 out_str(o, " .word 0x0c8fffd7\n") // vsetvli e16 m1 1919 out_str(o, " .word 0x020ed087\n") // vle16.v v1, (t4) 1920 out_str(o, " .word 0x020f5107\n") // vle16.v v2, (t5) 1921 out_str(o, " .word ") 1922 out_i64(o, enc_word) // <op>.vv v3, v1, v2 1923 out_char(o, 0x0A) 1924 materialise(f, locs, o, i.op2, "t4") // out pointer 1925 out_str(o, " .word 0x020ed1a7\n") // vse16.v v3, (t4) 1926 let dl: *ValueLoc = loc_at(locs, i.result) 1927 if dl.kind == 0 { 1928 out_str(o, " li ") 1929 reg_name(o, dl.idx) 1930 out_str(o, ", 0\n") 1931 } 1932 if dl.kind == 1 { 1933 out_str(o, " li t6, 0\n") 1934 emit_sp_sd(o, "t6" as *u8, dl.idx) 1935 } 1936 return 0 1937} 1938// vsadd retained as a named wrapper for the existing dispatch. 1939func rv_emit_simd_vsadd_i16(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 1940 return rv_emit_simd_vbinop_i16(f, locs, o, i, 0x861101d7) 1941} 1942 1943// ---- emit i16x16 per-lane shift (vsll/vsrl/vsra .vx form) ---- 1944// Args: op0 = *i64 src, op1 = i64 count (scalar), op2 = *i64 out. 1945// enc_word is the RVV encoding for `<op>.vx v3, v1, t4` where the 1946// scalar count register is fixed at t4 (= x29, encoded in rs1 of 1947// the shift instruction). 1948// 1949// Register sequencing: 1950// 1. t4 = src ptr (vle16.v v1, (t4) requires src in t4=rs1) 1951// 2. load v1 from (t4) 1952// 3. t4 = count (shift's rs1 is t4 -- overwrite, src ptr no longer needed) 1953// 4. <shift>.vx v3, v1, t4 1954// 5. t4 = out ptr (vse16.v v3, (t4)) 1955// 6. store v3 1956func rv_emit_simd_vshift_i16(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr, enc_word: i64) -> i64 { 1957 materialise(f, locs, o, i.op0, "t4") 1958 out_str(o, " li t6, 16\n") 1959 out_str(o, " .word 0x0c8fffd7\n") // vsetvli e16 m1 1960 out_str(o, " .word 0x020ed087\n") // vle16.v v1, (t4) 1961 materialise(f, locs, o, i.op1, "t4") // count into t4 (rs1) 1962 out_str(o, " .word ") 1963 out_i64(o, enc_word) // <op>.vx v3, v1, t4 1964 out_char(o, 0x0A) 1965 materialise(f, locs, o, i.op2, "t4") // out ptr 1966 out_str(o, " .word 0x020ed1a7\n") // vse16.v v3, (t4) 1967 let dl: *ValueLoc = loc_at(locs, i.result) 1968 if dl.kind == 0 { 1969 out_str(o, " li ") 1970 reg_name(o, dl.idx) 1971 out_str(o, ", 0\n") 1972 } 1973 if dl.kind == 1 { 1974 out_str(o, " li t6, 0\n") 1975 emit_sp_sd(o, "t6" as *u8, dl.idx) 1976 } 1977 return 0 1978} 1979 1980// ---- emit horizontal i16 sum -> i64 scalar ---- 1981// Lowers via vwredsum.vs (widening: e16 source -> e32 accumulator), 1982// then sign-extend the 32-bit scalar to 64-bit. 1983// 1. li t6, 16; vsetvli e16 m1 1984// 2. t4 = src; vle16.v v1, (t4) 1985// 3. li t6, 1; vsetvli e32 m1; vmv.v.i v6, 0 (init accumulator) 1986// 4. li t6, 16; vsetvli e16 m1 (back to source SEW for the reduce) 1987// 5. vwredsum.vs v6, v1, v6 -- widening sum into v6[0] as i32 1988// 6. li t6, 1; vsetvli e32 m1; vmv.x.s t6, v6 (extract, sign-ext to i64) 1989func rv_emit_simd_vreduce_sum_i16(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 1990 materialise(f, locs, o, i.op0, "t4") 1991 out_str(o, " li t6, 16\n") 1992 out_str(o, " .word 0x0c8fffd7\n") // vsetvli e16 m1 1993 out_str(o, " .word 0x020ed087\n") // vle16.v v1, (t4) 1994 // Init e32 m1 accumulator v6 = 0. 1995 out_str(o, " li t6, 1\n") 1996 out_str(o, " .word 0x0d0fffd7\n") // vsetvli e32 m1 1997 out_str(o, " .word 0x5e003357\n") // vmv.v.i v6, 0 1998 // Back to e16 m1 for the source spec; vwredsum widens internally. 1999 out_str(o, " li t6, 16\n") 2000 out_str(o, " .word 0x0c8fffd7\n") // vsetvli e16 m1 2001 out_str(o, " .word 0xc6130357\n") // vwredsum.vs v6, v1, v6 2002 // Extract i32 scalar from v6[0], vmv.x.s sign-extends to XLEN. 2003 out_str(o, " li t6, 1\n") 2004 out_str(o, " .word 0x0d0fffd7\n") // vsetvli e32 m1 2005 out_str(o, " .word 0x42602fd7\n") // vmv.x.s t6, v6 2006 let dl: *ValueLoc = loc_at(locs, i.result) 2007 if dl.kind == 0 { 2008 out_str(o, " mv ") 2009 reg_name(o, dl.idx) 2010 out_str(o, ", t6\n") 2011 } 2012 if dl.kind == 1 { 2013 emit_sp_sd(o, "t6" as *u8, dl.idx) 2014 } 2015 return 0 2016} 2017 2018// ---- emit i16x16 scalar broadcast ---- 2019// Args: op0 = i64 scalar value, op1 = *i64 out. 2020// Lowers to vmv.v.x v3, t4 with the scalar in t4. 2021func rv_emit_simd_vbroadcast_i16(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 2022 materialise(f, locs, o, i.op0, "t4") // scalar into t4 (= x29, rs1) 2023 out_str(o, " li t6, 16\n") 2024 out_str(o, " .word 0x0c8fffd7\n") // vsetvli e16 m1 2025 out_str(o, " .word 0x5e0ec1d7\n") // vmv.v.x v3, t4 2026 materialise(f, locs, o, i.op1, "t4") // out ptr 2027 out_str(o, " .word 0x020ed1a7\n") // vse16.v v3, (t4) 2028 let dl: *ValueLoc = loc_at(locs, i.result) 2029 if dl.kind == 0 { 2030 out_str(o, " li ") 2031 reg_name(o, dl.idx) 2032 out_str(o, ", 0\n") 2033 } 2034 if dl.kind == 1 { 2035 out_str(o, " li t6, 0\n") 2036 emit_sp_sd(o, "t6" as *u8, dl.idx) 2037 } 2038 return 0 2039} 2040 2041// ---- i8x32 generic per-lane vbinop (vadd/vsub/vsadd/vssub) ---- 2042// Same template as rv_emit_simd_vbinop_i16 but with e8 SEW. 2043// vsetvli e8 m1 avl=32 -> VL=32 (8-bit lanes in 256-bit vector). 2044// vle8.v / vse8.v for load/store; arith encodings are SEW-agnostic 2045// (same op-bytes used for i16x16 binops, just different vsetvli). 2046func rv_emit_simd_vbinop_i8(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr, enc_word: i64) -> i64 { 2047 materialise(f, locs, o, i.op0, "t4") 2048 materialise(f, locs, o, i.op1, "t5") 2049 out_str(o, " li t6, 32\n") 2050 out_str(o, " .word 0x0c0fffd7\n") // vsetvli e8 m1 2051 out_str(o, " .word 0x020e8087\n") // vle8.v v1, (t4) 2052 out_str(o, " .word 0x020f0107\n") // vle8.v v2, (t5) 2053 out_str(o, " .word ") 2054 out_i64(o, enc_word) // <op>.vv v3, v1, v2 2055 out_char(o, 0x0A) 2056 materialise(f, locs, o, i.op2, "t4") 2057 out_str(o, " .word 0x020e81a7\n") // vse8.v v3, (t4) 2058 let dl: *ValueLoc = loc_at(locs, i.result) 2059 if dl.kind == 0 { 2060 out_str(o, " li ") 2061 reg_name(o, dl.idx) 2062 out_str(o, ", 0\n") 2063 } 2064 if dl.kind == 1 { 2065 out_str(o, " li t6, 0\n") 2066 emit_sp_sd(o, "t6" as *u8, dl.idx) 2067 } 2068 return 0 2069} 2070 2071// ---- i8x32 horizontal sum -> i64 (widening reduce) ---- 2072// vwredsum.vs widens i8 -> i16 lanes during accumulation, ensures 2073// 32-lane sum can't overflow (max |sum| = 32 * 127 = 4064, fits 2074// in i16 range -32768..32767). Sign-extend i16 -> i64 on extract. 2075func rv_emit_simd_vreduce_sum_i8(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 2076 materialise(f, locs, o, i.op0, "t4") 2077 out_str(o, " li t6, 32\n") 2078 out_str(o, " .word 0x0c0fffd7\n") // vsetvli e8 m1 2079 out_str(o, " .word 0x020e8087\n") // vle8.v v1, (t4) 2080 // Init e16 m1 accumulator v6 = 0. 2081 out_str(o, " li t6, 1\n") 2082 out_str(o, " .word 0x0c8fffd7\n") // vsetvli e16 m1 2083 out_str(o, " .word 0x5e003357\n") // vmv.v.i v6, 0 2084 // Back to e8 m1 for widening reduce. 2085 out_str(o, " li t6, 32\n") 2086 out_str(o, " .word 0x0c0fffd7\n") // vsetvli e8 m1 2087 out_str(o, " .word 0xc6130357\n") // vwredsum.vs v6, v1, v6 2088 // Extract i16 scalar in e16 m1, sign-extend i16 -> i64. 2089 out_str(o, " li t6, 1\n") 2090 out_str(o, " .word 0x0c8fffd7\n") // vsetvli e16 m1 2091 out_str(o, " .word 0x42602fd7\n") // vmv.x.s t6, v6 2092 out_str(o, " slli t6, t6, 48\n") 2093 out_str(o, " srai t6, t6, 48\n") 2094 let dl: *ValueLoc = loc_at(locs, i.result) 2095 if dl.kind == 0 { 2096 out_str(o, " mv ") 2097 reg_name(o, dl.idx) 2098 out_str(o, ", t6\n") 2099 } 2100 if dl.kind == 1 { 2101 emit_sp_sd(o, "t6" as *u8, dl.idx) 2102 } 2103 return 0 2104} 2105 2106// ---- i8x32 scalar broadcast ---- 2107// vmv.v.x v3, t4 in e8 SEW writes low 8 bits of t4 into all 32 lanes. 2108func rv_emit_simd_vbroadcast_i8(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 2109 materialise(f, locs, o, i.op0, "t4") // scalar into t4 (rs1) 2110 out_str(o, " li t6, 32\n") 2111 out_str(o, " .word 0x0c0fffd7\n") // vsetvli e8 m1 2112 out_str(o, " .word 0x5e0ec1d7\n") // vmv.v.x v3, t4 2113 materialise(f, locs, o, i.op1, "t4") // out ptr 2114 out_str(o, " .word 0x020e81a7\n") // vse8.v v3, (t4) 2115 let dl: *ValueLoc = loc_at(locs, i.result) 2116 if dl.kind == 0 { 2117 out_str(o, " li ") 2118 reg_name(o, dl.idx) 2119 out_str(o, ", 0\n") 2120 } 2121 if dl.kind == 1 { 2122 out_str(o, " li t6, 0\n") 2123 emit_sp_sd(o, "t6" as *u8, dl.idx) 2124 } 2125 return 0 2126} 2127 2128// ---- i32x8 generic per-lane vbinop ---- 2129// vsetvli e32 m1 avl=8 -> VL=8 (32-bit lanes in 256-bit vector). 2130// vle32.v / vse32.v for load/store; arith encodings are SEW-agnostic. 2131func rv_emit_simd_vbinop_i32(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr, enc_word: i64) -> i64 { 2132 materialise(f, locs, o, i.op0, "t4") 2133 materialise(f, locs, o, i.op1, "t5") 2134 out_str(o, " li t6, 8\n") 2135 out_str(o, " .word 0x0d0fffd7\n") // vsetvli e32 m1 2136 out_str(o, " .word 0x020ee087\n") // vle32.v v1, (t4) 2137 out_str(o, " .word 0x020f6107\n") // vle32.v v2, (t5) 2138 out_str(o, " .word ") 2139 out_i64(o, enc_word) // <op>.vv v3, v1, v2 2140 out_char(o, 0x0A) 2141 materialise(f, locs, o, i.op2, "t4") 2142 out_str(o, " .word 0x020ee1a7\n") // vse32.v v3, (t4) 2143 let dl: *ValueLoc = loc_at(locs, i.result) 2144 if dl.kind == 0 { 2145 out_str(o, " li ") 2146 reg_name(o, dl.idx) 2147 out_str(o, ", 0\n") 2148 } 2149 if dl.kind == 1 { 2150 out_str(o, " li t6, 0\n") 2151 emit_sp_sd(o, "t6" as *u8, dl.idx) 2152 } 2153 return 0 2154} 2155 2156// ---- i32x8 horizontal sum -> i64 (widening reduce) ---- 2157// vwredsum.vs widens e32 -> e64 accumulator. No overflow for 2158// any 8-lane i32 sum (max |sum| = 8 * 2^31 ~ 2^34, fits i64). 2159func rv_emit_simd_vreduce_sum_i32(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 2160 materialise(f, locs, o, i.op0, "t4") 2161 out_str(o, " li t6, 8\n") 2162 out_str(o, " .word 0x0d0fffd7\n") // vsetvli e32 m1 2163 out_str(o, " .word 0x020ee087\n") // vle32.v v1, (t4) 2164 // Init e64 m1 accumulator v6 = 0. 2165 out_str(o, " li t6, 1\n") 2166 out_str(o, " .word 0x0d8fffd7\n") // vsetvli e64 m1 2167 out_str(o, " .word 0x5e003357\n") // vmv.v.i v6, 0 2168 // Back to e32 m1 for widening reduce. 2169 out_str(o, " li t6, 8\n") 2170 out_str(o, " .word 0x0d0fffd7\n") // vsetvli e32 m1 2171 out_str(o, " .word 0xc6130357\n") // vwredsum.vs v6, v1, v6 2172 // Extract i64 scalar in e64 m1 -- already correct width, no sign-ext needed. 2173 out_str(o, " li t6, 1\n") 2174 out_str(o, " .word 0x0d8fffd7\n") // vsetvli e64 m1 2175 out_str(o, " .word 0x42602fd7\n") // vmv.x.s t6, v6 2176 let dl: *ValueLoc = loc_at(locs, i.result) 2177 if dl.kind == 0 { 2178 out_str(o, " mv ") 2179 reg_name(o, dl.idx) 2180 out_str(o, ", t6\n") 2181 } 2182 if dl.kind == 1 { 2183 emit_sp_sd(o, "t6" as *u8, dl.idx) 2184 } 2185 return 0 2186} 2187 2188// ---- i32x8 scalar broadcast ---- 2189func rv_emit_simd_vbroadcast_i32(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 2190 materialise(f, locs, o, i.op0, "t4") 2191 out_str(o, " li t6, 8\n") 2192 out_str(o, " .word 0x0d0fffd7\n") // vsetvli e32 m1 2193 out_str(o, " .word 0x5e0ec1d7\n") // vmv.v.x v3, t4 2194 materialise(f, locs, o, i.op1, "t4") 2195 out_str(o, " .word 0x020ee1a7\n") // vse32.v v3, (t4) 2196 let dl: *ValueLoc = loc_at(locs, i.result) 2197 if dl.kind == 0 { 2198 out_str(o, " li ") 2199 reg_name(o, dl.idx) 2200 out_str(o, ", 0\n") 2201 } 2202 if dl.kind == 1 { 2203 out_str(o, " li t6, 0\n") 2204 emit_sp_sd(o, "t6" as *u8, dl.idx) 2205 } 2206 return 0 2207} 2208 2209// ---- i64x4 generic per-lane vbinop ---- 2210func rv_emit_simd_vbinop_i64(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr, enc_word: i64) -> i64 { 2211 materialise(f, locs, o, i.op0, "t4") 2212 materialise(f, locs, o, i.op1, "t5") 2213 out_str(o, " li t6, 4\n") 2214 out_str(o, " .word 0x0d8fffd7\n") // vsetvli e64 m1 2215 out_str(o, " .word 0x020ef087\n") // vle64.v v1, (t4) 2216 out_str(o, " .word 0x020f7107\n") // vle64.v v2, (t5) 2217 out_str(o, " .word ") 2218 out_i64(o, enc_word) 2219 out_char(o, 0x0A) 2220 materialise(f, locs, o, i.op2, "t4") 2221 out_str(o, " .word 0x020ef1a7\n") // vse64.v v3, (t4) 2222 let dl: *ValueLoc = loc_at(locs, i.result) 2223 if dl.kind == 0 { 2224 out_str(o, " li ") 2225 reg_name(o, dl.idx) 2226 out_str(o, ", 0\n") 2227 } 2228 if dl.kind == 1 { 2229 out_str(o, " li t6, 0\n") 2230 emit_sp_sd(o, "t6" as *u8, dl.idx) 2231 } 2232 return 0 2233} 2234 2235// ---- i64x4 horizontal sum -> i64 (non-widening) ---- 2236func rv_emit_simd_vreduce_sum_i64(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 2237 materialise(f, locs, o, i.op0, "t4") 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 0x5e003357\n") // vmv.v.i v6, 0 2242 out_str(o, " .word 0x02132357\n") // vredsum.vs v6, v1, v6 2243 out_str(o, " .word 0x42602fd7\n") // vmv.x.s t6, v6 2244 let dl: *ValueLoc = loc_at(locs, i.result) 2245 if dl.kind == 0 { 2246 out_str(o, " mv ") 2247 reg_name(o, dl.idx) 2248 out_str(o, ", t6\n") 2249 } 2250 if dl.kind == 1 { 2251 emit_sp_sd(o, "t6" as *u8, dl.idx) 2252 } 2253 return 0 2254} 2255 2256// ---- i64x4 scalar broadcast ---- 2257func rv_emit_simd_vbroadcast_i64(f: *Function, locs: *ValueLoc, o: *OutBuf, i: *Instr) -> i64 { 2258 materialise(f, locs, o, i.op0, "t4") 2259 out_str(o, " li t6, 4\n") 2260 out_str(o, " .word 0x0d8fffd7\n") // vsetvli e64 m1 2261 out_str(o, " .word 0x5e0ec1d7\n") // vmv.v.x v3, t4 2262 materialise(f, locs, o, i.op1, "t4") 2263 out_str(o, " .word 0x020ef1a7\n") // vse64.v v3, (t4) 2264 let dl: *ValueLoc = loc_at(locs, i.result) 2265 if dl.kind == 0 { 2266 out_str(o, " li ") 2267 reg_name(o, dl.idx) 2268 out_str(o, ", 0\n") 2269 } 2270 if dl.kind == 1 { 2271 out_str(o, " li t6, 0\n") 2272 emit_sp_sd(o, "t6" as *u8, dl.idx) 2273 } 2274 return 0 2275} 2276 2277func emit_instr(f: *Function, locs: *ValueLoc, o: *OutBuf, 2278 i: *Instr, frame_size: i64, ra_slot: i64, fn_name: *u8, 2279 alloca_off: *i64) -> i64 { 2280 let op: i64 = i.op 2281 // arithmetic / logic binops (1..15 minus 9 = NEG) 2282 if op >= 1 { if op <= 15 { if op != 9 { rv_emit_binop(f, locs, o, i); return 0 } } } 2283 // F-extension binops (OP_FADD=50 .. OP_FDIV=53). Register-homed 2284 // operands only in v0.0.1 -- f-reg spill support lands with the 2285 // f-reg allocator. 2286 if op >= OP_FADD { if op <= OP_FDIV { rv_emit_fbinop(f, locs, o, i); return 0 } } 2287 // F-extension casts (int<->float). OP_FCAST_I_TO_F=55, OP_FCAST_F_TO_I=56. 2288 if op == OP_FCAST_I_TO_F { rv_emit_fcast(f, locs, o, i); return 0 } 2289 if op == OP_FCAST_F_TO_I { rv_emit_fcast(f, locs, o, i); return 0 } 2290 // RVV vector binops (OP_VADD=80 .. OP_VFDIV=87). All operands 2291 // + result must be v-reg-homed; allocator pass is future work. 2292 if op >= OP_VADD { if op <= OP_VFDIV { rv_emit_vbinop(f, locs, o, i); return 0 } } 2293 // Width-specific SIMD: widening dot product i16x16 -> i64. 2294 // v0.0.1 shape: op0 = *i64 to packed 16 i16 lanes (4 words), 2295 // op1 = *i64 same. Lowers to vsetvli + vle16 + vwmul + vwredsum 2296 // chain, returns i64 scalar. 2297 if op == OP_SIMD_VDOT_I16_X16 { 2298 rv_emit_simd_vdot_i16(f, locs, o, i) 2299 return 0 2300 } 2301 if op == OP_SIMD_VREDUCE_MIN_I16_X16 { 2302 rv_emit_simd_vreduce_minmax_i16(f, locs, o, i, 0) 2303 return 0 2304 } 2305 if op == OP_SIMD_VREDUCE_MAX_I16_X16 { 2306 rv_emit_simd_vreduce_minmax_i16(f, locs, o, i, 1) 2307 return 0 2308 } 2309 if op == OP_SIMD_VSADD_I16_X16 { 2310 rv_emit_simd_vbinop_i16(f, locs, o, i, 0x861101d7) // vsadd.vv 2311 return 0 2312 } 2313 if op == OP_SIMD_VSSUB_I16_X16 { 2314 rv_emit_simd_vbinop_i16(f, locs, o, i, 0x8e1101d7) // vssub.vv 2315 return 0 2316 } 2317 if op == OP_SIMD_VSADDU_I16_X16 { 2318 rv_emit_simd_vbinop_i16(f, locs, o, i, 0x821101d7) // vsaddu.vv 2319 return 0 2320 } 2321 if op == OP_SIMD_VSSUBU_I16_X16 { 2322 rv_emit_simd_vbinop_i16(f, locs, o, i, 0x8a1101d7) // vssubu.vv 2323 return 0 2324 } 2325 if op == OP_SIMD_VMIN_LANE_I16_X16 { 2326 rv_emit_simd_vbinop_i16(f, locs, o, i, 0x161101d7) // vmin.vv 2327 return 0 2328 } 2329 if op == OP_SIMD_VMAX_LANE_I16_X16 { 2330 rv_emit_simd_vbinop_i16(f, locs, o, i, 0x1e1101d7) // vmax.vv 2331 return 0 2332 } 2333 if op == OP_SIMD_VADD_LANE_I16_X16 { 2334 rv_emit_simd_vbinop_i16(f, locs, o, i, 0x021101d7) // vadd.vv 2335 return 0 2336 } 2337 if op == OP_SIMD_VSUB_LANE_I16_X16 { 2338 rv_emit_simd_vbinop_i16(f, locs, o, i, 0x0a1101d7) // vsub.vv 2339 return 0 2340 } 2341 if op == OP_SIMD_VMUL_LANE_I16_X16 { 2342 rv_emit_simd_vbinop_i16(f, locs, o, i, 0x961121d7) // vmul.vv 2343 return 0 2344 } 2345 if op == OP_SIMD_VSLL_I16_X16 { 2346 rv_emit_simd_vshift_i16(f, locs, o, i, 0x961ec1d7) // vsll.vx 2347 return 0 2348 } 2349 if op == OP_SIMD_VSRL_I16_X16 { 2350 rv_emit_simd_vshift_i16(f, locs, o, i, 0xa21ec1d7) // vsrl.vx 2351 return 0 2352 } 2353 if op == OP_SIMD_VSRA_I16_X16 { 2354 rv_emit_simd_vshift_i16(f, locs, o, i, 0xa61ec1d7) // vsra.vx 2355 return 0 2356 } 2357 if op == OP_SIMD_VREDUCE_SUM_I16_X16 { 2358 rv_emit_simd_vreduce_sum_i16(f, locs, o, i) 2359 return 0 2360 } 2361 if op == OP_SIMD_VBROADCAST_I16_X16 { 2362 rv_emit_simd_vbroadcast_i16(f, locs, o, i) 2363 return 0 2364 } 2365 // i8x32 dispatch 2366 if op == OP_SIMD_VADD_I8_X32 { 2367 rv_emit_simd_vbinop_i8(f, locs, o, i, 0x021101d7) // vadd.vv 2368 return 0 2369 } 2370 if op == OP_SIMD_VSUB_I8_X32 { 2371 rv_emit_simd_vbinop_i8(f, locs, o, i, 0x0a1101d7) // vsub.vv 2372 return 0 2373 } 2374 if op == OP_SIMD_VSADD_I8_X32 { 2375 rv_emit_simd_vbinop_i8(f, locs, o, i, 0x861101d7) // vsadd.vv 2376 return 0 2377 } 2378 if op == OP_SIMD_VSSUB_I8_X32 { 2379 rv_emit_simd_vbinop_i8(f, locs, o, i, 0x8e1101d7) // vssub.vv 2380 return 0 2381 } 2382 if op == OP_SIMD_VREDUCE_SUM_I8_X32 { 2383 rv_emit_simd_vreduce_sum_i8(f, locs, o, i) 2384 return 0 2385 } 2386 if op == OP_SIMD_VBROADCAST_I8_X32 { 2387 rv_emit_simd_vbroadcast_i8(f, locs, o, i) 2388 return 0 2389 } 2390 // i32x8 dispatch 2391 if op == OP_SIMD_VADD_I32_X8 { 2392 rv_emit_simd_vbinop_i32(f, locs, o, i, 0x021101d7) // vadd.vv 2393 return 0 2394 } 2395 if op == OP_SIMD_VSUB_I32_X8 { 2396 rv_emit_simd_vbinop_i32(f, locs, o, i, 0x0a1101d7) // vsub.vv 2397 return 0 2398 } 2399 if op == OP_SIMD_VMUL_I32_X8 { 2400 rv_emit_simd_vbinop_i32(f, locs, o, i, 0x961121d7) // vmul.vv 2401 return 0 2402 } 2403 if op == OP_SIMD_VSADD_I32_X8 { 2404 rv_emit_simd_vbinop_i32(f, locs, o, i, 0x861101d7) // vsadd.vv 2405 return 0 2406 } 2407 if op == OP_SIMD_VSSUB_I32_X8 { 2408 rv_emit_simd_vbinop_i32(f, locs, o, i, 0x8e1101d7) // vssub.vv 2409 return 0 2410 } 2411 if op == OP_SIMD_VREDUCE_SUM_I32_X8 { 2412 rv_emit_simd_vreduce_sum_i32(f, locs, o, i) 2413 return 0 2414 } 2415 if op == OP_SIMD_VBROADCAST_I32_X8 { 2416 rv_emit_simd_vbroadcast_i32(f, locs, o, i) 2417 return 0 2418 } 2419 // i64x4 dispatch 2420 if op == OP_SIMD_VADD_I64_X4 { 2421 rv_emit_simd_vbinop_i64(f, locs, o, i, 0x021101d7); return 0 2422 } 2423 if op == OP_SIMD_VSUB_I64_X4 { 2424 rv_emit_simd_vbinop_i64(f, locs, o, i, 0x0a1101d7); return 0 2425 } 2426 if op == OP_SIMD_VMUL_I64_X4 { 2427 rv_emit_simd_vbinop_i64(f, locs, o, i, 0x961121d7); return 0 2428 } 2429 if op == OP_SIMD_VSADD_I64_X4 { 2430 rv_emit_simd_vbinop_i64(f, locs, o, i, 0x861101d7); return 0 2431 } 2432 if op == OP_SIMD_VSSUB_I64_X4 { 2433 rv_emit_simd_vbinop_i64(f, locs, o, i, 0x8e1101d7); return 0 2434 } 2435 if op == OP_SIMD_VREDUCE_SUM_I64_X4 { 2436 rv_emit_simd_vreduce_sum_i64(f, locs, o, i); return 0 2437 } 2438 if op == OP_SIMD_VBROADCAST_I64_X4 { 2439 rv_emit_simd_vbroadcast_i64(f, locs, o, i); return 0 2440 } 2441 if op == OP_ALLOCA { rv_emit_alloca(f, locs, o, i, alloca_off); return 0 } 2442 // comparisons: EQ=20, NE=21, LT_S=22, LE_S=23, GT_S=24, GE_S=25 2443 if op >= 20 { if op <= 25 { rv_emit_cmp(f, locs, o, i); return 0 } } 2444 if op == 30 { rv_emit_return(f, locs, o, frame_size, ra_slot, i, fn_name); return 0 } 2445 if op == OP_CALL { rv_emit_call(f, locs, o, i); return 0 } 2446 if op == OP_CALL_INDIRECT { rv_emit_call_indirect(f, locs, o, i); return 0 } 2447 if op == OP_LOAD { rv_emit_load(f, locs, o, i); return 0 } 2448 if op == OP_STORE { rv_emit_store(f, locs, o, i); return 0 } 2449 if op == OP_GEP { rv_emit_gep(f, locs, o, i); return 0 } 2450 if op == OP_ADDR_OF { rv_emit_addr_of(f, locs, o, i); return 0 } 2451 if op == OP_TAIL_CALL { rv_emit_tail_call(f, locs, o, i, frame_size, ra_slot); return 0 } 2452 if op == 31 { emit_branch(f, locs, o, i, fn_name); return 0 } 2453 if op == 32 { emit_branch(f, locs, o, i, fn_name); return 0 } 2454 if op == 40 { 2455 // COPY: just materialise into dst. 2456 materialise(f, locs, o, i.op0, "t4") 2457 let dl: *ValueLoc = loc_at(locs, i.result) 2458 if dl.kind == 0 { 2459 out_str(o, " mv ") 2460 reg_name(o, dl.idx) 2461 out_str(o, ", t4\n") 2462 } 2463 if dl.kind == 1 { 2464 emit_sp_sd(o, "t4" as *u8, dl.idx) 2465 } 2466 return 0 2467 } 2468 // OP_NOT (bitwise one's complement, `~x`). RV64 has no native NOT; 2469 // it is `xori rd, rs, -1` (the 12-bit -1 sign-extends to all-ones). 2470 // Two-is-one cross-arch parity: parse_unary emits OP_NOT for `~` and 2471 // the x86_64 backend lowers it to `notq` -- before this, OP_NOT fell 2472 // through emit_instr unhandled, silently dropping `~` on the RV64 2473 // target (the same silent-codegen class the x86 fix closed). 2474 if op == OP_NOT { 2475 materialise(f, locs, o, i.op0, "t4") 2476 let dl_not: *ValueLoc = loc_at(locs, i.result) 2477 if dl_not.kind == 0 { 2478 out_str(o, " xori ") 2479 reg_name(o, dl_not.idx) 2480 out_str(o, ", t4, -1\n") 2481 } 2482 if dl_not.kind == 1 { 2483 out_str(o, " xori t4, t4, -1\n") 2484 emit_sp_sd(o, "t4" as *u8, dl_not.idx) 2485 } 2486 return 0 2487 } 2488 // OP_RDTSC (`__rdtsc()`): read the cycle counter. Two-is-one parity 2489 // with x86's rdtsc -- RV64 has the `rdcycle rd` pseudo (csrr rd,cycle) 2490 // reading the full 64-bit cycle CSR. The dummy op0 is ignored. 2491 if op == OP_RDTSC { 2492 let dl_tsc: *ValueLoc = loc_at(locs, i.result) 2493 if dl_tsc.kind == 0 { 2494 out_str(o, " rdcycle ") 2495 reg_name(o, dl_tsc.idx) 2496 out_str(o, "\n") 2497 } 2498 if dl_tsc.kind == 1 { 2499 out_str(o, " rdcycle t4\n") 2500 emit_sp_sd(o, "t4" as *u8, dl_tsc.idx) 2501 } 2502 return 0 2503 } 2504 // Kernel intrinsics. Each lowers to a single RV64 instruction. 2505 if op == OP_WFI { 2506 out_str(o, " wfi\n") 2507 return 0 2508 } 2509 if op == OP_FENCE { 2510 out_str(o, " fence rw, rw\n") 2511 return 0 2512 } 2513 if op == OP_MRET { 2514 out_str(o, " mret\n") 2515 return 0 2516 } 2517 if op == OP_CSR_READ { 2518 // op0 = csr number. Result goes into the allocated dst reg 2519 // or spill slot. Emit `csrr <dst>, <csr>`. 2520 let dl_cr: *ValueLoc = loc_at(locs, i.result) 2521 if dl_cr.kind == 0 { 2522 out_str(o, " csrr ") 2523 reg_name(o, dl_cr.idx) 2524 out_str(o, ", ") 2525 out_i64(o, i.op0) 2526 out_char(o, 0x0A) 2527 } 2528 if dl_cr.kind == 1 { 2529 out_str(o, " csrr t4, ") 2530 out_i64(o, i.op0) 2531 out_char(o, 0x0A) 2532 emit_sp_sd(o, "t4" as *u8, dl_cr.idx) 2533 } 2534 return 0 2535 } 2536 if op == OP_CSR_WRITE { 2537 // op0 = csr number (literal), op1 = src value id. 2538 materialise(f, locs, o, i.op1, "t4") 2539 out_str(o, " csrw ") 2540 out_i64(o, i.op0) 2541 out_str(o, ", t4\n") 2542 return 0 2543 } 2544 if op == OP_SYSCALL { 2545 // ECALL: syscall number in a7, args in a0..a5, return in a0. 2546 // op0 = number, op1..op6 = args (up to 6). 2547 materialise(f, locs, o, i.op0, "a7") 2548 if i.n_operands > 1 { materialise(f, locs, o, i.op1, "a0") } 2549 if i.n_operands > 2 { materialise(f, locs, o, i.op2, "a1") } 2550 if i.n_operands > 3 { materialise(f, locs, o, i.op3, "a2") } 2551 if i.n_operands > 4 { materialise(f, locs, o, i.op4, "a3") } 2552 if i.n_operands > 5 { materialise(f, locs, o, i.op5, "a4") } 2553 if i.n_operands > 6 { materialise(f, locs, o, i.op6, "a5") } 2554 out_str(o, " ecall\n") 2555 // Result: a0 -> result location. 2556 let dl_sc: *ValueLoc = loc_at(locs, i.result) 2557 if dl_sc.kind == 0 { 2558 out_str(o, " mv ") 2559 reg_name(o, dl_sc.idx) 2560 out_str(o, ", a0\n") 2561 } 2562 if dl_sc.kind == 1 { 2563 emit_sp_sd(o, "a0" as *u8, dl_sc.idx) 2564 } 2565 return 0 2566 } 2567 out_str(o, " # unhandled op ") 2568 out_i64(o, op) 2569 out_char(o, 0x0A) 2570 return 0 2571} 2572 2573// ---- emit function ---- 2574// 2575// Prologue, per-block label + body, epilogue. Takes a name string 2576// (null-terminated), frame info, locs. 2577 2578func emit_function(f: *Function, locs: *ValueLoc, o: *OutBuf, 2579 fn_name: *u8, 2580 frame_size: i64, ra_slot: i64, 2581 save_mask: i64, save_mask_fpr: i64) -> i64 { 2582 // Frame layout when any saves are present: 2583 // [0 .. spill_bytes) spill slots (regalloc) 2584 // [ra_slot .. ra_slot+8) ra 2585 // [ra_slot+8 .. +N_gpr*8) N_gpr saved s-regs 2586 // [ra_slot+8+N_gpr*8 .. +N_fpr*8) N_fpr saved fs-regs 2587 // frame_size bumped by (N_gpr + N_fpr) * 8 2588 // When both masks are zero, layout is unchanged (frame_size=16, 2589 // ra_slot=8). 2590 // Pre-pass: total alloca bytes. Allocas live below ra at sp+0.., 2591 // so the ra slot must be moved past them or storing param `b` at 2592 // sp+8 (the second alloca's home) clobbers ra. Was a real bug 2593 // on callmin: 16-byte frame with 2 allocas overwrote ra and the 2594 // epilogue's `ld ra; ret` jumped to the param value (4) and 2595 // SIGSEGV'd at addr 4. Hoist the alloca walk here so frame 2596 // layout below sees the correct total. 2597 let alloca_off_raw: *u8 = sys_mmap(f.n_values * 8 + 16) 2598 let alloca_off: *i64 = alloca_off_raw as *i64 2599 // ★BUG 8 ROOT FIX: allocas must sit ABOVE the spill region, not at sp+0. regalloc spills SSA values to 2600 // slots [0..spill_bytes); compute_alloca_offsets was called with spill_bytes=0, so alloca #0 (`acc`) also 2601 // landed at sp+0 and a spilled value at slot 0 overwrote it (TRIPLE-NESTED gave 4 not 24 once a value 2602 // spilled to slot 0). The frame-layout comment above always intended "[0..spill_bytes) spill slots" then 2603 // allocas -- it just passed 0. Recover the true spill_bytes as the max spilled ValueLoc offset + 8 (loc 2604 // kind 1 = VL_SPILLED; idx = slot; idx<0 = never-live, skip) and base the allocas above it. 2605 var spill_bytes: i64 = 0 2606 var sv: i64 = 0 2607 while sv < f.n_values { 2608 let sl: *ValueLoc = loc_at(locs, sv) 2609 if sl.kind == 1 { 2610 if sl.idx >= 0 { 2611 if sl.idx + 8 > spill_bytes { spill_bytes = sl.idx + 8 } 2612 } 2613 } 2614 sv = sv + 1 2615 } 2616 let alloca_total: i64 = compute_alloca_offsets(f, spill_bytes, alloca_off) 2617 2618 // F14 fix: mark every alloca's loc as VL_ALLOCA so materialise() 2619 // rematerialises `addi reg, sp, off` at each use rather than 2620 // relying on a real register home that intermediate compute can 2621 // clobber. compute_alloca_offsets sets alloca_off[v] >= 0 for 2622 // any v that is an OP_ALLOCA result; other values stay at -1. 2623 var av: i64 = 0 2624 while av < f.n_values { 2625 if alloca_off[av] >= 0 { 2626 let al: *ValueLoc = loc_at(locs, av) 2627 al.kind = 3 2628 al.idx = alloca_off[av] 2629 } 2630 av = av + 1 2631 } 2632 2633 let save_count_gpr: i64 = popcount(save_mask) 2634 let save_count_fpr: i64 = popcount(save_mask_fpr) 2635 let save_count_total: i64 = save_count_gpr + save_count_fpr 2636 // Bump ra_slot up if the alloca area would land on top of it. 2637 // Round to 8 for sd alignment. 2638 var ra_slot_actual: i64 = ra_slot 2639 if alloca_total > ra_slot_actual { 2640 ra_slot_actual = (alloca_total + 7) & (0 - 8) 2641 } 2642 let save_base_gpr: i64 = ra_slot_actual + 8 2643 let save_base_fpr: i64 = ra_slot_actual + 8 + save_count_gpr * 8 2644 var actual_frame: i64 = ra_slot_actual + 8 2645 if save_count_total > 0 { 2646 actual_frame = ra_slot_actual + 8 + save_count_total * 8 2647 } 2648 2649 out_str(o, "\n .text\n") 2650 out_str(o, " .globl ") 2651 out_str(o, fn_name) 2652 out_char(o, 0x0A) 2653 // Mark function symbol so GDB classifies + sizes it correctly. 2654 out_str(o, " .type ") 2655 out_str(o, fn_name) 2656 out_str(o, ", @function\n") 2657 out_str(o, fn_name) 2658 out_str(o, ":\n") 2659 // DWARF Call-Frame Info (CFI) directives: tell GDB how to unwind 2660 // our frames. Without these the 'bt' backtrace stops at the 2661 // first frame because GDB doesn't know where we saved ra. 2662 // .cfi_startproc: begin CFI record for this function 2663 // .cfi_def_cfa_offset N: sp+N is the canonical frame address 2664 // (i.e. the caller's sp, which equals the incoming sp before 2665 // our addi sp, sp, -N) 2666 // .cfi_offset ra, -8: ra was spilled at CFA-8 (our sp+ra_slot) 2667 // .cfi_endproc: close the record (emitted at function end) 2668 out_str(o, " .cfi_startproc\n") 2669 emit_sp_adjust(o, 0 - actual_frame) 2670 out_str(o, " .cfi_def_cfa_offset ") 2671 out_i64(o, actual_frame) 2672 out_char(o, 0x0A) 2673 emit_sp_sd(o, "ra" as *u8, ra_slot_actual) 2674 out_str(o, " .cfi_offset ra, ") 2675 out_i64(o, ra_slot_actual - actual_frame) 2676 out_char(o, 0x0A) 2677 if save_count_gpr > 0 { 2678 emit_save_gpr(o, save_mask, save_base_gpr, actual_frame) 2679 } 2680 if save_count_fpr > 0 { 2681 emit_save_fpr(o, save_mask_fpr, save_base_fpr, actual_frame) 2682 } 2683 2684 // Param prologue: RV64 ABI passes params in a0..a7. Regalloc 2685 // assigned each VK_PARAM Value to a register / spill slot from 2686 // the function pool (t0..t6, s0..s11) -- but the function entry 2687 // has the params sitting in a0..a7. Emit a copy from a<idx> 2688 // to wherever regalloc placed the param. Without this, the 2689 // body reads from an uninitialised register and gets garbage 2690 // (the param ABI bug -- previously hit on `add(3,4)` returning 2691 // 3 and on `sys_mmap(8)` calling `mmap(0)`). 2692 // RV64GD ABI: int args go to a0..a7, FP args to fa0..fa7, with 2693 // SEPARATE counters per bank. Walk VAL_PARAMs in pi order 2694 // (parse.nx creates them in source order so pi monotonically 2695 // matches param_index for the prologue). Track int_abi / fp_abi 2696 // separately so a mixed signature like f(int, f64, int) is 2697 // unpacked correctly: a0 -> p0, fa0 -> p1, a1 -> p2. 2698 let lbase_pp: i64 = locs as i64 2699 var int_abi: i64 = 0 2700 var fp_abi: i64 = 0 2701 var pi: i64 = 0 2702 while pi < f.n_values { 2703 let v: *Value = val_at(f, pi) 2704 if v.kind == VK_PARAM { 2705 let pl: *ValueLoc = (lbase_pp + pi * 16) as *ValueLoc 2706 let pidx: i64 = v.param_index 2707 var p_is_fp: i64 = 0 2708 var p_is_d: i64 = 0 2709 if v.ty != (0 as *Type) { 2710 let kt: i64 = v.ty.kind 2711 if kt == TY_F32 { p_is_fp = 1 } 2712 if kt == TY_F64 { p_is_fp = 1; p_is_d = 1 } 2713 } 2714 if pidx >= 0 { 2715 if p_is_fp == 1 { 2716 if fp_abi < 8 { 2717 if pl.kind == VL_REGISTER { 2718 if p_is_d == 1 { 2719 out_str(o, " fmv.d ") 2720 } else { 2721 out_str(o, " fmv.s ") 2722 } 2723 reg_name(o, pl.idx) 2724 out_str(o, ", fa") 2725 out_i64(o, fp_abi) 2726 out_char(o, 0x0A) 2727 } 2728 if pl.kind == VL_SPILLED { 2729 if p_is_d == 1 { 2730 out_str(o, " fsd fa") 2731 } else { 2732 out_str(o, " fsw fa") 2733 } 2734 out_i64(o, fp_abi) 2735 out_str(o, ", ") 2736 out_i64(o, pl.idx) 2737 out_str(o, "(sp)\n") 2738 } 2739 } 2740 fp_abi = fp_abi + 1 2741 } else { 2742 if int_abi < 8 { 2743 if pl.kind == VL_REGISTER { 2744 out_str(o, " mv ") 2745 reg_name(o, pl.idx) 2746 out_str(o, ", a") 2747 out_i64(o, int_abi) 2748 out_char(o, 0x0A) 2749 } 2750 if pl.kind == VL_SPILLED { 2751 out_str(o, " sd a") 2752 out_i64(o, int_abi) 2753 out_str(o, ", ") 2754 out_i64(o, pl.idx) 2755 out_str(o, "(sp)\n") 2756 } 2757 } 2758 int_abi = int_abi + 1 2759 } 2760 } 2761 } 2762 pi = pi + 1 2763 } 2764 2765 // alloca_off was computed above (hoisted so frame layout knew 2766 // the total). No second walk needed here. 2767 var bi: i64 = 0 2768 while bi < f.n_blocks { 2769 let base: i64 = f.blocks as i64 2770 let b: *BasicBlock = (base + bi * 96) as *BasicBlock 2771 out_str(o, ".L") 2772 out_str(o, fn_name) 2773 out_str(o, "_bb") 2774 out_i64(o, b.id) 2775 out_str(o, ":\n") 2776 var inst: *Instr = b.head 2777 while inst != (0 as *Instr) { 2778 emit_instr(f, locs, o, inst, actual_frame, ra_slot_actual, 2779 fn_name, alloca_off) 2780 inst = inst.next 2781 } 2782 bi = bi + 1 2783 } 2784 2785 // Shared epilogue block. All RETURNs in the body jump here via 2786 // `j .L<fn>_epi`. Always emitted (even when save_mask == 0) so 2787 // rv_emit_return has a single uniform codepath; this keeps 2788 // emit_instr's argument list under the nxc2/riscv.c 8-arg ABI 2789 // cap (threading save_mask through would need 9 args). 2790 out_str(o, ".L") 2791 out_str(o, fn_name) 2792 out_str(o, "_epi:\n") 2793 if save_count_fpr > 0 { 2794 emit_restore_fpr(o, save_mask_fpr, save_base_fpr) 2795 } 2796 if save_count_gpr > 0 { 2797 emit_restore_gpr(o, save_mask, save_base_gpr) 2798 } 2799 emit_sp_ld(o, "ra" as *u8, ra_slot_actual) 2800 emit_sp_adjust(o, actual_frame) 2801 out_str(o, " ret\n") 2802 out_str(o, " .cfi_endproc\n") 2803 return 0 2804} 2805 2806// ===== self-test ==================================================== 2807// 2808// Build `func main() -> i64 { return 42 }`, fake a ValueLoc (const 2809// doesn't need one), emit the function, check the output buffer 2810// contains a recognisable snippet. 2811 2812func emit_return_instr(bb: *BasicBlock, v: i64) -> i64 { 2813 let f: *Function = bb.parent 2814 let i: *Instr = alloc_instr(f, 30, ir_type_i64()) 2815 i.n_operands = 1 2816 i.op0 = v 2817 append_instr(bb, i) 2818 return 0 2819} 2820 2821// Library only; self-test lives in riscv_test.nx.