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