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(®s, 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 = ®s; 171 172 EXPECT(native_reg_info_class_info(®s, NATIVE_REG_FP) == &classes[0] && 173 native_reg_info_class_info(®s, 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 }