code wiki / (root) / riscv_fp_test.nx

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}