strength_reduce_test.c (9619B)
1 /* strength_reduce_test — asserts the -O0 semantic peephole (src/cg/fold.c) 2 * turns multiply / unsigned-divide / unsigned-remainder by a power-of-two 3 * constant into a shift / and, AND that the shift/and is emitted in its 4 * immediate-operand form (no scratch register spent materializing the count). 5 * 6 * Each case builds a tiny 7 * i64 f(i64 x) { return x <op> <imm>; } 8 * through the public CG API, emits an ELF object, and disassembles its .text. 9 * 10 * Two layers of assertion: 11 * 1. The multiply/divide/remainder instruction disappears for power-of-two 12 * operands and survives for non-power-of-two (and for signed division, 13 * which is deliberately left to -O1+). Checked on mnemonic substrings 14 * ("mul"/"div"/"rem"), robust to per-arch aliasing. 15 * 2. The resulting shift/and carries an immediate operand (x64 "$imm", 16 * aa64 "#imm", rv64 i-suffixed mnemonic) rather than a materialized 17 * register — the NativeDirectTarget imm_legal pass-through (Piece A) plus 18 * the aa64 immediate shift/bitmask encodings (Piece B). 19 * 20 * Run by: make test-cg-api 21 */ 22 23 #include <kit/cg.h> 24 #include <kit/disasm.h> 25 #include <kit/frontend.h> 26 #include <kit/object.h> 27 #include <stdint.h> 28 #include <stdio.h> 29 #include <string.h> 30 31 #include "lib/kit_unit.h" 32 33 static KitUnit g_u; 34 #define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__) 35 36 typedef struct EmitCtx { 37 KitCgIntBinOp op; 38 int64_t imm; 39 KitObjBuilder* ob; 40 } EmitCtx; 41 42 /* Frontend callback: build `i64 f(i64 x) { return x op imm; }` into ctx->ob. */ 43 static KitStatus emit_binop_fn(KitCompiler* c, void* user) { 44 EmitCtx* ctx = (EmitCtx*)user; 45 KitCg* cg = NULL; 46 KitCgTypeId i64_ty; 47 KitCgFuncParam param_desc; 48 KitCgFuncResult sig_result; 49 KitCgFuncSig sig; 50 KitCgDecl decl; 51 KitCgSym sym; 52 KitCgLocalAttrs attrs; 53 KitCgLocal param; 54 KitCodeOptions opts; 55 56 if (kit_obj_builder_new(c, &ctx->ob) != KIT_OK) return KIT_ERR; 57 if (kit_cg_new(c, &cg) != KIT_OK || !cg) return KIT_ERR; 58 memset(&opts, 0, sizeof opts); 59 opts.opt_level = 0; /* the -O0 peephole is the subject under test */ 60 if (kit_cg_begin(cg, ctx->ob, &opts) != KIT_OK) return KIT_ERR; 61 62 i64_ty = kit_cg_type_builtin(c, KIT_CG_BUILTIN_I64); 63 64 memset(¶m_desc, 0, sizeof param_desc); 65 param_desc.type = i64_ty; 66 memset(&sig_result, 0, sizeof sig_result); 67 sig_result.type = i64_ty; 68 memset(&sig, 0, sizeof sig); 69 sig.params = ¶m_desc; 70 sig.nparams = 1; 71 sig.result = sig_result; 72 sig.call_conv = KIT_CG_CC_TARGET_C; 73 74 memset(&decl, 0, sizeof decl); 75 decl.kind = KIT_CG_DECL_FUNC; 76 decl.linkage_name = kit_sym_intern(c, kit_slice_cstr("f")); 77 decl.display_name = decl.linkage_name; 78 decl.type = kit_cg_type_func(c, sig); 79 decl.sym.bind = KIT_SB_GLOBAL; 80 decl.sym.visibility = KIT_CG_VIS_DEFAULT; 81 sym = kit_cg_decl(cg, decl); 82 if (sym == KIT_CG_SYM_NONE) return KIT_ERR; 83 84 kit_cg_func_begin(cg, sym); 85 memset(&attrs, 0, sizeof attrs); 86 attrs.name = kit_sym_intern(c, KIT_SLICE_LIT("x")); 87 param = kit_cg_param(cg, 0, i64_ty, attrs); 88 if (param == KIT_CG_LOCAL_NONE) return KIT_ERR; 89 90 /* return x <op> imm; */ 91 kit_cg_push_local(cg, param); 92 kit_cg_load(cg, (KitCgMemAccess){.type = i64_ty, 93 .align = kit_cg_type_align(c, i64_ty)}); 94 kit_cg_push_int(cg, (uint64_t)ctx->imm, i64_ty); 95 kit_cg_int_binop(cg, ctx->op, KIT_CG_INTOP_NONE); 96 kit_cg_ret(cg); 97 kit_cg_func_end(cg); 98 99 if (kit_cg_finish(cg, NULL) != KIT_OK) return KIT_ERR; 100 if (kit_cg_detach(cg) != KIT_OK) return KIT_ERR; 101 kit_cg_free(cg); 102 return KIT_OK; 103 } 104 105 typedef struct DisInsn { 106 char mnem[16]; 107 char ops[64]; 108 } DisInsn; 109 110 static void slice_to_buf(KitSlice s, char* buf, size_t cap) { 111 size_t n = s.len < cap - 1 ? s.len : cap - 1; 112 if (s.s && n) memcpy(buf, s.s, n); 113 buf[n] = '\0'; 114 } 115 116 /* Build the op and disassemble its .text into `out` (up to `cap` insns). 117 * Returns the instruction count, or -1 on harness failure. */ 118 static int op_disasm(KitArchKind arch, KitCgIntBinOp op, int64_t imm, 119 DisInsn* out, int cap) { 120 KitTargetSpec target = kit_unit_target(arch, KIT_OS_LINUX, KIT_OBJ_ELF); 121 KitTargetOptions target_opts; 122 KitTarget* kt = NULL; 123 KitCompiler* c = NULL; 124 EmitCtx ctx; 125 KitWriter* writer = NULL; 126 KitObjFile* file = NULL; 127 KitSlice bytes; 128 KitObjSection text_sec; 129 const uint8_t* data = NULL; 130 size_t len = 0; 131 KitDisasmContext dc; 132 KitDisasmIter* it = NULL; 133 KitInsn insn; 134 int result = -1; 135 int n = 0; 136 137 memset(&ctx, 0, sizeof ctx); 138 ctx.op = op; 139 ctx.imm = imm; 140 141 memset(&target_opts, 0, sizeof target_opts); 142 target_opts.spec = target; 143 if (kit_target_new(&g_u.ctx, &target_opts, &kt) != KIT_OK || !kt) return -1; 144 if (kit_compiler_new(kt, &g_u.ctx, &c) != KIT_OK || !c) goto done; 145 if (kit_frontend_run(c, emit_binop_fn, &ctx) != KIT_OK) goto done; 146 if (kit_writer_mem(&g_u.heap, &writer) != KIT_OK || !writer) goto done; 147 if (kit_obj_builder_emit(ctx.ob, writer) != KIT_OK) goto done; 148 bytes.data = kit_writer_mem_bytes(writer, &len); 149 bytes.len = len; 150 if (kit_obj_open(&g_u.ctx, KIT_SLICE_LIT("<sr-test>"), &bytes, &file) != 151 KIT_OK) 152 goto done; 153 if (kit_obj_section_by_name(file, KIT_SLICE_LIT(".text"), &text_sec) != 154 KIT_OK) 155 goto done; 156 if (kit_obj_section_data(file, text_sec, &data, &len) != KIT_OK) goto done; 157 158 memset(&dc, 0, sizeof dc); 159 dc.target = kt; 160 dc.context = g_u.ctx; 161 if (kit_disasm_iter_new(&dc, data, len, 0, file, &it) != KIT_OK || !it) 162 goto done; 163 while (n < cap && kit_disasm_iter_next(it, &insn) == KIT_ITER_ITEM) { 164 slice_to_buf(insn.mnemonic, out[n].mnem, sizeof out[n].mnem); 165 slice_to_buf(insn.operands, out[n].ops, sizeof out[n].ops); 166 ++n; 167 } 168 result = n; 169 170 done: 171 if (it) kit_disasm_iter_free(it); 172 if (file) kit_obj_free(file); 173 if (writer) kit_writer_close(writer); 174 if (ctx.ob) kit_obj_builder_free(ctx.ob); 175 kit_compiler_free(c); 176 kit_target_free(kt); 177 return result; 178 } 179 180 /* 1 if any instruction's mnemonic contains `mnem`. */ 181 static int have_mnem(const DisInsn* in, int n, const char* mnem) { 182 for (int i = 0; i < n; ++i) 183 if (strstr(in[i].mnem, mnem)) return 1; 184 return 0; 185 } 186 187 /* 1 if the first instruction whose mnemonic contains `mnem` has `marker` in its 188 * operands (an empty marker matches any). This is how we tell an immediate 189 * operand form (x64 "$", aa64 "#") from a register form, and — via the 190 * i-suffixed RISC-V mnemonics — picks out slli/srli/andi over sll/srl/and. */ 191 static int imm_form(const DisInsn* in, int n, const char* mnem, 192 const char* marker) { 193 for (int i = 0; i < n; ++i) 194 if (strstr(in[i].mnem, mnem)) return strstr(in[i].ops, marker) != NULL; 195 return 0; 196 } 197 198 typedef struct ArchExpect { 199 KitArchKind arch; 200 const char* name; 201 const char* shl_mnem; /* immediate shift-left mnemonic (x*2^k) */ 202 const char* shr_mnem; /* immediate logical shift-right (x u/ 2^k) */ 203 const char* and_mnem; /* immediate bitwise-and (x u% 2^k) */ 204 const char* mark; /* operand marker for an immediate ("" if in mnemonic) */ 205 } ArchExpect; 206 207 static void check_arch(const ArchExpect* ex) { 208 const char* an = ex->name; 209 DisInsn buf[128]; 210 int n; 211 212 /* --- multiply --- */ 213 n = op_disasm(ex->arch, KIT_CG_INT_MUL, 8, buf, 128); 214 EXPECT(n > 0, "[%s] mul8 disasm failed", an); 215 EXPECT(!have_mnem(buf, n, "mul"), 216 "[%s] x * 8 should strength-reduce to a shift (no 'mul')", an); 217 EXPECT(imm_form(buf, n, ex->shl_mnem, ex->mark), 218 "[%s] x * 8 should use the immediate shift form", an); 219 220 n = op_disasm(ex->arch, KIT_CG_INT_MUL, 7, buf, 128); 221 EXPECT(n > 0 && have_mnem(buf, n, "mul"), "[%s] x * 7 must stay a multiply", 222 an); 223 224 /* --- unsigned divide --- */ 225 n = op_disasm(ex->arch, KIT_CG_INT_UDIV, 8, buf, 128); 226 EXPECT(n > 0, "[%s] udiv8 disasm failed", an); 227 EXPECT(!have_mnem(buf, n, "div"), 228 "[%s] x u/ 8 should strength-reduce to a shift (no 'div')", an); 229 EXPECT(imm_form(buf, n, ex->shr_mnem, ex->mark), 230 "[%s] x u/ 8 should use the immediate shift form", an); 231 232 n = op_disasm(ex->arch, KIT_CG_INT_UDIV, 7, buf, 128); 233 EXPECT(n > 0 && have_mnem(buf, n, "div"), "[%s] x u/ 7 must stay a divide", 234 an); 235 236 /* --- unsigned remainder --- */ 237 n = op_disasm(ex->arch, KIT_CG_INT_UREM, 8, buf, 128); 238 EXPECT(n > 0, "[%s] urem8 disasm failed", an); 239 EXPECT(!have_mnem(buf, n, "div") && !have_mnem(buf, n, "rem"), 240 "[%s] x u%% 8 should strength-reduce to an and (no 'div'/'rem')", an); 241 EXPECT(imm_form(buf, n, ex->and_mnem, ex->mark), 242 "[%s] x u%% 8 should use the immediate and form", an); 243 244 n = op_disasm(ex->arch, KIT_CG_INT_UREM, 7, buf, 128); 245 EXPECT(n > 0 && (have_mnem(buf, n, "div") || have_mnem(buf, n, "rem")), 246 "[%s] x u%% 7 must stay a divide/remainder", an); 247 248 /* --- signed divide: deliberately left for the optimizer --- */ 249 n = op_disasm(ex->arch, KIT_CG_INT_SDIV, 8, buf, 128); 250 EXPECT(n > 0 && have_mnem(buf, n, "div"), 251 "[%s] signed x / 8 is left as a divide at -O0", an); 252 } 253 254 int main(void) { 255 /* x64 AT&T immediates read "$N"; aa64 reads "#N"; rv64 folds the immediate 256 * into the mnemonic (slli/srli/andi), so its operand marker is empty. */ 257 static const ArchExpect arches[] = { 258 {KIT_ARCH_X86_64, "x86_64", "shl", "shr", "and", "$"}, 259 {KIT_ARCH_ARM_64, "aarch64", "lsl", "lsr", "and", "#"}, 260 {KIT_ARCH_RV64, "riscv64", "slli", "srli", "andi", ""}, 261 }; 262 kit_unit_init(&g_u); 263 g_u.ctx.now = -1; 264 for (size_t i = 0; i < sizeof arches / sizeof arches[0]; ++i) 265 check_arch(&arches[i]); 266 kit_unit_summary(&g_u, "strength_reduce_test"); 267 return kit_unit_status(&g_u); 268 }