cg_fp_cmp_test.c (10710B)
1 /* cg_fp_cmp_test — drives every public KitCgFpCmpOp predicate through 2 * kit_cg_fp_cmp. The six discriminating predicates (ULT/ULE/UGT/UGE/UEQ/ONE) 3 * cannot be produced by any C or toy source — they are reachable *only* through 4 * this entry point — so this is the guard against the "advertise-but-ignore" 5 * gap the public surface used to have (api_map_fp_cmp collapsed all 12 down to 6 * 6, dropping the ordered/unordered distinction before any backend saw it). 7 * 8 * Two kinds of coverage: 9 * 10 * Execution — build `int f(double,double){ return pred(a,b); }` (and an f128 11 * variant that fpext's the operands to long double), capture each into an 12 * in-process interpreter, and assert per-predicate results for a spread of 13 * NaN / ordinary / signed-zero operands. Host-independent: the interpreter 14 * runs the target-independent IR, so this validates the public->internal 15 * mapping and the engine for all 12 predicates, scalar and f128 (§6). 16 * 17 * Emission — build the same functions for every native and special backend 18 * (aarch64 / x86-64 / riscv64 / wasm32 / C-source) at -O0 and -O1 and 19 * finalize the object. A backend that mishandles a new unordered opcode 20 * panics, which aborts (and fails) the test — this is what catches the 21 * wasm FP-eq/ne gap (§5) and the silent default-arm dispatch tables. 22 * 23 * Run by: make test-cg-api 24 */ 25 26 #include <kit/cg.h> 27 #include <kit/core.h> 28 #include <kit/interp.h> 29 #include <kit/object.h> 30 #include <stdint.h> 31 #include <stdio.h> 32 #include <string.h> 33 34 #include "lib/kit_unit.h" 35 36 static KitUnit g_u; 37 #define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__) 38 39 /* ---- the 12 predicates, in KitCgFpCmpOp order ---------------------- */ 40 41 typedef struct { 42 KitCgFpCmpOp op; 43 const char* name; 44 } Pred; 45 46 static const Pred PREDS[] = { 47 {KIT_CG_FP_OEQ, "oeq"}, {KIT_CG_FP_ONE, "one"}, {KIT_CG_FP_OLT, "olt"}, 48 {KIT_CG_FP_OLE, "ole"}, {KIT_CG_FP_OGT, "ogt"}, {KIT_CG_FP_OGE, "oge"}, 49 {KIT_CG_FP_UEQ, "ueq"}, {KIT_CG_FP_UNE, "une"}, {KIT_CG_FP_ULT, "ult"}, 50 {KIT_CG_FP_ULE, "ule"}, {KIT_CG_FP_UGT, "ugt"}, {KIT_CG_FP_UGE, "uge"}, 51 }; 52 enum { NPRED = (int)(sizeof PREDS / sizeof PREDS[0]) }; 53 54 /* IEEE-correct reference result (mirrors src/interp/engine.c do_cmp). */ 55 static int fp_expected(KitCgFpCmpOp op, double a, double b) { 56 int uno = (a != a) || (b != b); /* either operand is NaN */ 57 switch (op) { 58 case KIT_CG_FP_OEQ: 59 return a == b; 60 case KIT_CG_FP_ONE: 61 return !uno && (a != b); 62 case KIT_CG_FP_OLT: 63 return a < b; 64 case KIT_CG_FP_OLE: 65 return a <= b; 66 case KIT_CG_FP_OGT: 67 return a > b; 68 case KIT_CG_FP_OGE: 69 return a >= b; 70 case KIT_CG_FP_UEQ: 71 return uno || (a == b); 72 case KIT_CG_FP_UNE: 73 return a != b; 74 case KIT_CG_FP_ULT: 75 return uno || (a < b); 76 case KIT_CG_FP_ULE: 77 return uno || (a <= b); 78 case KIT_CG_FP_UGT: 79 return uno || (a > b); 80 case KIT_CG_FP_UGE: 81 return uno || (a >= b); 82 } 83 return 0; 84 } 85 86 /* ---- operand spread: bit patterns so NaN/-0.0 are exact ------------- */ 87 88 typedef struct { 89 uint64_t bits; 90 const char* desc; 91 } Operand64; 92 93 static const Operand64 OPS[] = { 94 {0x7ff8000000000000ull, "nan"}, {0x3ff0000000000000ull, "1.0"}, 95 {0x4000000000000000ull, "2.0"}, {0x8000000000000000ull, "-0.0"}, 96 {0x0000000000000000ull, "0.0"}, 97 }; 98 enum { NOPS = (int)(sizeof OPS / sizeof OPS[0]) }; 99 100 static double bits_to_double(uint64_t b) { 101 double d; 102 memcpy(&d, &b, sizeof d); 103 return d; 104 } 105 106 /* ---- build `int f(double,double){ return pred(a,b); }` -------------- * 107 * When use_f128 is set the loaded f64 operands are fpext'd to long double 108 * before the compare, exercising the soft-float libcall path. Returns the 109 * declared symbol; the function is captured by `name` for interp lookup. */ 110 static void build_cmp_fn(KitCompiler* c, KitCg* cg, const char* name, 111 KitCgFpCmpOp op, int use_f128) { 112 KitCgTypeId i32 = kit_cg_type_builtin(c, KIT_CG_BUILTIN_I32); 113 KitCgTypeId f64 = kit_cg_type_builtin(c, KIT_CG_BUILTIN_F64); 114 KitCgTypeId f128 = kit_cg_type_builtin(c, KIT_CG_BUILTIN_F128); 115 KitCgFuncParam params[2]; 116 KitCgFuncResult result; 117 KitCgFuncSig sig; 118 KitCgDecl decl; 119 KitCgSym sym; 120 KitCgLocalAttrs la; 121 KitCgLocal p0, p1; 122 KitCgMemAccess ma; 123 124 memset(params, 0, sizeof params); 125 params[0].type = f64; 126 params[1].type = f64; 127 memset(&result, 0, sizeof result); 128 result.type = i32; 129 memset(&sig, 0, sizeof sig); 130 sig.result = result; 131 sig.params = params; 132 sig.nparams = 2; 133 sig.call_conv = KIT_CG_CC_TARGET_C; 134 135 memset(&decl, 0, sizeof decl); 136 decl.kind = KIT_CG_DECL_FUNC; 137 decl.linkage_name = kit_sym_intern(c, kit_slice_cstr(name)); 138 decl.display_name = decl.linkage_name; 139 decl.type = kit_cg_type_func(c, sig); 140 decl.sym.bind = KIT_SB_GLOBAL; 141 decl.sym.visibility = KIT_CG_VIS_DEFAULT; 142 sym = kit_cg_decl(cg, decl); 143 EXPECT(sym != KIT_CG_SYM_NONE, "%s: decl failed", name); 144 145 kit_cg_func_begin(cg, sym); 146 memset(&la, 0, sizeof la); 147 p0 = kit_cg_param(cg, 0, f64, la); 148 p1 = kit_cg_param(cg, 1, f64, la); 149 150 memset(&ma, 0, sizeof ma); 151 ma.type = f64; 152 ma.align = kit_cg_type_align(c, f64); 153 154 kit_cg_push_local(cg, p0); 155 kit_cg_load(cg, ma); 156 if (use_f128) kit_cg_fpext(cg, f128); 157 kit_cg_push_local(cg, p1); 158 kit_cg_load(cg, ma); 159 if (use_f128) kit_cg_fpext(cg, f128); 160 kit_cg_fp_cmp(cg, op); 161 kit_cg_ret(cg); 162 kit_cg_func_end(cg); 163 } 164 165 /* ---- Execution coverage (in-process interpreter) -------------------- */ 166 167 /* The interpreter captures functions during the optimizer's emit pass, which 168 * only runs at opt_level >= 1 (matching `kit run --no-jit`), and the capture 169 * happens for the non-aarch64 codegen path — so we build for x86-64 here. The 170 * interpreter runs the *target-independent* IR, so the result is the host- 171 * independent semantics of every predicate regardless of this capture target. 172 * 173 * Scalar (f64) only: the f128 path lowers each compare to a soft-float libcall 174 * (__eqtf2/__unordtf2/...), which the bare interpreter cannot resolve without a 175 * linked runtime. The f128 lowering is covered by run_emit (emission) and, end 176 * to end, by the `long double` cases in the parse suite under JIT. */ 177 static void run_exec(void) { 178 KitTargetSpec tgt = 179 kit_unit_target(KIT_ARCH_X86_64, KIT_OS_LINUX, KIT_OBJ_ELF); 180 KitCompiler* c = NULL; 181 KitInterpProgram* pp; 182 KitObjBuilder* ob = NULL; 183 KitCg* cg = NULL; 184 KitCodeOptions opts; 185 const char* tag = "f64"; 186 int i, j, k; 187 char nm[32]; 188 189 if (kit_unit_compiler_new(&g_u, tgt, &c) != KIT_OK || !c) { 190 EXPECT(0, "%s: compiler_new failed", tag); 191 return; 192 } 193 pp = kit_interp_program_new(c); 194 EXPECT(pp != NULL, "%s: interp_program_new failed", tag); 195 kit_interp_program_attach(pp, c); 196 197 EXPECT(kit_obj_builder_new(c, &ob) == KIT_OK && ob, "%s: obj_new", tag); 198 EXPECT(kit_cg_new(c, &cg) == KIT_OK && cg, "%s: cg_new", tag); 199 memset(&opts, 0, sizeof opts); 200 opts.opt_level = 1; /* interp capture requires the optimizer pass */ 201 kit_cg_begin(cg, ob, &opts); 202 203 for (i = 0; i < NPRED; ++i) { 204 snprintf(nm, sizeof nm, "cmp_%s_%d", tag, i); 205 build_cmp_fn(c, cg, nm, PREDS[i].op, /*use_f128=*/0); 206 } 207 EXPECT(kit_cg_finish(cg, NULL) == KIT_OK, "%s: finish", tag); 208 EXPECT(kit_cg_detach(cg) == KIT_OK, "%s: detach", tag); 209 210 for (i = 0; i < NPRED; ++i) { 211 KitInterpFunc* fn; 212 snprintf(nm, sizeof nm, "cmp_%s_%d", tag, i); 213 fn = kit_interp_lookup(pp, kit_slice_cstr(nm)); 214 EXPECT(fn != NULL, "%s: %s not captured", tag, PREDS[i].name); 215 if (!fn) continue; 216 for (j = 0; j < NOPS; ++j) { 217 for (k = 0; k < NOPS; ++k) { 218 uint64_t args[2] = {OPS[j].bits, OPS[k].bits}; 219 int64_t ret = -1; 220 int want = fp_expected(PREDS[i].op, bits_to_double(OPS[j].bits), 221 bits_to_double(OPS[k].bits)); 222 KitInterpStatus s = kit_interp_call_args(pp, fn, args, 2, &ret); 223 EXPECT(s == KIT_INTERP_DONE && (int)ret == want, 224 "%s %s(%s,%s): want %d got %lld (status %d)", tag, PREDS[i].name, 225 OPS[j].desc, OPS[k].desc, want, (long long)ret, (int)s); 226 } 227 } 228 } 229 230 kit_cg_free(cg); 231 kit_obj_builder_free(ob); 232 kit_interp_program_free(pp); 233 kit_compiler_free(c); 234 } 235 236 /* ---- Emission coverage (every backend, no execution) ---------------- */ 237 238 static void run_emit(KitArchKind arch, KitOSKind os, KitObjFmt fmt, 239 const char* tag, int opt_level, int do_f128) { 240 KitTargetSpec tgt = kit_unit_target(arch, os, fmt); 241 KitCompiler* c = NULL; 242 KitObjBuilder* ob = NULL; 243 KitCg* cg = NULL; 244 KitCodeOptions opts; 245 int i; 246 char nm[40]; 247 248 if (arch == KIT_ARCH_WASM) { 249 tgt.ptr_size = 4; 250 tgt.ptr_align = 4; 251 } 252 253 if (kit_unit_compiler_new(&g_u, tgt, &c) != KIT_OK || !c) { 254 EXPECT(0, "%s/O%d: compiler_new failed", tag, opt_level); 255 return; 256 } 257 EXPECT(kit_obj_builder_new(c, &ob) == KIT_OK && ob, "%s: obj_new", tag); 258 EXPECT(kit_cg_new(c, &cg) == KIT_OK && cg, "%s: cg_new", tag); 259 memset(&opts, 0, sizeof opts); 260 opts.opt_level = opt_level; 261 kit_cg_begin(cg, ob, &opts); 262 263 for (i = 0; i < NPRED; ++i) { 264 snprintf(nm, sizeof nm, "emit_%s_o%d_f64_%d", tag, opt_level, i); 265 build_cmp_fn(c, cg, nm, PREDS[i].op, /*use_f128=*/0); 266 if (do_f128) { 267 snprintf(nm, sizeof nm, "emit_%s_o%d_f128_%d", tag, opt_level, i); 268 build_cmp_fn(c, cg, nm, PREDS[i].op, /*use_f128=*/1); 269 } 270 } 271 /* If any backend mishandles a new opcode it panics here (aborting the test); 272 * otherwise the object finalizes cleanly. */ 273 EXPECT(kit_cg_finish(cg, NULL) == KIT_OK, "%s/O%d: finish failed", tag, 274 opt_level); 275 EXPECT(kit_cg_detach(cg) == KIT_OK, "%s/O%d: detach failed", tag, opt_level); 276 277 kit_cg_free(cg); 278 kit_obj_builder_free(ob); 279 kit_compiler_free(c); 280 } 281 282 int main(void) { 283 int opt; 284 kit_unit_init(&g_u); 285 286 /* Execution: public mapping + interp semantics for all 12 scalar predicates. 287 */ 288 run_exec(); 289 290 /* Emission: every native backend lowers every predicate (f64 + f128) without 291 * panicking, at both opt levels. */ 292 for (opt = 0; opt <= 1; ++opt) { 293 run_emit(KIT_ARCH_ARM_64, KIT_OS_LINUX, KIT_OBJ_ELF, "aa64", opt, 1); 294 run_emit(KIT_ARCH_X86_64, KIT_OS_LINUX, KIT_OBJ_ELF, "x64", opt, 1); 295 run_emit(KIT_ARCH_RV64, KIT_OS_LINUX, KIT_OBJ_ELF, "rv64", opt, 1); 296 } 297 /* wasm: O0 only (the O1 optimizer is native-target only), f64 only — 298 * exercises the from-scratch FP eq/ne + unordered arms in the wasm backend 299 * (emit_fp_cmp), which previously had no float eq/ne path at all. */ 300 run_emit(KIT_ARCH_WASM, KIT_OS_WASI, KIT_OBJ_WASM, "wasm", 0, 0); 301 302 kit_unit_summary(&g_u, "cg_fp_cmp_test"); 303 return kit_unit_status(&g_u); 304 }