commit 626bb143300e32d23b3b80d4e92a4ba5bc80dd34
parent 5ae1703a745e575657fee65c946f9e1f241d4a3a
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Sat, 9 May 2026 15:21:40 -0700
test/cg: register Groups N, O, Q (TLS, globals, multi-function)
Adds 31 cases across three previously-deferred groups, each in its own
cases_<x>.c. Expectations target the eventual contract (tls_addr_of,
GLOBAL load/store, per-function text_section_id, multi-function TUs);
many will fail until the backend grows them.
Diffstat:
6 files changed, 1316 insertions(+), 3 deletions(-)
diff --git a/test/cg/CORPUS.md b/test/cg/CORPUS.md
@@ -322,15 +322,85 @@ in two REG dsts; cases observe each independently.
| `l19_sub_overflow_yes` | · | `sub_overflow(INT_MIN,1,&r) → ovf=1`; return `ovf` | 1 |
| `l20_mul_overflow_no` | · | `mul_overflow(6,7,&r) → ovf=0`; return `r` | 42 |
+## Group N — TLS (thread-local storage)
+
+Drives `CGTarget.tls_addr_of` plus the `SK_TLS` / `SF_TLS` section/symbol
+machinery on `ObjBuilder`. Each case allocates a `.tdata` (initialized)
+or `.tbss` (zero-init) section, defines a `SK_TLS` symbol in it, and
+accesses storage via `tls_addr_of` → INDIRECT load/store. The backend
+chooses the TLS model (LE/IE/LD/GD) from `c->target` and the symbol's
+visibility; the expectations here don't presume one.
+
+The aarch64 backend currently implements TLS Local-Exec only (commit
+c1cf117). Path E requires `test/link/harness/start.c`'s TCB+TLS image
+setup; paths D/J have no per-thread TLS context yet and are expected to
+fail until the JIT runners grow it.
+
+| Case | Status | Body | Expected |
+|---|---|---|---|
+| `n01_tls_load_le` | · | `_Thread_local int x=42; return x;` (`.tdata`) | 42 |
+| `n02_tls_store_le` | · | `_Thread_local int x; x=42; return x;` (`.tbss`) | 42 |
+| `n03_tls_addr_taken` | · | `_Thread_local int x=17; int*p=&x; *p+=1; return *p;` | 18 |
+| `n04_tls_i64` | · | `_Thread_local long long x=0x1_0000_002A; return (int)x;` | 42 |
+| `n05_tls_in_loop` | · | TLS access inside loop — addr may be hoisted but correct | 10 |
+| `n06_tls_two_vars` | · | two distinct TLS vars; `a+b = 10+32` | 42 |
+| `n07_tls_bss_zero_init` | · | `_Thread_local int x;` — `.tbss` reads as zero | 0 |
+| `n08_tls_addend_offset` | · | `_Thread_local int a[8]={...,42}; return a[7];` | 42 |
+
+## Group O — sections and globals (non-TLS)
+
+Drives `addr_of` on `OPK_GLOBAL` operands plus direct `load`/`store`
+through `GLOBAL_op` (the spec accepts `LOCAL|GLOBAL|INDIRECT` addr
+operands). Exercises the `SecKind` × `SymKind` × `SymBind` matrix on
+`ObjBuilder`: `SEC_DATA`, `SEC_BSS`, `SEC_RODATA` × `SK_OBJ` ×
+`SB_GLOBAL` / `SB_LOCAL`, plus a named non-default text section for a
+function. Aggregate-global cases reuse the `Pt` type from
+`cases_shared.c` so its `TagId` interns once across groups.
+
+| Case | Status | Body | Expected |
+|---|---|---|---|
+| `o01_global_load_data` | · | `int g=42; return g;` — direct GLOBAL load | 42 |
+| `o02_global_store_data` | · | `int g; g=42; return g;` — store via GLOBAL operand | 42 |
+| `o03_global_bss_zero` | · | uninitialized `.bss` reads as zero | 0 |
+| `o04_global_addr_taken` | · | b05 over a global: `&g`, +=1, reload | 18 |
+| `o05_global_i64` | · | `long long g=0x1_0000_002A; return (int)g;` | 42 |
+| `o06_rodata_load` | · | `static const int rd[4]={1,2,42,4}; return rd[2];` | 42 |
+| `o07_global_struct_field` | · | `struct Pt g={10,32}; return g.a + g.b;` | 42 |
+| `o08_global_array_runtime_idx` | · | `int g[5]={1..5}; int i=2; return g[i];` | 3 |
+| `o09_static_local_linkage` | · | `static int g=42;` — SB_LOCAL data sym | 42 |
+| `o10_global_addend` | · | `int g[8]={...,42};` access via OPK_GLOBAL addend = 28 | 42 |
+| `o11_text_section_named` | · | helper placed in `.text.o11_helper`; main calls it | 42 |
+| `o12_global_across_call` | · | `&g` materialized; intervening call must not corrupt it | 42 |
+
+## Group Q — multi-function (extends Group B's two-function pattern)
+
+Group B established that two `func_begin`/`func_end` pairs work in one
+TU. Group Q stresses what falls out as the function count grows and
+linkage/section attributes vary: many small helpers, mixed
+`SB_GLOBAL`/`SB_LOCAL`, distinct signatures sharing one `CGTarget`,
+per-function text sections (`-ffunction-sections` analogue), and
+forward-declared helpers defined later in the TU.
+
+| Case | Status | Body | Expected |
+|---|---|---|---|
+| `q01_three_helpers` | · | `a()+b()+c() = 10+15+17` | 42 |
+| `q02_static_internal_linkage` | · | `static int helper(void){return 42;}` — SB_LOCAL | 42 |
+| `q03_intra_tu_call_chain` | · | `a→b→c→d`; d returns 42 | 42 |
+| `q04_eight_helpers` | · | start at 6, chain through 8 helpers each adding `i+1` | 42 |
+| `q05_distinct_signatures` | · | int(int), long(long,long), void(int*), int(void) all called | 42 |
+| `q06_function_section_distinct` | · | helper in `.text.q06_helper`, main in default `.text` | 42 |
+| `q07_cross_section_calls` | · | a in `.text.q07_a` calls b in `.text.q07_b`; main calls a | 42 |
+| `q08_forward_decl_define_late` | · | main calls helper before the helper body is emitted | 42 |
+| `q09_helper_calls_helper` | · | `a()` calls `b()`; main calls `a()` | 42 |
+| `q10_global_and_static_mix` | · | one SB_GLOBAL + two SB_LOCAL helpers; sum = 12+15+15 | 42 |
+| `q11_addr_of_helper_through_global` | · | function ptr stored in `.data` (R_ABS64); indirect call | 42 |
+
## Deferred groups
| Group | Theme |
|---|---|
| M | inline asm |
-| N | TLS |
-| O | sections + globals |
| P | set_loc / debug |
-| Q | multi-function (extends Group B's two-function pattern) |
| R | opt-wrapped equivalence |
See `doc/cg_testing.md` for the strategy and group definitions.
diff --git a/test/cg/harness/cases.c b/test/cg/harness/cases.c
@@ -186,6 +186,40 @@ void build_l18_add_overflow_yes(CgTestCtx*);
void build_l19_sub_overflow_yes(CgTestCtx*);
void build_l20_mul_overflow_no(CgTestCtx*);
+void build_n01_tls_load_le(CgTestCtx*);
+void build_n02_tls_store_le(CgTestCtx*);
+void build_n03_tls_addr_taken(CgTestCtx*);
+void build_n04_tls_i64(CgTestCtx*);
+void build_n05_tls_in_loop(CgTestCtx*);
+void build_n06_tls_two_vars(CgTestCtx*);
+void build_n07_tls_bss_zero_init(CgTestCtx*);
+void build_n08_tls_addend_offset(CgTestCtx*);
+
+void build_o01_global_load_data(CgTestCtx*);
+void build_o02_global_store_data(CgTestCtx*);
+void build_o03_global_bss_zero(CgTestCtx*);
+void build_o04_global_addr_taken(CgTestCtx*);
+void build_o05_global_i64(CgTestCtx*);
+void build_o06_rodata_load(CgTestCtx*);
+void build_o07_global_struct_field(CgTestCtx*);
+void build_o08_global_array_runtime_idx(CgTestCtx*);
+void build_o09_static_local_linkage(CgTestCtx*);
+void build_o10_global_addend(CgTestCtx*);
+void build_o11_text_section_named(CgTestCtx*);
+void build_o12_global_across_call(CgTestCtx*);
+
+void build_q01_three_helpers(CgTestCtx*);
+void build_q02_static_internal_linkage(CgTestCtx*);
+void build_q03_intra_tu_call_chain(CgTestCtx*);
+void build_q04_eight_helpers(CgTestCtx*);
+void build_q05_distinct_signatures(CgTestCtx*);
+void build_q06_function_section_distinct(CgTestCtx*);
+void build_q07_cross_section_calls(CgTestCtx*);
+void build_q08_forward_decl_define_late(CgTestCtx*);
+void build_q09_helper_calls_helper(CgTestCtx*);
+void build_q10_global_and_static_mix(CgTestCtx*);
+void build_q11_addr_of_helper_through_global(CgTestCtx*);
+
/* ---- registry ---- */
const CgCase cg_cases[] = {
@@ -371,6 +405,43 @@ const CgCase cg_cases[] = {
{ "l18_add_overflow_yes", build_l18_add_overflow_yes, 1, CG_CASE_DEFAULT },
{ "l19_sub_overflow_yes", build_l19_sub_overflow_yes, 1, CG_CASE_DEFAULT },
{ "l20_mul_overflow_no", build_l20_mul_overflow_no, 42, CG_CASE_DEFAULT },
+
+ /* Group N — TLS */
+ { "n01_tls_load_le", build_n01_tls_load_le, 42, CG_CASE_DEFAULT },
+ { "n02_tls_store_le", build_n02_tls_store_le, 42, CG_CASE_DEFAULT },
+ { "n03_tls_addr_taken", build_n03_tls_addr_taken, 18, CG_CASE_DEFAULT },
+ { "n04_tls_i64", build_n04_tls_i64, 42, CG_CASE_DEFAULT },
+ { "n05_tls_in_loop", build_n05_tls_in_loop, 10, CG_CASE_DEFAULT },
+ { "n06_tls_two_vars", build_n06_tls_two_vars, 42, CG_CASE_DEFAULT },
+ { "n07_tls_bss_zero_init", build_n07_tls_bss_zero_init, 0, CG_CASE_DEFAULT },
+ { "n08_tls_addend_offset", build_n08_tls_addend_offset, 42, CG_CASE_DEFAULT },
+
+ /* Group O — sections and globals */
+ { "o01_global_load_data", build_o01_global_load_data, 42, CG_CASE_DEFAULT },
+ { "o02_global_store_data", build_o02_global_store_data, 42, CG_CASE_DEFAULT },
+ { "o03_global_bss_zero", build_o03_global_bss_zero, 0, CG_CASE_DEFAULT },
+ { "o04_global_addr_taken", build_o04_global_addr_taken, 18, CG_CASE_DEFAULT },
+ { "o05_global_i64", build_o05_global_i64, 42, CG_CASE_DEFAULT },
+ { "o06_rodata_load", build_o06_rodata_load, 42, CG_CASE_DEFAULT },
+ { "o07_global_struct_field", build_o07_global_struct_field, 42, CG_CASE_DEFAULT },
+ { "o08_global_array_runtime_idx", build_o08_global_array_runtime_idx, 3, CG_CASE_DEFAULT },
+ { "o09_static_local_linkage", build_o09_static_local_linkage, 42, CG_CASE_DEFAULT },
+ { "o10_global_addend", build_o10_global_addend, 42, CG_CASE_DEFAULT },
+ { "o11_text_section_named", build_o11_text_section_named, 42, CG_CASE_DEFAULT },
+ { "o12_global_across_call", build_o12_global_across_call, 42, CG_CASE_DEFAULT },
+
+ /* Group Q — multi-function */
+ { "q01_three_helpers", build_q01_three_helpers, 42, CG_CASE_DEFAULT },
+ { "q02_static_internal_linkage", build_q02_static_internal_linkage, 42, CG_CASE_DEFAULT },
+ { "q03_intra_tu_call_chain", build_q03_intra_tu_call_chain, 42, CG_CASE_DEFAULT },
+ { "q04_eight_helpers", build_q04_eight_helpers, 42, CG_CASE_DEFAULT },
+ { "q05_distinct_signatures", build_q05_distinct_signatures, 42, CG_CASE_DEFAULT },
+ { "q06_function_section_distinct", build_q06_function_section_distinct, 42, CG_CASE_DEFAULT },
+ { "q07_cross_section_calls", build_q07_cross_section_calls, 42, CG_CASE_DEFAULT },
+ { "q08_forward_decl_define_late", build_q08_forward_decl_define_late, 42, CG_CASE_DEFAULT },
+ { "q09_helper_calls_helper", build_q09_helper_calls_helper, 42, CG_CASE_DEFAULT },
+ { "q10_global_and_static_mix", build_q10_global_and_static_mix, 42, CG_CASE_DEFAULT },
+ { "q11_addr_of_helper_through_global", build_q11_addr_of_helper_through_global, 42, CG_CASE_DEFAULT },
};
const unsigned cg_cases_count = sizeof(cg_cases) / sizeof(cg_cases[0]);
diff --git a/test/cg/harness/cases_n.c b/test/cg/harness/cases_n.c
@@ -0,0 +1,299 @@
+/* Group N — TLS (thread-local storage).
+ * See CORPUS.md for the case list and expected values.
+ *
+ * Drives CGTarget.tls_addr_of and the SK_TLS / SF_TLS section/symbol
+ * machinery on ObjBuilder. Each case allocates a `.tdata` (initialized)
+ * or `.tbss` (zero-init) section, defines an SK_TLS symbol in it, and
+ * accesses the storage via tls_addr_of → INDIRECT load/store.
+ *
+ * The aarch64 backend currently implements TLS Local-Exec only (see
+ * c1cf117); GD/IE/LD models are not wired up. Path E (link+run) requires
+ * test/link/harness/start.c's TCB+TLS setup; paths D/J have no TLS host
+ * thread context, so they are expected to fail until the JIT runner
+ * grows TLS support. */
+
+#include "cg_test.h"
+
+/* ============================================================
+ * Group N: TLS — _Thread_local globals via tls_addr_of
+ * ============================================================ */
+
+/* Helper: define a `.tdata` section once, return its id. */
+static ObjSecId tls_get_tdata(CgTestCtx* ctx)
+{
+ Sym name = pool_intern_cstr(ctx->pool, ".tdata");
+ return obj_section(ctx->ob, name, SEC_DATA,
+ SF_ALLOC | SF_WRITE | SF_TLS, 4);
+}
+
+/* Helper: define a `.tbss` section once, return its id. */
+static ObjSecId tls_get_tbss(CgTestCtx* ctx)
+{
+ Sym name = pool_intern_cstr(ctx->pool, ".tbss");
+ return obj_section_ex(ctx->ob, name, SEC_BSS, SSEM_NOBITS,
+ SF_ALLOC | SF_WRITE | SF_TLS, 4, 0, 0, 0);
+}
+
+/* Helper: define an initialized TLS symbol. Writes `bytes` at the
+ * current `.tdata` position and emits a SK_TLS symbol pointing to it. */
+static ObjSymId tls_define_init(CgTestCtx* ctx, const char* name,
+ const u8* bytes, u32 size, u32 align)
+{
+ ObjSecId sec = tls_get_tdata(ctx);
+ obj_section_set_align(ctx->ob, sec, align);
+ u32 ofs = obj_pos(ctx->ob, sec);
+ /* Pad up to alignment. */
+ while (ofs & (align - 1)) {
+ u8 zero = 0;
+ obj_write(ctx->ob, sec, &zero, 1);
+ ofs++;
+ }
+ obj_write(ctx->ob, sec, bytes, size);
+ Sym sname = pool_intern_cstr(ctx->pool, name);
+ return obj_symbol(ctx->ob, sname, SB_GLOBAL, SK_TLS, sec, ofs, size);
+}
+
+/* Helper: define a zero-initialized TLS symbol in `.tbss`. */
+static ObjSymId tls_define_bss(CgTestCtx* ctx, const char* name,
+ u32 size, u32 align)
+{
+ ObjSecId sec = tls_get_tbss(ctx);
+ obj_section_set_align(ctx->ob, sec, align);
+ /* obj_reserve_bss tracks bss_size; the symbol value is the offset
+ * within .tbss, which equals the section's bss_size before reserve. */
+ const Section* s = obj_section_get(ctx->ob, sec);
+ u32 ofs = s->bss_size;
+ while (ofs & (align - 1)) ofs++;
+ obj_reserve_bss(ctx->ob, sec, ofs - s->bss_size + size, align);
+ Sym sname = pool_intern_cstr(ctx->pool, name);
+ return obj_symbol(ctx->ob, sname, SB_GLOBAL, SK_TLS, sec, ofs, size);
+}
+
+/* n01_tls_load_le — _Thread_local int x = 42; return x; */
+void build_n01_tls_load_le(CgTestCtx* ctx)
+{
+ const Type* I32 = T_i32(ctx);
+ static const u8 INIT[4] = { 42, 0, 0, 0 };
+ ObjSymId x = tls_define_init(ctx, "n01_x", INIT, 4, 4);
+
+ CgTestFn* tf = cgtest_begin_main(ctx, I32);
+ CGTarget* T = ctx->target;
+
+ Reg p = T->alloc_reg(T, RC_INT, T_ptr(ctx, I32));
+ T->tls_addr_of(T, REG_op(p, T_ptr(ctx, I32)), x, 0);
+
+ Reg r = T->alloc_reg(T, RC_INT, I32);
+ MemAccess ma = { .type = I32, .size = 4, .align = 4,
+ .alias.kind = ALIAS_GLOBAL };
+ T->load(T, REG_op(r, I32), IND_op(p, 0, I32), ma);
+ cgtest_ret_reg(tf, r, I32);
+ cgtest_end(tf);
+}
+
+/* n02_tls_store_le — _Thread_local int x; x = 42; return x; */
+void build_n02_tls_store_le(CgTestCtx* ctx)
+{
+ const Type* I32 = T_i32(ctx);
+ ObjSymId x = tls_define_bss(ctx, "n02_x", 4, 4);
+
+ CgTestFn* tf = cgtest_begin_main(ctx, I32);
+ CGTarget* T = ctx->target;
+
+ Reg p = T->alloc_reg(T, RC_INT, T_ptr(ctx, I32));
+ T->tls_addr_of(T, REG_op(p, T_ptr(ctx, I32)), x, 0);
+
+ MemAccess ma = { .type = I32, .size = 4, .align = 4,
+ .alias.kind = ALIAS_GLOBAL };
+ 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);
+}
+
+/* n03_tls_addr_taken — _Thread_local int x = 17; int *p = &x; *p += 1;
+ * return *p; — addr-taken TLS local; one materialization of the
+ * thread pointer is reused for the load/store/load sequence. */
+void build_n03_tls_addr_taken(CgTestCtx* ctx)
+{
+ const Type* I32 = T_i32(ctx);
+ static const u8 INIT[4] = { 17, 0, 0, 0 };
+ ObjSymId x = tls_define_init(ctx, "n03_x", INIT, 4, 4);
+
+ CgTestFn* tf = cgtest_begin_main(ctx, I32);
+ CGTarget* T = ctx->target;
+
+ Reg p = T->alloc_reg(T, RC_INT, T_ptr(ctx, I32));
+ T->tls_addr_of(T, REG_op(p, T_ptr(ctx, I32)), x, 0);
+
+ MemAccess ma = { .type = I32, .size = 4, .align = 4,
+ .alias.kind = ALIAS_GLOBAL };
+ 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);
+
+ 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);
+}
+
+/* n04_tls_i64 — _Thread_local long long x = 0x1_0000_002A;
+ * return (int)x; — exercises 8-byte TLS access with TLSLE_LDST64
+ * relocation kinds (vs the 32-bit family in n01/n02). */
+void build_n04_tls_i64(CgTestCtx* ctx)
+{
+ const Type* I32 = T_i32(ctx);
+ const Type* I64 = T_i64(ctx);
+ static const u8 INIT[8] = { 0x2A, 0, 0, 0, 0x01, 0, 0, 0 };
+ ObjSymId x = tls_define_init(ctx, "n04_x", INIT, 8, 8);
+
+ CgTestFn* tf = cgtest_begin_main(ctx, I32);
+ CGTarget* T = ctx->target;
+
+ Reg p = T->alloc_reg(T, RC_INT, T_ptr(ctx, I64));
+ T->tls_addr_of(T, REG_op(p, T_ptr(ctx, I64)), x, 0);
+
+ Reg r64 = T->alloc_reg(T, RC_INT, I64);
+ MemAccess ma = { .type = I64, .size = 8, .align = 8,
+ .alias.kind = ALIAS_GLOBAL };
+ T->load(T, REG_op(r64, I64), IND_op(p, 0, I64), ma);
+
+ Reg r32 = T->alloc_reg(T, RC_INT, I32);
+ T->convert(T, CV_TRUNC, REG_op(r32, I32), REG_op(r64, I64));
+ cgtest_ret_reg(tf, r32, I32);
+ cgtest_end(tf);
+}
+
+/* n05_tls_in_loop — TLS access inside a loop; the address materialization
+ * may be hoisted by opt_cgtarget but must remain correct. Body:
+ * _Thread_local int x = 0;
+ * for (i = 0; i < 10; i++) x += 1;
+ * return x; → 10 */
+void build_n05_tls_in_loop(CgTestCtx* ctx)
+{
+ const Type* I32 = T_i32(ctx);
+ static const u8 INIT[4] = { 0, 0, 0, 0 };
+ ObjSymId x = tls_define_init(ctx, "n05_x", INIT, 4, 4);
+
+ CgTestFn* tf = cgtest_begin_main(ctx, I32);
+ CGTarget* T = ctx->target;
+
+ FrameSlot islot = cgtest_local(tf, I32, FSF_NONE);
+ cgtest_store_local(tf, islot, 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);
+
+ /* x += 1; */
+ Reg p = T->alloc_reg(T, RC_INT, T_ptr(ctx, I32));
+ T->tls_addr_of(T, REG_op(p, T_ptr(ctx, I32)), x, 0);
+ MemAccess ma = { .type = I32, .size = 4, .align = 4,
+ .alias.kind = ALIAS_GLOBAL };
+ Reg cur = T->alloc_reg(T, RC_INT, I32);
+ T->load (T, REG_op(cur, I32), IND_op(p, 0, I32), ma);
+ T->binop(T, BO_IADD, REG_op(cur, I32), REG_op(cur, I32), IMM_op(1, I32));
+ T->store(T, IND_op(p, 0, I32), REG_op(cur, I32), ma);
+
+ 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 p2 = T->alloc_reg(T, RC_INT, T_ptr(ctx, I32));
+ T->tls_addr_of(T, REG_op(p2, T_ptr(ctx, I32)), x, 0);
+ Reg out = T->alloc_reg(T, RC_INT, I32);
+ T->load(T, REG_op(out, I32), IND_op(p2, 0, I32), ma);
+ cgtest_ret_reg(tf, out, I32);
+ cgtest_end(tf);
+}
+
+/* n06_tls_two_vars — two distinct TLS variables; sum = 42.
+ * _Thread_local int a = 10;
+ * _Thread_local int b = 32;
+ * return a + b; */
+void build_n06_tls_two_vars(CgTestCtx* ctx)
+{
+ const Type* I32 = T_i32(ctx);
+ static const u8 INIT_A[4] = { 10, 0, 0, 0 };
+ static const u8 INIT_B[4] = { 32, 0, 0, 0 };
+ ObjSymId a = tls_define_init(ctx, "n06_a", INIT_A, 4, 4);
+ ObjSymId b = tls_define_init(ctx, "n06_b", INIT_B, 4, 4);
+
+ CgTestFn* tf = cgtest_begin_main(ctx, I32);
+ CGTarget* T = ctx->target;
+ MemAccess ma = { .type = I32, .size = 4, .align = 4,
+ .alias.kind = ALIAS_GLOBAL };
+
+ Reg pa = T->alloc_reg(T, RC_INT, T_ptr(ctx, I32));
+ Reg pb = T->alloc_reg(T, RC_INT, T_ptr(ctx, I32));
+ T->tls_addr_of(T, REG_op(pa, T_ptr(ctx, I32)), a, 0);
+ T->tls_addr_of(T, REG_op(pb, T_ptr(ctx, I32)), b, 0);
+
+ Reg ra = T->alloc_reg(T, RC_INT, I32);
+ Reg rb = T->alloc_reg(T, RC_INT, I32);
+ T->load(T, REG_op(ra, I32), IND_op(pa, 0, I32), ma);
+ T->load(T, REG_op(rb, I32), IND_op(pb, 0, I32), ma);
+
+ Reg sum = T->alloc_reg(T, RC_INT, I32);
+ T->binop(T, BO_IADD, REG_op(sum, I32), REG_op(ra, I32), REG_op(rb, I32));
+ cgtest_ret_reg(tf, sum, I32);
+ cgtest_end(tf);
+}
+
+/* n07_tls_bss_zero_init — _Thread_local int x; (no initializer → .tbss);
+ * return x; → 0. The TLS image must zero-fill .tbss in the per-thread
+ * area; the harness's start.c is responsible for that on path E. */
+void build_n07_tls_bss_zero_init(CgTestCtx* ctx)
+{
+ const Type* I32 = T_i32(ctx);
+ ObjSymId x = tls_define_bss(ctx, "n07_x", 4, 4);
+
+ CgTestFn* tf = cgtest_begin_main(ctx, I32);
+ CGTarget* T = ctx->target;
+
+ Reg p = T->alloc_reg(T, RC_INT, T_ptr(ctx, I32));
+ T->tls_addr_of(T, REG_op(p, T_ptr(ctx, I32)), x, 0);
+ Reg r = T->alloc_reg(T, RC_INT, I32);
+ MemAccess ma = { .type = I32, .size = 4, .align = 4,
+ .alias.kind = ALIAS_GLOBAL };
+ T->load(T, REG_op(r, I32), IND_op(p, 0, I32), ma);
+ cgtest_ret_reg(tf, r, I32);
+ cgtest_end(tf);
+}
+
+/* n08_tls_addend_offset — _Thread_local int a[8] = {0,..,0,42};
+ * return a[7]; — exercises the addend on tls_addr_of (or an indirect
+ * +offset load). 32 bytes, 4-byte align. Offset of a[7] = 28. */
+void build_n08_tls_addend_offset(CgTestCtx* ctx)
+{
+ const Type* I32 = T_i32(ctx);
+ static const u8 INIT[32] = {
+ 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
+ 0,0,0,0, 0,0,0,0, 0,0,0,0, 42,0,0,0,
+ };
+ ObjSymId arr = tls_define_init(ctx, "n08_arr", INIT, 32, 4);
+
+ CgTestFn* tf = cgtest_begin_main(ctx, I32);
+ CGTarget* T = ctx->target;
+
+ /* Address of arr (no addend); read from base+28. The backend may
+ * fold the addend into the TLSLE relocation sequence. */
+ Reg p = T->alloc_reg(T, RC_INT, T_ptr(ctx, I32));
+ T->tls_addr_of(T, REG_op(p, T_ptr(ctx, I32)), arr, 0);
+
+ Reg r = T->alloc_reg(T, RC_INT, I32);
+ MemAccess ma = { .type = I32, .size = 4, .align = 4,
+ .alias.kind = ALIAS_GLOBAL };
+ T->load(T, REG_op(r, I32), IND_op(p, 28, I32), ma);
+ cgtest_ret_reg(tf, r, I32);
+ cgtest_end(tf);
+}
diff --git a/test/cg/harness/cases_o.c b/test/cg/harness/cases_o.c
@@ -0,0 +1,394 @@
+/* Group O — sections and globals (non-TLS).
+ * See CORPUS.md for the case list and expected values.
+ *
+ * Drives addr_of on OPK_GLOBAL operands plus direct GLOBAL load/store
+ * (the load/store methods accept LOCAL|GLOBAL|INDIRECT addr operands).
+ * Also exercises the SecKind / SymKind / SymBind matrix on ObjBuilder:
+ * SEC_DATA, SEC_BSS, SEC_RODATA × SK_OBJ × SB_GLOBAL/SB_LOCAL, plus a
+ * named non-default text section for a function. The aggregate-global
+ * cases reuse cases_shared's Pt to keep one TagId interned across
+ * groups. */
+
+#include "cg_test.h"
+#include "cases_shared.h"
+
+/* ============================================================
+ * Group O: sections and globals
+ * ============================================================ */
+
+/* Helper: define a `.data` symbol initialized to `bytes`. */
+static ObjSymId data_define(CgTestCtx* ctx, const char* name,
+ const u8* bytes, u32 size, u32 align,
+ SymBind bind)
+{
+ Sym sec_name = pool_intern_cstr(ctx->pool, ".data");
+ ObjSecId sec = obj_section(ctx->ob, sec_name, SEC_DATA,
+ SF_ALLOC | SF_WRITE, align);
+ obj_section_set_align(ctx->ob, sec, align);
+ u32 ofs = obj_pos(ctx->ob, sec);
+ while (ofs & (align - 1)) {
+ u8 z = 0; obj_write(ctx->ob, sec, &z, 1); ofs++;
+ }
+ obj_write(ctx->ob, sec, bytes, size);
+ Sym sname = pool_intern_cstr(ctx->pool, name);
+ return obj_symbol(ctx->ob, sname, bind, SK_OBJ, sec, ofs, size);
+}
+
+/* Helper: define a zero-initialized `.bss` symbol. */
+static ObjSymId bss_define(CgTestCtx* ctx, const char* name,
+ u32 size, u32 align, SymBind bind)
+{
+ Sym sec_name = pool_intern_cstr(ctx->pool, ".bss");
+ ObjSecId sec = obj_section_ex(ctx->ob, sec_name, SEC_BSS, SSEM_NOBITS,
+ SF_ALLOC | SF_WRITE, align, 0, 0, 0);
+ const Section* s = obj_section_get(ctx->ob, sec);
+ u32 ofs = s->bss_size;
+ while (ofs & (align - 1)) ofs++;
+ obj_reserve_bss(ctx->ob, sec, (ofs - s->bss_size) + size, align);
+ Sym sname = pool_intern_cstr(ctx->pool, name);
+ return obj_symbol(ctx->ob, sname, bind, SK_OBJ, sec, ofs, size);
+}
+
+/* Helper: define a `.rodata` symbol initialized to `bytes`. */
+static ObjSymId rodata_define(CgTestCtx* ctx, const char* name,
+ const u8* bytes, u32 size, u32 align)
+{
+ Sym sec_name = pool_intern_cstr(ctx->pool, ".rodata");
+ ObjSecId sec = obj_section(ctx->ob, sec_name, SEC_RODATA,
+ SF_ALLOC, align);
+ u32 ofs = obj_pos(ctx->ob, sec);
+ while (ofs & (align - 1)) {
+ u8 z = 0; obj_write(ctx->ob, sec, &z, 1); ofs++;
+ }
+ obj_write(ctx->ob, sec, bytes, size);
+ Sym sname = pool_intern_cstr(ctx->pool, name);
+ return obj_symbol(ctx->ob, sname, SB_LOCAL, SK_OBJ, sec, ofs, size);
+}
+
+/* o01_global_load_data — int g = 42; return g; — direct GLOBAL load. */
+void build_o01_global_load_data(CgTestCtx* ctx)
+{
+ const Type* I32 = T_i32(ctx);
+ static const u8 INIT[4] = { 42, 0, 0, 0 };
+ ObjSymId g = data_define(ctx, "o01_g", INIT, 4, 4, SB_GLOBAL);
+
+ CgTestFn* tf = cgtest_begin_main(ctx, I32);
+ CGTarget* T = ctx->target;
+ Reg r = T->alloc_reg(T, RC_INT, I32);
+ MemAccess ma = { .type = I32, .size = 4, .align = 4,
+ .alias.kind = ALIAS_GLOBAL };
+ /* load directly from a GLOBAL operand — the backend lowers the
+ * page-relative addressing internally. */
+ Operand addr = GLOBAL_op(g, 0);
+ addr.type = I32;
+ T->load(T, REG_op(r, I32), addr, ma);
+ cgtest_ret_reg(tf, r, I32);
+ cgtest_end(tf);
+}
+
+/* o02_global_store_data — int g = 0; g = 42; return g; — store via
+ * GLOBAL operand, then read back. */
+void build_o02_global_store_data(CgTestCtx* ctx)
+{
+ const Type* I32 = T_i32(ctx);
+ static const u8 INIT[4] = { 0, 0, 0, 0 };
+ ObjSymId g = data_define(ctx, "o02_g", INIT, 4, 4, SB_GLOBAL);
+
+ CgTestFn* tf = cgtest_begin_main(ctx, I32);
+ CGTarget* T = ctx->target;
+ MemAccess ma = { .type = I32, .size = 4, .align = 4,
+ .alias.kind = ALIAS_GLOBAL };
+ Operand addr = GLOBAL_op(g, 0);
+ addr.type = I32;
+ T->store(T, addr, IMM_op(42, I32), ma);
+
+ Reg r = T->alloc_reg(T, RC_INT, I32);
+ T->load(T, REG_op(r, I32), addr, ma);
+ cgtest_ret_reg(tf, r, I32);
+ cgtest_end(tf);
+}
+
+/* o03_global_bss_zero — int g; return g; — uninitialized .bss reads
+ * back as zero. The exit code is 0. */
+void build_o03_global_bss_zero(CgTestCtx* ctx)
+{
+ const Type* I32 = T_i32(ctx);
+ ObjSymId g = bss_define(ctx, "o03_g", 4, 4, SB_GLOBAL);
+
+ CgTestFn* tf = cgtest_begin_main(ctx, I32);
+ CGTarget* T = ctx->target;
+ Reg r = T->alloc_reg(T, RC_INT, I32);
+ MemAccess ma = { .type = I32, .size = 4, .align = 4,
+ .alias.kind = ALIAS_GLOBAL };
+ Operand addr = GLOBAL_op(g, 0);
+ addr.type = I32;
+ T->load(T, REG_op(r, I32), addr, ma);
+ cgtest_ret_reg(tf, r, I32);
+ cgtest_end(tf);
+}
+
+/* o04_global_addr_taken — int g = 17; int *p = &g; *p += 1; return *p;
+ * Mirrors b05 over a global storage class. */
+void build_o04_global_addr_taken(CgTestCtx* ctx)
+{
+ const Type* I32 = T_i32(ctx);
+ static const u8 INIT[4] = { 17, 0, 0, 0 };
+ ObjSymId g = data_define(ctx, "o04_g", INIT, 4, 4, SB_GLOBAL);
+
+ CgTestFn* tf = cgtest_begin_main(ctx, I32);
+ CGTarget* T = ctx->target;
+
+ Reg p = T->alloc_reg(T, RC_INT, T_ptr(ctx, I32));
+ T->addr_of(T, REG_op(p, T_ptr(ctx, I32)), GLOBAL_op(g, 0));
+
+ MemAccess ma = { .type = I32, .size = 4, .align = 4,
+ .alias.kind = ALIAS_GLOBAL };
+ 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);
+
+ 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);
+}
+
+/* o05_global_i64 — long long g = 0x1_0000_002A; return (int)g; — 8-byte
+ * global; exercises wider .data alignment + LDR Xt and downcast. */
+void build_o05_global_i64(CgTestCtx* ctx)
+{
+ const Type* I32 = T_i32(ctx);
+ const Type* I64 = T_i64(ctx);
+ static const u8 INIT[8] = { 0x2A, 0, 0, 0, 0x01, 0, 0, 0 };
+ ObjSymId g = data_define(ctx, "o05_g", INIT, 8, 8, SB_GLOBAL);
+
+ CgTestFn* tf = cgtest_begin_main(ctx, I32);
+ CGTarget* T = ctx->target;
+ Reg r64 = T->alloc_reg(T, RC_INT, I64);
+ MemAccess ma = { .type = I64, .size = 8, .align = 8,
+ .alias.kind = ALIAS_GLOBAL };
+ Operand addr = GLOBAL_op(g, 0);
+ addr.type = I64;
+ T->load(T, REG_op(r64, I64), addr, ma);
+
+ Reg r32 = T->alloc_reg(T, RC_INT, I32);
+ T->convert(T, CV_TRUNC, REG_op(r32, I32), REG_op(r64, I64));
+ cgtest_ret_reg(tf, r32, I32);
+ cgtest_end(tf);
+}
+
+/* o06_rodata_load — static const int rd[4] = {1, 2, 42, 4}; return rd[2];
+ * SEC_RODATA write fails at runtime if the linker emits the section
+ * unwritably (which is the point). */
+void build_o06_rodata_load(CgTestCtx* ctx)
+{
+ const Type* I32 = T_i32(ctx);
+ static const u8 INIT[16] = {
+ 1, 0, 0, 0, 2, 0, 0, 0, 42, 0, 0, 0, 4, 0, 0, 0,
+ };
+ ObjSymId rd = rodata_define(ctx, "o06_rd", INIT, 16, 4);
+
+ CgTestFn* tf = cgtest_begin_main(ctx, I32);
+ CGTarget* T = ctx->target;
+
+ Reg p = T->alloc_reg(T, RC_INT, T_ptr(ctx, I32));
+ T->addr_of(T, REG_op(p, T_ptr(ctx, I32)), GLOBAL_op(rd, 0));
+
+ Reg r = T->alloc_reg(T, RC_INT, I32);
+ MemAccess ma = { .type = I32, .size = 4, .align = 4,
+ .alias.kind = ALIAS_GLOBAL };
+ T->load(T, REG_op(r, I32), IND_op(p, 8, I32), ma);
+ cgtest_ret_reg(tf, r, I32);
+ cgtest_end(tf);
+}
+
+/* o07_global_struct_field — struct Pt g = {10, 32}; return g.a + g.b; */
+void build_o07_global_struct_field(CgTestCtx* ctx)
+{
+ const Type* I32 = T_i32(ctx);
+ const Type* PT = cases_pt_type(ctx);
+ (void)PT;
+ static const u8 INIT[8] = { 10, 0, 0, 0, 32, 0, 0, 0 };
+ ObjSymId g = data_define(ctx, "o07_g", INIT, 8, 4, SB_GLOBAL);
+
+ CgTestFn* tf = cgtest_begin_main(ctx, I32);
+ CGTarget* T = ctx->target;
+
+ Reg p = T->alloc_reg(T, RC_INT, T_ptr(ctx, I32));
+ T->addr_of(T, REG_op(p, T_ptr(ctx, I32)), GLOBAL_op(g, 0));
+
+ MemAccess ma = { .type = I32, .size = 4, .align = 4,
+ .alias.kind = ALIAS_GLOBAL };
+ Reg a = T->alloc_reg(T, RC_INT, I32);
+ Reg b = T->alloc_reg(T, RC_INT, I32);
+ T->load(T, REG_op(a, I32), IND_op(p, 0, I32), ma);
+ T->load(T, REG_op(b, I32), IND_op(p, 4, I32), ma);
+ Reg s = T->alloc_reg(T, RC_INT, I32);
+ T->binop(T, BO_IADD, REG_op(s, I32), REG_op(a, I32), REG_op(b, I32));
+ cgtest_ret_reg(tf, s, I32);
+ cgtest_end(tf);
+}
+
+/* o08_global_array_runtime_idx — int g[5] = {1,2,3,4,5}; int i=2; return g[i];
+ * Index is loaded from a local at runtime; the address is &g + i*4. */
+void build_o08_global_array_runtime_idx(CgTestCtx* ctx)
+{
+ const Type* I32 = T_i32(ctx);
+ static const u8 INIT[20] = {
+ 1,0,0,0, 2,0,0,0, 3,0,0,0, 4,0,0,0, 5,0,0,0,
+ };
+ ObjSymId g = data_define(ctx, "o08_g", INIT, 20, 4, SB_GLOBAL);
+
+ CgTestFn* tf = cgtest_begin_main(ctx, I32);
+ CGTarget* T = ctx->target;
+
+ /* int i = 2; — keep i in a local so the index is dynamic. */
+ FrameSlot islot = cgtest_local(tf, I32, FSF_NONE);
+ cgtest_store_local(tf, islot, IMM_op(2, I32), I32);
+
+ Reg base = T->alloc_reg(T, RC_INT, T_ptr(ctx, I32));
+ T->addr_of(T, REG_op(base, T_ptr(ctx, I32)), GLOBAL_op(g, 0));
+
+ Reg ireg = T->alloc_reg(T, RC_INT, I32);
+ cgtest_load_local(tf, REG_op(ireg, I32), islot, I32);
+ /* offs = i * 4 = i << 2 */
+ Reg offs = T->alloc_reg(T, RC_INT, T_i64(ctx));
+ T->convert(T, CV_SEXT, REG_op(offs, T_i64(ctx)), REG_op(ireg, I32));
+ T->binop(T, BO_SHL, REG_op(offs, T_i64(ctx)),
+ REG_op(offs, T_i64(ctx)), IMM_op(2, T_i64(ctx)));
+
+ /* addr = base + offs */
+ Reg addr = T->alloc_reg(T, RC_INT, T_ptr(ctx, I32));
+ T->binop(T, BO_IADD, REG_op(addr, T_ptr(ctx, I32)),
+ REG_op(base, T_ptr(ctx, I32)), REG_op(offs, T_i64(ctx)));
+
+ Reg r = T->alloc_reg(T, RC_INT, I32);
+ MemAccess ma = { .type = I32, .size = 4, .align = 4,
+ .alias.kind = ALIAS_GLOBAL };
+ T->load(T, REG_op(r, I32), IND_op(addr, 0, I32), ma);
+ cgtest_ret_reg(tf, r, I32);
+ cgtest_end(tf);
+}
+
+/* o09_static_local_linkage — static int g = 42; return g; — SB_LOCAL
+ * (file-static) symbol. The relocation must resolve to the local
+ * definition without going through a GOT. */
+void build_o09_static_local_linkage(CgTestCtx* ctx)
+{
+ const Type* I32 = T_i32(ctx);
+ static const u8 INIT[4] = { 42, 0, 0, 0 };
+ ObjSymId g = data_define(ctx, "o09_g", INIT, 4, 4, SB_LOCAL);
+
+ CgTestFn* tf = cgtest_begin_main(ctx, I32);
+ CGTarget* T = ctx->target;
+ Reg r = T->alloc_reg(T, RC_INT, I32);
+ MemAccess ma = { .type = I32, .size = 4, .align = 4,
+ .alias.kind = ALIAS_GLOBAL };
+ Operand addr = GLOBAL_op(g, 0);
+ addr.type = I32;
+ T->load(T, REG_op(r, I32), addr, ma);
+ cgtest_ret_reg(tf, r, I32);
+ cgtest_end(tf);
+}
+
+/* o10_global_addend — int g[8] = {0,...,0,42}; return *(g+7); — addend
+ * encoded into the OPK_GLOBAL operand rather than a runtime add. The
+ * backend may fold the addend into ADD_LO12_NC (or equivalent). */
+void build_o10_global_addend(CgTestCtx* ctx)
+{
+ const Type* I32 = T_i32(ctx);
+ static const u8 INIT[32] = {
+ 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
+ 0,0,0,0, 0,0,0,0, 0,0,0,0, 42,0,0,0,
+ };
+ ObjSymId g = data_define(ctx, "o10_g", INIT, 32, 4, SB_GLOBAL);
+
+ CgTestFn* tf = cgtest_begin_main(ctx, I32);
+ CGTarget* T = ctx->target;
+
+ /* addr_of(GLOBAL{g, 28}); load *addr. */
+ Reg p = T->alloc_reg(T, RC_INT, T_ptr(ctx, I32));
+ T->addr_of(T, REG_op(p, T_ptr(ctx, I32)), GLOBAL_op(g, 28));
+
+ Reg r = T->alloc_reg(T, RC_INT, I32);
+ MemAccess ma = { .type = I32, .size = 4, .align = 4,
+ .alias.kind = ALIAS_GLOBAL };
+ T->load(T, REG_op(r, I32), IND_op(p, 0, I32), ma);
+ cgtest_ret_reg(tf, r, I32);
+ cgtest_end(tf);
+}
+
+/* o11_text_section_named — function placed in `.text.helper`, called
+ * from test_main in the default `.text`. Models -ffunction-sections /
+ * __attribute__((section("..."))) on a function. */
+void build_o11_text_section_named(CgTestCtx* ctx)
+{
+ const Type* I32 = T_i32(ctx);
+ const Type* params[] = { I32 };
+
+ /* Create a separate text section and aim the next func_begin at it. */
+ Sym sec_name = pool_intern_cstr(ctx->pool, ".text.o11_helper");
+ ObjSecId helper_sec = obj_section(ctx->ob, sec_name, SEC_TEXT,
+ SF_ALLOC | SF_EXEC, 4);
+ ObjSecId saved = ctx->text_sec;
+ ctx->text_sec = helper_sec;
+ ctx->mc->set_section(ctx->mc, helper_sec);
+
+ /* Helper: int echo(int x) { return x; } */
+ CgTestFn* h = cgtest_begin_func(ctx, "o11_helper", I32, params, 1);
+ Reg hr = ctx->target->alloc_reg(ctx->target, RC_INT, I32);
+ cgtest_load_local(h, REG_op(hr, I32), cgtest_param_slot(h, 0), I32);
+ cgtest_ret_reg(h, hr, I32);
+ cgtest_end(h);
+ ObjSymId helper_sym = h->sym;
+
+ /* Restore default text section for test_main. */
+ ctx->text_sec = saved;
+ ctx->mc->set_section(ctx->mc, saved);
+
+ 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 = 42 } };
+ cgtest_call(tf, helper_sym, I32, params, args, 1, REG_op(dst, I32));
+ cgtest_ret_reg(tf, dst, I32);
+ cgtest_end(tf);
+}
+
+/* o12_global_across_call — int g = 42; helper modifies nothing relevant;
+ * return g; — verifies global address materialization is not corrupted
+ * by an intervening call (caller-saved register policy). */
+void build_o12_global_across_call(CgTestCtx* ctx)
+{
+ const Type* I32 = T_i32(ctx);
+ const Type* params[] = { I32 };
+ static const u8 INIT[4] = { 42, 0, 0, 0 };
+ ObjSymId g = data_define(ctx, "o12_g", INIT, 4, 4, SB_GLOBAL);
+
+ /* Simple int echo helper, isolated to this case. */
+ CgTestFn* h = cgtest_begin_func(ctx, "o12_echo", I32, params, 1);
+ Reg hr = ctx->target->alloc_reg(ctx->target, RC_INT, I32);
+ cgtest_load_local(h, REG_op(hr, I32), cgtest_param_slot(h, 0), I32);
+ cgtest_ret_reg(h, hr, I32);
+ cgtest_end(h);
+ ObjSymId echo = h->sym;
+
+ CgTestFn* tf = cgtest_begin_main(ctx, I32);
+ CGTarget* T = ctx->target;
+ MemAccess ma = { .type = I32, .size = 4, .align = 4,
+ .alias.kind = ALIAS_GLOBAL };
+
+ /* Materialize &g, do an intervening call that may clobber p, then
+ * load *p. The backend must either preserve p or remate the addr. */
+ Reg p = T->alloc_reg(T, RC_INT, T_ptr(ctx, I32));
+ T->addr_of(T, REG_op(p, T_ptr(ctx, I32)), GLOBAL_op(g, 0));
+
+ 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 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);
+}
diff --git a/test/cg/harness/cases_q.c b/test/cg/harness/cases_q.c
@@ -0,0 +1,476 @@
+/* Group Q — multi-function (extends Group B's two-function pattern).
+ * See CORPUS.md for the case list and expected values.
+ *
+ * Group B already validates that two func_begin/func_end pairs work in
+ * one TU. Group Q stresses what falls out as the function count grows:
+ * - many small helpers (8+ functions per TU)
+ * - mixed SB_GLOBAL / SB_LOCAL (file-static) linkage
+ * - distinct param/return signatures sharing a CGTarget
+ * - per-function text sections (-ffunction-sections analogue)
+ * - calls between functions placed in different text sections
+ * - forward-declared helpers defined later in the TU
+ *
+ * Each case constructs a flat call graph rooted at test_main; the oracle
+ * is the final exit code. */
+
+#include "cg_test.h"
+
+/* ============================================================
+ * Group Q: multi-function
+ * ============================================================ */
+
+/* Helper: int return, no params, body returns IMM `v`. */
+static ObjSymId qfn_const(CgTestCtx* ctx, const char* name, i64 v, SymBind bind)
+{
+ const Type* I32 = T_i32(ctx);
+ Sym sname = pool_intern_cstr(ctx->pool, name);
+ ObjSymId sym = obj_symbol(ctx->ob, sname, bind, SK_FUNC,
+ OBJ_SEC_NONE, 0, 0);
+ CgTestFn* tf = cgtest_begin_func_at(ctx, sym, I32, NULL, 0);
+ cgtest_ret_imm(tf, v, I32);
+ cgtest_end(tf);
+ return sym;
+}
+
+/* Helper: int echo(int x) — distinct symbol per case. */
+static ObjSymId qfn_echo(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;
+}
+
+/* q01_three_helpers — three int(void) helpers a/b/c each returning
+ * a partial sum; main returns a()+b()+c() = 10+15+17 = 42. */
+void build_q01_three_helpers(CgTestCtx* ctx)
+{
+ const Type* I32 = T_i32(ctx);
+ ObjSymId a = qfn_const(ctx, "q01_a", 10, SB_GLOBAL);
+ ObjSymId b = qfn_const(ctx, "q01_b", 15, SB_GLOBAL);
+ ObjSymId c = qfn_const(ctx, "q01_c", 17, SB_GLOBAL);
+
+ CgTestFn* tf = cgtest_begin_main(ctx, I32);
+ CGTarget* T = ctx->target;
+
+ Reg ra = T->alloc_reg(T, RC_INT, I32);
+ Reg rb = T->alloc_reg(T, RC_INT, I32);
+ Reg rc = T->alloc_reg(T, RC_INT, I32);
+ cgtest_call(tf, a, I32, NULL, NULL, 0, REG_op(ra, I32));
+ cgtest_call(tf, b, I32, NULL, NULL, 0, REG_op(rb, I32));
+ cgtest_call(tf, c, I32, NULL, NULL, 0, REG_op(rc, I32));
+
+ Reg s = T->alloc_reg(T, RC_INT, I32);
+ T->binop(T, BO_IADD, REG_op(s, I32), REG_op(ra, I32), REG_op(rb, I32));
+ T->binop(T, BO_IADD, REG_op(s, I32), REG_op(s, I32), REG_op(rc, I32));
+ cgtest_ret_reg(tf, s, I32);
+ cgtest_end(tf);
+}
+
+/* q02_static_internal_linkage — `static int helper(void) { return 42; }`
+ * SB_LOCAL symbol; the call lowers to a near branch resolved within
+ * this TU (no PLT/GOT). */
+void build_q02_static_internal_linkage(CgTestCtx* ctx)
+{
+ const Type* I32 = T_i32(ctx);
+ ObjSymId h = qfn_const(ctx, "q02_helper", 42, SB_LOCAL);
+
+ CgTestFn* tf = cgtest_begin_main(ctx, I32);
+ Reg dst = ctx->target->alloc_reg(ctx->target, RC_INT, I32);
+ cgtest_call(tf, h, I32, NULL, NULL, 0, REG_op(dst, I32));
+ cgtest_ret_reg(tf, dst, I32);
+ cgtest_end(tf);
+}
+
+/* q03_intra_tu_call_chain — a→b→c→d, where a/b/c are bodies that just
+ * tail-forward to the next, and d returns 42. Built without
+ * CG_CALL_TAIL — exercises a 4-deep linear call stack. */
+void build_q03_intra_tu_call_chain(CgTestCtx* ctx)
+{
+ const Type* I32 = T_i32(ctx);
+
+ /* d: returns 42. */
+ ObjSymId d = qfn_const(ctx, "q03_d", 42, SB_GLOBAL);
+ /* c: returns d(); */
+ ObjSymId c;
+ {
+ CgTestFn* tf = cgtest_begin_func(ctx, "q03_c", I32, NULL, 0);
+ Reg r = ctx->target->alloc_reg(ctx->target, RC_INT, I32);
+ cgtest_call(tf, d, I32, NULL, NULL, 0, REG_op(r, I32));
+ cgtest_ret_reg(tf, r, I32);
+ cgtest_end(tf);
+ c = tf->sym;
+ }
+ /* b: returns c(); */
+ ObjSymId b;
+ {
+ CgTestFn* tf = cgtest_begin_func(ctx, "q03_b", I32, NULL, 0);
+ Reg r = ctx->target->alloc_reg(ctx->target, RC_INT, I32);
+ cgtest_call(tf, c, I32, NULL, NULL, 0, REG_op(r, I32));
+ cgtest_ret_reg(tf, r, I32);
+ cgtest_end(tf);
+ b = tf->sym;
+ }
+ /* a: returns b(); */
+ ObjSymId a;
+ {
+ CgTestFn* tf = cgtest_begin_func(ctx, "q03_a", I32, NULL, 0);
+ Reg r = ctx->target->alloc_reg(ctx->target, RC_INT, I32);
+ cgtest_call(tf, b, I32, NULL, NULL, 0, REG_op(r, I32));
+ cgtest_ret_reg(tf, r, I32);
+ cgtest_end(tf);
+ a = tf->sym;
+ }
+
+ CgTestFn* tf = cgtest_begin_main(ctx, I32);
+ Reg dst = ctx->target->alloc_reg(ctx->target, RC_INT, I32);
+ cgtest_call(tf, a, I32, NULL, NULL, 0, REG_op(dst, I32));
+ cgtest_ret_reg(tf, dst, I32);
+ cgtest_end(tf);
+}
+
+/* q04_eight_helpers — eight int(int) helpers, each adding a constant.
+ * Composing them in order yields 0 + 1+2+3+4+5+6+7+8 = 36. Plus a 6
+ * baseline → 42. Stresses many func_begin/func_end pairs in one TU. */
+void build_q04_eight_helpers(CgTestCtx* ctx)
+{
+ const Type* I32 = T_i32(ctx);
+ const Type* params[] = { I32 };
+
+ ObjSymId helpers[8];
+ for (int i = 0; i < 8; ++i) {
+ char name[16];
+ name[0] = 'q'; name[1] = '0'; name[2] = '4'; name[3] = '_';
+ name[4] = 'h'; name[5] = (char)('1' + i); name[6] = 0;
+ Sym sn = pool_intern_cstr(ctx->pool, name);
+ ObjSymId sym = obj_symbol(ctx->ob, sn, SB_GLOBAL, SK_FUNC,
+ OBJ_SEC_NONE, 0, 0);
+ CgTestFn* tf = cgtest_begin_func_at(ctx, sym, I32, params, 1);
+ CGTarget* T = ctx->target;
+ Reg x = T->alloc_reg(T, RC_INT, I32);
+ cgtest_load_local(tf, REG_op(x, I32), cgtest_param_slot(tf, 0), I32);
+ Reg r = T->alloc_reg(T, RC_INT, I32);
+ T->binop(T, BO_IADD, REG_op(r, I32),
+ REG_op(x, I32), IMM_op(i + 1, I32));
+ cgtest_ret_reg(tf, r, I32);
+ cgtest_end(tf);
+ helpers[i] = sym;
+ }
+
+ CgTestFn* tf = cgtest_begin_main(ctx, I32);
+ CGTarget* T = ctx->target;
+ /* Start at 6, then chain h1..h8. 6 + (1+2+...+8) = 6 + 36 = 42. */
+ Reg cur = T->alloc_reg(T, RC_INT, I32);
+ T->load_imm(T, REG_op(cur, I32), 6);
+ for (int i = 0; i < 8; ++i) {
+ Reg next = T->alloc_reg(T, RC_INT, I32);
+ CgTestArg args[] = { { .kind = CGT_ARG_REG, .type = I32, .v.reg = cur } };
+ cgtest_call(tf, helpers[i], I32, params, args, 1, REG_op(next, I32));
+ cur = next;
+ }
+ cgtest_ret_reg(tf, cur, I32);
+ cgtest_end(tf);
+}
+
+/* q05_distinct_signatures — four helpers with distinct (ret, params)
+ * signatures, all called from main; sum truncated to 42.
+ * int h_int (int);
+ * long h_long(long, long);
+ * void h_void(int*);
+ * int h_zero(void);
+ * Sum: 10 + 20 + 5 + 7 = 42 (low 32 of long sum). */
+void build_q05_distinct_signatures(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);
+
+ /* h_int(x) = x + 5 */
+ const Type* p_int[] = { I32 };
+ ObjSymId h_int;
+ {
+ CgTestFn* tf = cgtest_begin_func(ctx, "q05_h_int", I32, p_int, 1);
+ CGTarget* T = ctx->target;
+ Reg x = T->alloc_reg(T, RC_INT, I32);
+ cgtest_load_local(tf, REG_op(x, I32), cgtest_param_slot(tf, 0), I32);
+ Reg r = T->alloc_reg(T, RC_INT, I32);
+ T->binop(T, BO_IADD, REG_op(r, I32), REG_op(x, I32), IMM_op(5, I32));
+ cgtest_ret_reg(tf, r, I32);
+ cgtest_end(tf);
+ h_int = tf->sym;
+ }
+ /* h_long(a, b) = a + b */
+ const Type* p_long[] = { I64, I64 };
+ ObjSymId h_long;
+ {
+ CgTestFn* tf = cgtest_begin_func(ctx, "q05_h_long", I64, p_long, 2);
+ CGTarget* T = ctx->target;
+ Reg a = T->alloc_reg(T, RC_INT, I64);
+ Reg b = T->alloc_reg(T, RC_INT, I64);
+ cgtest_load_local(tf, REG_op(a, I64), cgtest_param_slot(tf, 0), I64);
+ cgtest_load_local(tf, REG_op(b, I64), cgtest_param_slot(tf, 1), I64);
+ Reg s = T->alloc_reg(T, RC_INT, I64);
+ T->binop(T, BO_IADD, REG_op(s, I64), REG_op(a, I64), REG_op(b, I64));
+ cgtest_ret_reg(tf, s, I64);
+ cgtest_end(tf);
+ h_long = tf->sym;
+ }
+ /* h_void(p) { *p = 5; } */
+ const Type* p_void[] = { PI32 };
+ ObjSymId h_void;
+ {
+ CgTestFn* tf = cgtest_begin_func(ctx, "q05_h_void", VOID, p_void, 1);
+ CGTarget* T = ctx->target;
+ Reg p = T->alloc_reg(T, RC_INT, PI32);
+ cgtest_load_local(tf, REG_op(p, PI32), cgtest_param_slot(tf, 0), PI32);
+ MemAccess ma = { .type = I32, .size = 4, .align = 4,
+ .alias.kind = ALIAS_LOCAL };
+ T->store(T, IND_op(p, 0, I32), IMM_op(5, I32), ma);
+ cgtest_ret_void(tf);
+ cgtest_end(tf);
+ h_void = tf->sym;
+ }
+ /* h_zero(void) = 7 */
+ ObjSymId h_zero = qfn_const(ctx, "q05_h_zero", 7, SB_GLOBAL);
+
+ CgTestFn* tf = cgtest_begin_main(ctx, I32);
+ CGTarget* T = ctx->target;
+
+ /* h_int(5) = 10 */
+ Reg r_int = T->alloc_reg(T, RC_INT, I32);
+ CgTestArg a_int[] = { { .kind = CGT_ARG_IMM, .type = I32, .v.imm = 5 } };
+ cgtest_call(tf, h_int, I32, p_int, a_int, 1, REG_op(r_int, I32));
+
+ /* h_long(8, 12) = 20 */
+ Reg r_long = T->alloc_reg(T, RC_INT, I64);
+ CgTestArg a_long[] = {
+ { .kind = CGT_ARG_IMM, .type = I64, .v.imm = 8 },
+ { .kind = CGT_ARG_IMM, .type = I64, .v.imm = 12 },
+ };
+ cgtest_call(tf, h_long, I64, p_long, a_long, 2, REG_op(r_long, I64));
+
+ /* int x; h_void(&x); — x is set to 5. */
+ FrameSlot xslot = cgtest_local(tf, I32, FSF_ADDR_TAKEN);
+ cgtest_store_local(tf, xslot, IMM_op(0, I32), I32);
+ Reg px = T->alloc_reg(T, RC_INT, PI32);
+ T->addr_of(T, REG_op(px, PI32), LOCAL_op(xslot, I32));
+ CgTestArg a_void[] = { { .kind = CGT_ARG_REG, .type = PI32, .v.reg = px } };
+ cgtest_call(tf, h_void, VOID, p_void, a_void, 1, IMM_op(0, VOID));
+ Reg r_void = T->alloc_reg(T, RC_INT, I32);
+ cgtest_load_local(tf, REG_op(r_void, I32), xslot, I32);
+
+ /* h_zero() = 7 */
+ Reg r_zero = T->alloc_reg(T, RC_INT, I32);
+ cgtest_call(tf, h_zero, I32, NULL, NULL, 0, REG_op(r_zero, I32));
+
+ /* sum = r_int + (i32)r_long + r_void + r_zero. */
+ Reg r_long_lo = T->alloc_reg(T, RC_INT, I32);
+ T->convert(T, CV_TRUNC, REG_op(r_long_lo, I32), REG_op(r_long, I64));
+
+ Reg s = T->alloc_reg(T, RC_INT, I32);
+ T->binop(T, BO_IADD, REG_op(s, I32), REG_op(r_int, I32), REG_op(r_long_lo, I32));
+ T->binop(T, BO_IADD, REG_op(s, I32), REG_op(s, I32), REG_op(r_void, I32));
+ T->binop(T, BO_IADD, REG_op(s, I32), REG_op(s, I32), REG_op(r_zero, I32));
+ cgtest_ret_reg(tf, s, I32);
+ cgtest_end(tf);
+}
+
+/* q06_function_section_distinct — helper placed in `.text.q06_helper`,
+ * test_main in default `.text`. CGFuncDesc.text_section_id varies per
+ * function; the backend must honor it. */
+void build_q06_function_section_distinct(CgTestCtx* ctx)
+{
+ const Type* I32 = T_i32(ctx);
+
+ Sym sec_name = pool_intern_cstr(ctx->pool, ".text.q06_helper");
+ ObjSecId helper_sec = obj_section(ctx->ob, sec_name, SEC_TEXT,
+ SF_ALLOC | SF_EXEC, 4);
+ ObjSecId saved = ctx->text_sec;
+ ctx->text_sec = helper_sec;
+ ctx->mc->set_section(ctx->mc, helper_sec);
+
+ ObjSymId helper = qfn_const(ctx, "q06_helper", 42, SB_GLOBAL);
+
+ ctx->text_sec = saved;
+ ctx->mc->set_section(ctx->mc, saved);
+
+ CgTestFn* tf = cgtest_begin_main(ctx, I32);
+ Reg r = ctx->target->alloc_reg(ctx->target, RC_INT, I32);
+ cgtest_call(tf, helper, I32, NULL, NULL, 0, REG_op(r, I32));
+ cgtest_ret_reg(tf, r, I32);
+ cgtest_end(tf);
+}
+
+/* q07_cross_section_calls — two helpers, each in its own
+ * `.text.<name>`, calling each other plus test_main calling one.
+ * Caller and callee in distinct text sections must produce a CALL26
+ * relocation (or veneer) rather than a fixed PC-relative offset. */
+void build_q07_cross_section_calls(CgTestCtx* ctx)
+{
+ const Type* I32 = T_i32(ctx);
+
+ /* helper_a in .text.q07_a — returns helper_b() + 10. */
+ Sym sa = pool_intern_cstr(ctx->pool, ".text.q07_a");
+ Sym sb = pool_intern_cstr(ctx->pool, ".text.q07_b");
+ ObjSecId sec_a = obj_section(ctx->ob, sa, SEC_TEXT, SF_ALLOC | SF_EXEC, 4);
+ ObjSecId sec_b = obj_section(ctx->ob, sb, SEC_TEXT, SF_ALLOC | SF_EXEC, 4);
+
+ /* Forward-decl both syms so each body can reference the other. */
+ ObjSymId hb = cgtest_decl_func(ctx, "q07_b");
+
+ ObjSecId saved = ctx->text_sec;
+
+ /* helper_a body in sec_a. */
+ ctx->text_sec = sec_a;
+ ctx->mc->set_section(ctx->mc, sec_a);
+ ObjSymId ha;
+ {
+ CgTestFn* tf = cgtest_begin_func(ctx, "q07_a", I32, NULL, 0);
+ CGTarget* T = ctx->target;
+ Reg r_b = T->alloc_reg(T, RC_INT, I32);
+ cgtest_call(tf, hb, I32, NULL, NULL, 0, REG_op(r_b, I32));
+ Reg r = T->alloc_reg(T, RC_INT, I32);
+ T->binop(T, BO_IADD, REG_op(r, I32), REG_op(r_b, I32), IMM_op(10, I32));
+ cgtest_ret_reg(tf, r, I32);
+ cgtest_end(tf);
+ ha = tf->sym;
+ }
+
+ /* helper_b body in sec_b — returns 32. */
+ ctx->text_sec = sec_b;
+ ctx->mc->set_section(ctx->mc, sec_b);
+ {
+ CgTestFn* tf = cgtest_begin_func_at(ctx, hb, I32, NULL, 0);
+ cgtest_ret_imm(tf, 32, I32);
+ cgtest_end(tf);
+ }
+
+ /* test_main back in default `.text`, calls helper_a → 42. */
+ ctx->text_sec = saved;
+ ctx->mc->set_section(ctx->mc, saved);
+
+ CgTestFn* tf = cgtest_begin_main(ctx, I32);
+ Reg r = ctx->target->alloc_reg(ctx->target, RC_INT, I32);
+ cgtest_call(tf, ha, I32, NULL, NULL, 0, REG_op(r, I32));
+ cgtest_ret_reg(tf, r, I32);
+ cgtest_end(tf);
+}
+
+/* q08_forward_decl_define_late — declare helper at the start, define it
+ * after test_main. test_main's call site is emitted before the symbol
+ * has a section/value; obj_finalize is responsible for resolving the
+ * relocation once cgtest_begin_func_at fills in the symbol body. */
+void build_q08_forward_decl_define_late(CgTestCtx* ctx)
+{
+ const Type* I32 = T_i32(ctx);
+ ObjSymId h = cgtest_decl_func(ctx, "q08_late");
+
+ /* Emit test_main first — it calls h before h has a body. */
+ CgTestFn* tf = cgtest_begin_main(ctx, I32);
+ Reg dst = ctx->target->alloc_reg(ctx->target, RC_INT, I32);
+ cgtest_call(tf, h, I32, NULL, NULL, 0, REG_op(dst, I32));
+ cgtest_ret_reg(tf, dst, I32);
+ cgtest_end(tf);
+
+ /* Now define h. */
+ {
+ CgTestFn* tf2 = cgtest_begin_func_at(ctx, h, I32, NULL, 0);
+ cgtest_ret_imm(tf2, 42, I32);
+ cgtest_end(tf2);
+ }
+}
+
+/* q09_helper_calls_helper — a → b, both globals; main calls a. */
+void build_q09_helper_calls_helper(CgTestCtx* ctx)
+{
+ const Type* I32 = T_i32(ctx);
+ ObjSymId b = qfn_const(ctx, "q09_b", 42, SB_GLOBAL);
+
+ /* a returns b(). */
+ ObjSymId a;
+ {
+ CgTestFn* tf = cgtest_begin_func(ctx, "q09_a", I32, NULL, 0);
+ Reg r = ctx->target->alloc_reg(ctx->target, RC_INT, I32);
+ cgtest_call(tf, b, I32, NULL, NULL, 0, REG_op(r, I32));
+ cgtest_ret_reg(tf, r, I32);
+ cgtest_end(tf);
+ a = tf->sym;
+ }
+
+ CgTestFn* tf = cgtest_begin_main(ctx, I32);
+ Reg dst = ctx->target->alloc_reg(ctx->target, RC_INT, I32);
+ cgtest_call(tf, a, I32, NULL, NULL, 0, REG_op(dst, I32));
+ cgtest_ret_reg(tf, dst, I32);
+ cgtest_end(tf);
+}
+
+/* q10_global_and_static_mix — three helpers in one TU: SB_GLOBAL +
+ * SB_LOCAL + SB_LOCAL. All three are called; sum = 12+15+15 = 42. */
+void build_q10_global_and_static_mix(CgTestCtx* ctx)
+{
+ const Type* I32 = T_i32(ctx);
+ ObjSymId g = qfn_const(ctx, "q10_global", 12, SB_GLOBAL);
+ ObjSymId s1 = qfn_const(ctx, "q10_static1", 15, SB_LOCAL);
+ ObjSymId s2 = qfn_const(ctx, "q10_static2", 15, SB_LOCAL);
+
+ CgTestFn* tf = cgtest_begin_main(ctx, I32);
+ CGTarget* T = ctx->target;
+
+ Reg rg = T->alloc_reg(T, RC_INT, I32);
+ Reg rs1 = T->alloc_reg(T, RC_INT, I32);
+ Reg rs2 = T->alloc_reg(T, RC_INT, I32);
+ cgtest_call(tf, g, I32, NULL, NULL, 0, REG_op(rg, I32));
+ cgtest_call(tf, s1, I32, NULL, NULL, 0, REG_op(rs1, I32));
+ cgtest_call(tf, s2, I32, NULL, NULL, 0, REG_op(rs2, I32));
+
+ Reg s = T->alloc_reg(T, RC_INT, I32);
+ T->binop(T, BO_IADD, REG_op(s, I32), REG_op(rg, I32), REG_op(rs1, I32));
+ T->binop(T, BO_IADD, REG_op(s, I32), REG_op(s, I32), REG_op(rs2, I32));
+ cgtest_ret_reg(tf, s, I32);
+ cgtest_end(tf);
+}
+
+/* q11_addr_of_helper_through_global — store helper's address into a
+ * data global, load it, indirect-call. Tests function-symbol relocation
+ * into a non-text section (data ABS64) and indirect call via REG. */
+void build_q11_addr_of_helper_through_global(CgTestCtx* ctx)
+{
+ const Type* I32 = T_i32(ctx);
+ /* The helper. */
+ ObjSymId h = qfn_const(ctx, "q11_helper", 42, SB_GLOBAL);
+
+ /* Allocate a .data slot of pointer size with an ABS64 reloc to h. */
+ Sym dn = pool_intern_cstr(ctx->pool, ".data");
+ ObjSecId data_sec = obj_section(ctx->ob, dn, SEC_DATA,
+ SF_ALLOC | SF_WRITE, 8);
+ static const u8 ZERO8[8] = { 0 };
+ u32 dofs = obj_pos(ctx->ob, data_sec);
+ obj_write(ctx->ob, data_sec, ZERO8, 8);
+ obj_reloc(ctx->ob, data_sec, dofs, R_ABS64, h, 0);
+ Sym fn = pool_intern_cstr(ctx->pool, "q11_fp");
+ ObjSymId fp_sym = obj_symbol(ctx->ob, fn, SB_GLOBAL, SK_OBJ,
+ data_sec, dofs, 8);
+
+ CgTestFn* tf = cgtest_begin_main(ctx, I32);
+ CGTarget* T = ctx->target;
+
+ /* Load the function pointer from the global slot. */
+ const Type* fn_ty = type_func(ctx->pool, I32, NULL, 0, 0);
+ const Type* fnp_ty = T_ptr(ctx, fn_ty);
+ Reg fp = T->alloc_reg(T, RC_INT, fnp_ty);
+ MemAccess ma = { .type = fnp_ty, .size = 8, .align = 8,
+ .alias.kind = ALIAS_GLOBAL };
+ Operand addr = GLOBAL_op(fp_sym, 0);
+ addr.type = fnp_ty;
+ T->load(T, REG_op(fp, fnp_ty), addr, ma);
+
+ Reg dst = T->alloc_reg(T, RC_INT, I32);
+ cgtest_call_indirect(tf, fp, I32, NULL, NULL, 0, REG_op(dst, I32));
+ cgtest_ret_reg(tf, dst, I32);
+ cgtest_end(tf);
+}
diff --git a/test/cg/run.sh b/test/cg/run.sh
@@ -145,6 +145,9 @@ if $CC $CFREE_CFLAGS \
"$TEST_DIR/harness/cases_j.c" \
"$TEST_DIR/harness/cases_k.c" \
"$TEST_DIR/harness/cases_l.c" \
+ "$TEST_DIR/harness/cases_n.c" \
+ "$TEST_DIR/harness/cases_o.c" \
+ "$TEST_DIR/harness/cases_q.c" \
"$LIB_AR" -o "$CG_RUNNER" 2>"$BUILD_DIR/cg-runner.err"; then
printf ' %s cg-runner\n' "$(color_grn built)"
else