kit

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

native_direct_target_test.c (22681B)


      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 "cg/native_asm.h"
     11 #include "lib/kit_unit.h"
     12 
     13 /* Shared test context replaces the per-file heap/diag/counter globals;
     14  * EXPECT aliases CU_EXPECT so the call sites are unchanged. The original
     15  * ctx.now of -1 is preserved (set once in main after kit_unit_init). */
     16 static KitUnit g_u;
     17 #define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__)
     18 
     19 typedef struct TestCtx {
     20   Compiler* c;
     21   KitCgTypeId i32;
     22   KitCgTypeId ptr;
     23 } TestCtx;
     24 
     25 static void tc_init(TestCtx* tc) {
     26   KitTargetSpec target;
     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   tc->i32 = kit_cg_type_builtin(tc->c, KIT_CG_BUILTIN_I32);
     35   tc->ptr = kit_cg_type_ptr(tc->c,
     36                             kit_cg_type_builtin(tc->c, 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_MARSHAL_CALL,
     58   EV_EMIT_CALL,
     59   EV_MARSHAL_RET,
     60   EV_MOVE,
     61   EV_RET,
     62 } MockEventKind;
     63 
     64 typedef struct MockEvent {
     65   u8 kind;
     66   u8 a;
     67   u16 b;
     68   u32 c;
     69 } MockEvent;
     70 
     71 typedef struct MockNative {
     72   NativeTarget base;
     73   NativeFrameSlot next_slot;
     74   MCLabel next_label;
     75   MockEvent events[128];
     76   u32 nevents;
     77   u32 barrier_flags;
     78   u32 last_stack_arg_size;
     79 } MockNative;
     80 
     81 static const Reg mock_int_scratch[] = {1u, 2u, 3u};
     82 static const Reg mock_fp_scratch[] = {4u, 5u};
     83 static const Reg mock_ndt_int_allocable[] = {1u, 2u, 3u, 6u};
     84 static const Reg mock_ndt_fp_allocable[] = {4u, 5u, 6u};
     85 
     86 #define MOCK_PHYS(cls_, reg_, flags_)                      \
     87   {.reg = (reg_),                                          \
     88    .cls = (cls_),                                          \
     89    .abi_index = 0xffu,                                     \
     90    .flags = (flags_),                                      \
     91    .spill_cost = 1u,                                       \
     92    .copy_cost = 1u}
     93 
     94 static const NativePhysRegInfo mock_int_phys[] = {
     95     MOCK_PHYS(NATIVE_REG_INT, 0u,
     96               NATIVE_REG_CALLER_SAVED | NATIVE_REG_ARG | NATIVE_REG_RET),
     97     MOCK_PHYS(NATIVE_REG_INT, 1u,
     98               NATIVE_REG_ALLOCABLE | NATIVE_REG_CALLER_SAVED),
     99     MOCK_PHYS(NATIVE_REG_INT, 2u,
    100               NATIVE_REG_ALLOCABLE | NATIVE_REG_CALLER_SAVED),
    101     MOCK_PHYS(NATIVE_REG_INT, 3u,
    102               NATIVE_REG_ALLOCABLE | NATIVE_REG_CALLER_SAVED),
    103     MOCK_PHYS(NATIVE_REG_INT, 6u, NATIVE_REG_ALLOCABLE),
    104 };
    105 
    106 static const NativePhysRegInfo mock_fp_phys[] = {
    107     MOCK_PHYS(NATIVE_REG_FP, 0u,
    108               NATIVE_REG_CALLER_SAVED | NATIVE_REG_ARG | NATIVE_REG_RET),
    109     MOCK_PHYS(NATIVE_REG_FP, 4u,
    110               NATIVE_REG_ALLOCABLE | NATIVE_REG_CALLER_SAVED),
    111     MOCK_PHYS(NATIVE_REG_FP, 5u,
    112               NATIVE_REG_ALLOCABLE | NATIVE_REG_CALLER_SAVED),
    113     MOCK_PHYS(NATIVE_REG_FP, 6u, NATIVE_REG_ALLOCABLE),
    114 };
    115 
    116 static const NativeAllocClassInfo mock_classes[] = {
    117     {.cls = NATIVE_REG_INT,
    118      .ndt_allocable = mock_ndt_int_allocable,
    119      .ndt_allocable_count = 4,
    120      .scratch = mock_int_scratch,
    121      .nscratch = 3,
    122      .phys = mock_int_phys,
    123      .nphys = sizeof mock_int_phys / sizeof mock_int_phys[0]},
    124     {.cls = NATIVE_REG_FP,
    125      .ndt_allocable = mock_ndt_fp_allocable,
    126      .ndt_allocable_count = 3,
    127      .scratch = mock_fp_scratch,
    128      .nscratch = 2,
    129      .phys = mock_fp_phys,
    130      .nphys = sizeof mock_fp_phys / sizeof mock_fp_phys[0]},
    131 };
    132 
    133 static const NativeRegInfo mock_reg_info = {
    134     .classes = mock_classes,
    135     .nclasses = sizeof mock_classes / sizeof mock_classes[0],
    136 };
    137 
    138 static int reordered_constraint_reg(const NativeRegInfo* ri, const char* body,
    139                                     NativeAllocClass* cls_out, Reg* fixed_out,
    140                                     u32* allowed_mask_out) {
    141   (void)ri;
    142   if (strcmp(body, "x") != 0) return 0;
    143   *cls_out = NATIVE_REG_FP;
    144   *fixed_out = 4u;
    145   *allowed_mask_out = 1u << 4u;
    146   return 1;
    147 }
    148 
    149 static void test_register_class_lookup_is_not_positional(void) {
    150   NativePhysRegInfo int_phys =
    151       MOCK_PHYS(NATIVE_REG_INT, 1u, NATIVE_REG_ALLOCABLE);
    152   NativePhysRegInfo fp_phys =
    153       MOCK_PHYS(NATIVE_REG_FP, 4u, NATIVE_REG_ALLOCABLE);
    154   NativeAllocClassInfo classes[2];
    155   NativeRegInfo regs;
    156   NativeTarget target;
    157   NativeAsmConstraintInfo info;
    158   memset(classes, 0, sizeof classes);
    159   classes[0].cls = NATIVE_REG_FP;
    160   classes[0].phys = &fp_phys;
    161   classes[0].nphys = 1u;
    162   classes[1].cls = NATIVE_REG_INT;
    163   classes[1].phys = &int_phys;
    164   classes[1].nphys = 1u;
    165   memset(&regs, 0, sizeof regs);
    166   regs.classes = classes;
    167   regs.nclasses = 2u;
    168   regs.asm_constraint_reg = reordered_constraint_reg;
    169   memset(&target, 0, sizeof target);
    170   target.regs = &regs;
    171 
    172   EXPECT(native_reg_info_class_info(&regs, NATIVE_REG_FP) == &classes[0] &&
    173              native_reg_info_class_info(&regs, NATIVE_REG_INT) == &classes[1],
    174          "register class lookup depends on table order");
    175   EXPECT(native_asm_constraint_reg_info(&target, "x", &info) &&
    176              info.cls == NATIVE_REG_FP && info.fixed_reg == 4u,
    177          "inline-asm fallback used a positional register class");
    178 }
    179 
    180 static MockNative* mock_of(NativeTarget* t) { return (MockNative*)t; }
    181 
    182 static void ev(MockNative* m, MockEventKind kind, u32 a, u32 b, u32 c) {
    183   MockEvent* e;
    184   if (m->nevents >= sizeof m->events / sizeof m->events[0]) abort();
    185   e = &m->events[m->nevents++];
    186   memset(e, 0, sizeof *e);
    187   e->kind = (u8)kind;
    188   e->a = (u8)a;
    189   e->b = (u16)b;
    190   e->c = c;
    191 }
    192 
    193 static NativeAllocClass mock_class_for_type(NativeTarget* t, KitCgTypeId type) {
    194   (void)t;
    195   (void)type;
    196   return NATIVE_REG_INT;
    197 }
    198 
    199 static void mock_func_begin(NativeTarget* t, const CGFuncDesc* fd) {
    200   (void)fd;
    201   ev(mock_of(t), EV_FUNC_BEGIN, 0, 0, 0);
    202 }
    203 
    204 static void mock_func_end(NativeTarget* t) {
    205   ev(mock_of(t), EV_FUNC_END, 0, 0, 0);
    206 }
    207 
    208 static NativeFrameSlot mock_frame_slot(NativeTarget* t,
    209                                        const NativeFrameSlotDesc* d) {
    210   NativeFrameSlot slot = ++mock_of(t)->next_slot;
    211   ev(mock_of(t), EV_FRAME_SLOT, d->kind, d->size, slot);
    212   return slot;
    213 }
    214 
    215 static MCLabel mock_label_new(NativeTarget* t) {
    216   MCLabel label = ++mock_of(t)->next_label;
    217   ev(mock_of(t), EV_LABEL_NEW, 0, 0, label);
    218   return label;
    219 }
    220 
    221 static void mock_label_place(NativeTarget* t, MCLabel label) {
    222   ev(mock_of(t), EV_LABEL_PLACE, 0, 0, label);
    223 }
    224 
    225 static void mock_jump(NativeTarget* t, MCLabel label) {
    226   ev(mock_of(t), EV_JUMP, 0, 0, label);
    227 }
    228 
    229 static void mock_cmp_branch(NativeTarget* t, CmpOp op, NativeLoc a, NativeLoc b,
    230                             MCLabel label) {
    231   EXPECT(a.kind == NATIVE_LOC_REG && b.kind == NATIVE_LOC_REG,
    232          "cmp_branch should receive materialized registers");
    233   ev(mock_of(t), EV_CMP_BRANCH, op, a.v.reg, label);
    234 }
    235 
    236 static void mock_load(NativeTarget* t, NativeLoc dst, NativeAddr addr,
    237                       MemAccess mem) {
    238   EXPECT(dst.kind == NATIVE_LOC_REG, "load destination should be a register");
    239   (void)mem;
    240   ev(mock_of(t), EV_LOAD, addr.base_kind, dst.v.reg, addr.base.frame);
    241 }
    242 
    243 static void mock_store(NativeTarget* t, NativeAddr addr, NativeLoc src,
    244                        MemAccess mem) {
    245   EXPECT(src.kind == NATIVE_LOC_REG, "store source should be a register");
    246   (void)mem;
    247   ev(mock_of(t), EV_STORE, addr.base_kind, src.v.reg, addr.base.frame);
    248 }
    249 
    250 static void mock_load_imm(NativeTarget* t, NativeLoc dst, i64 imm) {
    251   EXPECT(dst.kind == NATIVE_LOC_REG, "load_imm destination should be register");
    252   ev(mock_of(t), EV_LOAD_IMM, dst.v.reg, 0, (u32)imm);
    253 }
    254 
    255 static void mock_binop(NativeTarget* t, BinOp op, NativeLoc dst, NativeLoc a,
    256                        NativeLoc b) {
    257   EXPECT(dst.kind == NATIVE_LOC_REG && a.kind == NATIVE_LOC_REG &&
    258              b.kind == NATIVE_LOC_REG,
    259          "binop operands should be materialized registers");
    260   ev(mock_of(t), EV_BINOP, op, dst.v.reg, (a.v.reg << 16) | b.v.reg);
    261 }
    262 
    263 static void mock_move_rr(NativeTarget* t, NativeRegLoc dst, NativeRegLoc src) {
    264   ev(mock_of(t), EV_MOVE, dst.v.reg, src.v.reg, dst.type);
    265 }
    266 
    267 static void mock_emit_call(NativeTarget* t, const NativeCallPhase* plan) {
    268   EXPECT(plan->callee.kind == NATIVE_LOC_REG,
    269          "frame callee should be materialized for call");
    270   ev(mock_of(t), EV_EMIT_CALL, plan->nargs, plan->nrets, plan->callee.v.reg);
    271 }
    272 
    273 static void mock_marshal_ret(NativeTarget* t, const CGFuncDesc* fd,
    274                              const NativeLoc* value,
    275                              NativeCallPhaseRet** out_rets, u32* out_nrets) {
    276   NativeCallPhaseRet* r;
    277   (void)fd;
    278   r = arena_zarray(t->c->tu, NativeCallPhaseRet, 1);
    279   if (value) {
    280     r[0].src = *value;
    281     r[0].dst.kind = NATIVE_LOC_REG;
    282     r[0].dst.cls = NATIVE_REG_INT;
    283     r[0].dst.type = value->type;
    284     r[0].dst.v.reg = 0;
    285     r[0].mem.type = value->type;
    286     r[0].mem.size = 4;
    287     r[0].mem.align = 4;
    288   }
    289   *out_rets = r;
    290   *out_nrets = value ? 1u : 0u;
    291   ev(mock_of(t), EV_MARSHAL_RET, 0, value ? 1u : 0u, 0);
    292 }
    293 
    294 static void mock_ret(NativeTarget* t) { ev(mock_of(t), EV_RET, 0, 0, 0); }
    295 
    296 static void mock_native_init(MockNative* m, Compiler* c) {
    297   memset(m, 0, sizeof *m);
    298   m->base.c = c;
    299   m->base.regs = &mock_reg_info;
    300   m->base.class_for_type = mock_class_for_type;
    301   m->base.func_begin = mock_func_begin;
    302   m->base.func_end = mock_func_end;
    303   m->base.frame_slot = mock_frame_slot;
    304   m->base.label_new = mock_label_new;
    305   m->base.label_place = mock_label_place;
    306   m->base.jump = mock_jump;
    307   m->base.cmp_branch = mock_cmp_branch;
    308   m->base.load = mock_load;
    309   m->base.store = mock_store;
    310   m->base.load_imm = mock_load_imm;
    311   m->base.binop = mock_binop;
    312   m->base.move_rr = mock_move_rr;
    313   m->base.emit_call = mock_emit_call;
    314   m->base.marshal_ret = mock_marshal_ret;
    315   m->base.ret = mock_ret;
    316 }
    317 
    318 static void mock_barrier(NativeDirectTarget* d, u32 flags) {
    319   MockNative* m = (MockNative*)d->native;
    320   m->barrier_flags |= flags;
    321   ev(m, EV_BARRIER, 0, 0, flags);
    322 }
    323 
    324 static void mock_marshal_call(NativeDirectTarget* d, const NativeCallDesc* desc,
    325                               NativeCallPhase* plan) {
    326   MockNative* m = (MockNative*)d->native;
    327   NativeCallPhaseMove* args =
    328       arena_zarray(d->base.c->tu, NativeCallPhaseMove, desc->nargs);
    329   NativeCallPhaseRet* rets =
    330       arena_zarray(d->base.c->tu, NativeCallPhaseRet, desc->nresults);
    331   memset(plan, 0, sizeof *plan);
    332   plan->callee = desc->callee;
    333   plan->args = args;
    334   plan->rets = rets;
    335   plan->nargs = desc->nargs;
    336   plan->nrets = desc->nresults;
    337   plan->stack_arg_size = 24;
    338   for (u32 i = 0; i < desc->nargs; ++i) {
    339     args[i].src = desc->args[i];
    340     args[i].dst.kind = NATIVE_LOC_REG;
    341     args[i].dst.cls = NATIVE_REG_INT;
    342     args[i].dst.type = desc->args[i].type;
    343     args[i].dst.v.reg = i;
    344     args[i].mem.type = desc->args[i].type;
    345     args[i].mem.size = 4;
    346     args[i].mem.align = 4;
    347   }
    348   for (u32 i = 0; i < desc->nresults; ++i) {
    349     rets[i].src.kind = NATIVE_LOC_REG;
    350     rets[i].src.cls = NATIVE_REG_INT;
    351     rets[i].src.type = desc->results[i].type;
    352     rets[i].src.v.reg = i;
    353     rets[i].dst = desc->results[i];
    354     rets[i].mem.type = desc->results[i].type;
    355     rets[i].mem.size = 4;
    356     rets[i].mem.align = 4;
    357   }
    358   m->last_stack_arg_size = plan->stack_arg_size;
    359   ev(m, EV_MARSHAL_CALL, desc->nargs, desc->nresults, plan->stack_arg_size);
    360 }
    361 
    362 static const NativeOps mock_ops = {
    363     .marshal_call = mock_marshal_call,
    364     .barrier = mock_barrier,
    365 };
    366 
    367 static Operand op_local(CGLocal local, KitCgTypeId type) {
    368   Operand o;
    369   memset(&o, 0, sizeof o);
    370   o.kind = OPK_LOCAL;
    371   o.type = type;
    372   o.v.local = local;
    373   return o;
    374 }
    375 
    376 static Operand op_imm(i64 value, KitCgTypeId type) {
    377   Operand o;
    378   memset(&o, 0, sizeof o);
    379   o.kind = OPK_IMM;
    380   o.type = type;
    381   o.v.imm = value;
    382   return o;
    383 }
    384 
    385 static CGLocal local_new(CgTarget* t, KitCgTypeId type) {
    386   CGLocalDesc d;
    387   memset(&d, 0, sizeof d);
    388   d.type = type;
    389   d.size = 4;
    390   d.align = 4;
    391   return t->local(t, &d);
    392 }
    393 
    394 static CGLocal local_new_ptr(CgTarget* t, KitCgTypeId type) {
    395   CGLocalDesc d;
    396   memset(&d, 0, sizeof d);
    397   d.type = type;
    398   d.size = 8;
    399   d.align = 8;
    400   return t->local(t, &d);
    401 }
    402 
    403 static Operand op_indirect(CGLocal base, i32 ofs, KitCgTypeId type) {
    404   Operand o;
    405   memset(&o, 0, sizeof o);
    406   o.kind = OPK_INDIRECT;
    407   o.type = type;
    408   o.v.ind.base = base;
    409   o.v.ind.index = CG_LOCAL_NONE;
    410   o.v.ind.ofs = ofs;
    411   return o;
    412 }
    413 
    414 static MemAccess mem_scalar(KitCgTypeId type, u16 flags) {
    415   MemAccess m;
    416   memset(&m, 0, sizeof m);
    417   m.type = type;
    418   m.size = 4;
    419   m.align = 4;
    420   m.flags = flags;
    421   return m;
    422 }
    423 
    424 /* Index of the first event of KIND at or after `from`, or -1. */
    425 static int event_index(const MockNative* m, MockEventKind kind, u32 from) {
    426   for (u32 i = from; i < m->nevents; ++i)
    427     if (m->events[i].kind == kind) return (int)i;
    428   return -1;
    429 }
    430 
    431 static CGFuncDesc fn_desc(TestCtx* tc) {
    432   CGFuncDesc fd;
    433   KitCgFuncSig sig;
    434   KitCgFuncResult sig_result;
    435   memset(&fd, 0, sizeof fd);
    436   memset(&sig, 0, sizeof sig);
    437   memset(&sig_result, 0, sizeof sig_result);
    438   sig_result.type = tc->i32;
    439   sig.result = sig_result;
    440   sig.call_conv = KIT_CG_CC_TARGET_C;
    441   fd.fn_type = kit_cg_type_func(tc->c, sig);
    442   fd.result_type = tc->i32;
    443   return fd;
    444 }
    445 
    446 static CgTarget* make_target(TestCtx* tc, MockNative* native) {
    447   NativeDirectTargetConfig cfg;
    448   memset(&cfg, 0, sizeof cfg);
    449   mock_native_init(native, tc->c);
    450   cfg.native = &native->base;
    451   cfg.ops = &mock_ops;
    452   return native_direct_target_new(tc->c, NULL, &cfg);
    453 }
    454 
    455 static int count_event(const MockNative* m, MockEventKind kind) {
    456   int count = 0;
    457   for (u32 i = 0; i < m->nevents; ++i)
    458     if (m->events[i].kind == kind) ++count;
    459   return count;
    460 }
    461 
    462 static void test_frame_locals_scratch_storeback_and_branches(void) {
    463   TestCtx tc;
    464   MockNative native;
    465   CgTarget* t;
    466   CGFuncDesc fd;
    467   CGLocal a, b, sum;
    468   Label done;
    469   tc_init(&tc);
    470   t = make_target(&tc, &native);
    471   fd = fn_desc(&tc);
    472   t->func_begin(t, &fd);
    473   a = local_new(t, tc.i32);
    474   b = local_new(t, tc.i32);
    475   sum = local_new(t, tc.i32);
    476   EXPECT(a == 1 && b == 2 && sum == 3, "locals should be semantic ids");
    477   EXPECT(native.next_slot == 3, "locals should allocate frame homes");
    478 
    479   t->load_imm(t, op_local(a, tc.i32), 7);
    480   t->load_imm(t, op_local(b, tc.i32), 9);
    481   t->binop(t, BO_IADD, op_local(sum, tc.i32), op_local(a, tc.i32),
    482            op_local(b, tc.i32));
    483   done = t->label_new(t);
    484   t->cmp_branch(t, CMP_EQ, op_local(sum, tc.i32), op_imm(16, tc.i32), done);
    485   t->jump(t, done);
    486   t->label_place(t, done);
    487   t->ret(t, sum);
    488   t->func_end(t);
    489 
    490   EXPECT(count_event(&native, EV_LOAD_IMM) == 3,
    491          "two explicit immediates plus cmp imm materialization expected");
    492   /* With the local register cache, a/b/sum live in registers across the
    493    * straight-line compute run. cmp_branch pins sum before flushing, so the
    494    * compare needs no reload; only the later return reloads it from its home. */
    495   EXPECT(count_event(&native, EV_LOAD) == 1,
    496          "sum is reloaded from its home only for the return "
    497          "(loads=%d)",
    498          count_event(&native, EV_LOAD));
    499   EXPECT(count_event(&native, EV_STORE) >= 3,
    500          "results should store back to frame homes");
    501   EXPECT(count_event(&native, EV_BINOP) == 1, "expected one native binop");
    502   EXPECT(count_event(&native, EV_LABEL_NEW) == 1, "expected one native label");
    503   EXPECT(count_event(&native, EV_CMP_BRANCH) == 1,
    504          "expected one compare branch");
    505   EXPECT(count_event(&native, EV_JUMP) == 1, "expected one jump");
    506   EXPECT(count_event(&native, EV_MARSHAL_RET) == 1 &&
    507              count_event(&native, EV_RET),
    508          "return should plan moves and emit ret");
    509   tc_fini(&tc);
    510 }
    511 
    512 static void test_call_barrier_storeback_and_max_outgoing(void) {
    513   TestCtx tc;
    514   MockNative native;
    515   CgTarget* t;
    516   CGFuncDesc fd;
    517   CGLocal arg, fnptr, result;
    518   CGCallDesc call;
    519   CGLocal args[1];
    520   NativeDirectTarget* nd;
    521   tc_init(&tc);
    522   t = make_target(&tc, &native);
    523   fd = fn_desc(&tc);
    524   t->func_begin(t, &fd);
    525   arg = local_new(t, tc.i32);
    526   fnptr = local_new(t, tc.ptr);
    527   result = local_new(t, tc.i32);
    528   t->load_imm(t, op_local(arg, tc.i32), 42);
    529   t->load_imm(t, op_local(fnptr, tc.ptr), 0x1000);
    530 
    531   memset(&call, 0, sizeof call);
    532   args[0] = arg;
    533   call.fn_type = fd.fn_type;
    534   call.callee = op_local(fnptr, tc.ptr);
    535   call.args = args;
    536   call.result = result;
    537   call.nargs = 1;
    538   t->call(t, &call);
    539 
    540   nd = (NativeDirectTarget*)t;
    541   EXPECT(native.barrier_flags ==
    542              (NATIVE_DIRECT_BARRIER_CALL | NATIVE_DIRECT_BARRIER_MEMORY),
    543          "call should request call+memory barrier");
    544   EXPECT(native.last_stack_arg_size == 24 && nd->max_outgoing == 24,
    545          "call marshalling should track max outgoing stack size");
    546   EXPECT(count_event(&native, EV_MARSHAL_CALL) == 1,
    547          "expected one call marshalling phase");
    548   EXPECT(count_event(&native, EV_EMIT_CALL) == 1, "expected one emitted call");
    549   EXPECT(count_event(&native, EV_STORE) == 2,
    550          "selective call setup should emit only its required stores (stores=%d)",
    551          count_event(&native, EV_STORE));
    552   t->func_end(t);
    553   tc_fini(&tc);
    554 }
    555 
    556 /* Design B: a cached pointer base is dereferenced straight from its register —
    557  * no spill, no reload of the base from its home. The discriminator is the
    558  * EV_LOAD count: an uncached base would emit a separate BASE_FRAME load to read
    559  * the pointer, plus the dereference. A cached base emits only the dereference.
    560  */
    561 static void test_b_cached_pointer_base_not_reloaded(void) {
    562   TestCtx tc;
    563   MockNative native;
    564   CgTarget* t;
    565   CGFuncDesc fd;
    566   CGLocal p, d;
    567   tc_init(&tc);
    568   t = make_target(&tc, &native);
    569   fd = fn_desc(&tc);
    570   t->func_begin(t, &fd);
    571   p = local_new_ptr(t, tc.ptr);
    572   d = local_new(t, tc.i32);
    573   t->load_imm(t, op_local(p, tc.ptr), 0x1000); /* p computed -> cached, dirty */
    574   t->load(t, op_local(d, tc.i32), op_indirect(p, 0, tc.i32),
    575           mem_scalar(tc.i32, 0));
    576 
    577   EXPECT(count_event(&native, EV_LOAD) == 1,
    578          "only the dereference loads; the cached base p is not reloaded");
    579   {
    580     int li = event_index(&native, EV_LOAD, 0);
    581     EXPECT(li >= 0 && native.events[li].a == NATIVE_ADDR_BASE_REG,
    582            "dereference addresses the live cache register for p");
    583   }
    584   EXPECT(count_event(&native, EV_STORE) == 0,
    585          "p stays live and the load result remains in the write-back cache "
    586          "(stores=%d)",
    587          count_event(&native, EV_STORE));
    588   t->func_end(t);
    589   tc_fini(&tc);
    590 }
    591 
    592 /* Design B: the cache survives across a store through a pointer. Neither the
    593  * base p nor the stored value a is spilled/reloaded, and a later use of a hits
    594  * the cache. Under Design A the store's flush_all would force both to memory.
    595  */
    596 static void test_b_cache_survives_store(void) {
    597   TestCtx tc;
    598   MockNative native;
    599   CgTarget* t;
    600   CGFuncDesc fd;
    601   CGLocal p, a, b;
    602   tc_init(&tc);
    603   t = make_target(&tc, &native);
    604   fd = fn_desc(&tc);
    605   t->func_begin(t, &fd);
    606   p = local_new_ptr(t, tc.ptr);
    607   a = local_new(t, tc.i32);
    608   b = local_new(t, tc.i32);
    609   t->load_imm(t, op_local(p, tc.ptr), 0x2000); /* p cached */
    610   t->load_imm(t, op_local(a, tc.i32), 5);      /* a cached, dirty */
    611   t->store(t, op_indirect(p, 0, tc.i32), op_local(a, tc.i32),
    612            mem_scalar(tc.i32, 0));
    613   t->binop(t, BO_IADD, op_local(b, tc.i32), op_local(a, tc.i32),
    614            op_local(a, tc.i32)); /* a is a cache hit */
    615 
    616   EXPECT(count_event(&native, EV_LOAD) == 0,
    617          "no home reloads: p and a are read from registers across the store");
    618   {
    619     int si = event_index(&native, EV_STORE, 0);
    620     EXPECT(si >= 0 && native.events[si].a == NATIVE_ADDR_BASE_REG,
    621            "the store dereferences p from its live register");
    622   }
    623   EXPECT(count_event(&native, EV_STORE) == 1,
    624          "only the user store is emitted; nothing is spilled");
    625   t->func_end(t);
    626   tc_fini(&tc);
    627 }
    628 
    629 /* A volatile access emits the ordering barrier but does not flush unrelated,
    630  * non-escaped locals: foreign memory cannot alias their private frame homes. */
    631 static void test_b_volatile_load_preserves_private_cache(void) {
    632   TestCtx tc;
    633   MockNative native;
    634   CgTarget* t;
    635   CGFuncDesc fd;
    636   CGLocal p, a, d;
    637   tc_init(&tc);
    638   t = make_target(&tc, &native);
    639   fd = fn_desc(&tc);
    640   t->func_begin(t, &fd);
    641   p = local_new_ptr(t, tc.ptr);
    642   a = local_new(t, tc.i32);
    643   d = local_new(t, tc.i32);
    644   t->load_imm(t, op_local(p, tc.ptr), 0x3000);
    645   t->load_imm(t, op_local(a, tc.i32), 9); /* a cached, dirty */
    646   t->load(t, op_local(d, tc.i32), op_indirect(p, 0, tc.i32),
    647           mem_scalar(tc.i32, MF_VOLATILE));
    648 
    649   EXPECT((native.barrier_flags & NATIVE_DIRECT_BARRIER_VOLATILE) != 0,
    650          "volatile access emits a volatile barrier");
    651   EXPECT(count_event(&native, EV_STORE) == 0,
    652          "volatile foreign-memory access preserves private cached locals "
    653          "(stores=%d)",
    654          count_event(&native, EV_STORE));
    655   t->func_end(t);
    656   tc_fini(&tc);
    657 }
    658 
    659 /* A call still flushes the whole cache: a dirty cached local is spilled before
    660  * the call is emitted (caller-saved registers die across the call). */
    661 static void test_b_call_still_flushes(void) {
    662   TestCtx tc;
    663   MockNative native;
    664   CgTarget* t;
    665   CGFuncDesc fd;
    666   CGLocal a, fnptr;
    667   CGCallDesc call;
    668   tc_init(&tc);
    669   t = make_target(&tc, &native);
    670   fd = fn_desc(&tc);
    671   t->func_begin(t, &fd);
    672   a = local_new(t, tc.i32);
    673   fnptr = local_new_ptr(t, tc.ptr);
    674   t->load_imm(t, op_local(a, tc.i32), 5); /* a cached, dirty */
    675   t->load_imm(t, op_local(fnptr, tc.ptr), 0x1000);
    676   memset(&call, 0, sizeof call);
    677   call.fn_type = fd.fn_type;
    678   call.callee = op_local(fnptr, tc.ptr);
    679   t->call(t, &call);
    680 
    681   {
    682     int ci = event_index(&native, EV_EMIT_CALL, 0);
    683     int si = event_index(&native, EV_STORE, 0);
    684     EXPECT(ci >= 0 && si >= 0 && si < ci,
    685            "dirty cached locals are spilled before the call is emitted");
    686   }
    687   t->func_end(t);
    688   tc_fini(&tc);
    689 }
    690 
    691 int main(void) {
    692   kit_unit_init(&g_u);
    693   g_u.ctx.now = -1;
    694   test_register_class_lookup_is_not_positional();
    695   test_frame_locals_scratch_storeback_and_branches();
    696   test_call_barrier_storeback_and_max_outgoing();
    697   test_b_cached_pointer_base_not_reloaded();
    698   test_b_cache_survives_store();
    699   test_b_volatile_load_preserves_private_cache();
    700   test_b_call_still_flushes();
    701   if (g_u.fails) {
    702     fprintf(stderr, "%d/%d checks failed\n", g_u.fails, g_u.checks);
    703     return 1;
    704   }
    705   printf("native_direct_target_test: %d checks passed\n", g_u.checks);
    706   return 0;
    707 }