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