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:
| M | test/cg/CORPUS.md | | | 119 | +++++++++++++++++++++++++++++++++++++++++++++++-------------------------------- |
| M | test/cg/harness/cases.c | | | 732 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------- |
| M | test/cg/harness/cg_test.c | | | 367 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------- |
| M | test/cg/harness/cg_test.h | | | 182 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------- |
| M | test/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'