kit

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

commit 9f4d8f11f638031838c7d85e6eb7bec53b495ff8
parent c1cf117c880ee320131d5be080375899b7acc02d
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat,  9 May 2026 14:49:51 -0700

test/cg: register Groups G, H, I (calls, control flow, alloca)

Adds 41 cases establishing expected behavior before backend impl: G stresses
calls beyond the direct-call path (indirect, recursion, mutual recursion,
HFA, oversized byval, register-preservation across calls); H covers loops,
multi-way switch via chained cmp_branch, short-circuit, ternary, and
unreachable-after-ret; I exercises alloca with const and runtime sizes,
alignment, in-loop distinctness, and crossing a call boundary.

Harness gets cgtest_call_indirect for OPK_REG callees, plus cgtest_decl_func
+ cgtest_begin_func_at so mutually-recursive cases can forward-declare a
symbol before its body emits.

Diffstat:
Mtest/cg/CORPUS.md | 82++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mtest/cg/harness/cases.c | 1834+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/cg/harness/cg_test.c | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Mtest/cg/harness/cg_test.h | 26++++++++++++++++++++++++++
4 files changed, 2005 insertions(+), 17 deletions(-)

diff --git a/test/cg/CORPUS.md b/test/cg/CORPUS.md @@ -160,13 +160,89 @@ non-zero offsets, store-from-IMM vs store-from-REG, `copy_bytes`, | `f12_bitfield_unsigned` | · | `{u: 5}` at bit_offset=3; store 21; load (zero-extend) | 21 | | `f13_bitfield_signed` | · | `{s: 5}` at bit_offset=0; store -1; load sign-extends; low 8 | 255 | +## Group G — calls (beyond direct-call path) + +Group B established the direct-call mechanics (param/return, stack +spill, sret, byval, fp param). Group G stresses what falls out once +calls *compose*: indirect calls through function pointers, recursion, +register-preservation across calls, HFAs, and pass-by-pointer for +oversized aggregates. Each helper is its own `func_begin`/`func_end` +under the same `CGTarget`. `cmp_branch`-driven recursion bases share +the Group D control surface. + +| Case | Status | Body | Expected | +|---|---|---|---| +| `g01_indirect_call` | · | `int (*fp)(int) = echo; return fp(42);` (call via REG, not direct symbol) | 42 | +| `g02_recursion_factorial` | · | `int f(int n){return n<2?1:n*f(n-1);}; f(5)` | 120 | +| `g03_recursion_fib` | · | `int f(int n){return n<2?n:f(n-1)+f(n-2);}; f(10)` | 55 | +| `g04_mutual_recursion` | · | `is_even`/`is_odd` cross-recursion; `is_even(8)` | 1 | +| `g05_chained_calls` | · | `inc(inc(inc(39)))` — return value of one is the arg of the next | 42 | +| `g06_mixed_int_fp_params` | · | `int f(int a, float b, int c, double d, int e)`; integer sum truncated | 42 | +| `g07_void_call_outparam` | · | `void fill(int *p, int v); int x; fill(&x, 42); return x;` | 42 | +| `g08_large_struct_byval` | · | `struct S{int a[8];}` (>16B) passed by value (ABI: indirect) | 42 | +| `g09_hfa_param_f32x2` | · | `struct V{float x,y;}` HFA param `(1.5,1.5)`; ftoi_s of caller-side sum | 3 | +| `g10_hfa_return_f32x2` | · | HFA return `{1.5f,1.5f}`; ftoi_s of caller-side sum | 3 | +| `g11_caller_saved_live_across_call` | · | local `int x=42` live across a clobbering call; backend must preserve | 42 | +| `g12_addr_taken_local_across_call` | · | b05-style addr-taken local survives an intervening call | 18 | +| `g13_call_in_loop_induction` | · | `for(i=0;i<10;i++) s += id(i);` — induction var preserved across call | 45 | + +## Group H — control flow + +Builds out the loop and multi-way branch surface beyond Group D's +`scope_if`/`scope_else`. Includes both structured loop ops +(`scope_loop`, `scope_break`, `scope_continue`) and the unstructured +`jump`/label form so the backend exercises arbitrary CFGs (forward +and backward `jump`). `switch`-style multi-way uses repeated +`cmp_branch`. Short-circuit `&&`/`||` are exercised at the IR level +(lowered to chained `cmp_branch` + materialize) — the test proves +short-circuit by observing that the RHS side effect did *not* run. + +| Case | Status | Body | Expected | +|---|---|---|---| +| `h01_while_sum_0_to_9` | · | `int s=0,i=0; while(i<10){ s+=i; i++; } return s;` | 45 | +| `h02_do_while_once` | · | `int i=0; do { i=42; } while(0); return i;` | 42 | +| `h03_for_count_to_10` | · | `int s=0; for(int i=1;i<=10;i++) s+=i; return s;` | 55 | +| `h04_loop_break` | · | `for(i=0;;i++) if(i==42) break; return i;` | 42 | +| `h05_loop_continue` | · | sum of even i in `[0,20)` using `continue` to skip odds | 90 | +| `h06_nested_loops` | · | `for(i=0;i<3;i++) for(j=0;j<2;j++) s++; return s;` | 6 | +| `h07_break_inner_only` | · | `break` exits inner loop only — outer continues | 9 | +| `h08_early_return_in_loop` | · | `for(i=0;;i++) if(i==17) return i;` | 17 | +| `h09_switch_three_cases` | · | `switch(2){case 1:r=10;break; case 2:r=42;break; case 3:r=99;break;}` | 42 | +| `h10_switch_fallthrough` | · | `case 1: r+=10; case 2: r+=20;` (no break) on input 1 | 30 | +| `h11_switch_default` | · | `switch(99){case 1:..;break; default: r=7;}` returns default | 7 | +| `h12_jump_forward` | · | `jump L; ret 99 (unreachable); L: ret 42;` — backend tolerates dead op | 42 | +| `h13_jump_backward` | · | counter loop built from `cmp_branch` + backward `jump` (no scope ops) | 10 | +| `h14_short_circuit_and_skip` | · | `int s=0; (0) && (s=99,1); return s;` — RHS side effect must be skipped | 0 | +| `h15_short_circuit_or_skip` | · | `int s=0; (1) \|\| (s=99,1); return s;` — RHS side effect must be skipped| 0 | +| `h16_ternary` | · | `int x = (5>3) ? 42 : 7; return x;` | 42 | +| `h17_ternary_side_effect_one_arm` | · | `int s=0; (1) ? (s=42) : (s=99); return s;` — only taken arm runs | 42 | +| `h18_unreachable_after_ret` | · | ops emitted after a `ret` (dead block); backend must not crash | 42 | + +## Group I — alloca / VLA + +Stack-allocated runtime-sized memory: the `alloca` op (constant- and +runtime-size), required-alignment variants, two-allocas-disjoint, and +VLAs as parameters. Oracles exercise both the *address* (alignment, +distinct per allocation) and the *contents* (writes survive, helpers +can deref). + +| Case | Status | Body | Expected | +|---|---|---|---| +| `i01_alloca_const_int` | · | `int *p = alloca(sizeof(int)); *p = 42; return *p;` | 42 | +| `i02_alloca_runtime_size` | · | `int n=5; int *p = alloca(n*sizeof(int));` fill `1..5`; sum | 15 | +| `i03_alloca_align_16` | · | alloca with 16-byte alignment request; return `((uintptr_t)p & 0xF)==0` | 1 | +| `i04_alloca_in_loop_distinct` | · | 3 iters, each `alloca(4)` + record addr; return `(a!=b && b!=c)` | 1 | +| `i05_alloca_then_call` | · | alloca buf; pass to helper that writes 42; reload after call | 42 | +| `i06_two_allocas_disjoint` | · | `int *p=alloca(4); int *q=alloca(4); *p=1; *q=2; return *p+*q;` | 3 | +| `i07_alloca_addr_escapes` | · | alloca buf; helper stores `&buf` then reads it back | 42 | +| `i08_vla_param_sum` | · | helper `int sum(int n, int a[n])`; pass VLA `1..9`; sum | 45 | +| `i09_alloca_preserves_locals` | · | named `int` locals before+after alloca; both readable post-alloca | 42 | +| `i10_alloca_after_named_local`| · | alloca after a fixed local — frame layout must keep both addressable | 42 | + ## Deferred groups | Group | Theme | |---|---| -| G | calls (beyond the direct-call path Group B exercises) | -| H | control flow | -| I | alloca | | J | varargs | | K | atomics | | L | intrinsics | diff --git a/test/cg/harness/cases.c b/test/cg/harness/cases.c @@ -1554,6 +1554,1793 @@ static void build_f13_bitfield_signed(CgTestCtx* ctx) } /* ============================================================ + * Group G: calls (beyond direct-call path) + * + * Group B established direct-call mechanics. Group G stresses what falls + * out once calls compose: indirect calls, recursion, mutual recursion, + * register-preservation across calls, HFAs, oversized struct byval. + * ============================================================ */ + +/* helper used by g01 and g11/g12: int echo(int x) { return x; } */ +static ObjSymId build_g_echo_helper(CgTestCtx* ctx, const char* name) +{ + const Type* I32 = T_i32(ctx); + const Type* params[] = { I32 }; + CgTestFn* tf = cgtest_begin_func(ctx, name, 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; +} + +/* g01_indirect_call — int (*fp)(int) = echo; return fp(42); */ +static void build_g01_indirect_call(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* params[] = { I32 }; + ObjSymId echo = build_g_echo_helper(ctx, "g01_echo"); + + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + /* Materialize the function pointer from the GLOBAL symbol. */ + const Type* fn_ty = type_func(ctx->pool, I32, params, 1, 0); + const Type* fnp_ty = T_ptr(ctx, fn_ty); + Reg fp = T->alloc_reg(T, RC_INT, fnp_ty); + T->addr_of(T, REG_op(fp, fnp_ty), GLOBAL_op(echo, 0)); + + Reg dst = T->alloc_reg(T, RC_INT, I32); + CgTestArg args[] = { { .kind = CGT_ARG_IMM, .type = I32, .v.imm = 42 } }; + cgtest_call_indirect(tf, fp, I32, params, args, 1, REG_op(dst, I32)); + cgtest_ret_reg(tf, dst, I32); + cgtest_end(tf); +} + +/* helper used by g02: int fact(int n) { return n<2 ? 1 : n*fact(n-1); } + * Forward-decl the symbol so the body can reference it for recursion. */ +static ObjSymId build_g02_helper(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* params[] = { I32 }; + ObjSymId sym = cgtest_decl_func(ctx, "g02_fact"); + CgTestFn* tf = cgtest_begin_func_at(ctx, sym, I32, params, 1); + CGTarget* T = ctx->target; + + Reg n = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(n, I32), cgtest_param_slot(tf, 0), I32); + + /* if (n < 2) goto base; */ + Label base = T->label_new(T); + T->cmp_branch(T, CMP_LT_S, REG_op(n, I32), IMM_op(2, I32), base); + + /* recursive: tmp = fact(n - 1); return n * tmp; */ + Reg n1 = T->alloc_reg(T, RC_INT, I32); + T->binop(T, BO_ISUB, REG_op(n1, I32), REG_op(n, I32), IMM_op(1, I32)); + + Reg rec = T->alloc_reg(T, RC_INT, I32); + CgTestArg rec_args[] = { { .kind = CGT_ARG_REG, .type = I32, .v.reg = n1 } }; + cgtest_call(tf, sym, I32, params, rec_args, 1, REG_op(rec, I32)); + + Reg prod = T->alloc_reg(T, RC_INT, I32); + T->binop(T, BO_IMUL, REG_op(prod, I32), REG_op(n, I32), REG_op(rec, I32)); + cgtest_ret_reg(tf, prod, I32); + + /* base: return 1; */ + T->label_place(T, base); + cgtest_ret_imm(tf, 1, I32); + cgtest_end(tf); + return sym; +} + +/* g02_recursion_factorial — fact(5) = 120. */ +static void build_g02_recursion_factorial(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* params[] = { I32 }; + ObjSymId fact = build_g02_helper(ctx); + + 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 = 5 } }; + cgtest_call(tf, fact, I32, params, args, 1, REG_op(dst, I32)); + cgtest_ret_reg(tf, dst, I32); + cgtest_end(tf); +} + +/* helper used by g03: int fib(int n) { return n<2?n:fib(n-1)+fib(n-2); } */ +static ObjSymId build_g03_helper(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* params[] = { I32 }; + ObjSymId sym = cgtest_decl_func(ctx, "g03_fib"); + CgTestFn* tf = cgtest_begin_func_at(ctx, sym, I32, params, 1); + CGTarget* T = ctx->target; + + Reg n = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(n, I32), cgtest_param_slot(tf, 0), I32); + + /* if (n < 2) return n; */ + Label base = T->label_new(T); + T->cmp_branch(T, CMP_LT_S, REG_op(n, I32), IMM_op(2, I32), base); + + /* a = fib(n-1); b = fib(n-2); return a+b; */ + Reg n1 = T->alloc_reg(T, RC_INT, I32); + Reg n2 = T->alloc_reg(T, RC_INT, I32); + T->binop(T, BO_ISUB, REG_op(n1, I32), REG_op(n, I32), IMM_op(1, I32)); + T->binop(T, BO_ISUB, REG_op(n2, I32), REG_op(n, I32), IMM_op(2, I32)); + + Reg a = T->alloc_reg(T, RC_INT, I32); + Reg b = T->alloc_reg(T, RC_INT, I32); + CgTestArg a1[] = { { .kind = CGT_ARG_REG, .type = I32, .v.reg = n1 } }; + CgTestArg a2[] = { { .kind = CGT_ARG_REG, .type = I32, .v.reg = n2 } }; + cgtest_call(tf, sym, I32, params, a1, 1, REG_op(a, I32)); + cgtest_call(tf, sym, I32, params, a2, 1, REG_op(b, I32)); + + Reg sum = T->alloc_reg(T, RC_INT, I32); + T->binop(T, BO_IADD, REG_op(sum, I32), REG_op(a, I32), REG_op(b, I32)); + cgtest_ret_reg(tf, sum, I32); + + T->label_place(T, base); + cgtest_ret_reg(tf, n, I32); + cgtest_end(tf); + return sym; +} + +/* g03_recursion_fib — fib(10) = 55. */ +static void build_g03_recursion_fib(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* params[] = { I32 }; + ObjSymId fib = build_g03_helper(ctx); + + 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 = 10 } }; + cgtest_call(tf, fib, I32, params, args, 1, REG_op(dst, I32)); + cgtest_ret_reg(tf, dst, I32); + cgtest_end(tf); +} + +/* g04_mutual_recursion — is_even(8) = 1. + * int is_even(int n) { return n==0 ? 1 : is_odd(n-1); } + * int is_odd (int n) { return n==0 ? 0 : is_even(n-1); } + * Forward-declare both symbols up front so each body can reference the + * other before it has been emitted. */ +static void build_g04_mutual_recursion(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* params[] = { I32 }; + CGTarget* T = ctx->target; + + ObjSymId sym_e = cgtest_decl_func(ctx, "g04_is_even"); + ObjSymId sym_o = cgtest_decl_func(ctx, "g04_is_odd"); + + /* is_even body. */ + { + CgTestFn* tf = cgtest_begin_func_at(ctx, sym_e, I32, params, 1); + Reg n = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(n, I32), cgtest_param_slot(tf, 0), I32); + Label base = T->label_new(T); + T->cmp_branch(T, CMP_EQ, REG_op(n, I32), IMM_op(0, I32), base); + Reg n1 = T->alloc_reg(T, RC_INT, I32); + T->binop(T, BO_ISUB, REG_op(n1, I32), REG_op(n, I32), IMM_op(1, I32)); + Reg r = T->alloc_reg(T, RC_INT, I32); + CgTestArg args[] = { { .kind = CGT_ARG_REG, .type = I32, .v.reg = n1 } }; + cgtest_call(tf, sym_o, I32, params, args, 1, REG_op(r, I32)); + cgtest_ret_reg(tf, r, I32); + T->label_place(T, base); + cgtest_ret_imm(tf, 1, I32); + cgtest_end(tf); + } + + /* is_odd body. */ + { + CgTestFn* tf = cgtest_begin_func_at(ctx, sym_o, I32, params, 1); + Reg n = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(n, I32), cgtest_param_slot(tf, 0), I32); + Label base = T->label_new(T); + T->cmp_branch(T, CMP_EQ, REG_op(n, I32), IMM_op(0, I32), base); + Reg n1 = T->alloc_reg(T, RC_INT, I32); + T->binop(T, BO_ISUB, REG_op(n1, I32), REG_op(n, I32), IMM_op(1, I32)); + Reg r = T->alloc_reg(T, RC_INT, I32); + CgTestArg args[] = { { .kind = CGT_ARG_REG, .type = I32, .v.reg = n1 } }; + cgtest_call(tf, sym_e, I32, params, args, 1, REG_op(r, I32)); + cgtest_ret_reg(tf, r, I32); + T->label_place(T, base); + cgtest_ret_imm(tf, 0, I32); + cgtest_end(tf); + } + + /* test_main: return is_even(8) → 1. */ + CgTestFn* tf = cgtest_begin_main(ctx, I32); + Reg dst = T->alloc_reg(T, RC_INT, I32); + CgTestArg args[] = { { .kind = CGT_ARG_IMM, .type = I32, .v.imm = 8 } }; + cgtest_call(tf, sym_e, I32, params, args, 1, REG_op(dst, I32)); + cgtest_ret_reg(tf, dst, I32); + cgtest_end(tf); +} + +/* helper used by g05: int inc(int x) { return x+1; } */ +static ObjSymId build_g05_helper(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* params[] = { I32 }; + CgTestFn* tf = cgtest_begin_func(ctx, "g05_inc", I32, params, 1); + CGTarget* T = ctx->target; + Reg x = T->alloc_reg(T, RC_INT, I32); + Reg r = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(x, I32), cgtest_param_slot(tf, 0), I32); + T->binop(T, BO_IADD, REG_op(r, I32), REG_op(x, I32), IMM_op(1, I32)); + cgtest_ret_reg(tf, r, I32); + cgtest_end(tf); + return tf->sym; +} + +/* g05_chained_calls — inc(inc(inc(39))) = 42. */ +static void build_g05_chained_calls(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* params[] = { I32 }; + ObjSymId inc = build_g05_helper(ctx); + + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + Reg r1 = T->alloc_reg(T, RC_INT, I32); + Reg r2 = T->alloc_reg(T, RC_INT, I32); + Reg r3 = T->alloc_reg(T, RC_INT, I32); + CgTestArg a1[] = { { .kind = CGT_ARG_IMM, .type = I32, .v.imm = 39 } }; + cgtest_call(tf, inc, I32, params, a1, 1, REG_op(r1, I32)); + CgTestArg a2[] = { { .kind = CGT_ARG_REG, .type = I32, .v.reg = r1 } }; + cgtest_call(tf, inc, I32, params, a2, 1, REG_op(r2, I32)); + CgTestArg a3[] = { { .kind = CGT_ARG_REG, .type = I32, .v.reg = r2 } }; + cgtest_call(tf, inc, I32, params, a3, 1, REG_op(r3, I32)); + cgtest_ret_reg(tf, r3, I32); + cgtest_end(tf); +} + +/* helper used by g06: + * int f(int a, float b, int c, double d, int e) + * { return a + (int)b + c + (int)d + e; } + * Mixes int and FP params — abi_func_info routes int→GPR and FP→FP. */ +static ObjSymId build_g06_helper(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* F32 = T_f32(ctx); + const Type* F64 = T_f64(ctx); + const Type* params[] = { I32, F32, I32, F64, I32 }; + CgTestFn* tf = cgtest_begin_func(ctx, "g06_f", I32, params, 5); + CGTarget* T = ctx->target; + + Reg a = T->alloc_reg(T, RC_INT, I32); + Reg c = T->alloc_reg(T, RC_INT, I32); + Reg e = T->alloc_reg(T, RC_INT, I32); + Reg fb = T->alloc_reg(T, RC_FP, F32); + Reg fd = T->alloc_reg(T, RC_FP, F64); + cgtest_load_local(tf, REG_op(a, I32), cgtest_param_slot(tf, 0), I32); + cgtest_load_local(tf, REG_op(fb, F32), cgtest_param_slot(tf, 1), F32); + cgtest_load_local(tf, REG_op(c, I32), cgtest_param_slot(tf, 2), I32); + cgtest_load_local(tf, REG_op(fd, F64), cgtest_param_slot(tf, 3), F64); + cgtest_load_local(tf, REG_op(e, I32), cgtest_param_slot(tf, 4), I32); + + Reg ib = T->alloc_reg(T, RC_INT, I32); + Reg id = T->alloc_reg(T, RC_INT, I32); + T->convert(T, CV_FTOI_S, REG_op(ib, I32), REG_op(fb, F32)); + T->convert(T, CV_FTOI_S, REG_op(id, I32), REG_op(fd, F64)); + + Reg s = T->alloc_reg(T, RC_INT, I32); + T->binop(T, BO_IADD, REG_op(s, I32), REG_op(a, I32), REG_op(ib, I32)); + T->binop(T, BO_IADD, REG_op(s, I32), REG_op(s, I32), REG_op(c, I32)); + T->binop(T, BO_IADD, REG_op(s, I32), REG_op(s, I32), REG_op(id, I32)); + T->binop(T, BO_IADD, REG_op(s, I32), REG_op(s, I32), REG_op(e, I32)); + cgtest_ret_reg(tf, s, I32); + cgtest_end(tf); + return tf->sym; +} + +/* g06_mixed_int_fp_params — f(2, 3.0f, 5, 7.0, 25) → 42. */ +static void build_g06_mixed_int_fp_params(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* F32 = T_f32(ctx); + const Type* F64 = T_f64(ctx); + const Type* params[] = { I32, F32, I32, F64, I32 }; + ObjSymId f = build_g06_helper(ctx); + + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + /* Materialize 3.0f and 7.0 in FP regs via load_const. */ + static const u8 BYTES_3F[4] = { 0x00, 0x00, 0x40, 0x40 }; /* 3.0f LE */ + static const u8 BYTES_7D[8] = { 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x1C, 0x40 }; /* 7.0 LE double */ + Reg fb = T->alloc_reg(T, RC_FP, F32); + Reg fd = T->alloc_reg(T, RC_FP, F64); + ConstBytes cbf = { .type = F32, .bytes = BYTES_3F, .size = 4, .align = 4 }; + ConstBytes cbd = { .type = F64, .bytes = BYTES_7D, .size = 8, .align = 8 }; + T->load_const(T, REG_op(fb, F32), cbf); + T->load_const(T, REG_op(fd, F64), cbd); + + Reg dst = T->alloc_reg(T, RC_INT, I32); + CgTestArg args[] = { + { .kind = CGT_ARG_IMM, .type = I32, .v.imm = 2 }, + { .kind = CGT_ARG_REG, .type = F32, .v.reg = fb }, + { .kind = CGT_ARG_IMM, .type = I32, .v.imm = 5 }, + { .kind = CGT_ARG_REG, .type = F64, .v.reg = fd }, + { .kind = CGT_ARG_IMM, .type = I32, .v.imm = 25 }, + }; + cgtest_call(tf, f, I32, params, args, 5, REG_op(dst, I32)); + cgtest_ret_reg(tf, dst, I32); + cgtest_end(tf); +} + +/* helper used by g07: void fill(int *p, int v) { *p = v; } */ +static ObjSymId build_g07_helper(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* PI32 = T_ptr(ctx, I32); + const Type* VOID = T_void(ctx); + const Type* params[] = { PI32, I32 }; + CgTestFn* tf = cgtest_begin_func(ctx, "g07_fill", VOID, params, 2); + CGTarget* T = ctx->target; + + Reg p = T->alloc_reg(T, RC_INT, PI32); + Reg v = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(p, PI32), cgtest_param_slot(tf, 0), PI32); + cgtest_load_local(tf, REG_op(v, I32), cgtest_param_slot(tf, 1), I32); + MemAccess ma = { .type = I32, .size = 4, .align = 4, + .alias.kind = ALIAS_LOCAL }; + T->store(T, IND_op(p, 0, I32), REG_op(v, I32), ma); + cgtest_ret_void(tf); + cgtest_end(tf); + return tf->sym; +} + +/* g07_void_call_outparam — int x; fill(&x, 42); return x → 42. */ +static void build_g07_void_call_outparam(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* PI32 = T_ptr(ctx, I32); + const Type* VOID = T_void(ctx); + const Type* params[] = { PI32, I32 }; + ObjSymId fill = build_g07_helper(ctx); + + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + FrameSlot x = cgtest_local(tf, I32, FSF_ADDR_TAKEN); + Reg p = T->alloc_reg(T, RC_INT, PI32); + T->addr_of(T, REG_op(p, PI32), LOCAL_op(x, I32)); + + CgTestArg args[] = { + { .kind = CGT_ARG_REG, .type = PI32, .v.reg = p }, + { .kind = CGT_ARG_IMM, .type = I32, .v.imm = 42 }, + }; + cgtest_call(tf, fill, VOID, params, args, 2, IMM_op(0, VOID)); + + Reg r = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(r, I32), x, I32); + cgtest_ret_reg(tf, r, I32); + cgtest_end(tf); +} + +/* struct S { int a[8]; }; — 32 bytes, exceeds the 16-byte threshold and is + * passed by reference (caller-allocated copy) on AArch64 SysV. */ +static const Type* build_g08_struct_type(CgTestCtx* ctx) +{ + Sym tag = pool_intern_cstr(ctx->pool, "S32"); + 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); + /* Eight i32 fields named a0..a7. */ + for (int i = 0; i < 8; ++i) { + char name[8]; + name[0] = 'a'; + name[1] = (char)('0' + i); + name[2] = 0; + type_record_field(b, (Field){ + .name = pool_intern_cstr(ctx->pool, name), + .type = T_i32(ctx) }); + } + return type_record_end(ctx->pool, b); +} + +/* helper used by g08: int take(struct S s) { return s.a7; } */ +static ObjSymId build_g08_helper(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* S = build_g08_struct_type(ctx); + const Type* params[] = { S }; + CgTestFn* tf = cgtest_begin_func(ctx, "g08_take", I32, params, 1); + CGTarget* T = ctx->target; + + Reg base = T->alloc_reg(T, RC_INT, T_ptr(ctx, S)); + T->addr_of(T, REG_op(base, T_ptr(ctx, S)), + LOCAL_op(cgtest_param_slot(tf, 0), S)); + MemAccess ma = { .type = I32, .size = 4, .align = 4, + .alias.kind = ALIAS_PARAM }; + Reg r = T->alloc_reg(T, RC_INT, I32); + /* a7 lives at offset 28. */ + T->load(T, REG_op(r, I32), IND_op(base, 28, I32), ma); + cgtest_ret_reg(tf, r, I32); + cgtest_end(tf); + return tf->sym; +} + +/* g08_large_struct_byval — 32-byte struct passed by value. */ +static void build_g08_large_struct_byval(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* S = build_g08_struct_type(ctx); + const Type* params[] = { S }; + ObjSymId take = build_g08_helper(ctx); + + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + FrameSlot src = cgtest_local(tf, S, FSF_ADDR_TAKEN); + Reg base = T->alloc_reg(T, RC_INT, T_ptr(ctx, S)); + T->addr_of(T, REG_op(base, T_ptr(ctx, S)), LOCAL_op(src, S)); + MemAccess ma = { .type = I32, .size = 4, .align = 4, + .alias.kind = ALIAS_LOCAL }; + /* Zero out a0..a6 so the helper observation depends only on a7. */ + for (int i = 0; i < 7; ++i) { + T->store(T, IND_op(base, i*4, I32), IMM_op(0, I32), ma); + } + T->store(T, IND_op(base, 28, I32), IMM_op(42, I32), ma); + + Reg dst = T->alloc_reg(T, RC_INT, I32); + CgTestArg args[] = { { .kind = CGT_ARG_BYVAL_LOCAL, .type = S, .v.slot = src } }; + cgtest_call(tf, take, I32, params, args, 1, REG_op(dst, I32)); + cgtest_ret_reg(tf, dst, I32); + cgtest_end(tf); +} + +/* struct V { float x, y; }; — HFA of two f32. AArch64 SysV passes in v0,v1 + * and returns in {v0, v1}. */ +static const Type* build_g_hfa_type(CgTestCtx* ctx) +{ + Sym tag = pool_intern_cstr(ctx->pool, "V"); + 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, "x"), .type = T_f32(ctx) }); + type_record_field(b, (Field){ + .name = pool_intern_cstr(ctx->pool, "y"), .type = T_f32(ctx) }); + return type_record_end(ctx->pool, b); +} + +/* helper used by g09: int f(struct V v) { return (int)(v.x + v.y); } */ +static ObjSymId build_g09_helper(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* F32 = T_f32(ctx); + const Type* V = build_g_hfa_type(ctx); + const Type* params[] = { V }; + CgTestFn* tf = cgtest_begin_func(ctx, "g09_f", I32, params, 1); + CGTarget* T = ctx->target; + + Reg base = T->alloc_reg(T, RC_INT, T_ptr(ctx, V)); + T->addr_of(T, REG_op(base, T_ptr(ctx, V)), + LOCAL_op(cgtest_param_slot(tf, 0), V)); + MemAccess ma = { .type = F32, .size = 4, .align = 4, + .alias.kind = ALIAS_PARAM }; + Reg fx = T->alloc_reg(T, RC_FP, F32); + Reg fy = T->alloc_reg(T, RC_FP, F32); + Reg fs = T->alloc_reg(T, RC_FP, F32); + T->load(T, REG_op(fx, F32), IND_op(base, 0, F32), ma); + T->load(T, REG_op(fy, F32), IND_op(base, 4, F32), ma); + T->binop(T, BO_FADD, REG_op(fs, F32), REG_op(fx, F32), REG_op(fy, F32)); + Reg r = T->alloc_reg(T, RC_INT, I32); + T->convert(T, CV_FTOI_S, REG_op(r, I32), REG_op(fs, F32)); + cgtest_ret_reg(tf, r, I32); + cgtest_end(tf); + return tf->sym; +} + +/* g09_hfa_param_f32x2 — f({1.5f, 1.5f}) → 3. */ +static void build_g09_hfa_param_f32x2(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* F32 = T_f32(ctx); + const Type* V = build_g_hfa_type(ctx); + const Type* params[] = { V }; + ObjSymId f = build_g09_helper(ctx); + + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + /* Build local {1.5f, 1.5f} via FP load_const + store_local. */ + static const u8 BYTES_15F[4] = { 0x00, 0x00, 0xC0, 0x3F }; /* 1.5f LE */ + FrameSlot src = cgtest_local(tf, V, FSF_ADDR_TAKEN); + Reg base = T->alloc_reg(T, RC_INT, T_ptr(ctx, V)); + T->addr_of(T, REG_op(base, T_ptr(ctx, V)), LOCAL_op(src, V)); + Reg fc = T->alloc_reg(T, RC_FP, F32); + ConstBytes cb = { .type = F32, .bytes = BYTES_15F, .size = 4, .align = 4 }; + T->load_const(T, REG_op(fc, F32), cb); + MemAccess ma = { .type = F32, .size = 4, .align = 4, + .alias.kind = ALIAS_LOCAL }; + T->store(T, IND_op(base, 0, F32), REG_op(fc, F32), ma); + T->store(T, IND_op(base, 4, F32), REG_op(fc, F32), ma); + + Reg dst = T->alloc_reg(T, RC_INT, I32); + CgTestArg args[] = { { .kind = CGT_ARG_BYVAL_LOCAL, .type = V, .v.slot = src } }; + cgtest_call(tf, f, I32, params, args, 1, REG_op(dst, I32)); + cgtest_ret_reg(tf, dst, I32); + cgtest_end(tf); +} + +/* helper used by g10: struct V g10_mk(void) { return (struct V){1.5f, 1.5f}; } + * Returned via the HFA path — abi_func_info classifies the struct as + * homogeneous-FP, so the backend places fields into v0/v1 instead of + * memcpying through an sret pointer. cgtest_ret_indirect drives both. */ +static ObjSymId build_g10_helper(CgTestCtx* ctx) +{ + const Type* F32 = T_f32(ctx); + const Type* V = build_g_hfa_type(ctx); + CgTestFn* tf = cgtest_begin_func(ctx, "g10_mk", V, NULL, 0); + CGTarget* T = ctx->target; + + static const u8 BYTES_15F[4] = { 0x00, 0x00, 0xC0, 0x3F }; + FrameSlot s = cgtest_local(tf, V, FSF_NONE); + Reg base = T->alloc_reg(T, RC_INT, T_ptr(ctx, V)); + T->addr_of(T, REG_op(base, T_ptr(ctx, V)), LOCAL_op(s, V)); + Reg fc = T->alloc_reg(T, RC_FP, F32); + ConstBytes cb = { .type = F32, .bytes = BYTES_15F, .size = 4, .align = 4 }; + T->load_const(T, REG_op(fc, F32), cb); + MemAccess ma = { .type = F32, .size = 4, .align = 4, + .alias.kind = ALIAS_LOCAL }; + T->store(T, IND_op(base, 0, F32), REG_op(fc, F32), ma); + T->store(T, IND_op(base, 4, F32), REG_op(fc, F32), ma); + + cgtest_ret_indirect(tf, s); + cgtest_end(tf); + return tf->sym; +} + +/* g10_hfa_return_f32x2 — sum fields of returned HFA, ftoi_s → 3. */ +static void build_g10_hfa_return_f32x2(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* F32 = T_f32(ctx); + const Type* V = build_g_hfa_type(ctx); + ObjSymId mk = build_g10_helper(ctx); + + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + FrameSlot dst = cgtest_local(tf, V, FSF_ADDR_TAKEN); + cgtest_call(tf, mk, V, NULL, NULL, 0, LOCAL_op(dst, V)); + + Reg base = T->alloc_reg(T, RC_INT, T_ptr(ctx, V)); + T->addr_of(T, REG_op(base, T_ptr(ctx, V)), LOCAL_op(dst, V)); + MemAccess ma = { .type = F32, .size = 4, .align = 4, + .alias.kind = ALIAS_LOCAL }; + Reg fx = T->alloc_reg(T, RC_FP, F32); + Reg fy = T->alloc_reg(T, RC_FP, F32); + Reg fs = T->alloc_reg(T, RC_FP, F32); + T->load(T, REG_op(fx, F32), IND_op(base, 0, F32), ma); + T->load(T, REG_op(fy, F32), IND_op(base, 4, F32), ma); + T->binop(T, BO_FADD, REG_op(fs, F32), REG_op(fx, F32), REG_op(fy, F32)); + Reg r = T->alloc_reg(T, RC_INT, I32); + T->convert(T, CV_FTOI_S, REG_op(r, I32), REG_op(fs, F32)); + cgtest_ret_reg(tf, r, I32); + cgtest_end(tf); +} + +/* g11_caller_saved_live_across_call — x=42 must survive a call that + * clobbers caller-saved regs. */ +static void build_g11_caller_saved_live_across_call(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* params[] = { I32 }; + ObjSymId echo = build_g_echo_helper(ctx, "g11_echo"); + + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + Reg x = T->alloc_reg(T, RC_INT, I32); + T->load_imm(T, REG_op(x, I32), 42); + + Reg ignored = T->alloc_reg(T, RC_INT, I32); + CgTestArg args[] = { { .kind = CGT_ARG_IMM, .type = I32, .v.imm = 99 } }; + cgtest_call(tf, echo, I32, params, args, 1, REG_op(ignored, I32)); + + cgtest_ret_reg(tf, x, I32); + cgtest_end(tf); +} + +/* g12_addr_taken_local_across_call — addr-taken local survives an + * intervening call. b05 body with a side call between increment and + * read-back. */ +static void build_g12_addr_taken_local_across_call(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* params[] = { I32 }; + ObjSymId echo = build_g_echo_helper(ctx, "g12_echo"); + + 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)); + MemAccess ma = { .type = I32, .size = 4, .align = 4, + .alias.kind = ALIAS_LOCAL }; + Reg val = T->alloc_reg(T, RC_INT, I32); + 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); + + /* intervening call — must not corrupt the local or its address. */ + Reg ignored = T->alloc_reg(T, RC_INT, I32); + CgTestArg args[] = { { .kind = CGT_ARG_IMM, .type = I32, .v.imm = 99 } }; + cgtest_call(tf, echo, I32, params, args, 1, REG_op(ignored, I32)); + + 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); +} + +/* g13_call_in_loop_induction — for(i=0;i<10;i++) s += id(i); → 45. + * Built on flat cmp_branch + jump, no SCOPE_LOOP — the induction var + * lives in an addr-taken slot to force frame-residency across the call. */ +static void build_g13_call_in_loop_induction(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* params[] = { I32 }; + ObjSymId id = build_g_echo_helper(ctx, "g13_id"); + + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + FrameSlot islot = cgtest_local(tf, I32, FSF_NONE); + FrameSlot sslot = cgtest_local(tf, I32, FSF_NONE); + cgtest_store_local(tf, islot, IMM_op(0, I32), I32); + cgtest_store_local(tf, sslot, IMM_op(0, I32), I32); + + Label top = T->label_new(T); + Label done = T->label_new(T); + T->label_place(T, top); + Reg ireg = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(ireg, I32), islot, I32); + T->cmp_branch(T, CMP_GE_S, REG_op(ireg, I32), IMM_op(10, I32), done); + + /* res = id(i); */ + Reg res = T->alloc_reg(T, RC_INT, I32); + CgTestArg args[] = { { .kind = CGT_ARG_REG, .type = I32, .v.reg = ireg } }; + cgtest_call(tf, id, I32, params, args, 1, REG_op(res, I32)); + + /* s += res; */ + Reg sreg = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(sreg, I32), sslot, I32); + T->binop(T, BO_IADD, REG_op(sreg, I32), REG_op(sreg, I32), REG_op(res, I32)); + cgtest_store_local(tf, sslot, REG_op(sreg, I32), I32); + + /* i++; jump top. */ + Reg inew = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(inew, I32), islot, I32); + T->binop(T, BO_IADD, REG_op(inew, I32), REG_op(inew, I32), IMM_op(1, I32)); + cgtest_store_local(tf, islot, REG_op(inew, I32), I32); + T->jump(T, top); + + T->label_place(T, done); + Reg out = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(out, I32), sslot, I32); + cgtest_ret_reg(tf, out, I32); + cgtest_end(tf); +} + +/* ============================================================ + * Group H: control flow + * + * Loops and multi-way branch beyond Group D's scope_if/scope_else. + * Loops use SCOPE_LOOP with explicit break/continue labels — the + * caller places continue at the appropriate point (top for while, + * after-body-before-incr for for-loops). Switches lower to chained + * cmp_branch + jump (no dedicated switch op). Short-circuit && / || + * are exercised by observing that the RHS side effect did not run. + * ============================================================ */ + +/* h01_while_sum_0_to_9 — int s=0,i=0; while(i<10){s+=i;i++;} return s; → 45. */ +static void build_h01_while_sum_0_to_9(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + FrameSlot ss = cgtest_local(tf, I32, FSF_NONE); + FrameSlot is = cgtest_local(tf, I32, FSF_NONE); + cgtest_store_local(tf, ss, IMM_op(0, I32), I32); + cgtest_store_local(tf, is, IMM_op(0, I32), I32); + + Label brk = T->label_new(T); + Label cnt = T->label_new(T); + CGScopeDesc d = { .kind = SCOPE_LOOP, + .break_label = brk, .continue_label = cnt }; + CGScope sc = T->scope_begin(T, &d); + T->label_place(T, cnt); + + Reg ir = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(ir, I32), is, I32); + T->cmp_branch(T, CMP_GE_S, REG_op(ir, I32), IMM_op(10, I32), brk); + + Reg sr = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(sr, I32), ss, I32); + T->binop(T, BO_IADD, REG_op(sr, I32), REG_op(sr, I32), REG_op(ir, I32)); + cgtest_store_local(tf, ss, REG_op(sr, I32), I32); + + T->binop(T, BO_IADD, REG_op(ir, I32), REG_op(ir, I32), IMM_op(1, I32)); + cgtest_store_local(tf, is, REG_op(ir, I32), I32); + T->jump(T, cnt); + + T->label_place(T, brk); + T->scope_end(T, sc); + + Reg out = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(out, I32), ss, I32); + cgtest_ret_reg(tf, out, I32); + cgtest_end(tf); +} + +/* h02_do_while_once — int i=0; do { i=42; } while(0); return i; → 42. */ +static void build_h02_do_while_once(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + FrameSlot is = cgtest_local(tf, I32, FSF_NONE); + cgtest_store_local(tf, is, IMM_op(0, I32), I32); + + Label brk = T->label_new(T); + Label cnt = T->label_new(T); + CGScopeDesc d = { .kind = SCOPE_LOOP, + .break_label = brk, .continue_label = cnt }; + CGScope sc = T->scope_begin(T, &d); + T->label_place(T, cnt); + + /* body: i = 42; */ + cgtest_store_local(tf, is, IMM_op(42, I32), I32); + + /* condition: while (0) — never taken. */ + Reg c = T->alloc_reg(T, RC_INT, I32); + T->load_imm(T, REG_op(c, I32), 0); + T->cmp_branch(T, CMP_NE, REG_op(c, I32), IMM_op(0, I32), cnt); + + T->label_place(T, brk); + T->scope_end(T, sc); + + Reg out = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(out, I32), is, I32); + cgtest_ret_reg(tf, out, I32); + cgtest_end(tf); +} + +/* h03_for_count_to_10 — for(i=1;i<=10;i++) s+=i; → 55. */ +static void build_h03_for_count_to_10(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + FrameSlot ss = cgtest_local(tf, I32, FSF_NONE); + FrameSlot is = cgtest_local(tf, I32, FSF_NONE); + cgtest_store_local(tf, ss, IMM_op(0, I32), I32); + cgtest_store_local(tf, is, IMM_op(1, I32), I32); + + Label brk = T->label_new(T); + Label cnt = T->label_new(T); /* increment site */ + Label top = T->label_new(T); /* condition test */ + CGScopeDesc d = { .kind = SCOPE_LOOP, + .break_label = brk, .continue_label = cnt }; + CGScope sc = T->scope_begin(T, &d); + + T->label_place(T, top); + Reg ir = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(ir, I32), is, I32); + T->cmp_branch(T, CMP_GT_S, REG_op(ir, I32), IMM_op(10, I32), brk); + + /* body: s += i; */ + Reg sr = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(sr, I32), ss, I32); + T->binop(T, BO_IADD, REG_op(sr, I32), REG_op(sr, I32), REG_op(ir, I32)); + cgtest_store_local(tf, ss, REG_op(sr, I32), I32); + + /* increment: i++; */ + T->label_place(T, cnt); + cgtest_load_local(tf, REG_op(ir, I32), is, I32); + T->binop(T, BO_IADD, REG_op(ir, I32), REG_op(ir, I32), IMM_op(1, I32)); + cgtest_store_local(tf, is, REG_op(ir, I32), I32); + T->jump(T, top); + + T->label_place(T, brk); + T->scope_end(T, sc); + + Reg out = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(out, I32), ss, I32); + cgtest_ret_reg(tf, out, I32); + cgtest_end(tf); +} + +/* h04_loop_break — for(i=0;;i++) if(i==42) break; return i; → 42. */ +static void build_h04_loop_break(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + FrameSlot is = cgtest_local(tf, I32, FSF_NONE); + cgtest_store_local(tf, is, IMM_op(0, I32), I32); + + Label brk = T->label_new(T); + Label cnt = T->label_new(T); + CGScopeDesc d = { .kind = SCOPE_LOOP, + .break_label = brk, .continue_label = cnt }; + CGScope sc = T->scope_begin(T, &d); + T->label_place(T, cnt); + + Reg ir = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(ir, I32), is, I32); + T->cmp_branch(T, CMP_EQ, REG_op(ir, I32), IMM_op(42, I32), brk); + + T->binop(T, BO_IADD, REG_op(ir, I32), REG_op(ir, I32), IMM_op(1, I32)); + cgtest_store_local(tf, is, REG_op(ir, I32), I32); + T->jump(T, cnt); + + T->label_place(T, brk); + T->scope_end(T, sc); + + Reg out = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(out, I32), is, I32); + cgtest_ret_reg(tf, out, I32); + cgtest_end(tf); +} + +/* h05_loop_continue — sum of even i in [0,20) using continue → 90. */ +static void build_h05_loop_continue(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + FrameSlot ss = cgtest_local(tf, I32, FSF_NONE); + FrameSlot is = cgtest_local(tf, I32, FSF_NONE); + cgtest_store_local(tf, ss, IMM_op(0, I32), I32); + cgtest_store_local(tf, is, IMM_op(0, I32), I32); + + Label brk = T->label_new(T); + Label cnt = T->label_new(T); /* the increment site */ + Label top = T->label_new(T); + CGScopeDesc d = { .kind = SCOPE_LOOP, + .break_label = brk, .continue_label = cnt }; + CGScope sc = T->scope_begin(T, &d); + + T->label_place(T, top); + Reg ir = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(ir, I32), is, I32); + T->cmp_branch(T, CMP_GE_S, REG_op(ir, I32), IMM_op(20, I32), brk); + + /* if (i & 1) continue; — odd → skip add. */ + Reg parity = T->alloc_reg(T, RC_INT, I32); + T->binop(T, BO_AND, REG_op(parity, I32), REG_op(ir, I32), IMM_op(1, I32)); + T->cmp_branch(T, CMP_NE, REG_op(parity, I32), IMM_op(0, I32), cnt); + + /* s += i; */ + Reg sr = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(sr, I32), ss, I32); + T->binop(T, BO_IADD, REG_op(sr, I32), REG_op(sr, I32), REG_op(ir, I32)); + cgtest_store_local(tf, ss, REG_op(sr, I32), I32); + + T->label_place(T, cnt); + cgtest_load_local(tf, REG_op(ir, I32), is, I32); + T->binop(T, BO_IADD, REG_op(ir, I32), REG_op(ir, I32), IMM_op(1, I32)); + cgtest_store_local(tf, is, REG_op(ir, I32), I32); + T->jump(T, top); + + T->label_place(T, brk); + T->scope_end(T, sc); + + Reg out = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(out, I32), ss, I32); + cgtest_ret_reg(tf, out, I32); + cgtest_end(tf); +} + +/* h06_nested_loops — for(i=0;i<3;i++) for(j=0;j<2;j++) s++; → 6. */ +static void build_h06_nested_loops(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + FrameSlot ss = cgtest_local(tf, I32, FSF_NONE); + FrameSlot is = cgtest_local(tf, I32, FSF_NONE); + FrameSlot js = cgtest_local(tf, I32, FSF_NONE); + cgtest_store_local(tf, ss, IMM_op(0, I32), I32); + cgtest_store_local(tf, is, IMM_op(0, I32), I32); + + Label outer_brk = T->label_new(T); + Label outer_cnt = T->label_new(T); + CGScopeDesc d_o = { .kind = SCOPE_LOOP, + .break_label = outer_brk, .continue_label = outer_cnt }; + CGScope outer = T->scope_begin(T, &d_o); + T->label_place(T, outer_cnt); + + Reg ir = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(ir, I32), is, I32); + T->cmp_branch(T, CMP_GE_S, REG_op(ir, I32), IMM_op(3, I32), outer_brk); + + cgtest_store_local(tf, js, IMM_op(0, I32), I32); + Label inner_brk = T->label_new(T); + Label inner_cnt = T->label_new(T); + CGScopeDesc d_i = { .kind = SCOPE_LOOP, + .break_label = inner_brk, .continue_label = inner_cnt }; + CGScope inner = T->scope_begin(T, &d_i); + T->label_place(T, inner_cnt); + + Reg jr = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(jr, I32), js, I32); + T->cmp_branch(T, CMP_GE_S, REG_op(jr, I32), IMM_op(2, I32), inner_brk); + + /* s++ */ + Reg sr = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(sr, I32), ss, I32); + T->binop(T, BO_IADD, REG_op(sr, I32), REG_op(sr, I32), IMM_op(1, I32)); + cgtest_store_local(tf, ss, REG_op(sr, I32), I32); + + /* j++ */ + T->binop(T, BO_IADD, REG_op(jr, I32), REG_op(jr, I32), IMM_op(1, I32)); + cgtest_store_local(tf, js, REG_op(jr, I32), I32); + T->jump(T, inner_cnt); + T->label_place(T, inner_brk); + T->scope_end(T, inner); + + /* i++ */ + T->binop(T, BO_IADD, REG_op(ir, I32), REG_op(ir, I32), IMM_op(1, I32)); + cgtest_store_local(tf, is, REG_op(ir, I32), I32); + T->jump(T, outer_cnt); + T->label_place(T, outer_brk); + T->scope_end(T, outer); + + Reg out = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(out, I32), ss, I32); + cgtest_ret_reg(tf, out, I32); + cgtest_end(tf); +} + +/* h07_break_inner_only — outer counts 3 iterations, inner breaks after + * incrementing s by 3 each time → s = 9. */ +static void build_h07_break_inner_only(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + FrameSlot ss = cgtest_local(tf, I32, FSF_NONE); + FrameSlot is = cgtest_local(tf, I32, FSF_NONE); + cgtest_store_local(tf, ss, IMM_op(0, I32), I32); + cgtest_store_local(tf, is, IMM_op(0, I32), I32); + + Label outer_brk = T->label_new(T); + Label outer_cnt = T->label_new(T); + CGScopeDesc d_o = { .kind = SCOPE_LOOP, + .break_label = outer_brk, .continue_label = outer_cnt }; + CGScope outer = T->scope_begin(T, &d_o); + T->label_place(T, outer_cnt); + + Reg ir = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(ir, I32), is, I32); + T->cmp_branch(T, CMP_GE_S, REG_op(ir, I32), IMM_op(3, I32), outer_brk); + + /* inner loop: counts 0..2, but inner-break exits after counter reaches 3 + * (so adds 3 to s each outer iteration). */ + FrameSlot js = cgtest_local(tf, I32, FSF_NONE); + cgtest_store_local(tf, js, IMM_op(0, I32), I32); + Label inner_brk = T->label_new(T); + Label inner_cnt = T->label_new(T); + CGScopeDesc d_i = { .kind = SCOPE_LOOP, + .break_label = inner_brk, .continue_label = inner_cnt }; + CGScope inner = T->scope_begin(T, &d_i); + T->label_place(T, inner_cnt); + + Reg jr = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(jr, I32), js, I32); + /* if (j >= 3) inner-break */ + T->cmp_branch(T, CMP_GE_S, REG_op(jr, I32), IMM_op(3, I32), inner_brk); + + /* s++ */ + Reg sr = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(sr, I32), ss, I32); + T->binop(T, BO_IADD, REG_op(sr, I32), REG_op(sr, I32), IMM_op(1, I32)); + cgtest_store_local(tf, ss, REG_op(sr, I32), I32); + + /* j++ */ + T->binop(T, BO_IADD, REG_op(jr, I32), REG_op(jr, I32), IMM_op(1, I32)); + cgtest_store_local(tf, js, REG_op(jr, I32), I32); + T->jump(T, inner_cnt); + T->label_place(T, inner_brk); + T->scope_end(T, inner); + + /* outer must continue past the inner break — i++ */ + T->binop(T, BO_IADD, REG_op(ir, I32), REG_op(ir, I32), IMM_op(1, I32)); + cgtest_store_local(tf, is, REG_op(ir, I32), I32); + T->jump(T, outer_cnt); + T->label_place(T, outer_brk); + T->scope_end(T, outer); + + Reg out = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(out, I32), ss, I32); + cgtest_ret_reg(tf, out, I32); + cgtest_end(tf); +} + +/* h08_early_return_in_loop — for(i=0;;i++) if(i==17) return i; → 17. */ +static void build_h08_early_return_in_loop(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + FrameSlot is = cgtest_local(tf, I32, FSF_NONE); + cgtest_store_local(tf, is, IMM_op(0, I32), I32); + + Label brk = T->label_new(T); + Label cnt = T->label_new(T); + Label hit = T->label_new(T); + CGScopeDesc d = { .kind = SCOPE_LOOP, + .break_label = brk, .continue_label = cnt }; + CGScope sc = T->scope_begin(T, &d); + T->label_place(T, cnt); + + Reg ir = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(ir, I32), is, I32); + T->cmp_branch(T, CMP_EQ, REG_op(ir, I32), IMM_op(17, I32), hit); + + T->binop(T, BO_IADD, REG_op(ir, I32), REG_op(ir, I32), IMM_op(1, I32)); + cgtest_store_local(tf, is, REG_op(ir, I32), I32); + T->jump(T, cnt); + + T->label_place(T, hit); + cgtest_ret_reg(tf, ir, I32); + /* the rest is dead. */ + T->label_place(T, brk); + T->scope_end(T, sc); + cgtest_ret_imm(tf, 0, I32); + cgtest_end(tf); +} + +/* h09_switch_three_cases — switch(2) {case 1:r=10;break; case 2:r=42;break; + * case 3:r=99;break;} → 42. */ +static void build_h09_switch_three_cases(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + FrameSlot rs = cgtest_local(tf, I32, FSF_NONE); + cgtest_store_local(tf, rs, IMM_op(0, I32), I32); + + Reg val = T->alloc_reg(T, RC_INT, I32); + T->load_imm(T, REG_op(val, I32), 2); + + Label l1 = T->label_new(T), l2 = T->label_new(T), l3 = T->label_new(T); + Label end = T->label_new(T); + T->cmp_branch(T, CMP_EQ, REG_op(val, I32), IMM_op(1, I32), l1); + T->cmp_branch(T, CMP_EQ, REG_op(val, I32), IMM_op(2, I32), l2); + T->cmp_branch(T, CMP_EQ, REG_op(val, I32), IMM_op(3, I32), l3); + T->jump(T, end); + + T->label_place(T, l1); + cgtest_store_local(tf, rs, IMM_op(10, I32), I32); + T->jump(T, end); + T->label_place(T, l2); + cgtest_store_local(tf, rs, IMM_op(42, I32), I32); + T->jump(T, end); + T->label_place(T, l3); + cgtest_store_local(tf, rs, IMM_op(99, I32), I32); + T->jump(T, end); + + T->label_place(T, end); + Reg out = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(out, I32), rs, I32); + cgtest_ret_reg(tf, out, I32); + cgtest_end(tf); +} + +/* h10_switch_fallthrough — switch(1){case 1: r+=10; case 2: r+=20;} → 30. */ +static void build_h10_switch_fallthrough(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + FrameSlot rs = cgtest_local(tf, I32, FSF_NONE); + cgtest_store_local(tf, rs, IMM_op(0, I32), I32); + + Reg val = T->alloc_reg(T, RC_INT, I32); + T->load_imm(T, REG_op(val, I32), 1); + + Label l1 = T->label_new(T), l2 = T->label_new(T); + Label end = T->label_new(T); + T->cmp_branch(T, CMP_EQ, REG_op(val, I32), IMM_op(1, I32), l1); + T->cmp_branch(T, CMP_EQ, REG_op(val, I32), IMM_op(2, I32), l2); + T->jump(T, end); + + T->label_place(T, l1); + { + Reg r = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(r, I32), rs, I32); + T->binop(T, BO_IADD, REG_op(r, I32), REG_op(r, I32), IMM_op(10, I32)); + cgtest_store_local(tf, rs, REG_op(r, I32), I32); + } + /* no break — fall through. */ + T->label_place(T, l2); + { + Reg r = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(r, I32), rs, I32); + T->binop(T, BO_IADD, REG_op(r, I32), REG_op(r, I32), IMM_op(20, I32)); + cgtest_store_local(tf, rs, REG_op(r, I32), I32); + } + T->jump(T, end); + + T->label_place(T, end); + Reg out = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(out, I32), rs, I32); + cgtest_ret_reg(tf, out, I32); + cgtest_end(tf); +} + +/* h11_switch_default — switch(99){case 1:r=10;break; default:r=7;} → 7. */ +static void build_h11_switch_default(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + FrameSlot rs = cgtest_local(tf, I32, FSF_NONE); + + Reg val = T->alloc_reg(T, RC_INT, I32); + T->load_imm(T, REG_op(val, I32), 99); + + Label l1 = T->label_new(T), ldef = T->label_new(T), end = T->label_new(T); + T->cmp_branch(T, CMP_EQ, REG_op(val, I32), IMM_op(1, I32), l1); + T->jump(T, ldef); + + T->label_place(T, l1); + cgtest_store_local(tf, rs, IMM_op(10, I32), I32); + T->jump(T, end); + T->label_place(T, ldef); + cgtest_store_local(tf, rs, IMM_op(7, I32), I32); + T->jump(T, end); + + T->label_place(T, end); + Reg out = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(out, I32), rs, I32); + cgtest_ret_reg(tf, out, I32); + cgtest_end(tf); +} + +/* h12_jump_forward — jump L; ret 99 (dead); L: ret 42; → 42. */ +static void build_h12_jump_forward(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + Label L = T->label_new(T); + T->jump(T, L); + cgtest_ret_imm(tf, 99, I32); /* dead */ + T->label_place(T, L); + cgtest_ret_imm(tf, 42, I32); + cgtest_end(tf); +} + +/* h13_jump_backward — counter loop entirely from cmp_branch + backward + * jump (no SCOPE_LOOP). Loops until i == 10. */ +static void build_h13_jump_backward(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + FrameSlot is = cgtest_local(tf, I32, FSF_NONE); + cgtest_store_local(tf, is, IMM_op(0, I32), I32); + + Label top = T->label_new(T); + Label end = T->label_new(T); + T->label_place(T, top); + Reg ir = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(ir, I32), is, I32); + T->cmp_branch(T, CMP_GE_S, REG_op(ir, I32), IMM_op(10, I32), end); + T->binop(T, BO_IADD, REG_op(ir, I32), REG_op(ir, I32), IMM_op(1, I32)); + cgtest_store_local(tf, is, REG_op(ir, I32), I32); + T->jump(T, top); + + T->label_place(T, end); + cgtest_ret_reg(tf, ir, I32); + cgtest_end(tf); +} + +/* h14_short_circuit_and_skip — `int s=0; (0) && (s=99,1); return s;` → 0. + * The RHS side effect must NOT execute when the LHS is 0. */ +static void build_h14_short_circuit_and_skip(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + FrameSlot ss = cgtest_local(tf, I32, FSF_NONE); + cgtest_store_local(tf, ss, IMM_op(0, I32), I32); + + Reg lhs = T->alloc_reg(T, RC_INT, I32); + T->load_imm(T, REG_op(lhs, I32), 0); + + Label rhs = T->label_new(T); + Label after = T->label_new(T); + /* if (lhs != 0) goto rhs; else fall through to "after" with skipped RHS. */ + T->cmp_branch(T, CMP_NE, REG_op(lhs, I32), IMM_op(0, I32), rhs); + T->jump(T, after); + + T->label_place(T, rhs); + /* RHS side effect: s = 99 (must not run). */ + cgtest_store_local(tf, ss, IMM_op(99, I32), I32); + + T->label_place(T, after); + Reg out = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(out, I32), ss, I32); + cgtest_ret_reg(tf, out, I32); + cgtest_end(tf); +} + +/* h15_short_circuit_or_skip — `int s=0; (1) || (s=99,1); return s;` → 0. */ +static void build_h15_short_circuit_or_skip(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + FrameSlot ss = cgtest_local(tf, I32, FSF_NONE); + cgtest_store_local(tf, ss, IMM_op(0, I32), I32); + + Reg lhs = T->alloc_reg(T, RC_INT, I32); + T->load_imm(T, REG_op(lhs, I32), 1); + + Label after = T->label_new(T); + /* if (lhs != 0) skip RHS. */ + T->cmp_branch(T, CMP_NE, REG_op(lhs, I32), IMM_op(0, I32), after); + + /* RHS side effect (must not run). */ + cgtest_store_local(tf, ss, IMM_op(99, I32), I32); + + T->label_place(T, after); + Reg out = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(out, I32), ss, I32); + cgtest_ret_reg(tf, out, I32); + cgtest_end(tf); +} + +/* h16_ternary — int x = (5 > 3) ? 42 : 7; return x; → 42. Uses scope_if. */ +static void build_h16_ternary(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + FrameSlot xs = cgtest_local(tf, I32, FSF_NONE); + + Reg c = T->alloc_reg(T, RC_INT, I32); + Reg a = T->alloc_reg(T, RC_INT, I32); + Reg b = T->alloc_reg(T, RC_INT, I32); + T->load_imm(T, REG_op(a, I32), 5); + T->load_imm(T, REG_op(b, I32), 3); + T->cmp(T, CMP_GT_S, REG_op(c, I32), REG_op(a, I32), REG_op(b, I32)); + CGScopeDesc desc = { .kind = SCOPE_IF, .cond = REG_op(c, I32) }; + CGScope s = T->scope_begin(T, &desc); + cgtest_store_local(tf, xs, IMM_op(42, I32), I32); + T->scope_else(T, s); + cgtest_store_local(tf, xs, IMM_op(7, I32), I32); + T->scope_end(T, s); + + Reg out = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(out, I32), xs, I32); + cgtest_ret_reg(tf, out, I32); + cgtest_end(tf); +} + +/* h17_ternary_side_effect_one_arm — int s=0; (1)?(s=42):(s=99); return s; → 42. + * Only the taken arm runs. Uses cmp_branch + flat labels (no scope). */ +static void build_h17_ternary_side_effect_one_arm(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + FrameSlot ss = cgtest_local(tf, I32, FSF_NONE); + cgtest_store_local(tf, ss, IMM_op(0, I32), I32); + + Reg c = T->alloc_reg(T, RC_INT, I32); + T->load_imm(T, REG_op(c, I32), 1); + + Label then_l = T->label_new(T); + Label end = T->label_new(T); + T->cmp_branch(T, CMP_NE, REG_op(c, I32), IMM_op(0, I32), then_l); + /* else arm */ + cgtest_store_local(tf, ss, IMM_op(99, I32), I32); + T->jump(T, end); + T->label_place(T, then_l); + cgtest_store_local(tf, ss, IMM_op(42, I32), I32); + T->label_place(T, end); + + Reg out = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(out, I32), ss, I32); + cgtest_ret_reg(tf, out, I32); + cgtest_end(tf); +} + +/* h18_unreachable_after_ret — emit a scalar ret followed by additional + * (unreachable) ops; backend must tolerate the dead tail. */ +static void build_h18_unreachable_after_ret(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + cgtest_ret_imm(tf, 42, I32); + + /* Dead instructions — should not execute, but the emitter must accept them. */ + Reg dead = T->alloc_reg(T, RC_INT, I32); + T->load_imm(T, REG_op(dead, I32), 99); + cgtest_ret_reg(tf, dead, I32); + cgtest_end(tf); +} + +/* ============================================================ + * Group I: alloca / VLA + * + * Stack-allocated runtime-sized memory: alloca with const and runtime + * sizes, alignment, in-loop distinctness, and crossing a call boundary + * with the alloca'd pointer. The alloca op signature is + * alloca_(target, dst REG, size Operand, align). + * ============================================================ */ + +/* i01_alloca_const_int — int *p = alloca(4); *p = 42; return *p. */ +static void build_i01_alloca_const_int(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* I64 = T_i64(ctx); + const Type* PI32 = T_ptr(ctx, I32); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + Reg p = T->alloc_reg(T, RC_INT, PI32); + T->alloca_(T, REG_op(p, PI32), IMM_op(4, I64), 4); + MemAccess ma = { .type = I32, .size = 4, .align = 4, + .alias.kind = ALIAS_LOCAL }; + T->store(T, IND_op(p, 0, I32), IMM_op(42, I32), ma); + Reg r = T->alloc_reg(T, RC_INT, I32); + T->load(T, REG_op(r, I32), IND_op(p, 0, I32), ma); + cgtest_ret_reg(tf, r, I32); + cgtest_end(tf); +} + +/* i02_alloca_runtime_size — int n=5; int *p = alloca(n*4); fill 1..5; sum=15. */ +static void build_i02_alloca_runtime_size(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* I64 = T_i64(ctx); + const Type* PI32 = T_ptr(ctx, I32); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + /* size_bytes = 5 * 4. Use I64 to match alloca's size operand. */ + Reg sz = T->alloc_reg(T, RC_INT, I64); + T->load_imm(T, REG_op(sz, I64), 20); + + Reg p = T->alloc_reg(T, RC_INT, PI32); + T->alloca_(T, REG_op(p, PI32), REG_op(sz, I64), 4); + + MemAccess ma = { .type = I32, .size = 4, .align = 4, + .alias.kind = ALIAS_LOCAL }; + /* p[0..4] = 1..5 */ + for (int i = 0; i < 5; ++i) { + T->store(T, IND_op(p, (i32)(i*4), I32), IMM_op(i+1, I32), ma); + } + /* sum */ + Reg acc = T->alloc_reg(T, RC_INT, I32); + T->load_imm(T, REG_op(acc, I32), 0); + for (int i = 0; i < 5; ++i) { + Reg v = T->alloc_reg(T, RC_INT, I32); + T->load(T, REG_op(v, I32), IND_op(p, (i32)(i*4), I32), ma); + T->binop(T, BO_IADD, REG_op(acc, I32), REG_op(acc, I32), REG_op(v, I32)); + } + cgtest_ret_reg(tf, acc, I32); + cgtest_end(tf); +} + +/* i03_alloca_align_16 — alloca(16, align=16); return ((p & 0xF) == 0). */ +static void build_i03_alloca_align_16(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* I64 = T_i64(ctx); + const Type* PV = T_ptr_void(ctx); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + Reg p = T->alloc_reg(T, RC_INT, PV); + T->alloca_(T, REG_op(p, PV), IMM_op(16, I64), 16); + + /* low_bits = p & 0xF */ + Reg lb = T->alloc_reg(T, RC_INT, I64); + T->binop(T, BO_AND, REG_op(lb, I64), REG_op(p, I64), IMM_op(0xF, I64)); + + /* result = (low_bits == 0) */ + Reg d = T->alloc_reg(T, RC_INT, I32); + T->cmp(T, CMP_EQ, REG_op(d, I32), REG_op(lb, I64), IMM_op(0, I64)); + cgtest_ret_reg(tf, d, I32); + cgtest_end(tf); +} + +/* i04_alloca_in_loop_distinct — three alloca(4)s in a loop; return + * (a != b && b != c). Addresses must differ across iterations. */ +static void build_i04_alloca_in_loop_distinct(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* I64 = T_i64(ctx); + const Type* PI32 = T_ptr(ctx, I32); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + /* Three slots to record the alloca'd addresses. */ + FrameSlot a = cgtest_local(tf, PI32, FSF_NONE); + FrameSlot b = cgtest_local(tf, PI32, FSF_NONE); + FrameSlot c = cgtest_local(tf, PI32, FSF_NONE); + FrameSlot is = cgtest_local(tf, I32, FSF_NONE); + cgtest_store_local(tf, is, IMM_op(0, I32), I32); + + Label brk = T->label_new(T); + Label cnt = T->label_new(T); + CGScopeDesc d = { .kind = SCOPE_LOOP, + .break_label = brk, .continue_label = cnt }; + CGScope sc = T->scope_begin(T, &d); + T->label_place(T, cnt); + + Reg ir = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(ir, I32), is, I32); + T->cmp_branch(T, CMP_GE_S, REG_op(ir, I32), IMM_op(3, I32), brk); + + /* p = alloca(4) */ + Reg p = T->alloc_reg(T, RC_INT, PI32); + T->alloca_(T, REG_op(p, PI32), IMM_op(4, I64), 4); + + /* select destination slot by i. */ + Label sa = T->label_new(T), sb = T->label_new(T), sc_l = T->label_new(T); + Label after_store = T->label_new(T); + T->cmp_branch(T, CMP_EQ, REG_op(ir, I32), IMM_op(0, I32), sa); + T->cmp_branch(T, CMP_EQ, REG_op(ir, I32), IMM_op(1, I32), sb); + T->jump(T, sc_l); + + T->label_place(T, sa); + cgtest_store_local(tf, a, REG_op(p, PI32), PI32); + T->jump(T, after_store); + T->label_place(T, sb); + cgtest_store_local(tf, b, REG_op(p, PI32), PI32); + T->jump(T, after_store); + T->label_place(T, sc_l); + cgtest_store_local(tf, c, REG_op(p, PI32), PI32); + T->label_place(T, after_store); + + /* i++ */ + T->binop(T, BO_IADD, REG_op(ir, I32), REG_op(ir, I32), IMM_op(1, I32)); + cgtest_store_local(tf, is, REG_op(ir, I32), I32); + T->jump(T, cnt); + T->label_place(T, brk); + T->scope_end(T, sc); + + /* return (a != b) & (b != c) */ + Reg ra = T->alloc_reg(T, RC_INT, PI32); + Reg rb = T->alloc_reg(T, RC_INT, PI32); + Reg rc = T->alloc_reg(T, RC_INT, PI32); + cgtest_load_local(tf, REG_op(ra, PI32), a, PI32); + cgtest_load_local(tf, REG_op(rb, PI32), b, PI32); + cgtest_load_local(tf, REG_op(rc, PI32), c, PI32); + + Reg ne1 = T->alloc_reg(T, RC_INT, I32); + Reg ne2 = T->alloc_reg(T, RC_INT, I32); + Reg both = T->alloc_reg(T, RC_INT, I32); + T->cmp(T, CMP_NE, REG_op(ne1, I32), REG_op(ra, PI32), REG_op(rb, PI32)); + T->cmp(T, CMP_NE, REG_op(ne2, I32), REG_op(rb, PI32), REG_op(rc, PI32)); + T->binop(T, BO_AND, REG_op(both, I32), REG_op(ne1, I32), REG_op(ne2, I32)); + cgtest_ret_reg(tf, both, I32); + cgtest_end(tf); +} + +/* helper used by i05: void fill(int *p, int v) { *p = v; } — same shape as + * g07 but a separate symbol so the cases don't share state. */ +static ObjSymId build_i05_helper(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* PI32 = T_ptr(ctx, I32); + const Type* VOID = T_void(ctx); + const Type* params[] = { PI32, I32 }; + CgTestFn* tf = cgtest_begin_func(ctx, "i05_fill", VOID, params, 2); + CGTarget* T = ctx->target; + + Reg p = T->alloc_reg(T, RC_INT, PI32); + Reg v = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(p, PI32), cgtest_param_slot(tf, 0), PI32); + cgtest_load_local(tf, REG_op(v, I32), cgtest_param_slot(tf, 1), I32); + MemAccess ma = { .type = I32, .size = 4, .align = 4, + .alias.kind = ALIAS_LOCAL }; + T->store(T, IND_op(p, 0, I32), REG_op(v, I32), ma); + cgtest_ret_void(tf); + cgtest_end(tf); + return tf->sym; +} + +/* i05_alloca_then_call — alloca buf; helper writes 42; load and return. */ +static void build_i05_alloca_then_call(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* I64 = T_i64(ctx); + const Type* PI32 = T_ptr(ctx, I32); + const Type* VOID = T_void(ctx); + const Type* params[] = { PI32, I32 }; + ObjSymId fill = build_i05_helper(ctx); + + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + Reg p = T->alloc_reg(T, RC_INT, PI32); + T->alloca_(T, REG_op(p, PI32), IMM_op(4, I64), 4); + + CgTestArg args[] = { + { .kind = CGT_ARG_REG, .type = PI32, .v.reg = p }, + { .kind = CGT_ARG_IMM, .type = I32, .v.imm = 42 }, + }; + cgtest_call(tf, fill, VOID, params, args, 2, IMM_op(0, VOID)); + + MemAccess ma = { .type = I32, .size = 4, .align = 4, + .alias.kind = ALIAS_LOCAL }; + Reg r = T->alloc_reg(T, RC_INT, I32); + T->load(T, REG_op(r, I32), IND_op(p, 0, I32), ma); + cgtest_ret_reg(tf, r, I32); + cgtest_end(tf); +} + +/* i06_two_allocas_disjoint — *p=1; *q=2; return *p + *q → 3. */ +static void build_i06_two_allocas_disjoint(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* I64 = T_i64(ctx); + const Type* PI32 = T_ptr(ctx, I32); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + Reg p = T->alloc_reg(T, RC_INT, PI32); + Reg q = T->alloc_reg(T, RC_INT, PI32); + T->alloca_(T, REG_op(p, PI32), IMM_op(4, I64), 4); + T->alloca_(T, REG_op(q, PI32), IMM_op(4, I64), 4); + + MemAccess ma = { .type = I32, .size = 4, .align = 4, + .alias.kind = ALIAS_LOCAL }; + T->store(T, IND_op(p, 0, I32), IMM_op(1, I32), ma); + T->store(T, IND_op(q, 0, I32), IMM_op(2, I32), ma); + + Reg vp = T->alloc_reg(T, RC_INT, I32); + Reg vq = T->alloc_reg(T, RC_INT, I32); + Reg s = T->alloc_reg(T, RC_INT, I32); + T->load(T, REG_op(vp, I32), IND_op(p, 0, I32), ma); + T->load(T, REG_op(vq, I32), IND_op(q, 0, I32), ma); + T->binop(T, BO_IADD, REG_op(s, I32), REG_op(vp, I32), REG_op(vq, I32)); + cgtest_ret_reg(tf, s, I32); + cgtest_end(tf); +} + +/* i07_alloca_addr_escapes — alloca'd pointer round-trips through an + * addr-taken local int**, then is dereferenced to write 42. The escape + * forces the alloca's pointer to be a real value, not a register-only + * temporary the optimizer could fold away. */ +static void build_i07_alloca_addr_escapes(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* I64 = T_i64(ctx); + const Type* PI32 = T_ptr(ctx, I32); + const Type* PPI32 = T_ptr(ctx, PI32); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + /* int *holder; */ + FrameSlot holder = cgtest_local(tf, PI32, FSF_ADDR_TAKEN); + + /* p = alloca(4); */ + Reg p = T->alloc_reg(T, RC_INT, PI32); + T->alloca_(T, REG_op(p, PI32), IMM_op(4, I64), 4); + + /* holder = p; */ + cgtest_store_local(tf, holder, REG_op(p, PI32), PI32); + + /* int **pp = &holder; */ + Reg pp = T->alloc_reg(T, RC_INT, PPI32); + T->addr_of(T, REG_op(pp, PPI32), LOCAL_op(holder, PI32)); + + /* int *back = *pp; *back = 42; return *back; */ + MemAccess ma_p = { .type = PI32, .size = 8, .align = 8, + .alias.kind = ALIAS_LOCAL }; + Reg back = T->alloc_reg(T, RC_INT, PI32); + T->load(T, REG_op(back, PI32), IND_op(pp, 0, PI32), ma_p); + + MemAccess ma_i = { .type = I32, .size = 4, .align = 4, + .alias.kind = ALIAS_LOCAL }; + T->store(T, IND_op(back, 0, I32), IMM_op(42, I32), ma_i); + Reg r = T->alloc_reg(T, RC_INT, I32); + T->load(T, REG_op(r, I32), IND_op(back, 0, I32), ma_i); + cgtest_ret_reg(tf, r, I32); + cgtest_end(tf); +} + +/* helper used by i08: int sum(int n, int *p) — n must be > 0. */ +static ObjSymId build_i08_helper(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* PI32 = T_ptr(ctx, I32); + const Type* params[] = { I32, PI32 }; + CgTestFn* tf = cgtest_begin_func(ctx, "i08_sum", I32, params, 2); + CGTarget* T = ctx->target; + + Reg n = T->alloc_reg(T, RC_INT, I32); + Reg p = T->alloc_reg(T, RC_INT, PI32); + cgtest_load_local(tf, REG_op(n, I32), cgtest_param_slot(tf, 0), I32); + cgtest_load_local(tf, REG_op(p, PI32), cgtest_param_slot(tf, 1), PI32); + + /* int s=0; for (i=0;i<n;i++) s += p[i]; */ + FrameSlot ss = cgtest_local(tf, I32, FSF_NONE); + FrameSlot is = cgtest_local(tf, I32, FSF_NONE); + cgtest_store_local(tf, ss, IMM_op(0, I32), I32); + cgtest_store_local(tf, is, IMM_op(0, I32), I32); + + Label top = T->label_new(T); + Label end = T->label_new(T); + T->label_place(T, top); + Reg ir = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(ir, I32), is, I32); + T->cmp_branch(T, CMP_GE_S, REG_op(ir, I32), REG_op(n, I32), end); + + /* offset_bytes = i * 4 */ + Reg ofs = T->alloc_reg(T, RC_INT, I32); + T->binop(T, BO_SHL, REG_op(ofs, I32), REG_op(ir, I32), IMM_op(2, I32)); + /* p_i = p + offset (use I64 ptr arith) */ + Reg pi = T->alloc_reg(T, RC_INT, PI32); + T->binop(T, BO_IADD, REG_op(pi, PI32), REG_op(p, PI32), REG_op(ofs, I32)); + + MemAccess ma = { .type = I32, .size = 4, .align = 4, + .alias.kind = ALIAS_LOCAL }; + Reg v = T->alloc_reg(T, RC_INT, I32); + T->load(T, REG_op(v, I32), IND_op(pi, 0, I32), ma); + + Reg sr = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(sr, I32), ss, I32); + T->binop(T, BO_IADD, REG_op(sr, I32), REG_op(sr, I32), REG_op(v, I32)); + cgtest_store_local(tf, ss, REG_op(sr, I32), I32); + + T->binop(T, BO_IADD, REG_op(ir, I32), REG_op(ir, I32), IMM_op(1, I32)); + cgtest_store_local(tf, is, REG_op(ir, I32), I32); + T->jump(T, top); + + T->label_place(T, end); + Reg out = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(out, I32), ss, I32); + cgtest_ret_reg(tf, out, I32); + cgtest_end(tf); + return tf->sym; +} + +/* i08_vla_param_sum — alloca 9 ints, fill 1..9, helper sums → 45. */ +static void build_i08_vla_param_sum(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* I64 = T_i64(ctx); + const Type* PI32 = T_ptr(ctx, I32); + const Type* params[] = { I32, PI32 }; + ObjSymId sum = build_i08_helper(ctx); + + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + /* alloca 9*4 = 36 bytes (round up to 40 for 8B align is fine). */ + Reg p = T->alloc_reg(T, RC_INT, PI32); + T->alloca_(T, REG_op(p, PI32), IMM_op(36, I64), 4); + + MemAccess ma = { .type = I32, .size = 4, .align = 4, + .alias.kind = ALIAS_LOCAL }; + for (int i = 0; i < 9; ++i) { + T->store(T, IND_op(p, (i32)(i*4), I32), IMM_op(i+1, I32), ma); + } + + Reg dst = T->alloc_reg(T, RC_INT, I32); + CgTestArg args[] = { + { .kind = CGT_ARG_IMM, .type = I32, .v.imm = 9 }, + { .kind = CGT_ARG_REG, .type = PI32, .v.reg = p }, + }; + cgtest_call(tf, sum, I32, params, args, 2, REG_op(dst, I32)); + cgtest_ret_reg(tf, dst, I32); + cgtest_end(tf); +} + +/* i09_alloca_preserves_locals — named locals declared before *and* after + * an alloca remain readable; the alloca must not overlap their slots. */ +static void build_i09_alloca_preserves_locals(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* I64 = T_i64(ctx); + const Type* PI32 = T_ptr(ctx, I32); + CgTestFn* tf = cgtest_begin_main(ctx, I32); + CGTarget* T = ctx->target; + + /* int x = 17 (declared before alloca). */ + FrameSlot x = cgtest_local(tf, I32, FSF_NONE); + cgtest_store_local(tf, x, IMM_op(17, I32), I32); + + /* alloca 4 bytes. */ + Reg p = T->alloc_reg(T, RC_INT, PI32); + T->alloca_(T, REG_op(p, PI32), IMM_op(4, I64), 4); + + /* int y = 25 (declared after alloca). */ + FrameSlot y = cgtest_local(tf, I32, FSF_NONE); + cgtest_store_local(tf, y, IMM_op(25, I32), I32); + + /* Touch the alloca'd memory so it isn't dead. */ + MemAccess ma = { .type = I32, .size = 4, .align = 4, + .alias.kind = ALIAS_LOCAL }; + T->store(T, IND_op(p, 0, I32), IMM_op(99, I32), ma); + + /* return x + y → 42. */ + Reg rx = T->alloc_reg(T, RC_INT, I32); + Reg ry = T->alloc_reg(T, RC_INT, I32); + Reg rs = T->alloc_reg(T, RC_INT, I32); + cgtest_load_local(tf, REG_op(rx, I32), x, I32); + cgtest_load_local(tf, REG_op(ry, I32), y, I32); + T->binop(T, BO_IADD, REG_op(rs, I32), REG_op(rx, I32), REG_op(ry, I32)); + cgtest_ret_reg(tf, rs, I32); + cgtest_end(tf); +} + +/* i10_alloca_after_named_local — frame layout must keep both addressable + * even when the named local is addr-taken. Same expected as i09 but the + * named local has FSF_ADDR_TAKEN so the backend must place it in the + * fixed-frame region, not in the dynamic alloca region. */ +static void build_i10_alloca_after_named_local(CgTestCtx* ctx) +{ + const Type* I32 = T_i32(ctx); + const Type* I64 = T_i64(ctx); + const Type* PI32 = T_ptr(ctx, I32); + 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(42, I32), I32); + + /* Take the address of x BEFORE the alloca. */ + Reg px = T->alloc_reg(T, RC_INT, PI32); + T->addr_of(T, REG_op(px, PI32), LOCAL_op(x, I32)); + + /* alloca; must not invalidate &x. */ + Reg dyn = T->alloc_reg(T, RC_INT, PI32); + T->alloca_(T, REG_op(dyn, PI32), IMM_op(8, I64), 4); + + /* Reload via the saved &x. */ + MemAccess ma = { .type = I32, .size = 4, .align = 4, + .alias.kind = ALIAS_LOCAL }; + Reg r = T->alloc_reg(T, RC_INT, I32); + T->load(T, REG_op(r, I32), IND_op(px, 0, I32), ma); + cgtest_ret_reg(tf, r, I32); + cgtest_end(tf); +} + +/* ============================================================ * Registry * ============================================================ */ @@ -1643,6 +3430,53 @@ const CgCase cg_cases[] = { { "f11_volatile_rw", build_f11_volatile_rw, 42, CG_CASE_DEFAULT }, { "f12_bitfield_unsigned", build_f12_bitfield_unsigned, 21, CG_CASE_DEFAULT }, { "f13_bitfield_signed", build_f13_bitfield_signed, 255, CG_CASE_DEFAULT }, + + /* Group G — calls (beyond direct-call path) */ + { "g01_indirect_call", build_g01_indirect_call, 42, CG_CASE_DEFAULT }, + { "g02_recursion_factorial", build_g02_recursion_factorial, 120, CG_CASE_DEFAULT }, + { "g03_recursion_fib", build_g03_recursion_fib, 55, CG_CASE_DEFAULT }, + { "g04_mutual_recursion", build_g04_mutual_recursion, 1, CG_CASE_DEFAULT }, + { "g05_chained_calls", build_g05_chained_calls, 42, CG_CASE_DEFAULT }, + { "g06_mixed_int_fp_params", build_g06_mixed_int_fp_params, 42, CG_CASE_DEFAULT }, + { "g07_void_call_outparam", build_g07_void_call_outparam, 42, CG_CASE_DEFAULT }, + { "g08_large_struct_byval", build_g08_large_struct_byval, 42, CG_CASE_DEFAULT }, + { "g09_hfa_param_f32x2", build_g09_hfa_param_f32x2, 3, CG_CASE_DEFAULT }, + { "g10_hfa_return_f32x2", build_g10_hfa_return_f32x2, 3, CG_CASE_DEFAULT }, + { "g11_caller_saved_live_across_call", build_g11_caller_saved_live_across_call, 42, CG_CASE_DEFAULT }, + { "g12_addr_taken_local_across_call", build_g12_addr_taken_local_across_call, 18, CG_CASE_DEFAULT }, + { "g13_call_in_loop_induction", build_g13_call_in_loop_induction, 45, CG_CASE_DEFAULT }, + + /* Group H — control flow */ + { "h01_while_sum_0_to_9", build_h01_while_sum_0_to_9, 45, CG_CASE_DEFAULT }, + { "h02_do_while_once", build_h02_do_while_once, 42, CG_CASE_DEFAULT }, + { "h03_for_count_to_10", build_h03_for_count_to_10, 55, CG_CASE_DEFAULT }, + { "h04_loop_break", build_h04_loop_break, 42, CG_CASE_DEFAULT }, + { "h05_loop_continue", build_h05_loop_continue, 90, CG_CASE_DEFAULT }, + { "h06_nested_loops", build_h06_nested_loops, 6, CG_CASE_DEFAULT }, + { "h07_break_inner_only", build_h07_break_inner_only, 9, CG_CASE_DEFAULT }, + { "h08_early_return_in_loop", build_h08_early_return_in_loop, 17, CG_CASE_DEFAULT }, + { "h09_switch_three_cases", build_h09_switch_three_cases, 42, CG_CASE_DEFAULT }, + { "h10_switch_fallthrough", build_h10_switch_fallthrough, 30, CG_CASE_DEFAULT }, + { "h11_switch_default", build_h11_switch_default, 7, CG_CASE_DEFAULT }, + { "h12_jump_forward", build_h12_jump_forward, 42, CG_CASE_DEFAULT }, + { "h13_jump_backward", build_h13_jump_backward, 10, CG_CASE_DEFAULT }, + { "h14_short_circuit_and_skip", build_h14_short_circuit_and_skip, 0, CG_CASE_DEFAULT }, + { "h15_short_circuit_or_skip", build_h15_short_circuit_or_skip, 0, CG_CASE_DEFAULT }, + { "h16_ternary", build_h16_ternary, 42, CG_CASE_DEFAULT }, + { "h17_ternary_side_effect_one_arm", build_h17_ternary_side_effect_one_arm, 42, CG_CASE_DEFAULT }, + { "h18_unreachable_after_ret", build_h18_unreachable_after_ret, 42, CG_CASE_DEFAULT }, + + /* Group I — alloca / VLA */ + { "i01_alloca_const_int", build_i01_alloca_const_int, 42, CG_CASE_DEFAULT }, + { "i02_alloca_runtime_size", build_i02_alloca_runtime_size, 15, CG_CASE_DEFAULT }, + { "i03_alloca_align_16", build_i03_alloca_align_16, 1, CG_CASE_DEFAULT }, + { "i04_alloca_in_loop_distinct", build_i04_alloca_in_loop_distinct, 1, CG_CASE_DEFAULT }, + { "i05_alloca_then_call", build_i05_alloca_then_call, 42, CG_CASE_DEFAULT }, + { "i06_two_allocas_disjoint", build_i06_two_allocas_disjoint, 3, CG_CASE_DEFAULT }, + { "i07_alloca_addr_escapes", build_i07_alloca_addr_escapes, 42, CG_CASE_DEFAULT }, + { "i08_vla_param_sum", build_i08_vla_param_sum, 45, CG_CASE_DEFAULT }, + { "i09_alloca_preserves_locals", build_i09_alloca_preserves_locals, 42, CG_CASE_DEFAULT }, + { "i10_alloca_after_named_local", build_i10_alloca_after_named_local, 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 @@ -101,12 +101,29 @@ CgTestFn* cgtest_begin_main(CgTestCtx* ctx, const Type* ret_ty) return cgtest_begin_func(ctx, "test_main", ret_ty, NULL, 0); } +ObjSymId cgtest_decl_func(CgTestCtx* ctx, const char* name) +{ + Sym sname = pool_intern_cstr(ctx->pool, name); + return obj_symbol(ctx->ob, sname, SB_GLOBAL, SK_FUNC, + OBJ_SEC_NONE, 0, 0); +} + CgTestFn* cgtest_begin_func(CgTestCtx* ctx, const char* name, const Type* ret_ty, const Type* const* param_types, u32 nparams) { + return cgtest_begin_func_at(ctx, cgtest_decl_func(ctx, name), + ret_ty, param_types, nparams); +} + +CgTestFn* cgtest_begin_func_at(CgTestCtx* ctx, + ObjSymId pre_sym, + 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; @@ -121,10 +138,7 @@ CgTestFn* cgtest_begin_func(CgTestCtx* ctx, 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); + tf->sym = pre_sym; /* Param slots + descriptors. Frame slots must be allocated against the * function's frame, which begins at func_begin — so we do this AFTER @@ -279,15 +293,17 @@ void cgtest_end(CgTestFn* tf) 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) +/* ---- calls ---- */ + +/* Shared body for direct and indirect calls. Direct sets callee.kind = + * OPK_GLOBAL; indirect sets OPK_REG. Everything else is identical. */ +static void cgtest_call_with_callee(CgTestFn* caller, + Operand callee, + const Type* ret_ty, + const Type* const* arg_types, + const CgTestArg* args, + u32 nargs, + Operand ret_storage) { CgTestCtx* ctx = caller->ctx; @@ -344,7 +360,7 @@ void cgtest_call(CgTestFn* caller, CGCallDesc desc; memset(&desc, 0, sizeof desc); desc.fn_type = fn_ty; desc.abi = info; - desc.callee = GLOBAL_op(callee_sym, 0); + desc.callee = callee; desc.args = avs; desc.nargs = nargs; desc.flags = CG_CALL_NONE; @@ -357,6 +373,42 @@ void cgtest_call(CgTestFn* caller, ctx->target->call(ctx->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) +{ + cgtest_call_with_callee(caller, GLOBAL_op(callee_sym, 0), + ret_ty, arg_types, args, nargs, ret_storage); +} + +void cgtest_call_indirect(CgTestFn* caller, + Reg callee, + const Type* ret_ty, + const Type* const* arg_types, + const CgTestArg* args, + u32 nargs, + Operand ret_storage) +{ + /* Function-pointer type for the callee operand; the backend reads + * desc.fn_type for ABI but uses callee.kind == OPK_REG to know it's + * indirect. The Type on the operand is informational. type_func wants + * a non-const argv, so copy through a fresh array. */ + const Type** ptypes_for_op = NULL; + if (nargs) { + ptypes_for_op = arena_array(caller->ctx->c->tu, const Type*, nargs); + for (u32 i = 0; i < nargs; ++i) ptypes_for_op[i] = arg_types[i]; + } + const Type* fn_ty_for_op = type_func(caller->ctx->pool, ret_ty, + ptypes_for_op, (u16)nargs, 0); + const Type* fnp_ty = type_ptr(caller->ctx->pool, fn_ty_for_op); + cgtest_call_with_callee(caller, REG_op(callee, fnp_ty), + ret_ty, arg_types, args, nargs, ret_storage); +} + /* ---- MC-only case helpers ---- */ ObjSymId cgtest_mc_begin_main(CgTestCtx* ctx) diff --git a/test/cg/harness/cg_test.h b/test/cg/harness/cg_test.h @@ -128,6 +128,21 @@ CgTestFn* cgtest_begin_func(CgTestCtx* ctx, const Type* const* param_types, u32 nparams); +/* Like cgtest_begin_func, but uses an already-allocated ObjSymId instead of + * creating one. Lets a case forward-declare a symbol with cgtest_decl_func + * (so a mutually-recursive partner can refer to it before its body emits) + * and then attach the definition here. */ +CgTestFn* cgtest_begin_func_at(CgTestCtx* ctx, + ObjSymId pre_sym, + const Type* ret_ty, + const Type* const* param_types, + u32 nparams); + +/* Forward-declare a function symbol with the given name. Returns an + * ObjSymId callable via cgtest_call before its body is emitted. The symbol + * is defined later by cgtest_begin_func_at(..., pre_sym, ...). */ +ObjSymId cgtest_decl_func(CgTestCtx*, const char* name); + FrameSlot cgtest_param_slot(CgTestFn*, u32 idx); /* ---- frame slots and memory ---- */ @@ -201,6 +216,17 @@ void cgtest_call(CgTestFn* caller, u32 nargs, Operand ret_storage); +/* Like cgtest_call, but the callee is held in a register at runtime — emits + * an indirect call. CGCallDesc.callee.kind is set to OPK_REG (as opposed to + * OPK_GLOBAL); the rest of the wiring (ABI, args, ret) is identical. */ +void cgtest_call_indirect(CgTestFn* caller, + Reg callee, + 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