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