kit

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

native_direct_target_test.c (19235B)


      1 #include "cg/native_direct_target.h"
      2 
      3 #include <kit/core.h>
      4 #include <stdarg.h>
      5 #include <stdio.h>
      6 #include <stdlib.h>
      7 #include <string.h>
      8 
      9 #include "core/arena.h"
     10 #include "lib/kit_unit.h"
     11 
     12 /* Shared test context replaces the per-file heap/diag/counter globals;
     13  * EXPECT aliases CU_EXPECT so the call sites are unchanged. The original
     14  * ctx.now of -1 is preserved (set once in main after kit_unit_init). */
     15 static KitUnit g_u;
     16 #define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__)
     17 
     18 typedef struct TestCtx {
     19   Compiler* c;
     20   KitCgTypeId i32;
     21   KitCgTypeId ptr;
     22 } TestCtx;
     23 
     24 static void tc_init(TestCtx* tc) {
     25   KitTargetSpec target;
     26   KitCgBuiltinTypes b;
     27   memset(tc, 0, sizeof *tc);
     28   target = kit_unit_target(KIT_ARCH_X86_64, KIT_OS_LINUX, KIT_OBJ_ELF);
     29   if (kit_unit_compiler_new(&g_u, target, (KitCompiler**)&tc->c) != KIT_OK ||
     30       !tc->c) {
     31     fprintf(stderr, "fatal: compiler allocation failed\n");
     32     abort();
     33   }
     34   b = kit_cg_builtin_types(tc->c);
     35   tc->i32 = b.id[KIT_CG_BUILTIN_I32];
     36   tc->ptr = kit_cg_type_ptr(tc->c, b.id[KIT_CG_BUILTIN_VOID], 0);
     37 }
     38 
     39 static void tc_fini(TestCtx* tc) {
     40   kit_compiler_free(tc->c);
     41   tc->c = NULL;
     42 }
     43 
     44 typedef enum MockEventKind {
     45   EV_FUNC_BEGIN,
     46   EV_FUNC_END,
     47   EV_FRAME_SLOT,
     48   EV_LABEL_NEW,
     49   EV_LABEL_PLACE,
     50   EV_JUMP,
     51   EV_CMP_BRANCH,
     52   EV_LOAD,
     53   EV_STORE,
     54   EV_LOAD_IMM,
     55   EV_BINOP,
     56   EV_BARRIER,
     57   EV_PLAN_CALL,
     58   EV_EMIT_CALL,
     59   EV_PLAN_RET,
     60   EV_RET,
     61 } MockEventKind;
     62 
     63 typedef struct MockEvent {
     64   u8 kind;
     65   u8 a;
     66   u16 b;
     67   u32 c;
     68 } MockEvent;
     69 
     70 typedef struct MockNative {
     71   NativeTarget base;
     72   NativeFrameSlot next_slot;
     73   MCLabel next_label;
     74   MockEvent events[128];
     75   u32 nevents;
     76   u32 barrier_flags;
     77   u32 last_stack_arg_size;
     78 } MockNative;
     79 
     80 static const Reg mock_int_scratch[] = {1u, 2u, 3u};
     81 static const Reg mock_fp_scratch[] = {4u, 5u};
     82 static const Reg mock_int_allocable[] = {1u, 2u, 3u, 6u};
     83 static const Reg mock_fp_allocable[] = {4u, 5u, 6u};
     84 
     85 static const NativeAllocClassInfo mock_classes[] = {
     86     {.cls = NATIVE_REG_INT,
     87      .allocable = mock_int_allocable,
     88      .nallocable = 4,
     89      .scratch = mock_int_scratch,
     90      .nscratch = 3,
     91      .caller_saved_mask = (1u << 1) | (1u << 2) | (1u << 3),
     92      .arg_mask = 1u << 0,
     93      .ret_mask = 1u << 0},
     94     {.cls = NATIVE_REG_FP,
     95      .allocable = mock_fp_allocable,
     96      .nallocable = 3,
     97      .scratch = mock_fp_scratch,
     98      .nscratch = 2,
     99      .caller_saved_mask = (1u << 4) | (1u << 5),
    100      .arg_mask = 1u << 0,
    101      .ret_mask = 1u << 0},
    102 };
    103 
    104 static const NativeRegInfo mock_reg_info = {
    105     .classes = mock_classes,
    106     .nclasses = sizeof mock_classes / sizeof mock_classes[0],
    107 };
    108 
    109 static MockNative* mock_of(NativeTarget* t) { return (MockNative*)t; }
    110 
    111 static void ev(MockNative* m, MockEventKind kind, u32 a, u32 b, u32 c) {
    112   MockEvent* e;
    113   if (m->nevents >= sizeof m->events / sizeof m->events[0]) abort();
    114   e = &m->events[m->nevents++];
    115   memset(e, 0, sizeof *e);
    116   e->kind = (u8)kind;
    117   e->a = (u8)a;
    118   e->b = (u16)b;
    119   e->c = c;
    120 }
    121 
    122 static NativeAllocClass mock_class_for_type(NativeTarget* t, KitCgTypeId type) {
    123   (void)t;
    124   (void)type;
    125   return NATIVE_REG_INT;
    126 }
    127 
    128 static void mock_func_begin(NativeTarget* t, const CGFuncDesc* fd) {
    129   (void)fd;
    130   ev(mock_of(t), EV_FUNC_BEGIN, 0, 0, 0);
    131 }
    132 
    133 static void mock_func_end(NativeTarget* t) {
    134   ev(mock_of(t), EV_FUNC_END, 0, 0, 0);
    135 }
    136 
    137 static NativeFrameSlot mock_frame_slot(NativeTarget* t,
    138                                        const NativeFrameSlotDesc* d) {
    139   NativeFrameSlot slot = ++mock_of(t)->next_slot;
    140   ev(mock_of(t), EV_FRAME_SLOT, d->kind, d->size, slot);
    141   return slot;
    142 }
    143 
    144 static MCLabel mock_label_new(NativeTarget* t) {
    145   MCLabel label = ++mock_of(t)->next_label;
    146   ev(mock_of(t), EV_LABEL_NEW, 0, 0, label);
    147   return label;
    148 }
    149 
    150 static void mock_label_place(NativeTarget* t, MCLabel label) {
    151   ev(mock_of(t), EV_LABEL_PLACE, 0, 0, label);
    152 }
    153 
    154 static void mock_jump(NativeTarget* t, MCLabel label) {
    155   ev(mock_of(t), EV_JUMP, 0, 0, label);
    156 }
    157 
    158 static void mock_cmp_branch(NativeTarget* t, CmpOp op, NativeLoc a, NativeLoc b,
    159                             MCLabel label) {
    160   EXPECT(a.kind == NATIVE_LOC_REG && b.kind == NATIVE_LOC_REG,
    161          "cmp_branch should receive materialized registers");
    162   ev(mock_of(t), EV_CMP_BRANCH, op, a.v.reg, label);
    163 }
    164 
    165 static void mock_load(NativeTarget* t, NativeLoc dst, NativeAddr addr,
    166                       MemAccess mem) {
    167   EXPECT(dst.kind == NATIVE_LOC_REG, "load destination should be a register");
    168   (void)mem;
    169   ev(mock_of(t), EV_LOAD, addr.base_kind, dst.v.reg, addr.base.frame);
    170 }
    171 
    172 static void mock_store(NativeTarget* t, NativeAddr addr, NativeLoc src,
    173                        MemAccess mem) {
    174   EXPECT(src.kind == NATIVE_LOC_REG, "store source should be a register");
    175   (void)mem;
    176   ev(mock_of(t), EV_STORE, addr.base_kind, src.v.reg, addr.base.frame);
    177 }
    178 
    179 static void mock_load_imm(NativeTarget* t, NativeLoc dst, i64 imm) {
    180   EXPECT(dst.kind == NATIVE_LOC_REG, "load_imm destination should be register");
    181   ev(mock_of(t), EV_LOAD_IMM, dst.v.reg, 0, (u32)imm);
    182 }
    183 
    184 static void mock_binop(NativeTarget* t, BinOp op, NativeLoc dst, NativeLoc a,
    185                        NativeLoc b) {
    186   EXPECT(dst.kind == NATIVE_LOC_REG && a.kind == NATIVE_LOC_REG &&
    187              b.kind == NATIVE_LOC_REG,
    188          "binop operands should be materialized registers");
    189   ev(mock_of(t), EV_BINOP, op, dst.v.reg, (a.v.reg << 16) | b.v.reg);
    190 }
    191 
    192 static void mock_emit_call(NativeTarget* t, const NativeCallPlan* plan) {
    193   EXPECT(plan->callee.kind == NATIVE_LOC_REG,
    194          "frame callee should be materialized for call");
    195   ev(mock_of(t), EV_EMIT_CALL, plan->nargs, plan->nrets, plan->callee.v.reg);
    196 }
    197 
    198 static void mock_plan_ret(NativeTarget* t, const CGFuncDesc* fd,
    199                           const NativeLoc* value,
    200                           NativeCallPlanRet** out_rets, u32* out_nrets) {
    201   NativeCallPlanRet* r;
    202   (void)fd;
    203   r = arena_zarray(t->c->tu, NativeCallPlanRet, 1);
    204   if (value) {
    205     r[0].src = *value;
    206     r[0].dst.kind = NATIVE_LOC_REG;
    207     r[0].dst.cls = NATIVE_REG_INT;
    208     r[0].dst.type = value->type;
    209     r[0].dst.v.reg = 0;
    210     r[0].mem.type = value->type;
    211     r[0].mem.size = 4;
    212     r[0].mem.align = 4;
    213   }
    214   *out_rets = r;
    215   *out_nrets = value ? 1u : 0u;
    216   ev(mock_of(t), EV_PLAN_RET, 0, value ? 1u : 0u, 0);
    217 }
    218 
    219 static void mock_ret(NativeTarget* t) { ev(mock_of(t), EV_RET, 0, 0, 0); }
    220 
    221 static void mock_native_init(MockNative* m, Compiler* c) {
    222   memset(m, 0, sizeof *m);
    223   m->base.c = c;
    224   m->base.regs = &mock_reg_info;
    225   m->base.class_for_type = mock_class_for_type;
    226   m->base.func_begin = mock_func_begin;
    227   m->base.func_end = mock_func_end;
    228   m->base.frame_slot = mock_frame_slot;
    229   m->base.label_new = mock_label_new;
    230   m->base.label_place = mock_label_place;
    231   m->base.jump = mock_jump;
    232   m->base.cmp_branch = mock_cmp_branch;
    233   m->base.load = mock_load;
    234   m->base.store = mock_store;
    235   m->base.load_imm = mock_load_imm;
    236   m->base.binop = mock_binop;
    237   m->base.emit_call = mock_emit_call;
    238   m->base.plan_ret = mock_plan_ret;
    239   m->base.ret = mock_ret;
    240 }
    241 
    242 static void mock_barrier(NativeDirectTarget* d, u32 flags) {
    243   MockNative* m = (MockNative*)d->native;
    244   m->barrier_flags |= flags;
    245   ev(m, EV_BARRIER, 0, 0, flags);
    246 }
    247 
    248 static void mock_plan_call(NativeDirectTarget* d, const NativeCallDesc* desc,
    249                            NativeCallPlan* plan) {
    250   MockNative* m = (MockNative*)d->native;
    251   NativeCallPlanMove* args =
    252       arena_zarray(d->base.c->tu, NativeCallPlanMove, desc->nargs);
    253   NativeCallPlanRet* rets =
    254       arena_zarray(d->base.c->tu, NativeCallPlanRet, desc->nresults);
    255   memset(plan, 0, sizeof *plan);
    256   plan->callee = desc->callee;
    257   plan->args = args;
    258   plan->rets = rets;
    259   plan->nargs = desc->nargs;
    260   plan->nrets = desc->nresults;
    261   plan->stack_arg_size = 24;
    262   for (u32 i = 0; i < desc->nargs; ++i) {
    263     args[i].src = desc->args[i];
    264     args[i].dst.kind = NATIVE_LOC_REG;
    265     args[i].dst.cls = NATIVE_REG_INT;
    266     args[i].dst.type = desc->args[i].type;
    267     args[i].dst.v.reg = i;
    268     args[i].mem.type = desc->args[i].type;
    269     args[i].mem.size = 4;
    270     args[i].mem.align = 4;
    271   }
    272   for (u32 i = 0; i < desc->nresults; ++i) {
    273     rets[i].src.kind = NATIVE_LOC_REG;
    274     rets[i].src.cls = NATIVE_REG_INT;
    275     rets[i].src.type = desc->results[i].type;
    276     rets[i].src.v.reg = i;
    277     rets[i].dst = desc->results[i];
    278     rets[i].mem.type = desc->results[i].type;
    279     rets[i].mem.size = 4;
    280     rets[i].mem.align = 4;
    281   }
    282   m->last_stack_arg_size = plan->stack_arg_size;
    283   ev(m, EV_PLAN_CALL, desc->nargs, desc->nresults, plan->stack_arg_size);
    284 }
    285 
    286 static const NativeOps mock_ops = {
    287     .plan_call = mock_plan_call,
    288     .barrier = mock_barrier,
    289 };
    290 
    291 static Operand op_local(CGLocal local, KitCgTypeId type) {
    292   Operand o;
    293   memset(&o, 0, sizeof o);
    294   o.kind = OPK_LOCAL;
    295   o.type = type;
    296   o.v.local = local;
    297   return o;
    298 }
    299 
    300 static Operand op_imm(i64 value, KitCgTypeId type) {
    301   Operand o;
    302   memset(&o, 0, sizeof o);
    303   o.kind = OPK_IMM;
    304   o.type = type;
    305   o.v.imm = value;
    306   return o;
    307 }
    308 
    309 static CGLocal local_new(CgTarget* t, KitCgTypeId type) {
    310   CGLocalDesc d;
    311   memset(&d, 0, sizeof d);
    312   d.type = type;
    313   d.size = 4;
    314   d.align = 4;
    315   return t->local(t, &d);
    316 }
    317 
    318 static CGLocal local_new_ptr(CgTarget* t, KitCgTypeId type) {
    319   CGLocalDesc d;
    320   memset(&d, 0, sizeof d);
    321   d.type = type;
    322   d.size = 8;
    323   d.align = 8;
    324   return t->local(t, &d);
    325 }
    326 
    327 static Operand op_indirect(CGLocal base, i32 ofs, KitCgTypeId type) {
    328   Operand o;
    329   memset(&o, 0, sizeof o);
    330   o.kind = OPK_INDIRECT;
    331   o.type = type;
    332   o.v.ind.base = base;
    333   o.v.ind.index = CG_LOCAL_NONE;
    334   o.v.ind.ofs = ofs;
    335   return o;
    336 }
    337 
    338 static MemAccess mem_scalar(KitCgTypeId type, u16 flags) {
    339   MemAccess m;
    340   memset(&m, 0, sizeof m);
    341   m.type = type;
    342   m.size = 4;
    343   m.align = 4;
    344   m.flags = flags;
    345   return m;
    346 }
    347 
    348 /* Index of the first event of KIND at or after `from`, or -1. */
    349 static int event_index(const MockNative* m, MockEventKind kind, u32 from) {
    350   for (u32 i = from; i < m->nevents; ++i)
    351     if (m->events[i].kind == kind) return (int)i;
    352   return -1;
    353 }
    354 
    355 static CGFuncDesc fn_desc(TestCtx* tc) {
    356   CGFuncDesc fd;
    357   KitCgFuncSig sig;
    358   KitCgFuncResult sig_result;
    359   memset(&fd, 0, sizeof fd);
    360   memset(&sig, 0, sizeof sig);
    361   memset(&sig_result, 0, sizeof sig_result);
    362   sig_result.type = tc->i32;
    363   sig.result = sig_result;
    364   sig.call_conv = KIT_CG_CC_TARGET_C;
    365   fd.fn_type = kit_cg_type_func(tc->c, sig);
    366   fd.result_type = tc->i32;
    367   return fd;
    368 }
    369 
    370 static CgTarget* make_target(TestCtx* tc, MockNative* native) {
    371   NativeDirectTargetConfig cfg;
    372   memset(&cfg, 0, sizeof cfg);
    373   mock_native_init(native, tc->c);
    374   cfg.native = &native->base;
    375   cfg.ops = &mock_ops;
    376   return native_direct_target_new(tc->c, NULL, &cfg);
    377 }
    378 
    379 static int count_event(const MockNative* m, MockEventKind kind) {
    380   int count = 0;
    381   for (u32 i = 0; i < m->nevents; ++i)
    382     if (m->events[i].kind == kind) ++count;
    383   return count;
    384 }
    385 
    386 static void test_frame_locals_scratch_storeback_and_branches(void) {
    387   TestCtx tc;
    388   MockNative native;
    389   CgTarget* t;
    390   CGFuncDesc fd;
    391   CGLocal a, b, sum;
    392   Label done;
    393   tc_init(&tc);
    394   t = make_target(&tc, &native);
    395   fd = fn_desc(&tc);
    396   t->func_begin(t, &fd);
    397   a = local_new(t, tc.i32);
    398   b = local_new(t, tc.i32);
    399   sum = local_new(t, tc.i32);
    400   EXPECT(a == 1 && b == 2 && sum == 3, "locals should be semantic ids");
    401   EXPECT(native.next_slot == 3, "locals should allocate frame homes");
    402 
    403   t->load_imm(t, op_local(a, tc.i32), 7);
    404   t->load_imm(t, op_local(b, tc.i32), 9);
    405   t->binop(t, BO_IADD, op_local(sum, tc.i32), op_local(a, tc.i32),
    406            op_local(b, tc.i32));
    407   done = t->label_new(t);
    408   t->cmp_branch(t, CMP_EQ, op_local(sum, tc.i32), op_imm(16, tc.i32), done);
    409   t->jump(t, done);
    410   t->label_place(t, done);
    411   t->ret(t, sum);
    412   t->func_end(t);
    413 
    414   EXPECT(count_event(&native, EV_LOAD_IMM) == 3,
    415          "two explicit immediates plus cmp imm materialization expected");
    416   /* With the local register cache, a/b/sum live in registers across the
    417    * straight-line compute run: the binop reloads neither operand. They are
    418    * spilled at the branch flush (EV_STORE), and sum is reloaded only after that
    419    * flush — once for the cmp_branch and once for the return. */
    420   EXPECT(count_event(&native, EV_LOAD) == 2,
    421          "sum is reloaded from its home for the cmp_branch and the return");
    422   EXPECT(count_event(&native, EV_STORE) >= 3,
    423          "results should store back to frame homes");
    424   EXPECT(count_event(&native, EV_BINOP) == 1, "expected one native binop");
    425   EXPECT(count_event(&native, EV_LABEL_NEW) == 1, "expected one native label");
    426   EXPECT(count_event(&native, EV_CMP_BRANCH) == 1,
    427          "expected one compare branch");
    428   EXPECT(count_event(&native, EV_JUMP) == 1, "expected one jump");
    429   EXPECT(count_event(&native, EV_PLAN_RET) == 1 && count_event(&native, EV_RET),
    430          "return should plan moves and emit ret");
    431   tc_fini(&tc);
    432 }
    433 
    434 static void test_call_barrier_storeback_and_max_outgoing(void) {
    435   TestCtx tc;
    436   MockNative native;
    437   CgTarget* t;
    438   CGFuncDesc fd;
    439   CGLocal arg, fnptr, result;
    440   CGCallDesc call;
    441   CGLocal args[1];
    442   NativeDirectTarget* nd;
    443   tc_init(&tc);
    444   t = make_target(&tc, &native);
    445   fd = fn_desc(&tc);
    446   t->func_begin(t, &fd);
    447   arg = local_new(t, tc.i32);
    448   fnptr = local_new(t, tc.ptr);
    449   result = local_new(t, tc.i32);
    450   t->load_imm(t, op_local(arg, tc.i32), 42);
    451   t->load_imm(t, op_local(fnptr, tc.ptr), 0x1000);
    452 
    453   memset(&call, 0, sizeof call);
    454   args[0] = arg;
    455   call.fn_type = fd.fn_type;
    456   call.callee = op_local(fnptr, tc.ptr);
    457   call.args = args;
    458   call.result = result;
    459   call.nargs = 1;
    460   t->call(t, &call);
    461 
    462   nd = (NativeDirectTarget*)t;
    463   EXPECT(native.barrier_flags ==
    464              (NATIVE_DIRECT_BARRIER_CALL | NATIVE_DIRECT_BARRIER_MEMORY),
    465          "call should request call+memory barrier");
    466   EXPECT(native.last_stack_arg_size == 24 && nd->max_outgoing == 24,
    467          "call planning should track max outgoing stack size");
    468   EXPECT(count_event(&native, EV_PLAN_CALL) == 1, "expected one call plan");
    469   EXPECT(count_event(&native, EV_EMIT_CALL) == 1, "expected one emitted call");
    470   EXPECT(count_event(&native, EV_STORE) >= 3,
    471          "arg setup and result should store through native writes");
    472   t->func_end(t);
    473   tc_fini(&tc);
    474 }
    475 
    476 /* Design B: a cached pointer base is dereferenced straight from its register —
    477  * no spill, no reload of the base from its home. The discriminator is the
    478  * EV_LOAD count: an uncached base would emit a separate BASE_FRAME load to read
    479  * the pointer, plus the dereference. A cached base emits only the dereference.
    480  */
    481 static void test_b_cached_pointer_base_not_reloaded(void) {
    482   TestCtx tc;
    483   MockNative native;
    484   CgTarget* t;
    485   CGFuncDesc fd;
    486   CGLocal p, d;
    487   tc_init(&tc);
    488   t = make_target(&tc, &native);
    489   fd = fn_desc(&tc);
    490   t->func_begin(t, &fd);
    491   p = local_new_ptr(t, tc.ptr);
    492   d = local_new(t, tc.i32);
    493   t->load_imm(t, op_local(p, tc.ptr), 0x1000); /* p computed -> cached, dirty */
    494   t->load(t, op_local(d, tc.i32), op_indirect(p, 0, tc.i32),
    495           mem_scalar(tc.i32, 0));
    496 
    497   EXPECT(count_event(&native, EV_LOAD) == 1,
    498          "only the dereference loads; the cached base p is not reloaded");
    499   {
    500     int li = event_index(&native, EV_LOAD, 0);
    501     EXPECT(li >= 0 && native.events[li].a == NATIVE_ADDR_BASE_REG,
    502            "dereference addresses the live cache register for p");
    503   }
    504   EXPECT(count_event(&native, EV_STORE) == 1,
    505          "p is not spilled; only the load result is written to d's home");
    506   t->func_end(t);
    507   tc_fini(&tc);
    508 }
    509 
    510 /* Design B: the cache survives across a store through a pointer. Neither the
    511  * base p nor the stored value a is spilled/reloaded, and a later use of a hits
    512  * the cache. Under Design A the store's flush_all would force both to memory.
    513  */
    514 static void test_b_cache_survives_store(void) {
    515   TestCtx tc;
    516   MockNative native;
    517   CgTarget* t;
    518   CGFuncDesc fd;
    519   CGLocal p, a, b;
    520   tc_init(&tc);
    521   t = make_target(&tc, &native);
    522   fd = fn_desc(&tc);
    523   t->func_begin(t, &fd);
    524   p = local_new_ptr(t, tc.ptr);
    525   a = local_new(t, tc.i32);
    526   b = local_new(t, tc.i32);
    527   t->load_imm(t, op_local(p, tc.ptr), 0x2000); /* p cached */
    528   t->load_imm(t, op_local(a, tc.i32), 5);      /* a cached, dirty */
    529   t->store(t, op_indirect(p, 0, tc.i32), op_local(a, tc.i32),
    530            mem_scalar(tc.i32, 0));
    531   t->binop(t, BO_IADD, op_local(b, tc.i32), op_local(a, tc.i32),
    532            op_local(a, tc.i32)); /* a is a cache hit */
    533 
    534   EXPECT(count_event(&native, EV_LOAD) == 0,
    535          "no home reloads: p and a are read from registers across the store");
    536   {
    537     int si = event_index(&native, EV_STORE, 0);
    538     EXPECT(si >= 0 && native.events[si].a == NATIVE_ADDR_BASE_REG,
    539            "the store dereferences p from its live register");
    540   }
    541   EXPECT(count_event(&native, EV_STORE) == 1,
    542          "only the user store is emitted; nothing is spilled");
    543   t->func_end(t);
    544   tc_fini(&tc);
    545 }
    546 
    547 /* A volatile access must still flush the cache (it may observe memory) and emit
    548  * the volatile barrier — the escape argument does not apply. */
    549 static void test_b_volatile_load_flushes(void) {
    550   TestCtx tc;
    551   MockNative native;
    552   CgTarget* t;
    553   CGFuncDesc fd;
    554   CGLocal p, a, d;
    555   tc_init(&tc);
    556   t = make_target(&tc, &native);
    557   fd = fn_desc(&tc);
    558   t->func_begin(t, &fd);
    559   p = local_new_ptr(t, tc.ptr);
    560   a = local_new(t, tc.i32);
    561   d = local_new(t, tc.i32);
    562   t->load_imm(t, op_local(p, tc.ptr), 0x3000);
    563   t->load_imm(t, op_local(a, tc.i32), 9); /* a cached, dirty */
    564   t->load(t, op_local(d, tc.i32), op_indirect(p, 0, tc.i32),
    565           mem_scalar(tc.i32, MF_VOLATILE));
    566 
    567   EXPECT((native.barrier_flags & NATIVE_DIRECT_BARRIER_VOLATILE) != 0,
    568          "volatile access emits a volatile barrier");
    569   EXPECT(count_event(&native, EV_STORE) >= 2,
    570          "volatile flush spills the dirty cached locals (p and a)");
    571   t->func_end(t);
    572   tc_fini(&tc);
    573 }
    574 
    575 /* A call still flushes the whole cache: a dirty cached local is spilled before
    576  * the call is emitted (caller-saved registers die across the call). */
    577 static void test_b_call_still_flushes(void) {
    578   TestCtx tc;
    579   MockNative native;
    580   CgTarget* t;
    581   CGFuncDesc fd;
    582   CGLocal a, fnptr;
    583   CGCallDesc call;
    584   tc_init(&tc);
    585   t = make_target(&tc, &native);
    586   fd = fn_desc(&tc);
    587   t->func_begin(t, &fd);
    588   a = local_new(t, tc.i32);
    589   fnptr = local_new_ptr(t, tc.ptr);
    590   t->load_imm(t, op_local(a, tc.i32), 5); /* a cached, dirty */
    591   t->load_imm(t, op_local(fnptr, tc.ptr), 0x1000);
    592   memset(&call, 0, sizeof call);
    593   call.fn_type = fd.fn_type;
    594   call.callee = op_local(fnptr, tc.ptr);
    595   t->call(t, &call);
    596 
    597   {
    598     int ci = event_index(&native, EV_EMIT_CALL, 0);
    599     int si = event_index(&native, EV_STORE, 0);
    600     EXPECT(ci >= 0 && si >= 0 && si < ci,
    601            "dirty cached locals are spilled before the call is emitted");
    602   }
    603   t->func_end(t);
    604   tc_fini(&tc);
    605 }
    606 
    607 int main(void) {
    608   kit_unit_init(&g_u);
    609   g_u.ctx.now = -1;
    610   test_frame_locals_scratch_storeback_and_branches();
    611   test_call_barrier_storeback_and_max_outgoing();
    612   test_b_cached_pointer_base_not_reloaded();
    613   test_b_cache_survives_store();
    614   test_b_volatile_load_flushes();
    615   test_b_call_still_flushes();
    616   if (g_u.fails) {
    617     fprintf(stderr, "%d/%d checks failed\n", g_u.fails, g_u.checks);
    618     return 1;
    619   }
    620   printf("native_direct_target_test: %d checks passed\n", g_u.checks);
    621   return 0;
    622 }