cg_fp_cmp_test.c (10637B)
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 KitCgBuiltinTypes bi = kit_cg_builtin_types(c); 113 KitCgTypeId i32 = bi.id[KIT_CG_BUILTIN_I32]; 114 KitCgTypeId f64 = bi.id[KIT_CG_BUILTIN_F64]; 115 KitCgTypeId f128 = bi.id[KIT_CG_BUILTIN_F128]; 116 KitCgFuncParam params[2]; 117 KitCgFuncResult result; 118 KitCgFuncSig sig; 119 KitCgDecl decl; 120 KitCgSym sym; 121 KitCgLocalAttrs la; 122 KitCgLocal p0, p1; 123 KitCgMemAccess ma; 124 125 memset(params, 0, sizeof params); 126 params[0].type = f64; 127 params[1].type = f64; 128 memset(&result, 0, sizeof result); 129 result.type = i32; 130 memset(&sig, 0, sizeof sig); 131 sig.result = result; 132 sig.params = params; 133 sig.nparams = 2; 134 sig.call_conv = KIT_CG_CC_TARGET_C; 135 136 memset(&decl, 0, sizeof decl); 137 decl.kind = KIT_CG_DECL_FUNC; 138 decl.linkage_name = kit_sym_intern(c, kit_slice_cstr(name)); 139 decl.display_name = decl.linkage_name; 140 decl.type = kit_cg_type_func(c, sig); 141 decl.sym.bind = KIT_SB_GLOBAL; 142 decl.sym.visibility = KIT_CG_VIS_DEFAULT; 143 sym = kit_cg_decl(cg, decl); 144 EXPECT(sym != KIT_CG_SYM_NONE, "%s: decl failed", name); 145 146 kit_cg_func_begin(cg, sym); 147 memset(&la, 0, sizeof la); 148 p0 = kit_cg_param(cg, 0, f64, la); 149 p1 = kit_cg_param(cg, 1, f64, la); 150 151 memset(&ma, 0, sizeof ma); 152 ma.type = f64; 153 ma.align = kit_cg_type_align(c, f64); 154 155 kit_cg_push_local(cg, p0); 156 kit_cg_load(cg, ma); 157 if (use_f128) kit_cg_fpext(cg, f128); 158 kit_cg_push_local(cg, p1); 159 kit_cg_load(cg, ma); 160 if (use_f128) kit_cg_fpext(cg, f128); 161 kit_cg_fp_cmp(cg, op); 162 kit_cg_ret(cg); 163 kit_cg_func_end(cg); 164 } 165 166 /* ---- Execution coverage (in-process interpreter) -------------------- */ 167 168 /* The interpreter captures functions during the optimizer's emit pass, which 169 * only runs at opt_level >= 1 (matching `kit run --no-jit`), and the capture 170 * happens for the non-aarch64 codegen path — so we build for x86-64 here. The 171 * interpreter runs the *target-independent* IR, so the result is the host- 172 * independent semantics of every predicate regardless of this capture target. 173 * 174 * Scalar (f64) only: the f128 path lowers each compare to a soft-float libcall 175 * (__eqtf2/__unordtf2/...), which the bare interpreter cannot resolve without a 176 * linked runtime. The f128 lowering is covered by run_emit (emission) and, end 177 * to end, by the `long double` cases in the parse suite under JIT. */ 178 static void run_exec(void) { 179 KitTargetSpec tgt = 180 kit_unit_target(KIT_ARCH_X86_64, KIT_OS_LINUX, KIT_OBJ_ELF); 181 KitCompiler* c = NULL; 182 KitInterpProgram* pp; 183 KitObjBuilder* ob = NULL; 184 KitCg* cg = NULL; 185 KitCodeOptions opts; 186 const char* tag = "f64"; 187 int i, j, k; 188 char nm[32]; 189 190 if (kit_unit_compiler_new(&g_u, tgt, &c) != KIT_OK || !c) { 191 EXPECT(0, "%s: compiler_new failed", tag); 192 return; 193 } 194 pp = kit_interp_program_new(c); 195 EXPECT(pp != NULL, "%s: interp_program_new failed", tag); 196 kit_interp_program_attach(pp, c); 197 198 EXPECT(kit_obj_builder_new(c, &ob) == KIT_OK && ob, "%s: obj_new", tag); 199 EXPECT(kit_cg_new(c, &cg) == KIT_OK && cg, "%s: cg_new", tag); 200 memset(&opts, 0, sizeof opts); 201 opts.opt_level = 1; /* interp capture requires the optimizer pass */ 202 kit_cg_begin(cg, ob, &opts); 203 204 for (i = 0; i < NPRED; ++i) { 205 snprintf(nm, sizeof nm, "cmp_%s_%d", tag, i); 206 build_cmp_fn(c, cg, nm, PREDS[i].op, /*use_f128=*/0); 207 } 208 EXPECT(kit_cg_finish(cg, NULL) == KIT_OK, "%s: finish", tag); 209 EXPECT(kit_cg_detach(cg) == KIT_OK, "%s: detach", tag); 210 211 for (i = 0; i < NPRED; ++i) { 212 KitInterpFunc* fn; 213 snprintf(nm, sizeof nm, "cmp_%s_%d", tag, i); 214 fn = kit_interp_lookup(pp, kit_slice_cstr(nm)); 215 EXPECT(fn != NULL, "%s: %s not captured", tag, PREDS[i].name); 216 if (!fn) continue; 217 for (j = 0; j < NOPS; ++j) { 218 for (k = 0; k < NOPS; ++k) { 219 uint64_t args[2] = {OPS[j].bits, OPS[k].bits}; 220 int64_t ret = -1; 221 int want = fp_expected(PREDS[i].op, bits_to_double(OPS[j].bits), 222 bits_to_double(OPS[k].bits)); 223 KitInterpStatus s = kit_interp_call_args(pp, fn, args, 2, &ret); 224 EXPECT(s == KIT_INTERP_DONE && (int)ret == want, 225 "%s %s(%s,%s): want %d got %lld (status %d)", tag, PREDS[i].name, 226 OPS[j].desc, OPS[k].desc, want, (long long)ret, (int)s); 227 } 228 } 229 } 230 231 kit_cg_free(cg); 232 kit_obj_builder_free(ob); 233 kit_interp_program_free(pp); 234 kit_compiler_free(c); 235 } 236 237 /* ---- Emission coverage (every backend, no execution) ---------------- */ 238 239 static void run_emit(KitArchKind arch, KitOSKind os, KitObjFmt fmt, 240 const char* tag, int opt_level, int do_f128) { 241 KitTargetSpec tgt = kit_unit_target(arch, os, fmt); 242 KitCompiler* c = NULL; 243 KitObjBuilder* ob = NULL; 244 KitCg* cg = NULL; 245 KitCodeOptions opts; 246 int i; 247 char nm[40]; 248 249 if (kit_unit_compiler_new(&g_u, tgt, &c) != KIT_OK || !c) { 250 EXPECT(0, "%s/O%d: compiler_new failed", tag, opt_level); 251 return; 252 } 253 EXPECT(kit_obj_builder_new(c, &ob) == KIT_OK && ob, "%s: obj_new", tag); 254 EXPECT(kit_cg_new(c, &cg) == KIT_OK && cg, "%s: cg_new", tag); 255 memset(&opts, 0, sizeof opts); 256 opts.opt_level = opt_level; 257 kit_cg_begin(cg, ob, &opts); 258 259 for (i = 0; i < NPRED; ++i) { 260 snprintf(nm, sizeof nm, "emit_%s_o%d_f64_%d", tag, opt_level, i); 261 build_cmp_fn(c, cg, nm, PREDS[i].op, /*use_f128=*/0); 262 if (do_f128) { 263 snprintf(nm, sizeof nm, "emit_%s_o%d_f128_%d", tag, opt_level, i); 264 build_cmp_fn(c, cg, nm, PREDS[i].op, /*use_f128=*/1); 265 } 266 } 267 /* If any backend mishandles a new opcode it panics here (aborting the test); 268 * otherwise the object finalizes cleanly. */ 269 EXPECT(kit_cg_finish(cg, NULL) == KIT_OK, "%s/O%d: finish failed", tag, 270 opt_level); 271 EXPECT(kit_cg_detach(cg) == KIT_OK, "%s/O%d: detach failed", tag, 272 opt_level); 273 274 kit_cg_free(cg); 275 kit_obj_builder_free(ob); 276 kit_compiler_free(c); 277 } 278 279 int main(void) { 280 int opt; 281 kit_unit_init(&g_u); 282 283 /* Execution: public mapping + interp semantics for all 12 scalar predicates. 284 */ 285 run_exec(); 286 287 /* Emission: every native backend lowers every predicate (f64 + f128) without 288 * panicking, at both opt levels. */ 289 for (opt = 0; opt <= 1; ++opt) { 290 run_emit(KIT_ARCH_ARM_64, KIT_OS_LINUX, KIT_OBJ_ELF, "aa64", opt, 1); 291 run_emit(KIT_ARCH_X86_64, KIT_OS_LINUX, KIT_OBJ_ELF, "x64", opt, 1); 292 run_emit(KIT_ARCH_RV64, KIT_OS_LINUX, KIT_OBJ_ELF, "rv64", opt, 1); 293 } 294 /* wasm: O0 only (the O1 optimizer is native-target only), f64 only — 295 * exercises the from-scratch FP eq/ne + unordered arms in the wasm backend 296 * (emit_fp_cmp), which previously had no float eq/ne path at all. */ 297 run_emit(KIT_ARCH_WASM, KIT_OS_WASI, KIT_OBJ_WASM, "wasm", 0, 0); 298 299 kit_unit_summary(&g_u, "cg_fp_cmp_test"); 300 return kit_unit_status(&g_u); 301 }