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:
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