riscv_fp_test.nx source
↩ module page · 266 lines · 10484 B
1// riscv_fp_test.nx -- self-test for rv_emit_fbinop (F-extension).
2//
3// Sanity-check emit_fbinop_mnem and rv_emit_fbinop by hand-building
4// a minimal FADD instruction and checking the emitted asm contains
5// the expected fadd.s mnemonic and register operands. Does NOT
6// attempt to link or execute the fp code path -- the allocator
7// doesn't yet assign f-register homes, so full end-to-end verification
8// waits on the f-reg regalloc commit.
9
10import "syscalls.nx"
11import "types.nx"
12import "ir.nx"
13import "outbuf.nx"
14import "regalloc.nx"
15import "riscv.nx"
16
17// Search buf[0..n] for needle (null-terminated). Returns 1 if found.
18func buf_contains(buf: *u8, n: i64, needle: *u8) -> i64 {
19 var needle_len: i64 = 0
20 while needle[needle_len] != 0 { needle_len = needle_len + 1 }
21 if needle_len == 0 { return 1 }
22 var i: i64 = 0
23 while i + needle_len <= n {
24 var j: i64 = 0
25 var hit: i64 = 1
26 while j < needle_len {
27 if buf[i + j] != needle[j] { hit = 0; j = needle_len }
28 j = j + 1
29 }
30 if hit == 1 { return 1 }
31 i = i + 1
32 }
33 return 0
34}
35
36func main() -> i64 {
37 // --- 1: emit_fbinop_mnem basic dispatch ------------------------
38 //
39 // fadd.s / fsub.s / fmul.s / fdiv.s must come out of the
40 // mnemonic lookup for OP_FADD..OP_FDIV + is_double=0.
41 let o1: *OutBuf = out_new(256)
42 emit_fbinop_mnem(o1, OP_FADD, 0)
43 if buf_contains(o1.buf, o1.pos, "fadd.s" as *u8) != 1 {
44 return __syscall(93, 10, 0, 0, 0, 0, 0)
45 }
46 let o2: *OutBuf = out_new(256)
47 emit_fbinop_mnem(o2, OP_FSUB, 0)
48 if buf_contains(o2.buf, o2.pos, "fsub.s" as *u8) != 1 {
49 return __syscall(93, 11, 0, 0, 0, 0, 0)
50 }
51 let o3: *OutBuf = out_new(256)
52 emit_fbinop_mnem(o3, OP_FMUL, 0)
53 if buf_contains(o3.buf, o3.pos, "fmul.s" as *u8) != 1 {
54 return __syscall(93, 12, 0, 0, 0, 0, 0)
55 }
56 let o4: *OutBuf = out_new(256)
57 emit_fbinop_mnem(o4, OP_FDIV, 0)
58 if buf_contains(o4.buf, o4.pos, "fdiv.s" as *u8) != 1 {
59 return __syscall(93, 13, 0, 0, 0, 0, 0)
60 }
61
62 // --- 2: double-precision suffix -------------------------------
63 let o5: *OutBuf = out_new(256)
64 emit_fbinop_mnem(o5, OP_FADD, 1)
65 if buf_contains(o5.buf, o5.pos, "fadd.d" as *u8) != 1 {
66 return __syscall(93, 20, 0, 0, 0, 0, 0)
67 }
68
69 // --- 3: full rv_emit_fbinop path ------------------------------
70 //
71 // Build a one-instruction function: (%0 + %1) with %0/%1/%result
72 // mapped to ft0/ft1/ft2. Call rv_emit_fbinop directly. Result
73 // buffer should contain exactly " fadd.s ft2, ft0, ft1\n".
74 let m_raw: *u8 = sys_mmap(256)
75 let m: *Module = m_raw as *Module
76 m.name = "fp_test" as *u8
77 m.functions = 0 as *Function
78 m.n_functions = 0
79
80 let f: *Function = ir_function_new(m, "fadd_demo" as *u8, 2, ir_type_i64())
81 let b: *BasicBlock = ir_block_new(f)
82 // Use VK_PARAM (not VK_CONST) so fmaterialise follows the
83 // register/spill paths rather than emitting 'li t6 ; fmv.w.x'
84 // inline. Matches the real codegen shape when the allocator
85 // hands out f-reg homes.
86 let v0: i64 = alloc_value(f, VK_PARAM, ir_type_i64())
87 let v1: i64 = alloc_value(f, VK_PARAM, ir_type_i64())
88 // ir_emit_binop allocates a proper result Value and links it to
89 // the Instr. Using it here (rather than raw alloc_instr) means
90 // add.result is a valid fresh value id, not 0.
91 let rid: i64 = ir_emit_binop(b, OP_FADD, v0, v1, ir_type_i64())
92
93 // Retrieve the just-appended Instr to pass to rv_emit_fbinop.
94 let add: *Instr = b.tail
95
96 // ValueLoc table: allocate slots for v0, v1, rid.
97 let locs_raw: *u8 = sys_mmap(256)
98 let locs: *ValueLoc = locs_raw as *ValueLoc
99 let loc0: *ValueLoc = loc_at(locs, v0)
100 loc0.kind = VL_REGISTER
101 loc0.idx = 100 // ft0
102 let loc1: *ValueLoc = loc_at(locs, v1)
103 loc1.kind = VL_REGISTER
104 loc1.idx = 101 // ft1
105 let locR: *ValueLoc = loc_at(locs, rid)
106 locR.kind = VL_REGISTER
107 locR.idx = 102 // ft2
108
109 let o6: *OutBuf = out_new(1024)
110 rv_emit_fbinop(f, locs, o6, add)
111 // Expected shape with uniform scratch materialisation:
112 // fmv.s ft4, ft0 (op0 -> ft4)
113 // fmv.s ft5, ft1 (op1 -> ft5)
114 // fadd.s ft2, ft4, ft5
115 if buf_contains(o6.buf, o6.pos, "fmv.s ft4, ft0" as *u8) != 1 {
116 return __syscall(93, 30, 0, 0, 0, 0, 0)
117 }
118 if buf_contains(o6.buf, o6.pos, "fmv.s ft5, ft1" as *u8) != 1 {
119 return __syscall(93, 31, 0, 0, 0, 0, 0)
120 }
121 if buf_contains(o6.buf, o6.pos, "fadd.s ft2, ft4, ft5" as *u8) != 1 {
122 return __syscall(93, 32, 0, 0, 0, 0, 0)
123 }
124
125 // --- 4: dispatch integration -----------------------------------
126 //
127 // emit_instr() must route OP_FADD to rv_emit_fbinop (not fall
128 // through to rv_emit_binop which would emit "add" instead of
129 // "fadd.s"). Re-use the same IR + locs; call emit_instr.
130 let o7: *OutBuf = out_new(1024)
131 let alloca_off: *i64 = sys_mmap(64) as *i64
132 emit_instr(f, locs, o7, add, 16, 8, "fadd_demo" as *u8, alloca_off)
133 if buf_contains(o7.buf, o7.pos, "fadd.s" as *u8) != 1 {
134 return __syscall(93, 40, 0, 0, 0, 0, 0)
135 }
136 // And confirm no "add ft2" slipped through (integer-add fallback).
137 if buf_contains(o7.buf, o7.pos, "add ft2" as *u8) == 1 {
138 return __syscall(93, 41, 0, 0, 0, 0, 0)
139 }
140
141 // --- 5: spill path --------------------------------------------
142 //
143 // Mark result as spilled (kind=1) at a stack offset; rv_emit_fbinop
144 // should emit `fadd.s ft6, ft4, ft5` followed by `fsw ft6, N(sp)`.
145 locR.kind = VL_SPILLED
146 locR.idx = 64 // arbitrary spill offset
147 let o8: *OutBuf = out_new(1024)
148 rv_emit_fbinop(f, locs, o8, add)
149 if buf_contains(o8.buf, o8.pos, "fadd.s ft6, ft4, ft5" as *u8) != 1 {
150 return __syscall(93, 50, 0, 0, 0, 0, 0)
151 }
152 if buf_contains(o8.buf, o8.pos, "fsw ft6, 64(sp)" as *u8) != 1 {
153 return __syscall(93, 51, 0, 0, 0, 0, 0)
154 }
155
156 // --- 6: spilled-operand materialise (flw path) ----------------
157 //
158 // Mark op0 as spilled at a stack offset, op1 still in ft1, result
159 // back in ft2. rv_emit_fbinop should emit `flw ft4, N(sp)` for
160 // op0's materialise.
161 loc0.kind = VL_SPILLED
162 loc0.idx = 32
163 locR.kind = VL_REGISTER
164 locR.idx = 102
165 let o9: *OutBuf = out_new(1024)
166 rv_emit_fbinop(f, locs, o9, add)
167 if buf_contains(o9.buf, o9.pos, "flw ft4, 32(sp)" as *u8) != 1 {
168 return __syscall(93, 60, 0, 0, 0, 0, 0)
169 }
170 if buf_contains(o9.buf, o9.pos, "fmv.s ft5, ft1" as *u8) != 1 {
171 return __syscall(93, 61, 0, 0, 0, 0, 0)
172 }
173
174 // --- 7: f-reg allocator end-to-end ----------------------------
175 //
176 // Build a fresh Function with TY_F32 values and call the real
177 // regalloc_function. Verify:
178 // a) Each f-typed Value lands in an f-register index (>=100)
179 // b) Scratch indices 104/105/106 (ft4/ft5/ft6) are NOT assigned
180 // c) is_float_value discriminates TY_F32 vs TY_I64
181 let m2_raw: *u8 = sys_mmap(256)
182 let m2: *Module = m2_raw as *Module
183 m2.name = "fpr_alloc_test" as *u8
184 m2.functions = 0 as *Function
185 m2.n_functions = 0
186
187 let f32ty: *Type = ir_type_f32()
188 let f2: *Function = ir_function_new(m2, "fpr_demo" as *u8, 8, f32ty)
189 let b2: *BasicBlock = ir_block_new(f2)
190
191 // Explicitly allocate two f32 params via alloc_value. ir_function_new
192 // does not create params automatically; caller owns the Value pool.
193 let p0_id: i64 = alloc_value(f2, VK_PARAM, f32ty)
194 let p1_id: i64 = alloc_value(f2, VK_PARAM, f32ty)
195
196 // Sanity: is_float_value discriminates.
197 if is_float_value(f2, p0_id) != 1 {
198 return __syscall(93, 70, 0, 0, 0, 0, 0)
199 }
200 if is_float_value(f2, p1_id) != 1 {
201 return __syscall(93, 71, 0, 0, 0, 0, 0)
202 }
203
204 let rid2: i64 = ir_emit_binop(b2, OP_FADD, p0_id, p1_id, f32ty)
205 // ir_emit_binop uses ret_ty; verify that propagated through:
206 if is_float_value(f2, rid2) != 1 {
207 return __syscall(93, 72, 0, 0, 0, 0, 0)
208 }
209
210 // Run the real allocator.
211 let locs2_raw: *u8 = sys_mmap(f2.n_values * 16 + 64)
212 let locs2: *ValueLoc = locs2_raw as *ValueLoc
213 let cs_raw: *u8 = sys_mmap(16)
214 let cs: *i64 = cs_raw as *i64
215 *cs = 0
216 let cs_fpr_raw: *u8 = sys_mmap(16)
217 let cs_fpr: *i64 = cs_fpr_raw as *i64
218 *cs_fpr = 0
219 let sb_raw: *u8 = sys_mmap(16)
220 let sb: *i64 = sb_raw as *i64
221 *sb = 0
222 regalloc_function(f2, locs2, cs, cs_fpr, sb)
223
224 // All three f-values should land in the FPR pool (>=100).
225 let lr: *ValueLoc = loc_at(locs2, rid2)
226 if lr.kind == VL_REGISTER {
227 if is_float_reg(lr.idx) != 1 {
228 return __syscall(93, 80, 0, 0, 0, 0, 0)
229 }
230 // Must not be a scratch slot.
231 if lr.idx == 104 { return __syscall(93, 81, 0, 0, 0, 0, 0) }
232 if lr.idx == 105 { return __syscall(93, 82, 0, 0, 0, 0, 0) }
233 if lr.idx == 106 { return __syscall(93, 83, 0, 0, 0, 0, 0) }
234 }
235
236 // Param locs too.
237 let lp0: *ValueLoc = loc_at(locs2, p0_id)
238 if lp0.kind == VL_REGISTER {
239 if is_float_reg(lp0.idx) != 1 {
240 return __syscall(93, 84, 0, 0, 0, 0, 0)
241 }
242 }
243
244 // --- 8: fmaterialise VK_CONST path ----------------------------
245 //
246 // Create a VK_CONST Value with a known fp32 bit pattern
247 // (1.0 = 0x3F800000), call fmaterialise, and assert the output
248 // contains 'li t6, ...' followed by 'fmv.w.x ft4, t6'.
249 let c_fval: i64 = ir_const_i64(f, 0x3F800000)
250 // Bump the const value's .ty to TY_F32 so fmaterialise treats it
251 // as a float bit pattern.
252 let cbase: i64 = f.values as i64
253 let cv: *Value = (cbase + c_fval * 48) as *Value
254 cv.ty = alloc_type(TY_F32, 4, 4)
255
256 let oA: *OutBuf = out_new(256)
257 fmaterialise(f, locs, oA, c_fval, "ft4" as *u8)
258 if buf_contains(oA.buf, oA.pos, "li t6, 1065353216" as *u8) != 1 {
259 return __syscall(93, 90, 0, 0, 0, 0, 0)
260 }
261 if buf_contains(oA.buf, oA.pos, "fmv.w.x ft4, t6" as *u8) != 1 {
262 return __syscall(93, 91, 0, 0, 0, 0, 0)
263 }
264
265 return 0
266}