kit

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

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 }