tiny_inline_test.c (9088B)
1 /* Unit tests for the O1 tiny-function inliner (opt_try_tiny_inline). 2 * 3 * Each case builds a callee and a caller as semantic CG IR via the recorder, 4 * lowers both to the optimizer's pre-machinize Func form with 5 * opt_func_from_cg_ir, then drives opt_try_tiny_inline with a lookup that 6 * resolves the callee symbol. We assert on whether the IR_CALL was replaced. */ 7 8 #include <kit/cg.h> 9 #include <kit/core.h> 10 #include <stdarg.h> 11 #include <stdio.h> 12 #include <stdlib.h> 13 #include <string.h> 14 15 #include "cg/ir.h" 16 #include "cg/ir_recorder.h" 17 #include "lib/kit_unit.h" 18 #include "opt/opt.h" 19 #include "opt/opt_internal.h" 20 21 #undef Operand 22 #undef CGFuncDesc 23 #undef CGParamDesc 24 #undef CGCallDesc 25 #undef CGLocalStorage 26 27 /* Shared test context replaces the per-file heap/diag/counter globals; 28 * EXPECT aliases CU_EXPECT so the call sites are unchanged. kit_unit_init 29 * runs once in main(); tc_init reuses g_u's heap/diag/ctx per compiler. */ 30 static KitUnit g_u; 31 #define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__) 32 33 typedef struct TestCtx { 34 Compiler* c; 35 KitCgTypeId i32; 36 KitCgTypeId ptr; 37 KitCgTypeId fn1; /* i32(i32) */ 38 } TestCtx; 39 40 static void tc_init(TestCtx* tc) { 41 KitTargetSpec target; 42 KitCgFuncSig sig; 43 KitCgFuncParam params[1]; 44 memset(tc, 0, sizeof *tc); 45 target = kit_unit_target(KIT_ARCH_ARM_64, KIT_OS_MACOS, KIT_OBJ_MACHO); 46 if (kit_unit_compiler_new(&g_u, target, (KitCompiler**)&tc->c) != KIT_OK || 47 !tc->c) { 48 fprintf(stderr, "fatal: compiler allocation failed\n"); 49 abort(); 50 } 51 tc->i32 = kit_cg_type_builtin(tc->c, KIT_CG_BUILTIN_I32); 52 tc->ptr = kit_cg_type_ptr(tc->c, 53 kit_cg_type_builtin(tc->c, KIT_CG_BUILTIN_VOID), 0); 54 memset(&sig, 0, sizeof sig); 55 memset(params, 0, sizeof params); 56 params[0].type = tc->i32; 57 KitCgFuncResult sig_result; 58 memset(&sig_result, 0, sizeof sig_result); 59 sig_result.type = tc->i32; 60 sig.result = sig_result; 61 sig.params = params; 62 sig.nparams = 1; 63 sig.call_conv = KIT_CG_CC_TARGET_C; 64 tc->fn1 = kit_cg_type_func((KitCompiler*)tc->c, sig); 65 } 66 67 static void tc_fini(TestCtx* tc) { 68 kit_compiler_free(tc->c); 69 tc->c = NULL; 70 } 71 72 static Operand op_local(CGLocal local, KitCgTypeId type) { 73 Operand o; 74 memset(&o, 0, sizeof o); 75 o.kind = OPK_LOCAL; 76 o.type = type; 77 o.v.local = local; 78 return o; 79 } 80 81 static Operand op_global(ObjSymId sym, KitCgTypeId type) { 82 Operand o; 83 memset(&o, 0, sizeof o); 84 o.kind = OPK_GLOBAL; 85 o.type = type; 86 o.v.global.sym = sym; 87 return o; 88 } 89 90 typedef struct CapturedFunc { 91 CgIrFunc* func; 92 } CapturedFunc; 93 94 static void on_func(void* user, CgIrFunc* func) { 95 ((CapturedFunc*)user)->func = func; 96 } 97 98 static CgTarget* make_recorder(TestCtx* tc, CapturedFunc* cap) { 99 CgIrRecorderConfig cfg; 100 memset(&cfg, 0, sizeof cfg); 101 cfg.func_recorded = on_func; 102 cfg.user = cap; 103 return cg_ir_recorder_new(tc->c, NULL, &cfg); 104 } 105 106 /* Callee: i32 f(i32 x) { acc = x + x; acc = acc + x; ... (nbinops total); 107 * return acc; } -> straightline body of cost == nbinops. */ 108 static CgIrFunc* build_callee(TestCtx* tc, ObjSymId sym, u32 nbinops, 109 KitCgInlinePolicy policy) { 110 CapturedFunc cap; 111 CgTarget* t; 112 CGFuncDesc fd; 113 CGParamDesc pd; 114 CGLocal x, acc; 115 memset(&cap, 0, sizeof cap); 116 t = make_recorder(tc, &cap); 117 memset(&fd, 0, sizeof fd); 118 fd.sym = sym; 119 fd.fn_type = tc->fn1; 120 fd.inline_policy = policy; 121 t->func_begin(t, &fd); 122 memset(&pd, 0, sizeof pd); 123 pd.index = 0; 124 pd.type = tc->i32; 125 pd.size = 4; 126 pd.align = 4; 127 x = t->param(t, &pd); 128 acc = t->local(t, &(CGLocalDesc){.type = tc->i32, .size = 4, .align = 4}); 129 for (u32 i = 0; i < nbinops; ++i) 130 t->binop(t, BO_IADD, op_local(acc, tc->i32), 131 op_local(i == 0 ? x : acc, tc->i32), op_local(x, tc->i32)); 132 t->ret(t, acc); 133 t->func_end(t); 134 return cap.func; 135 } 136 137 /* Caller: i32 g(void) { arg = 41; res = callee(arg); return res; } */ 138 static CgIrFunc* build_caller(TestCtx* tc, ObjSymId callee_sym, 139 KitCgInlinePolicy call_policy) { 140 CapturedFunc cap; 141 CgTarget* t; 142 CGFuncDesc fd; 143 CGCallDesc call; 144 CGLocal arg, res; 145 CGLocal cargs[1]; 146 KitCgFuncSig sig; 147 memset(&cap, 0, sizeof cap); 148 t = make_recorder(tc, &cap); 149 memset(&fd, 0, sizeof fd); 150 fd.sym = callee_sym + 1u; 151 memset(&sig, 0, sizeof sig); 152 KitCgFuncResult sig_result; 153 memset(&sig_result, 0, sizeof sig_result); 154 sig_result.type = tc->i32; 155 sig.result = sig_result; 156 sig.call_conv = KIT_CG_CC_TARGET_C; 157 fd.fn_type = kit_cg_type_func((KitCompiler*)tc->c, sig); 158 t->func_begin(t, &fd); 159 arg = t->local(t, &(CGLocalDesc){.type = tc->i32, .size = 4, .align = 4}); 160 res = t->local(t, &(CGLocalDesc){.type = tc->i32, .size = 4, .align = 4}); 161 t->load_imm(t, op_local(arg, tc->i32), 41); 162 memset(&call, 0, sizeof call); 163 cargs[0] = arg; 164 call.fn_type = tc->fn1; 165 call.callee = op_global(callee_sym, tc->ptr); 166 call.args = cargs; 167 call.nargs = 1; 168 call.result = res; 169 call.inline_policy = call_policy; 170 t->call(t, &call); 171 t->ret(t, res); 172 t->func_end(t); 173 return cap.func; 174 } 175 176 typedef struct LookupCtx { 177 ObjSymId sym; 178 Func* callee; 179 } LookupCtx; 180 181 static Func* lookup(void* ctx, ObjSymId sym) { 182 LookupCtx* l = (LookupCtx*)ctx; 183 return sym == l->sym ? l->callee : NULL; 184 } 185 186 static u32 count_calls(const Func* f) { 187 u32 n = 0; 188 for (u32 b = 0; b < f->nblocks; ++b) 189 for (u32 i = 0; i < f->blocks[b].ninsts; ++i) 190 if ((IROp)f->blocks[b].insts[i].op == IR_CALL) ++n; 191 return n; 192 } 193 194 static u32 count_binops(const Func* f) { 195 u32 n = 0; 196 for (u32 b = 0; b < f->nblocks; ++b) 197 for (u32 i = 0; i < f->blocks[b].ninsts; ++i) 198 if ((IROp)f->blocks[b].insts[i].op == IR_BINOP) ++n; 199 return n; 200 } 201 202 /* A tiny callee is inlined: the call is replaced and the callee's body lands in 203 * the caller. */ 204 static void tiny_callee_is_inlined(void) { 205 TestCtx tc; 206 ObjSymId sym = 1234; 207 tc_init(&tc); 208 Func* callee = opt_func_from_cg_ir( 209 tc.c, build_callee(&tc, sym, 1, KIT_CG_INLINE_DEFAULT)); 210 Func* caller = 211 opt_func_from_cg_ir(tc.c, build_caller(&tc, sym, KIT_CG_INLINE_DEFAULT)); 212 LookupCtx lc = {sym, callee}; 213 EXPECT(count_calls(caller) == 1, "precondition: caller should have one call"); 214 int n = opt_try_tiny_inline(caller, lookup, &lc); 215 EXPECT(n == 1, "expected one inline, got %d", n); 216 EXPECT(count_calls(caller) == 0, "call should be gone, %u remain", 217 count_calls(caller)); 218 EXPECT(count_binops(caller) >= 1, "callee binop should be cloned in"); 219 tc_fini(&tc); 220 } 221 222 /* A DEFAULT callee over the tiny cost cap is refused; the same body marked 223 * always_inline bypasses the cap. */ 224 static void over_budget_respects_policy(void) { 225 TestCtx tc; 226 ObjSymId sym = 2000; 227 tc_init(&tc); 228 /* cost 12 > INLINE_TINY_COST_LIMIT (8). */ 229 Func* big_default = opt_func_from_cg_ir( 230 tc.c, build_callee(&tc, sym, 12, KIT_CG_INLINE_DEFAULT)); 231 Func* caller1 = 232 opt_func_from_cg_ir(tc.c, build_caller(&tc, sym, KIT_CG_INLINE_DEFAULT)); 233 LookupCtx lc1 = {sym, big_default}; 234 int n1 = opt_try_tiny_inline(caller1, lookup, &lc1); 235 EXPECT(n1 == 0, "over-budget DEFAULT callee should be refused, got %d", n1); 236 EXPECT(count_calls(caller1) == 1, "call should survive refusal"); 237 238 Func* big_always = opt_func_from_cg_ir( 239 tc.c, build_callee(&tc, sym, 12, KIT_CG_INLINE_ALWAYS)); 240 Func* caller2 = 241 opt_func_from_cg_ir(tc.c, build_caller(&tc, sym, KIT_CG_INLINE_DEFAULT)); 242 LookupCtx lc2 = {sym, big_always}; 243 int n2 = opt_try_tiny_inline(caller2, lookup, &lc2); 244 EXPECT(n2 == 1, "always_inline should bypass the tiny cap, got %d", n2); 245 EXPECT(count_calls(caller2) == 0, "always_inline call should be inlined"); 246 tc_fini(&tc); 247 } 248 249 /* A NEVER (noinline) callee is refused even when tiny. */ 250 static void never_policy_is_refused(void) { 251 TestCtx tc; 252 ObjSymId sym = 3000; 253 tc_init(&tc); 254 Func* callee = 255 opt_func_from_cg_ir(tc.c, build_callee(&tc, sym, 1, KIT_CG_INLINE_NEVER)); 256 Func* caller = 257 opt_func_from_cg_ir(tc.c, build_caller(&tc, sym, KIT_CG_INLINE_DEFAULT)); 258 LookupCtx lc = {sym, callee}; 259 int n = opt_try_tiny_inline(caller, lookup, &lc); 260 EXPECT(n == 0, "NEVER callee should not be inlined, got %d", n); 261 EXPECT(count_calls(caller) == 1, "NEVER call should survive"); 262 tc_fini(&tc); 263 } 264 265 /* An unresolved (forward-defined) callee is left alone. */ 266 static void unknown_callee_is_skipped(void) { 267 TestCtx tc; 268 ObjSymId sym = 4000; 269 tc_init(&tc); 270 Func* caller = 271 opt_func_from_cg_ir(tc.c, build_caller(&tc, sym, KIT_CG_INLINE_DEFAULT)); 272 LookupCtx lc = {sym, NULL}; /* lookup never returns a body */ 273 int n = opt_try_tiny_inline(caller, lookup, &lc); 274 EXPECT(n == 0, "unresolved callee should be skipped, got %d", n); 275 EXPECT(count_calls(caller) == 1, "unresolved call should survive"); 276 tc_fini(&tc); 277 } 278 279 int main(void) { 280 kit_unit_init(&g_u); 281 g_u.ctx.now = -1; 282 tiny_callee_is_inlined(); 283 over_budget_respects_policy(); 284 never_policy_is_refused(); 285 unknown_callee_is_skipped(); 286 fprintf(stderr, "tiny-inline: %d checks, %d failures\n", g_u.checks, 287 g_u.fails); 288 return kit_unit_status(&g_u); 289 }