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