kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

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 }