commit d8f401b016a594425174caeacea85b888c39a1fa
parent c5025473abe14579fbfda9365b3b7a6d212f1fb9
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 20 May 2026 11:34:23 -0700
c_target: land Phase 4 of the C-source backend
Implements data emission (OPK_GLOBAL, with constructor-time reloc fixups),
atomics via __atomic_* builtins, inline asm re-serialization as GCC extended
asm, bitfield mask/shift on opaque storage, load_const via memcpy from a
static byte array, _Thread_local tls_addr_of, and identifier sanitization
for cfree's assembler-style local symbol names.
Toy under path C goes from 87/40 (pass/skip) to 124/3. The 3 remaining
SKIPs are pcrel and symdiff data relocs, which have no faithful in-language
C-source equivalent and are documented as deferred.
Diffstat:
4 files changed, 925 insertions(+), 99 deletions(-)
diff --git a/doc/CBACKEND.md b/doc/CBACKEND.md
@@ -517,14 +517,64 @@ Other choices that landed in Phase 3:
Acceptance met: 87 toy cases under path `C` pass; the remaining 40 SKIPs
all blame Phase 4 surface (data emission, atomics, inline asm).
-### Phase 4 — atomics, asm, TLS, exotic features
-
-- Atomic load/store/rmw/cas/fence.
-- `asm_block` re-serialization.
-- TLS, weak, visibility, `_Alignas`.
-- `bitfield_load`/`_store`.
-- `data_*` definition emission including const_data.
-- `tls_addr_of`, `alloca_`.
+### Phase 4 — atomics, asm, TLS, exotic features ✅ landed
+
+Implemented: `atomic_load`/`atomic_store`/`atomic_rmw`/`atomic_cas`/`fence`
+via `__atomic_*` builtins; `asm_block` re-serialized as GCC extended asm;
+data-symbol emission with `static`/`__attribute__((weak))`/visibility/
+`_Alignas`; `bitfield_load`/`_store` via mask+shift on opaque storage units;
+`load_const` via static-const byte array + `__builtin_memcpy`;
+`tls_addr_of` + `_Thread_local` extern TLS data; OPK_GLOBAL operand support
+in all use sites; identifier sanitization (cfree's `.L...` assembler-style
+local syms become C-safe).
+
+Other choices that landed in Phase 4:
+
+- **Data is opaque bytes.** Every data symbol emits as `_Alignas(A) uint8_t
+ name[N] = { 0x.., ... };` regardless of source type. Field/element access
+ goes through the existing `(*(T*)((char*)addr + ofs))` path, sidestepping
+ C's static-initializer constraints.
+- **Relocations run at startup.** Cross-symbol references in data
+ initializers become `__attribute__((constructor)) static void
+ __cfree_init_<name>(void) { *(uintN_t*)(...) = (uintN_t)(uintptr_t)(...);
+ }` thunks. Same path covers same-TU and cross-TU targets uniformly.
+ Cost: rodata-with-relocs loses its `const` qualifier (the constructor
+ writes through). Reasonable trade for the simplification.
+- **pcrel + symdiff are skipped.** Both are link-time concepts with no
+ faithful in-language C-source equivalent (a constructor fixup can write
+ the absolute value but not preserve the "section-relative" semantics that
+ pcrel users actually want — they want shared `.rodata` pages, not dirty
+ per-process tables). v1 lift would emit format-gated top-level
+ `__asm__(".long sym - .\n")`; deferred.
+- **Atomics via `__atomic_*` builtins.** Memory order maps 1-1 to
+ `__ATOMIC_*`. `compare_exchange_n` needs a real lvalue for `expected`,
+ but CG reuses reg ids across types so `&prior_reg` may have the wrong C
+ type — we materialize a scratch local typed by `prior.type` and copy out.
+- **Atomic value-arg cast bridging.** Same reg-id-reuse hazard exists for
+ store/rmw value operands; `c_emit_operand_as(t, op, target_ty)` bridges
+ the cast through `(uintptr_t)` (or skips for float dst) so `-Wint-
+ conversion` doesn't trip.
+- **asm_block input filter.** cfree synthesizes a matching `"N"` input for
+ every ASM_INOUT output to model the read in IR; gcc rejects that as
+ redundant when the output is `+r`-tied, so we drop synthesized matches.
+- **Bitfields don't use C bitfields.** Loads/stores spell explicit shift +
+ mask on a `uint{8,16,32,64}_t` storage view. Signed loads use the
+ shift-left-then-arith-shift-right trick on a signed cast. The host C
+ compiler never sees a `int x : 3;`-style declaration, so its ABI choices
+ don't enter the picture.
+- **Mach-O TLS definitions panic-as-skip.** On Mach-O the user-visible TLS
+ sym is the 24-byte TLV descriptor; reconstructing the original initial
+ value from the synthesized `__init.<name>` aux sym is non-trivial. ELF
+ TLS data definitions (where `.tdata`/`.tbss` carry the user's bytes
+ directly) work — emit `_Thread_local _Alignas(A) uint8_t name[N] =
+ {bytes};`. Mach-O TLS externs work too (they're SK_UNDEF, no descriptor).
+- **Identifier sanitization.** cfree mints assembler-style names like
+ `.Ltoy_static_0` for static locals; `c_sym_name` rewrites illegal C
+ identifier chars to `_`. Safe because such names are SB_LOCAL and have
+ no cross-TU contract.
+
+Acceptance: 124 toy cases under path `C` pass. The 3 remaining SKIPs are
+pcrel/symdiff data relocs as documented.
### Phase 5 — quality
diff --git a/src/arch/c_target/emit.c b/src/arch/c_target/emit.c
@@ -1,4 +1,4 @@
-/* Phase 1 C-source emission for the CGTarget vtable. See doc/CBACKEND.md.
+/* C-source emission for the CGTarget vtable. See doc/CBACKEND.md.
*
* Output strategy
* ---------------
@@ -246,9 +246,10 @@ static const char* c_float_type_name(u32 width) {
}
}
-/* Phase 1/3: void / bool / sized int / pointer / float / record / array /
- * enum / vararg-state. Returns interned C type names; for composites lazily
- * emits a typedef into t->typedefs and returns the typedef name. */
+/* Returns the C type name for a CG type id. Scalars map to fixed-width
+ * <stdint.h> types or float/double/long double; pointers collapse to void*;
+ * composites (records/arrays/funcs) emit an opaque-storage typedef on first
+ * sighting and return the typedef name. */
static const char* c_typename(CTarget* t, CfreeCgTypeId type) {
CfreeCgTypeId resolved = api_unalias_type(t->c, type);
const CgType* ty = cg_type_get(t->c, resolved);
@@ -449,13 +450,25 @@ void c_emit_operand(CTarget* t, Operand op) {
return;
}
case OPK_GLOBAL: {
- /* Data globals need an `extern T name;` declaration AND, in the
- * single-TU case, the actual data definition — both belong to data
- * emission (Phase 4). Defer with a graceful panic the harness reports
- * as SKIP. Function callees go through c_call directly, not here. */
- SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
- compiler_panic(t->c, loc,
- "C target: OPK_GLOBAL data reference not yet supported");
+ /* Address expression for `&sym + addend`. The data symbol is declared
+ * elsewhere (either as a function forward, or as `uint8_t name[N]`
+ * data emitted at finalize); here we just form the address with the
+ * right pointer type. */
+ const char* nm = c_sym_name(t, op.v.global.sym);
+ cbuf_puts(&t->body, "((");
+ if (op.type != CFREE_CG_TYPE_NONE) {
+ c_emit_type(t, &t->body, op.type);
+ } else {
+ cbuf_puts(&t->body, "void*");
+ }
+ cbuf_puts(&t->body, ")((char*)");
+ cbuf_puts(&t->body, nm);
+ if (op.v.global.addend != 0) {
+ cbuf_puts(&t->body, " + ");
+ cbuf_put_i64(&t->body, op.v.global.addend);
+ }
+ cbuf_puts(&t->body, "))");
+ return;
}
default: {
SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
@@ -547,6 +560,20 @@ static int c_operand_is_ptr_typed(CTarget* t, Operand op) {
return 0;
}
+/* Emit `(target_ty)(uintptr_t)(op)` (or `(target_ty)(op)` for float
+ * target_ty). Used when the caller knows the C expression type they want and
+ * the source operand may have been declared with a different type (CG reuses
+ * reg ids across types). Without the bridge, gcc trips -Wint-conversion. */
+static void c_emit_operand_as(CTarget* t, Operand op, CfreeCgTypeId target_ty) {
+ cbuf_puts(&t->body, "(");
+ c_emit_type(t, &t->body, target_ty);
+ cbuf_puts(&t->body, ")");
+ if (!c_type_is_float(t, target_ty)) cbuf_puts(&t->body, "(uintptr_t)");
+ cbuf_puts(&t->body, "(");
+ c_emit_operand(t, op);
+ cbuf_puts(&t->body, ")");
+}
+
/* Emit an operand for use in a C binary arithmetic expression. Pointer-typed
* operands are cast to uintptr_t so C arithmetic semantics apply uniformly
* (cfree IR carries byte offsets, not C-pointer-arith scaled indices). */
@@ -589,10 +616,17 @@ static void c_emit_addr_deref(CTarget* t, Operand addr, CfreeCgTypeId access_typ
cbuf_puts(&t->body, buf);
return;
case OPK_GLOBAL: {
- /* Phase 2 defers data emission to Phase 4. */
- (void)access_type;
- compiler_panic(t->c, loc,
- "C target: OPK_GLOBAL data reference not yet supported");
+ const char* nm = c_sym_name(t, addr.v.global.sym);
+ cbuf_puts(&t->body, "(*(");
+ c_emit_type(t, &t->body, access_type);
+ cbuf_puts(&t->body, "*)((char*)");
+ cbuf_puts(&t->body, nm);
+ if (addr.v.global.addend != 0) {
+ cbuf_puts(&t->body, " + ");
+ cbuf_put_i64(&t->body, addr.v.global.addend);
+ }
+ cbuf_puts(&t->body, "))");
+ return;
}
case OPK_INDIRECT: {
Operand base_reg;
@@ -643,9 +677,18 @@ static void c_emit_lvalue_addr(CTarget* t, Operand lv, CfreeCgTypeId dst_type) {
cbuf_puts(&t->body, ")");
return;
case OPK_GLOBAL: {
- (void)dst_type;
- compiler_panic(t->c, loc,
- "C target: OPK_GLOBAL data reference not yet supported");
+ const char* nm = c_sym_name(t, lv.v.global.sym);
+ cbuf_puts(&t->body, "((");
+ c_emit_type(t, &t->body, dst_type);
+ cbuf_puts(&t->body, ")((char*)");
+ cbuf_puts(&t->body, nm);
+ if (lv.v.global.addend != 0) {
+ cbuf_puts(&t->body, " + ");
+ cbuf_put_i64(&t->body, lv.v.global.addend);
+ }
+ cbuf_puts(&t->body, ")");
+ cbuf_puts(&t->body, ")");
+ return;
}
case OPK_INDIRECT: {
cbuf_puts(&t->body, "((");
@@ -675,11 +718,55 @@ const char* c_sym_name(CTarget* t, ObjSymId sym) {
compiler_panic(t->c, (SrcLoc){0, 0, 0}, "C target: unknown ObjSymId %u",
(unsigned)sym);
}
- const char* s = pool_str(t->c->global, os->name, NULL);
+ size_t n = 0;
+ const char* s = pool_str(t->c->global, os->name, &n);
/* Mach-O linker symbols are mangled with a leading underscore; the host
* C compiler will re-add it on its own, so strip when re-emitting source. */
- if (t->c->target.obj == CFREE_OBJ_MACHO && s && s[0] == '_') s += 1;
- return s;
+ if (t->c->target.obj == CFREE_OBJ_MACHO && s && n > 0 && s[0] == '_') {
+ s += 1;
+ n -= 1;
+ }
+ /* Sanitize for C identifier rules: assemblers accept '.', '$', etc. in
+ * symbol names; C does not. Replace each illegal byte with '_' and prepend
+ * '_' if the first char isn't alpha/underscore. Local syms (SB_LOCAL) get
+ * renamed freely since they have no cross-TU contract. Globals are assumed
+ * to come in with C-safe names; if they don't, we still rewrite — the
+ * resulting symbol won't link against other TUs that use the asm spelling,
+ * but cfree-produced code uses the rewritten spelling consistently. */
+ int needs_rewrite = 0;
+ if (n == 0) {
+ return s;
+ }
+ if (!((s[0] >= 'a' && s[0] <= 'z') || (s[0] >= 'A' && s[0] <= 'Z') ||
+ s[0] == '_')) {
+ needs_rewrite = 1;
+ } else {
+ for (size_t i = 0; i < n; ++i) {
+ char ch = s[i];
+ if (!((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
+ (ch >= '0' && ch <= '9') || ch == '_')) {
+ needs_rewrite = 1;
+ break;
+ }
+ }
+ }
+ if (!needs_rewrite) return s;
+ char buf[256];
+ size_t cap = sizeof(buf) - 1u;
+ size_t out = 0;
+ int first_alpha =
+ (s[0] >= 'a' && s[0] <= 'z') || (s[0] >= 'A' && s[0] <= 'Z') ||
+ s[0] == '_';
+ if (!first_alpha && out < cap) buf[out++] = '_';
+ for (size_t i = 0; i < n && out < cap; ++i) {
+ char ch = s[i];
+ int ok = (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
+ (ch >= '0' && ch <= '9') || ch == '_';
+ buf[out++] = ok ? ch : '_';
+ }
+ buf[out] = '\0';
+ Sym interned = pool_intern(t->c->global, buf, out);
+ return pool_str(t->c->global, interned, NULL);
}
/* === Prologue / finalize === */
@@ -1471,8 +1558,11 @@ void c_store(CGTarget* T, Operand addr, Operand src, MemAccess m) {
CfreeCgTypeId access_ty = m.type ? m.type : src.type;
cbuf_puts(&t->body, " ");
c_emit_addr_deref(t, addr, access_ty);
+ /* CG reuses reg ids across types; the src reg's C declaration may differ
+ * from access_ty. c_emit_operand_as bridges through uintptr_t so int/ptr
+ * roundtrips don't trip `-Wint-conversion`. */
cbuf_puts(&t->body, " = ");
- c_emit_operand(t, src);
+ c_emit_operand_as(t, src, access_ty);
cbuf_puts(&t->body, ";\n");
}
@@ -1894,6 +1984,489 @@ void c_set_bytes(CGTarget* T, Operand dst_addr, Operand byte_value,
cbuf_puts(&t->body, ");\n");
}
+/* === TLS ===
+ *
+ * Thread-local data is emitted as `_Thread_local _Alignas(A) uint8_t name[N];`
+ * during c_emit_data, and tls_addr_of spells `((char*)&name + addend)` with
+ * the requested pointer type. The host C compiler picks the TLS model. */
+
+void c_tls_addr_of(CGTarget* T, Operand dst, ObjSymId sym, i64 addend);
+
+void c_tls_addr_of(CGTarget* T, Operand dst, ObjSymId sym, i64 addend) {
+ CTarget* t = (CTarget*)T;
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ if (dst.kind != OPK_REG) {
+ compiler_panic(t->c, loc, "C target: tls_addr_of dst must be REG");
+ }
+ c_ensure_reg(t, dst.v.reg, dst.type, (RegClass)dst.cls);
+ const char* nm = c_sym_name(t, sym);
+ c_emit_reg_assign_open(t, dst.v.reg);
+ cbuf_puts(&t->body, "((char*)&");
+ cbuf_puts(&t->body, nm);
+ if (addend != 0) {
+ cbuf_puts(&t->body, " + ");
+ cbuf_put_i64(&t->body, addend);
+ }
+ cbuf_puts(&t->body, ")");
+ c_emit_reg_assign_close(t);
+}
+
+/* === bitfields ===
+ *
+ * cfree CG flattens bitfields to (storage_type, byte_offset, bit_offset,
+ * bit_width) at the access boundary, so the C target never sees a C-level
+ * bitfield declaration. We extract/insert via explicit mask+shift on the
+ * underlying storage unit (a fixed-width unsigned int loaded through the
+ * usual address-deref path), which sidesteps the C bitfield ABI ambiguity
+ * entirely. */
+
+void c_bitfield_load(CGTarget* T, Operand dst, Operand addr, BitFieldAccess bf);
+void c_bitfield_store(CGTarget* T, Operand addr, Operand src, BitFieldAccess bf);
+
+/* Returns the unsigned C integer type matching the storage-unit byte size. */
+static const char* c_bf_storage_type(u32 size) {
+ switch (size) {
+ case 1: return "uint8_t";
+ case 2: return "uint16_t";
+ case 4: return "uint32_t";
+ case 8: return "uint64_t";
+ default: return NULL;
+ }
+}
+
+/* Spell `*(uintN_t*)((char*)addr + bf.storage_offset)` into the body. */
+static void c_bf_storage_lvalue(CTarget* t, Operand addr, BitFieldAccess bf,
+ const char* storage_ty) {
+ cbuf_puts(&t->body, "(*(");
+ cbuf_puts(&t->body, storage_ty);
+ cbuf_puts(&t->body, "*)((char*)");
+ c_emit_operand(t, addr);
+ if (bf.storage_offset != 0) {
+ cbuf_puts(&t->body, " + ");
+ cbuf_put_u64(&t->body, (u64)bf.storage_offset);
+ }
+ cbuf_puts(&t->body, "))");
+}
+
+void c_bitfield_load(CGTarget* T, Operand dst, Operand addr,
+ BitFieldAccess bf) {
+ CTarget* t = (CTarget*)T;
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ if (dst.kind != OPK_REG) {
+ compiler_panic(t->c, loc, "C target: bitfield_load dst must be REG");
+ }
+ if (bf.bit_width == 0) {
+ /* Zero-width — layout barrier only; nothing to load. Emit a no-op
+ * assignment so the dst reg still gets a defined value. */
+ c_ensure_reg(t, dst.v.reg, dst.type, (RegClass)dst.cls);
+ c_emit_reg_assign_open(t, dst.v.reg);
+ cbuf_puts(&t->body, "0");
+ c_emit_reg_assign_close(t);
+ return;
+ }
+ const char* sty = c_bf_storage_type(bf.storage.size);
+ if (!sty) {
+ compiler_panic(t->c, loc, "C target: bitfield storage size %u unsupported",
+ (unsigned)bf.storage.size);
+ }
+ c_ensure_reg(t, dst.v.reg, dst.type, (RegClass)dst.cls);
+ c_emit_reg_assign_open(t, dst.v.reg);
+ /* For signed bitfields, sign-extend via the standard shift-up / arith-shift-
+ * down trick on a signed integer of the storage width. For unsigned, mask
+ * the extracted bits.
+ *
+ * Storage is little-endian-bit-indexed on every cfree-supported target
+ * (LSB-first within a storage unit on x86_64/aarch64/rv64). */
+ u32 sw = bf.storage.size * 8u;
+ if (bf.signed_) {
+ /* (int_storage_t)((storage << shl) >> shr) where:
+ * shl = sw - bit_width - bit_offset
+ * shr = sw - bit_width
+ * Then cast to dst type. */
+ u32 shl = sw - (u32)bf.bit_width - (u32)bf.bit_offset;
+ u32 shr = sw - (u32)bf.bit_width;
+ cbuf_puts(&t->body, "(((int");
+ cbuf_put_u64(&t->body, (u64)sw);
+ cbuf_puts(&t->body, "_t)(");
+ c_bf_storage_lvalue(t, addr, bf, sty);
+ cbuf_puts(&t->body, " << ");
+ cbuf_put_u64(&t->body, (u64)shl);
+ cbuf_puts(&t->body, ")) >> ");
+ cbuf_put_u64(&t->body, (u64)shr);
+ cbuf_puts(&t->body, ")");
+ } else {
+ /* ((storage >> bit_offset) & ((1u << bit_width) - 1)) */
+ u64 mask = (bf.bit_width >= 64) ? ~(u64)0
+ : (((u64)1 << bf.bit_width) - 1u);
+ cbuf_puts(&t->body, "((");
+ c_bf_storage_lvalue(t, addr, bf, sty);
+ cbuf_puts(&t->body, " >> ");
+ cbuf_put_u64(&t->body, (u64)bf.bit_offset);
+ cbuf_puts(&t->body, ") & (");
+ cbuf_puts(&t->body, sty);
+ cbuf_puts(&t->body, ")0x");
+ static const char hex[] = "0123456789abcdef";
+ int started = 0;
+ for (int sh = 60; sh >= 0; sh -= 4) {
+ u32 nib = (u32)((mask >> sh) & 0xfu);
+ if (nib || started || sh == 0) {
+ cbuf_putc(&t->body, hex[nib]);
+ started = 1;
+ }
+ }
+ cbuf_puts(&t->body, ")");
+ }
+ c_emit_reg_assign_close(t);
+}
+
+void c_bitfield_store(CGTarget* T, Operand addr, Operand src,
+ BitFieldAccess bf) {
+ CTarget* t = (CTarget*)T;
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ if (bf.bit_width == 0) return; /* zero-width: no-op */
+ const char* sty = c_bf_storage_type(bf.storage.size);
+ if (!sty) {
+ compiler_panic(t->c, loc, "C target: bitfield storage size %u unsupported",
+ (unsigned)bf.storage.size);
+ }
+ u64 mask = (bf.bit_width >= 64) ? ~(u64)0
+ : (((u64)1 << bf.bit_width) - 1u);
+ /* *(uintN_t*)p = (*(uintN_t*)p & ~(mask << bit_offset)) |
+ * (((uintN_t)src & mask) << bit_offset); */
+ cbuf_puts(&t->body, " ");
+ c_bf_storage_lvalue(t, addr, bf, sty);
+ cbuf_puts(&t->body, " = (");
+ c_bf_storage_lvalue(t, addr, bf, sty);
+ cbuf_puts(&t->body, " & ~((");
+ cbuf_puts(&t->body, sty);
+ cbuf_puts(&t->body, ")0x");
+ static const char hex[] = "0123456789abcdef";
+ int started = 0;
+ for (int sh = 60; sh >= 0; sh -= 4) {
+ u32 nib = (u32)((mask >> sh) & 0xfu);
+ if (nib || started || sh == 0) {
+ cbuf_putc(&t->body, hex[nib]);
+ started = 1;
+ }
+ }
+ cbuf_puts(&t->body, " << ");
+ cbuf_put_u64(&t->body, (u64)bf.bit_offset);
+ cbuf_puts(&t->body, ")) | ((((");
+ cbuf_puts(&t->body, sty);
+ cbuf_puts(&t->body, ")");
+ c_emit_operand(t, src);
+ cbuf_puts(&t->body, ") & (");
+ cbuf_puts(&t->body, sty);
+ cbuf_puts(&t->body, ")0x");
+ started = 0;
+ for (int sh = 60; sh >= 0; sh -= 4) {
+ u32 nib = (u32)((mask >> sh) & 0xfu);
+ if (nib || started || sh == 0) {
+ cbuf_putc(&t->body, hex[nib]);
+ started = 1;
+ }
+ }
+ cbuf_puts(&t->body, ") << ");
+ cbuf_put_u64(&t->body, (u64)bf.bit_offset);
+ cbuf_puts(&t->body, ");\n");
+}
+
+/* === inline asm ===
+ *
+ * Re-serialize cfree's asm-block IR (template + constraint-bound operands +
+ * clobbers) as GCC extended asm. The cfree CG already speaks GCC-style
+ * constraint strings ("r", "=r", "+m", "[name]constraint", matching "0"...),
+ * so we pass the template through and emit the constraint+operand pairs in
+ * order. */
+
+void c_asm_block(CGTarget* T, const char* tmpl, const AsmConstraint* outs,
+ u32 no, Operand* oo, const AsmConstraint* ins, u32 ni,
+ const Operand* io, const Sym* clobs, u32 nc);
+
+static void c_emit_c_string_literal(CBuf* b, const char* s) {
+ cbuf_putc(b, '"');
+ for (; *s; ++s) {
+ char ch = *s;
+ if (ch == '"' || ch == '\\') {
+ cbuf_putc(b, '\\');
+ cbuf_putc(b, ch);
+ } else if (ch == '\n') {
+ cbuf_puts(b, "\\n");
+ } else if (ch == '\r') {
+ cbuf_puts(b, "\\r");
+ } else if (ch == '\t') {
+ cbuf_puts(b, "\\t");
+ } else if ((unsigned char)ch < 0x20 || (unsigned char)ch >= 0x7f) {
+ static const char hex[] = "0123456789abcdef";
+ cbuf_puts(b, "\\x");
+ cbuf_putc(b, hex[((unsigned char)ch >> 4) & 0xfu]);
+ cbuf_putc(b, hex[(unsigned char)ch & 0xfu]);
+ } else {
+ cbuf_putc(b, ch);
+ }
+ }
+ cbuf_putc(b, '"');
+}
+
+void c_asm_block(CGTarget* T, const char* tmpl, const AsmConstraint* outs,
+ u32 no, Operand* oo, const AsmConstraint* ins, u32 ni,
+ const Operand* io, const Sym* clobs, u32 nc) {
+ CTarget* t = (CTarget*)T;
+ cbuf_puts(&t->body, " __asm__ __volatile__ (");
+ c_emit_c_string_literal(&t->body, tmpl ? tmpl : "");
+ /* Outputs. */
+ cbuf_puts(&t->body, " : ");
+ for (u32 i = 0; i < no; ++i) {
+ if (i > 0) cbuf_puts(&t->body, ", ");
+ if (outs[i].name) {
+ cbuf_puts(&t->body, "[");
+ cbuf_puts(&t->body, pool_str(t->c->global, outs[i].name, NULL));
+ cbuf_puts(&t->body, "] ");
+ }
+ c_emit_c_string_literal(&t->body, outs[i].str ? outs[i].str : "");
+ cbuf_puts(&t->body, "(");
+ /* Outputs must be an lvalue. OPK_REG → the C reg is a plain local; this
+ * works directly. OPK_LOCAL / OPK_INDIRECT also produce lvalues. */
+ if (oo[i].kind == OPK_REG) {
+ c_ensure_reg(t, oo[i].v.reg, oo[i].type, (RegClass)oo[i].cls);
+ char rb[24];
+ c_reg_name(oo[i].v.reg, rb, sizeof rb);
+ cbuf_puts(&t->body, rb);
+ } else {
+ c_emit_addr_deref(t, oo[i], oo[i].type);
+ }
+ cbuf_puts(&t->body, ")");
+ }
+ /* Inputs. cfree synthesizes a matching `"N"` input for every ASM_INOUT
+ * output (so its IR sees a fresh read), but gcc treats `+r` outputs as
+ * already serving the read role and rejects a redundant matching input.
+ * Drop those synthesized matches when the referenced output is `+`-tied. */
+ cbuf_puts(&t->body, " : ");
+ int emitted_any = 0;
+ for (u32 i = 0; i < ni; ++i) {
+ const char* cs = ins[i].str ? ins[i].str : "";
+ if (cs[0] >= '0' && cs[0] <= '9') {
+ u32 idx = (u32)(cs[0] - '0');
+ if (idx < no && outs[idx].str && outs[idx].str[0] == '+') continue;
+ }
+ if (emitted_any) cbuf_puts(&t->body, ", ");
+ emitted_any = 1;
+ if (ins[i].name) {
+ cbuf_puts(&t->body, "[");
+ cbuf_puts(&t->body, pool_str(t->c->global, ins[i].name, NULL));
+ cbuf_puts(&t->body, "] ");
+ }
+ c_emit_c_string_literal(&t->body, cs);
+ cbuf_puts(&t->body, "(");
+ c_emit_operand(t, io[i]);
+ cbuf_puts(&t->body, ")");
+ }
+ /* Clobbers. */
+ cbuf_puts(&t->body, " : ");
+ for (u32 i = 0; i < nc; ++i) {
+ if (i > 0) cbuf_puts(&t->body, ", ");
+ c_emit_c_string_literal(&t->body,
+ pool_str(t->c->global, clobs[i], NULL));
+ }
+ cbuf_puts(&t->body, ");\n");
+}
+
+/* === load_const ===
+ *
+ * Used by CG for non-integer literal pushes (mainly floats — `cfree_cg_push_float`).
+ * Bytes are the target's ABI encoding of the value; we copy them into the dst
+ * reg via a static const byte array and __builtin_memcpy so any host C
+ * compiler sees the same bit pattern. */
+
+void c_load_const(CGTarget* T, Operand dst, ConstBytes cb);
+
+void c_load_const(CGTarget* T, Operand dst, ConstBytes cb) {
+ CTarget* t = (CTarget*)T;
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ if (dst.kind != OPK_REG) {
+ compiler_panic(t->c, loc, "C target: load_const dst must be REG");
+ }
+ c_ensure_reg(t, dst.v.reg, dst.type, (RegClass)dst.cls);
+ char buf[24];
+ c_reg_name(dst.v.reg, buf, sizeof buf);
+ u32 id = ++t->next_tmp;
+ cbuf_puts(&t->body, " { static const uint8_t __k");
+ cbuf_put_u64(&t->body, (u64)id);
+ cbuf_puts(&t->body, "[");
+ cbuf_put_u64(&t->body, (u64)cb.size);
+ cbuf_puts(&t->body, "] = {");
+ static const char hex[] = "0123456789abcdef";
+ for (u32 i = 0; i < cb.size; ++i) {
+ if (i > 0) cbuf_putc(&t->body, ',');
+ cbuf_puts(&t->body, "0x");
+ cbuf_putc(&t->body, hex[(cb.bytes[i] >> 4) & 0xfu]);
+ cbuf_putc(&t->body, hex[cb.bytes[i] & 0xfu]);
+ }
+ cbuf_puts(&t->body, "}; __builtin_memcpy(&");
+ cbuf_puts(&t->body, buf);
+ cbuf_puts(&t->body, ", __k");
+ cbuf_put_u64(&t->body, (u64)id);
+ cbuf_puts(&t->body, ", ");
+ cbuf_put_u64(&t->body, (u64)cb.size);
+ cbuf_puts(&t->body, "); }\n");
+}
+
+/* === atomics ===
+ *
+ * Lowered to gcc/clang's `__atomic_*` generic builtins. The host compiler
+ * picks the inline sequence vs. libcall and applies the requested memory
+ * order. cfree's MemOrder enum aligns 1-1 with the `__ATOMIC_*` constants. */
+
+static const char* c_memorder_token(MemOrder o) {
+ switch (o) {
+ case MO_RELAXED: return "__ATOMIC_RELAXED";
+ case MO_CONSUME: return "__ATOMIC_CONSUME";
+ case MO_ACQUIRE: return "__ATOMIC_ACQUIRE";
+ case MO_RELEASE: return "__ATOMIC_RELEASE";
+ case MO_ACQ_REL: return "__ATOMIC_ACQ_REL";
+ case MO_SEQ_CST: return "__ATOMIC_SEQ_CST";
+ }
+ return "__ATOMIC_SEQ_CST";
+}
+
+void c_atomic_load(CGTarget* T, Operand dst, Operand addr, MemAccess m,
+ MemOrder o);
+void c_atomic_store(CGTarget* T, Operand addr, Operand src, MemAccess m,
+ MemOrder o);
+void c_atomic_rmw(CGTarget* T, AtomicOp op, Operand dst, Operand addr,
+ Operand val, MemAccess m, MemOrder o);
+void c_atomic_cas(CGTarget* T, Operand prior, Operand ok, Operand addr,
+ Operand expected, Operand desired, MemAccess m,
+ MemOrder so, MemOrder fo);
+void c_fence(CGTarget* T, MemOrder o);
+
+void c_atomic_load(CGTarget* T, Operand dst, Operand addr, MemAccess m,
+ MemOrder o) {
+ CTarget* t = (CTarget*)T;
+ (void)m;
+ c_ensure_reg(t, dst.v.reg, dst.type, (RegClass)dst.cls);
+ c_emit_reg_assign_open(t, dst.v.reg);
+ cbuf_puts(&t->body, "__atomic_load_n((");
+ c_emit_type(t, &t->body, dst.type);
+ cbuf_puts(&t->body, "*)");
+ c_emit_operand(t, addr);
+ cbuf_puts(&t->body, ", ");
+ cbuf_puts(&t->body, c_memorder_token(o));
+ cbuf_puts(&t->body, ")");
+ c_emit_reg_assign_close(t);
+}
+
+void c_atomic_store(CGTarget* T, Operand addr, Operand src, MemAccess m,
+ MemOrder o) {
+ CTarget* t = (CTarget*)T;
+ (void)m;
+ cbuf_puts(&t->body, " __atomic_store_n((");
+ c_emit_type(t, &t->body, src.type);
+ cbuf_puts(&t->body, "*)");
+ c_emit_operand(t, addr);
+ cbuf_puts(&t->body, ", ");
+ c_emit_operand_as(t, src, src.type);
+ cbuf_puts(&t->body, ", ");
+ cbuf_puts(&t->body, c_memorder_token(o));
+ cbuf_puts(&t->body, ");\n");
+}
+
+static const char* c_atomic_op_builtin(AtomicOp op) {
+ switch (op) {
+ case AO_XCHG: return "__atomic_exchange_n";
+ case AO_ADD: return "__atomic_fetch_add";
+ case AO_SUB: return "__atomic_fetch_sub";
+ case AO_AND: return "__atomic_fetch_and";
+ case AO_OR: return "__atomic_fetch_or";
+ case AO_XOR: return "__atomic_fetch_xor";
+ case AO_NAND: return "__atomic_fetch_nand";
+ }
+ return NULL;
+}
+
+void c_atomic_rmw(CGTarget* T, AtomicOp op, Operand dst, Operand addr,
+ Operand val, MemAccess m, MemOrder o) {
+ CTarget* t = (CTarget*)T;
+ (void)m;
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ const char* fn = c_atomic_op_builtin(op);
+ if (!fn) {
+ compiler_panic(t->c, loc, "C target: unknown atomic op %d", (int)op);
+ }
+ c_ensure_reg(t, dst.v.reg, dst.type, (RegClass)dst.cls);
+ c_emit_reg_assign_open(t, dst.v.reg);
+ cbuf_puts(&t->body, fn);
+ cbuf_puts(&t->body, "((");
+ c_emit_type(t, &t->body, val.type);
+ cbuf_puts(&t->body, "*)");
+ c_emit_operand(t, addr);
+ cbuf_puts(&t->body, ", ");
+ c_emit_operand_as(t, val, val.type);
+ cbuf_puts(&t->body, ", ");
+ cbuf_puts(&t->body, c_memorder_token(o));
+ cbuf_puts(&t->body, ")");
+ c_emit_reg_assign_close(t);
+}
+
+void c_atomic_cas(CGTarget* T, Operand prior, Operand ok, Operand addr,
+ Operand expected, Operand desired, MemAccess m,
+ MemOrder so, MemOrder fo) {
+ CTarget* t = (CTarget*)T;
+ (void)m;
+ /* gcc's __atomic_compare_exchange_n needs a real lvalue holding the
+ * expected value (it's updated on failure). We can't use `&prior_reg`
+ * directly because CG reuses reg ids across types — the C declaration may
+ * have been locked at a different type. Materialize a fresh scratch local
+ * typed by `prior.type`, then copy out to the prior reg using the existing
+ * uintptr_t bridge. */
+ c_ensure_reg(t, prior.v.reg, prior.type, (RegClass)prior.cls);
+ c_ensure_reg(t, ok.v.reg, ok.type, (RegClass)ok.cls);
+ u32 id = ++t->next_tmp;
+ cbuf_puts(&t->body, " { ");
+ c_emit_type(t, &t->body, prior.type);
+ cbuf_puts(&t->body, " __cas");
+ cbuf_put_u64(&t->body, (u64)id);
+ cbuf_puts(&t->body, " = ");
+ c_emit_operand_as(t, expected, prior.type);
+ cbuf_puts(&t->body, "; ");
+ char ok_name[24], prior_name[24];
+ c_reg_name(ok.v.reg, ok_name, sizeof ok_name);
+ c_reg_name(prior.v.reg, prior_name, sizeof prior_name);
+ cbuf_puts(&t->body, ok_name);
+ cbuf_puts(&t->body, " = (");
+ c_emit_type(t, &t->body, ok.type);
+ cbuf_puts(&t->body, ")__atomic_compare_exchange_n((");
+ c_emit_type(t, &t->body, prior.type);
+ cbuf_puts(&t->body, "*)");
+ c_emit_operand(t, addr);
+ cbuf_puts(&t->body, ", &__cas");
+ cbuf_put_u64(&t->body, (u64)id);
+ cbuf_puts(&t->body, ", ");
+ c_emit_operand_as(t, desired, prior.type);
+ cbuf_puts(&t->body, ", 0, ");
+ cbuf_puts(&t->body, c_memorder_token(so));
+ cbuf_puts(&t->body, ", ");
+ cbuf_puts(&t->body, c_memorder_token(fo));
+ cbuf_puts(&t->body, "); ");
+ /* prior reg = (DECL)(uintptr_t)__cas; */
+ cbuf_puts(&t->body, prior_name);
+ cbuf_puts(&t->body, " = (");
+ CfreeCgTypeId decl = t->reg_type[prior.v.reg];
+ c_emit_type(t, &t->body, decl);
+ cbuf_puts(&t->body, ")");
+ if (!c_type_is_float(t, decl)) cbuf_puts(&t->body, "(uintptr_t)");
+ cbuf_puts(&t->body, "__cas");
+ cbuf_put_u64(&t->body, (u64)id);
+ cbuf_puts(&t->body, "; }\n");
+}
+
+void c_fence(CGTarget* T, MemOrder o) {
+ CTarget* t = (CTarget*)T;
+ cbuf_puts(&t->body, " __atomic_thread_fence(");
+ cbuf_puts(&t->body, c_memorder_token(o));
+ cbuf_puts(&t->body, ");\n");
+}
+
/* === set_loc === */
void c_set_loc(CGTarget* T, SrcLoc l) {
@@ -1901,6 +2474,238 @@ void c_set_loc(CGTarget* T, SrcLoc l) {
(void)l;
}
+/* === data emission ===
+ *
+ * Walks the ObjBuilder's symbol table at finalize and emits a C declaration
+ * for every data object — defined or extern. Bytes are emitted verbatim as a
+ * `uint8_t name[N] = { 0x.., ... }` initializer. Relocations targeting bytes
+ * inside a defined symbol are spelled as runtime fixups in a constructor; this
+ * covers both same-TU and cross-TU references uniformly and avoids the C
+ * static-initializer restrictions on non-constant addresses.
+ *
+ * The host C compiler re-applies the Mach-O leading-underscore on link, so the
+ * C source uses the cfree linker name minus the `_` prefix (matching
+ * c_sym_name elsewhere in this file). */
+
+static int c_is_data_section(const Section* sec) {
+ if (!sec) return 0;
+ switch (sec->kind) {
+ case SEC_DATA:
+ case SEC_RODATA:
+ case SEC_BSS:
+ return 1;
+ case SEC_OTHER:
+ /* User-named sections holding allocated data (e.g. `.text.hot` would
+ * be EXEC, but a custom data section is just SF_ALLOC). */
+ return (sec->flags & SF_ALLOC) && !(sec->flags & SF_EXEC);
+ default:
+ return 0;
+ }
+}
+
+static void c_emit_link_attrs(CBuf* b, const ObjSym* os) {
+ if (os->bind == SB_WEAK) cbuf_puts(b, "__attribute__((weak)) ");
+ if (os->vis == SV_HIDDEN) {
+ cbuf_puts(b, "__attribute__((visibility(\"hidden\"))) ");
+ } else if (os->vis == SV_PROTECTED) {
+ cbuf_puts(b, "__attribute__((visibility(\"protected\"))) ");
+ }
+}
+
+/* Reads `len` bytes starting at `ofs` from the section's byte buffer. The
+ * Section uses a chunked Buf; buf_read does the splice for us. */
+static void c_read_section_bytes(const Section* sec, u32 ofs, u8* out,
+ size_t len) {
+ buf_read(&sec->bytes, ofs, out, len);
+}
+
+static void c_emit_data_bytes(CBuf* b, const u8* bytes, size_t n) {
+ cbuf_puts(b, " = {");
+ for (size_t i = 0; i < n; ++i) {
+ if (i > 0) cbuf_putc(b, ',');
+ if ((i & 15u) == 0) cbuf_puts(b, "\n ");
+ cbuf_puts(b, "0x");
+ static const char hex[] = "0123456789abcdef";
+ cbuf_putc(b, hex[(bytes[i] >> 4) & 0xfu]);
+ cbuf_putc(b, hex[bytes[i] & 0xfu]);
+ }
+ cbuf_puts(b, "\n }");
+}
+
+/* Returns 1 if any relocation falls into the half-open range [base, base+size)
+ * of section `sec_id` (i.e. patches the bytes of this symbol). */
+static int c_sym_has_relocs(CTarget* t, ObjSecId sec_id, u32 base, u32 size) {
+ u32 total = obj_reloc_total(t->obj);
+ for (u32 i = 0; i < total; ++i) {
+ const Reloc* r = obj_reloc_at(t->obj, i);
+ if (r->section_id != sec_id) continue;
+ if (r->offset >= base && r->offset < base + size) return 1;
+ }
+ return 0;
+}
+
+static void c_emit_sym_relocs_fixup(CTarget* t, const char* nm,
+ ObjSecId sec_id, u32 base, u32 size) {
+ u32 total = obj_reloc_total(t->obj);
+ int first = 1;
+ for (u32 i = 0; i < total; ++i) {
+ const Reloc* r = obj_reloc_at(t->obj, i);
+ if (r->section_id != sec_id) continue;
+ if (r->offset < base || r->offset >= base + size) continue;
+ if (first) {
+ cbuf_puts(&t->data_defs,
+ "__attribute__((constructor)) static void __cfree_init_");
+ cbuf_puts(&t->data_defs, nm);
+ cbuf_puts(&t->data_defs, "(void) {\n");
+ first = 0;
+ }
+ /* api_data_reloc_kind only emits R_ABS{32,64} and R_PC{32,64}; pcrel
+ * has no constructor-time spelling (see doc/CBACKEND.md §pcrel). */
+ u32 width = 0;
+ switch ((int)r->kind) {
+ case R_ABS32: width = 4; break;
+ case R_ABS64: width = 8; break;
+ case R_PC32:
+ case R_PC64:
+ compiler_panic(t->c, (SrcLoc){0, 0, 0},
+ "C target: pcrel data reloc not yet supported");
+ default:
+ compiler_panic(t->c, (SrcLoc){0, 0, 0},
+ "C target: data reloc kind %d not yet supported",
+ (int)r->kind);
+ }
+ const char* tgt = c_sym_name(t, r->sym);
+ const char* ity = (width == 8) ? "uint64_t" :
+ (width == 4) ? "uint32_t" :
+ (width == 2) ? "uint16_t" : "uint8_t";
+ cbuf_puts(&t->data_defs, " *(");
+ cbuf_puts(&t->data_defs, ity);
+ cbuf_puts(&t->data_defs, "*)((char*)");
+ cbuf_puts(&t->data_defs, nm);
+ cbuf_puts(&t->data_defs, " + ");
+ cbuf_put_u64(&t->data_defs, (u64)(r->offset - base));
+ cbuf_puts(&t->data_defs, ") = (");
+ cbuf_puts(&t->data_defs, ity);
+ cbuf_puts(&t->data_defs, ")(uintptr_t)((char*)");
+ cbuf_puts(&t->data_defs, tgt);
+ if (r->addend != 0) {
+ cbuf_puts(&t->data_defs, " + ");
+ cbuf_put_i64(&t->data_defs, r->addend);
+ }
+ cbuf_puts(&t->data_defs, ");\n");
+ }
+ if (!first) cbuf_puts(&t->data_defs, "}\n");
+}
+
+/* Emit one data symbol: extern declaration if undef, otherwise the full
+ * definition with bytes. Function symbols are skipped — those go through the
+ * forwards path. */
+static void c_emit_data_symbol(CTarget* t, ObjSymId id, const ObjSym* os) {
+ if (os->kind == SK_FUNC || os->kind == SK_IFUNC) return;
+ if (os->kind == SK_SECTION || os->kind == SK_FILE) return;
+ const char* nm = c_sym_name(t, id);
+ CBuf* b = &t->data_defs;
+ /* SK_TLS user-visible syms need a _Thread_local prefix. On ELF the sym
+ * lives in .tdata/.tbss with the right bytes/size and our normal data
+ * path handles them once we set the qualifier. On Mach-O the user-visible
+ * sym is the 24-byte TLV descriptor — its bytes are not the user's data,
+ * so we can't faithfully reproduce it in C; bail to a SKIP. */
+ int is_tls = (os->kind == SK_TLS);
+
+ /* Extern (undefined) data — only declare if referenced. We can't readily
+ * distinguish "referenced as data" from "referenced as func address" here,
+ * so declare it as `extern uint8_t name[];` only if it was actually
+ * referenced from somewhere; otherwise it'd produce unused warnings. The
+ * obj symbol's `referenced` bit is exactly the right signal. */
+ /* SK_TLS with no defining section = extern TLS — falls through to the
+ * undef branch below with the `_Thread_local` qualifier. */
+ if (os->kind == SK_UNDEF ||
+ (is_tls && os->section_id == OBJ_SEC_NONE)) {
+ if (!os->referenced) return;
+ cbuf_puts(b, "extern ");
+ if (is_tls) cbuf_puts(b, "_Thread_local ");
+ cbuf_puts(b, "uint8_t ");
+ cbuf_puts(b, nm);
+ cbuf_puts(b, "[];\n");
+ return;
+ }
+ if (is_tls && t->c->target.obj == CFREE_OBJ_MACHO) {
+ /* Mach-O TLV: the user sym is the descriptor (24 bytes, with relocs to
+ * __tlv_bootstrap and the synthesized __init.<name> sym). We can't
+ * faithfully express the descriptor in portable C — `_Thread_local`
+ * needs the underlying initial value, which lives in a different
+ * (synthesized) sym. Phase 4 leaves this for a future pass. */
+ compiler_panic(t->c, (SrcLoc){0, 0, 0},
+ "C target: Mach-O TLS data definition not yet supported");
+ }
+ /* Skip the synthesized `__init.<name>` aux sym on Mach-O — even if we got
+ * here, it shouldn't be emitted as user-visible data. (Currently
+ * unreachable because the descriptor path panicked above.) */
+ if (os->kind == SK_COMMON) {
+ /* Common — uninitialized, with explicit alignment. Emit as
+ * tentative-definition (`uint8_t name[size];` at file scope), which C
+ * treats as a common-style definition under -fcommon. */
+ cbuf_puts(b, "_Alignas(");
+ cbuf_put_u64(b, os->common_align ? os->common_align : 1);
+ cbuf_puts(b, ") uint8_t ");
+ cbuf_puts(b, nm);
+ cbuf_puts(b, "[");
+ cbuf_put_u64(b, os->size);
+ cbuf_puts(b, "];\n");
+ return;
+ }
+ if (os->section_id == OBJ_SEC_NONE) return;
+ const Section* sec = obj_section_get(t->obj, os->section_id);
+ if (!c_is_data_section(sec)) return;
+ u32 base = (u32)os->value;
+ u32 size = (u32)os->size;
+ /* Storage class: SB_LOCAL → `static`, SB_GLOBAL/WEAK → file scope. */
+ if (os->bind == SB_LOCAL) cbuf_puts(b, "static ");
+ if (is_tls) cbuf_puts(b, "_Thread_local ");
+ c_emit_link_attrs(b, os);
+ /* `const` only when the section is RODATA and no relocs patch it (we
+ * still drop const if there are relocs because the constructor will
+ * write through this storage). */
+ int has_relocs = c_sym_has_relocs(t, os->section_id, base, size);
+ int is_ro = (sec->kind == SEC_RODATA);
+ if (is_ro && !has_relocs) cbuf_puts(b, "const ");
+ cbuf_puts(b, "_Alignas(");
+ cbuf_put_u64(b, sec->align ? sec->align : 1);
+ cbuf_puts(b, ") uint8_t ");
+ cbuf_puts(b, nm);
+ cbuf_puts(b, "[");
+ cbuf_put_u64(b, size ? size : 1);
+ cbuf_puts(b, "]");
+ if (sec->kind == SEC_BSS || sec->sem == SSEM_NOBITS) {
+ /* BSS — no initializer (defaults to zero in C). */
+ cbuf_puts(b, ";\n");
+ } else if (size == 0) {
+ cbuf_puts(b, ";\n");
+ } else {
+ Heap* h = t->c->ctx->heap;
+ u8* bytes = (u8*)h->alloc(h, size, 1);
+ if (!bytes) {
+ compiler_panic(t->c, (SrcLoc){0, 0, 0}, "C target: oom on data bytes");
+ }
+ c_read_section_bytes(sec, base, bytes, size);
+ c_emit_data_bytes(b, bytes, size);
+ h->free(h, bytes, size);
+ cbuf_puts(b, ";\n");
+ }
+ if (has_relocs) c_emit_sym_relocs_fixup(t, nm, os->section_id, base, size);
+}
+
+static void c_emit_data(CTarget* t) {
+ ObjSymIter* it = obj_symiter_new(t->obj);
+ if (!it) return;
+ ObjSymEntry e;
+ while (obj_symiter_next(it, &e)) {
+ if (!e.sym) continue;
+ c_emit_data_symbol(t, e.id, e.sym);
+ }
+ obj_symiter_free(it);
+}
+
/* === finalize / destroy === */
void c_finalize(CGTarget* T) {
@@ -1919,6 +2724,11 @@ void c_finalize(CGTarget* T) {
c_writer_write(t, t->forwards.data, t->forwards.len);
c_writer_puts(t, "\n");
}
+ c_emit_data(t);
+ if (t->data_defs.len) {
+ c_writer_write(t, t->data_defs.data, t->data_defs.len);
+ c_writer_puts(t, "\n");
+ }
if (t->body.len) c_writer_write(t, t->body.data, t->body.len);
}
@@ -1926,6 +2736,8 @@ void c_destroy(CGTarget* T) {
CTarget* t = (CTarget*)T;
Heap* h = t->c->ctx->heap;
cbuf_fini(&t->forwards);
+ cbuf_fini(&t->typedefs);
+ cbuf_fini(&t->data_defs);
cbuf_fini(&t->decls);
cbuf_fini(&t->body);
if (t->sym_forwarded) h->free(h, t->sym_forwarded, t->sym_forwarded_cap);
diff --git a/src/arch/c_target/internal.h b/src/arch/c_target/internal.h
@@ -58,6 +58,11 @@ typedef struct CTarget {
/* TU-wide typedefs (records, arrays, function pointers, opaque storage).
* Emitted between prologue and forwards. */
CBuf typedefs;
+ /* TU-wide data definitions and extern data declarations. Emitted between
+ * forwards and function bodies so functions can reference data symbols and
+ * data initializers can reference function forward decls. Populated at
+ * finalize by walking the ObjBuilder's data sections. */
+ CBuf data_defs;
/* Per-type id state: 0 = not seen, 1 = inflight, 2 = emitted. */
u8* type_state;
u32 type_state_cap;
diff --git a/src/arch/c_target/target.c b/src/arch/c_target/target.c
@@ -52,9 +52,23 @@ void c_va_end(CGTarget*, Operand);
void c_va_copy(CGTarget*, Operand, Operand);
void c_copy_bytes(CGTarget*, Operand, Operand, AggregateAccess);
void c_set_bytes(CGTarget*, Operand, Operand, AggregateAccess);
-
-/* Unimplemented stubs panic specifically per the doc's Phase 0 acceptance
- * criterion. The method name is encoded in the panic message. */
+void c_load_const(CGTarget*, Operand, ConstBytes);
+void c_asm_block(CGTarget*, const char*, const AsmConstraint*, u32, Operand*,
+ const AsmConstraint*, u32, const Operand*, const Sym*, u32);
+void c_bitfield_load(CGTarget*, Operand, Operand, BitFieldAccess);
+void c_bitfield_store(CGTarget*, Operand, Operand, BitFieldAccess);
+void c_tls_addr_of(CGTarget*, Operand, ObjSymId, i64);
+void c_atomic_load(CGTarget*, Operand, Operand, MemAccess, MemOrder);
+void c_atomic_store(CGTarget*, Operand, Operand, MemAccess, MemOrder);
+void c_atomic_rmw(CGTarget*, AtomicOp, Operand, Operand, Operand, MemAccess,
+ MemOrder);
+void c_atomic_cas(CGTarget*, Operand, Operand, Operand, Operand, Operand,
+ MemAccess, MemOrder, MemOrder);
+void c_fence(CGTarget*, MemOrder);
+
+/* Unimplemented stubs panic with the method name in the message — the toy
+ * harness recognizes "C target: method <name> not implemented" as a graceful
+ * SKIP rather than a hard FAIL. */
#define C_UNIMPL(name) \
compiler_panic(((CTarget*)t)->c, ((CTarget*)t)->cur_fn ? \
((CTarget*)t)->cur_fn->loc : (SrcLoc){0,0,0}, \
@@ -123,26 +137,6 @@ static u32 c_call_stack_size_zero(CGTarget* t, const CGCallDesc* d) {
return 0;
}
-static void c_unimpl_load_const(CGTarget* t, Operand dst, ConstBytes cb) {
- (void)dst; (void)cb;
- C_UNIMPL("load_const");
-}
-static void c_unimpl_tls_addr_of(CGTarget* t, Operand dst, ObjSymId sym,
- i64 addend) {
- (void)dst; (void)sym; (void)addend;
- C_UNIMPL("tls_addr_of");
-}
-static void c_unimpl_bitfield_load(CGTarget* t, Operand dst, Operand addr,
- BitFieldAccess bf) {
- (void)dst; (void)addr; (void)bf;
- C_UNIMPL("bitfield_load");
-}
-static void c_unimpl_bitfield_store(CGTarget* t, Operand addr, Operand src,
- BitFieldAccess bf) {
- (void)addr; (void)src; (void)bf;
- C_UNIMPL("bitfield_store");
-}
-
static void c_unimpl_plan_call(CGTarget* t, const CGCallDesc* d, CGCallPlan* p) {
(void)d; (void)p;
C_UNIMPL("plan_call");
@@ -166,42 +160,6 @@ static void c_unimpl_emit_call_plan(CGTarget* t, const CGCallPlan* p) {
C_UNIMPL("emit_call_plan");
}
-static void c_unimpl_atomic_load(CGTarget* t, Operand d, Operand a, MemAccess m,
- MemOrder o) {
- (void)d; (void)a; (void)m; (void)o;
- C_UNIMPL("atomic_load");
-}
-static void c_unimpl_atomic_store(CGTarget* t, Operand a, Operand s, MemAccess m,
- MemOrder o) {
- (void)a; (void)s; (void)m; (void)o;
- C_UNIMPL("atomic_store");
-}
-static void c_unimpl_atomic_rmw(CGTarget* t, AtomicOp op, Operand d, Operand a,
- Operand v, MemAccess m, MemOrder o) {
- (void)op; (void)d; (void)a; (void)v; (void)m; (void)o;
- C_UNIMPL("atomic_rmw");
-}
-static void c_unimpl_atomic_cas(CGTarget* t, Operand p, Operand ok, Operand a,
- Operand e, Operand de, MemAccess m, MemOrder so,
- MemOrder fo) {
- (void)p; (void)ok; (void)a; (void)e; (void)de; (void)m; (void)so; (void)fo;
- C_UNIMPL("atomic_cas");
-}
-static void c_unimpl_fence(CGTarget* t, MemOrder o) {
- (void)o;
- C_UNIMPL("fence");
-}
-
-static void c_unimpl_asm_block(CGTarget* t, const char* tmpl,
- const AsmConstraint* outs, u32 no, Operand* oo,
- const AsmConstraint* ins, u32 ni,
- const Operand* io, const Sym* clobs, u32 nc) {
- (void)tmpl; (void)outs; (void)no; (void)oo;
- (void)ins; (void)ni; (void)io;
- (void)clobs; (void)nc;
- C_UNIMPL("asm_block");
-}
-
static void cgt_cleanup(void* arg) { cgtarget_free((CGTarget*)arg); }
CGTarget* c_cgtarget_new(Compiler* c, ObjBuilder* o, CfreeWriter* w) {
@@ -213,6 +171,7 @@ CGTarget* c_cgtarget_new(Compiler* c, ObjBuilder* o, CfreeWriter* w) {
x->w = w;
cbuf_init(&x->forwards, c->ctx->heap);
cbuf_init(&x->typedefs, c->ctx->heap);
+ cbuf_init(&x->data_defs, c->ctx->heap);
cbuf_init(&x->decls, c->ctx->heap);
cbuf_init(&x->body, c->ctx->heap);
@@ -261,16 +220,16 @@ CGTarget* c_cgtarget_new(Compiler* c, ObjBuilder* o, CfreeWriter* w) {
/* ---- data movement ---- */
t->load_imm = c_load_imm;
- t->load_const = c_unimpl_load_const;
+ t->load_const = c_load_const;
t->copy = c_copy;
t->load = c_load;
t->store = c_store;
t->addr_of = c_addr_of;
- t->tls_addr_of = c_unimpl_tls_addr_of;
+ t->tls_addr_of = c_tls_addr_of;
t->copy_bytes = c_copy_bytes;
t->set_bytes = c_set_bytes;
- t->bitfield_load = c_unimpl_bitfield_load;
- t->bitfield_store = c_unimpl_bitfield_store;
+ t->bitfield_load = c_bitfield_load;
+ t->bitfield_store = c_bitfield_store;
/* ---- arithmetic, compare, convert ---- */
t->binop = c_binop;
@@ -295,15 +254,15 @@ CGTarget* c_cgtarget_new(Compiler* c, ObjBuilder* o, CfreeWriter* w) {
t->va_copy_ = c_va_copy;
/* ---- atomics ---- */
- t->atomic_load = c_unimpl_atomic_load;
- t->atomic_store = c_unimpl_atomic_store;
- t->atomic_rmw = c_unimpl_atomic_rmw;
- t->atomic_cas = c_unimpl_atomic_cas;
- t->fence = c_unimpl_fence;
+ t->atomic_load = c_atomic_load;
+ t->atomic_store = c_atomic_store;
+ t->atomic_rmw = c_atomic_rmw;
+ t->atomic_cas = c_atomic_cas;
+ t->fence = c_fence;
/* ---- intrinsics / asm ---- */
t->intrinsic = c_intrinsic;
- t->asm_block = c_unimpl_asm_block;
+ t->asm_block = c_asm_block;
t->resolve_reg_name = NULL;
t->set_loc = c_set_loc;