kit

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

commit 0a1b3558e46400ad6b0da26fa98aa0ffeb4f97f0
parent ca82e9d4c2e19e924af115c4a0917accd053d77f
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat,  9 May 2026 12:49:05 -0700

test/cg: expand A/B/C corpus; harness now drives real type+abi APIs

Replaces hand-crafted ABI fixtures and the ad-hoc T_I32-style static
literals with the same building blocks the parser will use:
type_func + abi_func_info + pool-interned Types. CGFuncDesc, CGParamDesc,
CGCallDesc, and CGABIValue are populated from the live TargetABI.

cg_test.{h,c}: new helpers for multi-function setup (cgtest_begin_func),
parameter slots, locals, byval/sret/indirect call args, and aggregate
return shapes. cgtest_begin_main now routes through cgtest_begin_func.

cases.c: 31 cases (was 9). Group A gains a05-a10 covering MOVN-negative,
i64 high-bits return, void return, multiple rets, multi-step movz/movk
materialization, and narrow-int return. Group B is fully populated
(b01-b08): int-param echo/sum, 9-param spill, local store/load,
addr-taken local, struct sret, byval struct param, float param via FTOI.
Group C gains c05-c12: signed div/mod, xor, i64 add/mul, unsigned div,
NEG, logical NOT, signed SHR.

CORPUS.md: matrix updated with all 31 cases and concrete expected exit
codes (mod 256). Notes that cases depending on still-unimplemented
type_/abi_/CGTarget methods fail at runtime by design — these expectations
are the spec those interfaces will be implemented against.

Diffstat:
Mtest/cg/CORPUS.md | 119+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mtest/cg/harness/cases.c | 732++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Mtest/cg/harness/cg_test.c | 367+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mtest/cg/harness/cg_test.h | 182++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mtest/cg/run.sh | 149++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
5 files changed, 1262 insertions(+), 287 deletions(-)

diff --git a/test/cg/CORPUS.md b/test/cg/CORPUS.md @@ -2,7 +2,8 @@ Coverage matrix for `test/cg/`. Each registered case in `harness/cases.c` is one row; behavioral oracle is `test_main`'s return -value. Mirrors the CORPUS shape used by `test/elf/` and `test/link/`. +value (mod 256, since POSIX exit codes are one byte). Mirrors the CORPUS +shape used by `test/elf/` and `test/link/`. Test paths per case (run.sh): @@ -16,73 +17,95 @@ Test paths per case (run.sh): `O` (opt-wrapped) lands once `opt_cgtarget` is implemented. +The harness drives the same building blocks the parser will: pool-interned +Types via `type_*`, ABI classification via `abi_func_info`, and `CGTarget` +for lowering. There are no ABI mocks. Cases that exercise interfaces the +lib does not yet implement (param/call/aggregate/FP methods on the +backend; `type_func`/`abi_func_info`) link against the same symbols the +parser will, and fail at runtime until those land — that is intentional. + ## Status legend - ★ landed in the spine -- · planned +- · planned (case registered, expected value fixed) - (deferred) — explicit non-goal for the current pass ## MC-only — direct MCEmitter -| Case | Status | Notes | -|---|---|---| -| `mc_smoke` | ★ | hand-built `mov w0, #42; ret`; analogue of `test/elf/unit/smoke.c` | +| Case | Status | Expected | Notes | +|---|---|---|---| +| `mc_smoke` | ★ | 42 | hand-built `mov w0, #42; ret`; analogue of `test/elf/unit/smoke.c` | ## Group A — function lifecycle and return | Case | Status | Body | Expected | |---|---|---|---| -| `a01_return_const_42` | ★ | `alloc_reg; load_imm 42; ret reg` | 42 | -| `a02_return_zero` | ★ | `load_imm 0; ret reg` | 0 | -| `a03_ret_imm` | ★ | `ret IMM 17` (backend materializes) | 17 | -| `a04_copy_reg` | ★ | `load_imm 7; copy r1->r2; ret r2` | 7 | -| `a05_return_neg_small`| · | `load_imm -7` via MOVN; ret | 249 (mod 256) | -| `a06_return_i64` | · | 64-bit return register selection | per width | -| `a07_void_return` | · | `ret(NULL)` | 0 (via _start init) | -| `a08_multiple_returns`| · | early return | depends | +| `a01_return_const_42` | ★ | `alloc_reg; load_imm 42; ret reg` | 42 | +| `a02_return_zero` | ★ | `load_imm 0; ret reg` | 0 | +| `a03_ret_imm` | ★ | `ret IMM 17` (backend materializes) | 17 | +| `a04_copy_reg` | ★ | `load_imm 7; copy r1->r2; ret r2` | 7 | +| `a05_return_neg_small` | · | `load_imm -7` via MOVN; ret | 249 (= -7 & 0xff) | +| `a06_return_i64` | · | i64 `load_imm 0x1_0000_002A`; ret as i64 | 42 (low 32 of x0) | +| `a07_void_return` | · | `ret(NULL)` | 0 (via _start zeroing x0) | +| `a08_multiple_returns` | · | `ret_imm 1; ret_imm 2` (second is dead) | 1 | +| `a09_load_imm_movz_movk` | · | `load_imm 0xABCD` (multi-step materialize) | 205 (= 0xCD) | +| `a10_return_u8` | · | `load_imm 200` into u8 reg; ret | 200 | ## Group B — frame slots, parameters, locals -(deferred until frame_slot/param implemented) +Param/call cases pair a helper function with `test_main`. Both share one +`CGTarget` instance — the backend must support multiple +`func_begin`/`func_end` pairs per TU. `cgtest_begin_func` builds +`CGFuncDesc` from a real `type_func`/`abi_func_info` pair; param +materialization, slot allocation, and call lowering use the live +`TargetABI`. -| Case | Status | Notes | -|---|---|---| -| `b01_param_int` | · | one int param echoed | -| `b02_param_sum` | · | two params summed | -| `b03_param_spill` | · | nine params (overflow registers) | -| `b04_local_int` | · | local stored, loaded, returned | -| `b05_addr_taken_local` | · | `&local` forces frame residency | -| `b06_sret` | · | struct return | -| `b07_byval_param` | · | aggregate-by-value parameter | -| `b08_fp_param` | · | float/double param | +| Case | Status | Body | Expected | +|---|---|---|---| +| `b01_param_int` | · | `int echo(int x){return x;}; echo(201)` | 201 | +| `b02_param_sum` | · | `int sum2(int a,int b){return a+b;}; sum2(40,2)` | 42 | +| `b03_param_spill` | · | `int sum9(a..i)`; nine int params (8 GPR, 1 stack); `sum9(1..9)` | 45 | +| `b04_local_int` | · | local int slot; `*p = 42; return *p` | 42 | +| `b05_addr_taken_local` | · | `int x=17; int*p=&x; *p+=1; return *p` | 18 | +| `b06_sret` | · | `struct Pt{int a,b;}; Pt mk(){{10,32}}; pt=mk(); return pt.a+pt.b` | 42 | +| `b07_byval_param` | · | `int take(struct Pt p){return p.a+p.b;}; take({15,27})` | 42 | +| `b08_fp_param` | · | `int trunc(float f){return (int)f;}; trunc(7.5f)` | 7 | ## Group C — integer arithmetic | Case | Status | Body | Expected | |---|---|---|---| -| `c01_add` | ★ | `1 + 2` | 3 | -| `c02_sub_mul` | ★ | `7 * 3 - 4` | 17 | -| `c03_bitwise` | ★ | `(~3) & 0xff` | 252 | -| `c04_shift` | ★ | `(1<<5) | (16>>1)` | 40 | -| `c05_div_mod` | · | `23 / 4 + 23 % 4` | 8 (needs SDIV+SREM) | -| `c06_xor` | · | `0xa5 ^ 0x5a` | 0xff | -| `c07_iadd_i64` | · | 64-bit adds | depends | -| `c08_unsigned_div` | · | `100u / 7u` | 14 | - -## Group D — compare and branch (deferred) -## Group E — conversions (deferred) -## Group F — memory (deferred) -## Group G — calls (deferred) -## Group H — control flow (deferred) -## Group I — alloca (deferred) -## Group J — varargs (deferred) -## Group K — atomics (deferred) -## Group L — intrinsics (deferred) -## Group M — inline asm (deferred) -## Group N — TLS (deferred) -## Group O — sections + globals (deferred) -## Group P — set_loc / debug (deferred) -## Group Q — multi-function (deferred) -## Group R — opt-wrapped equivalence (deferred) +| `c01_add` | ★ | `1 + 2` | 3 | +| `c02_sub_mul` | ★ | `7 * 3 - 4` | 17 | +| `c03_bitwise` | ★ | `(~3) & 0xff` | 252 | +| `c04_shift` | ★ | `(1<<5) \| (16>>1)` (logical shr) | 40 | +| `c05_div_mod` | · | `23 / 4 + 23 % 4` (signed) | 8 | +| `c06_xor` | · | `0xa5 ^ 0x5a` | 255 | +| `c07_iadd_i64` | · | i64 `0x1_0000_0029 + 0x1_0000_0001` | 42 (low 32) | +| `c08_unsigned_div` | · | `100u / 7u` | 14 | +| `c09_neg` | · | `UO_NEG` 42 | 214 (= -42 & 0xff) | +| `c10_logical_not` | · | `UO_NOT 0` (zero-test → 0/1) | 1 | +| `c11_shr_signed` | · | `-16 >>(s) 2` | 252 (= -4 & 0xff) | +| `c12_imul_i64` | · | i64 `7 * 6` | 42 | + +## Deferred groups + +| Group | Theme | +|---|---| +| D | compare and branch | +| E | conversions | +| F | memory (loads/stores beyond locals) | +| G | calls (beyond the direct-call path Group B exercises) | +| H | control flow | +| I | alloca | +| J | varargs | +| K | atomics | +| L | intrinsics | +| M | inline asm | +| N | TLS | +| O | sections + globals | +| P | set_loc / debug | +| Q | multi-function (extends Group B's two-function pattern) | +| R | opt-wrapped equivalence | See `doc/cg_testing.md` for the strategy and group definitions. diff --git a/test/cg/harness/cases.c b/test/cg/harness/cases.c @@ -7,7 +7,16 @@ * --emit). * * Adding a case: write a static `build_<name>(CgTestCtx*)`, add a row to - * the array, and update CORPUS.md. */ + * the array, and update CORPUS.md. + * + * Cases drive the same building blocks the parser will use: pool-interned + * Types via type_*, ABI classification via abi_func_info, and CGTarget for + * lowering. Cases that exercise features the lib does not yet implement + * (calls, params, aggregates, FP) compile and link against the same + * symbols the parser depends on; they fail at runtime until those land. + * + * Expected exit codes are stored modulo 256 — POSIX exit() truncates to + * one byte, and the harness compares against (expected & 0xff). */ #include "cg_test.h" @@ -41,41 +50,451 @@ static void build_mc_smoke(CgTestCtx* ctx) /* a01_return_const_42 — alloc reg, load_imm(42), ret reg. */ static void build_a01_return_const_42(CgTestCtx* ctx) { - CgTestFn* tf = cgtest_begin_main(ctx, T_I32); - Reg r = ctx->target->alloc_reg(ctx->target, RC_INT, T_I32); - ctx->target->load_imm(ctx->target, REG_i32(r), 42); - cgtest_ret_reg(tf, r, T_I32); + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + Reg r = ctx->target->alloc_reg(ctx->target, RC_INT, I32); + ctx->target->load_imm(ctx->target, REG_op(r, I32), 42); + cgtest_ret_reg(tf, r, I32); cgtest_end(tf); } /* a02_return_zero — load_imm(0). */ static void build_a02_return_zero(CgTestCtx* ctx) { - CgTestFn* tf = cgtest_begin_main(ctx, T_I32); - Reg r = ctx->target->alloc_reg(ctx->target, RC_INT, T_I32); - ctx->target->load_imm(ctx->target, REG_i32(r), 0); - cgtest_ret_reg(tf, r, T_I32); + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + Reg r = ctx->target->alloc_reg(ctx->target, RC_INT, I32); + ctx->target->load_imm(ctx->target, REG_op(r, I32), 0); + cgtest_ret_reg(tf, r, I32); cgtest_end(tf); } /* a03_ret_imm — backend materializes the imm directly inside ret(). */ static void build_a03_ret_imm(CgTestCtx* ctx) { - CgTestFn* tf = cgtest_begin_main(ctx, T_I32); - cgtest_ret_imm(tf, 17, T_I32); + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + cgtest_ret_imm(tf, 17, I32); cgtest_end(tf); } -/* a04_copy_reg — load_imm(7) into r1, copy r1->r2, ret r2. Exercises - * CGTarget.copy. */ +/* a04_copy_reg — load_imm(7) into r1, copy r1->r2, ret r2. */ static void build_a04_copy_reg(CgTestCtx* ctx) { - CgTestFn* tf = cgtest_begin_main(ctx, T_I32); - Reg r1 = ctx->target->alloc_reg(ctx->target, RC_INT, T_I32); - Reg r2 = ctx->target->alloc_reg(ctx->target, RC_INT, T_I32); - ctx->target->load_imm(ctx->target, REG_i32(r1), 7); - ctx->target->copy(ctx->target, REG_i32(r2), REG_i32(r1)); - cgtest_ret_reg(tf, r2, T_I32); + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + Reg r1 = ctx->target->alloc_reg(ctx->target, RC_INT, I32); + Reg r2 = ctx->target->alloc_reg(ctx->target, RC_INT, I32); + ctx->target->load_imm(ctx->target, REG_op(r1, I32), 7); + ctx->target->copy(ctx->target, REG_op(r2, I32), REG_op(r1, I32)); + cgtest_ret_reg(tf, r2, I32); + cgtest_end(tf); +} + +/* a05_return_neg_small — load_imm(-7), ret. Backend should select MOVN. + * Exit code = -7 & 0xff = 249. */ +static void build_a05_return_neg_small(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + Reg r = ctx->target->alloc_reg(ctx->target, RC_INT, I32); + ctx->target->load_imm(ctx->target, REG_op(r, I32), -7); + cgtest_ret_reg(tf, r, I32); + cgtest_end(tf); +} + +/* a06_return_i64 — i64 load_imm with high bits set, return as i64. + * test_main is cast to int(*)(void) by the runner, which reads w0 + * (low 32 of x0). Value 0x100000002A → low 32 = 42. */ +static void build_a06_return_i64(CgTestCtx* ctx) +{ + const Type* I64 = T_i64(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I64); + Reg r = ctx->target->alloc_reg(ctx->target, RC_INT, I64); + ctx->target->load_imm(ctx->target, REG_op(r, I64), 0x100000002Aull); + cgtest_ret_reg(tf, r, I64); + cgtest_end(tf); +} + +/* a07_void_return — function returns void. The harness wrapper (start.c + * for path E; the C compiler for path D/J) zeroes x0 before the call, so + * the observed exit code is 0. */ +static void build_a07_void_return(CgTestCtx* ctx) +{ + CgTestFn* tf = cgtest_begin_main(ctx, T_void(ctx)); + cgtest_ret_void(tf); + cgtest_end(tf); +} + +/* a08_multiple_returns — two consecutive ret() calls in straight-line + * code. The first is taken; the second is dead. Exercises that the + * backend can emit more than one return-shaped sequence per function. */ +static void build_a08_multiple_returns(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + cgtest_ret_imm(tf, 1, I32); + cgtest_ret_imm(tf, 2, I32); /* unreachable */ + cgtest_end(tf); +} + +/* a09_load_imm_movz_movk — value that requires multi-step materialization + * on AArch64 (low 16 != 0, high 16 != 0). 0xABCD → exit 0xCD = 205. */ +static void build_a09_load_imm_movz_movk(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + Reg r = ctx->target->alloc_reg(ctx->target, RC_INT, I32); + ctx->target->load_imm(ctx->target, REG_op(r, I32), 0xABCD); + cgtest_ret_reg(tf, r, I32); + cgtest_end(tf); +} + +/* a10_return_u8 — narrow integer return. Value 200 fits in u8 → 200. */ +static void build_a10_return_u8(CgTestCtx* ctx) +{ + const Type* U8 = T_u8(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, U8); + Reg r = ctx->target->alloc_reg(ctx->target, RC_INT, U8); + ctx->target->load_imm(ctx->target, REG_op(r, U8), 200); + cgtest_ret_reg(tf, r, U8); + cgtest_end(tf); +} + +/* ============================================================ + * Group B: frame slots, parameters, locals + * + * Several cases here define a helper function and have test_main call + * it. The helper exercises param wiring; test_main exercises the call + * sequence. Both share one CGTarget instance — the backend must support + * multiple func_begin/func_end pairs per TU. + * ============================================================ */ + +/* helper used by b01: int echo(int x) { return x; } */ +static ObjSymId build_b01_helper(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* params[] = { I32 }; + CgTestFn* tf = cgtest_begin_func(ctx, "b01_echo", I32, params, 1); + Reg r = ctx->target->alloc_reg(ctx->target, RC_INT, I32); + cgtest_load_local(tf, REG_op(r, I32), cgtest_param_slot(tf, 0), I32); + cgtest_ret_reg(tf, r, I32); + cgtest_end(tf); + return tf->sym; +} + +/* b01_param_int — echo(201) → 201. */ +static void build_b01_param_int(CgTestCtx* ctx) +{ + ObjSymId echo = build_b01_helper(ctx); + const Type* I32 = T_i32(ctx); + const Type* params[] = { I32 }; + + CgTestFn* tf = cgtest_begin_main(ctx, I32); + Reg dst = ctx->target->alloc_reg(ctx->target, RC_INT, I32); + CgTestArg args[] = { { .kind = CGT_ARG_IMM, .type = I32, .v.imm = 201 } }; + cgtest_call(tf, echo, I32, params, args, 1, REG_op(dst, I32)); + cgtest_ret_reg(tf, dst, I32); + cgtest_end(tf); +} + +/* helper used by b02: int sum2(int a, int b) { return a + b; } */ +static ObjSymId build_b02_helper(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* params[] = { I32, I32 }; + CgTestFn* tf = cgtest_begin_func(ctx, "b02_sum2", I32, params, 2); + CGTarget* T = ctx->target; + Reg a = T->alloc_reg(T, RC_INT, I32); + Reg b = T->alloc_reg(T, RC_INT, I32); + Reg s = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(a, I32), cgtest_param_slot(tf, 0), I32); + cgtest_load_local(tf, REG_op(b, I32), cgtest_param_slot(tf, 1), I32); + T->binop(T, BO_IADD, REG_op(s, I32), REG_op(a, I32), REG_op(b, I32)); + cgtest_ret_reg(tf, s, I32); + cgtest_end(tf); + return tf->sym; +} + +/* b02_param_sum — sum2(40, 2) → 42. */ +static void build_b02_param_sum(CgTestCtx* ctx) +{ + ObjSymId sum2 = build_b02_helper(ctx); + const Type* I32 = T_i32(ctx); + const Type* params[] = { I32, I32 }; + + CgTestFn* tf = cgtest_begin_main(ctx, I32); + Reg dst = ctx->target->alloc_reg(ctx->target, RC_INT, I32); + CgTestArg args[] = { + { .kind = CGT_ARG_IMM, .type = I32, .v.imm = 40 }, + { .kind = CGT_ARG_IMM, .type = I32, .v.imm = 2 }, + }; + cgtest_call(tf, sum2, I32, params, args, 2, REG_op(dst, I32)); + cgtest_ret_reg(tf, dst, I32); + cgtest_end(tf); +} + +/* helper used by b03: int sum9(a..i) { return a+b+c+d+e+f+g+h+i; } + * Nine int parameters force at least one to spill onto the stack on + * AArch64 SysV (8 GP arg registers). */ +static ObjSymId build_b03_helper(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* p[9] = { I32,I32,I32,I32,I32,I32,I32,I32,I32 }; + CgTestFn* tf = cgtest_begin_func(ctx, "b03_sum9", I32, p, 9); + CGTarget* T = ctx->target; + Reg accum = T->alloc_reg(T, RC_INT, I32); + T->load_imm(T, REG_op(accum, I32), 0); + for (u32 i = 0; i < 9; ++i) { + Reg t = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(t, I32), cgtest_param_slot(tf, i), I32); + T->binop(T, BO_IADD, REG_op(accum, I32), + REG_op(accum, I32), REG_op(t, I32)); + } + cgtest_ret_reg(tf, accum, I32); + cgtest_end(tf); + return tf->sym; +} + +/* b03_param_spill — sum9(1..9) = 45. */ +static void build_b03_param_spill(CgTestCtx* ctx) +{ + ObjSymId sum9 = build_b03_helper(ctx); + const Type* I32 = T_i32(ctx); + const Type* params[9] = { I32,I32,I32,I32,I32,I32,I32,I32,I32 }; + + CgTestFn* tf = cgtest_begin_main(ctx, I32); + Reg dst = ctx->target->alloc_reg(ctx->target, RC_INT, I32); + CgTestArg args[9]; + for (int i = 0; i < 9; ++i) { + args[i] = (CgTestArg){ .kind = CGT_ARG_IMM, .type = I32, .v.imm = i+1 }; + } + cgtest_call(tf, sum9, I32, params, args, 9, REG_op(dst, I32)); + cgtest_ret_reg(tf, dst, I32); + cgtest_end(tf); +} + +/* b04_local_int — alloc local int, store 42, load it back, return. */ +static void build_b04_local_int(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + FrameSlot s = cgtest_local(tf, I32, FSF_NONE); + cgtest_store_local(tf, s, IMM_op(42, I32), I32); + + Reg r = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(r, I32), s, I32); + cgtest_ret_reg(tf, r, I32); + cgtest_end(tf); +} + +/* b05_addr_taken_local — addr_of forces frame residency. Take address, + * store via INDIRECT, load via INDIRECT, return. + * int x; store-to-slot 17 + * int* p = &x; + * *p = *p + 1; → 18 + * return *p; */ +static void build_b05_addr_taken_local(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + FrameSlot x = cgtest_local(tf, I32, FSF_ADDR_TAKEN); + cgtest_store_local(tf, x, IMM_op(17, I32), I32); + + Reg p = T->alloc_reg(T, RC_INT, T_ptr(ctx, I32)); + T->addr_of(T, REG_op(p, T_ptr(ctx, I32)), LOCAL_op(x, I32)); + + /* val = *p; val += 1; *p = val; return val; */ + Reg val = T->alloc_reg(T, RC_INT, I32); + MemAccess ma = { .type = I32, .size = 4, .align = 4, + .alias.kind = ALIAS_LOCAL }; + T->load (T, REG_op(val, I32), IND_op(p, 0, I32), ma); + T->binop(T, BO_IADD, REG_op(val, I32), REG_op(val, I32), IMM_op(1, I32)); + T->store(T, IND_op(p, 0, I32), REG_op(val, I32), ma); + + Reg out = T->alloc_reg(T, RC_INT, I32); + T->load(T, REG_op(out, I32), IND_op(p, 0, I32), ma); + cgtest_ret_reg(tf, out, I32); + cgtest_end(tf); +} + +/* helper used by b06: + * struct Pt { int a; int b; }; + * struct Pt mk(void) { return (struct Pt){10, 32}; } + * On AArch64 SysV a 8-byte struct returns in x0 (or split across regs); + * the harness leaves register placement to abi_func_info. The body + * stores .a=10 and .b=32 into a local struct and returns it. */ +static const Type* build_b06_pt_type(CgTestCtx* ctx) +{ + Sym tag = pool_intern_cstr(ctx->pool, "Pt"); + TagId tid = type_tag_new(ctx->pool, TAG_STRUCT, tag, (SrcLoc){0,0,0}); + TypeRecordBuilder* b = type_record_begin(ctx->pool, TY_STRUCT, tid, tag); + type_record_field(b, (Field){ + .name = pool_intern_cstr(ctx->pool, "a"), .type = T_i32(ctx) }); + type_record_field(b, (Field){ + .name = pool_intern_cstr(ctx->pool, "b"), .type = T_i32(ctx) }); + return type_record_end(ctx->pool, b); +} + +static ObjSymId build_b06_helper(CgTestCtx* ctx) +{ + const Type* PT = build_b06_pt_type(ctx); + CgTestFn* tf = cgtest_begin_func(ctx, "b06_mk", PT, NULL, 0); + CGTarget* T = ctx->target; + + /* Build the struct in a local then return its address; the backend + * uses fn->abi_info->ret to decide whether to copy into the sret + * pointer (large struct) or load into the return regs (small). */ + FrameSlot s = cgtest_local(tf, PT, FSF_NONE); + /* &s + 0 = .a, &s + 4 = .b */ + Reg base = T->alloc_reg(T, RC_INT, T_ptr(ctx, PT)); + T->addr_of(T, REG_op(base, T_ptr(ctx, PT)), LOCAL_op(s, PT)); + MemAccess ma_i32 = { .type = T_i32(ctx), .size = 4, .align = 4, + .alias.kind = ALIAS_LOCAL }; + T->store(T, IND_op(base, 0, T_i32(ctx)), IMM_op(10, T_i32(ctx)), ma_i32); + T->store(T, IND_op(base, 4, T_i32(ctx)), IMM_op(32, T_i32(ctx)), ma_i32); + + cgtest_ret_indirect(tf, s); + cgtest_end(tf); + return tf->sym; +} + +/* b06_sret — pt = mk(); return pt.a + pt.b → 42. */ +static void build_b06_sret(CgTestCtx* ctx) +{ + const Type* PT = build_b06_pt_type(ctx); + ObjSymId mk = build_b06_helper(ctx); + const Type* I32 = T_i32(ctx); + + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + /* Caller provides a local for the sret destination. The harness + * passes its address as the ret_storage operand; the backend will + * either materialize an sret pointer (large) or unpack regs into + * the local (small) per the ABI. */ + FrameSlot dst = cgtest_local(tf, PT, FSF_ADDR_TAKEN); + cgtest_call(tf, mk, PT, NULL, NULL, 0, LOCAL_op(dst, PT)); + + /* Load .a, .b, sum, return. */ + Reg base = T->alloc_reg(T, RC_INT, T_ptr(ctx, PT)); + T->addr_of(T, REG_op(base, T_ptr(ctx, PT)), LOCAL_op(dst, PT)); + MemAccess ma = { .type = I32, .size = 4, .align = 4, + .alias.kind = ALIAS_LOCAL }; + Reg ra = T->alloc_reg(T, RC_INT, I32); + Reg rb = T->alloc_reg(T, RC_INT, I32); + Reg sum = T->alloc_reg(T, RC_INT, I32); + T->load(T, REG_op(ra, I32), IND_op(base, 0, I32), ma); + T->load(T, REG_op(rb, I32), IND_op(base, 4, I32), ma); + T->binop(T, BO_IADD, REG_op(sum, I32), REG_op(ra, I32), REG_op(rb, I32)); + cgtest_ret_reg(tf, sum, I32); + cgtest_end(tf); +} + +/* helper used by b07: + * struct Pt { int a; int b; }; + * int take(struct Pt p) { return p.a + p.b; } + * Aggregate-by-value parameter. Caller builds a local Pt and passes its + * address with byval semantics; the callee receives a local copy. */ +static ObjSymId build_b07_helper(CgTestCtx* ctx) +{ + const Type* PT = build_b06_pt_type(ctx); + const Type* I32 = T_i32(ctx); + const Type* params[] = { PT }; + CgTestFn* tf = cgtest_begin_func(ctx, "b07_take", I32, params, 1); + CGTarget* T = ctx->target; + + /* The param's home slot holds the byval copy. Compute its address + * and load .a, .b. */ + Reg base = T->alloc_reg(T, RC_INT, T_ptr(ctx, PT)); + T->addr_of(T, REG_op(base, T_ptr(ctx, PT)), + LOCAL_op(cgtest_param_slot(tf, 0), PT)); + MemAccess ma = { .type = I32, .size = 4, .align = 4, + .alias.kind = ALIAS_PARAM }; + Reg ra = T->alloc_reg(T, RC_INT, I32); + Reg rb = T->alloc_reg(T, RC_INT, I32); + Reg sum = T->alloc_reg(T, RC_INT, I32); + T->load(T, REG_op(ra, I32), IND_op(base, 0, I32), ma); + T->load(T, REG_op(rb, I32), IND_op(base, 4, I32), ma); + T->binop(T, BO_IADD, REG_op(sum, I32), REG_op(ra, I32), REG_op(rb, I32)); + cgtest_ret_reg(tf, sum, I32); + cgtest_end(tf); + return tf->sym; +} + +/* b07_byval_param — take({.a=15, .b=27}) → 42. */ +static void build_b07_byval_param(CgTestCtx* ctx) +{ + const Type* PT = build_b06_pt_type(ctx); + const Type* I32 = T_i32(ctx); + ObjSymId take = build_b07_helper(ctx); + const Type* params[] = { PT }; + + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + FrameSlot src = cgtest_local(tf, PT, FSF_ADDR_TAKEN); + Reg base = T->alloc_reg(T, RC_INT, T_ptr(ctx, PT)); + T->addr_of(T, REG_op(base, T_ptr(ctx, PT)), LOCAL_op(src, PT)); + MemAccess ma = { .type = I32, .size = 4, .align = 4, + .alias.kind = ALIAS_LOCAL }; + T->store(T, IND_op(base, 0, I32), IMM_op(15, I32), ma); + T->store(T, IND_op(base, 4, I32), IMM_op(27, I32), ma); + + Reg dst = T->alloc_reg(T, RC_INT, I32); + CgTestArg args[] = { + { .kind = CGT_ARG_BYVAL_LOCAL, .type = PT, .v.slot = src } + }; + cgtest_call(tf, take, I32, params, args, 1, REG_op(dst, I32)); + cgtest_ret_reg(tf, dst, I32); + cgtest_end(tf); +} + +/* helper used by b08: int trunc(float f) { return (int)f; } */ +static ObjSymId build_b08_helper(CgTestCtx* ctx) +{ + const Type* F32 = T_f32(ctx); + const Type* I32 = T_i32(ctx); + const Type* params[] = { F32 }; + CgTestFn* tf = cgtest_begin_func(ctx, "b08_trunc", I32, params, 1); + CGTarget* T = ctx->target; + + Reg f = T->alloc_reg(T, RC_FP, F32); + cgtest_load_local(tf, REG_op(f, F32), cgtest_param_slot(tf, 0), F32); + Reg i = T->alloc_reg(T, RC_INT, I32); + T->convert(T, CV_FTOI_S, REG_op(i, I32), REG_op(f, F32)); + cgtest_ret_reg(tf, i, I32); + cgtest_end(tf); + return tf->sym; +} + +/* b08_fp_param — trunc(7.5f) → 7. The IMM operand encodes the float + * bit-pattern; backend uses cls=RC_FP + type=float to materialize via + * load_const or fmov literal. */ +static void build_b08_fp_param(CgTestCtx* ctx) +{ + const Type* F32 = T_f32(ctx); + const Type* I32 = T_i32(ctx); + ObjSymId fn = build_b08_helper(ctx); + const Type* params[] = { F32 }; + + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + /* Materialize 7.5f via load_const (immediate float). */ + static const u8 BYTES_75F[4] = { 0x00, 0x00, 0xF0, 0x40 }; /* IEEE 7.5f LE */ + Reg f = T->alloc_reg(T, RC_FP, F32); + ConstBytes cb = { .type = F32, .bytes = BYTES_75F, .size = 4, .align = 4 }; + T->load_const(T, REG_op(f, F32), cb); + + Reg dst = T->alloc_reg(T, RC_INT, I32); + CgTestArg args[] = { { .kind = CGT_ARG_REG, .type = F32, .v.reg = f } }; + cgtest_call(tf, fn, I32, params, args, 1, REG_op(dst, I32)); + cgtest_ret_reg(tf, dst, I32); cgtest_end(tf); } @@ -86,74 +505,207 @@ static void build_a04_copy_reg(CgTestCtx* ctx) /* c01_add — 1 + 2 = 3 */ static void build_c01_add(CgTestCtx* ctx) { - CgTestFn* tf = cgtest_begin_main(ctx, T_I32); + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); CGTarget* T = ctx->target; - Reg a = T->alloc_reg(T, RC_INT, T_I32); - Reg b = T->alloc_reg(T, RC_INT, T_I32); - Reg d = T->alloc_reg(T, RC_INT, T_I32); - T->load_imm(T, REG_i32(a), 1); - T->load_imm(T, REG_i32(b), 2); - T->binop(T, BO_IADD, REG_i32(d), REG_i32(a), REG_i32(b)); - cgtest_ret_reg(tf, d, T_I32); + Reg a = T->alloc_reg(T, RC_INT, I32); + Reg b = T->alloc_reg(T, RC_INT, I32); + Reg d = T->alloc_reg(T, RC_INT, I32); + T->load_imm(T, REG_op(a, I32), 1); + T->load_imm(T, REG_op(b, I32), 2); + T->binop(T, BO_IADD, REG_op(d, I32), REG_op(a, I32), REG_op(b, I32)); + cgtest_ret_reg(tf, d, I32); cgtest_end(tf); } /* c02_sub_mul — 7 * 3 - 4 = 17 */ static void build_c02_sub_mul(CgTestCtx* ctx) { - CgTestFn* tf = cgtest_begin_main(ctx, T_I32); + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); CGTarget* T = ctx->target; - Reg r7 = T->alloc_reg(T, RC_INT, T_I32); - Reg r3 = T->alloc_reg(T, RC_INT, T_I32); - Reg r4 = T->alloc_reg(T, RC_INT, T_I32); - Reg rmul = T->alloc_reg(T, RC_INT, T_I32); - Reg rsub = T->alloc_reg(T, RC_INT, T_I32); - T->load_imm(T, REG_i32(r7), 7); - T->load_imm(T, REG_i32(r3), 3); - T->load_imm(T, REG_i32(r4), 4); - T->binop(T, BO_IMUL, REG_i32(rmul), REG_i32(r7), REG_i32(r3)); - T->binop(T, BO_ISUB, REG_i32(rsub), REG_i32(rmul), REG_i32(r4)); - cgtest_ret_reg(tf, rsub, T_I32); + Reg r7 = T->alloc_reg(T, RC_INT, I32); + Reg r3 = T->alloc_reg(T, RC_INT, I32); + Reg r4 = T->alloc_reg(T, RC_INT, I32); + Reg rmul = T->alloc_reg(T, RC_INT, I32); + Reg rsub = T->alloc_reg(T, RC_INT, I32); + T->load_imm(T, REG_op(r7, I32), 7); + T->load_imm(T, REG_op(r3, I32), 3); + T->load_imm(T, REG_op(r4, I32), 4); + T->binop(T, BO_IMUL, REG_op(rmul, I32), REG_op(r7, I32), REG_op(r3, I32)); + T->binop(T, BO_ISUB, REG_op(rsub, I32), REG_op(rmul, I32), REG_op(r4, I32)); + cgtest_ret_reg(tf, rsub, I32); cgtest_end(tf); } /* c03_bitwise — (~3) & 0xff = 252 */ static void build_c03_bitwise(CgTestCtx* ctx) { - CgTestFn* tf = cgtest_begin_main(ctx, T_I32); + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); CGTarget* T = ctx->target; - Reg r3 = T->alloc_reg(T, RC_INT, T_I32); - Reg rinv = T->alloc_reg(T, RC_INT, T_I32); - Reg rmask= T->alloc_reg(T, RC_INT, T_I32); - Reg rand = T->alloc_reg(T, RC_INT, T_I32); - T->load_imm(T, REG_i32(r3), 3); - T->load_imm(T, REG_i32(rmask),0xff); - T->unop (T, UO_BNOT, REG_i32(rinv), REG_i32(r3)); - T->binop(T, BO_AND, REG_i32(rand), REG_i32(rinv), REG_i32(rmask)); - cgtest_ret_reg(tf, rand, T_I32); + Reg r3 = T->alloc_reg(T, RC_INT, I32); + Reg rinv = T->alloc_reg(T, RC_INT, I32); + Reg rmask= T->alloc_reg(T, RC_INT, I32); + Reg rand_ = T->alloc_reg(T, RC_INT, I32); + T->load_imm(T, REG_op(r3, I32), 3); + T->load_imm(T, REG_op(rmask, I32), 0xff); + T->unop (T, UO_BNOT, REG_op(rinv, I32), REG_op(r3, I32)); + T->binop(T, BO_AND, REG_op(rand_, I32), REG_op(rinv, I32), REG_op(rmask, I32)); + cgtest_ret_reg(tf, rand_, I32); cgtest_end(tf); } /* c04_shift — (1<<5) | (16>>1) = 40 */ static void build_c04_shift(CgTestCtx* ctx) { - CgTestFn* tf = cgtest_begin_main(ctx, T_I32); + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); CGTarget* T = ctx->target; - Reg r1 = T->alloc_reg(T, RC_INT, T_I32); - Reg r5 = T->alloc_reg(T, RC_INT, T_I32); - Reg r16 = T->alloc_reg(T, RC_INT, T_I32); - Reg r1s = T->alloc_reg(T, RC_INT, T_I32); - Reg rshl= T->alloc_reg(T, RC_INT, T_I32); - Reg rshr= T->alloc_reg(T, RC_INT, T_I32); - Reg ror = T->alloc_reg(T, RC_INT, T_I32); - T->load_imm(T, REG_i32(r1), 1); - T->load_imm(T, REG_i32(r5), 5); - T->load_imm(T, REG_i32(r16), 16); - T->load_imm(T, REG_i32(r1s), 1); - T->binop(T, BO_SHL, REG_i32(rshl), REG_i32(r1), REG_i32(r5)); - T->binop(T, BO_SHR_U, REG_i32(rshr), REG_i32(r16), REG_i32(r1s)); - T->binop(T, BO_OR, REG_i32(ror), REG_i32(rshl),REG_i32(rshr)); - cgtest_ret_reg(tf, ror, T_I32); + Reg r1 = T->alloc_reg(T, RC_INT, I32); + Reg r5 = T->alloc_reg(T, RC_INT, I32); + Reg r16 = T->alloc_reg(T, RC_INT, I32); + Reg r1s = T->alloc_reg(T, RC_INT, I32); + Reg rshl= T->alloc_reg(T, RC_INT, I32); + Reg rshr= T->alloc_reg(T, RC_INT, I32); + Reg ror = T->alloc_reg(T, RC_INT, I32); + T->load_imm(T, REG_op(r1, I32), 1); + T->load_imm(T, REG_op(r5, I32), 5); + T->load_imm(T, REG_op(r16, I32), 16); + T->load_imm(T, REG_op(r1s, I32), 1); + T->binop(T, BO_SHL, REG_op(rshl, I32), REG_op(r1, I32), REG_op(r5, I32)); + T->binop(T, BO_SHR_U, REG_op(rshr, I32), REG_op(r16, I32), REG_op(r1s, I32)); + T->binop(T, BO_OR, REG_op(ror, I32), REG_op(rshl, I32),REG_op(rshr, I32)); + cgtest_ret_reg(tf, ror, I32); + cgtest_end(tf); +} + +/* c05_div_mod — 23/4 + 23%4 = 5 + 3 = 8 (signed) */ +static void build_c05_div_mod(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + Reg r23 = T->alloc_reg(T, RC_INT, I32); + Reg r4 = T->alloc_reg(T, RC_INT, I32); + Reg rd = T->alloc_reg(T, RC_INT, I32); + Reg rm = T->alloc_reg(T, RC_INT, I32); + Reg rs = T->alloc_reg(T, RC_INT, I32); + T->load_imm(T, REG_op(r23, I32), 23); + T->load_imm(T, REG_op(r4, I32), 4); + T->binop(T, BO_SDIV, REG_op(rd, I32), REG_op(r23, I32), REG_op(r4, I32)); + T->binop(T, BO_SREM, REG_op(rm, I32), REG_op(r23, I32), REG_op(r4, I32)); + T->binop(T, BO_IADD, REG_op(rs, I32), REG_op(rd, I32), REG_op(rm, I32)); + cgtest_ret_reg(tf, rs, I32); + cgtest_end(tf); +} + +/* c06_xor — 0xa5 ^ 0x5a = 0xff = 255 */ +static void build_c06_xor(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + Reg ra = T->alloc_reg(T, RC_INT, I32); + Reg rb = T->alloc_reg(T, RC_INT, I32); + Reg rx = T->alloc_reg(T, RC_INT, I32); + T->load_imm(T, REG_op(ra, I32), 0xa5); + T->load_imm(T, REG_op(rb, I32), 0x5a); + T->binop(T, BO_XOR, REG_op(rx, I32), REG_op(ra, I32), REG_op(rb, I32)); + cgtest_ret_reg(tf, rx, I32); + cgtest_end(tf); +} + +/* c07_iadd_i64 — i64 add. Two i64 values added; low 32 bits returned. + * (1<<32 | 0x29) + (1<<32 | 0x01) = (2<<32 | 0x2A) → low 32 = 42. */ +static void build_c07_iadd_i64(CgTestCtx* ctx) +{ + const Type* I64 = T_i64(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I64); + CGTarget* T = ctx->target; + Reg ra = T->alloc_reg(T, RC_INT, I64); + Reg rb = T->alloc_reg(T, RC_INT, I64); + Reg rs = T->alloc_reg(T, RC_INT, I64); + T->load_imm(T, REG_op(ra, I64), 0x100000029ll); + T->load_imm(T, REG_op(rb, I64), 0x100000001ll); + T->binop(T, BO_IADD, REG_op(rs, I64), REG_op(ra, I64), REG_op(rb, I64)); + cgtest_ret_reg(tf, rs, I64); + cgtest_end(tf); +} + +/* c08_unsigned_div — 100u / 7u = 14 */ +static void build_c08_unsigned_div(CgTestCtx* ctx) +{ + const Type* U32 = T_u32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, U32); + CGTarget* T = ctx->target; + Reg r100 = T->alloc_reg(T, RC_INT, U32); + Reg r7 = T->alloc_reg(T, RC_INT, U32); + Reg rq = T->alloc_reg(T, RC_INT, U32); + T->load_imm(T, REG_op(r100, U32), 100); + T->load_imm(T, REG_op(r7, U32), 7); + T->binop(T, BO_UDIV, REG_op(rq, U32), REG_op(r100, U32), REG_op(r7, U32)); + cgtest_ret_reg(tf, rq, U32); + cgtest_end(tf); +} + +/* c09_neg — UO_NEG. -42 → exit 256-42 = 214. */ +static void build_c09_neg(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + Reg r = T->alloc_reg(T, RC_INT, I32); + Reg n = T->alloc_reg(T, RC_INT, I32); + T->load_imm(T, REG_op(r, I32), 42); + T->unop(T, UO_NEG, REG_op(n, I32), REG_op(r, I32)); + cgtest_ret_reg(tf, n, I32); + cgtest_end(tf); +} + +/* c10_logical_not — UO_NOT yields 0/1 from any int. !0 = 1. */ +static void build_c10_logical_not(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + Reg z = T->alloc_reg(T, RC_INT, I32); + Reg ln = T->alloc_reg(T, RC_INT, I32); + T->load_imm(T, REG_op(z, I32), 0); + T->unop(T, UO_NOT, REG_op(ln, I32), REG_op(z, I32)); + cgtest_ret_reg(tf, ln, I32); + cgtest_end(tf); +} + +/* c11_shr_signed — -16 >>(s) 2 = -4 → exit 252. */ +static void build_c11_shr_signed(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + Reg rv = T->alloc_reg(T, RC_INT, I32); + Reg r2 = T->alloc_reg(T, RC_INT, I32); + Reg rs = T->alloc_reg(T, RC_INT, I32); + T->load_imm(T, REG_op(rv, I32), -16); + T->load_imm(T, REG_op(r2, I32), 2); + T->binop(T, BO_SHR_S, REG_op(rs, I32), REG_op(rv, I32), REG_op(r2, I32)); + cgtest_ret_reg(tf, rs, I32); + cgtest_end(tf); +} + +/* c12_imul_i64 — i64 mul. 7 * 6 = 42; high bits zero. Exit 42. */ +static void build_c12_imul_i64(CgTestCtx* ctx) +{ + const Type* I64 = T_i64(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I64); + CGTarget* T = ctx->target; + Reg r7 = T->alloc_reg(T, RC_INT, I64); + Reg r6 = T->alloc_reg(T, RC_INT, I64); + Reg rm = T->alloc_reg(T, RC_INT, I64); + T->load_imm(T, REG_op(r7, I64), 7); + T->load_imm(T, REG_op(r6, I64), 6); + T->binop(T, BO_IMUL, REG_op(rm, I64), REG_op(r7, I64), REG_op(r6, I64)); + cgtest_ret_reg(tf, rm, I64); cgtest_end(tf); } @@ -163,19 +715,43 @@ static void build_c04_shift(CgTestCtx* ctx) const CgCase cg_cases[] = { /* MC-only */ - { "mc_smoke", build_mc_smoke, 42, CG_CASE_MC_ONLY }, - - /* Group A */ - { "a01_return_const_42", build_a01_return_const_42, 42, CG_CASE_DEFAULT }, - { "a02_return_zero", build_a02_return_zero, 0, CG_CASE_DEFAULT }, - { "a03_ret_imm", build_a03_ret_imm, 17, CG_CASE_DEFAULT }, - { "a04_copy_reg", build_a04_copy_reg, 7, CG_CASE_DEFAULT }, - - /* Group C */ - { "c01_add", build_c01_add, 3, CG_CASE_DEFAULT }, - { "c02_sub_mul", build_c02_sub_mul, 17, CG_CASE_DEFAULT }, - { "c03_bitwise", build_c03_bitwise, 252, CG_CASE_DEFAULT }, - { "c04_shift", build_c04_shift, 40, CG_CASE_DEFAULT }, + { "mc_smoke", build_mc_smoke, 42, CG_CASE_MC_ONLY }, + + /* Group A — function lifecycle and return */ + { "a01_return_const_42", build_a01_return_const_42, 42, CG_CASE_DEFAULT }, + { "a02_return_zero", build_a02_return_zero, 0, CG_CASE_DEFAULT }, + { "a03_ret_imm", build_a03_ret_imm, 17, CG_CASE_DEFAULT }, + { "a04_copy_reg", build_a04_copy_reg, 7, CG_CASE_DEFAULT }, + { "a05_return_neg_small", build_a05_return_neg_small, 249, CG_CASE_DEFAULT }, + { "a06_return_i64", build_a06_return_i64, 42, CG_CASE_DEFAULT }, + { "a07_void_return", build_a07_void_return, 0, CG_CASE_DEFAULT }, + { "a08_multiple_returns", build_a08_multiple_returns, 1, CG_CASE_DEFAULT }, + { "a09_load_imm_movz_movk", build_a09_load_imm_movz_movk, 205, CG_CASE_DEFAULT }, + { "a10_return_u8", build_a10_return_u8, 200, CG_CASE_DEFAULT }, + + /* Group B — frame slots, parameters, locals */ + { "b01_param_int", build_b01_param_int, 201, CG_CASE_DEFAULT }, + { "b02_param_sum", build_b02_param_sum, 42, CG_CASE_DEFAULT }, + { "b03_param_spill", build_b03_param_spill, 45, CG_CASE_DEFAULT }, + { "b04_local_int", build_b04_local_int, 42, CG_CASE_DEFAULT }, + { "b05_addr_taken_local", build_b05_addr_taken_local, 18, CG_CASE_DEFAULT }, + { "b06_sret", build_b06_sret, 42, CG_CASE_DEFAULT }, + { "b07_byval_param", build_b07_byval_param, 42, CG_CASE_DEFAULT }, + { "b08_fp_param", build_b08_fp_param, 7, CG_CASE_DEFAULT }, + + /* Group C — integer arithmetic */ + { "c01_add", build_c01_add, 3, CG_CASE_DEFAULT }, + { "c02_sub_mul", build_c02_sub_mul, 17, CG_CASE_DEFAULT }, + { "c03_bitwise", build_c03_bitwise, 252, CG_CASE_DEFAULT }, + { "c04_shift", build_c04_shift, 40, CG_CASE_DEFAULT }, + { "c05_div_mod", build_c05_div_mod, 8, CG_CASE_DEFAULT }, + { "c06_xor", build_c06_xor, 255, CG_CASE_DEFAULT }, + { "c07_iadd_i64", build_c07_iadd_i64, 42, CG_CASE_DEFAULT }, + { "c08_unsigned_div", build_c08_unsigned_div, 14, CG_CASE_DEFAULT }, + { "c09_neg", build_c09_neg, 214, CG_CASE_DEFAULT }, + { "c10_logical_not", build_c10_logical_not, 1, CG_CASE_DEFAULT }, + { "c11_shr_signed", build_c11_shr_signed, 252, CG_CASE_DEFAULT }, + { "c12_imul_i64", build_c12_imul_i64, 42, CG_CASE_DEFAULT }, }; const unsigned cg_cases_count = sizeof(cg_cases) / sizeof(cg_cases[0]); diff --git a/test/cg/harness/cg_test.c b/test/cg/harness/cg_test.c @@ -1,135 +1,360 @@ -/* test/cg fixture API implementation. */ +/* test/cg fixture API implementation. + * + * Drives the same building blocks the parser will: pool-interned Types, + * abi_func_info classification, and the CGTarget lowering interface. No + * ABI mocks; CGFuncDesc/CGParamDesc/CGCallDesc/CGABIValue are populated + * from `c->abi` exactly as the parser will populate them. */ #include "cg_test.h" #include "core/pool.h" +#include "core/arena.h" #include <string.h> -/* ---- static pre-interned types ---- - * These are not pool_interned (pool_type is a stub until the parser comes - * online), but the AArch64 backend only reads Type.kind to derive scalar - * width, which static literals satisfy. */ -static const Type s_void = { .kind = TY_VOID }; -static const Type s_i8 = { .kind = TY_SCHAR }; -static const Type s_i16 = { .kind = TY_SHORT }; -static const Type s_i32 = { .kind = TY_INT }; -static const Type s_i64 = { .kind = TY_LLONG }; -static const Type s_u8 = { .kind = TY_UCHAR }; -static const Type s_u16 = { .kind = TY_USHORT }; -static const Type s_u32 = { .kind = TY_UINT }; -static const Type s_u64 = { .kind = TY_ULLONG }; -static const Type s_ptr_v = { .kind = TY_PTR }; - -const Type*T_VOID = &s_void; -const Type*T_I8 = &s_i8; -const Type*T_I16 = &s_i16; -const Type*T_I32 = &s_i32; -const Type*T_I64 = &s_i64; -const Type*T_U8 = &s_u8; -const Type*T_U16 = &s_u16; -const Type*T_U32 = &s_u32; -const Type*T_U64 = &s_u64; -const Type*T_PTR_VOID= &s_ptr_v; +/* ---- pre-interned type accessors ---- */ + +const Type* T_void (CgTestCtx* x) { return type_void(x->pool); } +const Type* T_i8 (CgTestCtx* x) { return type_prim(x->pool, TY_SCHAR); } +const Type* T_u8 (CgTestCtx* x) { return type_prim(x->pool, TY_UCHAR); } +const Type* T_i16 (CgTestCtx* x) { return type_prim(x->pool, TY_SHORT); } +const Type* T_u16 (CgTestCtx* x) { return type_prim(x->pool, TY_USHORT); } +const Type* T_i32 (CgTestCtx* x) { return type_prim(x->pool, TY_INT); } +const Type* T_u32 (CgTestCtx* x) { return type_prim(x->pool, TY_UINT); } +const Type* T_i64 (CgTestCtx* x) { return type_prim(x->pool, TY_LLONG); } +const Type* T_u64 (CgTestCtx* x) { return type_prim(x->pool, TY_ULLONG); } +const Type* T_f32 (CgTestCtx* x) { return type_prim(x->pool, TY_FLOAT); } +const Type* T_f64 (CgTestCtx* x) { return type_prim(x->pool, TY_DOUBLE); } +const Type* T_ptr_void(CgTestCtx* x){ return type_ptr (x->pool, type_void(x->pool)); } +const Type* T_ptr (CgTestCtx* x, const Type* p) { return type_ptr(x->pool, p); } /* ---- operand sugar ---- */ -Operand IMM_i32(i64 v) +Operand IMM_op(i64 v, const Type* ty) { Operand o = { 0 }; - o.kind = OPK_IMM; o.cls = RC_INT; o.type = T_I32; + o.kind = OPK_IMM; + o.cls = (ty && (ty->kind == TY_FLOAT || ty->kind == TY_DOUBLE + || ty->kind == TY_LDOUBLE)) ? RC_FP : RC_INT; + o.type = ty; o.v.imm = v; return o; } -Operand IMM_i64(i64 v) +Operand REG_op(Reg r, const Type* ty) { Operand o = { 0 }; - o.kind = OPK_IMM; o.cls = RC_INT; o.type = T_I64; - o.v.imm = v; + o.kind = OPK_REG; + o.cls = (ty && (ty->kind == TY_FLOAT || ty->kind == TY_DOUBLE + || ty->kind == TY_LDOUBLE)) ? RC_FP : RC_INT; + o.type = ty; + o.v.reg = r; return o; } -Operand REG_i32(Reg r) +Operand LOCAL_op(FrameSlot s, const Type* ty) { Operand o = { 0 }; - o.kind = OPK_REG; o.cls = RC_INT; o.type = T_I32; - o.v.reg = r; + o.kind = OPK_LOCAL; + o.cls = RC_INT; /* address class is INT */ + o.type = ty; + o.v.frame_slot = s; return o; } -Operand REG_i64(Reg r) +Operand IND_op(Reg base, i32 ofs, const Type* ty) { Operand o = { 0 }; - o.kind = OPK_REG; o.cls = RC_INT; o.type = T_I64; - o.v.reg = r; + o.kind = OPK_INDIRECT; + o.cls = (ty && (ty->kind == TY_FLOAT || ty->kind == TY_DOUBLE + || ty->kind == TY_LDOUBLE)) ? RC_FP : RC_INT; + o.type = ty; + o.v.ind.base = base; + o.v.ind.ofs = ofs; + return o; +} +Operand GLOBAL_op(ObjSymId sym, i64 addend) +{ + Operand o = { 0 }; + o.kind = OPK_GLOBAL; + o.cls = RC_INT; + o.type = NULL; + o.v.global.sym = sym; + o.v.global.addend = addend; return o; } +/* ---- internal helpers ---- */ + +static MemAccess default_memaccess(CgTestCtx* ctx, const Type* ty) +{ + MemAccess ma = { 0 }; + ma.type = ty; + ma.size = abi_sizeof(ctx->c->abi, ty); + ma.align = abi_alignof(ctx->c->abi, ty); + ma.flags = MF_NONE; + ma.alias.kind = ALIAS_LOCAL; + return ma; +} + /* ---- function-fixture helpers ---- */ CgTestFn* cgtest_begin_main(CgTestCtx* ctx, const Type* ret_ty) { - /* Allocate fixture state in tu arena so its lifetime spans the case. */ + return cgtest_begin_func(ctx, "test_main", ret_ty, NULL, 0); +} + +CgTestFn* cgtest_begin_func(CgTestCtx* ctx, + const char* name, + const Type* ret_ty, + const Type* const* param_types, + u32 nparams) +{ CgTestFn* tf = arena_new(ctx->c->tu, CgTestFn); memset(tf, 0, sizeof *tf); tf->ctx = ctx; tf->ret_ty = ret_ty; - Sym name = pool_intern_cstr(ctx->pool, "test_main"); - tf->sym = obj_symbol(ctx->ob, name, SB_GLOBAL, SK_FUNC, + /* Build TY_FUNC and classify with the live TargetABI. */ + const Type** ptypes = NULL; + if (nparams) { + ptypes = arena_array(ctx->c->tu, const Type*, nparams); + for (u32 i = 0; i < nparams; ++i) ptypes[i] = param_types[i]; + } + tf->fn_type = type_func(ctx->pool, ret_ty, ptypes, (u16)nparams, 0); + tf->abi_info = abi_func_info(ctx->c->abi, tf->fn_type); + + /* Symbol. */ + Sym sname = pool_intern_cstr(ctx->pool, name); + tf->sym = obj_symbol(ctx->ob, sname, SB_GLOBAL, SK_FUNC, OBJ_SEC_NONE, 0, 0); + /* Param slots + descriptors. Frame slots must be allocated against the + * function's frame, which begins at func_begin — so we do this AFTER + * func_begin below. We pre-allocate the descriptor array here. */ + CGParamDesc* pds = NULL; + if (nparams) { + tf->params = arena_array(ctx->c->tu, CgTestParam, nparams); + memset(tf->params, 0, sizeof(CgTestParam) * nparams); + pds = arena_array(ctx->c->tu, CGParamDesc, nparams); + memset(pds, 0, sizeof(CGParamDesc) * nparams); + for (u32 i = 0; i < nparams; ++i) { + tf->params[i].type = ptypes[i]; + tf->params[i].abi = &tf->abi_info->params[i]; + pds[i].index = i; + pds[i].name = 0; + pds[i].type = ptypes[i]; + pds[i].slot = FRAME_SLOT_NONE; /* filled below */ + pds[i].abi = &tf->abi_info->params[i]; + pds[i].incoming = tf->abi_info->params[i].parts; + pds[i].nincoming= tf->abi_info->params[i].nparts; + pds[i].loc = (SrcLoc){0,0,0}; + } + } + tf->nparams = nparams; + tf->fd.sym = tf->sym; tf->fd.text_section_id = ctx->text_sec; tf->fd.group_id = OBJ_GROUP_NONE; - tf->fd.fn_type = NULL; /* unused by minimal AArch64 backend */ - tf->fd.abi = NULL; - tf->fd.params = NULL; - tf->fd.nparams = 0; + tf->fd.fn_type = tf->fn_type; + tf->fd.abi = tf->abi_info; + tf->fd.params = pds; + tf->fd.nparams = nparams; tf->fd.loc = (SrcLoc){0, 0, 0}; ctx->target->func_begin(ctx->target, &tf->fd); + + /* Allocate FS_PARAM slots and dispatch param() in declaration order. */ + for (u32 i = 0; i < nparams; ++i) { + FrameSlotDesc fsd = { + .type = ptypes[i], + .name = 0, + .loc = (SrcLoc){0,0,0}, + .size = abi_sizeof (ctx->c->abi, ptypes[i]), + .align = abi_alignof(ctx->c->abi, ptypes[i]), + .kind = FS_PARAM, + .flags = FSF_NONE, + }; + FrameSlot s = ctx->target->frame_slot(ctx->target, &fsd); + tf->params[i].slot = s; + pds[i].slot = s; + ctx->target->param(ctx->target, &pds[i]); + } return tf; } +FrameSlot cgtest_param_slot(CgTestFn* tf, u32 idx) +{ + return tf->params[idx].slot; +} + +/* ---- frame slots and memory ---- */ + +FrameSlot cgtest_local(CgTestFn* tf, const Type* ty, u16 flags) +{ + FrameSlotDesc fsd = { + .type = ty, + .name = 0, + .loc = (SrcLoc){0,0,0}, + .size = abi_sizeof (tf->ctx->c->abi, ty), + .align = abi_alignof(tf->ctx->c->abi, ty), + .kind = FS_LOCAL, + .flags = flags, + }; + return tf->ctx->target->frame_slot(tf->ctx->target, &fsd); +} + +void cgtest_load_local(CgTestFn* tf, Operand dst_reg, FrameSlot s, const Type* ty) +{ + MemAccess ma = default_memaccess(tf->ctx, ty); + tf->ctx->target->load(tf->ctx->target, dst_reg, LOCAL_op(s, ty), ma); +} + +void cgtest_store_local(CgTestFn* tf, FrameSlot s, Operand src, const Type* ty) +{ + MemAccess ma = default_memaccess(tf->ctx, ty); + tf->ctx->target->store(tf->ctx->target, LOCAL_op(s, ty), src, ma); +} + +/* ---- return ---- */ + void cgtest_ret_reg(CgTestFn* tf, Reg r, const Type* ty) { - CgTestCtx* ctx = tf->ctx; - memset(&tf->ret_val, 0, sizeof tf->ret_val); - tf->ret_val.type = ty; - tf->ret_val.abi = NULL; - tf->ret_val.storage.kind = OPK_REG; - tf->ret_val.storage.cls = RC_INT; - tf->ret_val.storage.type = ty; - tf->ret_val.storage.v.reg = r; - tf->ret_val.parts = NULL; - tf->ret_val.nparts = 0; - ctx->target->ret(ctx->target, &tf->ret_val); + CGABIValue v = { 0 }; + v.type = ty; + v.abi = &tf->abi_info->ret; + v.storage = REG_op(r, ty); + v.parts = NULL; + v.nparts = 0; + tf->ctx->target->ret(tf->ctx->target, &v); } void cgtest_ret_imm(CgTestFn* tf, i64 imm, const Type* ty) { - CgTestCtx* ctx = tf->ctx; - memset(&tf->ret_val, 0, sizeof tf->ret_val); - tf->ret_val.type = ty; - tf->ret_val.abi = NULL; - tf->ret_val.storage.kind = OPK_IMM; - tf->ret_val.storage.cls = RC_INT; - tf->ret_val.storage.type = ty; - tf->ret_val.storage.v.imm = imm; - tf->ret_val.parts = NULL; - tf->ret_val.nparts = 0; - ctx->target->ret(ctx->target, &tf->ret_val); + CGABIValue v = { 0 }; + v.type = ty; + v.abi = &tf->abi_info->ret; + v.storage = IMM_op(imm, ty); + v.parts = NULL; + v.nparts = 0; + tf->ctx->target->ret(tf->ctx->target, &v); } void cgtest_ret_void(CgTestFn* tf) { - CgTestCtx* ctx = tf->ctx; - ctx->target->ret(ctx->target, NULL); + tf->ctx->target->ret(tf->ctx->target, NULL); +} + +void cgtest_ret_indirect(CgTestFn* tf, FrameSlot addr_local) +{ + CGABIValue v = { 0 }; + v.type = tf->ret_ty; + v.abi = &tf->abi_info->ret; + v.storage = LOCAL_op(addr_local, tf->ret_ty); + v.parts = NULL; + v.nparts = 0; + tf->ctx->target->ret(tf->ctx->target, &v); +} + +void cgtest_ret_struct_in_regs(CgTestFn* tf, const Reg* part_regs, u32 nparts) +{ + CGABIValue v = { 0 }; + const ABIArgInfo* a = &tf->abi_info->ret; + CGABIPart* parts = arena_array(tf->ctx->c->tu, CGABIPart, nparts); + memset(parts, 0, sizeof(CGABIPart) * nparts); + for (u32 i = 0; i < nparts; ++i) { + parts[i].abi_part = &a->parts[i]; + parts[i].op = REG_op(part_regs[i], NULL); + parts[i].src_offset = a->parts[i].src_offset; + parts[i].size = a->parts[i].size; + parts[i].flags = CG_ABI_PART_NONE; + } + v.type = tf->ret_ty; + v.abi = a; + v.storage = (Operand){ 0 }; + v.parts = parts; + v.nparts = nparts; + tf->ctx->target->ret(tf->ctx->target, &v); } void cgtest_end(CgTestFn* tf) { - CgTestCtx* ctx = tf->ctx; - ctx->target->func_end(ctx->target); + tf->ctx->target->func_end(tf->ctx->target); +} + +/* ---- direct calls ---- */ + +void cgtest_call(CgTestFn* caller, + ObjSymId callee_sym, + const Type* ret_ty, + const Type* const* arg_types, + const CgTestArg* args, + u32 nargs, + Operand ret_storage) +{ + CgTestCtx* ctx = caller->ctx; + + /* Build callee fn_type and ABIFuncInfo independently of the caller's. */ + const Type** ptypes = NULL; + if (nargs) { + ptypes = arena_array(ctx->c->tu, const Type*, nargs); + for (u32 i = 0; i < nargs; ++i) ptypes[i] = arg_types[i]; + } + const Type* fn_ty = type_func(ctx->pool, ret_ty, ptypes, (u16)nargs, 0); + const ABIFuncInfo* info = abi_func_info(ctx->c->abi, fn_ty); + + /* Materialize a CGABIValue per arg. */ + CGABIValue* avs = NULL; + if (nargs) { + avs = arena_array(ctx->c->tu, CGABIValue, nargs); + memset(avs, 0, sizeof(CGABIValue) * nargs); + for (u32 i = 0; i < nargs; ++i) { + CGABIValue* av = &avs[i]; + av->type = arg_types[i]; + av->abi = &info->params[i]; + av->parts = NULL; + av->nparts = 0; + switch (args[i].kind) { + case CGT_ARG_IMM: + av->storage = IMM_op(args[i].v.imm, arg_types[i]); + break; + case CGT_ARG_REG: + av->storage = REG_op(args[i].v.reg, arg_types[i]); + break; + case CGT_ARG_LOCAL_VALUE: { + /* Load into a fresh reg; storage is the reg. */ + Reg r = ctx->target->alloc_reg(ctx->target, + (av->abi->parts && av->abi->parts[0].cls == ABI_CLASS_FP) + ? RC_FP : RC_INT, + arg_types[i]); + cgtest_load_local(caller, REG_op(r, arg_types[i]), + args[i].v.slot, arg_types[i]); + av->storage = REG_op(r, arg_types[i]); + break; + } + case CGT_ARG_BYVAL_LOCAL: + case CGT_ARG_INDIRECT_LOCAL: + /* Storage is the address of the local; backend reads + * abi.flags (BYVAL/INDIRECT) and copies as needed. */ + av->storage = LOCAL_op(args[i].v.slot, arg_types[i]); + break; + default: + break; + } + } + } + + CGCallDesc desc; memset(&desc, 0, sizeof desc); + desc.fn_type = fn_ty; + desc.abi = info; + desc.callee = GLOBAL_op(callee_sym, 0); + desc.args = avs; + desc.nargs = nargs; + desc.flags = CG_CALL_NONE; + desc.ret.type = ret_ty; + desc.ret.abi = &info->ret; + desc.ret.storage = ret_storage; + desc.ret.parts = NULL; + desc.ret.nparts = 0; + + ctx->target->call(ctx->target, &desc); } /* ---- MC-only case helpers ---- */ @@ -139,8 +364,6 @@ ObjSymId cgtest_mc_begin_main(CgTestCtx* ctx) Sym name = pool_intern_cstr(ctx->pool, "test_main"); ObjSymId sym = obj_symbol(ctx->ob, name, SB_GLOBAL, SK_FUNC, OBJ_SEC_NONE, 0, 0); - /* Caller is responsible for set_section + emit_align before recording - * the start position. */ return sym; } diff --git a/test/cg/harness/cg_test.h b/test/cg/harness/cg_test.h @@ -10,10 +10,18 @@ * --jit NAME : link in-process and call test_main, exit with result * --list : list every registered case name * - * Pre-interned `Type`s here are static literals, not pool-interned. The - * AArch64 backend only inspects Type.kind to derive scalar width, which the - * literals satisfy. When the parser+TargetABI come online, the harness will - * switch to pool_type-interned Types but cases shouldn't need changes. */ + * The harness drives the same building blocks the parser will use: + * type_* — to construct interned Types (TY_INT, TY_PTR, TY_FUNC, ...) + * abi_* — to classify return + parameters (abi_func_info) + * CGTarget — to lower function lifecycle, params, locals, calls, ret + * MCEmitter — for raw byte emission (mc_smoke and similar) + * + * No ABI mocks: the harness asks the live TargetABI for ABIFuncInfo from a + * pool-interned function Type. That is the same contract the parser will + * rely on, so test cases here double as a behavioral spec for those + * interfaces. Cases requiring features the lib does not yet implement + * (type_func, abi_func_info, the call/param/aggregate methods on CGTarget) + * fail at link/runtime until the dependencies land — that is intentional. */ #ifndef CFREE_TEST_CG_TEST_H #define CFREE_TEST_CG_TEST_H @@ -52,49 +60,153 @@ typedef struct CgCase { extern const CgCase cg_cases[]; extern const unsigned cg_cases_count; -/* ---- pre-interned types ---- - * Static Type literals; backend reads only .kind for width. */ -extern const Type *T_VOID, *T_I8, *T_I16, *T_I32, *T_I64, - *T_U8, *T_U16, *T_U32, *T_U64, - *T_PTR_VOID; +/* ---- pre-interned type accessors ---- + * Resolved once per ctx via type_prim/type_void/type_ptr against + * ctx->pool. Sugar so cases don't repeat the lookup. */ +const Type* T_void (CgTestCtx*); +const Type* T_i8 (CgTestCtx*); +const Type* T_u8 (CgTestCtx*); +const Type* T_i16 (CgTestCtx*); +const Type* T_u16 (CgTestCtx*); +const Type* T_i32 (CgTestCtx*); +const Type* T_u32 (CgTestCtx*); +const Type* T_i64 (CgTestCtx*); +const Type* T_u64 (CgTestCtx*); +const Type* T_f32 (CgTestCtx*); +const Type* T_f64 (CgTestCtx*); +const Type* T_ptr_void(CgTestCtx*); +const Type* T_ptr (CgTestCtx*, const Type* pointee); /* ---- operand sugar ---- */ -Operand IMM_i32 (i64 v); -Operand IMM_i64 (i64 v); -Operand REG_i32 (Reg r); -Operand REG_i64 (Reg r); - -/* ---- function-fixture helpers ---- - * cgtest_begin_main creates an undefined `test_main` symbol of kind SK_FUNC - * and calls CGTarget.func_begin against the .text section. Use it from any - * CGTarget-driving case. - * - * cgtest_ret_reg materializes a CGABIValue holding the given register and - * type, then calls CGTarget.ret. Use it for cases that compute a value in - * a backend-allocated register and return it as int. - * - * cgtest_end calls CGTarget.func_end. */ +Operand IMM_op (i64 v, const Type* ty); +Operand REG_op (Reg r, const Type* ty); +Operand LOCAL_op(FrameSlot s, const Type* ty); +Operand IND_op (Reg base, i32 ofs, const Type* ty); +Operand GLOBAL_op(ObjSymId sym, i64 addend); + +/* ---- function-fixture helpers ---- */ + +typedef struct CgTestParam { + const Type* type; + FrameSlot slot; /* FS_PARAM home, allocated by helper */ + const ABIArgInfo* abi; /* points into ABIFuncInfo.params[i] */ +} CgTestParam; typedef struct CgTestFn { - CgTestCtx* ctx; - const Type* ret_ty; - ObjSymId sym; - CGFuncDesc fd; - CGABIValue ret_val; /* reused by cgtest_ret_* */ + CgTestCtx* ctx; + const Type* fn_type; /* TY_FUNC; built from ret + param types */ + const Type* ret_ty; + const ABIFuncInfo* abi_info; /* abi_func_info(c->abi, fn_type) */ + ObjSymId sym; + CGFuncDesc fd; + CgTestParam* params; + u32 nparams; } CgTestFn; +/* Begin a function returning ret_ty with no parameters. test_main is the + * canonical entry; the runner casts it to int(*)(void). Internally calls + * cgtest_begin_func with name="test_main" and zero params. */ CgTestFn* cgtest_begin_main(CgTestCtx* ctx, const Type* ret_ty); -void cgtest_ret_reg (CgTestFn*, Reg r, const Type* ty); -void cgtest_ret_imm (CgTestFn*, i64 imm, const Type* ty); -void cgtest_ret_void (CgTestFn*); -void cgtest_end (CgTestFn*); + +/* Begin an arbitrary named function. param_types[i] is the type of param i. + * + * - Builds fn_type via type_func(pool, ret_ty, param_types, nparams, 0). + * - Computes ABIFuncInfo via abi_func_info(c->abi, fn_type). + * - Allocates an FS_PARAM frame slot for each param (size/align from + * abi_sizeof/abi_alignof on the param type). + * - Constructs CGParamDesc{index,name=0,type,slot,abi=info->params[i], + * incoming=info->params[i]->parts, nincoming=info->params[i]->nparts, + * loc=0} and stores into fd.params[]. + * - Calls target->func_begin(target, &fd). + * - For each param, calls target->param(target, &fd.params[i]). + * + * Returns a CgTestFn the body can use; cgtest_param_slot(tf,i) reads the + * home slot for param i. */ +CgTestFn* cgtest_begin_func(CgTestCtx* ctx, + const char* name, + const Type* ret_ty, + const Type* const* param_types, + u32 nparams); + +FrameSlot cgtest_param_slot(CgTestFn*, u32 idx); + +/* ---- frame slots and memory ---- */ + +/* Allocate a local frame slot of the given type with default size/align + * from the live TargetABI. flags is a FrameSlotFlag mask (FSF_ADDR_TAKEN, + * etc.). */ +FrameSlot cgtest_local(CgTestFn*, const Type* ty, u16 flags); + +/* Convenience wrappers around target->load/store with a default MemAccess + * derived from `ty` (size/align from TargetABI, alias=ALIAS_LOCAL). */ +void cgtest_load_local (CgTestFn*, Operand dst_reg, FrameSlot, const Type*); +void cgtest_store_local(CgTestFn*, FrameSlot, Operand src, const Type*); + +/* ---- return ---- */ + +void cgtest_ret_reg (CgTestFn*, Reg r, const Type* ty); +void cgtest_ret_imm (CgTestFn*, i64 imm, const Type* ty); +void cgtest_ret_void(CgTestFn*); +/* Aggregate / sret return: result lives at the address held in addr_local + * (typically an FS_LOCAL of the ret type). Builds CGABIValue with + * abi=fn->abi_info->ret, storage=OPK_LOCAL{addr_local}, parts=NULL. */ +void cgtest_ret_indirect(CgTestFn*, FrameSlot addr_local); +/* For a struct return that is split into two registers per ABI: caller has + * already loaded each part into a register; this packs them into the + * CGABIValue.parts array so the backend can place them in the ABI-classed + * registers. */ +void cgtest_ret_struct_in_regs(CgTestFn*, const Reg* part_regs, u32 nparts); + +void cgtest_end(CgTestFn*); + +/* ---- direct calls ---- */ + +typedef enum { + CGT_ARG_IMM, /* scalar immediate */ + CGT_ARG_REG, /* scalar register */ + CGT_ARG_LOCAL_VALUE, /* scalar value loaded from a local slot */ + CGT_ARG_BYVAL_LOCAL, /* aggregate by value: backend reads from &local */ + CGT_ARG_INDIRECT_LOCAL, /* aggregate indirect: pointer to &local */ +} CgTestArgKind; + +typedef struct CgTestArg { + u8 kind; /* CgTestArgKind */ + const Type* type; + union { + i64 imm; + Reg reg; + FrameSlot slot; + } v; +} CgTestArg; + +/* Emit a direct call to `callee_sym` whose signature matches the function + * defined by cgtest_begin_func with `ret_ty` + `arg_types`. Internally: + * + * - Builds fn_type = type_func(pool, ret_ty, arg_types, nargs, 0). + * - Looks up abi_func_info(c->abi, fn_type) to classify ret + each arg. + * - Materializes a CGABIValue for each arg using args[i] (IMM / REG / + * LOCAL_VALUE pack into storage; BYVAL_LOCAL/INDIRECT_LOCAL pack the + * local's address as storage). + * - Builds CGCallDesc.callee = OPK_GLOBAL{callee_sym, 0} for direct call. + * - Sets CGCallDesc.ret with ret_storage as the destination operand: + * scalar return : REG_op(dst, ret_ty) + * sret return : LOCAL_op(sret_slot, ret_ty) + * void return : IMM_op(0, T_void) (storage unused) + * - Calls target->call(target, &desc). */ +void cgtest_call(CgTestFn* caller, + ObjSymId callee_sym, + const Type* ret_ty, + const Type* const* arg_types, + const CgTestArg* args, + u32 nargs, + Operand ret_storage); /* ---- low-level helpers (used by mc_smoke and similar) ---- */ /* Define a function symbol at the current MCEmitter section position with * size = current_pos - start_pos. Used by MC-only cases that emit bytes * directly without a CGTarget. */ -ObjSymId cgtest_mc_begin_main(CgTestCtx*); -void cgtest_mc_end_main (CgTestCtx*, ObjSymId, u32 start_pos); +ObjSymId cgtest_mc_begin_main(CgTestCtx*); +void cgtest_mc_end_main (CgTestCtx*, ObjSymId, u32 start_pos); #endif diff --git a/test/cg/run.sh b/test/cg/run.sh @@ -16,6 +16,12 @@ # # Skip-vs-fail follows test/link convention: skipped layers are treated as # failures unless CFREE_TEST_ALLOW_SKIP=1. +# +# Filtering: +# ./run.sh [name_filter] [paths] +# name_filter substring match against case name (e.g. "a01", "add") +# paths subset of "DREJ" (default "DREJ") +# Equivalent env vars: CFREE_TEST_FILTER, CFREE_TEST_PATHS. set -u @@ -36,6 +42,18 @@ CC="${CC:-cc}" CFREE_CFLAGS="-I$ROOT/include -I$ROOT/src -I$TEST_DIR/harness" ALLOW_SKIP="${CFREE_TEST_ALLOW_SKIP:-0}" +# Filters (env vars or positional args; args win): +# $1 / CFREE_TEST_FILTER — substring match against case name +# $2 / CFREE_TEST_PATHS — subset of "DREJ" (default "DREJ") +FILTER="${1:-${CFREE_TEST_FILTER:-}}" +PATHS="${2:-${CFREE_TEST_PATHS:-DREJ}}" +case "$PATHS" in *D*) RUN_D=1;; *) RUN_D=0;; esac +case "$PATHS" in *R*) RUN_R=1;; *) RUN_R=0;; esac +case "$PATHS" in *E*) RUN_E=1;; *) RUN_E=0;; esac +case "$PATHS" in *J*) RUN_J=1;; *) RUN_J=0;; esac +T_D=0; T_R=0; T_E=0; T_J=0 # accumulated wall-clock seconds per path +now_ms() { python3 -c 'import time;print(int(time.time()*1000))'; } + mkdir -p "$BUILD_DIR" "$BUILD_DIR/cg" PASS=0; FAIL=0; SKIP=0 @@ -86,9 +104,13 @@ run_aarch64() { "$QEMU_BIN" "$exe" >"$out" 2>"$err"; RUN_RC=$?; return fi if [ $have_podman -eq 1 ]; then - local dir base + local dir base platform_flag=() dir="$(cd "$(dirname "$exe")" && pwd)"; base="$(basename "$exe")" - podman run --rm --platform linux/arm64 --net=none \ + # `--platform linux/arm64` triggers a registry manifest lookup (~30s) + # even when the local image is already arm64. Only pass it on hosts + # where the podman machine isn't natively arm64. + [ $is_aarch64 -eq 0 ] && platform_flag=(--platform linux/arm64) + podman run --rm "${platform_flag[@]}" --net=none \ -v "$dir":/work:Z -w /work "$RUN_AARCH64_IMAGE" "./$base" >"$out" 2>"$err" RUN_RC=$?; return fi @@ -169,6 +191,7 @@ printf 'Running cases...\n' CASES="$($CG_RUNNER --list)" for name in $CASES; do + [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]] && continue work="$BUILD_DIR/cg/$name" mkdir -p "$work" @@ -179,86 +202,104 @@ for name in $CASES; do expected_byte=$(( expected & 0xff )) # ---- Path D: in-process JIT (only on aarch64) ------------------------ - if [ $is_aarch64 -eq 1 ]; then - "$CG_RUNNER" --jit "$name" >"$work/d.out" 2>"$work/d.err" - d_rc=$? - if [ "$d_rc" -eq "$expected_byte" ]; then - note_pass "$name/D" + if [ $RUN_D -eq 1 ]; then + if [ $is_aarch64 -eq 1 ]; then + t0=$(now_ms) + "$CG_RUNNER" --jit "$name" >"$work/d.out" 2>"$work/d.err" + d_rc=$? + dt=$(( $(now_ms) - t0 )); T_D=$(( T_D + dt )) + if [ "$d_rc" -eq "$expected_byte" ]; then + note_pass "$name/D (${dt}ms)" + else + note_fail "$name/D (expected $expected_byte got $d_rc, ${dt}ms)" + fi else - note_fail "$name/D (expected $expected_byte got $d_rc)" + note_skip "$name/D" "not on aarch64 host" fi - else - note_skip "$name/D" "not on aarch64 host" fi - # ---- Path R: ELF roundtrip -------------------------------------------- + # ---- emit (needed by R/E/J) ------------------------------------------- obj="$work/$name.o" - if "$CG_RUNNER" --emit "$name" "$obj" 2>"$work/emit.err"; then - : - else - note_fail "$name/emit (cg-runner --emit failed; see $work/emit.err)" - continue + if [ $RUN_R -eq 1 ] || [ $RUN_E -eq 1 ] || [ $RUN_J -eq 1 ]; then + if ! "$CG_RUNNER" --emit "$name" "$obj" 2>"$work/emit.err"; then + note_fail "$name/emit (cg-runner --emit failed; see $work/emit.err)" + continue + fi fi - if [ $have_roundtrip -eq 1 ] && [ $have_readelf -eq 1 ] && [ $have_python3 -eq 1 ]; then - rt="$work/$name.rt.o" - if ! "$ROUNDTRIP_BIN" "$obj" "$rt" 2>"$work/rt.err"; then - note_fail "$name/R (roundtrip failed)" - else - "$READELF_BIN" -aW "$obj" | python3 "$NORMALIZE" >"$work/golden.norm" 2>/dev/null - "$READELF_BIN" -aW "$rt" | python3 "$NORMALIZE" >"$work/rt.norm" 2>/dev/null - if diff -u "$work/golden.norm" "$work/rt.norm" >"$work/r.diff" 2>&1; then - note_pass "$name/R" + # ---- Path R: ELF roundtrip -------------------------------------------- + if [ $RUN_R -eq 1 ]; then + if [ $have_roundtrip -eq 1 ] && [ $have_readelf -eq 1 ] && [ $have_python3 -eq 1 ]; then + t0=$(now_ms) + rt="$work/$name.rt.o" + r_ok=1; r_msg="" + if ! "$ROUNDTRIP_BIN" "$obj" "$rt" 2>"$work/rt.err"; then + r_ok=0; r_msg=" (roundtrip failed)" else - note_fail "$name/R" + "$READELF_BIN" -aW "$obj" | python3 "$NORMALIZE" >"$work/golden.norm" 2>/dev/null + "$READELF_BIN" -aW "$rt" | python3 "$NORMALIZE" >"$work/rt.norm" 2>/dev/null + diff -u "$work/golden.norm" "$work/rt.norm" >"$work/r.diff" 2>&1 || r_ok=0 fi + dt=$(( $(now_ms) - t0 )); T_R=$(( T_R + dt )) + if [ $r_ok -eq 1 ]; then note_pass "$name/R (${dt}ms)" + else note_fail "$name/R${r_msg} (${dt}ms)"; fi + else + note_skip "$name/R" "missing roundtrip/readelf/python3" fi - else - note_skip "$name/R" "missing roundtrip/readelf/python3" fi # ---- Path E: link + qemu ---------------------------------------------- - if [ $have_exe_runner -eq 1 ] && [ $have_clang_cross -eq 1 ]; then - start_obj="$work/start.o" - clang $CLANG_TARGET -O1 -ffreestanding -fno-stack-protector \ - -fno-PIC -fno-pie \ - -c "$LINK_TEST_DIR/harness/start.c" -o "$start_obj" 2>/dev/null - - exe="$work/linked.exe" - if ! "$LINK_EXE_RUNNER" -o "$exe" "$obj" "$start_obj" \ - >"$work/exec_link.out" 2>"$work/exec_link.err"; then - note_fail "$name/E (link failed)" - elif [ $have_runner -eq 1 ]; then - run_aarch64 "$exe" "$work/exec.out" "$work/exec.err" - if [ "$RUN_RC" -eq "$expected_byte" ]; then - note_pass "$name/E" + if [ $RUN_E -eq 1 ]; then + if [ $have_exe_runner -eq 1 ] && [ $have_clang_cross -eq 1 ]; then + t0=$(now_ms) + start_obj="$work/start.o" + clang $CLANG_TARGET -O1 -ffreestanding -fno-stack-protector \ + -fno-PIC -fno-pie \ + -c "$LINK_TEST_DIR/harness/start.c" -o "$start_obj" 2>/dev/null + + exe="$work/linked.exe" + if ! "$LINK_EXE_RUNNER" -o "$exe" "$obj" "$start_obj" \ + >"$work/exec_link.out" 2>"$work/exec_link.err"; then + dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + dt )) + note_fail "$name/E (link failed, ${dt}ms)" + elif [ $have_runner -eq 1 ]; then + run_aarch64 "$exe" "$work/exec.out" "$work/exec.err" + dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + dt )) + if [ "$RUN_RC" -eq "$expected_byte" ]; then + note_pass "$name/E (${dt}ms)" + else + note_fail "$name/E (expected $expected_byte got $RUN_RC, ${dt}ms)" + fi else - note_fail "$name/E (expected $expected_byte got $RUN_RC)" + note_skip "$name/E" "no qemu/podman" fi else - note_skip "$name/E" "no qemu/podman" + note_skip "$name/E" "no link-exe-runner or aarch64 clang" fi - else - note_skip "$name/E" "no link-exe-runner or aarch64 clang" fi # ---- Path J: jit-via-file --------------------------------------------- - if [ $have_jit_runner -eq 1 ]; then - "$JIT_RUNNER" "$obj" >"$work/jit.out" 2>"$work/jit.err" - j_rc=$? - if [ "$j_rc" -eq "$expected_byte" ]; then - note_pass "$name/J" + if [ $RUN_J -eq 1 ]; then + if [ $have_jit_runner -eq 1 ]; then + t0=$(now_ms) + "$JIT_RUNNER" "$obj" >"$work/jit.out" 2>"$work/jit.err" + j_rc=$? + dt=$(( $(now_ms) - t0 )); T_J=$(( T_J + dt )) + if [ "$j_rc" -eq "$expected_byte" ]; then + note_pass "$name/J (${dt}ms)" + else + note_fail "$name/J (expected $expected_byte got $j_rc, ${dt}ms)" + fi else - note_fail "$name/J (expected $expected_byte got $j_rc)" + note_skip "$name/J" "no jit-runner (not aarch64 host)" fi - else - note_skip "$name/J" "no jit-runner (not aarch64 host)" fi done # ---- summary --------------------------------------------------------------- printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP" +printf 'Time: D=%dms R=%dms E=%dms J=%dms\n' "$T_D" "$T_R" "$T_E" "$T_J" if [ ${#FAIL_NAMES[@]} -gt 0 ]; then printf 'Failed:\n'