commit 684dd8591fd698225fd8ed3f148cddb1e337d63c
parent 6042aecbf851abe32a7a0428bda3d13c803fb0cb
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 26 May 2026 12:02:20 -0700
Add IR recorder C backend path
Diffstat:
14 files changed, 5956 insertions(+), 4187 deletions(-)
diff --git a/src/arch/c_target/c_emit.c b/src/arch/c_target/c_emit.c
@@ -0,0 +1,3827 @@
+/* C-source emission core. See doc/CBACKEND.md.
+ *
+ * Output strategy
+ * ---------------
+ * Each function buffers two CBufs while CG walks the body:
+ * decls — variable declarations: " long long v3;\n"
+ * body — TU-wide running output; we accumulate signature/body/closing-brace
+ * across all functions; func_end splices decls in after the open
+ * brace using the recorded fn_body_start bookmark.
+ *
+ * c_emit_finalize flushes a tiny prologue + body to the writer.
+ *
+ * Local declaration is lazy: every operand emit goes through c_ensure_local,
+ * which appends one declaration for each semantic local. */
+
+#include "arch/c_target/c_emit.h"
+
+#include <stdio.h>
+#include <string.h>
+
+#include "cg/type.h"
+#include "core/arena.h"
+#include "core/core.h"
+#include "core/heap.h"
+#include "core/pool.h"
+#include "core/slice.h"
+#include "obj/obj.h"
+
+/* Forward decls. */
+static void c_ensure_typedef(CTarget* t, CfreeCgTypeId tid);
+static const char* c_typedef_name(CTarget* t, CfreeCgTypeId tid);
+static const char* c_typename(CTarget* t, CfreeCgTypeId type);
+static CfreeCgTypeId c_local_type_or_panic(CTarget* t, CGLocal local);
+static void c_ensure_tuple_typedef(CTarget* t, const CfreeCgTypeId* types,
+ u32 ntypes);
+static void c_emit_tuple_type_name(CTarget* t, CBuf* b,
+ const CfreeCgTypeId* types, u32 ntypes);
+static Operand c_op_local(CGLocal local, CfreeCgTypeId type);
+static int c_type_is_aggregate(CTarget* t, CfreeCgTypeId type);
+static int c_type_is_bool(CTarget* t, CfreeCgTypeId type);
+static int c_type_is_ptr(CTarget* t, CfreeCgTypeId type);
+static int c_operand_is_ptr_typed(CTarget* t, Operand op);
+static void c_emit_addr_deref(CTarget* t, Operand addr,
+ CfreeCgTypeId access_type);
+static void c_emit_copy_addr(CTarget* t, Operand addr);
+CGLocal c_emit_local(CTarget* t, const CGLocalDesc* d);
+static void c_ensure_forward_decl_with_results(
+ CTarget* t, ObjSymId sym, CfreeCgTypeId fn_type,
+ const CfreeCgTypeId* result_types, u32 nresults);
+/* Private accessor on ObjBuilder (defined in obj/obj.c, not in obj.h).
+ * Same forward-decl trick as obj_tls.c uses. */
+ObjSymId obj_tlv_bootstrap_get(const ObjBuilder*);
+
+/* === Target state === */
+
+void c_emit_target_init(CTarget* t, Compiler* c, ObjBuilder* o,
+ CfreeWriter* w) {
+ memset(t, 0, sizeof *t);
+ t->c = c;
+ t->obj = o;
+ t->w = w;
+ cbuf_init(&t->forwards, c->ctx->heap);
+ cbuf_init(&t->typedefs, c->ctx->heap);
+ cbuf_init(&t->data_defs, c->ctx->heap);
+ cbuf_init(&t->decls, c->ctx->heap);
+ cbuf_init(&t->body, c->ctx->heap);
+}
+
+CTarget* c_emit_target_new(Compiler* c, ObjBuilder* o, CfreeWriter* w) {
+ CTarget* t = arena_new(c->tu, CTarget);
+ if (!t) return NULL;
+ c_emit_target_init(t, c, o, w);
+ return t;
+}
+
+/* === Writer helpers === */
+
+void c_writer_write(CTarget* t, const void* data, size_t n) {
+ CfreeStatus st = cfree_writer_write(t->w, data, n);
+ if (st != CFREE_OK) {
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ compiler_panic(t->c, loc, "C target: writer error %d", (int)st);
+ }
+}
+
+void c_writer_puts(CTarget* t, const char* s) {
+ size_t n = 0;
+ while (s[n]) ++n;
+ c_writer_write(t, s, n);
+}
+
+/* === CLocal / type emission === */
+
+static const char* c_int_type_name_for_width(u32 width, int signed_) {
+ switch (width) {
+ case 1:
+ case 8:
+ return signed_ ? "int8_t" : "uint8_t";
+ case 16:
+ return signed_ ? "int16_t" : "uint16_t";
+ case 32:
+ return signed_ ? "int32_t" : "uint32_t";
+ case 64:
+ return signed_ ? "int64_t" : "uint64_t";
+ case 128:
+ return signed_ ? "__int128" : "unsigned __int128";
+ default:
+ return NULL;
+ }
+}
+
+/* Returns the integer width for sign-aware emission. 0 if the type isn't a
+ * fixed-width integer (float, ptr, void, aggregate). */
+static u32 c_int_width_for_signedness(CTarget* t, CfreeCgTypeId type) {
+ if (type == CFREE_CG_TYPE_NONE) return 0;
+ CfreeCgTypeId u = api_unalias_type(t->c, type);
+ const CgType* ty = cg_type_get(t->c, u);
+ if (!ty) return 0;
+ if (ty->kind == CFREE_CG_TYPE_INT) return ty->integer.width;
+ if (ty->kind == CFREE_CG_TYPE_BOOL) return 32; /* bool maps to int32_t */
+ return 0;
+}
+
+/* === Typedef machinery ===
+ *
+ * Composite types (records, arrays, function types) are emitted as opaque
+ * byte-storage typedefs in a TU-wide typedefs section. The typedef name is
+ * `__ty_<id>` keyed on the unaliased type id; this is stable for the
+ * compiler instance.
+ *
+ * For records and arrays the typedef wraps a single `_Alignas(A) uint8_t
+ * raw[N];` member, so all field/element access is mediated by the existing
+ * `(*(T*)((char*)addr + ofs))` path. This sidesteps any ABI ambiguity (C
+ * bitfield rules, array decay, packed/aligned attribute interactions) and
+ * keeps types orthogonal to access patterns.
+ *
+ * For function types we emit a function-pointer typedef `R (*__ty_N)(...)`,
+ * used for indirect calls and function-pointer-typed values. */
+
+static void c_grow_type_state(CTarget* t, u32 needed) {
+ Heap* h = t->c->ctx->heap;
+ u32 newcap = t->type_state_cap ? t->type_state_cap : 32;
+ while (newcap < needed) newcap *= 2;
+ u8* nd = (u8*)h->realloc(h, t->type_state, t->type_state_cap, newcap, 1);
+ if (!nd && newcap) {
+ compiler_panic(t->c, (SrcLoc){0, 0, 0}, "C target: out of memory");
+ }
+ for (u32 i = t->type_state_cap; i < newcap; ++i) nd[i] = 0;
+ t->type_state = nd;
+ t->type_state_cap = newcap;
+}
+
+static const char* c_typedef_name(CTarget* t, CfreeCgTypeId tid) {
+ CfreeCgTypeId u = api_unalias_type(t->c, tid);
+ char buf[32];
+ int n = snprintf(buf, sizeof buf, "__ty_%u", (unsigned)u);
+ Sym s = pool_intern_slice(t->c->global, (Slice){.s = buf, .len = (size_t)n});
+ return pool_slice(t->c->global, s).s;
+}
+
+/* Forward decl. */
+static void c_emit_typedef_for_func(CTarget* t, CfreeCgTypeId tid,
+ const CgType* ty);
+
+static void c_ensure_typedef(CTarget* t, CfreeCgTypeId tid) {
+ CfreeCgTypeId u = api_unalias_type(t->c, tid);
+ if ((u32)u >= t->type_state_cap) c_grow_type_state(t, (u32)u + 1u);
+ if (t->type_state[u] >= 2) return;
+ if (t->type_state[u] == 1) return; /* cyclic — emit forward-only */
+ t->type_state[u] = 1;
+ const CgType* ty = cg_type_get(t->c, u);
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ if (!ty)
+ compiler_panic(t->c, loc, "C target: unknown type id %u", (unsigned)u);
+ switch (ty->kind) {
+ case CFREE_CG_TYPE_FUNC:
+ c_emit_typedef_for_func(t, u, ty);
+ break;
+ case CFREE_CG_TYPE_RECORD: {
+ /* Recurse on field types so any composite-typed field has its
+ * typedef emitted first. (Records-by-value are accessed only via
+ * pointer arithmetic in cfree CG, but emitting deps first keeps the
+ * output readable and stable.) */
+ for (u32 i = 0; i < ty->record.nfields; ++i) {
+ if (!(ty->record.fields[i].flags & CFREE_CG_FIELD_BITFIELD)) {
+ CfreeCgTypeId ft = ty->record.fields[i].type;
+ CfreeCgTypeId ftu = api_unalias_type(t->c, ft);
+ const CgType* fty = cg_type_get(t->c, ftu);
+ if (fty && (fty->kind == CFREE_CG_TYPE_RECORD ||
+ fty->kind == CFREE_CG_TYPE_ARRAY ||
+ fty->kind == CFREE_CG_TYPE_FUNC)) {
+ c_ensure_typedef(t, ftu);
+ }
+ }
+ }
+ cbuf_puts(&t->typedefs, "typedef struct { _Alignas(");
+ cbuf_put_u64(&t->typedefs, (u64)cg_type_align(t->c, u));
+ cbuf_puts(&t->typedefs, ") uint8_t raw[");
+ cbuf_put_u64(&t->typedefs, cg_type_size(t->c, u));
+ cbuf_puts(&t->typedefs, "]; } __ty_");
+ cbuf_put_u64(&t->typedefs, (u64)u);
+ cbuf_puts(&t->typedefs, ";\n");
+ break;
+ }
+ case CFREE_CG_TYPE_ARRAY: {
+ CfreeCgTypeId eu = api_unalias_type(t->c, ty->array.elem);
+ const CgType* ety = cg_type_get(t->c, eu);
+ if (ety && (ety->kind == CFREE_CG_TYPE_RECORD ||
+ ety->kind == CFREE_CG_TYPE_ARRAY ||
+ ety->kind == CFREE_CG_TYPE_FUNC)) {
+ c_ensure_typedef(t, eu);
+ }
+ cbuf_puts(&t->typedefs, "typedef struct { _Alignas(");
+ cbuf_put_u64(&t->typedefs, (u64)cg_type_align(t->c, u));
+ cbuf_puts(&t->typedefs, ") uint8_t raw[");
+ cbuf_put_u64(&t->typedefs, cg_type_size(t->c, u));
+ cbuf_puts(&t->typedefs, "]; } __ty_");
+ cbuf_put_u64(&t->typedefs, (u64)u);
+ cbuf_puts(&t->typedefs, ";\n");
+ break;
+ }
+ default:
+ compiler_panic(t->c, loc,
+ "C target: c_ensure_typedef on non-composite kind %d",
+ (int)ty->kind);
+ }
+ t->type_state[u] = 2;
+}
+
+static void c_emit_typedef_for_func(CTarget* t, CfreeCgTypeId tid,
+ const CgType* ty) {
+ /* Emit recursively for return and param types if they're composites. */
+ CfreeCgTypeId ret = api_unalias_type(t->c, ty->func.ret);
+ const CgType* rty = cg_type_get(t->c, ret);
+ if (rty &&
+ (rty->kind == CFREE_CG_TYPE_RECORD || rty->kind == CFREE_CG_TYPE_ARRAY ||
+ rty->kind == CFREE_CG_TYPE_FUNC)) {
+ c_ensure_typedef(t, ret);
+ }
+ for (u32 i = 0; i < ty->func.nparams; ++i) {
+ CfreeCgTypeId pt = api_unalias_type(t->c, ty->func.params[i].type);
+ const CgType* pty = cg_type_get(t->c, pt);
+ if (pty &&
+ (pty->kind == CFREE_CG_TYPE_RECORD ||
+ pty->kind == CFREE_CG_TYPE_ARRAY || pty->kind == CFREE_CG_TYPE_FUNC)) {
+ c_ensure_typedef(t, pt);
+ }
+ }
+ cbuf_puts(&t->typedefs, "typedef ");
+ cbuf_puts(&t->typedefs, c_typename(t, ty->func.ret));
+ cbuf_puts(&t->typedefs, " (*__ty_");
+ cbuf_put_u64(&t->typedefs, (u64)tid);
+ cbuf_puts(&t->typedefs, ")(");
+ if (ty->func.nparams == 0 && !ty->func.abi_variadic) {
+ cbuf_puts(&t->typedefs, "void");
+ } else {
+ for (u32 i = 0; i < ty->func.nparams; ++i) {
+ if (i > 0) cbuf_puts(&t->typedefs, ", ");
+ cbuf_puts(&t->typedefs, c_typename(t, ty->func.params[i].type));
+ }
+ if (ty->func.abi_variadic) {
+ if (ty->func.nparams > 0) cbuf_puts(&t->typedefs, ", ");
+ cbuf_puts(&t->typedefs, "...");
+ }
+ }
+ cbuf_puts(&t->typedefs, ");\n");
+}
+
+static const char* c_int_type_for_width_panic(CTarget* t, u32 width,
+ int signed_) {
+ const char* s = c_int_type_name_for_width(width, signed_);
+ if (!s) {
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ compiler_panic(t->c, loc, "C target: int width %u not yet supported",
+ (unsigned)width);
+ }
+ return s;
+}
+
+static const char* c_float_type_name(u32 width) {
+ switch (width) {
+ case 32:
+ return "float";
+ case 64:
+ return "double";
+ case 80:
+ case 128:
+ return "long double";
+ default:
+ return NULL;
+ }
+}
+
+/* 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);
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ if (!ty) {
+ compiler_panic(t->c, loc, "C target: unknown type id %u", (unsigned)type);
+ }
+ switch (ty->kind) {
+ case CFREE_CG_TYPE_VOID:
+ return "void";
+ case CFREE_CG_TYPE_BOOL:
+ return "int32_t";
+ case CFREE_CG_TYPE_INT:
+ return c_int_type_for_width_panic(t, ty->integer.width, 1);
+ case CFREE_CG_TYPE_FLOAT: {
+ const char* s = c_float_type_name(ty->fp.width);
+ if (!s) {
+ compiler_panic(t->c, loc, "C target: fp width %u not yet supported",
+ (unsigned)ty->fp.width);
+ }
+ return s;
+ }
+ case CFREE_CG_TYPE_PTR:
+ return "void*";
+ case CFREE_CG_TYPE_ENUM:
+ /* CG enums are width-only; treat as their underlying integer base. */
+ return c_typename(t, ty->enum_.base);
+ case CFREE_CG_TYPE_VARARG_STATE:
+ t->need_stdarg = 1;
+ return "va_list";
+ case CFREE_CG_TYPE_RECORD:
+ case CFREE_CG_TYPE_ARRAY:
+ case CFREE_CG_TYPE_FUNC:
+ c_ensure_typedef(t, resolved);
+ return c_typedef_name(t, resolved);
+ default:
+ compiler_panic(t->c, loc, "C target: type kind %d not yet supported",
+ (int)ty->kind);
+ }
+}
+
+void c_emit_type(CTarget* t, CBuf* b, CfreeCgTypeId type) {
+ cbuf_puts(b, c_typename(t, type));
+}
+
+static CfreeCgTypeId c_local_type_or_panic(CTarget* t, CGLocal local) {
+ if ((u32)local < t->local_cap && t->local_declared[local] &&
+ t->local_type[local]) {
+ return t->local_type[local];
+ }
+ compiler_panic(t->c, t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0},
+ "C target: unknown local type for v%u", (unsigned)local);
+ return CFREE_CG_TYPE_NONE;
+}
+
+static void c_emit_tuple_type_name(CTarget* t, CBuf* b,
+ const CfreeCgTypeId* types, u32 ntypes) {
+ (void)t;
+ cbuf_puts(b, "__cfree_tuple");
+ cbuf_put_u64(b, (u64)ntypes);
+ for (u32 i = 0; i < ntypes; ++i) {
+ cbuf_putc(b, '_');
+ cbuf_put_u64(b, (u64)api_unalias_type(t->c, types[i]));
+ }
+}
+
+static void c_ensure_tuple_typedef(CTarget* t, const CfreeCgTypeId* types,
+ u32 ntypes) {
+ if (ntypes <= 1) return;
+ cbuf_puts(&t->typedefs, "#ifndef __cfree_tuple_guard_");
+ c_emit_tuple_type_name(t, &t->typedefs, types, ntypes);
+ cbuf_puts(&t->typedefs, "\n#define __cfree_tuple_guard_");
+ c_emit_tuple_type_name(t, &t->typedefs, types, ntypes);
+ cbuf_puts(&t->typedefs, "\ntypedef struct ");
+ c_emit_tuple_type_name(t, &t->typedefs, types, ntypes);
+ cbuf_puts(&t->typedefs, " {");
+ for (u32 i = 0; i < ntypes; ++i) {
+ cbuf_putc(&t->typedefs, ' ');
+ c_emit_type(t, &t->typedefs, types[i]);
+ cbuf_puts(&t->typedefs, " f");
+ cbuf_put_u64(&t->typedefs, (u64)i);
+ cbuf_putc(&t->typedefs, ';');
+ }
+ cbuf_puts(&t->typedefs, " } ");
+ c_emit_tuple_type_name(t, &t->typedefs, types, ntypes);
+ cbuf_puts(&t->typedefs, ";\n#endif\n");
+}
+
+static Operand c_op_local(CGLocal local, CfreeCgTypeId type) {
+ Operand op;
+ memset(&op, 0, sizeof op);
+ op.kind = OPK_LOCAL;
+ op.type = type;
+ op.v.local = local;
+ return op;
+}
+
+void c_local_name(CLocal r, char* out, size_t cap) {
+ size_t i = 0;
+ if (cap == 0) return;
+ if (cap > 1) out[i++] = 'v';
+ char tmp[16];
+ size_t n = 0;
+ u32 v = (u32)r;
+ if (v == 0) {
+ tmp[n++] = '0';
+ } else {
+ while (v) {
+ tmp[n++] = (char)('0' + (v % 10));
+ v /= 10;
+ }
+ }
+ while (n && i + 1 < cap) out[i++] = tmp[--n];
+ out[i] = '\0';
+}
+
+static void c_grow_local_table(CTarget* t, u32 needed) {
+ Heap* h = t->c->ctx->heap;
+ u32 newcap = t->local_cap ? t->local_cap : 16;
+ while (newcap < needed) newcap *= 2;
+ u8* nd = (u8*)h->realloc(h, t->local_declared, t->local_cap, newcap, 1);
+ CfreeCgTypeId* nt = (CfreeCgTypeId*)h->realloc(
+ h, t->local_type, t->local_cap * sizeof(CfreeCgTypeId),
+ newcap * sizeof(CfreeCgTypeId), _Alignof(CfreeCgTypeId));
+ if ((!nd && newcap) || (!nt && newcap)) {
+ compiler_panic(t->c, (SrcLoc){0, 0, 0}, "C target: out of memory");
+ }
+ for (u32 i = t->local_cap; i < newcap; ++i) {
+ nd[i] = 0;
+ nt[i] = 0;
+ }
+ t->local_declared = nd;
+ t->local_type = nt;
+ t->local_cap = newcap;
+}
+
+/* Emit the trailing `__attribute__((unused)) = INIT;` for a local decl of
+ * type `ty`. Scalars get `= 0` (readable); aggregates get `= {0}` (which is
+ * the only form that compiles for record/array). */
+static void c_emit_zero_init(CTarget* t, CfreeCgTypeId ty) {
+ const CgType* cgt = ty ? cg_type_get(t->c, api_unalias_type(t->c, ty)) : NULL;
+ int is_aggregate = cgt && (cgt->kind == CFREE_CG_TYPE_RECORD ||
+ cgt->kind == CFREE_CG_TYPE_ARRAY);
+ cbuf_puts(&t->decls, is_aggregate ? " __attribute__((unused)) = {0};\n"
+ : " __attribute__((unused)) = 0;\n");
+}
+
+void c_ensure_local(CTarget* t, CLocal r, CfreeCgTypeId type) {
+ if (r == (CLocal)CG_LOCAL_NONE) {
+ compiler_panic(t->c, (SrcLoc){0, 0, 0},
+ "C target: CG_LOCAL_NONE reached emission");
+ }
+ if ((u32)r >= t->local_cap) c_grow_local_table(t, (u32)r + 1u);
+ if (t->local_declared[r]) {
+ if (type && api_unalias_type(t->c, t->local_type[r]) !=
+ api_unalias_type(t->c, type)) {
+ compiler_panic(t->c, t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0},
+ "C target: local v%u used with inconsistent type "
+ "(declared %u, used %u)",
+ (unsigned)r,
+ (unsigned)api_unalias_type(t->c, t->local_type[r]),
+ (unsigned)api_unalias_type(t->c, type));
+ }
+ return;
+ }
+ t->local_declared[r] = 1;
+ t->local_type[r] = type;
+ cbuf_puts(&t->decls, " ");
+ c_emit_type(t, &t->decls, type);
+ cbuf_puts(&t->decls, " ");
+ char buf[24];
+ c_local_name(r, buf, sizeof buf);
+ cbuf_puts(&t->decls, buf);
+ /* Zero-init kills -Wsometimes-uninitialized for control flow clang can't
+ * reason through; the host C compiler DSEs the init when a real
+ * assignment dominates. Scalars get `= 0`, aggregates `= {0}`. */
+ c_emit_zero_init(t, type);
+}
+
+/* Emit a signed-int64 literal. INT64_MIN can't be written directly: clang
+ * treats `-9223372036854775808` as `-(9223372036854775808)` with the inner
+ * literal too large for any signed type, which trips
+ * -Wimplicitly-unsigned-literal. The standard workaround is
+ * `(-9223372036854775807LL - 1)`. */
+static void c_emit_imm_literal(CTarget* t, i64 v) {
+ if (v == (i64)((u64)1u << 63u)) {
+ cbuf_puts(&t->body, "(-9223372036854775807LL - 1)");
+ return;
+ }
+ cbuf_put_i64(&t->body, v);
+}
+
+/* Address-mode tuple decoded from an OPK_INDIRECT operand. Mirrors the
+ * `addr_mode` helper in the machine-code backends so all targets share a
+ * single in-backend view of `base [+ index << log2_scale] + ofs`. */
+typedef struct CAddrMode {
+ CLocal base;
+ CLocal index; /* CG_LOCAL_NONE when no index operand */
+ u8 log2_scale; /* meaningful only when index != CG_LOCAL_NONE */
+ i32 ofs;
+} CAddrMode;
+
+static CAddrMode c_addr_mode(Operand addr) {
+ CAddrMode m;
+ m.base = addr.v.ind.base;
+ m.index = addr.v.ind.index;
+ m.log2_scale = addr.v.ind.log2_scale;
+ m.ofs = addr.v.ind.ofs;
+ return m;
+}
+
+/* Emit `(char*)base [+ (uintptr_t)index * (1u << log2_scale)] [+ ofs]` into
+ * the body, with each optional term suppressed when absent. Used by every
+ * OPK_INDIRECT renderer; the caller wraps it with the appropriate
+ * `(*(T*)(...))` or `((T)(...))` cast. */
+static void c_emit_indirect_addr_expr(CTarget* t, CAddrMode m) {
+ char rbuf[24];
+ cbuf_puts(&t->body, "(char*)");
+ c_local_name(m.base, rbuf, sizeof rbuf);
+ cbuf_puts(&t->body, rbuf);
+ if (m.index != CG_LOCAL_NONE) {
+ cbuf_puts(&t->body, " + (uintptr_t)");
+ c_local_name(m.index, rbuf, sizeof rbuf);
+ cbuf_puts(&t->body, rbuf);
+ cbuf_puts(&t->body, " * ");
+ /* Spell as the explicit 1/2/4/8 literal corresponding to log2_scale.
+ * log2_scale is normalized to {0,1,2,3} by cg. */
+ cbuf_put_u64(&t->body, (u64)(1u << m.log2_scale));
+ }
+ if (m.ofs != 0) {
+ cbuf_puts(&t->body, " + ");
+ cbuf_put_i64(&t->body, (i64)m.ofs);
+ }
+}
+
+/* Assert that `addr`, if OPK_INDIRECT, has no index operand. Used by paths
+ * the cg layer guarantees never carry the indexed shape (bitfield, atomics,
+ * copy_bytes/set_bytes, inline asm). */
+static void c_assert_no_index(CTarget* t, Operand addr, const char* where) {
+ if (addr.kind != OPK_INDIRECT) return;
+ if (addr.v.ind.index == CG_LOCAL_NONE) return;
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ compiler_panic(t->c, loc,
+ "C target: %.*s: indexed OPK_INDIRECT not allowed here",
+ SLICE_ARG(slice_from_cstr(where)));
+}
+
+void c_emit_operand(CTarget* t, Operand op) {
+ char buf[24];
+ switch (op.kind) {
+ case OPK_IMM:
+ if (op.type == CFREE_CG_TYPE_NONE) {
+ /* Untyped IMM (e.g. memset byte value): emit the literal raw. */
+ cbuf_putc(&t->body, '(');
+ c_emit_imm_literal(t, op.v.imm);
+ cbuf_putc(&t->body, ')');
+ } else {
+ cbuf_puts(&t->body, "((");
+ c_emit_type(t, &t->body, op.type);
+ cbuf_puts(&t->body, ")");
+ c_emit_imm_literal(t, op.v.imm);
+ cbuf_puts(&t->body, ")");
+ }
+ return;
+ case OPK_LOCAL: {
+ c_ensure_local(t, op.v.local, op.type);
+ c_local_name(op.v.local, buf, sizeof buf);
+ cbuf_puts(&t->body, buf);
+ return;
+ }
+ case OPK_INDIRECT: {
+ /* Used by call paths to pass aggregates by-address: the operand's type
+ * is the aggregate, the storage is `base + index*scale + ofs`. Emit the
+ * deref as a value expression. */
+ cbuf_puts(&t->body, "(*(");
+ c_emit_type(t, &t->body, op.type);
+ cbuf_puts(&t->body, "*)(");
+ c_emit_indirect_addr_expr(t, c_addr_mode(op));
+ cbuf_puts(&t->body, "))");
+ return;
+ }
+ case OPK_GLOBAL: {
+ /* OPK_GLOBAL carries `&sym + addend`. How we spell it depends on
+ * op.type:
+ * - pointer/scalar/void: the value IS the address, so cast through
+ * `((T)((char*)sym + addend))`.
+ * - aggregate (RECORD/ARRAY): the symbol's storage is an aggregate
+ * value; emit `(*(T*)((char*)sym + addend))` so the deref reads
+ * the aggregate value (used by call args that pass struct
+ * by-value via a global initialized buffer). */
+ obj_sym_mark_referenced(t->obj, op.v.global.sym);
+ const char* nm = c_sym_name(t, op.v.global.sym);
+ const CgType* gty =
+ (op.type != CFREE_CG_TYPE_NONE)
+ ? cg_type_get(t->c, api_unalias_type(t->c, op.type))
+ : NULL;
+ int is_aggregate = gty && (gty->kind == CFREE_CG_TYPE_RECORD ||
+ gty->kind == CFREE_CG_TYPE_ARRAY);
+ if (is_aggregate) {
+ cbuf_puts(&t->body, "(*(");
+ c_emit_type(t, &t->body, op.type);
+ 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, "))");
+ } else {
+ 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};
+ compiler_panic(t->c, loc, "C target: operand kind %d not yet supported",
+ (int)op.kind);
+ }
+ }
+}
+
+static int c_type_is_float(CTarget* t, CfreeCgTypeId type) {
+ if (type == CFREE_CG_TYPE_NONE) return 0;
+ const CgType* ty = cg_type_get(t->c, api_unalias_type(t->c, type));
+ return ty && ty->kind == CFREE_CG_TYPE_FLOAT;
+}
+
+/* True iff a and b name the same CG type after alias resolution. */
+static int c_types_equiv(CTarget* t, CfreeCgTypeId a, CfreeCgTypeId b) {
+ if (a == 0 || b == 0) return 0;
+ return api_unalias_type(t->c, a) == api_unalias_type(t->c, b);
+}
+
+/* Emit " vN = " plus any cast needed for a C assignment expression.
+ * Caller must then emit the RHS expression and call c_emit_local_assign_close.
+ *
+ * `rhs_ty` is the CG type the RHS expression will produce (or 0 if unknown).
+ * Pointer/int crossings bridge through uintptr_t to keep host-C diagnostics
+ * quiet. The outer `(...)` parens are kept so the closer's `);` stays
+ * balanced. */
+static void c_emit_local_assign_open(CTarget* t, CLocal r,
+ CfreeCgTypeId rhs_ty) {
+ if ((u32)r >= t->local_cap || !t->local_declared[r]) {
+ compiler_panic(t->c, t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0},
+ "C target: assign to undeclared local v%u", (unsigned)r);
+ }
+ CfreeCgTypeId decl = t->local_type[r];
+ char buf[24];
+ c_local_name(r, buf, sizeof buf);
+ cbuf_puts(&t->body, " ");
+ cbuf_puts(&t->body, buf);
+ cbuf_puts(&t->body, " = ");
+ if (!c_types_equiv(t, rhs_ty, decl)) {
+ cbuf_putc(&t->body, '(');
+ c_emit_type(t, &t->body, decl);
+ cbuf_putc(&t->body, ')');
+ if (!c_type_is_float(t, decl) &&
+ (!rhs_ty || c_type_is_ptr(t, decl) || c_type_is_ptr(t, rhs_ty))) {
+ cbuf_puts(&t->body, "(uintptr_t)");
+ }
+ }
+ cbuf_puts(&t->body, "(");
+}
+
+static void c_emit_local_assign_close(CTarget* t) {
+ cbuf_puts(&t->body, ");\n");
+}
+
+void c_emit_operand_signed(CTarget* t, Operand op, int signed_) {
+ u32 w = c_int_width_for_signedness(t, op.type);
+ if (w == 0) {
+ /* Not an integer — emit without sign cast. */
+ c_emit_operand(t, op);
+ return;
+ }
+ const char* tn = c_int_type_name_for_width(w, signed_);
+ if (!tn) {
+ c_emit_operand(t, op);
+ return;
+ }
+ int via_uptr = c_operand_is_ptr_typed(t, op);
+ /* CG ints are width-only; the C target declares every int local/IMM
+ * as the signed `int{W}_t` of its width. So when `signed_` is true and
+ * the operand's emit-width matches `w`, the explicit cast is redundant
+ * with what c_emit_operand already produces. Skipping it cuts the
+ * ubiquitous `((int32_t)((int32_t)23))` double-cast down to one. */
+ if (!via_uptr && signed_) {
+ CfreeCgTypeId et = op.type;
+ if (c_int_width_for_signedness(t, et) == w) {
+ c_emit_operand(t, op);
+ return;
+ }
+ }
+ cbuf_puts(&t->body, "((");
+ cbuf_puts(&t->body, tn);
+ cbuf_puts(&t->body, ")");
+ if (via_uptr) {
+ cbuf_puts(&t->body, "(uintptr_t)");
+ }
+ c_emit_operand(t, op);
+ cbuf_puts(&t->body, ")");
+}
+
+/* Returns 1 if `type` is a pointer (or void*). */
+static int c_type_is_ptr(CTarget* t, CfreeCgTypeId type) {
+ if (type == CFREE_CG_TYPE_NONE) return 0;
+ const CgType* ty = cg_type_get(t->c, api_unalias_type(t->c, type));
+ return ty && ty->kind == CFREE_CG_TYPE_PTR;
+}
+
+static int c_type_is_bool(CTarget* t, CfreeCgTypeId type) {
+ if (type == CFREE_CG_TYPE_NONE) return 0;
+ const CgType* ty = cg_type_get(t->c, api_unalias_type(t->c, type));
+ return ty && ty->kind == CFREE_CG_TYPE_BOOL;
+}
+
+static int c_type_is_aggregate(CTarget* t, CfreeCgTypeId type) {
+ if (type == CFREE_CG_TYPE_NONE) return 0;
+ const CgType* ty = cg_type_get(t->c, api_unalias_type(t->c, type));
+ return ty &&
+ (ty->kind == CFREE_CG_TYPE_RECORD || ty->kind == CFREE_CG_TYPE_ARRAY);
+}
+
+static int c_operand_is_ptr_typed(CTarget* t, Operand op) {
+ if (c_type_is_ptr(t, op.type)) return 1;
+ return 0;
+}
+
+/* Emit `(target_ty)(uintptr_t)(op)` (or `(target_ty)(op)` for float
+ * target_ty). Used when the caller needs a specific C expression type.
+ * Pointer/int crossings bridge through uintptr_t. */
+static void c_emit_operand_as(CTarget* t, Operand op, CfreeCgTypeId target_ty) {
+ if (c_types_equiv(t, op.type, target_ty)) {
+ c_emit_operand(t, op);
+ return;
+ }
+ cbuf_puts(&t->body, "(");
+ c_emit_type(t, &t->body, target_ty);
+ cbuf_puts(&t->body, ")");
+ if (!c_type_is_float(t, target_ty) &&
+ (!op.type || c_type_is_ptr(t, op.type) || c_type_is_ptr(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). */
+static void c_emit_operand_arith(CTarget* t, Operand op) {
+ if (c_operand_is_ptr_typed(t, op)) {
+ cbuf_puts(&t->body, "((uintptr_t)");
+ if (op.kind == OPK_IMM) {
+ c_emit_imm_literal(t, op.v.imm);
+ } else {
+ c_emit_operand(t, op);
+ }
+ cbuf_puts(&t->body, ")");
+ return;
+ }
+ c_emit_operand(t, op);
+}
+
+/* Same, but applies the requested signedness when the operand is an integer
+ * (used for SDIV/UDIV/SREM/UREM/SHR_S/SHR_U). Pointer operands always go
+ * through the uintptr_t cast regardless of the requested signedness. */
+static void c_emit_operand_arith_signed(CTarget* t, Operand op, int signed_) {
+ if (c_operand_is_ptr_typed(t, op)) {
+ cbuf_puts(&t->body, "((uintptr_t)");
+ if (op.kind == OPK_IMM) {
+ c_emit_imm_literal(t, op.v.imm);
+ } else {
+ c_emit_operand(t, op);
+ }
+ cbuf_puts(&t->body, ")");
+ return;
+ }
+ c_emit_operand_signed(t, op, signed_);
+}
+
+/* Emit a C lvalue expression for an addr operand (OPK_LOCAL / OPK_GLOBAL /
+ * OPK_INDIRECT) using `access_type` as the access type. The result is the
+ * full `*(T*)(...)` dereference, or the C variable directly when the access
+ * type matches the underlying local/global object. */
+static void c_emit_addr_deref(CTarget* t, Operand addr,
+ CfreeCgTypeId access_type) {
+ char buf[24];
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ switch (addr.kind) {
+ case OPK_LOCAL: {
+ c_ensure_local(t, addr.v.local, addr.type);
+ c_local_name(addr.v.local, buf, sizeof buf);
+ if (access_type == 0 || addr.type == 0 ||
+ api_unalias_type(t->c, access_type) ==
+ api_unalias_type(t->c, addr.type)) {
+ cbuf_puts(&t->body, buf);
+ } else {
+ cbuf_puts(&t->body, "(*(");
+ c_emit_type(t, &t->body, access_type);
+ cbuf_puts(&t->body, "*)&");
+ cbuf_puts(&t->body, buf);
+ cbuf_puts(&t->body, ")");
+ }
+ return;
+ }
+ case OPK_GLOBAL: {
+ obj_sym_mark_referenced(t->obj, addr.v.global.sym);
+ 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: {
+ CAddrMode m = c_addr_mode(addr);
+ if ((u32)m.base >= t->local_cap || !t->local_declared[m.base]) {
+ compiler_panic(t->c, loc,
+ "C target: indirect on undeclared base local v%u",
+ (unsigned)m.base);
+ }
+ if (m.index != CG_LOCAL_NONE &&
+ ((u32)m.index >= t->local_cap || !t->local_declared[m.index])) {
+ compiler_panic(t->c, loc,
+ "C target: indirect on undeclared index local v%u",
+ (unsigned)m.index);
+ }
+ cbuf_puts(&t->body, "(*(");
+ c_emit_type(t, &t->body, access_type);
+ cbuf_puts(&t->body, "*)(");
+ c_emit_indirect_addr_expr(t, m);
+ cbuf_puts(&t->body, "))");
+ return;
+ }
+ default:
+ compiler_panic(t->c, loc,
+ "C target: addr-deref on operand kind %d not supported",
+ (int)addr.kind);
+ }
+}
+
+/* Emit a C address-of expression for a lvalue operand. Output is a pointer
+ * value (cast to dst_type). */
+static void c_emit_lvalue_addr(CTarget* t, Operand lv, CfreeCgTypeId dst_type) {
+ char buf[24];
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ switch (lv.kind) {
+ case OPK_LOCAL:
+ cbuf_puts(&t->body, "((");
+ c_emit_type(t, &t->body, dst_type);
+ cbuf_puts(&t->body, ")");
+ cbuf_puts(&t->body, "&");
+ c_ensure_local(t, lv.v.local, lv.type);
+ c_local_name(lv.v.local, buf, sizeof buf);
+ cbuf_puts(&t->body, buf);
+ cbuf_puts(&t->body, ")");
+ return;
+ case OPK_GLOBAL: {
+ obj_sym_mark_referenced(t->obj, lv.v.global.sym);
+ 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, "((");
+ c_emit_type(t, &t->body, dst_type);
+ cbuf_puts(&t->body, ")(");
+ c_emit_indirect_addr_expr(t, c_addr_mode(lv));
+ cbuf_puts(&t->body, "))");
+ return;
+ }
+ default:
+ compiler_panic(t->c, loc,
+ "C target: addr-of on operand kind %d not supported",
+ (int)lv.kind);
+ }
+}
+
+/* === Symbol name lookup === */
+
+const char* c_sym_name(CTarget* t, ObjSymId sym) {
+ const ObjSym* os = obj_symbol_get(t->obj, sym);
+ if (!os) {
+ compiler_panic(t->c, (SrcLoc){0, 0, 0}, "C target: unknown ObjSymId %u",
+ (unsigned)sym);
+ }
+ Slice nm = pool_slice(t->c->global, os->name);
+ const char* s = nm.s;
+ size_t n = nm.len;
+ /* 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 && 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_slice(t->c->global, (Slice){.s = buf, .len = out});
+ return pool_slice(t->c->global, interned).s;
+}
+
+/* === Prologue / finalize === */
+
+void c_emit_prologue(CTarget* t) {
+ if (t->prologue_emitted) return;
+ t->prologue_emitted = 1;
+ c_writer_puts(t,
+ "/* generated by cfree --emit=c */\n"
+ "#include <stdint.h>\n"
+ "#include <stdalign.h>\n");
+ /* Other headers are decided at finalize so include lines remain
+ * deterministic regardless of when the type was first referenced.
+ * Writer flushes are not stream-buffered, so we keep prologue compact and
+ * tack the rest on at c_emit_finalize. */
+ c_writer_puts(t, "\n");
+}
+
+/* === func_begin / func_end === */
+
+/* Write `RetT name(P0, P1, ...)` (without trailing `;` or `{`) to `b`. */
+static void c_emit_func_signature(CTarget* t, CBuf* b, const char* name,
+ CfreeCgTypeId fn_type,
+ const CfreeCgTypeId* result_types,
+ u32 nresults) {
+ CfreeCgTypeId ret_type = cg_type_func_ret_id(t->c, fn_type);
+ const CgType* fty = cg_type_get(t->c, api_unalias_type(t->c, fn_type));
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ if (!fty || fty->kind != CFREE_CG_TYPE_FUNC) {
+ compiler_panic(t->c, loc, "C target: fn_type is not a function type");
+ }
+ if (!result_types) {
+ nresults = cg_type_is_void(t->c, ret_type) ? 0u : 1u;
+ result_types = &ret_type;
+ }
+ if (nresults == 0) {
+ cbuf_puts(b, "void");
+ } else if (nresults == 1) {
+ c_emit_type(t, b, result_types[0]);
+ } else {
+ c_ensure_tuple_typedef(t, result_types, nresults);
+ c_emit_tuple_type_name(t, b, result_types, nresults);
+ }
+ cbuf_puts(b, " ");
+ cbuf_puts(b, name);
+ cbuf_puts(b, "(");
+ if (fty->func.nparams == 0 && !fty->func.abi_variadic) {
+ cbuf_puts(b, "void");
+ } else {
+ for (u32 i = 0; i < fty->func.nparams; ++i) {
+ if (i > 0) cbuf_puts(b, ", ");
+ c_emit_type(t, b, fty->func.params[i].type);
+ cbuf_puts(b, " p");
+ cbuf_put_u64(b, (u64)i);
+ }
+ if (fty->func.abi_variadic) {
+ if (fty->func.nparams > 0) cbuf_puts(b, ", ");
+ cbuf_puts(b, "...");
+ }
+ }
+ cbuf_puts(b, ")");
+}
+
+void c_emit_func_begin(CTarget* t, const CGFuncDesc* fd) {
+ c_emit_prologue(t);
+
+ t->cur_fn = fd;
+ cbuf_reset(&t->decls);
+ for (u32 i = 0; i < t->local_cap; ++i) {
+ t->local_declared[i] = 0;
+ t->local_type[i] = 0;
+ }
+ t->next_label = 0;
+ t->next_local = 0;
+ t->next_tmp = 0;
+ t->nscopes = 0;
+ t->last_was_terminator = 0;
+ t->have_emitted_loc = 0;
+ t->emitted_loc = (SrcLoc){0, 0, 0};
+
+ const char* name = c_sym_name(t, fd->sym);
+
+ /* Forward-declare so out-of-order callers and same-TU references find the
+ * prototype regardless of definition order. */
+ c_ensure_forward_decl_with_results(t, fd->sym, fd->fn_type, fd->result_types,
+ fd->nresults);
+
+ c_emit_func_signature(t, &t->body, name, fd->fn_type, fd->result_types,
+ fd->nresults);
+ cbuf_puts(&t->body, " {\n");
+ t->fn_body_start = t->body.len;
+}
+
+static void c_ensure_forward_decl_with_results(
+ CTarget* t, ObjSymId sym, CfreeCgTypeId fn_type,
+ const CfreeCgTypeId* result_types, u32 nresults) {
+ Heap* h = t->c->ctx->heap;
+ if ((u32)sym >= t->sym_forwarded_cap) {
+ u32 newcap = t->sym_forwarded_cap ? t->sym_forwarded_cap : 16;
+ while (newcap <= (u32)sym) newcap *= 2;
+ u8* nd =
+ (u8*)h->realloc(h, t->sym_forwarded, t->sym_forwarded_cap, newcap, 1);
+ if (!nd && newcap) {
+ compiler_panic(t->c, (SrcLoc){0, 0, 0}, "C target: out of memory");
+ }
+ for (u32 i = t->sym_forwarded_cap; i < newcap; ++i) nd[i] = 0;
+ t->sym_forwarded = nd;
+ t->sym_forwarded_cap = newcap;
+ }
+ if (t->sym_forwarded[sym]) return;
+ t->sym_forwarded[sym] = 1;
+ const char* name = c_sym_name(t, sym);
+ const ObjSym* os = obj_symbol_get(t->obj, sym);
+ if ((os && (os->kind == SK_FUNC || os->kind == SK_IFUNC)) || fn_type != 0) {
+ c_emit_func_signature(t, &t->forwards, name, fn_type, result_types,
+ nresults);
+ cbuf_puts(&t->forwards, ";\n");
+ } else {
+ if (os && os->bind == SB_LOCAL)
+ cbuf_puts(&t->forwards, "static ");
+ else
+ cbuf_puts(&t->forwards, "extern ");
+ if (os && os->section_id != OBJ_SEC_NONE) {
+ const Section* sec = obj_section_get(t->obj, os->section_id);
+ if (sec->kind == SEC_RODATA) cbuf_puts(&t->forwards, "const ");
+ }
+ cbuf_puts(&t->forwards, "struct __cfree_data_");
+ cbuf_puts(&t->forwards, name);
+ cbuf_puts(&t->forwards, " ");
+ cbuf_puts(&t->forwards, name);
+ cbuf_puts(&t->forwards, ";\n");
+ }
+}
+
+void c_ensure_forward_decl(CTarget* t, ObjSymId sym, CfreeCgTypeId fn_type) {
+ c_ensure_forward_decl_with_results(t, sym, fn_type, NULL, 0);
+}
+
+void c_emit_func_end(CTarget* t) {
+ size_t splice_at = t->fn_body_start;
+ size_t body_after = t->body.len;
+ size_t fn_body_len = body_after - splice_at;
+ Heap* h = t->c->ctx->heap;
+
+ u8* tmp = NULL;
+ if (fn_body_len) {
+ tmp = (u8*)h->alloc(h, fn_body_len, 1);
+ if (!tmp) {
+ compiler_panic(t->c, t->cur_fn->loc, "C target: out of memory");
+ }
+ for (size_t i = 0; i < fn_body_len; ++i) {
+ tmp[i] = t->body.data[splice_at + i];
+ }
+ }
+
+ t->body.len = splice_at;
+ if (t->decls.len)
+ cbuf_putn(&t->body, (const char*)t->decls.data, t->decls.len);
+ if (tmp) {
+ cbuf_putn(&t->body, (const char*)tmp, fn_body_len);
+ h->free(h, tmp, fn_body_len);
+ }
+ cbuf_puts(&t->body, "}\n\n");
+
+ t->cur_fn = NULL;
+}
+
+/* === locals, params === */
+
+void c_emit_param_bind(CTarget* t, CGLocal local, CfreeCgTypeId type,
+ u32 index) {
+ char buf[24];
+ c_ensure_local(t, local, type);
+ c_local_name(local, buf, sizeof buf);
+ cbuf_puts(&t->body, " ");
+ cbuf_puts(&t->body, buf);
+ cbuf_puts(&t->body, " = p");
+ cbuf_put_u64(&t->body, (u64)index);
+ cbuf_puts(&t->body, ";\n");
+}
+
+CGLocal c_emit_param(CTarget* t, const CGParamDesc* pd) {
+ CGLocalDesc d;
+ memset(&d, 0, sizeof d);
+ d.type = pd->type;
+ d.name = pd->name;
+ d.loc = pd->loc;
+ d.size = pd->size;
+ d.align = pd->align;
+ d.flags = pd->flags;
+ CGLocal local = c_emit_local(t, &d);
+ c_emit_param_bind(t, local, pd->type, pd->index);
+ return local;
+}
+
+/* === load_imm, copy, binop === */
+
+void c_emit_load_imm(CTarget* t, Operand dst, i64 imm) {
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ if (dst.kind != OPK_LOCAL) {
+ compiler_panic(t->c, loc, "C target: load_imm dst must be LOCAL");
+ }
+ c_ensure_local(t, dst.v.local, dst.type);
+ /* The literal is emitted bare; its C type is `long long`. We can drop
+ * the bridge cast iff the bare assignment compiles cleanly:
+ * - integer dst: imm must fit in dst's signed range (else
+ * -Wconstant-conversion). 64-bit dst always fits.
+ * - pointer dst: only `0` (null pointer constant) is safe; any other
+ * literal trips -Wint-conversion.
+ * Otherwise keep the bridge. */
+ u32 w = c_int_width_for_signedness(t, dst.type);
+ int can_drop_bridge;
+ if (w > 0) {
+ can_drop_bridge = (w >= 64) || (imm >= -((i64)1 << (w - 1)) &&
+ imm <= (((i64)1 << (w - 1)) - 1));
+ } else if (c_type_is_ptr(t, dst.type)) {
+ can_drop_bridge = (imm == 0);
+ } else {
+ can_drop_bridge = 0;
+ }
+ c_emit_local_assign_open(t, dst.v.local,
+ can_drop_bridge ? dst.type : (CfreeCgTypeId)0);
+ c_emit_imm_literal(t, imm);
+ c_emit_local_assign_close(t);
+}
+
+void c_emit_copy(CTarget* t, Operand dst, Operand src) {
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ if (dst.kind != OPK_LOCAL) {
+ compiler_panic(t->c, loc, "C target: copy dst must be LOCAL");
+ }
+ c_ensure_local(t, dst.v.local, dst.type);
+ c_emit_local_assign_open(t, dst.v.local, src.type);
+ c_emit_operand(t, src);
+ c_emit_local_assign_close(t);
+}
+
+static const char* binop_to_c(BinOp op) {
+ switch (op) {
+ case BO_IADD:
+ case BO_FADD:
+ return "+";
+ case BO_ISUB:
+ case BO_FSUB:
+ return "-";
+ case BO_IMUL:
+ case BO_FMUL:
+ return "*";
+ case BO_SDIV:
+ case BO_UDIV:
+ case BO_FDIV:
+ return "/";
+ case BO_SREM:
+ case BO_UREM:
+ return "%";
+ case BO_AND:
+ return "&";
+ case BO_OR:
+ return "|";
+ case BO_XOR:
+ return "^";
+ case BO_SHL:
+ return "<<";
+ case BO_SHR_S:
+ case BO_SHR_U:
+ return ">>";
+ }
+ return NULL;
+}
+
+/* For BinOp `op`, decide how to sign-cast the operands. Returns 0 for "no
+ * cast", 1 for "cast both to signed", 2 for "cast both to unsigned", 3 for
+ * "cast lhs only (signedness `lhs_signed`)" (used for shifts). */
+typedef enum { BSC_NONE, BSC_SIGNED, BSC_UNSIGNED, BSC_SHIFT_LHS } BinSignCast;
+
+static BinSignCast binop_sign_kind(BinOp op, int* lhs_signed_out) {
+ *lhs_signed_out = 1;
+ switch (op) {
+ case BO_SDIV:
+ case BO_SREM:
+ return BSC_SIGNED;
+ case BO_UDIV:
+ case BO_UREM:
+ return BSC_UNSIGNED;
+ case BO_SHR_S:
+ *lhs_signed_out = 1;
+ return BSC_SHIFT_LHS;
+ case BO_SHR_U:
+ *lhs_signed_out = 0;
+ return BSC_SHIFT_LHS;
+ default:
+ return BSC_NONE;
+ }
+}
+
+void c_emit_binop(CTarget* t, BinOp op, Operand dst, Operand a, Operand b) {
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ const char* sym = binop_to_c(op);
+ if (!sym) {
+ compiler_panic(t->c, loc, "C target: unknown binop %d", (int)op);
+ }
+ if (dst.kind != OPK_LOCAL) {
+ compiler_panic(t->c, loc, "C target: binop dst must be LOCAL");
+ }
+ c_ensure_local(t, dst.v.local, dst.type);
+ /* Pointer operands get cast to uintptr_t inside c_emit_operand_arith,
+ * so the binop's C result type is `uintptr_t`, not the original pointer
+ * type. Keep the bridge when dst or either operand is pointer-typed so
+ * the assignment back to a pointer dst doesn't trip -Wint-conversion. */
+ int has_ptr = c_operand_is_ptr_typed(t, dst) ||
+ c_operand_is_ptr_typed(t, a) || c_operand_is_ptr_typed(t, b);
+ c_emit_local_assign_open(t, dst.v.local,
+ has_ptr ? (CfreeCgTypeId)0 : dst.type);
+ int lhs_signed = 1;
+ BinSignCast bsc = binop_sign_kind(op, &lhs_signed);
+ switch (bsc) {
+ case BSC_NONE:
+ c_emit_operand_arith(t, a);
+ cbuf_puts(&t->body, " ");
+ cbuf_puts(&t->body, sym);
+ cbuf_puts(&t->body, " ");
+ c_emit_operand_arith(t, b);
+ break;
+ case BSC_SIGNED:
+ c_emit_operand_arith_signed(t, a, 1);
+ cbuf_puts(&t->body, " ");
+ cbuf_puts(&t->body, sym);
+ cbuf_puts(&t->body, " ");
+ c_emit_operand_arith_signed(t, b, 1);
+ break;
+ case BSC_UNSIGNED:
+ c_emit_operand_arith_signed(t, a, 0);
+ cbuf_puts(&t->body, " ");
+ cbuf_puts(&t->body, sym);
+ cbuf_puts(&t->body, " ");
+ c_emit_operand_arith_signed(t, b, 0);
+ break;
+ case BSC_SHIFT_LHS:
+ c_emit_operand_arith_signed(t, a, lhs_signed);
+ cbuf_puts(&t->body, " ");
+ cbuf_puts(&t->body, sym);
+ cbuf_puts(&t->body, " ");
+ c_emit_operand(t, b);
+ break;
+ }
+ c_emit_local_assign_close(t);
+}
+
+/* ===== unop ===== */
+
+void c_emit_unop(CTarget* t, UnOp op, Operand dst, Operand a) {
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ if (dst.kind != OPK_LOCAL) {
+ compiler_panic(t->c, loc, "C target: unop dst must be LOCAL");
+ }
+ c_ensure_local(t, dst.v.local, dst.type);
+ const char* sym = NULL;
+ switch (op) {
+ case UO_NEG:
+ case UO_FNEG:
+ sym = "-";
+ break;
+ case UO_NOT:
+ sym = "!";
+ break;
+ case UO_BNOT:
+ sym = "~";
+ break;
+ default:
+ compiler_panic(t->c, loc, "C target: unknown unop %d", (int)op);
+ }
+ c_emit_local_assign_open(t, dst.v.local, dst.type);
+ cbuf_puts(&t->body, sym);
+ c_emit_operand(t, a);
+ c_emit_local_assign_close(t);
+}
+
+/* ===== compare ops ===== */
+
+static const char* cmp_to_c(CmpOp op) {
+ switch (op) {
+ case CMP_EQ:
+ return "==";
+ case CMP_NE:
+ return "!=";
+ case CMP_LT_S:
+ case CMP_LT_U:
+ case CMP_LT_F:
+ return "<";
+ case CMP_LE_S:
+ case CMP_LE_U:
+ case CMP_LE_F:
+ return "<=";
+ case CMP_GT_S:
+ case CMP_GT_U:
+ case CMP_GT_F:
+ return ">";
+ case CMP_GE_S:
+ case CMP_GE_U:
+ case CMP_GE_F:
+ return ">=";
+ }
+ return NULL;
+}
+
+/* Returns 1 if cmp op needs unsigned operand cast. -1 if signed. 0 if no cast
+ * (EQ/NE — sign doesn't matter for integer equality at the same width — and
+ * float compares). */
+static int cmp_signedness(CmpOp op) {
+ switch (op) {
+ case CMP_LT_S:
+ case CMP_LE_S:
+ case CMP_GT_S:
+ case CMP_GE_S:
+ return -1;
+ case CMP_LT_U:
+ case CMP_LE_U:
+ case CMP_GT_U:
+ case CMP_GE_U:
+ return 1;
+ default:
+ return 0;
+ }
+}
+
+static void c_emit_cmp_operands(CTarget* t, CmpOp op, Operand a, Operand b) {
+ int sg = cmp_signedness(op);
+ if (sg == 0) {
+ c_emit_operand_arith(t, a);
+ cbuf_puts(&t->body, " ");
+ cbuf_puts(&t->body, cmp_to_c(op));
+ cbuf_puts(&t->body, " ");
+ c_emit_operand_arith(t, b);
+ } else {
+ int signed_ = (sg < 0);
+ c_emit_operand_arith_signed(t, a, signed_);
+ cbuf_puts(&t->body, " ");
+ cbuf_puts(&t->body, cmp_to_c(op));
+ cbuf_puts(&t->body, " ");
+ c_emit_operand_arith_signed(t, b, signed_);
+ }
+}
+
+void c_emit_cmp(CTarget* t, CmpOp op, Operand dst, Operand a, Operand b) {
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ if (dst.kind != OPK_LOCAL) {
+ compiler_panic(t->c, loc, "C target: cmp dst must be LOCAL");
+ }
+ if (!cmp_to_c(op)) {
+ compiler_panic(t->c, loc, "C target: unknown cmp %d", (int)op);
+ }
+ c_ensure_local(t, dst.v.local, dst.type);
+ /* Compare result is C `int` (0/1); assigning to integer dst.type narrows
+ * implicitly without -Wall complaint. */
+ c_emit_local_assign_open(t, dst.v.local, dst.type);
+ c_emit_cmp_operands(t, op, a, b);
+ c_emit_local_assign_close(t);
+}
+
+/* ===== labels, jump, cmp_branch ===== */
+
+static void c_label_name(Label l, char* out, size_t cap) {
+ size_t i = 0;
+ if (cap == 0) return;
+ const char* p = "L";
+ while (*p && i + 1 < cap) out[i++] = *p++;
+ char tmp[16];
+ size_t n = 0;
+ u32 v = (u32)l;
+ if (v == 0) {
+ tmp[n++] = '0';
+ } else {
+ while (v) {
+ tmp[n++] = (char)('0' + (v % 10));
+ v /= 10;
+ }
+ }
+ while (n && i + 1 < cap) out[i++] = tmp[--n];
+ out[i] = '\0';
+}
+
+Label c_emit_label_new(CTarget* t) {
+ t->next_label += 1;
+ return (Label)t->next_label;
+}
+
+void c_emit_label_place(CTarget* t, Label l) {
+ char buf[24];
+ c_label_name(l, buf, sizeof buf);
+ /* `Lk: __attribute__((unused));` — empty stmt keeps it valid at end-of-block,
+ * and the attribute silences -Wunused-label when the goto got folded away. */
+ cbuf_puts(&t->body, " ");
+ cbuf_puts(&t->body, buf);
+ cbuf_puts(&t->body, ": __attribute__((unused));\n");
+ t->last_was_terminator = 0;
+}
+
+/* If `l` is the innermost structured scope's break/continue label, return
+ * the C keyword that exits/iterates that scope (a literal `break` or
+ * `continue`). NULL means "fall back to goto." Matches only the innermost
+ * scope because C `break`/`continue` only escape the nearest enclosing
+ * loop/switch — outer-scope targets must stay as goto. */
+static const char* c_scope_kw_for_label(CTarget* t, Label l) {
+ if (t->nscopes == 0) return NULL;
+ const CScopeInfo* s = &t->scopes[t->nscopes - 1u];
+ if (!s->structured) return NULL;
+ if (l == s->break_label) return "break";
+ if (l == s->continue_label) return "continue";
+ return NULL;
+}
+
+void c_emit_jump(CTarget* t, Label l) {
+ if (t->last_was_terminator) return;
+ const char* kw = c_scope_kw_for_label(t, l);
+ if (kw) {
+ cbuf_puts(&t->body, " ");
+ cbuf_puts(&t->body, kw);
+ cbuf_puts(&t->body, ";\n");
+ } else {
+ char buf[24];
+ c_label_name(l, buf, sizeof buf);
+ cbuf_puts(&t->body, " goto ");
+ cbuf_puts(&t->body, buf);
+ cbuf_puts(&t->body, ";\n");
+ }
+ t->last_was_terminator = 1;
+}
+
+void c_emit_cmp_branch(CTarget* t, CmpOp op, Operand a, Operand b, Label l) {
+ if (t->last_was_terminator) return;
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ if (!cmp_to_c(op)) {
+ compiler_panic(t->c, loc, "C target: unknown cmp %d", (int)op);
+ }
+ const char* kw = c_scope_kw_for_label(t, l);
+ cbuf_puts(&t->body, " if (");
+ c_emit_cmp_operands(t, op, a, b);
+ if (kw) {
+ cbuf_puts(&t->body, ") ");
+ cbuf_puts(&t->body, kw);
+ cbuf_puts(&t->body, ";\n");
+ } else {
+ char buf[24];
+ c_label_name(l, buf, sizeof buf);
+ cbuf_puts(&t->body, ") goto ");
+ cbuf_puts(&t->body, buf);
+ cbuf_puts(&t->body, ";\n");
+ }
+}
+
+/* ===== scopes =====
+ *
+ * SCOPE_LOOP maps to C's `for (;;) { ... }`. CG places the continue label
+ * just before `scope_begin` and the break label just before `scope_end`
+ * (see src/cg/control.c:208,253). The C target leaves those label
+ * placements in the body — they sit just before `for (;;) {` and just
+ * after `}` respectively, so any outer-scope `goto continue_lbl` or
+ * `goto break_lbl` (e.g. a nested loop's `continue` targeting this
+ * outer loop) still resolves. Inside the `for` body, `c_jump` and
+ * `c_cmp_branch` translate jumps whose target is the *innermost* scope's
+ * break/continue label into `break;` / `continue;`; outer-scope targets
+ * fall back to `goto`. The redundant `Lk: ;` adjacent to the `for` is
+ * cosmetic; gcc/clang fold it. */
+
+static void c_grow_scopes(CTarget* t, u32 needed) {
+ Heap* h = t->c->ctx->heap;
+ u32 newcap = t->scopes_cap ? t->scopes_cap : 8;
+ while (newcap < needed) newcap *= 2;
+ CScopeInfo* ns = (CScopeInfo*)h->realloc(
+ h, t->scopes, t->scopes_cap * sizeof(CScopeInfo),
+ newcap * sizeof(CScopeInfo), _Alignof(CScopeInfo));
+ if (!ns && newcap) {
+ compiler_panic(t->c, (SrcLoc){0, 0, 0}, "C target: out of memory");
+ }
+ t->scopes = ns;
+ t->scopes_cap = newcap;
+}
+
+CGScope c_emit_scope_begin(CTarget* t, const CGScopeDesc* d) {
+ if (t->nscopes + 1u >= t->scopes_cap) c_grow_scopes(t, t->nscopes + 2u);
+ u32 idx = t->nscopes;
+ t->scopes[idx].kind = d->kind;
+ t->scopes[idx].break_label = d->break_label;
+ t->scopes[idx].continue_label = d->continue_label;
+ t->scopes[idx].structured = 0;
+ t->nscopes += 1u;
+ if (d->kind == SCOPE_LOOP) {
+ cbuf_puts(&t->body, " for (;;) {\n");
+ t->scopes[idx].structured = 1;
+ t->last_was_terminator = 0;
+ return (CGScope)(idx + 1u);
+ }
+ /* SCOPE_IF carries a cond consumed here. The public CfreeCg API always
+ * emits SCOPE_LOOP, so this branch only fires for internal callers. */
+ if (d->kind == SCOPE_IF) {
+ char buf[24];
+ c_label_name(d->break_label, buf, sizeof buf);
+ cbuf_puts(&t->body, " if (!(");
+ c_emit_operand(t, d->cond);
+ cbuf_puts(&t->body, ")) goto ");
+ cbuf_puts(&t->body, buf);
+ cbuf_puts(&t->body, ";\n");
+ }
+ return (CGScope)(idx + 1u);
+}
+
+void c_emit_scope_else(CTarget* t, CGScope s) {
+ (void)t;
+ (void)s;
+ /* Public API doesn't emit SCOPE_IF; if it ever does, the frontend is
+ * responsible for placing the else label and the break_label itself. */
+}
+
+void c_emit_scope_end(CTarget* t, CGScope s) {
+ if (s == 0 || (u32)s > t->nscopes) {
+ compiler_panic(t->c, t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0},
+ "C target: scope_end on invalid handle");
+ }
+ u32 idx = (u32)s - 1u;
+ if (t->scopes[idx].structured) {
+ /* CG places break_label just before scope_end, so the label sits
+ * inside the for-body. Anything that lands on it (including a
+ * `goto break_lbl` from a nested scope's labeled break) needs to
+ * exit the for — without an explicit `break;`, fall-through would
+ * iterate again. Always emit; if the body already terminated the
+ * defensive break is dead but harmless. */
+ cbuf_puts(&t->body, " break;\n");
+ cbuf_puts(&t->body, " }\n");
+ /* The closing brace is not a terminator; control can fall through it
+ * (e.g., off the end of a void function). */
+ t->last_was_terminator = 0;
+ }
+ t->nscopes -= 1u;
+}
+
+void c_emit_break_to(CTarget* t, CGScope s) {
+ if (s == 0 || (u32)s > t->nscopes) {
+ compiler_panic(t->c, t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0},
+ "C target: break_to on invalid handle");
+ }
+ c_emit_jump(t, t->scopes[s - 1u].break_label);
+}
+
+void c_emit_continue_to(CTarget* t, CGScope s) {
+ if (s == 0 || (u32)s > t->nscopes) {
+ compiler_panic(t->c, t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0},
+ "C target: continue_to on invalid handle");
+ }
+ c_emit_jump(t, t->scopes[s - 1u].continue_label);
+}
+
+/* ===== switch dispatch ===== */
+
+/* Emit `case <value>:`. For an int32_t selector the bare literal is
+ * already the right type, so we skip the cast; for wider/narrower
+ * integers we wrap in `(T)` so the case constant matches the switch
+ * value's promoted type (avoids -Wswitch warnings on narrower
+ * selectors). */
+static void c_emit_case_value(CTarget* t, CfreeCgTypeId sel_ty, u64 v) {
+ u32 w = c_int_width_for_signedness(t, sel_ty);
+ cbuf_puts(&t->body, " case ");
+ if (w != 0 && w != 32) {
+ cbuf_putc(&t->body, '(');
+ c_emit_type(t, &t->body, sel_ty);
+ cbuf_puts(&t->body, ")");
+ }
+ c_emit_imm_literal(t, (i64)v);
+ cbuf_puts(&t->body, ":");
+}
+
+void c_emit_switch_(
+ CTarget* t, const CGSwitchDesc* d) { /* gcc/clang ignore strategy hints and
+ pick their own dispatch shape. */
+ (void)d->hint;
+ if (t->last_was_terminator) return;
+ cbuf_puts(&t->body, " switch (");
+ c_emit_operand(t, d->selector);
+ cbuf_puts(&t->body, ") {\n");
+ for (u32 i = 0; i < d->ncases; ++i) {
+ char buf[24];
+ c_label_name(d->cases[i].label, buf, sizeof buf);
+ c_emit_case_value(t, d->selector.type, d->cases[i].value);
+ cbuf_puts(&t->body, " goto ");
+ cbuf_puts(&t->body, buf);
+ cbuf_puts(&t->body, ";\n");
+ }
+ cbuf_puts(&t->body, " default: ");
+ if (d->default_label != (Label)LABEL_NONE) {
+ char buf[24];
+ c_label_name(d->default_label, buf, sizeof buf);
+ cbuf_puts(&t->body, "goto ");
+ cbuf_puts(&t->body, buf);
+ cbuf_puts(&t->body, ";\n");
+ } else {
+ /* No default supplied — the cfree IR's contract for that case is
+ * "if no case matches, fall through." `break;` does exactly that
+ * inside the for-wrapper around structured scopes. */
+ cbuf_puts(&t->body, "break;\n");
+ }
+ cbuf_puts(&t->body, " }\n");
+ /* The switch always transfers control (every arm jumps or breaks).
+ * Mark as terminator so any frontend-emitted defensive jump after
+ * dispatch is dropped. */
+ t->last_was_terminator = 1;
+}
+
+/* ===== load_label_addr / indirect_branch =====
+ * GCC computed-goto extension: `&&L` is the address of label L within
+ * the current function, and `goto *p;` jumps to such an address. This
+ * is the lowering every cc1-like backend uses (and what the toy
+ * frontend ultimately compiles to via the C target). */
+void c_emit_load_label_addr(CTarget* t, Operand dst, Label l) {
+ char buf[24];
+ if (dst.kind != OPK_LOCAL) {
+ compiler_panic(t->c, t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0},
+ "C target: load_label_addr dst must be LOCAL");
+ }
+ c_ensure_local(t, dst.v.local, dst.type);
+ c_emit_local_assign_open(t, dst.v.local, (CfreeCgTypeId)0);
+ cbuf_puts(&t->body, "(void*)&&");
+ c_label_name(l, buf, sizeof buf);
+ cbuf_puts(&t->body, buf);
+ c_emit_local_assign_close(t);
+}
+
+void c_emit_indirect_branch(CTarget* t, Operand addr,
+ const Label* valid_targets, u32 ntargets) {
+ (void)valid_targets;
+ (void)ntargets;
+ if (t->last_was_terminator) return;
+ cbuf_puts(&t->body, " goto *");
+ c_emit_operand(t, addr);
+ cbuf_puts(&t->body, ";\n");
+ t->last_was_terminator = 1;
+}
+
+/* ===== function-local static label-address data ===== */
+
+static int c_is_local_static_sym(CTarget* t, ObjSymId sym) {
+ for (u32 i = 0; i < t->local_static_nsyms; ++i) {
+ if (t->local_static_syms[i] == sym) return 1;
+ }
+ return 0;
+}
+
+static void c_mark_local_static_sym(CTarget* t, ObjSymId sym) {
+ Heap* h = t->c->ctx->heap;
+ if (sym == OBJ_SYM_NONE || c_is_local_static_sym(t, sym)) return;
+ if (t->local_static_nsyms + 1u > t->local_static_syms_cap) {
+ u32 oldcap = t->local_static_syms_cap;
+ u32 newcap = oldcap ? oldcap * 2u : 16u;
+ ObjSymId* ns = (ObjSymId*)h->realloc(
+ h, t->local_static_syms, oldcap * sizeof(*t->local_static_syms),
+ newcap * sizeof(*t->local_static_syms), _Alignof(ObjSymId));
+ if (!ns) {
+ compiler_panic(t->c, t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0},
+ "C target: out of memory");
+ }
+ t->local_static_syms = ns;
+ t->local_static_syms_cap = newcap;
+ }
+ t->local_static_syms[t->local_static_nsyms++] = sym;
+}
+
+static void c_grow_local_static_entries(CTarget* t, u32 want) {
+ Heap* h = t->c->ctx->heap;
+ if (want <= t->local_static_entries_cap) return;
+ u32 oldcap = t->local_static_entries_cap;
+ u32 newcap = oldcap ? oldcap * 2u : 8u;
+ while (newcap < want) newcap *= 2u;
+ CLocalStaticLabelEntry* ne = (CLocalStaticLabelEntry*)h->realloc(
+ h, t->local_static_entries, oldcap * sizeof(*t->local_static_entries),
+ newcap * sizeof(*t->local_static_entries),
+ _Alignof(CLocalStaticLabelEntry));
+ if (!ne) {
+ compiler_panic(t->c, t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0},
+ "C target: out of memory");
+ }
+ t->local_static_entries = ne;
+ t->local_static_entries_cap = newcap;
+}
+
+int c_emit_can_local_static_data(CTarget* t,
+ const CGLocalStaticDataDesc* desc) {
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ const CgType* ty = cg_type_get(t->c, api_unalias_type(t->c, desc->type));
+ if (!ty) {
+ compiler_panic(t->c, loc, "C target: unknown local static type %u",
+ (unsigned)desc->type);
+ }
+ if (ty->kind == CFREE_CG_TYPE_ARRAY) {
+ ty = cg_type_get(t->c, api_unalias_type(t->c, ty->array.elem));
+ }
+ return ty && ty->kind == CFREE_CG_TYPE_PTR;
+}
+
+int c_emit_local_static_data_begin(CTarget* t,
+ const CGLocalStaticDataDesc* desc) {
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ if (!t->cur_fn) {
+ compiler_panic(t->c, loc,
+ "C target: function-local static data outside function");
+ }
+ if (t->local_static_active) {
+ compiler_panic(t->c, loc,
+ "C target: nested function-local static data definition");
+ }
+ const CgType* ty = cg_type_get(t->c, api_unalias_type(t->c, desc->type));
+ if (!ty) {
+ compiler_panic(t->c, loc, "C target: unknown local static type %u",
+ (unsigned)desc->type);
+ }
+
+ u64 count = 1;
+ int is_array = 0;
+ CfreeCgTypeId elem = desc->type;
+ if (ty->kind == CFREE_CG_TYPE_ARRAY) {
+ is_array = 1;
+ count = ty->array.count;
+ elem = ty->array.elem;
+ ty = cg_type_get(t->c, api_unalias_type(t->c, elem));
+ }
+ if (!c_emit_can_local_static_data(t, desc)) {
+ return 0;
+ }
+ if (count > UINT32_MAX) {
+ compiler_panic(t->c, loc, "C target: local static pointer table too large");
+ }
+
+ c_grow_local_static_entries(t, (u32)count);
+ for (u32 i = 0; i < (u32)count; ++i) {
+ t->local_static_entries[i].label = LABEL_NONE;
+ t->local_static_entries[i].addend = 0;
+ t->local_static_entries[i].has_label = 0;
+ }
+ t->local_static_nentries = (u32)count;
+ t->local_static_sym = desc->sym;
+ t->local_static_type = desc->type;
+ t->local_static_count = count;
+ t->local_static_offset = 0;
+ t->local_static_ptr_width = (u32)cg_type_size(t->c, elem);
+ t->local_static_align =
+ desc->align ? desc->align : cg_type_align(t->c, desc->type);
+ t->local_static_active = 1;
+ t->local_static_is_array = (u8)is_array;
+ t->local_static_readonly =
+ (desc->attrs.flags & CFREE_CG_DATADEF_READONLY) ? 1u : 0u;
+ c_mark_local_static_sym(t, desc->sym);
+ return 1;
+}
+
+void c_emit_local_static_data_write(CTarget* t, const u8* data, u64 len) {
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ if (!t->local_static_active || !len) return;
+ if (data) {
+ for (u64 i = 0; i < len; ++i) {
+ if (data[i] != 0) {
+ compiler_panic(t->c, loc,
+ "C target: function-local static label table supports "
+ "only zero bytes and label addresses");
+ }
+ }
+ }
+ t->local_static_offset += len;
+}
+
+void c_emit_local_static_data_label_addr(CTarget* t, Label target, i64 addend,
+ u32 width, u32 address_space) {
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ (void)address_space;
+ if (!t->local_static_active) {
+ compiler_panic(t->c, loc,
+ "C target: label address outside local static data");
+ }
+ if (width != t->local_static_ptr_width) {
+ compiler_panic(t->c, loc,
+ "C target: label address width %u does not match pointer "
+ "width %u",
+ (unsigned)width, (unsigned)t->local_static_ptr_width);
+ }
+ if ((t->local_static_offset % t->local_static_ptr_width) != 0) {
+ compiler_panic(t->c, loc,
+ "C target: unaligned label address in local static data");
+ }
+ u64 idx = t->local_static_offset / t->local_static_ptr_width;
+ if (idx >= t->local_static_count) {
+ compiler_panic(t->c, loc,
+ "C target: too many local static label table entries");
+ }
+ CLocalStaticLabelEntry* e = &t->local_static_entries[(u32)idx];
+ if (e->has_label) {
+ compiler_panic(t->c, loc,
+ "C target: duplicate local static label table entry");
+ }
+ e->label = target;
+ e->addend = addend;
+ e->has_label = 1;
+ t->local_static_offset += width;
+}
+
+static void c_emit_local_static_label_expr(CTarget* t,
+ const CLocalStaticLabelEntry* e) {
+ char lbuf[24];
+ if (!e->has_label) {
+ cbuf_puts(&t->decls, "(void*)0");
+ return;
+ }
+ if (e->addend == 0) {
+ cbuf_puts(&t->decls, "&&");
+ c_label_name(e->label, lbuf, sizeof lbuf);
+ cbuf_puts(&t->decls, lbuf);
+ return;
+ }
+ cbuf_puts(&t->decls, "(void*)((char*)&&");
+ c_label_name(e->label, lbuf, sizeof lbuf);
+ cbuf_puts(&t->decls, lbuf);
+ cbuf_puts(&t->decls, " + ");
+ cbuf_put_i64(&t->decls, e->addend);
+ cbuf_puts(&t->decls, ")");
+}
+
+void c_emit_local_static_data_end(CTarget* t) {
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ if (!t->local_static_active) return;
+ u64 total_size = t->local_static_count * t->local_static_ptr_width;
+ if (t->local_static_offset > total_size) {
+ compiler_panic(t->c, loc,
+ "C target: local static initializer exceeds object size");
+ }
+ const char* nm = c_sym_name(t, t->local_static_sym);
+ cbuf_puts(&t->decls, " static __attribute__((unused)) ");
+ cbuf_puts(&t->decls, "_Alignas(");
+ cbuf_put_u64(&t->decls, t->local_static_align ? t->local_static_align : 1);
+ cbuf_puts(&t->decls, ") void* ");
+ if (t->local_static_readonly) cbuf_puts(&t->decls, "const ");
+ cbuf_puts(&t->decls, nm);
+ if (t->local_static_is_array) {
+ cbuf_puts(&t->decls, "[");
+ cbuf_put_u64(&t->decls, t->local_static_count);
+ cbuf_puts(&t->decls, "]");
+ }
+ cbuf_puts(&t->decls, " = {");
+ for (u32 i = 0; i < t->local_static_nentries; ++i) {
+ if (i > 0) cbuf_putc(&t->decls, ',');
+ if ((i & 3u) == 0) cbuf_puts(&t->decls, "\n ");
+ c_emit_local_static_label_expr(t, &t->local_static_entries[i]);
+ }
+ cbuf_puts(&t->decls, "\n };\n");
+
+ t->local_static_active = 0;
+ t->local_static_sym = OBJ_SYM_NONE;
+ t->local_static_type = CFREE_CG_TYPE_NONE;
+ t->local_static_count = 0;
+ t->local_static_offset = 0;
+ t->local_static_ptr_width = 0;
+ t->local_static_align = 0;
+ t->local_static_nentries = 0;
+ t->local_static_is_array = 0;
+ t->local_static_readonly = 0;
+}
+
+/* ===== local, local_addr ===== */
+
+CGLocal c_emit_local(CTarget* t, const CGLocalDesc* d) {
+ t->next_local += 1u;
+ if (t->next_local == CG_LOCAL_NONE) {
+ compiler_panic(t->c, t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0},
+ "C target: semantic local id exhausted");
+ return CG_LOCAL_NONE;
+ }
+ c_ensure_local(t, (CGLocal)t->next_local, d->type);
+ return (CGLocal)t->next_local;
+}
+
+void c_emit_local_addr(CTarget* t, Operand dst, const CGLocalDesc* d,
+ CGLocal s) {
+ (void)d;
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ if (dst.kind != OPK_LOCAL) {
+ compiler_panic(t->c, loc, "C target: local_addr dst must be LOCAL");
+ }
+ c_ensure_local(t, dst.v.local, dst.type);
+ c_ensure_local(t, s, d->type);
+ char buf[24];
+ c_emit_local_assign_open(t, dst.v.local, (CfreeCgTypeId)0);
+ cbuf_puts(&t->body, "&");
+ c_local_name(s, buf, sizeof buf);
+ cbuf_puts(&t->body, buf);
+ c_emit_local_assign_close(t);
+}
+
+/* ===== convert ===== */
+
+void c_emit_convert(CTarget* t, ConvKind k, Operand dst, Operand src) {
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ if (dst.kind != OPK_LOCAL) {
+ compiler_panic(t->c, loc, "C target: convert dst must be LOCAL");
+ }
+ c_ensure_local(t, dst.v.local, dst.type);
+ char buf[24];
+ c_local_name(dst.v.local, buf, sizeof buf);
+
+ if (k == CV_BITCAST) {
+ /* Same-size reinterpretation. Use __builtin_memcpy through a temp so
+ * neither aliasing nor representation assumptions creep in. The temp
+ * lives in its own `{ ... }` block, so no name collision tracking. */
+ u32 id = ++t->next_tmp;
+ cbuf_puts(&t->body, " { ");
+ c_emit_type(t, &t->body, src.type);
+ cbuf_puts(&t->body, " __bc");
+ cbuf_put_u64(&t->body, (u64)id);
+ cbuf_puts(&t->body, " = ");
+ c_emit_operand(t, src);
+ cbuf_puts(&t->body, "; __builtin_memcpy(&");
+ cbuf_puts(&t->body, buf);
+ cbuf_puts(&t->body, ", &__bc");
+ cbuf_put_u64(&t->body, (u64)id);
+ cbuf_puts(&t->body, ", sizeof __bc");
+ cbuf_put_u64(&t->body, (u64)id);
+ cbuf_puts(&t->body, "); }\n");
+ return;
+ }
+
+ if (c_type_is_bool(t, dst.type)) {
+ c_emit_local_assign_open(t, dst.v.local, dst.type);
+ cbuf_puts(&t->body, "(");
+ c_emit_type(t, &t->body, dst.type);
+ cbuf_puts(&t->body, ")(");
+ c_emit_operand(t, src);
+ cbuf_puts(&t->body, " != 0)");
+ c_emit_local_assign_close(t);
+ return;
+ }
+
+ /* Integer and float conversions: a C cast does the right thing once the
+ * source is first cast to the appropriate signedness (for SEXT/ZEXT and
+ * ITOF_S/U / FTOI_S/U). */
+ int src_signed = 1;
+ switch (k) {
+ case CV_ZEXT:
+ case CV_ITOF_U:
+ case CV_FTOI_U:
+ src_signed = 0;
+ break;
+ default:
+ src_signed = 1;
+ break;
+ }
+
+ /* The cast `(dst.type)(src)` produces a value of dst.type. */
+ c_emit_local_assign_open(t, dst.v.local, dst.type);
+ cbuf_puts(&t->body, "(");
+ c_emit_type(t, &t->body, dst.type);
+ cbuf_puts(&t->body, ")");
+ if (k == CV_SEXT || k == CV_ZEXT) {
+ c_emit_operand_signed(t, src, src_signed);
+ } else if (k == CV_TRUNC && c_operand_is_ptr_typed(t, src)) {
+ /* Casting a pointer directly to a narrower integer trips
+ * -Wvoid-pointer-to-int-cast (and -Wpointer-to-int-cast). Bridge
+ * through uintptr_t. */
+ cbuf_puts(&t->body, "((uintptr_t)");
+ c_emit_operand(t, src);
+ cbuf_puts(&t->body, ")");
+ } else {
+ /* TRUNC / FTOI / ITOF / FEXT / FTRUNC: rely on C cast semantics. */
+ c_emit_operand(t, src);
+ }
+ c_emit_local_assign_close(t);
+}
+
+/* === call === */
+
+static CfreeCgTypeId c_call_arg_type(CTarget* t, const CgType* fty,
+ const CGCallDesc* d, u32 i) {
+ if (i < fty->func.nparams) return fty->func.params[i].type;
+ return c_local_type_or_panic(t, d->args[i]);
+}
+
+static void c_emit_call_arg(CTarget* t, const CgType* fty, const CGCallDesc* d,
+ u32 i) {
+ CfreeCgTypeId ty = c_call_arg_type(t, fty, d, i);
+ c_ensure_local(t, d->args[i], ty);
+ c_emit_operand(t, c_op_local(d->args[i], ty));
+}
+
+static void c_emit_call_expr(CTarget* t, const CgType* fty, const CGCallDesc* d,
+ const CfreeCgTypeId* result_types) {
+ if (d->callee.kind == OPK_GLOBAL) {
+ c_ensure_forward_decl_with_results(t, d->callee.v.global.sym, d->fn_type,
+ d->nresults > 1 ? result_types : NULL,
+ d->nresults);
+ cbuf_puts(&t->body, c_sym_name(t, d->callee.v.global.sym));
+ } else if (d->callee.kind == OPK_LOCAL) {
+ cbuf_puts(&t->body, "((");
+ if (d->nresults > 1) {
+ c_emit_tuple_type_name(t, &t->body, result_types, d->nresults);
+ cbuf_puts(&t->body, " (*)(");
+ if (fty->func.nparams == 0 && !fty->func.abi_variadic) {
+ cbuf_puts(&t->body, "void");
+ } else {
+ for (u32 i = 0; i < fty->func.nparams; ++i) {
+ if (i > 0) cbuf_puts(&t->body, ", ");
+ c_emit_type(t, &t->body, fty->func.params[i].type);
+ }
+ if (fty->func.abi_variadic) {
+ if (fty->func.nparams > 0) cbuf_puts(&t->body, ", ");
+ cbuf_puts(&t->body, "...");
+ }
+ }
+ cbuf_puts(&t->body, ")");
+ } else {
+ const char* fp = c_typedef_name(t, d->fn_type);
+ c_ensure_typedef(t, d->fn_type);
+ cbuf_puts(&t->body, fp);
+ }
+ cbuf_puts(&t->body, ")");
+ c_emit_operand(t, d->callee);
+ cbuf_puts(&t->body, ")");
+ } else {
+ compiler_panic(t->c, t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0},
+ "C target: callee kind %d not supported",
+ (int)d->callee.kind);
+ }
+
+ cbuf_puts(&t->body, "(");
+ for (u32 i = 0; i < d->nargs; ++i) {
+ if (i > 0) cbuf_puts(&t->body, ", ");
+ c_emit_call_arg(t, fty, d, i);
+ }
+ cbuf_puts(&t->body, ")");
+}
+
+const char* c_emit_tail_call_unrealizable_reason(CTarget* t,
+ const CGCallDesc* d) {
+ return c_emit_tail_call_unrealizable_reason_for(t, t->cur_fn, d);
+}
+
+const char* c_emit_tail_call_unrealizable_reason_for(
+ CTarget* t, const CGFuncDesc* caller_fd, const CGCallDesc* d) {
+ SrcLoc loc = caller_fd ? caller_fd->loc : (SrcLoc){0, 0, 0};
+ const CgType* fty = cg_type_get(t->c, api_unalias_type(t->c, d->fn_type));
+ if (!fty || fty->kind != CFREE_CG_TYPE_FUNC) {
+ compiler_panic(t->c, loc, "C target: tail call: bad fn_type");
+ }
+ const CgType* caller =
+ caller_fd ? cg_type_get(t->c, api_unalias_type(t->c, caller_fd->fn_type))
+ : NULL;
+ if (!caller || caller->kind != CFREE_CG_TYPE_FUNC) {
+ compiler_panic(t->c, loc, "C target: tail call outside function");
+ }
+ if (caller->func.abi_variadic) {
+ return "C target: caller variadic tail call not yet supported by clang "
+ "musttail";
+ }
+ if (fty->func.abi_variadic) {
+ return "C target: variadic tail call not yet supported by clang musttail";
+ }
+ if (caller->func.nparams != fty->func.nparams) {
+ return "C target: tail call with differing parameter counts not yet "
+ "supported by clang musttail";
+ }
+ return NULL;
+}
+
+void c_emit_call(CTarget* t, const CGCallDesc* d) {
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ Heap* h = t->c->ctx->heap;
+
+ const CgType* fty = cg_type_get(t->c, api_unalias_type(t->c, d->fn_type));
+ if (!fty || fty->kind != CFREE_CG_TYPE_FUNC) {
+ compiler_panic(t->c, loc, "C target: call: bad fn_type");
+ }
+ CfreeCgTypeId ret_type = fty->func.ret;
+ int is_tail = (d->flags & CG_CALL_TAIL) != 0;
+ CfreeCgTypeId* result_types = NULL;
+ if (d->nresults > 1) {
+ result_types = (CfreeCgTypeId*)h->alloc(
+ h, sizeof(CfreeCgTypeId) * d->nresults, _Alignof(CfreeCgTypeId));
+ if (!result_types) {
+ compiler_panic(t->c, loc, "C target: out of memory");
+ return;
+ }
+ for (u32 i = 0; i < d->nresults; ++i)
+ result_types[i] = c_local_type_or_panic(t, d->results[i]);
+ c_ensure_tuple_typedef(t, result_types, d->nresults);
+ }
+
+ if (is_tail) {
+ cbuf_puts(&t->body, " __attribute__((musttail)) return ");
+ c_emit_call_expr(t, fty, d, result_types);
+ cbuf_puts(&t->body, ";\n");
+ t->last_was_terminator = 1;
+ } else if (d->nresults == 0) {
+ cbuf_puts(&t->body, " ");
+ c_emit_call_expr(t, fty, d, result_types);
+ cbuf_puts(&t->body, ";\n");
+ } else if (d->nresults == 1) {
+ c_ensure_local(t, d->results[0], ret_type);
+ c_emit_local_assign_open(t, d->results[0], ret_type);
+ c_emit_call_expr(t, fty, d, result_types);
+ c_emit_local_assign_close(t);
+ } else {
+ char tmp[32];
+ u32 tmp_id = t->next_tmp++;
+ snprintf(tmp, sizeof tmp, "__cfree_call_%u", (unsigned)tmp_id);
+ cbuf_puts(&t->body, " ");
+ c_emit_tuple_type_name(t, &t->body, result_types, d->nresults);
+ cbuf_putc(&t->body, ' ');
+ cbuf_puts(&t->body, tmp);
+ cbuf_puts(&t->body, " = ");
+ c_emit_call_expr(t, fty, d, result_types);
+ cbuf_puts(&t->body, ";\n");
+ for (u32 i = 0; i < d->nresults; ++i) {
+ c_ensure_local(t, d->results[i], result_types[i]);
+ c_emit_local_assign_open(t, d->results[i], result_types[i]);
+ cbuf_puts(&t->body, tmp);
+ cbuf_puts(&t->body, ".f");
+ cbuf_put_u64(&t->body, (u64)i);
+ c_emit_local_assign_close(t);
+ }
+ }
+ if (result_types)
+ h->free(h, result_types, sizeof(CfreeCgTypeId) * d->nresults);
+}
+
+/* === load / store === */
+
+void c_emit_load(CTarget* t, Operand dst, Operand addr, MemAccess m) {
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ if (dst.kind != OPK_LOCAL) {
+ compiler_panic(t->c, loc, "C target: load dst must be LOCAL");
+ }
+ c_ensure_local(t, dst.v.local, dst.type);
+ CfreeCgTypeId access_ty = m.type ? m.type : dst.type;
+ if (c_type_is_aggregate(t, access_ty) && !c_type_is_aggregate(t, dst.type))
+ access_ty = dst.type;
+ /* The deref `*(access_ty*)addr` produces a value of access_ty. */
+ c_emit_local_assign_open(t, dst.v.local, access_ty);
+ c_emit_addr_deref(t, addr, access_ty);
+ c_emit_local_assign_close(t);
+}
+
+void c_emit_store(CTarget* t, Operand addr, Operand src, MemAccess m) {
+ CfreeCgTypeId access_ty = m.type ? m.type : src.type;
+ if (c_type_is_aggregate(t, access_ty) && !c_type_is_aggregate(t, src.type))
+ access_ty = src.type;
+ cbuf_puts(&t->body, " ");
+ c_emit_addr_deref(t, addr, access_ty);
+ /* c_emit_operand_as bridges int/ptr crossings through uintptr_t so
+ * roundtrips don't trip `-Wint-conversion`. */
+ cbuf_puts(&t->body, " = ");
+ c_emit_operand_as(t, src, access_ty);
+ cbuf_puts(&t->body, ";\n");
+}
+
+void c_emit_addr_of(CTarget* t, Operand dst, Operand lv) {
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ if (dst.kind != OPK_LOCAL) {
+ compiler_panic(t->c, loc, "C target: addr_of dst must be LOCAL");
+ }
+ c_ensure_local(t, dst.v.local, dst.type);
+ /* `c_emit_lvalue_addr` casts its output to dst.type already. */
+ c_emit_local_assign_open(t, dst.v.local, dst.type);
+ c_emit_lvalue_addr(t, lv, dst.type);
+ c_emit_local_assign_close(t);
+}
+
+void c_emit_ret(
+ CTarget* t, const CGLocal* values,
+ u32 nvalues) { /* Already-terminated block: this ret is unreachable (the
+ * frontend's defensive `return 0;` epilogue lands here right
+ * after a user return). */
+ if (t->last_was_terminator) return;
+ /* CG emits a defensive ret_void epilogue at the end of every function. For
+ * a non-void function that's unreachable; emitting a bare `return;` would
+ * trip -Wreturn-type. Spell it as `__builtin_unreachable()` so the host C
+ * compiler sees the path is dead without us inventing a fake value. */
+ if (nvalues == 0 && t->cur_fn) {
+ if (t->cur_fn->nresults != 0) {
+ cbuf_puts(&t->body, " __builtin_unreachable();\n");
+ t->last_was_terminator = 1;
+ return;
+ }
+ }
+ cbuf_puts(&t->body, " return");
+ if (nvalues == 1) {
+ cbuf_puts(&t->body, " ");
+ CfreeCgTypeId ret_type =
+ t->cur_fn ? t->cur_fn->result_types[0] : (CfreeCgTypeId)0;
+ const CgType* rty =
+ ret_type ? cg_type_get(t->c, api_unalias_type(t->c, ret_type)) : NULL;
+ int is_aggregate = rty && (rty->kind == CFREE_CG_TYPE_RECORD ||
+ rty->kind == CFREE_CG_TYPE_ARRAY);
+ if (ret_type && !is_aggregate) {
+ CfreeCgTypeId value_ty = c_local_type_or_panic(t, values[0]);
+ c_emit_operand_as(t, c_op_local(values[0], value_ty), ret_type);
+ } else {
+ c_emit_operand(t, c_op_local(values[0], ret_type));
+ }
+ } else if (nvalues > 1) {
+ const CfreeCgTypeId* result_types =
+ t->cur_fn ? t->cur_fn->result_types : NULL;
+ if (!result_types || t->cur_fn->nresults != nvalues) {
+ compiler_panic(t->c, t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0},
+ "C target: multi-return shape mismatch");
+ return;
+ }
+ c_ensure_tuple_typedef(t, result_types, nvalues);
+ cbuf_puts(&t->body, " (");
+ c_emit_tuple_type_name(t, &t->body, result_types, nvalues);
+ cbuf_puts(&t->body, "){");
+ for (u32 i = 0; i < nvalues; ++i) {
+ if (i > 0) cbuf_puts(&t->body, ", ");
+ c_emit_operand(t, c_op_local(values[i], result_types[i]));
+ }
+ cbuf_puts(&t->body, "}");
+ }
+ cbuf_puts(&t->body, ";\n");
+ t->last_was_terminator = 1;
+}
+
+/* === alias ===
+ * `cfree_cg_alias` makes alias_sym refer to target_sym's body. In obj-file
+ * land that's two ObjSyms sharing a (section_id, value); in C source we
+ * have to spell it out:
+ *
+ * ELF/PE → `Ret alias(args) __attribute__((alias("target")));`
+ * Single definition, true aliasing, &alias == &target.
+ * Mach-O → emit a thunk `Ret alias(args) { return target(args); }`.
+ * Clang on Darwin rejects __attribute__((alias)) outright,
+ * so we fall back to a wrapper. Loses the `&alias==&target`
+ * identity but preserves call-through semantics, which is
+ * all the cfree-emitted code path needs.
+ *
+ * The emitted decl serves as the alias definition AND a forward prototype
+ * for callers, so we mark sym_forwarded to dedup against a later c_call. */
+void c_emit_alias(CTarget* t, ObjSymId alias_sym, ObjSymId target_sym,
+ CfreeCgTypeId type) {
+ Heap* h = t->c->ctx->heap;
+ if ((u32)alias_sym >= t->sym_forwarded_cap) {
+ u32 newcap = t->sym_forwarded_cap ? t->sym_forwarded_cap : 16;
+ while (newcap <= (u32)alias_sym) newcap *= 2;
+ u8* nd =
+ (u8*)h->realloc(h, t->sym_forwarded, t->sym_forwarded_cap, newcap, 1);
+ if (!nd && newcap) {
+ compiler_panic(t->c, (SrcLoc){0, 0, 0}, "C target: out of memory");
+ }
+ for (u32 i = t->sym_forwarded_cap; i < newcap; ++i) nd[i] = 0;
+ t->sym_forwarded = nd;
+ t->sym_forwarded_cap = newcap;
+ }
+ if (t->sym_forwarded[alias_sym]) return;
+ t->sym_forwarded[alias_sym] = 1;
+ const char* alias_name = c_sym_name(t, alias_sym);
+ const char* target_name = c_sym_name(t, target_sym);
+ const CgType* fty = cg_type_get(t->c, api_unalias_type(t->c, type));
+ int is_func = fty && fty->kind == CFREE_CG_TYPE_FUNC;
+
+ if (t->c->target.obj != CFREE_OBJ_MACHO) {
+ /* Attribute form. Works for both function and object aliases on ELF
+ * and PE/COFF. */
+ c_emit_func_signature(t, &t->forwards, alias_name, type, NULL, 0);
+ cbuf_puts(&t->forwards, " __attribute__((alias(\"");
+ cbuf_puts(&t->forwards, target_name);
+ cbuf_puts(&t->forwards, "\")));\n");
+ return;
+ }
+
+ /* Mach-O thunk fallback. Functions only for v1 — object aliases on
+ * Darwin would need a more elaborate scheme (see doc/CBACKEND.md). */
+ if (!is_func) {
+ compiler_panic(t->c, (SrcLoc){0, 0, 0},
+ "C target: object alias on Mach-O not yet supported");
+ }
+ /* Forward prototype for the target (its full definition lands separately
+ * via c_func_begin). Also dedup that. */
+ c_ensure_forward_decl(t, target_sym, type);
+ /* `static`? No — alias must be externally visible. */
+ c_emit_func_signature(t, &t->forwards, alias_name, type, NULL, 0);
+ cbuf_puts(&t->forwards, " { ");
+ CfreeCgTypeId ret_type = cg_type_func_ret_id(t->c, type);
+ if (!cg_type_is_void(t->c, ret_type)) cbuf_puts(&t->forwards, "return ");
+ cbuf_puts(&t->forwards, target_name);
+ cbuf_puts(&t->forwards, "(");
+ for (u32 i = 0; i < fty->func.nparams; ++i) {
+ if (i > 0) cbuf_puts(&t->forwards, ", ");
+ cbuf_puts(&t->forwards, "p");
+ cbuf_put_u64(&t->forwards, (u64)i);
+ }
+ cbuf_puts(&t->forwards, "); }\n");
+}
+
+/* === intrinsic ===
+ *
+ * All cfree IntrinKinds map onto gcc/clang `__builtin_*` builtins, which
+ * the host C compiler then turns into the appropriate sequence (inline op,
+ * libcall, runtime CAS, etc.). This is exactly the seam the doc described:
+ * cfree records intent, the downstream toolchain picks the mechanism.
+ *
+ * Operand shapes follow arch.h §IntrinKind. */
+
+static const char* c_bitop_builtin(IntrinKind k, u32 width) {
+ switch (k) {
+ case INTRIN_POPCOUNT:
+ if (width == 32) return "__builtin_popcount";
+ if (width == 64) return "__builtin_popcountll";
+ if (width == 16 || width == 8) return "__builtin_popcount";
+ return NULL;
+ case INTRIN_CTZ:
+ if (width == 32) return "__builtin_ctz";
+ if (width == 64) return "__builtin_ctzll";
+ if (width == 16 || width == 8) return "__builtin_ctz";
+ return NULL;
+ case INTRIN_CLZ:
+ if (width == 32) return "__builtin_clz";
+ if (width == 64) return "__builtin_clzll";
+ if (width == 16 || width == 8) return "__builtin_clz";
+ return NULL;
+ case INTRIN_BSWAP16:
+ return "__builtin_bswap16";
+ case INTRIN_BSWAP32:
+ return "__builtin_bswap32";
+ case INTRIN_BSWAP64:
+ return "__builtin_bswap64";
+ default:
+ return NULL;
+ }
+}
+
+static const char* c_overflow_builtin(IntrinKind k) {
+ switch (k) {
+ case INTRIN_SADD_OVERFLOW:
+ case INTRIN_UADD_OVERFLOW:
+ return "__builtin_add_overflow";
+ case INTRIN_SSUB_OVERFLOW:
+ case INTRIN_USUB_OVERFLOW:
+ return "__builtin_sub_overflow";
+ case INTRIN_SMUL_OVERFLOW:
+ case INTRIN_UMUL_OVERFLOW:
+ return "__builtin_mul_overflow";
+ default:
+ return NULL;
+ }
+}
+
+void c_emit_intrinsic(CTarget* t, IntrinKind k, Operand* dsts, u32 ndst,
+ const Operand* args, u32 narg) {
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ switch (k) {
+ case INTRIN_UNREACHABLE:
+ cbuf_puts(&t->body, " __builtin_unreachable();\n");
+ return;
+ case INTRIN_TRAP:
+ cbuf_puts(&t->body, " __builtin_trap();\n");
+ return;
+ case INTRIN_PREFETCH: {
+ cbuf_puts(&t->body, " __builtin_prefetch(");
+ for (u32 i = 0; i < narg; ++i) {
+ if (i > 0) cbuf_puts(&t->body, ", ");
+ c_emit_operand(t, args[i]);
+ }
+ cbuf_puts(&t->body, ");\n");
+ return;
+ }
+ case INTRIN_ASSUME_ALIGNED: {
+ /* dsts[0] is the result local (pointer); args = (ptr, align [, ofs]) */
+ if (ndst != 1) {
+ compiler_panic(t->c, loc,
+ "C target: assume_aligned: expected 1 dst, got %u",
+ (unsigned)ndst);
+ }
+ c_ensure_local(t, dsts[0].v.local, dsts[0].type);
+ /* Returns void*; bridge to dst pointer type. */
+ c_emit_local_assign_open(t, dsts[0].v.local, (CfreeCgTypeId)0);
+ cbuf_puts(&t->body, "__builtin_assume_aligned(");
+ for (u32 i = 0; i < narg; ++i) {
+ if (i > 0) cbuf_puts(&t->body, ", ");
+ c_emit_operand(t, args[i]);
+ }
+ cbuf_puts(&t->body, ")");
+ c_emit_local_assign_close(t);
+ return;
+ }
+ case INTRIN_EXPECT: {
+ /* dsts[0] = __builtin_expect(args[0], args[1]) but typed via long. */
+ if (ndst != 1 || narg != 2) {
+ compiler_panic(t->c, loc,
+ "C target: expect: bad shape (ndst=%u narg=%u)",
+ (unsigned)ndst, (unsigned)narg);
+ }
+ c_ensure_local(t, dsts[0].v.local, dsts[0].type);
+ /* Returns `long`; dst.type may be a narrower int — keep the bridge. */
+ c_emit_local_assign_open(t, dsts[0].v.local, (CfreeCgTypeId)0);
+ cbuf_puts(&t->body, "__builtin_expect((long)");
+ c_emit_operand(t, args[0]);
+ cbuf_puts(&t->body, ", (long)");
+ c_emit_operand(t, args[1]);
+ cbuf_puts(&t->body, ")");
+ c_emit_local_assign_close(t);
+ return;
+ }
+ case INTRIN_POPCOUNT:
+ case INTRIN_CTZ:
+ case INTRIN_CLZ:
+ case INTRIN_BSWAP16:
+ case INTRIN_BSWAP32:
+ case INTRIN_BSWAP64: {
+ if (ndst != 1 || narg != 1) {
+ compiler_panic(t->c, loc,
+ "C target: bit-intrin: bad shape (ndst=%u narg=%u)",
+ (unsigned)ndst, (unsigned)narg);
+ }
+ u32 w = c_int_width_for_signedness(t, args[0].type);
+ const char* fn = c_bitop_builtin(k, w);
+ if (!fn) {
+ compiler_panic(t->c, loc, "C target: bit-intrin width %u unsupported",
+ (unsigned)w);
+ }
+ c_ensure_local(t, dsts[0].v.local, dsts[0].type);
+ /* __builtin_popcount/ctz/clz return `int`; bswap returns its input
+ * type. Narrow to dst.type via the bridge. */
+ c_emit_local_assign_open(t, dsts[0].v.local, (CfreeCgTypeId)0);
+ cbuf_puts(&t->body, fn);
+ cbuf_puts(&t->body, "(");
+ c_emit_operand(t, args[0]);
+ cbuf_puts(&t->body, ")");
+ c_emit_local_assign_close(t);
+ return;
+ }
+ case INTRIN_MEMCPY:
+ case INTRIN_MEMMOVE:
+ case INTRIN_MEMSET: {
+ const char* fn = (k == INTRIN_MEMCPY) ? "__builtin_memcpy"
+ : (k == INTRIN_MEMMOVE) ? "__builtin_memmove"
+ : "__builtin_memset";
+ cbuf_puts(&t->body, " ");
+ cbuf_puts(&t->body, fn);
+ cbuf_puts(&t->body, "(");
+ for (u32 i = 0; i < narg; ++i) {
+ if (i > 0) cbuf_puts(&t->body, ", ");
+ /* The pointer operands (dst for all three; src for mem{cpy,move})
+ * may be typed as a plain integer local when they come from address
+ * arithmetic, which the C target declares as int64_t. The builtins
+ * take void*, so cast explicitly to avoid -Wint-conversion. */
+ int is_ptr_arg =
+ (i == 0) || (i == 1 && (k == INTRIN_MEMCPY || k == INTRIN_MEMMOVE));
+ if (is_ptr_arg) cbuf_puts(&t->body, "(void*)");
+ c_emit_operand(t, args[i]);
+ }
+ cbuf_puts(&t->body, ");\n");
+ return;
+ }
+ case INTRIN_SADD_OVERFLOW:
+ case INTRIN_UADD_OVERFLOW:
+ case INTRIN_SSUB_OVERFLOW:
+ case INTRIN_USUB_OVERFLOW:
+ case INTRIN_SMUL_OVERFLOW:
+ case INTRIN_UMUL_OVERFLOW: {
+ /* dsts[0] = value local, dsts[1] = i1 overflow flag.
+ *
+ * Signedness comes from the intrinsic kind, but cfree's CG int type
+ * is width-only and the C target declares every result as a signed
+ * fixed-width (int{8,16,32,64}_t). __builtin_*_overflow keys its
+ * overflow check on the result type, so passing the signed local
+ * directly makes a UADD test as if it were signed and miss true
+ * unsigned overflow. Wrap the call in a block with a scratch result
+ * of the right signedness and copy it back through the int/uint
+ * bridge. */
+ if (ndst != 2 || narg != 2) {
+ compiler_panic(t->c, loc, "C target: overflow-intrin: bad shape");
+ }
+ int is_unsigned =
+ (k == INTRIN_UADD_OVERFLOW || k == INTRIN_USUB_OVERFLOW ||
+ k == INTRIN_UMUL_OVERFLOW);
+ const char* fn = c_overflow_builtin(k);
+ c_ensure_local(t, dsts[0].v.local, dsts[0].type);
+ c_ensure_local(t, dsts[1].v.local, dsts[1].type);
+ char vbuf[24], obuf[24];
+ c_local_name(dsts[0].v.local, vbuf, sizeof vbuf);
+ c_local_name(dsts[1].v.local, obuf, sizeof obuf);
+ u32 w = c_int_width_for_signedness(t, dsts[0].type);
+ const char* sty = c_int_type_name_for_width(w, !is_unsigned);
+ if (!sty) {
+ compiler_panic(t->c, loc,
+ "C target: overflow-intrin: unsupported width %u",
+ (unsigned)w);
+ }
+ cbuf_puts(&t->body, " { ");
+ cbuf_puts(&t->body, sty);
+ cbuf_puts(&t->body, " __ovsc; ");
+ cbuf_puts(&t->body, obuf);
+ cbuf_puts(&t->body, " = (");
+ c_emit_type(t, &t->body, dsts[1].type);
+ cbuf_puts(&t->body, ")");
+ cbuf_puts(&t->body, fn);
+ cbuf_puts(&t->body, "((");
+ cbuf_puts(&t->body, sty);
+ cbuf_puts(&t->body, ")");
+ c_emit_operand(t, args[0]);
+ cbuf_puts(&t->body, ", (");
+ cbuf_puts(&t->body, sty);
+ cbuf_puts(&t->body, ")");
+ c_emit_operand(t, args[1]);
+ cbuf_puts(&t->body, ", &__ovsc); ");
+ cbuf_puts(&t->body, vbuf);
+ cbuf_puts(&t->body, " = (");
+ c_emit_type(t, &t->body, dsts[0].type);
+ cbuf_puts(&t->body, ")__ovsc; }\n");
+ return;
+ }
+ case INTRIN_SETJMP: {
+ t->need_setjmp = 1;
+ if (ndst != 1 || narg != 1) {
+ compiler_panic(t->c, loc, "C target: setjmp: bad shape");
+ }
+ c_ensure_local(t, dsts[0].v.local, dsts[0].type);
+ /* setjmp returns `int`; bridge to dst.type. */
+ c_emit_local_assign_open(t, dsts[0].v.local, (CfreeCgTypeId)0);
+ cbuf_puts(&t->body, "setjmp(*(jmp_buf*)(");
+ c_emit_operand(t, args[0]);
+ cbuf_puts(&t->body, "))");
+ c_emit_local_assign_close(t);
+ return;
+ }
+ case INTRIN_LONGJMP: {
+ t->need_setjmp = 1;
+ cbuf_puts(&t->body, " longjmp(*(jmp_buf*)(");
+ c_emit_operand(t, args[0]);
+ cbuf_puts(&t->body, "), (int)");
+ c_emit_operand(t, args[1]);
+ cbuf_puts(&t->body, ");\n");
+ return;
+ }
+ case INTRIN_NONE:
+ default:
+ compiler_panic(t->c, loc, "C target: intrinsic kind %d not handled",
+ (int)k);
+ }
+}
+
+/* === alloca === */
+
+void c_emit_alloca(CTarget* t, Operand dst, Operand size, u32 align) {
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ if (dst.kind != OPK_LOCAL) {
+ compiler_panic(t->c, loc, "C target: alloca dst must be LOCAL");
+ }
+ c_ensure_local(t, dst.v.local, dst.type);
+ /* __builtin_alloca returns `void*`; dst.type is typically void* too. */
+ c_emit_local_assign_open(t, dst.v.local, dst.type);
+ if (align > 1) {
+ /* gcc has __builtin_alloca_with_align taking bits, not bytes. */
+ cbuf_puts(&t->body, "__builtin_alloca_with_align(");
+ c_emit_operand(t, size);
+ cbuf_puts(&t->body, ", ");
+ cbuf_put_u64(&t->body, (u64)align * 8u);
+ cbuf_puts(&t->body, ")");
+ } else {
+ cbuf_puts(&t->body, "__builtin_alloca(");
+ c_emit_operand(t, size);
+ cbuf_puts(&t->body, ")");
+ }
+ c_emit_local_assign_close(t);
+}
+
+/* === varargs ===
+ *
+ * The C-target va_list is the host toolchain's `va_list` from <stdarg.h>.
+ * The first arg of all va_* is `ap_addr` - the address of the va_list local.
+ * We deref to get the va_list lvalue C's macros expect. */
+
+void c_emit_va_start(CTarget* t, Operand ap_addr) {
+ t->need_stdarg = 1;
+ /* va_start needs the "last named parameter". CG doesn't pass that to the
+ * backend; gcc/clang accept any non-modified ident here for variadic
+ * compatibility — feed the synthesized parameter name `p<nparams-1>` from
+ * the enclosing function. */
+ const CGFuncDesc* fd = t->cur_fn;
+ SrcLoc loc = fd ? fd->loc : (SrcLoc){0, 0, 0};
+ if (!fd) compiler_panic(t->c, loc, "C target: va_start outside function");
+ const CgType* fty = cg_type_get(t->c, api_unalias_type(t->c, fd->fn_type));
+ if (!fty || fty->kind != CFREE_CG_TYPE_FUNC || fty->func.nparams == 0) {
+ compiler_panic(t->c, loc,
+ "C target: va_start in non-variadic function shape");
+ }
+ cbuf_puts(&t->body, " __builtin_va_start(*(va_list*)(");
+ c_emit_operand(t, ap_addr);
+ cbuf_puts(&t->body, "), p");
+ cbuf_put_u64(&t->body, (u64)(fty->func.nparams - 1u));
+ cbuf_puts(&t->body, ");\n");
+}
+
+void c_emit_va_end(CTarget* t, Operand ap_addr) {
+ cbuf_puts(&t->body, " __builtin_va_end(*(va_list*)(");
+ c_emit_operand(t, ap_addr);
+ cbuf_puts(&t->body, "));\n");
+}
+
+void c_emit_va_copy(CTarget* t, Operand dst_addr, Operand src_addr) {
+ cbuf_puts(&t->body, " __builtin_va_copy(*(va_list*)(");
+ c_emit_operand(t, dst_addr);
+ cbuf_puts(&t->body, "), *(va_list*)(");
+ c_emit_operand(t, src_addr);
+ cbuf_puts(&t->body, "));\n");
+}
+
+void c_emit_va_arg(CTarget* t, Operand dst, Operand ap_addr, CfreeCgTypeId ty) {
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ if (dst.kind != OPK_LOCAL) {
+ compiler_panic(t->c, loc, "C target: va_arg dst must be LOCAL");
+ }
+ c_ensure_local(t, dst.v.local, dst.type);
+ /* __builtin_va_arg yields a value of `ty`. */
+ c_emit_local_assign_open(t, dst.v.local, ty);
+ cbuf_puts(&t->body, "__builtin_va_arg(*(va_list*)(");
+ c_emit_operand(t, ap_addr);
+ cbuf_puts(&t->body, "), ");
+ c_emit_type(t, &t->body, ty);
+ cbuf_puts(&t->body, ")");
+ c_emit_local_assign_close(t);
+}
+
+/* === copy_bytes / set_bytes === */
+
+void c_emit_copy_bytes(CTarget* t, Operand dst_addr, Operand src_addr,
+ AggregateAccess m) {
+ c_assert_no_index(t, dst_addr, "copy_bytes dst");
+ c_assert_no_index(t, src_addr, "copy_bytes src");
+ /* dst/src may be plain integer regs from address arithmetic (declared
+ * int64_t); __builtin_memcpy takes void*, so cast to avoid
+ * -Wint-conversion. */
+ cbuf_puts(&t->body, " __builtin_memcpy((void*)");
+ c_emit_copy_addr(t, dst_addr);
+ cbuf_puts(&t->body, ", (void*)");
+ c_emit_copy_addr(t, src_addr);
+ cbuf_puts(&t->body, ", ");
+ cbuf_put_u64(&t->body, (u64)m.size);
+ cbuf_puts(&t->body, ");\n");
+}
+
+static void c_emit_copy_addr(CTarget* t, Operand addr) {
+ char buf[24];
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ switch (addr.kind) {
+ case OPK_LOCAL:
+ c_ensure_local(t, addr.v.local, addr.type);
+ if (c_operand_is_ptr_typed(t, addr)) {
+ c_emit_operand(t, addr);
+ } else {
+ cbuf_putc(&t->body, '&');
+ c_local_name(addr.v.local, buf, sizeof buf);
+ cbuf_puts(&t->body, buf);
+ }
+ return;
+ case OPK_GLOBAL: {
+ obj_sym_mark_referenced(t->obj, addr.v.global.sym);
+ cbuf_puts(&t->body, "((char*)&");
+ cbuf_puts(&t->body, c_sym_name(t, addr.v.global.sym));
+ if (addr.v.global.addend != 0) {
+ cbuf_puts(&t->body, " + ");
+ cbuf_put_i64(&t->body, addr.v.global.addend);
+ }
+ cbuf_putc(&t->body, ')');
+ return;
+ }
+ case OPK_INDIRECT:
+ c_emit_indirect_addr_expr(t, c_addr_mode(addr));
+ return;
+ default:
+ compiler_panic(t->c, loc,
+ "C target: copy_bytes address operand kind %d not "
+ "supported",
+ (int)addr.kind);
+ }
+}
+
+void c_emit_set_bytes(CTarget* t, Operand dst_addr, Operand byte_value,
+ AggregateAccess m) {
+ c_assert_no_index(t, dst_addr, "set_bytes dst");
+ /* dst may be a plain integer local from address arithmetic (declared
+ * int64_t); __builtin_memset takes void*, so cast to avoid
+ * -Wint-conversion. */
+ cbuf_puts(&t->body, " __builtin_memset((void*)");
+ c_emit_copy_addr(t, dst_addr);
+ cbuf_puts(&t->body, ", (int)");
+ c_emit_operand(t, byte_value);
+ cbuf_puts(&t->body, ", ");
+ cbuf_put_u64(&t->body, (u64)m.size);
+ 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_emit_tls_addr_of(CTarget* t, Operand dst, ObjSymId sym, i64 addend);
+
+void c_emit_tls_addr_of(CTarget* t, Operand dst, ObjSymId sym, i64 addend) {
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ if (dst.kind != OPK_LOCAL) {
+ compiler_panic(t->c, loc, "C target: tls_addr_of dst must be LOCAL");
+ }
+ c_ensure_local(t, dst.v.local, dst.type);
+ const char* nm = c_sym_name(t, sym);
+ /* RHS spells `(char*)&sym + addend` — pointer type that may not match
+ * dst.type; keep the bridge to cast through cleanly. */
+ c_emit_local_assign_open(t, dst.v.local, (CfreeCgTypeId)0);
+ 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_local_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_emit_bitfield_load(CTarget* t, Operand dst, Operand addr,
+ BitFieldAccess bf);
+void c_emit_bitfield_store(CTarget* 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 an address expression for a backend-addressable lvalue operand.
+ * Unlike c_emit_operand, this never reads the object value; it materializes
+ * the address of the local/global/indirect storage itself. */
+static void c_emit_lvalue_addr_expr_raw(CTarget* t, Operand addr) {
+ char buf[24];
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ switch (addr.kind) {
+ case OPK_LOCAL:
+ cbuf_putc(&t->body, '&');
+ c_ensure_local(t, addr.v.local, addr.type);
+ c_local_name(addr.v.local, buf, sizeof buf);
+ cbuf_puts(&t->body, buf);
+ return;
+ case OPK_GLOBAL: {
+ obj_sym_mark_referenced(t->obj, addr.v.global.sym);
+ const char* nm = c_sym_name(t, addr.v.global.sym);
+ 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_putc(&t->body, ')');
+ return;
+ }
+ case OPK_INDIRECT: {
+ CAddrMode m = c_addr_mode(addr);
+ if ((u32)m.base >= t->local_cap || !t->local_declared[m.base]) {
+ compiler_panic(t->c, loc,
+ "C target: bitfield on undeclared base local v%u",
+ (unsigned)m.base);
+ }
+ cbuf_putc(&t->body, '(');
+ c_emit_indirect_addr_expr(t, m);
+ cbuf_putc(&t->body, ')');
+ return;
+ }
+ default:
+ compiler_panic(t->c, loc,
+ "C target: bitfield address on operand kind %d not "
+ "supported",
+ (int)addr.kind);
+ }
+}
+
+/* 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_lvalue_addr_expr_raw(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_emit_bitfield_load(CTarget* t, Operand dst, Operand addr,
+ BitFieldAccess bf) {
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ if (dst.kind != OPK_LOCAL) {
+ compiler_panic(t->c, loc, "C target: bitfield_load dst must be LOCAL");
+ }
+ c_assert_no_index(t, addr, "bitfield_load");
+ if (bf.bit_width == 0) {
+ /* Zero-width — layout barrier only; nothing to load. Emit a no-op
+ * assignment so the dst local still gets a defined value. */
+ c_ensure_local(t, dst.v.local, dst.type);
+ /* RHS is the literal 0 (int); narrowing to dst.type is fine. */
+ c_emit_local_assign_open(t, dst.v.local, dst.type);
+ cbuf_puts(&t->body, "0");
+ c_emit_local_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_local(t, dst.v.local, dst.type);
+ /* RHS is the storage-width int from the mask/shift expression; bridge
+ * to dst.type so any signedness/width adjustment is explicit. */
+ c_emit_local_assign_open(t, dst.v.local, (CfreeCgTypeId)0);
+ /* 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_local_assign_close(t);
+}
+
+void c_emit_bitfield_store(CTarget* t, Operand addr, Operand src,
+ BitFieldAccess bf) {
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ c_assert_no_index(t, addr, "bitfield_store");
+ 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_emit_asm_block(CTarget* 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_emit_asm_block(CTarget* t, const char* tmpl, const AsmConstraint* outs,
+ u32 no, Operand* oo, const AsmConstraint* ins, u32 ni,
+ const Operand* io, const Sym* clobs, u32 nc) {
+ for (u32 i = 0; i < no; ++i) c_assert_no_index(t, oo[i], "asm_block out");
+ for (u32 i = 0; i < ni; ++i) c_assert_no_index(t, io[i], "asm_block in");
+ 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_slice(t->c->global, outs[i].name).s);
+ 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_LOCAL is a plain C local; this
+ * works directly. OPK_LOCAL / OPK_INDIRECT also produce lvalues. */
+ if (oo[i].kind == OPK_LOCAL) {
+ c_ensure_local(t, oo[i].v.local, oo[i].type);
+ char rb[24];
+ c_local_name(oo[i].v.local, 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_slice(t->c->global, ins[i].name).s);
+ 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_slice(t->c->global, clobs[i]).s);
+ }
+ 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 local via a static const byte array and
+ * __builtin_memcpy so any host C compiler sees the same bit pattern. */
+
+void c_emit_load_const(CTarget* t, Operand dst, ConstBytes cb);
+
+void c_emit_load_const(CTarget* t, Operand dst, ConstBytes cb) {
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ if (dst.kind != OPK_LOCAL) {
+ compiler_panic(t->c, loc, "C target: load_const dst must be LOCAL");
+ }
+ c_ensure_local(t, dst.v.local, dst.type);
+ char buf[24];
+ c_local_name(dst.v.local, 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_emit_atomic_load(CTarget* t, Operand dst, Operand addr, MemAccess m,
+ MemOrder o);
+void c_emit_atomic_store(CTarget* t, Operand addr, Operand src, MemAccess m,
+ MemOrder o);
+void c_emit_atomic_rmw(CTarget* t, AtomicOp op, Operand dst, Operand addr,
+ Operand val, MemAccess m, MemOrder o);
+void c_emit_atomic_cas(CTarget* t, Operand prior, Operand ok, Operand addr,
+ Operand expected, Operand desired, MemAccess m,
+ MemOrder so, MemOrder fo);
+void c_emit_fence(CTarget* t, MemOrder o);
+
+void c_emit_atomic_load(CTarget* t, Operand dst, Operand addr, MemAccess m,
+ MemOrder o) {
+ (void)m;
+ c_assert_no_index(t, addr, "atomic_load");
+ c_ensure_local(t, dst.v.local, dst.type);
+ /* __atomic_load_n returns a value of the pointed-to type (dst.type). */
+ c_emit_local_assign_open(t, dst.v.local, dst.type);
+ 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_local_assign_close(t);
+}
+
+void c_emit_atomic_store(CTarget* t, Operand addr, Operand src, MemAccess m,
+ MemOrder o) {
+ (void)m;
+ c_assert_no_index(t, addr, "atomic_store");
+ 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_emit_atomic_rmw(CTarget* t, AtomicOp op, Operand dst, Operand addr,
+ Operand val, MemAccess m, MemOrder o) {
+ (void)m;
+ SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
+ c_assert_no_index(t, addr, "atomic_rmw");
+ 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_local(t, dst.v.local, dst.type);
+ /* __atomic_fetch_* returns the prior value of the pointed-to type. */
+ c_emit_local_assign_open(t, dst.v.local, val.type);
+ 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_local_assign_close(t);
+}
+
+void c_emit_atomic_cas(CTarget* t, Operand prior, Operand ok, Operand addr,
+ Operand expected, Operand desired, MemAccess m,
+ MemOrder so, MemOrder fo) {
+ (void)m;
+ c_assert_no_index(t, addr, "atomic_cas");
+ /* gcc's __atomic_compare_exchange_n needs a real lvalue holding the
+ * expected value because it is updated on failure. Materialize a scratch
+ * local with the compare type, then copy it out to the prior result. */
+ c_ensure_local(t, prior.v.local, prior.type);
+ c_ensure_local(t, ok.v.local, ok.type);
+ 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_local_name(ok.v.local, ok_name, sizeof ok_name);
+ c_local_name(prior.v.local, 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 local = __cas; */
+ cbuf_puts(&t->body, prior_name);
+ cbuf_puts(&t->body, " = __cas");
+ cbuf_put_u64(&t->body, (u64)id);
+ cbuf_puts(&t->body, "; }\n");
+}
+
+void c_emit_fence(CTarget* t, MemOrder o) {
+ cbuf_puts(&t->body, " __atomic_thread_fence(");
+ cbuf_puts(&t->body, c_memorder_token(o));
+ cbuf_puts(&t->body, ");\n");
+}
+
+/* === set_loc === */
+
+static void cbuf_put_line_filename(CBuf* b, CfreeSlice s) {
+ size_t i;
+ cbuf_putc(b, '"');
+ for (i = 0; i < s.len; ++i) {
+ {
+ unsigned char ch = (unsigned char)s.s[i];
+ switch (ch) {
+ case '\\':
+ case '"':
+ cbuf_putc(b, '\\');
+ cbuf_putc(b, (char)ch);
+ break;
+ case '\n':
+ cbuf_puts(b, "\\n");
+ break;
+ case '\r':
+ cbuf_puts(b, "\\r");
+ break;
+ case '\t':
+ cbuf_puts(b, "\\t");
+ break;
+ default:
+ cbuf_putc(b, (char)ch);
+ break;
+ }
+ }
+ }
+ cbuf_putc(b, '"');
+}
+
+void c_emit_set_loc(CTarget* t, SrcLoc l) {
+ CfreeSlice file;
+
+ if (!t->cur_fn || l.file_id == 0 || l.line == 0) return;
+ if (t->have_emitted_loc && t->emitted_loc.file_id == l.file_id &&
+ t->emitted_loc.line == l.line) {
+ return;
+ }
+
+ file = cfree_compiler_file_name(t->c, l.file_id);
+ if (!file.len) return;
+
+ cbuf_puts(&t->body, "#line ");
+ cbuf_put_u64(&t->body, (u64)l.line);
+ cbuf_putc(&t->body, ' ');
+ cbuf_put_line_filename(&t->body, file);
+ cbuf_putc(&t->body, '\n');
+
+ t->emitted_loc = l;
+ t->have_emitted_loc = 1;
+}
+
+/* === 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 }");
+}
+
+/* Mach-O TLS support: the user-visible SK_TLS symbol is a 24-byte TLV
+ * descriptor in __DATA,__thread_vars, and the actual initial bytes live in
+ * a synthesized `<name>$tlv$init` sym in __thread_data (or __thread_bss).
+ * The descriptor carries an R_ABS64 reloc at offset +16 pointing at that
+ * init sym. For C-source emission we don't care about the descriptor at all
+ * — we just emit `_Thread_local` with the init sym's bytes and let the host
+ * C compiler synthesize whatever TLV plumbing it needs. */
+
+/* Find the data init sym referenced by a Mach-O TLS descriptor at
+ * `desc_base` in section `desc_sec`. Looks for an R_ABS64 reloc at
+ * `desc_base + 16`. Returns OBJ_SYM_NONE if not found. */
+static ObjSymId c_macho_tls_find_init(CTarget* t, ObjSecId desc_sec,
+ u32 desc_base) {
+ 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 != desc_sec) continue;
+ if (r->offset != desc_base + 16u) continue;
+ return r->sym;
+ }
+ return OBJ_SYM_NONE;
+}
+
+/* Returns 1 if the section is __DATA,__thread_vars (the descriptor section
+ * on Mach-O). Compared by interned Sym id. */
+static int c_sec_name_is_macho_tvars(CTarget* t, const Section* sec) {
+ if (!sec) return 0;
+ Sym tvars =
+ pool_intern_slice(t->c->global, SLICE_LIT("__DATA,__thread_vars"));
+ return sec->name == tvars;
+}
+
+/* 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;
+}
+
+/* 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 (c_is_local_static_sym(t, id)) return;
+ if (os->kind == SK_FUNC || os->kind == SK_IFUNC) return;
+ if (os->kind == SK_SECTION || os->kind == SK_FILE) return;
+ /* On Mach-O, obj_tls.c synthesizes `__tlv_bootstrap` as an SK_UNDEF
+ * extern for the TLV descriptor's first field. The C target delegates all
+ * TLS lowering to the host compiler via `_Thread_local`, so this
+ * descriptor-time-only symbol has no place in the emitted source. */
+ if (os->kind == SK_UNDEF && t->c->target.obj == CFREE_OBJ_MACHO) {
+ const ObjBuilder* ob = t->obj;
+ if (id == obj_tlv_bootstrap_get(ob)) 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. */
+ /* Extern: SK_UNDEF, or any other defined-kind sym that the producer
+ * marked as having no defining section (the C frontend uses SK_OBJ +
+ * section=NONE for `extern T x __attribute__((weak));`). */
+ int is_extern = (os->kind == SK_UNDEF) ||
+ (os->kind != SK_COMMON && os->section_id == OBJ_SEC_NONE);
+ if (is_extern) {
+ /* Always declare extern data syms in C source: the host cc tolerates
+ * unused externs, and the ObjSym::referenced bit isn't reliably set on
+ * syms the C target only addresses by writing the name into the source
+ * (no relocation gets emitted against them).
+ *
+ * Weak externs need different attributes per object format: on Mach-O
+ * the `weak` attribute requires a definition; the right spelling for an
+ * undefined weak ref is `__attribute__((weak_import))`. On ELF/PE the
+ * existing `weak` attribute works as expected. */
+ if (os->bind == SB_WEAK) {
+ if (t->c->target.obj == CFREE_OBJ_MACHO) {
+ cbuf_puts(b, "__attribute__((weak_import)) ");
+ } else {
+ 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\"))) ");
+ }
+ 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 splits TLS across two object-file symbols (see obj_tls.c): the
+ * user-visible sym is a 24-byte TLV descriptor in
+ * __DATA,__thread_vars; the actual initial bytes live in a synthesized
+ * `<name>$tlv$init` sym in __DATA,__thread_data (or __thread_bss). For
+ * C source emission we don't need either of those — `_Thread_local`
+ * delegates to the host C compiler, which builds its own descriptor.
+ *
+ * We use the descriptor sym as the carrier (its name is what user code
+ * references) and pull the initial bytes/size/alignment from the init
+ * sym, found via the R_ABS64 reloc at descriptor offset +16. The init
+ * sym is skipped in its own iteration. */
+ const Section* desc_sec = obj_section_get(t->obj, os->section_id);
+ if (c_sec_name_is_macho_tvars(t, desc_sec)) {
+ ObjSymId init_id =
+ c_macho_tls_find_init(t, os->section_id, (u32)os->value);
+ if (init_id == OBJ_SYM_NONE) {
+ compiler_panic(t->c, (SrcLoc){0, 0, 0},
+ "C target: Mach-O TLS descriptor missing init reloc");
+ }
+ const ObjSym* init_os = obj_symbol_get(t->obj, init_id);
+ if (!init_os || init_os->section_id == OBJ_SEC_NONE) {
+ compiler_panic(t->c, (SrcLoc){0, 0, 0},
+ "C target: Mach-O TLS init sym not defined");
+ }
+ const Section* init_sec = obj_section_get(t->obj, init_os->section_id);
+ u32 init_base = (u32)init_os->value;
+ u32 init_size = (u32)init_os->size;
+ /* TLS data with relocations would need the constructor-fixup path
+ * (and we'd have to rewrite the reloc target's section/offset to
+ * the descriptor's name in the emitted C). No test currently
+ * exercises this; surface it as a clear panic-as-skip if we hit it. */
+ if (c_sym_has_relocs(t, init_os->section_id, init_base, init_size)) {
+ compiler_panic(t->c, (SrcLoc){0, 0, 0},
+ "C target: Mach-O TLS with pointer init not yet "
+ "supported");
+ }
+ if (os->bind == SB_LOCAL) cbuf_puts(b, "static ");
+ cbuf_puts(b, "_Thread_local ");
+ c_emit_link_attrs(b, os);
+ cbuf_puts(b, "__attribute__((unused)) ");
+ cbuf_puts(b, "_Alignas(");
+ cbuf_put_u64(b, init_sec->align ? init_sec->align : 1);
+ cbuf_puts(b, ") uint8_t ");
+ cbuf_puts(b, nm);
+ cbuf_puts(b, "[");
+ cbuf_put_u64(b, init_size ? init_size : 1);
+ cbuf_puts(b, "]");
+ if (init_sec->kind == SEC_BSS || init_sec->sem == SSEM_NOBITS ||
+ init_size == 0) {
+ cbuf_puts(b, ";\n");
+ } else {
+ Heap* h = t->c->ctx->heap;
+ u8* bytes = (u8*)h->alloc(h, init_size, 1);
+ if (!bytes) {
+ compiler_panic(t->c, (SrcLoc){0, 0, 0},
+ "C target: oom on TLS init bytes");
+ }
+ c_read_section_bytes(init_sec, init_base, bytes, init_size);
+ c_emit_data_bytes(b, bytes, init_size);
+ h->free(h, bytes, init_size);
+ cbuf_puts(b, ";\n");
+ }
+ return;
+ }
+ /* Not the descriptor: this is the synthesized `<name>$tlv$init` data
+ * sym (or a __thread_ptrs entry). The descriptor case above already
+ * emitted the user-facing _Thread_local; nothing more to do. */
+ return;
+ }
+ 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, "__attribute__((unused)) _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;
+ u32 nrelocs = 0;
+ u32 total_relocs = obj_reloc_total(t->obj);
+ for (u32 i = 0; i < total_relocs; ++i) {
+ const Reloc* r = obj_reloc_at(t->obj, i);
+ if (r->section_id == os->section_id && r->offset >= base &&
+ r->offset < base + size) {
+ nrelocs++;
+ }
+ }
+
+ Heap* h = t->c->ctx->heap;
+ const Reloc** rs = NULL;
+ if (nrelocs) {
+ rs = (const Reloc**)h->alloc(h, nrelocs * sizeof(const Reloc*), 1);
+ u32 j = 0;
+ for (u32 i = 0; i < total_relocs; ++i) {
+ const Reloc* r = obj_reloc_at(t->obj, i);
+ if (r->section_id == os->section_id && r->offset >= base &&
+ r->offset < base + size) {
+ rs[j++] = r;
+ }
+ }
+ for (u32 i = 1; i < nrelocs; ++i) {
+ const Reloc* tmp = rs[i];
+ u32 k = i;
+ while (k > 0 && rs[k - 1]->offset > tmp->offset) {
+ rs[k] = rs[k - 1];
+ k--;
+ }
+ rs[k] = tmp;
+ }
+ }
+
+ cbuf_puts(b, "struct ");
+ if (nrelocs > 0) cbuf_puts(b, "__attribute__((packed)) ");
+ cbuf_puts(b, "__cfree_data_");
+ cbuf_puts(b, nm);
+ cbuf_puts(b, " {\n");
+
+ if (nrelocs == 0) {
+ cbuf_puts(b, " uint8_t raw[");
+ cbuf_put_u64(b, size ? size : 1);
+ cbuf_puts(b, "];\n");
+ } else {
+ u32 cur = base;
+ for (u32 i = 0; i < nrelocs; ++i) {
+ const Reloc* r = rs[i];
+ if (r->offset > cur) {
+ cbuf_puts(b, " uint8_t chunk_");
+ cbuf_put_u64(b, i);
+ cbuf_puts(b, "[");
+ cbuf_put_u64(b, r->offset - cur);
+ cbuf_puts(b, "];\n");
+ }
+ u32 width = (r->kind == R_ABS32) ? 4 : 8;
+ const char* ty = (width == 4) ? "uint32_t" : "void*";
+ cbuf_puts(b, " ");
+ cbuf_puts(b, ty);
+ cbuf_puts(b, " ptr_");
+ cbuf_put_u64(b, i);
+ cbuf_puts(b, ";\n");
+ cur = r->offset + width;
+ }
+ if (cur < base + size) {
+ cbuf_puts(b, " uint8_t chunk_");
+ cbuf_put_u64(b, nrelocs);
+ cbuf_puts(b, "[");
+ cbuf_put_u64(b, base + size - cur);
+ cbuf_puts(b, "];\n");
+ }
+ }
+ cbuf_puts(b, "};\n");
+
+ if (os->bind == SB_LOCAL) cbuf_puts(b, "static ");
+ if (is_tls) cbuf_puts(b, "_Thread_local ");
+ c_emit_link_attrs(b, os);
+ cbuf_puts(b, "__attribute__((unused)) ");
+
+ int is_ro = (sec->kind == SEC_RODATA);
+ if (is_ro) cbuf_puts(b, "const ");
+
+ cbuf_puts(b, "_Alignas(");
+ cbuf_put_u64(b, sec->align ? sec->align : 1);
+ cbuf_puts(b, ") struct __cfree_data_");
+ cbuf_puts(b, nm);
+ cbuf_puts(b, " ");
+ cbuf_puts(b, nm);
+
+ if (sec->kind == SEC_BSS || sec->sem == SSEM_NOBITS) {
+ cbuf_puts(b, ";\n");
+ } else if (size == 0) {
+ cbuf_puts(b, " = {{0}};\n");
+ } else {
+ cbuf_puts(b, " = {\n");
+ u8* bytes = (u8*)h->alloc(h, size, 1);
+ c_read_section_bytes(sec, base, bytes, size);
+
+ if (nrelocs == 0) {
+ cbuf_puts(b, " .raw = {");
+ for (u32 i = 0; i < size; ++i) {
+ if (i > 0) cbuf_puts(b, ", ");
+ cbuf_put_u64(b, bytes[i]);
+ }
+ cbuf_puts(b, "}\n");
+ } else {
+ u32 cur = base;
+ for (u32 i = 0; i < nrelocs; ++i) {
+ const Reloc* r = rs[i];
+ if (r->offset > cur) {
+ cbuf_puts(b, " .chunk_");
+ cbuf_put_u64(b, i);
+ cbuf_puts(b, " = {");
+ for (u32 k = 0; k < r->offset - cur; ++k) {
+ if (k > 0) cbuf_puts(b, ", ");
+ cbuf_put_u64(b, bytes[cur - base + k]);
+ }
+ cbuf_puts(b, "},\n");
+ }
+
+ u32 width = (r->kind == R_ABS32) ? 4 : 8;
+ c_ensure_forward_decl(t, r->sym, 0);
+ const char* tgt = c_sym_name(t, r->sym);
+ const char* cast = (width == 4) ? "(uint32_t)(uintptr_t)" : "(void*)";
+
+ cbuf_puts(b, " .ptr_");
+ cbuf_put_u64(b, i);
+ cbuf_puts(b, " = ");
+ cbuf_puts(b, cast);
+ cbuf_puts(b, "((char*)&");
+ cbuf_puts(b, tgt);
+ if (r->addend != 0) {
+ cbuf_puts(b, " + ");
+ cbuf_put_i64(b, r->addend);
+ }
+ cbuf_puts(b, "),\n");
+ cur = r->offset + width;
+ }
+ if (cur < base + size) {
+ cbuf_puts(b, " .chunk_");
+ cbuf_put_u64(b, nrelocs);
+ cbuf_puts(b, " = {");
+ for (u32 k = 0; k < base + size - cur; ++k) {
+ if (k > 0) cbuf_puts(b, ", ");
+ cbuf_put_u64(b, bytes[cur - base + k]);
+ }
+ cbuf_puts(b, "}\n");
+ }
+ }
+ h->free(h, bytes, size);
+ cbuf_puts(b, "};\n");
+ }
+
+ if (nrelocs) h->free(h, (void*)rs, nrelocs * sizeof(const Reloc*));
+}
+
+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_emit_finalize(CTarget* t) {
+ if (t->finalized) return;
+ t->finalized = 1;
+ c_emit_prologue(t);
+ if (t->need_stdarg) c_writer_puts(t, "#include <stdarg.h>\n");
+ if (t->need_setjmp) c_writer_puts(t, "#include <setjmp.h>\n");
+ if (t->need_stdarg || t->need_setjmp) c_writer_puts(t, "\n");
+ if (t->typedefs.len) {
+ c_writer_write(t, t->typedefs.data, t->typedefs.len);
+ c_writer_puts(t, "\n");
+ }
+ c_emit_data(t);
+ if (t->forwards.len) {
+ c_writer_write(t, t->forwards.data, t->forwards.len);
+ c_writer_puts(t, "\n");
+ }
+ 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);
+}
+
+void c_emit_destroy(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);
+ t->sym_forwarded = NULL;
+ t->sym_forwarded_cap = 0;
+ if (t->local_static_syms) {
+ h->free(h, t->local_static_syms,
+ t->local_static_syms_cap * sizeof(*t->local_static_syms));
+ }
+ if (t->local_static_entries) {
+ h->free(h, t->local_static_entries,
+ t->local_static_entries_cap * sizeof(*t->local_static_entries));
+ }
+ if (t->local_declared) h->free(h, t->local_declared, t->local_cap);
+ if (t->local_type)
+ h->free(h, t->local_type, t->local_cap * sizeof(CfreeCgTypeId));
+ if (t->scopes) h->free(h, t->scopes, t->scopes_cap * sizeof(CScopeInfo));
+ t->local_declared = NULL;
+ t->local_type = NULL;
+ t->scopes = NULL;
+ t->local_static_syms = NULL;
+ t->local_static_entries = NULL;
+ t->local_cap = 0;
+ t->scopes_cap = 0;
+ t->local_static_syms_cap = 0;
+ t->local_static_entries_cap = 0;
+ t->local_static_nsyms = 0;
+ t->local_static_nentries = 0;
+}
diff --git a/src/arch/c_target/c_emit.h b/src/arch/c_target/c_emit.h
@@ -0,0 +1,256 @@
+#ifndef CFREE_C_TARGET_C_EMIT_H
+#define CFREE_C_TARGET_C_EMIT_H
+
+/* C-source emission core. See doc/CBACKEND.md.
+ *
+ * This target is selected when CodeOptions.emit_c_source is set. It writes
+ * target-locked C source to CodeOptions.c_source_writer instead of object
+ * bytes. Operates with semantic temporary locals minted by CG. */
+
+#include <cfree/core.h>
+
+#include "cg/cgtarget.h"
+#include "core/core.h"
+
+typedef CGLocal CLocal;
+
+/* Heap-backed growable byte buffer. Used for the per-function declaration
+ * and body buffers; CG needs decls at function top but doesn't surface them
+ * before body emission, so we accumulate both and flush at func_end. */
+typedef struct CBuf {
+ Heap* heap;
+ u8* data;
+ size_t len;
+ size_t cap;
+} CBuf;
+
+void cbuf_init(CBuf* b, Heap* h);
+void cbuf_fini(CBuf* b);
+void cbuf_reset(CBuf* b);
+void cbuf_putc(CBuf* b, char c);
+void cbuf_puts(CBuf* b, const char* s);
+void cbuf_putn(CBuf* b, const char* s, size_t n);
+void cbuf_put_i64(CBuf* b, i64 v);
+void cbuf_put_u64(CBuf* b, u64 v);
+
+typedef struct CLocalStaticLabelEntry {
+ Label label;
+ i64 addend;
+ u8 has_label;
+ u8 pad[3];
+} CLocalStaticLabelEntry;
+
+typedef struct CTarget {
+ Compiler* c;
+ ObjBuilder* obj;
+ CfreeWriter* w;
+
+ /* TU prologue (e.g. #include <stdint.h>) emitted once, on first
+ * function or finalize, whichever first. */
+ u8 prologue_emitted;
+ u8 finalized;
+ u8 pad[2];
+
+ /* TU-wide forward declarations: one `RetT name(params);` line per function
+ * we've seen a definition or call for. Emitted just after the prologue so
+ * callers defined earlier in the TU still see the prototype, and calls to
+ * undefined-in-TU externs get a declaration too. */
+ CBuf forwards;
+ /* Forward-decl dedup: which ObjSymIds have we already declared. Lazily
+ * grown bitmap indexed by ObjSymId. */
+ u8* sym_forwarded;
+ u32 sym_forwarded_cap;
+
+ /* 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;
+ /* Tracks whether <stdarg.h> needs to be emitted in the prologue. Set when
+ * c_typename first encounters VARARG_STATE. Flushed lazily at finalize. */
+ u8 need_stdarg;
+ u8 need_setjmp;
+ u8 pad2[2];
+
+ /* Per-function buffers. Reset on func_end. */
+ CBuf decls;
+ CBuf body;
+
+ /* Function-local static data consumed from CG's narrow source-backend data
+ * hook. These symbols are emitted inside the owning function and skipped by
+ * the TU-wide object-data walker. */
+ ObjSymId* local_static_syms;
+ u32 local_static_nsyms;
+ u32 local_static_syms_cap;
+
+ CLocalStaticLabelEntry* local_static_entries;
+ u32 local_static_nentries;
+ u32 local_static_entries_cap;
+ ObjSymId local_static_sym;
+ CfreeCgTypeId local_static_type;
+ u64 local_static_count;
+ u64 local_static_offset;
+ u32 local_static_ptr_width;
+ u32 local_static_align;
+ u8 local_static_active;
+ u8 local_static_is_array;
+ u8 local_static_readonly;
+ u8 pad_local_static;
+
+ /* Per-function local-decl tracking: for each local id seen, mark whether we
+ * have already emitted a declaration into `decls`. Sized by local_cap.
+ * Grown lazily as new local ids appear. */
+ u8* local_declared;
+ CfreeCgTypeId* local_type; /* declared type for each semantic local */
+ u32 local_cap;
+
+ /* Splice bookmark: byte offset into body where the current function's body
+ * region starts (right after the open brace). func_end uses this to insert
+ * the per-function declarations between the signature and the body. */
+ size_t fn_body_start;
+
+ const CGFuncDesc* cur_fn;
+
+ /* Most recent source line directive emitted into the current function body.
+ * Reset on func_begin; used by c_set_loc to avoid noisy duplicate #line
+ * directives. */
+ SrcLoc emitted_loc;
+ u8 have_emitted_loc;
+ u8 pad_cur_loc[3];
+
+ /* Label minting: ids 1..next_label. 0 is reserved as LABEL_NONE. */
+ u32 next_label;
+ u32 next_local;
+
+ /* Stack of active scopes. CGScope handles are (scope_index + 1). */
+ struct CScopeInfo* scopes;
+ u32 scopes_cap;
+ u32 nscopes;
+
+ /* Monotone counter for synthesizing unique temporary names within a
+ * function (e.g. bitcast scratch). Reset on func_begin. */
+ u32 next_tmp;
+
+ /* Tracks whether the last emitted body statement was an unconditional
+ * terminator (`return`, `goto`, etc.) with no intervening label. When
+ * set, subsequent ret/jump/cmp_branch emissions are dropped — they're
+ * unreachable, and emitting them produces dead C that distracts from
+ * the live code. Reset on func_begin and label_place. */
+ u8 last_was_terminator;
+ u8 pad3[3];
+} CTarget;
+
+typedef struct CScopeInfo {
+ u8 kind; /* ScopeKind */
+ /* Set when the C target emitted a `for (;;) { ... }` wrapper around
+ * this scope. Drives jump/cmp_branch's translation of break/continue
+ * labels into C `break;`/`continue;` and scope_end's `}`. */
+ u8 structured;
+ u8 pad[2];
+ Label break_label;
+ Label continue_label;
+} CScopeInfo;
+
+void c_emit_target_init(CTarget* t, Compiler* c, ObjBuilder* o, CfreeWriter* w);
+CTarget* c_emit_target_new(Compiler* c, ObjBuilder* o, CfreeWriter* w);
+
+void c_emit_prologue(CTarget* t);
+/* Ensure local `r` (typed `type`) has been declared. */
+void c_ensure_local(CTarget* t, CLocal r, CfreeCgTypeId type);
+/* Get a stable C identifier for local r. Writes into caller-supplied buf. */
+void c_local_name(CLocal r, char* out, size_t cap);
+/* Write the C type for a CG int/float/ptr type to `b`. */
+void c_emit_type(CTarget* t, CBuf* b, CfreeCgTypeId type);
+/* Write operand expression to body (e.g. "v3", "(int32_t)42"). Supports
+ * OPK_LOCAL / OPK_IMM / OPK_GLOBAL. INDIRECT is only valid in
+ * lvalue positions and is emitted via load/store/addr_of paths. */
+void c_emit_operand(CTarget* t, Operand op);
+/* Like c_emit_operand but wraps in an explicit signed/unsigned cast of the
+ * operand's type width. For integer ops where signedness affects semantics
+ * (UDIV/UREM, SHR_U, unsigned compares). Falls back to c_emit_operand when
+ * the operand is not integer-typed. */
+void c_emit_operand_signed(CTarget* t, Operand op, int signed_);
+
+/* Lookup the C linker name for an ObjSymId. Returns interned string. */
+const char* c_sym_name(CTarget* t, ObjSymId sym);
+
+/* Emit a forward declaration for `sym` (of function type `fn_type`) into
+ * the TU forwards buffer if not already done. Idempotent per sym. */
+void c_ensure_forward_decl(CTarget* t, ObjSymId sym, CfreeCgTypeId fn_type);
+
+/* Write `n` bytes to t->w; panic on error. */
+void c_writer_write(CTarget* t, const void* data, size_t n);
+void c_writer_puts(CTarget* t, const char* s);
+
+void c_emit_func_begin(CTarget*, const CGFuncDesc*);
+void c_emit_func_end(CTarget*);
+void c_emit_alias(CTarget*, ObjSymId, ObjSymId, CfreeCgTypeId);
+void c_emit_ret(CTarget*, const CGLocal*, u32);
+void c_emit_load_imm(CTarget*, Operand, i64);
+void c_emit_load_const(CTarget*, Operand, ConstBytes);
+void c_emit_copy(CTarget*, Operand, Operand);
+void c_emit_binop(CTarget*, BinOp, Operand, Operand, Operand);
+void c_emit_unop(CTarget*, UnOp, Operand, Operand);
+void c_emit_cmp(CTarget*, CmpOp, Operand, Operand, Operand);
+void c_emit_convert(CTarget*, ConvKind, Operand, Operand);
+void c_emit_call(CTarget*, const CGCallDesc*);
+const char* c_emit_tail_call_unrealizable_reason(CTarget*, const CGCallDesc*);
+const char* c_emit_tail_call_unrealizable_reason_for(CTarget*,
+ const CGFuncDesc*,
+ const CGCallDesc*);
+void c_emit_load(CTarget*, Operand, Operand, MemAccess);
+void c_emit_store(CTarget*, Operand, Operand, MemAccess);
+void c_emit_addr_of(CTarget*, Operand, Operand);
+CGLocal c_emit_param(CTarget*, const CGParamDesc*);
+CGLocal c_emit_local(CTarget*, const CGLocalDesc*);
+void c_emit_param_bind(CTarget*, CGLocal, CfreeCgTypeId, u32);
+void c_emit_local_addr(CTarget*, Operand, const CGLocalDesc*, CGLocal);
+Label c_emit_label_new(CTarget*);
+void c_emit_label_place(CTarget*, Label);
+void c_emit_jump(CTarget*, Label);
+void c_emit_cmp_branch(CTarget*, CmpOp, Operand, Operand, Label);
+void c_emit_switch_(CTarget*, const CGSwitchDesc*);
+void c_emit_indirect_branch(CTarget*, Operand, const Label*, u32);
+void c_emit_load_label_addr(CTarget*, Operand, Label);
+int c_emit_local_static_data_begin(CTarget*, const CGLocalStaticDataDesc*);
+int c_emit_can_local_static_data(CTarget*, const CGLocalStaticDataDesc*);
+void c_emit_local_static_data_write(CTarget*, const u8*, u64);
+void c_emit_local_static_data_label_addr(CTarget*, Label, i64, u32, u32);
+void c_emit_local_static_data_end(CTarget*);
+CGScope c_emit_scope_begin(CTarget*, const CGScopeDesc*);
+void c_emit_scope_else(CTarget*, CGScope);
+void c_emit_scope_end(CTarget*, CGScope);
+void c_emit_break_to(CTarget*, CGScope);
+void c_emit_continue_to(CTarget*, CGScope);
+void c_emit_set_loc(CTarget*, SrcLoc);
+void c_emit_finalize(CTarget*);
+void c_emit_destroy(CTarget*);
+void c_emit_intrinsic(CTarget*, IntrinKind, Operand*, u32, const Operand*, u32);
+void c_emit_alloca(CTarget*, Operand, Operand, u32);
+void c_emit_va_start(CTarget*, Operand);
+void c_emit_va_arg(CTarget*, Operand, Operand, CfreeCgTypeId);
+void c_emit_va_end(CTarget*, Operand);
+void c_emit_va_copy(CTarget*, Operand, Operand);
+void c_emit_copy_bytes(CTarget*, Operand, Operand, AggregateAccess);
+void c_emit_set_bytes(CTarget*, Operand, Operand, AggregateAccess);
+void c_emit_asm_block(CTarget*, const char*, const AsmConstraint*, u32,
+ Operand*, const AsmConstraint*, u32, const Operand*,
+ const Sym*, u32);
+void c_emit_bitfield_load(CTarget*, Operand, Operand, BitFieldAccess);
+void c_emit_bitfield_store(CTarget*, Operand, Operand, BitFieldAccess);
+void c_emit_tls_addr_of(CTarget*, Operand, ObjSymId, i64);
+void c_emit_atomic_load(CTarget*, Operand, Operand, MemAccess, MemOrder);
+void c_emit_atomic_store(CTarget*, Operand, Operand, MemAccess, MemOrder);
+void c_emit_atomic_rmw(CTarget*, AtomicOp, Operand, Operand, Operand, MemAccess,
+ MemOrder);
+void c_emit_atomic_cas(CTarget*, Operand, Operand, Operand, Operand, Operand,
+ MemAccess, MemOrder, MemOrder);
+void c_emit_fence(CTarget*, MemOrder);
+
+#endif
diff --git a/src/arch/c_target/cbuf.c b/src/arch/c_target/cbuf.c
@@ -3,7 +3,7 @@
* declarations and body emission interleaved; we accumulate both and flush
* at func_end. */
-#include "arch/c_target/internal.h"
+#include "arch/c_target/c_emit.h"
#include "core/heap.h"
enum { CBUF_MIN_CAP = 256 };
diff --git a/src/arch/c_target/emit.c b/src/arch/c_target/emit.c
@@ -1,3830 +0,0 @@
-/* C-source emission for the CgTarget vtable. See doc/CBACKEND.md.
- *
- * Output strategy
- * ---------------
- * Each function buffers two CBufs while CG walks the body:
- * decls — variable declarations: " long long v3;\n"
- * body — TU-wide running output; we accumulate signature/body/closing-brace
- * across all functions; func_end splices decls in after the open
- * brace using the recorded fn_body_start bookmark.
- *
- * c_finalize flushes a tiny prologue + body to the writer.
- *
- * Local declaration is lazy: every operand emit goes through c_ensure_local,
- * which appends one declaration for each semantic local. */
-
-#include <stdio.h>
-#include <string.h>
-
-#include "arch/c_target/internal.h"
-#include "cg/type.h"
-#include "core/core.h"
-#include "core/heap.h"
-#include "core/pool.h"
-#include "core/slice.h"
-#include "obj/obj.h"
-
-/* Forward decls. */
-static void c_ensure_typedef(CTarget* t, CfreeCgTypeId tid);
-static const char* c_typedef_name(CTarget* t, CfreeCgTypeId tid);
-static const char* c_typename(CTarget* t, CfreeCgTypeId type);
-static CfreeCgTypeId c_local_type_or_panic(CTarget* t, CGLocal local);
-static void c_ensure_tuple_typedef(CTarget* t, const CfreeCgTypeId* types,
- u32 ntypes);
-static void c_emit_tuple_type_name(CTarget* t, CBuf* b,
- const CfreeCgTypeId* types, u32 ntypes);
-static Operand c_op_local(CGLocal local, CfreeCgTypeId type);
-static int c_type_is_aggregate(CTarget* t, CfreeCgTypeId type);
-static int c_type_is_bool(CTarget* t, CfreeCgTypeId type);
-static int c_type_is_ptr(CTarget* t, CfreeCgTypeId type);
-static int c_operand_is_ptr_typed(CTarget* t, Operand op);
-static void c_emit_addr_deref(CTarget* t, Operand addr,
- CfreeCgTypeId access_type);
-static void c_emit_copy_addr(CTarget* t, Operand addr);
-CGLocal c_local(CgTarget* T, const CGLocalDesc* d);
-static void c_ensure_forward_decl_with_results(CTarget* t, ObjSymId sym,
- CfreeCgTypeId fn_type,
- const CfreeCgTypeId* result_types,
- u32 nresults);
-/* Private accessor on ObjBuilder (defined in obj/obj.c, not in obj.h).
- * Same forward-decl trick as obj_tls.c uses. */
-ObjSymId obj_tlv_bootstrap_get(const ObjBuilder*);
-
-/* === Writer helpers === */
-
-void c_writer_write(CTarget* t, const void* data, size_t n) {
- CfreeStatus st = cfree_writer_write(t->w, data, n);
- if (st != CFREE_OK) {
- SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
- compiler_panic(t->c, loc, "C target: writer error %d", (int)st);
- }
-}
-
-void c_writer_puts(CTarget* t, const char* s) {
- size_t n = 0;
- while (s[n]) ++n;
- c_writer_write(t, s, n);
-}
-
-/* === CLocal / type emission === */
-
-static const char* c_int_type_name_for_width(u32 width, int signed_) {
- switch (width) {
- case 1:
- case 8:
- return signed_ ? "int8_t" : "uint8_t";
- case 16:
- return signed_ ? "int16_t" : "uint16_t";
- case 32:
- return signed_ ? "int32_t" : "uint32_t";
- case 64:
- return signed_ ? "int64_t" : "uint64_t";
- case 128:
- return signed_ ? "__int128" : "unsigned __int128";
- default:
- return NULL;
- }
-}
-
-/* Returns the integer width for sign-aware emission. 0 if the type isn't a
- * fixed-width integer (float, ptr, void, aggregate). */
-static u32 c_int_width_for_signedness(CTarget* t, CfreeCgTypeId type) {
- if (type == CFREE_CG_TYPE_NONE) return 0;
- CfreeCgTypeId u = api_unalias_type(t->c, type);
- const CgType* ty = cg_type_get(t->c, u);
- if (!ty) return 0;
- if (ty->kind == CFREE_CG_TYPE_INT) return ty->integer.width;
- if (ty->kind == CFREE_CG_TYPE_BOOL) return 32; /* bool maps to int32_t */
- return 0;
-}
-
-/* === Typedef machinery ===
- *
- * Composite types (records, arrays, function types) are emitted as opaque
- * byte-storage typedefs in a TU-wide typedefs section. The typedef name is
- * `__ty_<id>` keyed on the unaliased type id; this is stable for the
- * compiler instance.
- *
- * For records and arrays the typedef wraps a single `_Alignas(A) uint8_t
- * raw[N];` member, so all field/element access is mediated by the existing
- * `(*(T*)((char*)addr + ofs))` path. This sidesteps any ABI ambiguity (C
- * bitfield rules, array decay, packed/aligned attribute interactions) and
- * keeps types orthogonal to access patterns.
- *
- * For function types we emit a function-pointer typedef `R (*__ty_N)(...)`,
- * used for indirect calls and function-pointer-typed values. */
-
-static void c_grow_type_state(CTarget* t, u32 needed) {
- Heap* h = t->c->ctx->heap;
- u32 newcap = t->type_state_cap ? t->type_state_cap : 32;
- while (newcap < needed) newcap *= 2;
- u8* nd = (u8*)h->realloc(h, t->type_state, t->type_state_cap, newcap, 1);
- if (!nd && newcap) {
- compiler_panic(t->c, (SrcLoc){0, 0, 0}, "C target: out of memory");
- }
- for (u32 i = t->type_state_cap; i < newcap; ++i) nd[i] = 0;
- t->type_state = nd;
- t->type_state_cap = newcap;
-}
-
-static const char* c_typedef_name(CTarget* t, CfreeCgTypeId tid) {
- CfreeCgTypeId u = api_unalias_type(t->c, tid);
- char buf[32];
- int n = snprintf(buf, sizeof buf, "__ty_%u", (unsigned)u);
- Sym s = pool_intern_slice(t->c->global, (Slice){.s = buf, .len = (size_t)n});
- return pool_slice(t->c->global, s).s;
-}
-
-/* Forward decl. */
-static void c_emit_typedef_for_func(CTarget* t, CfreeCgTypeId tid,
- const CgType* ty);
-
-static void c_ensure_typedef(CTarget* t, CfreeCgTypeId tid) {
- CfreeCgTypeId u = api_unalias_type(t->c, tid);
- if ((u32)u >= t->type_state_cap) c_grow_type_state(t, (u32)u + 1u);
- if (t->type_state[u] >= 2) return;
- if (t->type_state[u] == 1) return; /* cyclic — emit forward-only */
- t->type_state[u] = 1;
- const CgType* ty = cg_type_get(t->c, u);
- SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
- if (!ty)
- compiler_panic(t->c, loc, "C target: unknown type id %u", (unsigned)u);
- switch (ty->kind) {
- case CFREE_CG_TYPE_FUNC:
- c_emit_typedef_for_func(t, u, ty);
- break;
- case CFREE_CG_TYPE_RECORD: {
- /* Recurse on field types so any composite-typed field has its
- * typedef emitted first. (Records-by-value are accessed only via
- * pointer arithmetic in cfree CG, but emitting deps first keeps the
- * output readable and stable.) */
- for (u32 i = 0; i < ty->record.nfields; ++i) {
- if (!(ty->record.fields[i].flags & CFREE_CG_FIELD_BITFIELD)) {
- CfreeCgTypeId ft = ty->record.fields[i].type;
- CfreeCgTypeId ftu = api_unalias_type(t->c, ft);
- const CgType* fty = cg_type_get(t->c, ftu);
- if (fty && (fty->kind == CFREE_CG_TYPE_RECORD ||
- fty->kind == CFREE_CG_TYPE_ARRAY ||
- fty->kind == CFREE_CG_TYPE_FUNC)) {
- c_ensure_typedef(t, ftu);
- }
- }
- }
- cbuf_puts(&t->typedefs, "typedef struct { _Alignas(");
- cbuf_put_u64(&t->typedefs, (u64)cg_type_align(t->c, u));
- cbuf_puts(&t->typedefs, ") uint8_t raw[");
- cbuf_put_u64(&t->typedefs, cg_type_size(t->c, u));
- cbuf_puts(&t->typedefs, "]; } __ty_");
- cbuf_put_u64(&t->typedefs, (u64)u);
- cbuf_puts(&t->typedefs, ";\n");
- break;
- }
- case CFREE_CG_TYPE_ARRAY: {
- CfreeCgTypeId eu = api_unalias_type(t->c, ty->array.elem);
- const CgType* ety = cg_type_get(t->c, eu);
- if (ety && (ety->kind == CFREE_CG_TYPE_RECORD ||
- ety->kind == CFREE_CG_TYPE_ARRAY ||
- ety->kind == CFREE_CG_TYPE_FUNC)) {
- c_ensure_typedef(t, eu);
- }
- cbuf_puts(&t->typedefs, "typedef struct { _Alignas(");
- cbuf_put_u64(&t->typedefs, (u64)cg_type_align(t->c, u));
- cbuf_puts(&t->typedefs, ") uint8_t raw[");
- cbuf_put_u64(&t->typedefs, cg_type_size(t->c, u));
- cbuf_puts(&t->typedefs, "]; } __ty_");
- cbuf_put_u64(&t->typedefs, (u64)u);
- cbuf_puts(&t->typedefs, ";\n");
- break;
- }
- default:
- compiler_panic(t->c, loc,
- "C target: c_ensure_typedef on non-composite kind %d",
- (int)ty->kind);
- }
- t->type_state[u] = 2;
-}
-
-static void c_emit_typedef_for_func(CTarget* t, CfreeCgTypeId tid,
- const CgType* ty) {
- /* Emit recursively for return and param types if they're composites. */
- CfreeCgTypeId ret = api_unalias_type(t->c, ty->func.ret);
- const CgType* rty = cg_type_get(t->c, ret);
- if (rty &&
- (rty->kind == CFREE_CG_TYPE_RECORD || rty->kind == CFREE_CG_TYPE_ARRAY ||
- rty->kind == CFREE_CG_TYPE_FUNC)) {
- c_ensure_typedef(t, ret);
- }
- for (u32 i = 0; i < ty->func.nparams; ++i) {
- CfreeCgTypeId pt = api_unalias_type(t->c, ty->func.params[i].type);
- const CgType* pty = cg_type_get(t->c, pt);
- if (pty &&
- (pty->kind == CFREE_CG_TYPE_RECORD ||
- pty->kind == CFREE_CG_TYPE_ARRAY || pty->kind == CFREE_CG_TYPE_FUNC)) {
- c_ensure_typedef(t, pt);
- }
- }
- cbuf_puts(&t->typedefs, "typedef ");
- cbuf_puts(&t->typedefs, c_typename(t, ty->func.ret));
- cbuf_puts(&t->typedefs, " (*__ty_");
- cbuf_put_u64(&t->typedefs, (u64)tid);
- cbuf_puts(&t->typedefs, ")(");
- if (ty->func.nparams == 0 && !ty->func.abi_variadic) {
- cbuf_puts(&t->typedefs, "void");
- } else {
- for (u32 i = 0; i < ty->func.nparams; ++i) {
- if (i > 0) cbuf_puts(&t->typedefs, ", ");
- cbuf_puts(&t->typedefs, c_typename(t, ty->func.params[i].type));
- }
- if (ty->func.abi_variadic) {
- if (ty->func.nparams > 0) cbuf_puts(&t->typedefs, ", ");
- cbuf_puts(&t->typedefs, "...");
- }
- }
- cbuf_puts(&t->typedefs, ");\n");
-}
-
-static const char* c_int_type_for_width_panic(CTarget* t, u32 width,
- int signed_) {
- const char* s = c_int_type_name_for_width(width, signed_);
- if (!s) {
- SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
- compiler_panic(t->c, loc, "C target: int width %u not yet supported",
- (unsigned)width);
- }
- return s;
-}
-
-static const char* c_float_type_name(u32 width) {
- switch (width) {
- case 32:
- return "float";
- case 64:
- return "double";
- case 80:
- case 128:
- return "long double";
- default:
- return NULL;
- }
-}
-
-/* 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);
- SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
- if (!ty) {
- compiler_panic(t->c, loc, "C target: unknown type id %u", (unsigned)type);
- }
- switch (ty->kind) {
- case CFREE_CG_TYPE_VOID:
- return "void";
- case CFREE_CG_TYPE_BOOL:
- return "int32_t";
- case CFREE_CG_TYPE_INT:
- return c_int_type_for_width_panic(t, ty->integer.width, 1);
- case CFREE_CG_TYPE_FLOAT: {
- const char* s = c_float_type_name(ty->fp.width);
- if (!s) {
- compiler_panic(t->c, loc, "C target: fp width %u not yet supported",
- (unsigned)ty->fp.width);
- }
- return s;
- }
- case CFREE_CG_TYPE_PTR:
- return "void*";
- case CFREE_CG_TYPE_ENUM:
- /* CG enums are width-only; treat as their underlying integer base. */
- return c_typename(t, ty->enum_.base);
- case CFREE_CG_TYPE_VARARG_STATE:
- t->need_stdarg = 1;
- return "va_list";
- case CFREE_CG_TYPE_RECORD:
- case CFREE_CG_TYPE_ARRAY:
- case CFREE_CG_TYPE_FUNC:
- c_ensure_typedef(t, resolved);
- return c_typedef_name(t, resolved);
- default:
- compiler_panic(t->c, loc, "C target: type kind %d not yet supported",
- (int)ty->kind);
- }
-}
-
-void c_emit_type(CTarget* t, CBuf* b, CfreeCgTypeId type) {
- cbuf_puts(b, c_typename(t, type));
-}
-
-static CfreeCgTypeId c_local_type_or_panic(CTarget* t, CGLocal local) {
- if ((u32)local < t->local_cap && t->local_declared[local] &&
- t->local_type[local]) {
- return t->local_type[local];
- }
- compiler_panic(t->c, t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0},
- "C target: unknown local type for v%u", (unsigned)local);
- return CFREE_CG_TYPE_NONE;
-}
-
-static void c_emit_tuple_type_name(CTarget* t, CBuf* b,
- const CfreeCgTypeId* types, u32 ntypes) {
- (void)t;
- cbuf_puts(b, "__cfree_tuple");
- cbuf_put_u64(b, (u64)ntypes);
- for (u32 i = 0; i < ntypes; ++i) {
- cbuf_putc(b, '_');
- cbuf_put_u64(b, (u64)api_unalias_type(t->c, types[i]));
- }
-}
-
-static void c_ensure_tuple_typedef(CTarget* t, const CfreeCgTypeId* types,
- u32 ntypes) {
- if (ntypes <= 1) return;
- cbuf_puts(&t->typedefs, "#ifndef __cfree_tuple_guard_");
- c_emit_tuple_type_name(t, &t->typedefs, types, ntypes);
- cbuf_puts(&t->typedefs, "\n#define __cfree_tuple_guard_");
- c_emit_tuple_type_name(t, &t->typedefs, types, ntypes);
- cbuf_puts(&t->typedefs, "\ntypedef struct ");
- c_emit_tuple_type_name(t, &t->typedefs, types, ntypes);
- cbuf_puts(&t->typedefs, " {");
- for (u32 i = 0; i < ntypes; ++i) {
- cbuf_putc(&t->typedefs, ' ');
- c_emit_type(t, &t->typedefs, types[i]);
- cbuf_puts(&t->typedefs, " f");
- cbuf_put_u64(&t->typedefs, (u64)i);
- cbuf_putc(&t->typedefs, ';');
- }
- cbuf_puts(&t->typedefs, " } ");
- c_emit_tuple_type_name(t, &t->typedefs, types, ntypes);
- cbuf_puts(&t->typedefs, ";\n#endif\n");
-}
-
-static Operand c_op_local(CGLocal local, CfreeCgTypeId type) {
- Operand op;
- memset(&op, 0, sizeof op);
- op.kind = OPK_LOCAL;
- op.type = type;
- op.v.local = local;
- return op;
-}
-
-void c_local_name(CLocal r, char* out, size_t cap) {
- size_t i = 0;
- if (cap == 0) return;
- if (cap > 1) out[i++] = 'v';
- char tmp[16];
- size_t n = 0;
- u32 v = (u32)r;
- if (v == 0) {
- tmp[n++] = '0';
- } else {
- while (v) {
- tmp[n++] = (char)('0' + (v % 10));
- v /= 10;
- }
- }
- while (n && i + 1 < cap) out[i++] = tmp[--n];
- out[i] = '\0';
-}
-
-static void c_grow_local_table(CTarget* t, u32 needed) {
- Heap* h = t->c->ctx->heap;
- u32 newcap = t->local_cap ? t->local_cap : 16;
- while (newcap < needed) newcap *= 2;
- u8* nd = (u8*)h->realloc(h, t->local_declared, t->local_cap, newcap, 1);
- CfreeCgTypeId* nt = (CfreeCgTypeId*)h->realloc(
- h, t->local_type, t->local_cap * sizeof(CfreeCgTypeId),
- newcap * sizeof(CfreeCgTypeId), _Alignof(CfreeCgTypeId));
- if ((!nd && newcap) || (!nt && newcap)) {
- compiler_panic(t->c, (SrcLoc){0, 0, 0}, "C target: out of memory");
- }
- for (u32 i = t->local_cap; i < newcap; ++i) {
- nd[i] = 0;
- nt[i] = 0;
- }
- t->local_declared = nd;
- t->local_type = nt;
- t->local_cap = newcap;
-}
-
-/* Emit the trailing `__attribute__((unused)) = INIT;` for a local decl of
- * type `ty`. Scalars get `= 0` (readable); aggregates get `= {0}` (which is
- * the only form that compiles for record/array). */
-static void c_emit_zero_init(CTarget* t, CfreeCgTypeId ty) {
- const CgType* cgt = ty ? cg_type_get(t->c, api_unalias_type(t->c, ty)) : NULL;
- int is_aggregate = cgt && (cgt->kind == CFREE_CG_TYPE_RECORD ||
- cgt->kind == CFREE_CG_TYPE_ARRAY);
- cbuf_puts(&t->decls, is_aggregate ? " __attribute__((unused)) = {0};\n"
- : " __attribute__((unused)) = 0;\n");
-}
-
-void c_ensure_local(CTarget* t, CLocal r, CfreeCgTypeId type) {
- if (r == (CLocal)CG_LOCAL_NONE) {
- compiler_panic(t->c, (SrcLoc){0, 0, 0},
- "C target: CG_LOCAL_NONE reached emission");
- }
- if ((u32)r >= t->local_cap) c_grow_local_table(t, (u32)r + 1u);
- if (t->local_declared[r]) {
- if (type && api_unalias_type(t->c, t->local_type[r]) !=
- api_unalias_type(t->c, type)) {
- compiler_panic(t->c, t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0},
- "C target: local v%u used with inconsistent type "
- "(declared %u, used %u)",
- (unsigned)r, (unsigned)api_unalias_type(t->c, t->local_type[r]),
- (unsigned)api_unalias_type(t->c, type));
- }
- return;
- }
- t->local_declared[r] = 1;
- t->local_type[r] = type;
- cbuf_puts(&t->decls, " ");
- c_emit_type(t, &t->decls, type);
- cbuf_puts(&t->decls, " ");
- char buf[24];
- c_local_name(r, buf, sizeof buf);
- cbuf_puts(&t->decls, buf);
- /* Zero-init kills -Wsometimes-uninitialized for control flow clang can't
- * reason through; the host C compiler DSEs the init when a real
- * assignment dominates. Scalars get `= 0`, aggregates `= {0}`. */
- c_emit_zero_init(t, type);
-}
-
-/* Emit a signed-int64 literal. INT64_MIN can't be written directly: clang
- * treats `-9223372036854775808` as `-(9223372036854775808)` with the inner
- * literal too large for any signed type, which trips
- * -Wimplicitly-unsigned-literal. The standard workaround is
- * `(-9223372036854775807LL - 1)`. */
-static void c_emit_imm_literal(CTarget* t, i64 v) {
- if (v == (i64)((u64)1u << 63u)) {
- cbuf_puts(&t->body, "(-9223372036854775807LL - 1)");
- return;
- }
- cbuf_put_i64(&t->body, v);
-}
-
-/* Address-mode tuple decoded from an OPK_INDIRECT operand. Mirrors the
- * `addr_mode` helper in the machine-code backends so all targets share a
- * single in-backend view of `base [+ index << log2_scale] + ofs`. */
-typedef struct CAddrMode {
- CLocal base;
- CLocal index; /* CG_LOCAL_NONE when no index operand */
- u8 log2_scale; /* meaningful only when index != CG_LOCAL_NONE */
- i32 ofs;
-} CAddrMode;
-
-static CAddrMode c_addr_mode(Operand addr) {
- CAddrMode m;
- m.base = addr.v.ind.base;
- m.index = addr.v.ind.index;
- m.log2_scale = addr.v.ind.log2_scale;
- m.ofs = addr.v.ind.ofs;
- return m;
-}
-
-/* Emit `(char*)base [+ (uintptr_t)index * (1u << log2_scale)] [+ ofs]` into
- * the body, with each optional term suppressed when absent. Used by every
- * OPK_INDIRECT renderer; the caller wraps it with the appropriate
- * `(*(T*)(...))` or `((T)(...))` cast. */
-static void c_emit_indirect_addr_expr(CTarget* t, CAddrMode m) {
- char rbuf[24];
- cbuf_puts(&t->body, "(char*)");
- c_local_name(m.base, rbuf, sizeof rbuf);
- cbuf_puts(&t->body, rbuf);
- if (m.index != CG_LOCAL_NONE) {
- cbuf_puts(&t->body, " + (uintptr_t)");
- c_local_name(m.index, rbuf, sizeof rbuf);
- cbuf_puts(&t->body, rbuf);
- cbuf_puts(&t->body, " * ");
- /* Spell as the explicit 1/2/4/8 literal corresponding to log2_scale.
- * log2_scale is normalized to {0,1,2,3} by cg. */
- cbuf_put_u64(&t->body, (u64)(1u << m.log2_scale));
- }
- if (m.ofs != 0) {
- cbuf_puts(&t->body, " + ");
- cbuf_put_i64(&t->body, (i64)m.ofs);
- }
-}
-
-/* Assert that `addr`, if OPK_INDIRECT, has no index operand. Used by paths
- * the cg layer guarantees never carry the indexed shape (bitfield, atomics,
- * copy_bytes/set_bytes, inline asm). */
-static void c_assert_no_index(CTarget* t, Operand addr, const char* where) {
- if (addr.kind != OPK_INDIRECT) return;
- if (addr.v.ind.index == CG_LOCAL_NONE) return;
- SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
- compiler_panic(t->c, loc,
- "C target: %.*s: indexed OPK_INDIRECT not allowed here",
- SLICE_ARG(slice_from_cstr(where)));
-}
-
-void c_emit_operand(CTarget* t, Operand op) {
- char buf[24];
- switch (op.kind) {
- case OPK_IMM:
- if (op.type == CFREE_CG_TYPE_NONE) {
- /* Untyped IMM (e.g. memset byte value): emit the literal raw. */
- cbuf_putc(&t->body, '(');
- c_emit_imm_literal(t, op.v.imm);
- cbuf_putc(&t->body, ')');
- } else {
- cbuf_puts(&t->body, "((");
- c_emit_type(t, &t->body, op.type);
- cbuf_puts(&t->body, ")");
- c_emit_imm_literal(t, op.v.imm);
- cbuf_puts(&t->body, ")");
- }
- return;
- case OPK_LOCAL: {
- c_ensure_local(t, op.v.local, op.type);
- c_local_name(op.v.local, buf, sizeof buf);
- cbuf_puts(&t->body, buf);
- return;
- }
- case OPK_INDIRECT: {
- /* Used by call paths to pass aggregates by-address: the operand's type
- * is the aggregate, the storage is `base + index*scale + ofs`. Emit the
- * deref as a value expression. */
- cbuf_puts(&t->body, "(*(");
- c_emit_type(t, &t->body, op.type);
- cbuf_puts(&t->body, "*)(");
- c_emit_indirect_addr_expr(t, c_addr_mode(op));
- cbuf_puts(&t->body, "))");
- return;
- }
- case OPK_GLOBAL: {
- /* OPK_GLOBAL carries `&sym + addend`. How we spell it depends on
- * op.type:
- * - pointer/scalar/void: the value IS the address, so cast through
- * `((T)((char*)sym + addend))`.
- * - aggregate (RECORD/ARRAY): the symbol's storage is an aggregate
- * value; emit `(*(T*)((char*)sym + addend))` so the deref reads
- * the aggregate value (used by call args that pass struct
- * by-value via a global initialized buffer). */
- obj_sym_mark_referenced(t->obj, op.v.global.sym);
- const char* nm = c_sym_name(t, op.v.global.sym);
- const CgType* gty =
- (op.type != CFREE_CG_TYPE_NONE)
- ? cg_type_get(t->c, api_unalias_type(t->c, op.type))
- : NULL;
- int is_aggregate = gty && (gty->kind == CFREE_CG_TYPE_RECORD ||
- gty->kind == CFREE_CG_TYPE_ARRAY);
- if (is_aggregate) {
- cbuf_puts(&t->body, "(*(");
- c_emit_type(t, &t->body, op.type);
- 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, "))");
- } else {
- 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};
- compiler_panic(t->c, loc, "C target: operand kind %d not yet supported",
- (int)op.kind);
- }
- }
-}
-
-static int c_type_is_float(CTarget* t, CfreeCgTypeId type) {
- if (type == CFREE_CG_TYPE_NONE) return 0;
- const CgType* ty = cg_type_get(t->c, api_unalias_type(t->c, type));
- return ty && ty->kind == CFREE_CG_TYPE_FLOAT;
-}
-
-/* True iff a and b name the same CG type after alias resolution. */
-static int c_types_equiv(CTarget* t, CfreeCgTypeId a, CfreeCgTypeId b) {
- if (a == 0 || b == 0) return 0;
- return api_unalias_type(t->c, a) == api_unalias_type(t->c, b);
-}
-
-/* Emit " vN = " plus any cast needed for a C assignment expression.
- * Caller must then emit the RHS expression and call c_emit_local_assign_close.
- *
- * `rhs_ty` is the CG type the RHS expression will produce (or 0 if unknown).
- * Pointer/int crossings bridge through uintptr_t to keep host-C diagnostics
- * quiet. The outer `(...)` parens are kept so the closer's `);` stays
- * balanced. */
-static void c_emit_local_assign_open(CTarget* t, CLocal r, CfreeCgTypeId rhs_ty) {
- if ((u32)r >= t->local_cap || !t->local_declared[r]) {
- compiler_panic(t->c, t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0},
- "C target: assign to undeclared local v%u", (unsigned)r);
- }
- CfreeCgTypeId decl = t->local_type[r];
- char buf[24];
- c_local_name(r, buf, sizeof buf);
- cbuf_puts(&t->body, " ");
- cbuf_puts(&t->body, buf);
- cbuf_puts(&t->body, " = ");
- if (!c_types_equiv(t, rhs_ty, decl)) {
- cbuf_putc(&t->body, '(');
- c_emit_type(t, &t->body, decl);
- cbuf_putc(&t->body, ')');
- if (!c_type_is_float(t, decl) &&
- (!rhs_ty || c_type_is_ptr(t, decl) || c_type_is_ptr(t, rhs_ty))) {
- cbuf_puts(&t->body, "(uintptr_t)");
- }
- }
- cbuf_puts(&t->body, "(");
-}
-
-static void c_emit_local_assign_close(CTarget* t) { cbuf_puts(&t->body, ");\n"); }
-
-void c_emit_operand_signed(CTarget* t, Operand op, int signed_) {
- u32 w = c_int_width_for_signedness(t, op.type);
- if (w == 0) {
- /* Not an integer — emit without sign cast. */
- c_emit_operand(t, op);
- return;
- }
- const char* tn = c_int_type_name_for_width(w, signed_);
- if (!tn) {
- c_emit_operand(t, op);
- return;
- }
- int via_uptr = c_operand_is_ptr_typed(t, op);
- /* CG ints are width-only; the C target declares every int local/IMM
- * as the signed `int{W}_t` of its width. So when `signed_` is true and
- * the operand's emit-width matches `w`, the explicit cast is redundant
- * with what c_emit_operand already produces. Skipping it cuts the
- * ubiquitous `((int32_t)((int32_t)23))` double-cast down to one. */
- if (!via_uptr && signed_) {
- CfreeCgTypeId et = op.type;
- if (c_int_width_for_signedness(t, et) == w) {
- c_emit_operand(t, op);
- return;
- }
- }
- cbuf_puts(&t->body, "((");
- cbuf_puts(&t->body, tn);
- cbuf_puts(&t->body, ")");
- if (via_uptr) {
- cbuf_puts(&t->body, "(uintptr_t)");
- }
- c_emit_operand(t, op);
- cbuf_puts(&t->body, ")");
-}
-
-/* Returns 1 if `type` is a pointer (or void*). */
-static int c_type_is_ptr(CTarget* t, CfreeCgTypeId type) {
- if (type == CFREE_CG_TYPE_NONE) return 0;
- const CgType* ty = cg_type_get(t->c, api_unalias_type(t->c, type));
- return ty && ty->kind == CFREE_CG_TYPE_PTR;
-}
-
-static int c_type_is_bool(CTarget* t, CfreeCgTypeId type) {
- if (type == CFREE_CG_TYPE_NONE) return 0;
- const CgType* ty = cg_type_get(t->c, api_unalias_type(t->c, type));
- return ty && ty->kind == CFREE_CG_TYPE_BOOL;
-}
-
-static int c_type_is_aggregate(CTarget* t, CfreeCgTypeId type) {
- if (type == CFREE_CG_TYPE_NONE) return 0;
- const CgType* ty = cg_type_get(t->c, api_unalias_type(t->c, type));
- return ty &&
- (ty->kind == CFREE_CG_TYPE_RECORD || ty->kind == CFREE_CG_TYPE_ARRAY);
-}
-
-static int c_operand_is_ptr_typed(CTarget* t, Operand op) {
- if (c_type_is_ptr(t, op.type)) return 1;
- return 0;
-}
-
-/* Emit `(target_ty)(uintptr_t)(op)` (or `(target_ty)(op)` for float
- * target_ty). Used when the caller needs a specific C expression type.
- * Pointer/int crossings bridge through uintptr_t. */
-static void c_emit_operand_as(CTarget* t, Operand op, CfreeCgTypeId target_ty) {
- if (c_types_equiv(t, op.type, target_ty)) {
- c_emit_operand(t, op);
- return;
- }
- cbuf_puts(&t->body, "(");
- c_emit_type(t, &t->body, target_ty);
- cbuf_puts(&t->body, ")");
- if (!c_type_is_float(t, target_ty) &&
- (!op.type || c_type_is_ptr(t, op.type) || c_type_is_ptr(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). */
-static void c_emit_operand_arith(CTarget* t, Operand op) {
- if (c_operand_is_ptr_typed(t, op)) {
- cbuf_puts(&t->body, "((uintptr_t)");
- if (op.kind == OPK_IMM) {
- c_emit_imm_literal(t, op.v.imm);
- } else {
- c_emit_operand(t, op);
- }
- cbuf_puts(&t->body, ")");
- return;
- }
- c_emit_operand(t, op);
-}
-
-/* Same, but applies the requested signedness when the operand is an integer
- * (used for SDIV/UDIV/SREM/UREM/SHR_S/SHR_U). Pointer operands always go
- * through the uintptr_t cast regardless of the requested signedness. */
-static void c_emit_operand_arith_signed(CTarget* t, Operand op, int signed_) {
- if (c_operand_is_ptr_typed(t, op)) {
- cbuf_puts(&t->body, "((uintptr_t)");
- if (op.kind == OPK_IMM) {
- c_emit_imm_literal(t, op.v.imm);
- } else {
- c_emit_operand(t, op);
- }
- cbuf_puts(&t->body, ")");
- return;
- }
- c_emit_operand_signed(t, op, signed_);
-}
-
-/* Emit a C lvalue expression for an addr operand (OPK_LOCAL / OPK_GLOBAL /
- * OPK_INDIRECT) using `access_type` as the access type. The result is the
- * full `*(T*)(...)` dereference, or the C variable directly when the access
- * type matches the underlying local/global object. */
-static void c_emit_addr_deref(CTarget* t, Operand addr,
- CfreeCgTypeId access_type) {
- char buf[24];
- SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
- switch (addr.kind) {
- case OPK_LOCAL: {
- c_ensure_local(t, addr.v.local, addr.type);
- c_local_name(addr.v.local, buf, sizeof buf);
- if (access_type == 0 || addr.type == 0 ||
- api_unalias_type(t->c, access_type) ==
- api_unalias_type(t->c, addr.type)) {
- cbuf_puts(&t->body, buf);
- } else {
- cbuf_puts(&t->body, "(*(");
- c_emit_type(t, &t->body, access_type);
- cbuf_puts(&t->body, "*)&");
- cbuf_puts(&t->body, buf);
- cbuf_puts(&t->body, ")");
- }
- return;
- }
- case OPK_GLOBAL: {
- obj_sym_mark_referenced(t->obj, addr.v.global.sym);
- 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: {
- CAddrMode m = c_addr_mode(addr);
- if ((u32)m.base >= t->local_cap || !t->local_declared[m.base]) {
- compiler_panic(t->c, loc,
- "C target: indirect on undeclared base local v%u",
- (unsigned)m.base);
- }
- if (m.index != CG_LOCAL_NONE &&
- ((u32)m.index >= t->local_cap || !t->local_declared[m.index])) {
- compiler_panic(t->c, loc,
- "C target: indirect on undeclared index local v%u",
- (unsigned)m.index);
- }
- cbuf_puts(&t->body, "(*(");
- c_emit_type(t, &t->body, access_type);
- cbuf_puts(&t->body, "*)(");
- c_emit_indirect_addr_expr(t, m);
- cbuf_puts(&t->body, "))");
- return;
- }
- default:
- compiler_panic(t->c, loc,
- "C target: addr-deref on operand kind %d not supported",
- (int)addr.kind);
- }
-}
-
-/* Emit a C address-of expression for a lvalue operand. Output is a pointer
- * value (cast to dst_type). */
-static void c_emit_lvalue_addr(CTarget* t, Operand lv, CfreeCgTypeId dst_type) {
- char buf[24];
- SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
- switch (lv.kind) {
- case OPK_LOCAL:
- cbuf_puts(&t->body, "((");
- c_emit_type(t, &t->body, dst_type);
- cbuf_puts(&t->body, ")");
- cbuf_puts(&t->body, "&");
- c_ensure_local(t, lv.v.local, lv.type);
- c_local_name(lv.v.local, buf, sizeof buf);
- cbuf_puts(&t->body, buf);
- cbuf_puts(&t->body, ")");
- return;
- case OPK_GLOBAL: {
- obj_sym_mark_referenced(t->obj, lv.v.global.sym);
- 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, "((");
- c_emit_type(t, &t->body, dst_type);
- cbuf_puts(&t->body, ")(");
- c_emit_indirect_addr_expr(t, c_addr_mode(lv));
- cbuf_puts(&t->body, "))");
- return;
- }
- default:
- compiler_panic(t->c, loc,
- "C target: addr-of on operand kind %d not supported",
- (int)lv.kind);
- }
-}
-
-/* === Symbol name lookup === */
-
-const char* c_sym_name(CTarget* t, ObjSymId sym) {
- const ObjSym* os = obj_symbol_get(t->obj, sym);
- if (!os) {
- compiler_panic(t->c, (SrcLoc){0, 0, 0}, "C target: unknown ObjSymId %u",
- (unsigned)sym);
- }
- Slice nm = pool_slice(t->c->global, os->name);
- const char* s = nm.s;
- size_t n = nm.len;
- /* 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 && 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_slice(t->c->global, (Slice){.s = buf, .len = out});
- return pool_slice(t->c->global, interned).s;
-}
-
-/* === Prologue / finalize === */
-
-void c_emit_prologue(CTarget* t) {
- if (t->prologue_emitted) return;
- t->prologue_emitted = 1;
- c_writer_puts(t,
- "/* generated by cfree --emit=c */\n"
- "#include <stdint.h>\n"
- "#include <stdalign.h>\n");
- /* Other headers are decided at finalize so include lines remain
- * deterministic regardless of when the type was first referenced.
- * Writer flushes are not stream-buffered, so we keep prologue compact and
- * tack the rest on at c_finalize. */
- c_writer_puts(t, "\n");
-}
-
-/* === func_begin / func_end === */
-
-/* Write `RetT name(P0, P1, ...)` (without trailing `;` or `{`) to `b`. */
-static void c_emit_func_signature(CTarget* t, CBuf* b, const char* name,
- CfreeCgTypeId fn_type,
- const CfreeCgTypeId* result_types,
- u32 nresults) {
- CfreeCgTypeId ret_type = cg_type_func_ret_id(t->c, fn_type);
- const CgType* fty = cg_type_get(t->c, api_unalias_type(t->c, fn_type));
- SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
- if (!fty || fty->kind != CFREE_CG_TYPE_FUNC) {
- compiler_panic(t->c, loc, "C target: fn_type is not a function type");
- }
- if (!result_types) {
- nresults = cg_type_is_void(t->c, ret_type) ? 0u : 1u;
- result_types = &ret_type;
- }
- if (nresults == 0) {
- cbuf_puts(b, "void");
- } else if (nresults == 1) {
- c_emit_type(t, b, result_types[0]);
- } else {
- c_ensure_tuple_typedef(t, result_types, nresults);
- c_emit_tuple_type_name(t, b, result_types, nresults);
- }
- cbuf_puts(b, " ");
- cbuf_puts(b, name);
- cbuf_puts(b, "(");
- if (fty->func.nparams == 0 && !fty->func.abi_variadic) {
- cbuf_puts(b, "void");
- } else {
- for (u32 i = 0; i < fty->func.nparams; ++i) {
- if (i > 0) cbuf_puts(b, ", ");
- c_emit_type(t, b, fty->func.params[i].type);
- cbuf_puts(b, " p");
- cbuf_put_u64(b, (u64)i);
- }
- if (fty->func.abi_variadic) {
- if (fty->func.nparams > 0) cbuf_puts(b, ", ");
- cbuf_puts(b, "...");
- }
- }
- cbuf_puts(b, ")");
-}
-
-void c_func_begin(CgTarget* T, const CGFuncDesc* fd) {
- CTarget* t = (CTarget*)T;
-
- c_emit_prologue(t);
-
- t->cur_fn = fd;
- cbuf_reset(&t->decls);
- for (u32 i = 0; i < t->local_cap; ++i) {
- t->local_declared[i] = 0;
- t->local_type[i] = 0;
- }
- t->next_label = 0;
- t->next_local = 0;
- t->next_tmp = 0;
- t->nscopes = 0;
- t->last_was_terminator = 0;
- t->have_emitted_loc = 0;
- t->emitted_loc = (SrcLoc){0, 0, 0};
-
- const char* name = c_sym_name(t, fd->sym);
-
- /* Forward-declare so out-of-order callers and same-TU references find the
- * prototype regardless of definition order. */
- c_ensure_forward_decl_with_results(t, fd->sym, fd->fn_type, fd->result_types,
- fd->nresults);
-
- c_emit_func_signature(t, &t->body, name, fd->fn_type, fd->result_types,
- fd->nresults);
- cbuf_puts(&t->body, " {\n");
- t->fn_body_start = t->body.len;
-}
-
-static void c_ensure_forward_decl_with_results(CTarget* t, ObjSymId sym,
- CfreeCgTypeId fn_type,
- const CfreeCgTypeId* result_types,
- u32 nresults) {
- Heap* h = t->c->ctx->heap;
- if ((u32)sym >= t->sym_forwarded_cap) {
- u32 newcap = t->sym_forwarded_cap ? t->sym_forwarded_cap : 16;
- while (newcap <= (u32)sym) newcap *= 2;
- u8* nd =
- (u8*)h->realloc(h, t->sym_forwarded, t->sym_forwarded_cap, newcap, 1);
- if (!nd && newcap) {
- compiler_panic(t->c, (SrcLoc){0, 0, 0}, "C target: out of memory");
- }
- for (u32 i = t->sym_forwarded_cap; i < newcap; ++i) nd[i] = 0;
- t->sym_forwarded = nd;
- t->sym_forwarded_cap = newcap;
- }
- if (t->sym_forwarded[sym]) return;
- t->sym_forwarded[sym] = 1;
- const char* name = c_sym_name(t, sym);
- const ObjSym* os = obj_symbol_get(t->obj, sym);
- if ((os && (os->kind == SK_FUNC || os->kind == SK_IFUNC)) || fn_type != 0) {
- c_emit_func_signature(t, &t->forwards, name, fn_type, result_types,
- nresults);
- cbuf_puts(&t->forwards, ";\n");
- } else {
- if (os && os->bind == SB_LOCAL)
- cbuf_puts(&t->forwards, "static ");
- else
- cbuf_puts(&t->forwards, "extern ");
- if (os && os->section_id != OBJ_SEC_NONE) {
- const Section* sec = obj_section_get(t->obj, os->section_id);
- if (sec->kind == SEC_RODATA) cbuf_puts(&t->forwards, "const ");
- }
- cbuf_puts(&t->forwards, "struct __cfree_data_");
- cbuf_puts(&t->forwards, name);
- cbuf_puts(&t->forwards, " ");
- cbuf_puts(&t->forwards, name);
- cbuf_puts(&t->forwards, ";\n");
- }
-}
-
-void c_ensure_forward_decl(CTarget* t, ObjSymId sym, CfreeCgTypeId fn_type) {
- c_ensure_forward_decl_with_results(t, sym, fn_type, NULL, 0);
-}
-
-void c_func_end(CgTarget* T) {
- CTarget* t = (CTarget*)T;
- size_t splice_at = t->fn_body_start;
- size_t body_after = t->body.len;
- size_t fn_body_len = body_after - splice_at;
- Heap* h = t->c->ctx->heap;
-
- u8* tmp = NULL;
- if (fn_body_len) {
- tmp = (u8*)h->alloc(h, fn_body_len, 1);
- if (!tmp) {
- compiler_panic(t->c, t->cur_fn->loc, "C target: out of memory");
- }
- for (size_t i = 0; i < fn_body_len; ++i) {
- tmp[i] = t->body.data[splice_at + i];
- }
- }
-
- t->body.len = splice_at;
- if (t->decls.len)
- cbuf_putn(&t->body, (const char*)t->decls.data, t->decls.len);
- if (tmp) {
- cbuf_putn(&t->body, (const char*)tmp, fn_body_len);
- h->free(h, tmp, fn_body_len);
- }
- cbuf_puts(&t->body, "}\n\n");
-
- t->cur_fn = NULL;
-}
-
-/* === locals, params === */
-
-CGLocal c_param(CgTarget* T, const CGParamDesc* pd) {
- CTarget* t = (CTarget*)T;
- CGLocalDesc d;
- memset(&d, 0, sizeof d);
- d.type = pd->type;
- d.name = pd->name;
- d.loc = pd->loc;
- d.size = pd->size;
- d.align = pd->align;
- d.flags = pd->flags;
- CGLocal local = c_local(T, &d);
- c_ensure_local(t, local, pd->type);
-
- char buf[24];
- c_local_name(local, buf, sizeof buf);
- cbuf_puts(&t->body, " ");
- cbuf_puts(&t->body, buf);
- cbuf_puts(&t->body, " = p");
- cbuf_put_u64(&t->body, (u64)pd->index);
- cbuf_puts(&t->body, ";\n");
-
- return local;
-}
-
-/* === load_imm, copy, binop === */
-
-void c_load_imm(CgTarget* T, Operand dst, i64 imm) {
- CTarget* t = (CTarget*)T;
- SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
- if (dst.kind != OPK_LOCAL) {
- compiler_panic(t->c, loc, "C target: load_imm dst must be LOCAL");
- }
- c_ensure_local(t, dst.v.local, dst.type);
- /* The literal is emitted bare; its C type is `long long`. We can drop
- * the bridge cast iff the bare assignment compiles cleanly:
- * - integer dst: imm must fit in dst's signed range (else
- * -Wconstant-conversion). 64-bit dst always fits.
- * - pointer dst: only `0` (null pointer constant) is safe; any other
- * literal trips -Wint-conversion.
- * Otherwise keep the bridge. */
- u32 w = c_int_width_for_signedness(t, dst.type);
- int can_drop_bridge;
- if (w > 0) {
- can_drop_bridge = (w >= 64) || (imm >= -((i64)1 << (w - 1)) &&
- imm <= (((i64)1 << (w - 1)) - 1));
- } else if (c_type_is_ptr(t, dst.type)) {
- can_drop_bridge = (imm == 0);
- } else {
- can_drop_bridge = 0;
- }
- c_emit_local_assign_open(t, dst.v.local,
- can_drop_bridge ? dst.type : (CfreeCgTypeId)0);
- c_emit_imm_literal(t, imm);
- c_emit_local_assign_close(t);
-}
-
-void c_copy(CgTarget* T, Operand dst, Operand src) {
- CTarget* t = (CTarget*)T;
- SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
- if (dst.kind != OPK_LOCAL) {
- compiler_panic(t->c, loc, "C target: copy dst must be LOCAL");
- }
- c_ensure_local(t, dst.v.local, dst.type);
- c_emit_local_assign_open(t, dst.v.local, src.type);
- c_emit_operand(t, src);
- c_emit_local_assign_close(t);
-}
-
-static const char* binop_to_c(BinOp op) {
- switch (op) {
- case BO_IADD:
- case BO_FADD:
- return "+";
- case BO_ISUB:
- case BO_FSUB:
- return "-";
- case BO_IMUL:
- case BO_FMUL:
- return "*";
- case BO_SDIV:
- case BO_UDIV:
- case BO_FDIV:
- return "/";
- case BO_SREM:
- case BO_UREM:
- return "%";
- case BO_AND:
- return "&";
- case BO_OR:
- return "|";
- case BO_XOR:
- return "^";
- case BO_SHL:
- return "<<";
- case BO_SHR_S:
- case BO_SHR_U:
- return ">>";
- }
- return NULL;
-}
-
-/* For BinOp `op`, decide how to sign-cast the operands. Returns 0 for "no
- * cast", 1 for "cast both to signed", 2 for "cast both to unsigned", 3 for
- * "cast lhs only (signedness `lhs_signed`)" (used for shifts). */
-typedef enum { BSC_NONE, BSC_SIGNED, BSC_UNSIGNED, BSC_SHIFT_LHS } BinSignCast;
-
-static BinSignCast binop_sign_kind(BinOp op, int* lhs_signed_out) {
- *lhs_signed_out = 1;
- switch (op) {
- case BO_SDIV:
- case BO_SREM:
- return BSC_SIGNED;
- case BO_UDIV:
- case BO_UREM:
- return BSC_UNSIGNED;
- case BO_SHR_S:
- *lhs_signed_out = 1;
- return BSC_SHIFT_LHS;
- case BO_SHR_U:
- *lhs_signed_out = 0;
- return BSC_SHIFT_LHS;
- default:
- return BSC_NONE;
- }
-}
-
-void c_binop(CgTarget* T, BinOp op, Operand dst, Operand a, Operand b) {
- CTarget* t = (CTarget*)T;
- SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
- const char* sym = binop_to_c(op);
- if (!sym) {
- compiler_panic(t->c, loc, "C target: unknown binop %d", (int)op);
- }
- if (dst.kind != OPK_LOCAL) {
- compiler_panic(t->c, loc, "C target: binop dst must be LOCAL");
- }
- c_ensure_local(t, dst.v.local, dst.type);
- /* Pointer operands get cast to uintptr_t inside c_emit_operand_arith,
- * so the binop's C result type is `uintptr_t`, not the original pointer
- * type. Keep the bridge when dst or either operand is pointer-typed so
- * the assignment back to a pointer dst doesn't trip -Wint-conversion. */
- int has_ptr = c_operand_is_ptr_typed(t, dst) ||
- c_operand_is_ptr_typed(t, a) || c_operand_is_ptr_typed(t, b);
- c_emit_local_assign_open(t, dst.v.local, has_ptr ? (CfreeCgTypeId)0 : dst.type);
- int lhs_signed = 1;
- BinSignCast bsc = binop_sign_kind(op, &lhs_signed);
- switch (bsc) {
- case BSC_NONE:
- c_emit_operand_arith(t, a);
- cbuf_puts(&t->body, " ");
- cbuf_puts(&t->body, sym);
- cbuf_puts(&t->body, " ");
- c_emit_operand_arith(t, b);
- break;
- case BSC_SIGNED:
- c_emit_operand_arith_signed(t, a, 1);
- cbuf_puts(&t->body, " ");
- cbuf_puts(&t->body, sym);
- cbuf_puts(&t->body, " ");
- c_emit_operand_arith_signed(t, b, 1);
- break;
- case BSC_UNSIGNED:
- c_emit_operand_arith_signed(t, a, 0);
- cbuf_puts(&t->body, " ");
- cbuf_puts(&t->body, sym);
- cbuf_puts(&t->body, " ");
- c_emit_operand_arith_signed(t, b, 0);
- break;
- case BSC_SHIFT_LHS:
- c_emit_operand_arith_signed(t, a, lhs_signed);
- cbuf_puts(&t->body, " ");
- cbuf_puts(&t->body, sym);
- cbuf_puts(&t->body, " ");
- c_emit_operand(t, b);
- break;
- }
- c_emit_local_assign_close(t);
-}
-
-/* ===== unop ===== */
-
-void c_unop(CgTarget* T, UnOp op, Operand dst, Operand a) {
- CTarget* t = (CTarget*)T;
- SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
- if (dst.kind != OPK_LOCAL) {
- compiler_panic(t->c, loc, "C target: unop dst must be LOCAL");
- }
- c_ensure_local(t, dst.v.local, dst.type);
- const char* sym = NULL;
- switch (op) {
- case UO_NEG:
- case UO_FNEG:
- sym = "-";
- break;
- case UO_NOT:
- sym = "!";
- break;
- case UO_BNOT:
- sym = "~";
- break;
- default:
- compiler_panic(t->c, loc, "C target: unknown unop %d", (int)op);
- }
- c_emit_local_assign_open(t, dst.v.local, dst.type);
- cbuf_puts(&t->body, sym);
- c_emit_operand(t, a);
- c_emit_local_assign_close(t);
-}
-
-/* ===== compare ops ===== */
-
-static const char* cmp_to_c(CmpOp op) {
- switch (op) {
- case CMP_EQ:
- return "==";
- case CMP_NE:
- return "!=";
- case CMP_LT_S:
- case CMP_LT_U:
- case CMP_LT_F:
- return "<";
- case CMP_LE_S:
- case CMP_LE_U:
- case CMP_LE_F:
- return "<=";
- case CMP_GT_S:
- case CMP_GT_U:
- case CMP_GT_F:
- return ">";
- case CMP_GE_S:
- case CMP_GE_U:
- case CMP_GE_F:
- return ">=";
- }
- return NULL;
-}
-
-/* Returns 1 if cmp op needs unsigned operand cast. -1 if signed. 0 if no cast
- * (EQ/NE — sign doesn't matter for integer equality at the same width — and
- * float compares). */
-static int cmp_signedness(CmpOp op) {
- switch (op) {
- case CMP_LT_S:
- case CMP_LE_S:
- case CMP_GT_S:
- case CMP_GE_S:
- return -1;
- case CMP_LT_U:
- case CMP_LE_U:
- case CMP_GT_U:
- case CMP_GE_U:
- return 1;
- default:
- return 0;
- }
-}
-
-static void c_emit_cmp_operands(CTarget* t, CmpOp op, Operand a, Operand b) {
- int sg = cmp_signedness(op);
- if (sg == 0) {
- c_emit_operand_arith(t, a);
- cbuf_puts(&t->body, " ");
- cbuf_puts(&t->body, cmp_to_c(op));
- cbuf_puts(&t->body, " ");
- c_emit_operand_arith(t, b);
- } else {
- int signed_ = (sg < 0);
- c_emit_operand_arith_signed(t, a, signed_);
- cbuf_puts(&t->body, " ");
- cbuf_puts(&t->body, cmp_to_c(op));
- cbuf_puts(&t->body, " ");
- c_emit_operand_arith_signed(t, b, signed_);
- }
-}
-
-void c_cmp(CgTarget* T, CmpOp op, Operand dst, Operand a, Operand b) {
- CTarget* t = (CTarget*)T;
- SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
- if (dst.kind != OPK_LOCAL) {
- compiler_panic(t->c, loc, "C target: cmp dst must be LOCAL");
- }
- if (!cmp_to_c(op)) {
- compiler_panic(t->c, loc, "C target: unknown cmp %d", (int)op);
- }
- c_ensure_local(t, dst.v.local, dst.type);
- /* Compare result is C `int` (0/1); assigning to integer dst.type narrows
- * implicitly without -Wall complaint. */
- c_emit_local_assign_open(t, dst.v.local, dst.type);
- c_emit_cmp_operands(t, op, a, b);
- c_emit_local_assign_close(t);
-}
-
-/* ===== labels, jump, cmp_branch ===== */
-
-static void c_label_name(Label l, char* out, size_t cap) {
- size_t i = 0;
- if (cap == 0) return;
- const char* p = "L";
- while (*p && i + 1 < cap) out[i++] = *p++;
- char tmp[16];
- size_t n = 0;
- u32 v = (u32)l;
- if (v == 0) {
- tmp[n++] = '0';
- } else {
- while (v) {
- tmp[n++] = (char)('0' + (v % 10));
- v /= 10;
- }
- }
- while (n && i + 1 < cap) out[i++] = tmp[--n];
- out[i] = '\0';
-}
-
-Label c_label_new(CgTarget* T) {
- CTarget* t = (CTarget*)T;
- t->next_label += 1;
- return (Label)t->next_label;
-}
-
-void c_label_place(CgTarget* T, Label l) {
- CTarget* t = (CTarget*)T;
- char buf[24];
- c_label_name(l, buf, sizeof buf);
- /* `Lk: __attribute__((unused));` — empty stmt keeps it valid at end-of-block,
- * and the attribute silences -Wunused-label when the goto got folded away. */
- cbuf_puts(&t->body, " ");
- cbuf_puts(&t->body, buf);
- cbuf_puts(&t->body, ": __attribute__((unused));\n");
- t->last_was_terminator = 0;
-}
-
-/* If `l` is the innermost structured scope's break/continue label, return
- * the C keyword that exits/iterates that scope (a literal `break` or
- * `continue`). NULL means "fall back to goto." Matches only the innermost
- * scope because C `break`/`continue` only escape the nearest enclosing
- * loop/switch — outer-scope targets must stay as goto. */
-static const char* c_scope_kw_for_label(CTarget* t, Label l) {
- if (t->nscopes == 0) return NULL;
- const CScopeInfo* s = &t->scopes[t->nscopes - 1u];
- if (!s->structured) return NULL;
- if (l == s->break_label) return "break";
- if (l == s->continue_label) return "continue";
- return NULL;
-}
-
-void c_jump(CgTarget* T, Label l) {
- CTarget* t = (CTarget*)T;
- if (t->last_was_terminator) return;
- const char* kw = c_scope_kw_for_label(t, l);
- if (kw) {
- cbuf_puts(&t->body, " ");
- cbuf_puts(&t->body, kw);
- cbuf_puts(&t->body, ";\n");
- } else {
- char buf[24];
- c_label_name(l, buf, sizeof buf);
- cbuf_puts(&t->body, " goto ");
- cbuf_puts(&t->body, buf);
- cbuf_puts(&t->body, ";\n");
- }
- t->last_was_terminator = 1;
-}
-
-void c_cmp_branch(CgTarget* T, CmpOp op, Operand a, Operand b, Label l) {
- CTarget* t = (CTarget*)T;
- if (t->last_was_terminator) return;
- SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
- if (!cmp_to_c(op)) {
- compiler_panic(t->c, loc, "C target: unknown cmp %d", (int)op);
- }
- const char* kw = c_scope_kw_for_label(t, l);
- cbuf_puts(&t->body, " if (");
- c_emit_cmp_operands(t, op, a, b);
- if (kw) {
- cbuf_puts(&t->body, ") ");
- cbuf_puts(&t->body, kw);
- cbuf_puts(&t->body, ";\n");
- } else {
- char buf[24];
- c_label_name(l, buf, sizeof buf);
- cbuf_puts(&t->body, ") goto ");
- cbuf_puts(&t->body, buf);
- cbuf_puts(&t->body, ";\n");
- }
-}
-
-/* ===== scopes =====
- *
- * SCOPE_LOOP maps to C's `for (;;) { ... }`. CG places the continue label
- * just before `scope_begin` and the break label just before `scope_end`
- * (see src/cg/control.c:208,253). The C target leaves those label
- * placements in the body — they sit just before `for (;;) {` and just
- * after `}` respectively, so any outer-scope `goto continue_lbl` or
- * `goto break_lbl` (e.g. a nested loop's `continue` targeting this
- * outer loop) still resolves. Inside the `for` body, `c_jump` and
- * `c_cmp_branch` translate jumps whose target is the *innermost* scope's
- * break/continue label into `break;` / `continue;`; outer-scope targets
- * fall back to `goto`. The redundant `Lk: ;` adjacent to the `for` is
- * cosmetic; gcc/clang fold it. */
-
-static void c_grow_scopes(CTarget* t, u32 needed) {
- Heap* h = t->c->ctx->heap;
- u32 newcap = t->scopes_cap ? t->scopes_cap : 8;
- while (newcap < needed) newcap *= 2;
- CScopeInfo* ns = (CScopeInfo*)h->realloc(
- h, t->scopes, t->scopes_cap * sizeof(CScopeInfo),
- newcap * sizeof(CScopeInfo), _Alignof(CScopeInfo));
- if (!ns && newcap) {
- compiler_panic(t->c, (SrcLoc){0, 0, 0}, "C target: out of memory");
- }
- t->scopes = ns;
- t->scopes_cap = newcap;
-}
-
-CGScope c_scope_begin(CgTarget* T, const CGScopeDesc* d) {
- CTarget* t = (CTarget*)T;
- if (t->nscopes + 1u >= t->scopes_cap) c_grow_scopes(t, t->nscopes + 2u);
- u32 idx = t->nscopes;
- t->scopes[idx].kind = d->kind;
- t->scopes[idx].break_label = d->break_label;
- t->scopes[idx].continue_label = d->continue_label;
- t->scopes[idx].structured = 0;
- t->nscopes += 1u;
- if (d->kind == SCOPE_LOOP) {
- cbuf_puts(&t->body, " for (;;) {\n");
- t->scopes[idx].structured = 1;
- t->last_was_terminator = 0;
- return (CGScope)(idx + 1u);
- }
- /* SCOPE_IF carries a cond consumed here. The public CfreeCg API always
- * emits SCOPE_LOOP, so this branch only fires for internal callers. */
- if (d->kind == SCOPE_IF) {
- char buf[24];
- c_label_name(d->break_label, buf, sizeof buf);
- cbuf_puts(&t->body, " if (!(");
- c_emit_operand(t, d->cond);
- cbuf_puts(&t->body, ")) goto ");
- cbuf_puts(&t->body, buf);
- cbuf_puts(&t->body, ";\n");
- }
- return (CGScope)(idx + 1u);
-}
-
-void c_scope_else(CgTarget* T, CGScope s) {
- (void)T;
- (void)s;
- /* Public API doesn't emit SCOPE_IF; if it ever does, the frontend is
- * responsible for placing the else label and the break_label itself. */
-}
-
-void c_scope_end(CgTarget* T, CGScope s) {
- CTarget* t = (CTarget*)T;
- if (s == 0 || (u32)s > t->nscopes) {
- compiler_panic(t->c, t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0},
- "C target: scope_end on invalid handle");
- }
- u32 idx = (u32)s - 1u;
- if (t->scopes[idx].structured) {
- /* CG places break_label just before scope_end, so the label sits
- * inside the for-body. Anything that lands on it (including a
- * `goto break_lbl` from a nested scope's labeled break) needs to
- * exit the for — without an explicit `break;`, fall-through would
- * iterate again. Always emit; if the body already terminated the
- * defensive break is dead but harmless. */
- cbuf_puts(&t->body, " break;\n");
- cbuf_puts(&t->body, " }\n");
- /* The closing brace is not a terminator; control can fall through it
- * (e.g., off the end of a void function). */
- t->last_was_terminator = 0;
- }
- t->nscopes -= 1u;
-}
-
-void c_break_to(CgTarget* T, CGScope s) {
- CTarget* t = (CTarget*)T;
- if (s == 0 || (u32)s > t->nscopes) {
- compiler_panic(t->c, t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0},
- "C target: break_to on invalid handle");
- }
- c_jump(T, t->scopes[s - 1u].break_label);
-}
-
-void c_continue_to(CgTarget* T, CGScope s) {
- CTarget* t = (CTarget*)T;
- if (s == 0 || (u32)s > t->nscopes) {
- compiler_panic(t->c, t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0},
- "C target: continue_to on invalid handle");
- }
- c_jump(T, t->scopes[s - 1u].continue_label);
-}
-
-/* ===== switch dispatch ===== */
-
-/* Emit `case <value>:`. For an int32_t selector the bare literal is
- * already the right type, so we skip the cast; for wider/narrower
- * integers we wrap in `(T)` so the case constant matches the switch
- * value's promoted type (avoids -Wswitch warnings on narrower
- * selectors). */
-static void c_emit_case_value(CTarget* t, CfreeCgTypeId sel_ty, u64 v) {
- u32 w = c_int_width_for_signedness(t, sel_ty);
- cbuf_puts(&t->body, " case ");
- if (w != 0 && w != 32) {
- cbuf_putc(&t->body, '(');
- c_emit_type(t, &t->body, sel_ty);
- cbuf_puts(&t->body, ")");
- }
- c_emit_imm_literal(t, (i64)v);
- cbuf_puts(&t->body, ":");
-}
-
-void c_switch_(CgTarget* T, const CGSwitchDesc* d) {
- CTarget* t = (CTarget*)T;
- /* gcc/clang ignore strategy hints and pick their own dispatch shape. */
- (void)d->hint;
- if (t->last_was_terminator) return;
- cbuf_puts(&t->body, " switch (");
- c_emit_operand(t, d->selector);
- cbuf_puts(&t->body, ") {\n");
- for (u32 i = 0; i < d->ncases; ++i) {
- char buf[24];
- c_label_name(d->cases[i].label, buf, sizeof buf);
- c_emit_case_value(t, d->selector.type, d->cases[i].value);
- cbuf_puts(&t->body, " goto ");
- cbuf_puts(&t->body, buf);
- cbuf_puts(&t->body, ";\n");
- }
- cbuf_puts(&t->body, " default: ");
- if (d->default_label != (Label)LABEL_NONE) {
- char buf[24];
- c_label_name(d->default_label, buf, sizeof buf);
- cbuf_puts(&t->body, "goto ");
- cbuf_puts(&t->body, buf);
- cbuf_puts(&t->body, ";\n");
- } else {
- /* No default supplied — the cfree IR's contract for that case is
- * "if no case matches, fall through." `break;` does exactly that
- * inside the for-wrapper around structured scopes. */
- cbuf_puts(&t->body, "break;\n");
- }
- cbuf_puts(&t->body, " }\n");
- /* The switch always transfers control (every arm jumps or breaks).
- * Mark as terminator so any frontend-emitted defensive jump after
- * dispatch is dropped. */
- t->last_was_terminator = 1;
-}
-
-/* ===== load_label_addr / indirect_branch =====
- * GCC computed-goto extension: `&&L` is the address of label L within
- * the current function, and `goto *p;` jumps to such an address. This
- * is the lowering every cc1-like backend uses (and what the toy
- * frontend ultimately compiles to via the C target). */
-void c_load_label_addr(CgTarget* T, Operand dst, Label l) {
- CTarget* t = (CTarget*)T;
- char buf[24];
- if (dst.kind != OPK_LOCAL) {
- compiler_panic(t->c, t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0},
- "C target: load_label_addr dst must be LOCAL");
- }
- c_ensure_local(t, dst.v.local, dst.type);
- c_emit_local_assign_open(t, dst.v.local, (CfreeCgTypeId)0);
- cbuf_puts(&t->body, "(void*)&&");
- c_label_name(l, buf, sizeof buf);
- cbuf_puts(&t->body, buf);
- c_emit_local_assign_close(t);
-}
-
-void c_indirect_branch(CgTarget* T, Operand addr, const Label* valid_targets,
- u32 ntargets) {
- CTarget* t = (CTarget*)T;
- (void)valid_targets;
- (void)ntargets;
- if (t->last_was_terminator) return;
- cbuf_puts(&t->body, " goto *");
- c_emit_operand(t, addr);
- cbuf_puts(&t->body, ";\n");
- t->last_was_terminator = 1;
-}
-
-/* ===== function-local static label-address data ===== */
-
-static int c_is_local_static_sym(CTarget* t, ObjSymId sym) {
- for (u32 i = 0; i < t->local_static_nsyms; ++i) {
- if (t->local_static_syms[i] == sym) return 1;
- }
- return 0;
-}
-
-static void c_mark_local_static_sym(CTarget* t, ObjSymId sym) {
- Heap* h = t->c->ctx->heap;
- if (sym == OBJ_SYM_NONE || c_is_local_static_sym(t, sym)) return;
- if (t->local_static_nsyms + 1u > t->local_static_syms_cap) {
- u32 oldcap = t->local_static_syms_cap;
- u32 newcap = oldcap ? oldcap * 2u : 16u;
- ObjSymId* ns = (ObjSymId*)h->realloc(
- h, t->local_static_syms, oldcap * sizeof(*t->local_static_syms),
- newcap * sizeof(*t->local_static_syms), _Alignof(ObjSymId));
- if (!ns) {
- compiler_panic(t->c, t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0},
- "C target: out of memory");
- }
- t->local_static_syms = ns;
- t->local_static_syms_cap = newcap;
- }
- t->local_static_syms[t->local_static_nsyms++] = sym;
-}
-
-static void c_grow_local_static_entries(CTarget* t, u32 want) {
- Heap* h = t->c->ctx->heap;
- if (want <= t->local_static_entries_cap) return;
- u32 oldcap = t->local_static_entries_cap;
- u32 newcap = oldcap ? oldcap * 2u : 8u;
- while (newcap < want) newcap *= 2u;
- CLocalStaticLabelEntry* ne = (CLocalStaticLabelEntry*)h->realloc(
- h, t->local_static_entries, oldcap * sizeof(*t->local_static_entries),
- newcap * sizeof(*t->local_static_entries),
- _Alignof(CLocalStaticLabelEntry));
- if (!ne) {
- compiler_panic(t->c, t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0},
- "C target: out of memory");
- }
- t->local_static_entries = ne;
- t->local_static_entries_cap = newcap;
-}
-
-int c_local_static_data_begin(CgTarget* T, const CGLocalStaticDataDesc* desc) {
- CTarget* t = (CTarget*)T;
- SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
- if (!t->cur_fn) {
- compiler_panic(t->c, loc,
- "C target: function-local static data outside function");
- }
- if (t->local_static_active) {
- compiler_panic(t->c, loc,
- "C target: nested function-local static data definition");
- }
- const CgType* ty = cg_type_get(t->c, api_unalias_type(t->c, desc->type));
- if (!ty) {
- compiler_panic(t->c, loc, "C target: unknown local static type %u",
- (unsigned)desc->type);
- }
-
- u64 count = 1;
- int is_array = 0;
- CfreeCgTypeId elem = desc->type;
- if (ty->kind == CFREE_CG_TYPE_ARRAY) {
- is_array = 1;
- count = ty->array.count;
- elem = ty->array.elem;
- ty = cg_type_get(t->c, api_unalias_type(t->c, elem));
- }
- if (!ty || ty->kind != CFREE_CG_TYPE_PTR) {
- return 0;
- }
- if (count > UINT32_MAX) {
- compiler_panic(t->c, loc, "C target: local static pointer table too large");
- }
-
- c_grow_local_static_entries(t, (u32)count);
- for (u32 i = 0; i < (u32)count; ++i) {
- t->local_static_entries[i].label = LABEL_NONE;
- t->local_static_entries[i].addend = 0;
- t->local_static_entries[i].has_label = 0;
- }
- t->local_static_nentries = (u32)count;
- t->local_static_sym = desc->sym;
- t->local_static_type = desc->type;
- t->local_static_count = count;
- t->local_static_offset = 0;
- t->local_static_ptr_width = (u32)cg_type_size(t->c, elem);
- t->local_static_align =
- desc->align ? desc->align : cg_type_align(t->c, desc->type);
- t->local_static_active = 1;
- t->local_static_is_array = (u8)is_array;
- t->local_static_readonly =
- (desc->attrs.flags & CFREE_CG_DATADEF_READONLY) ? 1u : 0u;
- c_mark_local_static_sym(t, desc->sym);
- return 1;
-}
-
-void c_local_static_data_write(CgTarget* T, const u8* data, u64 len) {
- CTarget* t = (CTarget*)T;
- SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
- if (!t->local_static_active || !len) return;
- if (data) {
- for (u64 i = 0; i < len; ++i) {
- if (data[i] != 0) {
- compiler_panic(t->c, loc,
- "C target: function-local static label table supports "
- "only zero bytes and label addresses");
- }
- }
- }
- t->local_static_offset += len;
-}
-
-void c_local_static_data_label_addr(CgTarget* T, Label target, i64 addend,
- u32 width, u32 address_space) {
- CTarget* t = (CTarget*)T;
- SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
- (void)address_space;
- if (!t->local_static_active) {
- compiler_panic(t->c, loc,
- "C target: label address outside local static data");
- }
- if (width != t->local_static_ptr_width) {
- compiler_panic(t->c, loc,
- "C target: label address width %u does not match pointer "
- "width %u",
- (unsigned)width, (unsigned)t->local_static_ptr_width);
- }
- if ((t->local_static_offset % t->local_static_ptr_width) != 0) {
- compiler_panic(t->c, loc,
- "C target: unaligned label address in local static data");
- }
- u64 idx = t->local_static_offset / t->local_static_ptr_width;
- if (idx >= t->local_static_count) {
- compiler_panic(t->c, loc,
- "C target: too many local static label table entries");
- }
- CLocalStaticLabelEntry* e = &t->local_static_entries[(u32)idx];
- if (e->has_label) {
- compiler_panic(t->c, loc,
- "C target: duplicate local static label table entry");
- }
- e->label = target;
- e->addend = addend;
- e->has_label = 1;
- t->local_static_offset += width;
-}
-
-static void c_emit_local_static_label_expr(CTarget* t,
- const CLocalStaticLabelEntry* e) {
- char lbuf[24];
- if (!e->has_label) {
- cbuf_puts(&t->decls, "(void*)0");
- return;
- }
- if (e->addend == 0) {
- cbuf_puts(&t->decls, "&&");
- c_label_name(e->label, lbuf, sizeof lbuf);
- cbuf_puts(&t->decls, lbuf);
- return;
- }
- cbuf_puts(&t->decls, "(void*)((char*)&&");
- c_label_name(e->label, lbuf, sizeof lbuf);
- cbuf_puts(&t->decls, lbuf);
- cbuf_puts(&t->decls, " + ");
- cbuf_put_i64(&t->decls, e->addend);
- cbuf_puts(&t->decls, ")");
-}
-
-void c_local_static_data_end(CgTarget* T) {
- CTarget* t = (CTarget*)T;
- SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
- if (!t->local_static_active) return;
- u64 total_size = t->local_static_count * t->local_static_ptr_width;
- if (t->local_static_offset > total_size) {
- compiler_panic(t->c, loc,
- "C target: local static initializer exceeds object size");
- }
- const char* nm = c_sym_name(t, t->local_static_sym);
- cbuf_puts(&t->decls, " static __attribute__((unused)) ");
- cbuf_puts(&t->decls, "_Alignas(");
- cbuf_put_u64(&t->decls, t->local_static_align ? t->local_static_align : 1);
- cbuf_puts(&t->decls, ") void* ");
- if (t->local_static_readonly) cbuf_puts(&t->decls, "const ");
- cbuf_puts(&t->decls, nm);
- if (t->local_static_is_array) {
- cbuf_puts(&t->decls, "[");
- cbuf_put_u64(&t->decls, t->local_static_count);
- cbuf_puts(&t->decls, "]");
- }
- cbuf_puts(&t->decls, " = {");
- for (u32 i = 0; i < t->local_static_nentries; ++i) {
- if (i > 0) cbuf_putc(&t->decls, ',');
- if ((i & 3u) == 0) cbuf_puts(&t->decls, "\n ");
- c_emit_local_static_label_expr(t, &t->local_static_entries[i]);
- }
- cbuf_puts(&t->decls, "\n };\n");
-
- t->local_static_active = 0;
- t->local_static_sym = OBJ_SYM_NONE;
- t->local_static_type = CFREE_CG_TYPE_NONE;
- t->local_static_count = 0;
- t->local_static_offset = 0;
- t->local_static_ptr_width = 0;
- t->local_static_align = 0;
- t->local_static_nentries = 0;
- t->local_static_is_array = 0;
- t->local_static_readonly = 0;
-}
-
-/* ===== local, local_addr ===== */
-
-CGLocal c_local(CgTarget* T, const CGLocalDesc* d) {
- CTarget* t = (CTarget*)T;
- t->next_local += 1u;
- if (t->next_local == CG_LOCAL_NONE) {
- compiler_panic(t->c, t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0},
- "C target: semantic local id exhausted");
- return CG_LOCAL_NONE;
- }
- c_ensure_local(t, (CGLocal)t->next_local, d->type);
- return (CGLocal)t->next_local;
-}
-
-void c_local_addr(CgTarget* T, Operand dst, const CGLocalDesc* d,
- CGLocal s) {
- CTarget* t = (CTarget*)T;
- (void)d;
- SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
- if (dst.kind != OPK_LOCAL) {
- compiler_panic(t->c, loc, "C target: local_addr dst must be LOCAL");
- }
- c_ensure_local(t, dst.v.local, dst.type);
- c_ensure_local(t, s, d->type);
- char buf[24];
- c_emit_local_assign_open(t, dst.v.local, (CfreeCgTypeId)0);
- cbuf_puts(&t->body, "&");
- c_local_name(s, buf, sizeof buf);
- cbuf_puts(&t->body, buf);
- c_emit_local_assign_close(t);
-}
-
-/* ===== convert ===== */
-
-void c_convert(CgTarget* T, ConvKind k, Operand dst, Operand src) {
- CTarget* t = (CTarget*)T;
- SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
- if (dst.kind != OPK_LOCAL) {
- compiler_panic(t->c, loc, "C target: convert dst must be LOCAL");
- }
- c_ensure_local(t, dst.v.local, dst.type);
- char buf[24];
- c_local_name(dst.v.local, buf, sizeof buf);
-
- if (k == CV_BITCAST) {
- /* Same-size reinterpretation. Use __builtin_memcpy through a temp so
- * neither aliasing nor representation assumptions creep in. The temp
- * lives in its own `{ ... }` block, so no name collision tracking. */
- u32 id = ++t->next_tmp;
- cbuf_puts(&t->body, " { ");
- c_emit_type(t, &t->body, src.type);
- cbuf_puts(&t->body, " __bc");
- cbuf_put_u64(&t->body, (u64)id);
- cbuf_puts(&t->body, " = ");
- c_emit_operand(t, src);
- cbuf_puts(&t->body, "; __builtin_memcpy(&");
- cbuf_puts(&t->body, buf);
- cbuf_puts(&t->body, ", &__bc");
- cbuf_put_u64(&t->body, (u64)id);
- cbuf_puts(&t->body, ", sizeof __bc");
- cbuf_put_u64(&t->body, (u64)id);
- cbuf_puts(&t->body, "); }\n");
- return;
- }
-
- if (c_type_is_bool(t, dst.type)) {
- c_emit_local_assign_open(t, dst.v.local, dst.type);
- cbuf_puts(&t->body, "(");
- c_emit_type(t, &t->body, dst.type);
- cbuf_puts(&t->body, ")(");
- c_emit_operand(t, src);
- cbuf_puts(&t->body, " != 0)");
- c_emit_local_assign_close(t);
- return;
- }
-
- /* Integer and float conversions: a C cast does the right thing once the
- * source is first cast to the appropriate signedness (for SEXT/ZEXT and
- * ITOF_S/U / FTOI_S/U). */
- int src_signed = 1;
- switch (k) {
- case CV_ZEXT:
- case CV_ITOF_U:
- case CV_FTOI_U:
- src_signed = 0;
- break;
- default:
- src_signed = 1;
- break;
- }
-
- /* The cast `(dst.type)(src)` produces a value of dst.type. */
- c_emit_local_assign_open(t, dst.v.local, dst.type);
- cbuf_puts(&t->body, "(");
- c_emit_type(t, &t->body, dst.type);
- cbuf_puts(&t->body, ")");
- if (k == CV_SEXT || k == CV_ZEXT) {
- c_emit_operand_signed(t, src, src_signed);
- } else if (k == CV_TRUNC && c_operand_is_ptr_typed(t, src)) {
- /* Casting a pointer directly to a narrower integer trips
- * -Wvoid-pointer-to-int-cast (and -Wpointer-to-int-cast). Bridge
- * through uintptr_t. */
- cbuf_puts(&t->body, "((uintptr_t)");
- c_emit_operand(t, src);
- cbuf_puts(&t->body, ")");
- } else {
- /* TRUNC / FTOI / ITOF / FEXT / FTRUNC: rely on C cast semantics. */
- c_emit_operand(t, src);
- }
- c_emit_local_assign_close(t);
-}
-
-/* === call === */
-
-static CfreeCgTypeId c_call_arg_type(CTarget* t, const CgType* fty,
- const CGCallDesc* d, u32 i) {
- if (i < fty->func.nparams) return fty->func.params[i].type;
- return c_local_type_or_panic(t, d->args[i]);
-}
-
-static void c_emit_call_arg(CTarget* t, const CgType* fty,
- const CGCallDesc* d, u32 i) {
- CfreeCgTypeId ty = c_call_arg_type(t, fty, d, i);
- c_ensure_local(t, d->args[i], ty);
- c_emit_operand(t, c_op_local(d->args[i], ty));
-}
-
-static void c_emit_call_expr(CTarget* t, const CgType* fty,
- const CGCallDesc* d,
- const CfreeCgTypeId* result_types) {
- if (d->callee.kind == OPK_GLOBAL) {
- c_ensure_forward_decl_with_results(
- t, d->callee.v.global.sym, d->fn_type,
- d->nresults > 1 ? result_types : NULL, d->nresults);
- cbuf_puts(&t->body, c_sym_name(t, d->callee.v.global.sym));
- } else if (d->callee.kind == OPK_LOCAL) {
- cbuf_puts(&t->body, "((");
- if (d->nresults > 1) {
- c_emit_tuple_type_name(t, &t->body, result_types, d->nresults);
- cbuf_puts(&t->body, " (*)(");
- if (fty->func.nparams == 0 && !fty->func.abi_variadic) {
- cbuf_puts(&t->body, "void");
- } else {
- for (u32 i = 0; i < fty->func.nparams; ++i) {
- if (i > 0) cbuf_puts(&t->body, ", ");
- c_emit_type(t, &t->body, fty->func.params[i].type);
- }
- if (fty->func.abi_variadic) {
- if (fty->func.nparams > 0) cbuf_puts(&t->body, ", ");
- cbuf_puts(&t->body, "...");
- }
- }
- cbuf_puts(&t->body, ")");
- } else {
- const char* fp = c_typedef_name(t, d->fn_type);
- c_ensure_typedef(t, d->fn_type);
- cbuf_puts(&t->body, fp);
- }
- cbuf_puts(&t->body, ")");
- c_emit_operand(t, d->callee);
- cbuf_puts(&t->body, ")");
- } else {
- compiler_panic(t->c, t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0},
- "C target: callee kind %d not supported",
- (int)d->callee.kind);
- }
-
- cbuf_puts(&t->body, "(");
- for (u32 i = 0; i < d->nargs; ++i) {
- if (i > 0) cbuf_puts(&t->body, ", ");
- c_emit_call_arg(t, fty, d, i);
- }
- cbuf_puts(&t->body, ")");
-}
-
-const char* c_tail_call_unrealizable_reason(CgTarget* T,
- const CGCallDesc* d) {
- CTarget* t = (CTarget*)T;
- SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
- const CgType* fty = cg_type_get(t->c, api_unalias_type(t->c, d->fn_type));
- if (!fty || fty->kind != CFREE_CG_TYPE_FUNC) {
- compiler_panic(t->c, loc, "C target: tail call: bad fn_type");
- }
- const CgType* caller =
- t->cur_fn
- ? cg_type_get(t->c, api_unalias_type(t->c, t->cur_fn->fn_type))
- : NULL;
- if (!caller || caller->kind != CFREE_CG_TYPE_FUNC) {
- compiler_panic(t->c, loc, "C target: tail call outside function");
- }
- if (caller->func.abi_variadic) {
- return "C target: caller variadic tail call not yet supported by clang "
- "musttail";
- }
- if (fty->func.abi_variadic) {
- return "C target: variadic tail call not yet supported by clang musttail";
- }
- if (caller->func.nparams != fty->func.nparams) {
- return "C target: tail call with differing parameter counts not yet "
- "supported by clang musttail";
- }
- return NULL;
-}
-
-void c_call(CgTarget* T, const CGCallDesc* d) {
- CTarget* t = (CTarget*)T;
- SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
- Heap* h = t->c->ctx->heap;
-
- const CgType* fty = cg_type_get(t->c, api_unalias_type(t->c, d->fn_type));
- if (!fty || fty->kind != CFREE_CG_TYPE_FUNC) {
- compiler_panic(t->c, loc, "C target: call: bad fn_type");
- }
- CfreeCgTypeId ret_type = fty->func.ret;
- int is_tail = (d->flags & CG_CALL_TAIL) != 0;
- CfreeCgTypeId* result_types = NULL;
- if (d->nresults > 1) {
- result_types = (CfreeCgTypeId*)h->alloc(
- h, sizeof(CfreeCgTypeId) * d->nresults, _Alignof(CfreeCgTypeId));
- if (!result_types) {
- compiler_panic(t->c, loc, "C target: out of memory");
- return;
- }
- for (u32 i = 0; i < d->nresults; ++i)
- result_types[i] = c_local_type_or_panic(t, d->results[i]);
- c_ensure_tuple_typedef(t, result_types, d->nresults);
- }
-
- if (is_tail) {
- cbuf_puts(&t->body, " __attribute__((musttail)) return ");
- c_emit_call_expr(t, fty, d, result_types);
- cbuf_puts(&t->body, ";\n");
- t->last_was_terminator = 1;
- } else if (d->nresults == 0) {
- cbuf_puts(&t->body, " ");
- c_emit_call_expr(t, fty, d, result_types);
- cbuf_puts(&t->body, ";\n");
- } else if (d->nresults == 1) {
- c_ensure_local(t, d->results[0], ret_type);
- c_emit_local_assign_open(t, d->results[0], ret_type);
- c_emit_call_expr(t, fty, d, result_types);
- c_emit_local_assign_close(t);
- } else {
- char tmp[32];
- u32 tmp_id = t->next_tmp++;
- snprintf(tmp, sizeof tmp, "__cfree_call_%u", (unsigned)tmp_id);
- cbuf_puts(&t->body, " ");
- c_emit_tuple_type_name(t, &t->body, result_types, d->nresults);
- cbuf_putc(&t->body, ' ');
- cbuf_puts(&t->body, tmp);
- cbuf_puts(&t->body, " = ");
- c_emit_call_expr(t, fty, d, result_types);
- cbuf_puts(&t->body, ";\n");
- for (u32 i = 0; i < d->nresults; ++i) {
- c_ensure_local(t, d->results[i], result_types[i]);
- c_emit_local_assign_open(t, d->results[i], result_types[i]);
- cbuf_puts(&t->body, tmp);
- cbuf_puts(&t->body, ".f");
- cbuf_put_u64(&t->body, (u64)i);
- c_emit_local_assign_close(t);
- }
- }
- if (result_types) h->free(h, result_types,
- sizeof(CfreeCgTypeId) * d->nresults);
-}
-
-/* === load / store === */
-
-void c_load(CgTarget* T, Operand dst, Operand addr, MemAccess m) {
- CTarget* t = (CTarget*)T;
- SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
- if (dst.kind != OPK_LOCAL) {
- compiler_panic(t->c, loc, "C target: load dst must be LOCAL");
- }
- c_ensure_local(t, dst.v.local, dst.type);
- CfreeCgTypeId access_ty = m.type ? m.type : dst.type;
- if (c_type_is_aggregate(t, access_ty) && !c_type_is_aggregate(t, dst.type))
- access_ty = dst.type;
- /* The deref `*(access_ty*)addr` produces a value of access_ty. */
- c_emit_local_assign_open(t, dst.v.local, access_ty);
- c_emit_addr_deref(t, addr, access_ty);
- c_emit_local_assign_close(t);
-}
-
-void c_store(CgTarget* T, Operand addr, Operand src, MemAccess m) {
- CTarget* t = (CTarget*)T;
- CfreeCgTypeId access_ty = m.type ? m.type : src.type;
- if (c_type_is_aggregate(t, access_ty) && !c_type_is_aggregate(t, src.type))
- access_ty = src.type;
- cbuf_puts(&t->body, " ");
- c_emit_addr_deref(t, addr, access_ty);
- /* c_emit_operand_as bridges int/ptr crossings through uintptr_t so
- * roundtrips don't trip `-Wint-conversion`. */
- cbuf_puts(&t->body, " = ");
- c_emit_operand_as(t, src, access_ty);
- cbuf_puts(&t->body, ";\n");
-}
-
-void c_addr_of(CgTarget* T, Operand dst, Operand lv) {
- CTarget* t = (CTarget*)T;
- SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
- if (dst.kind != OPK_LOCAL) {
- compiler_panic(t->c, loc, "C target: addr_of dst must be LOCAL");
- }
- c_ensure_local(t, dst.v.local, dst.type);
- /* `c_emit_lvalue_addr` casts its output to dst.type already. */
- c_emit_local_assign_open(t, dst.v.local, dst.type);
- c_emit_lvalue_addr(t, lv, dst.type);
- c_emit_local_assign_close(t);
-}
-
-void c_ret(CgTarget* T, const CGLocal* values, u32 nvalues) {
- CTarget* t = (CTarget*)T;
- /* Already-terminated block: this ret is unreachable (the frontend's
- * defensive `return 0;` epilogue lands here right after a user return). */
- if (t->last_was_terminator) return;
- /* CG emits a defensive ret_void epilogue at the end of every function. For
- * a non-void function that's unreachable; emitting a bare `return;` would
- * trip -Wreturn-type. Spell it as `__builtin_unreachable()` so the host C
- * compiler sees the path is dead without us inventing a fake value. */
- if (nvalues == 0 && t->cur_fn) {
- if (t->cur_fn->nresults != 0) {
- cbuf_puts(&t->body, " __builtin_unreachable();\n");
- t->last_was_terminator = 1;
- return;
- }
- }
- cbuf_puts(&t->body, " return");
- if (nvalues == 1) {
- cbuf_puts(&t->body, " ");
- CfreeCgTypeId ret_type = t->cur_fn
- ? t->cur_fn->result_types[0]
- : (CfreeCgTypeId)0;
- const CgType* rty =
- ret_type ? cg_type_get(t->c, api_unalias_type(t->c, ret_type)) : NULL;
- int is_aggregate = rty && (rty->kind == CFREE_CG_TYPE_RECORD ||
- rty->kind == CFREE_CG_TYPE_ARRAY);
- if (ret_type && !is_aggregate) {
- CfreeCgTypeId value_ty = c_local_type_or_panic(t, values[0]);
- c_emit_operand_as(t, c_op_local(values[0], value_ty), ret_type);
- } else {
- c_emit_operand(t, c_op_local(values[0], ret_type));
- }
- } else if (nvalues > 1) {
- const CfreeCgTypeId* result_types =
- t->cur_fn ? t->cur_fn->result_types : NULL;
- if (!result_types || t->cur_fn->nresults != nvalues) {
- compiler_panic(t->c, t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0},
- "C target: multi-return shape mismatch");
- return;
- }
- c_ensure_tuple_typedef(t, result_types, nvalues);
- cbuf_puts(&t->body, " (");
- c_emit_tuple_type_name(t, &t->body, result_types, nvalues);
- cbuf_puts(&t->body, "){");
- for (u32 i = 0; i < nvalues; ++i) {
- if (i > 0) cbuf_puts(&t->body, ", ");
- c_emit_operand(t, c_op_local(values[i], result_types[i]));
- }
- cbuf_puts(&t->body, "}");
- }
- cbuf_puts(&t->body, ";\n");
- t->last_was_terminator = 1;
-}
-
-/* === alias ===
- * `cfree_cg_alias` makes alias_sym refer to target_sym's body. In obj-file
- * land that's two ObjSyms sharing a (section_id, value); in C source we
- * have to spell it out:
- *
- * ELF/PE → `Ret alias(args) __attribute__((alias("target")));`
- * Single definition, true aliasing, &alias == &target.
- * Mach-O → emit a thunk `Ret alias(args) { return target(args); }`.
- * Clang on Darwin rejects __attribute__((alias)) outright,
- * so we fall back to a wrapper. Loses the `&alias==&target`
- * identity but preserves call-through semantics, which is
- * all the cfree-emitted code path needs.
- *
- * The emitted decl serves as the alias definition AND a forward prototype
- * for callers, so we mark sym_forwarded to dedup against a later c_call. */
-void c_alias(CgTarget* T, ObjSymId alias_sym, ObjSymId target_sym,
- CfreeCgTypeId type) {
- CTarget* t = (CTarget*)T;
- Heap* h = t->c->ctx->heap;
- if ((u32)alias_sym >= t->sym_forwarded_cap) {
- u32 newcap = t->sym_forwarded_cap ? t->sym_forwarded_cap : 16;
- while (newcap <= (u32)alias_sym) newcap *= 2;
- u8* nd =
- (u8*)h->realloc(h, t->sym_forwarded, t->sym_forwarded_cap, newcap, 1);
- if (!nd && newcap) {
- compiler_panic(t->c, (SrcLoc){0, 0, 0}, "C target: out of memory");
- }
- for (u32 i = t->sym_forwarded_cap; i < newcap; ++i) nd[i] = 0;
- t->sym_forwarded = nd;
- t->sym_forwarded_cap = newcap;
- }
- if (t->sym_forwarded[alias_sym]) return;
- t->sym_forwarded[alias_sym] = 1;
- const char* alias_name = c_sym_name(t, alias_sym);
- const char* target_name = c_sym_name(t, target_sym);
- const CgType* fty = cg_type_get(t->c, api_unalias_type(t->c, type));
- int is_func = fty && fty->kind == CFREE_CG_TYPE_FUNC;
-
- if (t->c->target.obj != CFREE_OBJ_MACHO) {
- /* Attribute form. Works for both function and object aliases on ELF
- * and PE/COFF. */
- c_emit_func_signature(t, &t->forwards, alias_name, type, NULL, 0);
- cbuf_puts(&t->forwards, " __attribute__((alias(\"");
- cbuf_puts(&t->forwards, target_name);
- cbuf_puts(&t->forwards, "\")));\n");
- return;
- }
-
- /* Mach-O thunk fallback. Functions only for v1 — object aliases on
- * Darwin would need a more elaborate scheme (see doc/CBACKEND.md). */
- if (!is_func) {
- compiler_panic(t->c, (SrcLoc){0, 0, 0},
- "C target: object alias on Mach-O not yet supported");
- }
- /* Forward prototype for the target (its full definition lands separately
- * via c_func_begin). Also dedup that. */
- c_ensure_forward_decl(t, target_sym, type);
- /* `static`? No — alias must be externally visible. */
- c_emit_func_signature(t, &t->forwards, alias_name, type, NULL, 0);
- cbuf_puts(&t->forwards, " { ");
- CfreeCgTypeId ret_type = cg_type_func_ret_id(t->c, type);
- if (!cg_type_is_void(t->c, ret_type)) cbuf_puts(&t->forwards, "return ");
- cbuf_puts(&t->forwards, target_name);
- cbuf_puts(&t->forwards, "(");
- for (u32 i = 0; i < fty->func.nparams; ++i) {
- if (i > 0) cbuf_puts(&t->forwards, ", ");
- cbuf_puts(&t->forwards, "p");
- cbuf_put_u64(&t->forwards, (u64)i);
- }
- cbuf_puts(&t->forwards, "); }\n");
-}
-
-/* === intrinsic ===
- *
- * All cfree IntrinKinds map onto gcc/clang `__builtin_*` builtins, which
- * the host C compiler then turns into the appropriate sequence (inline op,
- * libcall, runtime CAS, etc.). This is exactly the seam the doc described:
- * cfree records intent, the downstream toolchain picks the mechanism.
- *
- * Operand shapes follow arch.h §IntrinKind. */
-
-static const char* c_bitop_builtin(IntrinKind k, u32 width) {
- switch (k) {
- case INTRIN_POPCOUNT:
- if (width == 32) return "__builtin_popcount";
- if (width == 64) return "__builtin_popcountll";
- if (width == 16 || width == 8) return "__builtin_popcount";
- return NULL;
- case INTRIN_CTZ:
- if (width == 32) return "__builtin_ctz";
- if (width == 64) return "__builtin_ctzll";
- if (width == 16 || width == 8) return "__builtin_ctz";
- return NULL;
- case INTRIN_CLZ:
- if (width == 32) return "__builtin_clz";
- if (width == 64) return "__builtin_clzll";
- if (width == 16 || width == 8) return "__builtin_clz";
- return NULL;
- case INTRIN_BSWAP16:
- return "__builtin_bswap16";
- case INTRIN_BSWAP32:
- return "__builtin_bswap32";
- case INTRIN_BSWAP64:
- return "__builtin_bswap64";
- default:
- return NULL;
- }
-}
-
-static const char* c_overflow_builtin(IntrinKind k) {
- switch (k) {
- case INTRIN_SADD_OVERFLOW:
- case INTRIN_UADD_OVERFLOW:
- return "__builtin_add_overflow";
- case INTRIN_SSUB_OVERFLOW:
- case INTRIN_USUB_OVERFLOW:
- return "__builtin_sub_overflow";
- case INTRIN_SMUL_OVERFLOW:
- case INTRIN_UMUL_OVERFLOW:
- return "__builtin_mul_overflow";
- default:
- return NULL;
- }
-}
-
-void c_intrinsic(CgTarget* T, IntrinKind k, Operand* dsts, u32 ndst,
- const Operand* args, u32 narg) {
- CTarget* t = (CTarget*)T;
- SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
- switch (k) {
- case INTRIN_UNREACHABLE:
- cbuf_puts(&t->body, " __builtin_unreachable();\n");
- return;
- case INTRIN_TRAP:
- cbuf_puts(&t->body, " __builtin_trap();\n");
- return;
- case INTRIN_PREFETCH: {
- cbuf_puts(&t->body, " __builtin_prefetch(");
- for (u32 i = 0; i < narg; ++i) {
- if (i > 0) cbuf_puts(&t->body, ", ");
- c_emit_operand(t, args[i]);
- }
- cbuf_puts(&t->body, ");\n");
- return;
- }
- case INTRIN_ASSUME_ALIGNED: {
- /* dsts[0] is the result local (pointer); args = (ptr, align [, ofs]) */
- if (ndst != 1) {
- compiler_panic(t->c, loc,
- "C target: assume_aligned: expected 1 dst, got %u",
- (unsigned)ndst);
- }
- c_ensure_local(t, dsts[0].v.local, dsts[0].type);
- /* Returns void*; bridge to dst pointer type. */
- c_emit_local_assign_open(t, dsts[0].v.local, (CfreeCgTypeId)0);
- cbuf_puts(&t->body, "__builtin_assume_aligned(");
- for (u32 i = 0; i < narg; ++i) {
- if (i > 0) cbuf_puts(&t->body, ", ");
- c_emit_operand(t, args[i]);
- }
- cbuf_puts(&t->body, ")");
- c_emit_local_assign_close(t);
- return;
- }
- case INTRIN_EXPECT: {
- /* dsts[0] = __builtin_expect(args[0], args[1]) but typed via long. */
- if (ndst != 1 || narg != 2) {
- compiler_panic(t->c, loc,
- "C target: expect: bad shape (ndst=%u narg=%u)",
- (unsigned)ndst, (unsigned)narg);
- }
- c_ensure_local(t, dsts[0].v.local, dsts[0].type);
- /* Returns `long`; dst.type may be a narrower int — keep the bridge. */
- c_emit_local_assign_open(t, dsts[0].v.local, (CfreeCgTypeId)0);
- cbuf_puts(&t->body, "__builtin_expect((long)");
- c_emit_operand(t, args[0]);
- cbuf_puts(&t->body, ", (long)");
- c_emit_operand(t, args[1]);
- cbuf_puts(&t->body, ")");
- c_emit_local_assign_close(t);
- return;
- }
- case INTRIN_POPCOUNT:
- case INTRIN_CTZ:
- case INTRIN_CLZ:
- case INTRIN_BSWAP16:
- case INTRIN_BSWAP32:
- case INTRIN_BSWAP64: {
- if (ndst != 1 || narg != 1) {
- compiler_panic(t->c, loc,
- "C target: bit-intrin: bad shape (ndst=%u narg=%u)",
- (unsigned)ndst, (unsigned)narg);
- }
- u32 w = c_int_width_for_signedness(t, args[0].type);
- const char* fn = c_bitop_builtin(k, w);
- if (!fn) {
- compiler_panic(t->c, loc, "C target: bit-intrin width %u unsupported",
- (unsigned)w);
- }
- c_ensure_local(t, dsts[0].v.local, dsts[0].type);
- /* __builtin_popcount/ctz/clz return `int`; bswap returns its input
- * type. Narrow to dst.type via the bridge. */
- c_emit_local_assign_open(t, dsts[0].v.local, (CfreeCgTypeId)0);
- cbuf_puts(&t->body, fn);
- cbuf_puts(&t->body, "(");
- c_emit_operand(t, args[0]);
- cbuf_puts(&t->body, ")");
- c_emit_local_assign_close(t);
- return;
- }
- case INTRIN_MEMCPY:
- case INTRIN_MEMMOVE:
- case INTRIN_MEMSET: {
- const char* fn = (k == INTRIN_MEMCPY) ? "__builtin_memcpy"
- : (k == INTRIN_MEMMOVE) ? "__builtin_memmove"
- : "__builtin_memset";
- cbuf_puts(&t->body, " ");
- cbuf_puts(&t->body, fn);
- cbuf_puts(&t->body, "(");
- for (u32 i = 0; i < narg; ++i) {
- if (i > 0) cbuf_puts(&t->body, ", ");
- /* The pointer operands (dst for all three; src for mem{cpy,move})
- * may be typed as a plain integer local when they come from address
- * arithmetic, which the C target declares as int64_t. The builtins
- * take void*, so cast explicitly to avoid -Wint-conversion. */
- int is_ptr_arg =
- (i == 0) || (i == 1 && (k == INTRIN_MEMCPY || k == INTRIN_MEMMOVE));
- if (is_ptr_arg) cbuf_puts(&t->body, "(void*)");
- c_emit_operand(t, args[i]);
- }
- cbuf_puts(&t->body, ");\n");
- return;
- }
- case INTRIN_SADD_OVERFLOW:
- case INTRIN_UADD_OVERFLOW:
- case INTRIN_SSUB_OVERFLOW:
- case INTRIN_USUB_OVERFLOW:
- case INTRIN_SMUL_OVERFLOW:
- case INTRIN_UMUL_OVERFLOW: {
- /* dsts[0] = value local, dsts[1] = i1 overflow flag.
- *
- * Signedness comes from the intrinsic kind, but cfree's CG int type
- * is width-only and the C target declares every result as a signed
- * fixed-width (int{8,16,32,64}_t). __builtin_*_overflow keys its
- * overflow check on the result type, so passing the signed local
- * directly makes a UADD test as if it were signed and miss true
- * unsigned overflow. Wrap the call in a block with a scratch result
- * of the right signedness and copy it back through the int/uint
- * bridge. */
- if (ndst != 2 || narg != 2) {
- compiler_panic(t->c, loc, "C target: overflow-intrin: bad shape");
- }
- int is_unsigned =
- (k == INTRIN_UADD_OVERFLOW || k == INTRIN_USUB_OVERFLOW ||
- k == INTRIN_UMUL_OVERFLOW);
- const char* fn = c_overflow_builtin(k);
- c_ensure_local(t, dsts[0].v.local, dsts[0].type);
- c_ensure_local(t, dsts[1].v.local, dsts[1].type);
- char vbuf[24], obuf[24];
- c_local_name(dsts[0].v.local, vbuf, sizeof vbuf);
- c_local_name(dsts[1].v.local, obuf, sizeof obuf);
- u32 w = c_int_width_for_signedness(t, dsts[0].type);
- const char* sty = c_int_type_name_for_width(w, !is_unsigned);
- if (!sty) {
- compiler_panic(t->c, loc,
- "C target: overflow-intrin: unsupported width %u",
- (unsigned)w);
- }
- cbuf_puts(&t->body, " { ");
- cbuf_puts(&t->body, sty);
- cbuf_puts(&t->body, " __ovsc; ");
- cbuf_puts(&t->body, obuf);
- cbuf_puts(&t->body, " = (");
- c_emit_type(t, &t->body, dsts[1].type);
- cbuf_puts(&t->body, ")");
- cbuf_puts(&t->body, fn);
- cbuf_puts(&t->body, "((");
- cbuf_puts(&t->body, sty);
- cbuf_puts(&t->body, ")");
- c_emit_operand(t, args[0]);
- cbuf_puts(&t->body, ", (");
- cbuf_puts(&t->body, sty);
- cbuf_puts(&t->body, ")");
- c_emit_operand(t, args[1]);
- cbuf_puts(&t->body, ", &__ovsc); ");
- cbuf_puts(&t->body, vbuf);
- cbuf_puts(&t->body, " = (");
- c_emit_type(t, &t->body, dsts[0].type);
- cbuf_puts(&t->body, ")__ovsc; }\n");
- return;
- }
- case INTRIN_SETJMP: {
- t->need_setjmp = 1;
- if (ndst != 1 || narg != 1) {
- compiler_panic(t->c, loc, "C target: setjmp: bad shape");
- }
- c_ensure_local(t, dsts[0].v.local, dsts[0].type);
- /* setjmp returns `int`; bridge to dst.type. */
- c_emit_local_assign_open(t, dsts[0].v.local, (CfreeCgTypeId)0);
- cbuf_puts(&t->body, "setjmp(*(jmp_buf*)(");
- c_emit_operand(t, args[0]);
- cbuf_puts(&t->body, "))");
- c_emit_local_assign_close(t);
- return;
- }
- case INTRIN_LONGJMP: {
- t->need_setjmp = 1;
- cbuf_puts(&t->body, " longjmp(*(jmp_buf*)(");
- c_emit_operand(t, args[0]);
- cbuf_puts(&t->body, "), (int)");
- c_emit_operand(t, args[1]);
- cbuf_puts(&t->body, ");\n");
- return;
- }
- case INTRIN_NONE:
- default:
- compiler_panic(t->c, loc, "C target: intrinsic kind %d not handled",
- (int)k);
- }
-}
-
-/* === alloca === */
-
-void c_alloca(CgTarget* T, Operand dst, Operand size, u32 align) {
- CTarget* t = (CTarget*)T;
- SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
- if (dst.kind != OPK_LOCAL) {
- compiler_panic(t->c, loc, "C target: alloca dst must be LOCAL");
- }
- c_ensure_local(t, dst.v.local, dst.type);
- /* __builtin_alloca returns `void*`; dst.type is typically void* too. */
- c_emit_local_assign_open(t, dst.v.local, dst.type);
- if (align > 1) {
- /* gcc has __builtin_alloca_with_align taking bits, not bytes. */
- cbuf_puts(&t->body, "__builtin_alloca_with_align(");
- c_emit_operand(t, size);
- cbuf_puts(&t->body, ", ");
- cbuf_put_u64(&t->body, (u64)align * 8u);
- cbuf_puts(&t->body, ")");
- } else {
- cbuf_puts(&t->body, "__builtin_alloca(");
- c_emit_operand(t, size);
- cbuf_puts(&t->body, ")");
- }
- c_emit_local_assign_close(t);
-}
-
-/* === varargs ===
- *
- * The C-target va_list is the host toolchain's `va_list` from <stdarg.h>.
- * The first arg of all va_* is `ap_addr` - the address of the va_list local.
- * We deref to get the va_list lvalue C's macros expect. */
-
-void c_va_start(CgTarget* T, Operand ap_addr) {
- CTarget* t = (CTarget*)T;
- t->need_stdarg = 1;
- /* va_start needs the "last named parameter". CG doesn't pass that to the
- * backend; gcc/clang accept any non-modified ident here for variadic
- * compatibility — feed the synthesized parameter name `p<nparams-1>` from
- * the enclosing function. */
- const CGFuncDesc* fd = t->cur_fn;
- SrcLoc loc = fd ? fd->loc : (SrcLoc){0, 0, 0};
- if (!fd) compiler_panic(t->c, loc, "C target: va_start outside function");
- const CgType* fty = cg_type_get(t->c, api_unalias_type(t->c, fd->fn_type));
- if (!fty || fty->kind != CFREE_CG_TYPE_FUNC || fty->func.nparams == 0) {
- compiler_panic(t->c, loc,
- "C target: va_start in non-variadic function shape");
- }
- cbuf_puts(&t->body, " __builtin_va_start(*(va_list*)(");
- c_emit_operand(t, ap_addr);
- cbuf_puts(&t->body, "), p");
- cbuf_put_u64(&t->body, (u64)(fty->func.nparams - 1u));
- cbuf_puts(&t->body, ");\n");
-}
-
-void c_va_end(CgTarget* T, Operand ap_addr) {
- CTarget* t = (CTarget*)T;
- cbuf_puts(&t->body, " __builtin_va_end(*(va_list*)(");
- c_emit_operand(t, ap_addr);
- cbuf_puts(&t->body, "));\n");
-}
-
-void c_va_copy(CgTarget* T, Operand dst_addr, Operand src_addr) {
- CTarget* t = (CTarget*)T;
- cbuf_puts(&t->body, " __builtin_va_copy(*(va_list*)(");
- c_emit_operand(t, dst_addr);
- cbuf_puts(&t->body, "), *(va_list*)(");
- c_emit_operand(t, src_addr);
- cbuf_puts(&t->body, "));\n");
-}
-
-void c_va_arg(CgTarget* T, Operand dst, Operand ap_addr, CfreeCgTypeId ty) {
- CTarget* t = (CTarget*)T;
- SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
- if (dst.kind != OPK_LOCAL) {
- compiler_panic(t->c, loc, "C target: va_arg dst must be LOCAL");
- }
- c_ensure_local(t, dst.v.local, dst.type);
- /* __builtin_va_arg yields a value of `ty`. */
- c_emit_local_assign_open(t, dst.v.local, ty);
- cbuf_puts(&t->body, "__builtin_va_arg(*(va_list*)(");
- c_emit_operand(t, ap_addr);
- cbuf_puts(&t->body, "), ");
- c_emit_type(t, &t->body, ty);
- cbuf_puts(&t->body, ")");
- c_emit_local_assign_close(t);
-}
-
-/* === copy_bytes / set_bytes === */
-
-void c_copy_bytes(CgTarget* T, Operand dst_addr, Operand src_addr,
- AggregateAccess m) {
- CTarget* t = (CTarget*)T;
- c_assert_no_index(t, dst_addr, "copy_bytes dst");
- c_assert_no_index(t, src_addr, "copy_bytes src");
- /* dst/src may be plain integer regs from address arithmetic (declared
- * int64_t); __builtin_memcpy takes void*, so cast to avoid
- * -Wint-conversion. */
- cbuf_puts(&t->body, " __builtin_memcpy((void*)");
- c_emit_copy_addr(t, dst_addr);
- cbuf_puts(&t->body, ", (void*)");
- c_emit_copy_addr(t, src_addr);
- cbuf_puts(&t->body, ", ");
- cbuf_put_u64(&t->body, (u64)m.size);
- cbuf_puts(&t->body, ");\n");
-}
-
-static void c_emit_copy_addr(CTarget* t, Operand addr) {
- char buf[24];
- SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
- switch (addr.kind) {
- case OPK_LOCAL:
- c_ensure_local(t, addr.v.local, addr.type);
- if (c_operand_is_ptr_typed(t, addr)) {
- c_emit_operand(t, addr);
- } else {
- cbuf_putc(&t->body, '&');
- c_local_name(addr.v.local, buf, sizeof buf);
- cbuf_puts(&t->body, buf);
- }
- return;
- case OPK_GLOBAL: {
- obj_sym_mark_referenced(t->obj, addr.v.global.sym);
- cbuf_puts(&t->body, "((char*)&");
- cbuf_puts(&t->body, c_sym_name(t, addr.v.global.sym));
- if (addr.v.global.addend != 0) {
- cbuf_puts(&t->body, " + ");
- cbuf_put_i64(&t->body, addr.v.global.addend);
- }
- cbuf_putc(&t->body, ')');
- return;
- }
- case OPK_INDIRECT:
- c_emit_indirect_addr_expr(t, c_addr_mode(addr));
- return;
- default:
- compiler_panic(t->c, loc,
- "C target: copy_bytes address operand kind %d not "
- "supported",
- (int)addr.kind);
- }
-}
-
-void c_set_bytes(CgTarget* T, Operand dst_addr, Operand byte_value,
- AggregateAccess m) {
- CTarget* t = (CTarget*)T;
- c_assert_no_index(t, dst_addr, "set_bytes dst");
- /* dst may be a plain integer local from address arithmetic (declared
- * int64_t); __builtin_memset takes void*, so cast to avoid
- * -Wint-conversion. */
- cbuf_puts(&t->body, " __builtin_memset((void*)");
- c_emit_copy_addr(t, dst_addr);
- cbuf_puts(&t->body, ", (int)");
- c_emit_operand(t, byte_value);
- cbuf_puts(&t->body, ", ");
- cbuf_put_u64(&t->body, (u64)m.size);
- 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_LOCAL) {
- compiler_panic(t->c, loc, "C target: tls_addr_of dst must be LOCAL");
- }
- c_ensure_local(t, dst.v.local, dst.type);
- const char* nm = c_sym_name(t, sym);
- /* RHS spells `(char*)&sym + addend` — pointer type that may not match
- * dst.type; keep the bridge to cast through cleanly. */
- c_emit_local_assign_open(t, dst.v.local, (CfreeCgTypeId)0);
- 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_local_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 an address expression for a backend-addressable lvalue operand.
- * Unlike c_emit_operand, this never reads the object value; it materializes
- * the address of the local/global/indirect storage itself. */
-static void c_emit_lvalue_addr_expr_raw(CTarget* t, Operand addr) {
- char buf[24];
- SrcLoc loc = t->cur_fn ? t->cur_fn->loc : (SrcLoc){0, 0, 0};
- switch (addr.kind) {
- case OPK_LOCAL:
- cbuf_putc(&t->body, '&');
- c_ensure_local(t, addr.v.local, addr.type);
- c_local_name(addr.v.local, buf, sizeof buf);
- cbuf_puts(&t->body, buf);
- return;
- case OPK_GLOBAL: {
- obj_sym_mark_referenced(t->obj, addr.v.global.sym);
- const char* nm = c_sym_name(t, addr.v.global.sym);
- 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_putc(&t->body, ')');
- return;
- }
- case OPK_INDIRECT: {
- CAddrMode m = c_addr_mode(addr);
- if ((u32)m.base >= t->local_cap || !t->local_declared[m.base]) {
- compiler_panic(t->c, loc,
- "C target: bitfield on undeclared base local v%u",
- (unsigned)m.base);
- }
- cbuf_putc(&t->body, '(');
- c_emit_indirect_addr_expr(t, m);
- cbuf_putc(&t->body, ')');
- return;
- }
- default:
- compiler_panic(t->c, loc,
- "C target: bitfield address on operand kind %d not "
- "supported",
- (int)addr.kind);
- }
-}
-
-/* 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_lvalue_addr_expr_raw(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_LOCAL) {
- compiler_panic(t->c, loc, "C target: bitfield_load dst must be LOCAL");
- }
- c_assert_no_index(t, addr, "bitfield_load");
- if (bf.bit_width == 0) {
- /* Zero-width — layout barrier only; nothing to load. Emit a no-op
- * assignment so the dst local still gets a defined value. */
- c_ensure_local(t, dst.v.local, dst.type);
- /* RHS is the literal 0 (int); narrowing to dst.type is fine. */
- c_emit_local_assign_open(t, dst.v.local, dst.type);
- cbuf_puts(&t->body, "0");
- c_emit_local_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_local(t, dst.v.local, dst.type);
- /* RHS is the storage-width int from the mask/shift expression; bridge
- * to dst.type so any signedness/width adjustment is explicit. */
- c_emit_local_assign_open(t, dst.v.local, (CfreeCgTypeId)0);
- /* 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_local_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};
- c_assert_no_index(t, addr, "bitfield_store");
- 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;
- for (u32 i = 0; i < no; ++i) c_assert_no_index(t, oo[i], "asm_block out");
- for (u32 i = 0; i < ni; ++i) c_assert_no_index(t, io[i], "asm_block in");
- 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_slice(t->c->global, outs[i].name).s);
- 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_LOCAL is a plain C local; this
- * works directly. OPK_LOCAL / OPK_INDIRECT also produce lvalues. */
- if (oo[i].kind == OPK_LOCAL) {
- c_ensure_local(t, oo[i].v.local, oo[i].type);
- char rb[24];
- c_local_name(oo[i].v.local, 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_slice(t->c->global, ins[i].name).s);
- 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_slice(t->c->global, clobs[i]).s);
- }
- 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 local 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_LOCAL) {
- compiler_panic(t->c, loc, "C target: load_const dst must be LOCAL");
- }
- c_ensure_local(t, dst.v.local, dst.type);
- char buf[24];
- c_local_name(dst.v.local, 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_assert_no_index(t, addr, "atomic_load");
- c_ensure_local(t, dst.v.local, dst.type);
- /* __atomic_load_n returns a value of the pointed-to type (dst.type). */
- c_emit_local_assign_open(t, dst.v.local, dst.type);
- 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_local_assign_close(t);
-}
-
-void c_atomic_store(CgTarget* T, Operand addr, Operand src, MemAccess m,
- MemOrder o) {
- CTarget* t = (CTarget*)T;
- (void)m;
- c_assert_no_index(t, addr, "atomic_store");
- 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};
- c_assert_no_index(t, addr, "atomic_rmw");
- 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_local(t, dst.v.local, dst.type);
- /* __atomic_fetch_* returns the prior value of the pointed-to type. */
- c_emit_local_assign_open(t, dst.v.local, val.type);
- 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_local_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;
- c_assert_no_index(t, addr, "atomic_cas");
- /* gcc's __atomic_compare_exchange_n needs a real lvalue holding the
- * expected value because it is updated on failure. Materialize a scratch
- * local with the compare type, then copy it out to the prior result. */
- c_ensure_local(t, prior.v.local, prior.type);
- c_ensure_local(t, ok.v.local, ok.type);
- 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_local_name(ok.v.local, ok_name, sizeof ok_name);
- c_local_name(prior.v.local, 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 local = __cas; */
- cbuf_puts(&t->body, prior_name);
- 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 === */
-
-static void cbuf_put_line_filename(CBuf* b, CfreeSlice s) {
- size_t i;
- cbuf_putc(b, '"');
- for (i = 0; i < s.len; ++i) {
- {
- unsigned char ch = (unsigned char)s.s[i];
- switch (ch) {
- case '\\':
- case '"':
- cbuf_putc(b, '\\');
- cbuf_putc(b, (char)ch);
- break;
- case '\n':
- cbuf_puts(b, "\\n");
- break;
- case '\r':
- cbuf_puts(b, "\\r");
- break;
- case '\t':
- cbuf_puts(b, "\\t");
- break;
- default:
- cbuf_putc(b, (char)ch);
- break;
- }
- }
- }
- cbuf_putc(b, '"');
-}
-
-void c_set_loc(CgTarget* T, SrcLoc l) {
- CTarget* t = (CTarget*)T;
- CfreeSlice file;
-
- if (!t->cur_fn || l.file_id == 0 || l.line == 0) return;
- if (t->have_emitted_loc && t->emitted_loc.file_id == l.file_id &&
- t->emitted_loc.line == l.line) {
- return;
- }
-
- file = cfree_compiler_file_name(t->c, l.file_id);
- if (!file.len) return;
-
- cbuf_puts(&t->body, "#line ");
- cbuf_put_u64(&t->body, (u64)l.line);
- cbuf_putc(&t->body, ' ');
- cbuf_put_line_filename(&t->body, file);
- cbuf_putc(&t->body, '\n');
-
- t->emitted_loc = l;
- t->have_emitted_loc = 1;
-}
-
-/* === 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 }");
-}
-
-/* Mach-O TLS support: the user-visible SK_TLS symbol is a 24-byte TLV
- * descriptor in __DATA,__thread_vars, and the actual initial bytes live in
- * a synthesized `<name>$tlv$init` sym in __thread_data (or __thread_bss).
- * The descriptor carries an R_ABS64 reloc at offset +16 pointing at that
- * init sym. For C-source emission we don't care about the descriptor at all
- * — we just emit `_Thread_local` with the init sym's bytes and let the host
- * C compiler synthesize whatever TLV plumbing it needs. */
-
-/* Find the data init sym referenced by a Mach-O TLS descriptor at
- * `desc_base` in section `desc_sec`. Looks for an R_ABS64 reloc at
- * `desc_base + 16`. Returns OBJ_SYM_NONE if not found. */
-static ObjSymId c_macho_tls_find_init(CTarget* t, ObjSecId desc_sec,
- u32 desc_base) {
- 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 != desc_sec) continue;
- if (r->offset != desc_base + 16u) continue;
- return r->sym;
- }
- return OBJ_SYM_NONE;
-}
-
-/* Returns 1 if the section is __DATA,__thread_vars (the descriptor section
- * on Mach-O). Compared by interned Sym id. */
-static int c_sec_name_is_macho_tvars(CTarget* t, const Section* sec) {
- if (!sec) return 0;
- Sym tvars =
- pool_intern_slice(t->c->global, SLICE_LIT("__DATA,__thread_vars"));
- return sec->name == tvars;
-}
-
-/* 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;
-}
-
-/* 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 (c_is_local_static_sym(t, id)) return;
- if (os->kind == SK_FUNC || os->kind == SK_IFUNC) return;
- if (os->kind == SK_SECTION || os->kind == SK_FILE) return;
- /* On Mach-O, obj_tls.c synthesizes `__tlv_bootstrap` as an SK_UNDEF
- * extern for the TLV descriptor's first field. The C target delegates all
- * TLS lowering to the host compiler via `_Thread_local`, so this
- * descriptor-time-only symbol has no place in the emitted source. */
- if (os->kind == SK_UNDEF && t->c->target.obj == CFREE_OBJ_MACHO) {
- const ObjBuilder* ob = t->obj;
- if (id == obj_tlv_bootstrap_get(ob)) 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. */
- /* Extern: SK_UNDEF, or any other defined-kind sym that the producer
- * marked as having no defining section (the C frontend uses SK_OBJ +
- * section=NONE for `extern T x __attribute__((weak));`). */
- int is_extern = (os->kind == SK_UNDEF) ||
- (os->kind != SK_COMMON && os->section_id == OBJ_SEC_NONE);
- if (is_extern) {
- /* Always declare extern data syms in C source: the host cc tolerates
- * unused externs, and the ObjSym::referenced bit isn't reliably set on
- * syms the C target only addresses by writing the name into the source
- * (no relocation gets emitted against them).
- *
- * Weak externs need different attributes per object format: on Mach-O
- * the `weak` attribute requires a definition; the right spelling for an
- * undefined weak ref is `__attribute__((weak_import))`. On ELF/PE the
- * existing `weak` attribute works as expected. */
- if (os->bind == SB_WEAK) {
- if (t->c->target.obj == CFREE_OBJ_MACHO) {
- cbuf_puts(b, "__attribute__((weak_import)) ");
- } else {
- 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\"))) ");
- }
- 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 splits TLS across two object-file symbols (see obj_tls.c): the
- * user-visible sym is a 24-byte TLV descriptor in
- * __DATA,__thread_vars; the actual initial bytes live in a synthesized
- * `<name>$tlv$init` sym in __DATA,__thread_data (or __thread_bss). For
- * C source emission we don't need either of those — `_Thread_local`
- * delegates to the host C compiler, which builds its own descriptor.
- *
- * We use the descriptor sym as the carrier (its name is what user code
- * references) and pull the initial bytes/size/alignment from the init
- * sym, found via the R_ABS64 reloc at descriptor offset +16. The init
- * sym is skipped in its own iteration. */
- const Section* desc_sec = obj_section_get(t->obj, os->section_id);
- if (c_sec_name_is_macho_tvars(t, desc_sec)) {
- ObjSymId init_id =
- c_macho_tls_find_init(t, os->section_id, (u32)os->value);
- if (init_id == OBJ_SYM_NONE) {
- compiler_panic(t->c, (SrcLoc){0, 0, 0},
- "C target: Mach-O TLS descriptor missing init reloc");
- }
- const ObjSym* init_os = obj_symbol_get(t->obj, init_id);
- if (!init_os || init_os->section_id == OBJ_SEC_NONE) {
- compiler_panic(t->c, (SrcLoc){0, 0, 0},
- "C target: Mach-O TLS init sym not defined");
- }
- const Section* init_sec = obj_section_get(t->obj, init_os->section_id);
- u32 init_base = (u32)init_os->value;
- u32 init_size = (u32)init_os->size;
- /* TLS data with relocations would need the constructor-fixup path
- * (and we'd have to rewrite the reloc target's section/offset to
- * the descriptor's name in the emitted C). No test currently
- * exercises this; surface it as a clear panic-as-skip if we hit it. */
- if (c_sym_has_relocs(t, init_os->section_id, init_base, init_size)) {
- compiler_panic(t->c, (SrcLoc){0, 0, 0},
- "C target: Mach-O TLS with pointer init not yet "
- "supported");
- }
- if (os->bind == SB_LOCAL) cbuf_puts(b, "static ");
- cbuf_puts(b, "_Thread_local ");
- c_emit_link_attrs(b, os);
- cbuf_puts(b, "__attribute__((unused)) ");
- cbuf_puts(b, "_Alignas(");
- cbuf_put_u64(b, init_sec->align ? init_sec->align : 1);
- cbuf_puts(b, ") uint8_t ");
- cbuf_puts(b, nm);
- cbuf_puts(b, "[");
- cbuf_put_u64(b, init_size ? init_size : 1);
- cbuf_puts(b, "]");
- if (init_sec->kind == SEC_BSS || init_sec->sem == SSEM_NOBITS ||
- init_size == 0) {
- cbuf_puts(b, ";\n");
- } else {
- Heap* h = t->c->ctx->heap;
- u8* bytes = (u8*)h->alloc(h, init_size, 1);
- if (!bytes) {
- compiler_panic(t->c, (SrcLoc){0, 0, 0},
- "C target: oom on TLS init bytes");
- }
- c_read_section_bytes(init_sec, init_base, bytes, init_size);
- c_emit_data_bytes(b, bytes, init_size);
- h->free(h, bytes, init_size);
- cbuf_puts(b, ";\n");
- }
- return;
- }
- /* Not the descriptor: this is the synthesized `<name>$tlv$init` data
- * sym (or a __thread_ptrs entry). The descriptor case above already
- * emitted the user-facing _Thread_local; nothing more to do. */
- return;
- }
- 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, "__attribute__((unused)) _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;
- u32 nrelocs = 0;
- u32 total_relocs = obj_reloc_total(t->obj);
- for (u32 i = 0; i < total_relocs; ++i) {
- const Reloc* r = obj_reloc_at(t->obj, i);
- if (r->section_id == os->section_id && r->offset >= base &&
- r->offset < base + size) {
- nrelocs++;
- }
- }
-
- Heap* h = t->c->ctx->heap;
- const Reloc** rs = NULL;
- if (nrelocs) {
- rs = (const Reloc**)h->alloc(h, nrelocs * sizeof(const Reloc*), 1);
- u32 j = 0;
- for (u32 i = 0; i < total_relocs; ++i) {
- const Reloc* r = obj_reloc_at(t->obj, i);
- if (r->section_id == os->section_id && r->offset >= base &&
- r->offset < base + size) {
- rs[j++] = r;
- }
- }
- for (u32 i = 1; i < nrelocs; ++i) {
- const Reloc* tmp = rs[i];
- u32 k = i;
- while (k > 0 && rs[k - 1]->offset > tmp->offset) {
- rs[k] = rs[k - 1];
- k--;
- }
- rs[k] = tmp;
- }
- }
-
- cbuf_puts(b, "struct ");
- if (nrelocs > 0) cbuf_puts(b, "__attribute__((packed)) ");
- cbuf_puts(b, "__cfree_data_");
- cbuf_puts(b, nm);
- cbuf_puts(b, " {\n");
-
- if (nrelocs == 0) {
- cbuf_puts(b, " uint8_t raw[");
- cbuf_put_u64(b, size ? size : 1);
- cbuf_puts(b, "];\n");
- } else {
- u32 cur = base;
- for (u32 i = 0; i < nrelocs; ++i) {
- const Reloc* r = rs[i];
- if (r->offset > cur) {
- cbuf_puts(b, " uint8_t chunk_");
- cbuf_put_u64(b, i);
- cbuf_puts(b, "[");
- cbuf_put_u64(b, r->offset - cur);
- cbuf_puts(b, "];\n");
- }
- u32 width = (r->kind == R_ABS32) ? 4 : 8;
- const char* ty = (width == 4) ? "uint32_t" : "void*";
- cbuf_puts(b, " ");
- cbuf_puts(b, ty);
- cbuf_puts(b, " ptr_");
- cbuf_put_u64(b, i);
- cbuf_puts(b, ";\n");
- cur = r->offset + width;
- }
- if (cur < base + size) {
- cbuf_puts(b, " uint8_t chunk_");
- cbuf_put_u64(b, nrelocs);
- cbuf_puts(b, "[");
- cbuf_put_u64(b, base + size - cur);
- cbuf_puts(b, "];\n");
- }
- }
- cbuf_puts(b, "};\n");
-
- if (os->bind == SB_LOCAL) cbuf_puts(b, "static ");
- if (is_tls) cbuf_puts(b, "_Thread_local ");
- c_emit_link_attrs(b, os);
- cbuf_puts(b, "__attribute__((unused)) ");
-
- int is_ro = (sec->kind == SEC_RODATA);
- if (is_ro) cbuf_puts(b, "const ");
-
- cbuf_puts(b, "_Alignas(");
- cbuf_put_u64(b, sec->align ? sec->align : 1);
- cbuf_puts(b, ") struct __cfree_data_");
- cbuf_puts(b, nm);
- cbuf_puts(b, " ");
- cbuf_puts(b, nm);
-
- if (sec->kind == SEC_BSS || sec->sem == SSEM_NOBITS) {
- cbuf_puts(b, ";\n");
- } else if (size == 0) {
- cbuf_puts(b, " = {{0}};\n");
- } else {
- cbuf_puts(b, " = {\n");
- u8* bytes = (u8*)h->alloc(h, size, 1);
- c_read_section_bytes(sec, base, bytes, size);
-
- if (nrelocs == 0) {
- cbuf_puts(b, " .raw = {");
- for (u32 i = 0; i < size; ++i) {
- if (i > 0) cbuf_puts(b, ", ");
- cbuf_put_u64(b, bytes[i]);
- }
- cbuf_puts(b, "}\n");
- } else {
- u32 cur = base;
- for (u32 i = 0; i < nrelocs; ++i) {
- const Reloc* r = rs[i];
- if (r->offset > cur) {
- cbuf_puts(b, " .chunk_");
- cbuf_put_u64(b, i);
- cbuf_puts(b, " = {");
- for (u32 k = 0; k < r->offset - cur; ++k) {
- if (k > 0) cbuf_puts(b, ", ");
- cbuf_put_u64(b, bytes[cur - base + k]);
- }
- cbuf_puts(b, "},\n");
- }
-
- u32 width = (r->kind == R_ABS32) ? 4 : 8;
- c_ensure_forward_decl(t, r->sym, 0);
- const char* tgt = c_sym_name(t, r->sym);
- const char* cast = (width == 4) ? "(uint32_t)(uintptr_t)" : "(void*)";
-
- cbuf_puts(b, " .ptr_");
- cbuf_put_u64(b, i);
- cbuf_puts(b, " = ");
- cbuf_puts(b, cast);
- cbuf_puts(b, "((char*)&");
- cbuf_puts(b, tgt);
- if (r->addend != 0) {
- cbuf_puts(b, " + ");
- cbuf_put_i64(b, r->addend);
- }
- cbuf_puts(b, "),\n");
- cur = r->offset + width;
- }
- if (cur < base + size) {
- cbuf_puts(b, " .chunk_");
- cbuf_put_u64(b, nrelocs);
- cbuf_puts(b, " = {");
- for (u32 k = 0; k < base + size - cur; ++k) {
- if (k > 0) cbuf_puts(b, ", ");
- cbuf_put_u64(b, bytes[cur - base + k]);
- }
- cbuf_puts(b, "}\n");
- }
- }
- h->free(h, bytes, size);
- cbuf_puts(b, "};\n");
- }
-
- if (nrelocs) h->free(h, (void*)rs, nrelocs * sizeof(const Reloc*));
-}
-
-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) {
- CTarget* t = (CTarget*)T;
- if (t->finalized) return;
- t->finalized = 1;
- c_emit_prologue(t);
- if (t->need_stdarg) c_writer_puts(t, "#include <stdarg.h>\n");
- if (t->need_setjmp) c_writer_puts(t, "#include <setjmp.h>\n");
- if (t->need_stdarg || t->need_setjmp) c_writer_puts(t, "\n");
- if (t->typedefs.len) {
- c_writer_write(t, t->typedefs.data, t->typedefs.len);
- c_writer_puts(t, "\n");
- }
- c_emit_data(t);
- if (t->forwards.len) {
- c_writer_write(t, t->forwards.data, t->forwards.len);
- c_writer_puts(t, "\n");
- }
- 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);
-}
-
-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);
- t->sym_forwarded = NULL;
- t->sym_forwarded_cap = 0;
- if (t->local_static_syms) {
- h->free(h, t->local_static_syms,
- t->local_static_syms_cap * sizeof(*t->local_static_syms));
- }
- if (t->local_static_entries) {
- h->free(h, t->local_static_entries,
- t->local_static_entries_cap * sizeof(*t->local_static_entries));
- }
- if (t->local_declared) h->free(h, t->local_declared, t->local_cap);
- if (t->local_type) h->free(h, t->local_type, t->local_cap * sizeof(CfreeCgTypeId));
- if (t->scopes) h->free(h, t->scopes, t->scopes_cap * sizeof(CScopeInfo));
- t->local_declared = NULL;
- t->local_type = NULL;
- t->scopes = NULL;
- t->local_static_syms = NULL;
- t->local_static_entries = NULL;
- t->local_cap = 0;
- t->scopes_cap = 0;
- t->local_static_syms_cap = 0;
- t->local_static_entries_cap = 0;
- t->local_static_nsyms = 0;
- t->local_static_nentries = 0;
-}
diff --git a/src/arch/c_target/internal.h b/src/arch/c_target/internal.h
@@ -1,193 +0,0 @@
-#ifndef CFREE_C_TARGET_INTERNAL_H
-#define CFREE_C_TARGET_INTERNAL_H
-
-/* C-source emission CgTarget. See doc/CBACKEND.md.
- *
- * This target is selected when CodeOptions.emit_c_source is set. It writes
- * target-locked C source to CodeOptions.c_source_writer instead of object
- * bytes. Operates with semantic temporary locals minted by CG. */
-
-#include <cfree/core.h>
-
-#include "cg/cgtarget.h"
-#include "core/core.h"
-
-typedef CGLocal CLocal;
-
-/* Heap-backed growable byte buffer. Used for the per-function declaration
- * and body buffers; CG needs decls at function top but doesn't surface them
- * before body emission, so we accumulate both and flush at func_end. */
-typedef struct CBuf {
- Heap* heap;
- u8* data;
- size_t len;
- size_t cap;
-} CBuf;
-
-void cbuf_init(CBuf* b, Heap* h);
-void cbuf_fini(CBuf* b);
-void cbuf_reset(CBuf* b);
-void cbuf_putc(CBuf* b, char c);
-void cbuf_puts(CBuf* b, const char* s);
-void cbuf_putn(CBuf* b, const char* s, size_t n);
-void cbuf_put_i64(CBuf* b, i64 v);
-void cbuf_put_u64(CBuf* b, u64 v);
-
-typedef struct CLocalStaticLabelEntry {
- Label label;
- i64 addend;
- u8 has_label;
- u8 pad[3];
-} CLocalStaticLabelEntry;
-
-typedef struct CTarget {
- CgTarget base;
-
- Compiler* c;
- ObjBuilder* obj;
- CfreeWriter* w;
-
- /* TU prologue (e.g. #include <stdint.h>) emitted once, on first
- * function or finalize, whichever first. */
- u8 prologue_emitted;
- u8 finalized;
- u8 pad[2];
-
- /* TU-wide forward declarations: one `RetT name(params);` line per function
- * we've seen a definition or call for. Emitted just after the prologue so
- * callers defined earlier in the TU still see the prototype, and calls to
- * undefined-in-TU externs get a declaration too. */
- CBuf forwards;
- /* Forward-decl dedup: which ObjSymIds have we already declared. Lazily
- * grown bitmap indexed by ObjSymId. */
- u8* sym_forwarded;
- u32 sym_forwarded_cap;
-
- /* 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;
- /* Tracks whether <stdarg.h> needs to be emitted in the prologue. Set when
- * c_typename first encounters VARARG_STATE. Flushed lazily at finalize. */
- u8 need_stdarg;
- u8 need_setjmp;
- u8 pad2[2];
-
- /* Per-function buffers. Reset on func_end. */
- CBuf decls;
- CBuf body;
-
- /* Function-local static data consumed from CG's narrow source-backend data
- * hook. These symbols are emitted inside the owning function and skipped by
- * the TU-wide object-data walker. */
- ObjSymId* local_static_syms;
- u32 local_static_nsyms;
- u32 local_static_syms_cap;
-
- CLocalStaticLabelEntry* local_static_entries;
- u32 local_static_nentries;
- u32 local_static_entries_cap;
- ObjSymId local_static_sym;
- CfreeCgTypeId local_static_type;
- u64 local_static_count;
- u64 local_static_offset;
- u32 local_static_ptr_width;
- u32 local_static_align;
- u8 local_static_active;
- u8 local_static_is_array;
- u8 local_static_readonly;
- u8 pad_local_static;
-
- /* Per-function local-decl tracking: for each local id seen, mark whether we
- * have already emitted a declaration into `decls`. Sized by local_cap.
- * Grown lazily as new local ids appear. */
- u8* local_declared;
- CfreeCgTypeId* local_type; /* declared type for each semantic local */
- u32 local_cap;
-
- /* Splice bookmark: byte offset into body where the current function's body
- * region starts (right after the open brace). func_end uses this to insert
- * the per-function declarations between the signature and the body. */
- size_t fn_body_start;
-
- const CGFuncDesc* cur_fn;
-
- /* Most recent source line directive emitted into the current function body.
- * Reset on func_begin; used by c_set_loc to avoid noisy duplicate #line
- * directives. */
- SrcLoc emitted_loc;
- u8 have_emitted_loc;
- u8 pad_cur_loc[3];
-
- /* Label minting: ids 1..next_label. 0 is reserved as LABEL_NONE. */
- u32 next_label;
- u32 next_local;
-
- /* Stack of active scopes. CGScope handles are (scope_index + 1). */
- struct CScopeInfo* scopes;
- u32 scopes_cap;
- u32 nscopes;
-
- /* Monotone counter for synthesizing unique temporary names within a
- * function (e.g. bitcast scratch). Reset on func_begin. */
- u32 next_tmp;
-
- /* Tracks whether the last emitted body statement was an unconditional
- * terminator (`return`, `goto`, etc.) with no intervening label. When
- * set, subsequent ret/jump/cmp_branch emissions are dropped — they're
- * unreachable, and emitting them produces dead C that distracts from
- * the live code. Reset on func_begin and label_place. */
- u8 last_was_terminator;
- u8 pad3[3];
-} CTarget;
-
-typedef struct CScopeInfo {
- u8 kind; /* ScopeKind */
- /* Set when the C target emitted a `for (;;) { ... }` wrapper around
- * this scope. Drives jump/cmp_branch's translation of break/continue
- * labels into C `break;`/`continue;` and scope_end's `}`. */
- u8 structured;
- u8 pad[2];
- Label break_label;
- Label continue_label;
-} CScopeInfo;
-
-CgTarget* c_cgtarget_new(Compiler* c, ObjBuilder* o, CfreeWriter* w);
-
-/* Helpers shared across emit.c. */
-void c_emit_prologue(CTarget* t);
-/* Ensure local `r` (typed `type`) has been declared. */
-void c_ensure_local(CTarget* t, CLocal r, CfreeCgTypeId type);
-/* Get a stable C identifier for local r. Writes into caller-supplied buf. */
-void c_local_name(CLocal r, char* out, size_t cap);
-/* Write the C type for a CG int/float/ptr type to `b`. */
-void c_emit_type(CTarget* t, CBuf* b, CfreeCgTypeId type);
-/* Write operand expression to body (e.g. "v3", "(int32_t)42"). Supports
- * OPK_LOCAL / OPK_IMM / OPK_GLOBAL. INDIRECT is only valid in
- * lvalue positions and is emitted via load/store/addr_of paths. */
-void c_emit_operand(CTarget* t, Operand op);
-/* Like c_emit_operand but wraps in an explicit signed/unsigned cast of the
- * operand's type width. For integer ops where signedness affects semantics
- * (UDIV/UREM, SHR_U, unsigned compares). Falls back to c_emit_operand when
- * the operand is not integer-typed. */
-void c_emit_operand_signed(CTarget* t, Operand op, int signed_);
-
-/* Lookup the C linker name for an ObjSymId. Returns interned string. */
-const char* c_sym_name(CTarget* t, ObjSymId sym);
-
-/* Emit a forward declaration for `sym` (of function type `fn_type`) into
- * the TU forwards buffer if not already done. Idempotent per sym. */
-void c_ensure_forward_decl(CTarget* t, ObjSymId sym, CfreeCgTypeId fn_type);
-
-/* Write `n` bytes to t->w; panic on error. */
-void c_writer_write(CTarget* t, const void* data, size_t n);
-void c_writer_puts(CTarget* t, const char* s);
-
-#endif
diff --git a/src/arch/c_target/ir_emit.c b/src/arch/c_target/ir_emit.c
@@ -0,0 +1,281 @@
+#include "arch/c_target/ir_emit.h"
+
+#include <string.h>
+
+typedef struct CIrEmitter {
+ CTarget* target;
+ CGScope* scope_map;
+ u32 scope_map_n;
+} CIrEmitter;
+
+static CGScope ir_scope_lookup(CIrEmitter* e, CGScope recorded, SrcLoc loc) {
+ if ((u32)recorded >= e->scope_map_n || !e->scope_map[recorded]) {
+ compiler_panic(e->target->c, loc,
+ "C target IR emit: unknown recorded scope %u",
+ (unsigned)recorded);
+ }
+ return e->scope_map[recorded];
+}
+
+static void ir_scope_bind(CIrEmitter* e, CGScope recorded, CGScope emitted,
+ SrcLoc loc) {
+ if ((u32)recorded >= e->scope_map_n) {
+ compiler_panic(e->target->c, loc,
+ "C target IR emit: recorded scope %u out of range",
+ (unsigned)recorded);
+ }
+ e->scope_map[recorded] = emitted;
+}
+
+static void ir_emit_param_bind(CIrEmitter* e, const CgIrParam* p) {
+ c_emit_param_bind(e->target, p->local, p->desc.type, p->desc.index);
+}
+
+static void ir_declare_locals(CIrEmitter* e, const CgIrFunc* f) {
+ for (u32 i = 0; i < f->nlocals; ++i) {
+ const CgIrLocal* l = &f->locals[i];
+ c_ensure_local(e->target, l->id, l->desc.type);
+ }
+ for (u32 i = 0; i < f->nparams; ++i) ir_emit_param_bind(e, &f->params[i]);
+}
+
+static void ir_emit_switch(CIrEmitter* e, const CgIrInst* in) {
+ const CgIrSwitchAux* aux = (const CgIrSwitchAux*)in->extra.aux;
+ CGSwitchDesc d;
+ memset(&d, 0, sizeof d);
+ d.selector = in->opnds[0];
+ d.selector_type = aux->selector_type;
+ d.default_label = aux->default_label;
+ d.cases = aux->cases;
+ d.ncases = aux->ncases;
+ d.hint = aux->hint;
+ d.opt_level = aux->opt_level;
+ c_emit_switch_(e->target, &d);
+}
+
+static void ir_emit_inst(CIrEmitter* e, const CgIrInst* in) {
+ CTarget* t = e->target;
+ c_emit_set_loc(t, in->loc);
+ switch ((CgIrOp)in->op) {
+ case CG_IR_NOP:
+ return;
+ case CG_IR_LABEL:
+ c_emit_label_place(t, (Label)in->extra.imm);
+ return;
+ case CG_IR_LOAD_IMM:
+ c_emit_load_imm(t, in->opnds[0], in->extra.imm);
+ return;
+ case CG_IR_LOAD_CONST:
+ c_emit_load_const(t, in->opnds[0], in->extra.cbytes);
+ return;
+ case CG_IR_COPY:
+ c_emit_copy(t, in->opnds[0], in->opnds[1]);
+ return;
+ case CG_IR_LOAD:
+ c_emit_load(t, in->opnds[0], in->opnds[1], in->extra.mem);
+ return;
+ case CG_IR_STORE:
+ c_emit_store(t, in->opnds[0], in->opnds[1], in->extra.mem);
+ return;
+ case CG_IR_ADDR_OF:
+ c_emit_addr_of(t, in->opnds[0], in->opnds[1]);
+ return;
+ case CG_IR_TLS_ADDR_OF: {
+ const CgIrTlsAux* aux = (const CgIrTlsAux*)in->extra.aux;
+ c_emit_tls_addr_of(t, in->opnds[0], aux->sym, aux->addend);
+ return;
+ }
+ case CG_IR_AGG_COPY: {
+ const CgIrAggAux* aux = (const CgIrAggAux*)in->extra.aux;
+ c_emit_copy_bytes(t, in->opnds[0], in->opnds[1], aux->access);
+ return;
+ }
+ case CG_IR_AGG_SET: {
+ const CgIrAggAux* aux = (const CgIrAggAux*)in->extra.aux;
+ c_emit_set_bytes(t, in->opnds[0], in->opnds[1], aux->access);
+ return;
+ }
+ case CG_IR_BITFIELD_LOAD: {
+ const CgIrBitFieldAux* aux = (const CgIrBitFieldAux*)in->extra.aux;
+ c_emit_bitfield_load(t, in->opnds[0], in->opnds[1], aux->access);
+ return;
+ }
+ case CG_IR_BITFIELD_STORE: {
+ const CgIrBitFieldAux* aux = (const CgIrBitFieldAux*)in->extra.aux;
+ c_emit_bitfield_store(t, in->opnds[0], in->opnds[1], aux->access);
+ return;
+ }
+ case CG_IR_BINOP:
+ c_emit_binop(t, (BinOp)in->extra.imm, in->opnds[0], in->opnds[1],
+ in->opnds[2]);
+ return;
+ case CG_IR_UNOP:
+ c_emit_unop(t, (UnOp)in->extra.imm, in->opnds[0], in->opnds[1]);
+ return;
+ case CG_IR_CMP:
+ c_emit_cmp(t, (CmpOp)in->extra.imm, in->opnds[0], in->opnds[1],
+ in->opnds[2]);
+ return;
+ case CG_IR_CONVERT:
+ c_emit_convert(t, (ConvKind)in->extra.imm, in->opnds[0], in->opnds[1]);
+ return;
+ case CG_IR_CALL: {
+ const CgIrCallAux* aux = (const CgIrCallAux*)in->extra.aux;
+ c_emit_call(t, &aux->desc);
+ return;
+ }
+ case CG_IR_RET: {
+ const CgIrRetAux* aux = (const CgIrRetAux*)in->extra.aux;
+ c_emit_ret(t, aux->values, aux->nvalues);
+ return;
+ }
+ case CG_IR_BR:
+ c_emit_jump(t, (Label)in->extra.imm);
+ return;
+ case CG_IR_CMP_BRANCH: {
+ const CgIrCmpBranchAux* aux = (const CgIrCmpBranchAux*)in->extra.aux;
+ c_emit_cmp_branch(t, aux->op, in->opnds[0], in->opnds[1], aux->target);
+ return;
+ }
+ case CG_IR_SWITCH:
+ ir_emit_switch(e, in);
+ return;
+ case CG_IR_INDIRECT_BRANCH: {
+ const CgIrIndirectAux* aux = (const CgIrIndirectAux*)in->extra.aux;
+ c_emit_indirect_branch(t, in->opnds[0], aux->targets, aux->ntargets);
+ return;
+ }
+ case CG_IR_LOAD_LABEL_ADDR:
+ c_emit_load_label_addr(t, in->opnds[0], (Label)in->extra.imm);
+ return;
+ case CG_IR_LOCAL_STATIC_DATA_BEGIN: {
+ const CgIrLocalStaticBeginAux* aux =
+ (const CgIrLocalStaticBeginAux*)in->extra.aux;
+ (void)c_emit_local_static_data_begin(t, &aux->desc);
+ return;
+ }
+ case CG_IR_LOCAL_STATIC_DATA_WRITE: {
+ const CgIrLocalStaticWriteAux* aux =
+ (const CgIrLocalStaticWriteAux*)in->extra.aux;
+ c_emit_local_static_data_write(t, aux->has_data ? aux->data : NULL,
+ aux->len);
+ return;
+ }
+ case CG_IR_LOCAL_STATIC_DATA_LABEL_ADDR: {
+ const CgIrLocalStaticLabelAux* aux =
+ (const CgIrLocalStaticLabelAux*)in->extra.aux;
+ c_emit_local_static_data_label_addr(t, aux->target, aux->addend,
+ aux->width, aux->address_space);
+ return;
+ }
+ case CG_IR_LOCAL_STATIC_DATA_END:
+ c_emit_local_static_data_end(t);
+ return;
+ case CG_IR_SCOPE_BEGIN: {
+ const CgIrScopeAux* aux = (const CgIrScopeAux*)in->extra.aux;
+ CGScope emitted = c_emit_scope_begin(t, &aux->desc);
+ ir_scope_bind(e, aux->scope, emitted, in->loc);
+ return;
+ }
+ case CG_IR_SCOPE_ELSE:
+ c_emit_scope_else(t, ir_scope_lookup(e, (CGScope)in->extra.imm, in->loc));
+ return;
+ case CG_IR_SCOPE_END:
+ c_emit_scope_end(t, ir_scope_lookup(e, (CGScope)in->extra.imm, in->loc));
+ return;
+ case CG_IR_BREAK_TO:
+ c_emit_break_to(t, ir_scope_lookup(e, (CGScope)in->extra.imm, in->loc));
+ return;
+ case CG_IR_CONTINUE_TO:
+ c_emit_continue_to(t,
+ ir_scope_lookup(e, (CGScope)in->extra.imm, in->loc));
+ return;
+ case CG_IR_ALLOCA:
+ c_emit_alloca(t, in->opnds[0], in->opnds[1], (u32)in->extra.imm);
+ return;
+ case CG_IR_VA_START:
+ c_emit_va_start(t, in->opnds[0]);
+ return;
+ case CG_IR_VA_ARG:
+ c_emit_va_arg(t, in->opnds[0], in->opnds[1],
+ (CfreeCgTypeId)in->extra.imm);
+ return;
+ case CG_IR_VA_END:
+ c_emit_va_end(t, in->opnds[0]);
+ return;
+ case CG_IR_VA_COPY:
+ c_emit_va_copy(t, in->opnds[0], in->opnds[1]);
+ return;
+ case CG_IR_ATOMIC_LOAD: {
+ const CgIrAtomicAux* aux = (const CgIrAtomicAux*)in->extra.aux;
+ c_emit_atomic_load(t, in->opnds[0], in->opnds[1], aux->mem, aux->order);
+ return;
+ }
+ case CG_IR_ATOMIC_STORE: {
+ const CgIrAtomicAux* aux = (const CgIrAtomicAux*)in->extra.aux;
+ c_emit_atomic_store(t, in->opnds[0], in->opnds[1], aux->mem, aux->order);
+ return;
+ }
+ case CG_IR_ATOMIC_RMW: {
+ const CgIrAtomicAux* aux = (const CgIrAtomicAux*)in->extra.aux;
+ c_emit_atomic_rmw(t, aux->op, in->opnds[0], in->opnds[1], in->opnds[2],
+ aux->mem, aux->order);
+ return;
+ }
+ case CG_IR_ATOMIC_CAS: {
+ const CgIrAtomicAux* aux = (const CgIrAtomicAux*)in->extra.aux;
+ c_emit_atomic_cas(t, in->opnds[0], in->opnds[1], in->opnds[2],
+ in->opnds[3], in->opnds[4], aux->mem, aux->order,
+ aux->failure);
+ return;
+ }
+ case CG_IR_FENCE:
+ c_emit_fence(t, (MemOrder)in->extra.imm);
+ return;
+ case CG_IR_INTRINSIC: {
+ const CgIrIntrinsicAux* aux = (const CgIrIntrinsicAux*)in->extra.aux;
+ c_emit_intrinsic(t, aux->kind, aux->dsts, aux->ndst, aux->args,
+ aux->narg);
+ return;
+ }
+ case CG_IR_ASM_BLOCK: {
+ const CgIrAsmAux* aux = (const CgIrAsmAux*)in->extra.aux;
+ c_emit_asm_block(t, aux->tmpl, aux->outs, aux->nout, aux->out_ops,
+ aux->ins, aux->nin, aux->in_ops, aux->clobbers,
+ aux->nclob);
+ return;
+ }
+ }
+ compiler_panic(t->c, in->loc, "C target IR emit: unknown op %u",
+ (unsigned)in->op);
+}
+
+static void ir_emit_func(CTarget* t, const CgIrFunc* f) {
+ CIrEmitter e;
+ Heap* h = t->c->ctx->heap;
+ memset(&e, 0, sizeof e);
+ e.target = t;
+ e.scope_map_n = f->nscopes + 1u;
+ if (e.scope_map_n) {
+ e.scope_map = (CGScope*)h->alloc(h, sizeof(CGScope) * e.scope_map_n,
+ _Alignof(CGScope));
+ if (!e.scope_map) {
+ compiler_panic(t->c, f->desc.loc, "C target IR emit: out of memory");
+ }
+ memset(e.scope_map, 0, sizeof(CGScope) * e.scope_map_n);
+ }
+
+ c_emit_func_begin(t, &f->desc);
+ ir_declare_locals(&e, f);
+ for (u32 i = 0; i < f->ninsts; ++i) ir_emit_inst(&e, &f->insts[i]);
+ c_emit_func_end(t);
+
+ if (e.scope_map) h->free(h, e.scope_map, sizeof(CGScope) * e.scope_map_n);
+}
+
+void c_emit_ir_module(CTarget* t, const CgIrModule* module) {
+ if (!t || !module) return;
+ for (u32 i = 0; i < module->nfuncs; ++i) {
+ ir_emit_func(t, module->funcs[i]);
+ }
+}
diff --git a/src/arch/c_target/ir_emit.h b/src/arch/c_target/ir_emit.h
@@ -0,0 +1,9 @@
+#ifndef CFREE_C_TARGET_IR_EMIT_H
+#define CFREE_C_TARGET_IR_EMIT_H
+
+#include "arch/c_target/c_emit.h"
+#include "cg/ir.h"
+
+void c_emit_ir_module(CTarget* t, const CgIrModule* module);
+
+#endif
diff --git a/src/arch/c_target/target.c b/src/arch/c_target/target.c
@@ -1,183 +1,54 @@
-/* C-source CgTarget construction and vtable wiring.
+/* C-source backend registration.
*
- * See doc/CBACKEND.md. The C target writes portable, target-locked C source
- * text to the CfreeWriter passed via CodeOptions.c_source_writer. CG operates
- * with semantic temporary locals. */
+ * The public backend path records CG into semantic IR, then emits C from that
+ * IR. The legacy CgTarget adapter lives in cg_emit.c only so it can be deleted
+ * whole once all callers use the IR path directly. */
#include <string.h>
-#include "arch/c_target/internal.h"
-#include "core/arena.h"
+#include "arch/c_target/c_emit.h"
+#include "arch/c_target/ir_emit.h"
+#include "cg/ir_recorder.h"
#include "core/core.h"
-#include "core/heap.h"
-/* Forward declarations for all CgTarget methods. Implementations either land
- * in emit.c (working) or are stubs that compiler_panic. */
-void c_func_begin(CgTarget*, const CGFuncDesc*);
-void c_func_end(CgTarget*);
-void c_alias(CgTarget*, ObjSymId, ObjSymId, CfreeCgTypeId);
-void c_ret(CgTarget*, const CGLocal*, u32);
-void c_load_imm(CgTarget*, Operand, i64);
-void c_copy(CgTarget*, Operand, Operand);
-void c_binop(CgTarget*, BinOp, Operand, Operand, Operand);
-void c_unop(CgTarget*, UnOp, Operand, Operand);
-void c_cmp(CgTarget*, CmpOp, Operand, Operand, Operand);
-void c_convert(CgTarget*, ConvKind, Operand, Operand);
-void c_call(CgTarget*, const CGCallDesc*);
-const char* c_tail_call_unrealizable_reason(CgTarget*, const CGCallDesc*);
-void c_load(CgTarget*, Operand, Operand, MemAccess);
-void c_store(CgTarget*, Operand, Operand, MemAccess);
-void c_addr_of(CgTarget*, Operand, Operand);
-CGLocal c_param(CgTarget*, const CGParamDesc*);
-CGLocal c_local(CgTarget*, const CGLocalDesc*);
-void c_local_addr(CgTarget*, Operand, const CGLocalDesc*, CGLocal);
-Label c_label_new(CgTarget*);
-void c_label_place(CgTarget*, Label);
-void c_jump(CgTarget*, Label);
-void c_cmp_branch(CgTarget*, CmpOp, Operand, Operand, Label);
-void c_switch_(CgTarget*, const CGSwitchDesc*);
-void c_indirect_branch(CgTarget*, Operand, const Label*, u32);
-void c_load_label_addr(CgTarget*, Operand, Label);
-int c_local_static_data_begin(CgTarget*, const CGLocalStaticDataDesc*);
-void c_local_static_data_write(CgTarget*, const u8*, u64);
-void c_local_static_data_label_addr(CgTarget*, Label, i64, u32, u32);
-void c_local_static_data_end(CgTarget*);
-CGScope c_scope_begin(CgTarget*, const CGScopeDesc*);
-void c_scope_else(CgTarget*, CGScope);
-void c_scope_end(CgTarget*, CGScope);
-void c_break_to(CgTarget*, CGScope);
-void c_continue_to(CgTarget*, CGScope);
-void c_set_loc(CgTarget*, SrcLoc);
-void c_finalize(CgTarget*);
-void c_destroy(CgTarget*);
-void c_intrinsic(CgTarget*, IntrinKind, Operand*, u32, const Operand*, u32);
-void c_alloca(CgTarget*, Operand, Operand, u32);
-void c_va_start(CgTarget*, Operand);
-void c_va_arg(CgTarget*, Operand, Operand, CfreeCgTypeId);
-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);
-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);
-
-static void cgt_cleanup(void* arg) { cgtarget_free((CgTarget*)arg); }
-
-CgTarget* c_cgtarget_new(Compiler* c, ObjBuilder* o, CfreeWriter* w) {
- CTarget* x = arena_new(c->tu, CTarget);
- memset(x, 0, sizeof *x);
-
- x->c = c;
- x->obj = o;
- 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);
-
- CgTarget* t = &x->base;
- t->c = c;
- t->obj = o;
-
- /* ---- function lifecycle ---- */
- t->func_begin = c_func_begin;
- t->func_end = c_func_end;
- t->alias = c_alias;
-
- /* ---- locals ---- */
- t->local = c_local;
- t->local_addr = c_local_addr;
- t->param = c_param;
-
- /* ---- labels and control flow ---- */
- t->label_new = c_label_new;
- t->label_place = c_label_place;
- t->jump = c_jump;
- t->cmp_branch = c_cmp_branch;
- t->switch_ = c_switch_;
- t->indirect_branch = c_indirect_branch;
- t->load_label_addr = c_load_label_addr;
- t->local_static_data_begin = c_local_static_data_begin;
- t->local_static_data_write = c_local_static_data_write;
- t->local_static_data_label_addr = c_local_static_data_label_addr;
- t->local_static_data_end = c_local_static_data_end;
- t->scope_begin = c_scope_begin;
- t->scope_else = c_scope_else;
- t->scope_end = c_scope_end;
- t->break_to = c_break_to;
- t->continue_to = c_continue_to;
-
- /* ---- data movement ---- */
- t->load_imm = c_load_imm;
- 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_tls_addr_of;
- t->copy_bytes = c_copy_bytes;
- t->set_bytes = c_set_bytes;
- t->bitfield_load = c_bitfield_load;
- t->bitfield_store = c_bitfield_store;
-
- /* ---- arithmetic, compare, convert ---- */
- t->binop = c_binop;
- t->unop = c_unop;
- t->cmp = c_cmp;
- t->convert = c_convert;
-
- /* ---- calls / return ---- */
- t->call = c_call;
- t->tail_call_unrealizable_reason = c_tail_call_unrealizable_reason;
- t->ret = c_ret;
-
- /* ---- alloca / varargs ---- */
- t->alloca_ = c_alloca;
- t->va_start_ = c_va_start;
- t->va_arg_ = c_va_arg;
- t->va_end_ = c_va_end;
- t->va_copy_ = c_va_copy;
-
- /* ---- atomics ---- */
- 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;
+static void c_ir_finalize(void* user, const CgIrModule* module) {
+ CTarget* t = (CTarget*)user;
+ c_emit_ir_module(t, module);
+ c_emit_finalize(t);
+}
- /* ---- intrinsics / asm ---- */
- t->intrinsic = c_intrinsic;
- t->asm_block = c_asm_block;
+static void c_ir_destroy(void* user) {
+ CTarget* t = (CTarget*)user;
+ if (t) c_emit_destroy(t);
+}
- t->set_loc = c_set_loc;
- t->finalize = c_finalize;
- t->destroy = c_destroy;
+static int c_ir_local_static_data_begin(void* user,
+ const CGLocalStaticDataDesc* desc) {
+ return c_emit_can_local_static_data((CTarget*)user, desc);
+}
- compiler_defer(c, cgt_cleanup, t);
- return t;
+static const char* c_ir_tail_call_unrealizable_reason(void* user,
+ const CGFuncDesc* caller,
+ const CGCallDesc* call) {
+ return c_emit_tail_call_unrealizable_reason_for((CTarget*)user, caller, call);
}
static CgTarget* c_target_backend_make(Compiler* c, ObjBuilder* o,
const CfreeCodeOptions* opts) {
- /* c_target ignores opt_level and debug_info entirely — see
- * doc/CBACKEND.md §"Sequencing with opt". It only needs the writer. */
if (!opts || !opts->c_source_writer) {
compiler_panic(c, (SrcLoc){0, 0, 0},
"c_target backend: emit_c_source requires c_source_writer");
}
- return c_cgtarget_new(c, o, opts->c_source_writer);
+
+ CTarget* ctarget = c_emit_target_new(c, o, opts->c_source_writer);
+ CgIrRecorderConfig cfg;
+ memset(&cfg, 0, sizeof cfg);
+ cfg.finalize = c_ir_finalize;
+ cfg.destroy = c_ir_destroy;
+ cfg.local_static_data_begin = c_ir_local_static_data_begin;
+ cfg.tail_call_unrealizable_reason = c_ir_tail_call_unrealizable_reason;
+ cfg.user = ctarget;
+ return cg_ir_recorder_new(c, o, &cfg);
}
const CGBackend cg_backend_c_target = {
diff --git a/src/cg/ir.c b/src/cg/ir.c
@@ -0,0 +1,272 @@
+#include "cg/ir.h"
+
+#include <string.h>
+
+static _Noreturn void ir_panic(Compiler* c, SrcLoc loc, const char* what) {
+ compiler_panic(c, loc, "cg ir: %s", what);
+}
+
+static void* ir_zalloc_or_panic(Compiler* c, Arena* a, size_t size,
+ size_t align) {
+ void* p = arena_zalloc(a, size, align);
+ if (!p) ir_panic(c, (SrcLoc){0, 0, 0}, "out of memory");
+ return p;
+}
+
+char* cg_ir_dup_cstr(Arena* a, const char* s) {
+ size_t n;
+ if (!s) return NULL;
+ n = strlen(s);
+ return arena_strdup(a, s, n);
+}
+
+static void module_grow(CgIrModule* m, u32 want) {
+ CgIrFunc** next;
+ u32 cap;
+ if (m->funcs_cap >= want) return;
+ cap = m->funcs_cap ? m->funcs_cap : 8u;
+ while (cap < want) cap *= 2u;
+ next = ir_zalloc_or_panic(m->c, m->arena, sizeof(*next) * cap,
+ _Alignof(CgIrFunc*));
+ if (m->funcs) memcpy(next, m->funcs, sizeof(*next) * m->nfuncs);
+ m->funcs = next;
+ m->funcs_cap = cap;
+}
+
+CgIrModule* cg_ir_module_new(Compiler* c) {
+ CgIrModule* m = arena_znew(c->tu, CgIrModule);
+ if (!m) return NULL;
+ m->arena = c->tu;
+ m->c = c;
+ return m;
+}
+
+void cg_ir_module_add_func(CgIrModule* m, CgIrFunc* f) {
+ if (!m || !f) return;
+ module_grow(m, m->nfuncs + 1u);
+ m->funcs[m->nfuncs++] = f;
+}
+
+static CGFuncDesc dup_func_desc(Arena* a, const CGFuncDesc* in) {
+ CGFuncDesc out = *in;
+ if (in->nresults) {
+ CfreeCgTypeId* r = arena_array(a, CfreeCgTypeId, in->nresults);
+ memcpy(r, in->result_types, sizeof(*r) * in->nresults);
+ out.result_types = r;
+ }
+ if (in->nparams) {
+ CGParamDesc* p = arena_array(a, CGParamDesc, in->nparams);
+ memcpy(p, in->params, sizeof(*p) * in->nparams);
+ out.params = p;
+ }
+ return out;
+}
+
+CgIrFunc* cg_ir_func_new(Compiler* c, const CGFuncDesc* desc) {
+ CgIrFunc* f = arena_znew(c->tu, CgIrFunc);
+ if (!f) return NULL;
+ f->arena = c->tu;
+ f->c = c;
+ f->desc = dup_func_desc(f->arena, desc);
+ f->next_inst_id = 1;
+ return f;
+}
+
+static void inst_grow(CgIrFunc* f, u32 want) {
+ CgIrInst* next;
+ u32 cap;
+ if (f->insts_cap >= want) return;
+ cap = f->insts_cap ? f->insts_cap : 32u;
+ while (cap < want) cap *= 2u;
+ next = ir_zalloc_or_panic(f->c, f->arena, sizeof(*next) * cap,
+ _Alignof(CgIrInst));
+ if (f->insts) memcpy(next, f->insts, sizeof(*next) * f->ninsts);
+ f->insts = next;
+ f->insts_cap = cap;
+}
+
+CgIrInst* cg_ir_emit(CgIrFunc* f, CgIrOp op, SrcLoc loc) {
+ CgIrInst* in;
+ inst_grow(f, f->ninsts + 1u);
+ in = &f->insts[f->ninsts++];
+ memset(in, 0, sizeof *in);
+ in->id = f->next_inst_id++;
+ in->op = (u16)op;
+ in->loc = loc;
+ return in;
+}
+
+static void local_grow(CgIrFunc* f, u32 want) {
+ CgIrLocal* next;
+ u32 cap;
+ if (f->locals_cap >= want) return;
+ cap = f->locals_cap ? f->locals_cap : 32u;
+ while (cap < want) cap *= 2u;
+ next = ir_zalloc_or_panic(f->c, f->arena, sizeof(*next) * cap,
+ _Alignof(CgIrLocal));
+ if (f->locals) memcpy(next, f->locals, sizeof(*next) * f->nlocals);
+ f->locals = next;
+ f->locals_cap = cap;
+}
+
+CGLocal cg_ir_func_add_local(CgIrFunc* f, const CGLocalDesc* desc, int is_param,
+ u32 param_index) {
+ CGLocal id;
+ CgIrLocal* l;
+ local_grow(f, f->nlocals + 1u);
+ id = f->nlocals + 1u;
+ l = &f->locals[f->nlocals++];
+ memset(l, 0, sizeof *l);
+ l->id = id;
+ l->desc = *desc;
+ l->is_param = is_param ? 1u : 0u;
+ l->param_index = param_index;
+ l->address_taken = (desc->flags & CG_LOCAL_ADDR_TAKEN) != 0;
+ return id;
+}
+
+void cg_ir_func_mark_local_address_taken(CgIrFunc* f, CGLocal id) {
+ CgIrLocal* l;
+ if (!f || id == CG_LOCAL_NONE || id > f->nlocals) return;
+ l = &f->locals[id - 1u];
+ l->address_taken = 1;
+ l->desc.flags |= CG_LOCAL_ADDR_TAKEN;
+}
+
+static void param_grow(CgIrFunc* f, u32 want) {
+ CgIrParam* next;
+ u32 cap;
+ if (f->params_cap >= want) return;
+ cap = f->params_cap ? f->params_cap : 8u;
+ while (cap < want) cap *= 2u;
+ next = ir_zalloc_or_panic(f->c, f->arena, sizeof(*next) * cap,
+ _Alignof(CgIrParam));
+ if (f->params) memcpy(next, f->params, sizeof(*next) * f->nparams);
+ f->params = next;
+ f->params_cap = cap;
+}
+
+void cg_ir_func_add_param(CgIrFunc* f, CGLocal local, const CGParamDesc* desc) {
+ CgIrParam* p;
+ param_grow(f, f->nparams + 1u);
+ p = &f->params[f->nparams++];
+ memset(p, 0, sizeof *p);
+ p->local = local;
+ p->desc = *desc;
+}
+
+static void label_grow(CgIrFunc* f, u32 want) {
+ CgIrLabel* next;
+ u32 cap;
+ if (f->labels_cap >= want) return;
+ cap = f->labels_cap ? f->labels_cap : 16u;
+ while (cap < want) cap *= 2u;
+ next = ir_zalloc_or_panic(f->c, f->arena, sizeof(*next) * cap,
+ _Alignof(CgIrLabel));
+ if (f->labels) memcpy(next, f->labels, sizeof(*next) * f->nlabels);
+ f->labels = next;
+ f->labels_cap = cap;
+}
+
+Label cg_ir_func_add_label(CgIrFunc* f) {
+ Label id;
+ CgIrLabel* l;
+ label_grow(f, f->nlabels + 1u);
+ id = f->nlabels + 1u;
+ l = &f->labels[f->nlabels++];
+ memset(l, 0, sizeof *l);
+ l->id = id;
+ return id;
+}
+
+void cg_ir_func_note_label_place(CgIrFunc* f, Label id, SrcLoc loc) {
+ CgIrLabel* l;
+ if (!f || id == LABEL_NONE || id > f->nlabels) return;
+ l = &f->labels[id - 1u];
+ if (!l->nplaces) l->first_place_loc = loc;
+ ++l->nplaces;
+}
+
+static void scope_grow(CgIrFunc* f, u32 want) {
+ CgIrScope* next;
+ u32 cap;
+ if (f->scopes_cap >= want) return;
+ cap = f->scopes_cap ? f->scopes_cap : 16u;
+ while (cap < want) cap *= 2u;
+ next = ir_zalloc_or_panic(f->c, f->arena, sizeof(*next) * cap,
+ _Alignof(CgIrScope));
+ if (f->scopes) memcpy(next, f->scopes, sizeof(*next) * f->nscopes);
+ f->scopes = next;
+ f->scopes_cap = cap;
+}
+
+CGScope cg_ir_func_add_scope(CgIrFunc* f, const CGScopeDesc* desc) {
+ CGScope id;
+ CgIrScope* s;
+ scope_grow(f, f->nscopes + 1u);
+ id = f->nscopes + 1u;
+ s = &f->scopes[f->nscopes++];
+ memset(s, 0, sizeof *s);
+ s->id = id;
+ s->desc = *desc;
+ return id;
+}
+
+Operand* cg_ir_dup_operands(Arena* a, const Operand* src, u32 n) {
+ Operand* out;
+ if (!n) return NULL;
+ out = arena_array(a, Operand, n);
+ memcpy(out, src, sizeof(*out) * n);
+ return out;
+}
+
+CGLocal* cg_ir_dup_locals(Arena* a, const CGLocal* src, u32 n) {
+ CGLocal* out;
+ if (!n) return NULL;
+ out = arena_array(a, CGLocal, n);
+ memcpy(out, src, sizeof(*out) * n);
+ return out;
+}
+
+Label* cg_ir_dup_labels(Arena* a, const Label* src, u32 n) {
+ Label* out;
+ if (!n) return NULL;
+ out = arena_array(a, Label, n);
+ memcpy(out, src, sizeof(*out) * n);
+ return out;
+}
+
+CGSwitchCase* cg_ir_dup_switch_cases(Arena* a, const CGSwitchCase* src, u32 n) {
+ CGSwitchCase* out;
+ if (!n) return NULL;
+ out = arena_array(a, CGSwitchCase, n);
+ memcpy(out, src, sizeof(*out) * n);
+ return out;
+}
+
+ConstBytes cg_ir_dup_const_bytes(Arena* a, ConstBytes in) {
+ ConstBytes out = in;
+ if (in.size) {
+ u8* bytes = arena_array(a, u8, in.size);
+ memcpy(bytes, in.bytes, in.size);
+ out.bytes = bytes;
+ }
+ return out;
+}
+
+CGCallDesc cg_ir_dup_call_desc(Arena* a, const CGCallDesc* in) {
+ CGCallDesc out = *in;
+ out.args = cg_ir_dup_locals(a, in->args, in->nargs);
+ out.results = cg_ir_dup_locals(a, in->results, in->nresults);
+ return out;
+}
+
+AsmConstraint* cg_ir_dup_asm_constraints(Arena* a, const AsmConstraint* src,
+ u32 n) {
+ AsmConstraint* out;
+ if (!n) return NULL;
+ out = arena_array(a, AsmConstraint, n);
+ memcpy(out, src, sizeof(*out) * n);
+ for (u32 i = 0; i < n; ++i) out[i].str = cg_ir_dup_cstr(a, src[i].str);
+ return out;
+}
diff --git a/src/cg/ir.h b/src/cg/ir.h
@@ -0,0 +1,255 @@
+#ifndef CFREE_CG_IR_H
+#define CFREE_CG_IR_H
+
+#include "cg/cgtarget.h"
+#include "core/arena.h"
+
+typedef enum CgIrOp {
+ CG_IR_NOP,
+ CG_IR_LABEL,
+
+ CG_IR_LOAD_IMM,
+ CG_IR_LOAD_CONST,
+ CG_IR_COPY,
+ CG_IR_LOAD,
+ CG_IR_STORE,
+ CG_IR_ADDR_OF,
+ CG_IR_TLS_ADDR_OF,
+ CG_IR_AGG_COPY,
+ CG_IR_AGG_SET,
+ CG_IR_BITFIELD_LOAD,
+ CG_IR_BITFIELD_STORE,
+
+ CG_IR_BINOP,
+ CG_IR_UNOP,
+ CG_IR_CMP,
+ CG_IR_CONVERT,
+
+ CG_IR_CALL,
+ CG_IR_RET,
+
+ CG_IR_BR,
+ CG_IR_CMP_BRANCH,
+ CG_IR_SWITCH,
+ CG_IR_INDIRECT_BRANCH,
+ CG_IR_LOAD_LABEL_ADDR,
+ CG_IR_LOCAL_STATIC_DATA_BEGIN,
+ CG_IR_LOCAL_STATIC_DATA_WRITE,
+ CG_IR_LOCAL_STATIC_DATA_LABEL_ADDR,
+ CG_IR_LOCAL_STATIC_DATA_END,
+ CG_IR_SCOPE_BEGIN,
+ CG_IR_SCOPE_ELSE,
+ CG_IR_SCOPE_END,
+ CG_IR_BREAK_TO,
+ CG_IR_CONTINUE_TO,
+
+ CG_IR_ALLOCA,
+ CG_IR_VA_START,
+ CG_IR_VA_ARG,
+ CG_IR_VA_END,
+ CG_IR_VA_COPY,
+
+ CG_IR_ATOMIC_LOAD,
+ CG_IR_ATOMIC_STORE,
+ CG_IR_ATOMIC_RMW,
+ CG_IR_ATOMIC_CAS,
+ CG_IR_FENCE,
+
+ CG_IR_INTRINSIC,
+ CG_IR_ASM_BLOCK,
+} CgIrOp;
+
+typedef struct CgIrLocal {
+ CGLocal id;
+ CGLocalDesc desc;
+ u32 param_index;
+ u8 is_param;
+ u8 address_taken;
+ u8 pad[2];
+} CgIrLocal;
+
+typedef struct CgIrParam {
+ CGLocal local;
+ CGParamDesc desc;
+} CgIrParam;
+
+typedef struct CgIrLabel {
+ Label id;
+ SrcLoc first_place_loc;
+ u32 nplaces;
+} CgIrLabel;
+
+typedef struct CgIrScope {
+ CGScope id;
+ CGScopeDesc desc;
+} CgIrScope;
+
+typedef struct CgIrTlsAux {
+ ObjSymId sym;
+ i64 addend;
+} CgIrTlsAux;
+
+typedef struct CgIrAggAux {
+ AggregateAccess access;
+} CgIrAggAux;
+
+typedef struct CgIrBitFieldAux {
+ BitFieldAccess access;
+} CgIrBitFieldAux;
+
+typedef struct CgIrCallAux {
+ CGCallDesc desc;
+} CgIrCallAux;
+
+typedef struct CgIrRetAux {
+ CGLocal* values;
+ u32 nvalues;
+} CgIrRetAux;
+
+typedef struct CgIrCmpBranchAux {
+ CmpOp op;
+ Label target;
+} CgIrCmpBranchAux;
+
+typedef struct CgIrSwitchAux {
+ CfreeCgTypeId selector_type;
+ Label default_label;
+ CGSwitchCase* cases;
+ u32 ncases;
+ u8 hint;
+ u8 opt_level;
+ u8 pad[2];
+} CgIrSwitchAux;
+
+typedef struct CgIrIndirectAux {
+ Label* targets;
+ u32 ntargets;
+} CgIrIndirectAux;
+
+typedef struct CgIrLocalStaticBeginAux {
+ CGLocalStaticDataDesc desc;
+} CgIrLocalStaticBeginAux;
+
+typedef struct CgIrLocalStaticWriteAux {
+ u8* data;
+ u64 len;
+ u8 has_data;
+ u8 pad[7];
+} CgIrLocalStaticWriteAux;
+
+typedef struct CgIrLocalStaticLabelAux {
+ Label target;
+ i64 addend;
+ u32 width;
+ u32 address_space;
+} CgIrLocalStaticLabelAux;
+
+typedef struct CgIrScopeAux {
+ CGScope scope;
+ CGScopeDesc desc;
+} CgIrScopeAux;
+
+typedef struct CgIrAtomicAux {
+ MemAccess mem;
+ MemOrder order;
+ AtomicOp op;
+ MemOrder failure;
+} CgIrAtomicAux;
+
+typedef struct CgIrAsmAux {
+ char* tmpl;
+ AsmConstraint* outs;
+ Operand* out_ops;
+ AsmConstraint* ins;
+ Operand* in_ops;
+ Sym* clobbers;
+ u32 nout;
+ u32 nin;
+ u32 nclob;
+} CgIrAsmAux;
+
+typedef struct CgIrIntrinsicAux {
+ IntrinKind kind;
+ Operand* dsts;
+ Operand* args;
+ u32 ndst;
+ u32 narg;
+} CgIrIntrinsicAux;
+
+typedef struct CgIrInst {
+ u32 id;
+ u16 op;
+ u16 flags;
+ SrcLoc loc;
+ u32 nopnds;
+ Operand* opnds;
+ union {
+ i64 imm;
+ ConstBytes cbytes;
+ MemAccess mem;
+ void* aux;
+ } extra;
+} CgIrInst;
+
+typedef struct CgIrFunc {
+ Arena* arena;
+ Compiler* c;
+ CGFuncDesc desc;
+
+ CgIrInst* insts;
+ u32 ninsts;
+ u32 insts_cap;
+
+ CgIrLocal* locals;
+ u32 nlocals;
+ u32 locals_cap;
+
+ CgIrParam* params;
+ u32 nparams;
+ u32 params_cap;
+
+ CgIrLabel* labels;
+ u32 nlabels;
+ u32 labels_cap;
+
+ CgIrScope* scopes;
+ u32 nscopes;
+ u32 scopes_cap;
+
+ u32 next_inst_id;
+ u8 complete;
+ u8 pad[3];
+} CgIrFunc;
+
+typedef struct CgIrModule {
+ Arena* arena;
+ Compiler* c;
+ CgIrFunc** funcs;
+ u32 nfuncs;
+ u32 funcs_cap;
+} CgIrModule;
+
+CgIrModule* cg_ir_module_new(Compiler*);
+CgIrFunc* cg_ir_func_new(Compiler*, const CGFuncDesc*);
+void cg_ir_module_add_func(CgIrModule*, CgIrFunc*);
+
+CGLocal cg_ir_func_add_local(CgIrFunc*, const CGLocalDesc*, int is_param,
+ u32 param_index);
+void cg_ir_func_mark_local_address_taken(CgIrFunc*, CGLocal);
+void cg_ir_func_add_param(CgIrFunc*, CGLocal, const CGParamDesc*);
+Label cg_ir_func_add_label(CgIrFunc*);
+void cg_ir_func_note_label_place(CgIrFunc*, Label, SrcLoc);
+CGScope cg_ir_func_add_scope(CgIrFunc*, const CGScopeDesc*);
+
+CgIrInst* cg_ir_emit(CgIrFunc*, CgIrOp, SrcLoc);
+
+Operand* cg_ir_dup_operands(Arena*, const Operand*, u32 n);
+CGLocal* cg_ir_dup_locals(Arena*, const CGLocal*, u32 n);
+Label* cg_ir_dup_labels(Arena*, const Label*, u32 n);
+CGSwitchCase* cg_ir_dup_switch_cases(Arena*, const CGSwitchCase*, u32 n);
+ConstBytes cg_ir_dup_const_bytes(Arena*, ConstBytes);
+CGCallDesc cg_ir_dup_call_desc(Arena*, const CGCallDesc*);
+AsmConstraint* cg_ir_dup_asm_constraints(Arena*, const AsmConstraint*, u32 n);
+char* cg_ir_dup_cstr(Arena*, const char*);
+
+#endif
diff --git a/src/cg/ir_recorder.c b/src/cg/ir_recorder.c
@@ -0,0 +1,645 @@
+#include "cg/ir_recorder.h"
+
+#include <string.h>
+
+struct CgIrRecorder {
+ CgTarget base;
+ u32 magic;
+ CgIrModule* module;
+ CgIrFunc* cur;
+ SrcLoc loc;
+ void (*func_recorded)(void*, CgIrFunc*);
+ void (*finalize_recorded)(void*, const CgIrModule*);
+ void (*destroy_user)(void*);
+ int (*local_static_data_begin)(void*, const CGLocalStaticDataDesc*);
+ const char* (*tail_call_unrealizable_reason)(void*, const CGFuncDesc*,
+ const CGCallDesc*);
+ void* user;
+};
+
+#define CG_IR_RECORDER_MAGIC 0x43524952u
+
+static CgIrRecorder* rec_of(CgTarget* t) { return (CgIrRecorder*)t; }
+
+static void rec_panic(CgIrRecorder* r, const char* what) {
+ compiler_panic(r->base.c, r->loc, "cg ir recorder: %s", what);
+}
+
+static CgIrFunc* require_func(CgIrRecorder* r) {
+ if (!r->cur) rec_panic(r, "operation outside function");
+ return r->cur;
+}
+
+static void* aux_alloc(CgIrRecorder* r, size_t size, size_t align) {
+ void* p = arena_zalloc(require_func(r)->arena, size, align);
+ if (!p) rec_panic(r, "out of memory");
+ return p;
+}
+
+#define AUX_NEW(r, T) ((T*)aux_alloc((r), sizeof(T), _Alignof(T)))
+
+static CgIrInst* emit(CgIrRecorder* r, CgIrOp op) {
+ return cg_ir_emit(require_func(r), op, r->loc);
+}
+
+static void set_ops(CgIrRecorder* r, CgIrInst* in, const Operand* ops, u32 n) {
+ in->opnds = cg_ir_dup_operands(require_func(r)->arena, ops, n);
+ in->nopnds = n;
+}
+
+static void rec_func_begin(CgTarget* t, const CGFuncDesc* desc) {
+ CgIrRecorder* r = rec_of(t);
+ if (r->cur) rec_panic(r, "nested function begin");
+ r->cur = cg_ir_func_new(t->c, desc);
+ if (!r->cur) rec_panic(r, "out of memory");
+ cg_ir_module_add_func(r->module, r->cur);
+ r->loc = desc ? desc->loc : (SrcLoc){0, 0, 0};
+}
+
+static void rec_func_end(CgTarget* t) {
+ CgIrRecorder* r = rec_of(t);
+ CgIrFunc* f = require_func(r);
+ f->complete = 1;
+ if (r->func_recorded) r->func_recorded(r->user, f);
+ r->cur = NULL;
+}
+
+static void rec_alias(CgTarget* t, ObjSymId alias_sym, ObjSymId target_sym,
+ CfreeCgTypeId type) {
+ (void)t;
+ (void)alias_sym;
+ (void)target_sym;
+ (void)type;
+}
+
+static CGLocal rec_local(CgTarget* t, const CGLocalDesc* desc) {
+ CgIrRecorder* r = rec_of(t);
+ return cg_ir_func_add_local(require_func(r), desc, 0, 0);
+}
+
+static void rec_local_addr(CgTarget* t, Operand dst, const CGLocalDesc* desc,
+ CGLocal local) {
+ CgIrRecorder* r = rec_of(t);
+ CgIrInst* in = emit(r, CG_IR_ADDR_OF);
+ Operand ops[2];
+ (void)desc;
+ cg_ir_func_mark_local_address_taken(require_func(r), local);
+ memset(ops, 0, sizeof ops);
+ ops[0] = dst;
+ ops[1].kind = OPK_LOCAL;
+ ops[1].type = desc ? desc->type : dst.type;
+ ops[1].v.local = local;
+ set_ops(r, in, ops, 2);
+}
+
+static CGLocal rec_param(CgTarget* t, const CGParamDesc* desc) {
+ CgIrRecorder* r = rec_of(t);
+ CgIrFunc* f = require_func(r);
+ CGLocalDesc ld;
+ CGLocal local;
+ memset(&ld, 0, sizeof ld);
+ ld.type = desc->type;
+ ld.name = desc->name;
+ ld.loc = desc->loc;
+ ld.size = desc->size;
+ ld.align = desc->align;
+ ld.flags = desc->flags;
+ local = cg_ir_func_add_local(f, &ld, 1, desc->index);
+ cg_ir_func_add_param(f, local, desc);
+ return local;
+}
+
+static Label rec_label_new(CgTarget* t) {
+ return cg_ir_func_add_label(require_func(rec_of(t)));
+}
+
+static void rec_label_place(CgTarget* t, Label label) {
+ CgIrRecorder* r = rec_of(t);
+ CgIrInst* in = emit(r, CG_IR_LABEL);
+ in->extra.imm = (i64)label;
+ cg_ir_func_note_label_place(require_func(r), label, r->loc);
+}
+
+static void rec_jump(CgTarget* t, Label label) {
+ CgIrInst* in = emit(rec_of(t), CG_IR_BR);
+ in->extra.imm = (i64)label;
+}
+
+static void rec_cmp_branch(CgTarget* t, CmpOp op, Operand a, Operand b,
+ Label label) {
+ CgIrRecorder* r = rec_of(t);
+ CgIrInst* in = emit(r, CG_IR_CMP_BRANCH);
+ CgIrCmpBranchAux* aux = AUX_NEW(r, CgIrCmpBranchAux);
+ Operand ops[2] = {a, b};
+ aux->op = op;
+ aux->target = label;
+ set_ops(r, in, ops, 2);
+ in->extra.aux = aux;
+}
+
+static void rec_switch(CgTarget* t, const CGSwitchDesc* desc) {
+ CgIrRecorder* r = rec_of(t);
+ CgIrInst* in = emit(r, CG_IR_SWITCH);
+ CgIrSwitchAux* aux = AUX_NEW(r, CgIrSwitchAux);
+ Operand op = desc->selector;
+ aux->selector_type = desc->selector_type;
+ aux->default_label = desc->default_label;
+ aux->cases =
+ cg_ir_dup_switch_cases(require_func(r)->arena, desc->cases, desc->ncases);
+ aux->ncases = desc->ncases;
+ aux->hint = desc->hint;
+ aux->opt_level = desc->opt_level;
+ set_ops(r, in, &op, 1);
+ in->extra.aux = aux;
+}
+
+static void rec_indirect_branch(CgTarget* t, Operand addr,
+ const Label* valid_targets, u32 ntargets) {
+ CgIrRecorder* r = rec_of(t);
+ CgIrInst* in = emit(r, CG_IR_INDIRECT_BRANCH);
+ CgIrIndirectAux* aux = AUX_NEW(r, CgIrIndirectAux);
+ aux->targets =
+ cg_ir_dup_labels(require_func(r)->arena, valid_targets, ntargets);
+ aux->ntargets = ntargets;
+ set_ops(r, in, &addr, 1);
+ in->extra.aux = aux;
+}
+
+static void rec_load_label_addr(CgTarget* t, Operand dst, Label label) {
+ CgIrInst* in = emit(rec_of(t), CG_IR_LOAD_LABEL_ADDR);
+ set_ops(rec_of(t), in, &dst, 1);
+ in->extra.imm = (i64)label;
+}
+
+static int rec_local_static_data_begin(CgTarget* t,
+ const CGLocalStaticDataDesc* desc) {
+ CgIrRecorder* r = rec_of(t);
+ if (r->local_static_data_begin &&
+ !r->local_static_data_begin(r->user, desc)) {
+ return 0;
+ }
+ CgIrInst* in = emit(r, CG_IR_LOCAL_STATIC_DATA_BEGIN);
+ CgIrLocalStaticBeginAux* aux = AUX_NEW(r, CgIrLocalStaticBeginAux);
+ aux->desc = *desc;
+ in->extra.aux = aux;
+ return 1;
+}
+
+static void rec_local_static_data_write(CgTarget* t, const u8* data, u64 len) {
+ CgIrRecorder* r = rec_of(t);
+ CgIrFunc* f = require_func(r);
+ CgIrInst* in = emit(r, CG_IR_LOCAL_STATIC_DATA_WRITE);
+ CgIrLocalStaticWriteAux* aux = AUX_NEW(r, CgIrLocalStaticWriteAux);
+ aux->len = len;
+ if (data && len) {
+ if (len > UINT32_MAX) rec_panic(r, "local static data chunk too large");
+ aux->data = arena_array(f->arena, u8, (u32)len);
+ memcpy(aux->data, data, (size_t)len);
+ aux->has_data = 1;
+ }
+ in->extra.aux = aux;
+}
+
+static void rec_local_static_data_label_addr(CgTarget* t, Label target,
+ i64 addend, u32 width,
+ u32 address_space) {
+ CgIrRecorder* r = rec_of(t);
+ CgIrInst* in = emit(r, CG_IR_LOCAL_STATIC_DATA_LABEL_ADDR);
+ CgIrLocalStaticLabelAux* aux = AUX_NEW(r, CgIrLocalStaticLabelAux);
+ aux->target = target;
+ aux->addend = addend;
+ aux->width = width;
+ aux->address_space = address_space;
+ in->extra.aux = aux;
+}
+
+static void rec_local_static_data_end(CgTarget* t) {
+ (void)emit(rec_of(t), CG_IR_LOCAL_STATIC_DATA_END);
+}
+
+static const char* rec_data_label_addr_unsupported_msg(CgTarget* t) {
+ (void)t;
+ return "IR recorder supports function-local label address data";
+}
+
+static CGScope rec_scope_begin(CgTarget* t, const CGScopeDesc* desc) {
+ CgIrRecorder* r = rec_of(t);
+ CgIrInst* in = emit(r, CG_IR_SCOPE_BEGIN);
+ CgIrScopeAux* aux = AUX_NEW(r, CgIrScopeAux);
+ CGScope scope = cg_ir_func_add_scope(require_func(r), desc);
+ aux->scope = scope;
+ aux->desc = *desc;
+ in->extra.aux = aux;
+ return scope;
+}
+
+static void rec_scope_id_op(CgTarget* t, CgIrOp op, CGScope scope) {
+ CgIrInst* in = emit(rec_of(t), op);
+ in->extra.imm = (i64)scope;
+}
+
+static void rec_scope_else(CgTarget* t, CGScope scope) {
+ rec_scope_id_op(t, CG_IR_SCOPE_ELSE, scope);
+}
+
+static void rec_scope_end(CgTarget* t, CGScope scope) {
+ rec_scope_id_op(t, CG_IR_SCOPE_END, scope);
+}
+
+static void rec_break_to(CgTarget* t, CGScope scope) {
+ rec_scope_id_op(t, CG_IR_BREAK_TO, scope);
+}
+
+static void rec_continue_to(CgTarget* t, CGScope scope) {
+ rec_scope_id_op(t, CG_IR_CONTINUE_TO, scope);
+}
+
+static void rec_load_imm(CgTarget* t, Operand dst, i64 imm) {
+ CgIrInst* in = emit(rec_of(t), CG_IR_LOAD_IMM);
+ set_ops(rec_of(t), in, &dst, 1);
+ in->extra.imm = imm;
+}
+
+static void rec_load_const(CgTarget* t, Operand dst, ConstBytes cbytes) {
+ CgIrRecorder* r = rec_of(t);
+ CgIrInst* in = emit(r, CG_IR_LOAD_CONST);
+ set_ops(r, in, &dst, 1);
+ in->extra.cbytes = cg_ir_dup_const_bytes(require_func(r)->arena, cbytes);
+}
+
+static void rec_copy(CgTarget* t, Operand dst, Operand src) {
+ CgIrInst* in = emit(rec_of(t), CG_IR_COPY);
+ Operand ops[2] = {dst, src};
+ set_ops(rec_of(t), in, ops, 2);
+}
+
+static void rec_load(CgTarget* t, Operand dst, Operand addr, MemAccess mem) {
+ CgIrInst* in = emit(rec_of(t), CG_IR_LOAD);
+ Operand ops[2] = {dst, addr};
+ set_ops(rec_of(t), in, ops, 2);
+ in->extra.mem = mem;
+}
+
+static void rec_store(CgTarget* t, Operand addr, Operand src, MemAccess mem) {
+ CgIrInst* in = emit(rec_of(t), CG_IR_STORE);
+ Operand ops[2] = {addr, src};
+ set_ops(rec_of(t), in, ops, 2);
+ in->extra.mem = mem;
+}
+
+static void rec_addr_of(CgTarget* t, Operand dst, Operand lv) {
+ CgIrInst* in = emit(rec_of(t), CG_IR_ADDR_OF);
+ Operand ops[2] = {dst, lv};
+ set_ops(rec_of(t), in, ops, 2);
+ if (lv.kind == OPK_LOCAL)
+ cg_ir_func_mark_local_address_taken(require_func(rec_of(t)), lv.v.local);
+}
+
+static void rec_tls_addr_of(CgTarget* t, Operand dst, ObjSymId sym,
+ i64 addend) {
+ CgIrRecorder* r = rec_of(t);
+ CgIrInst* in = emit(r, CG_IR_TLS_ADDR_OF);
+ CgIrTlsAux* aux = AUX_NEW(r, CgIrTlsAux);
+ aux->sym = sym;
+ aux->addend = addend;
+ set_ops(r, in, &dst, 1);
+ in->extra.aux = aux;
+}
+
+static void rec_copy_bytes(CgTarget* t, Operand dst_addr, Operand src_addr,
+ AggregateAccess access) {
+ CgIrRecorder* r = rec_of(t);
+ CgIrInst* in = emit(r, CG_IR_AGG_COPY);
+ CgIrAggAux* aux = AUX_NEW(r, CgIrAggAux);
+ Operand ops[2] = {dst_addr, src_addr};
+ aux->access = access;
+ set_ops(r, in, ops, 2);
+ in->extra.aux = aux;
+}
+
+static void rec_set_bytes(CgTarget* t, Operand dst_addr, Operand byte_value,
+ AggregateAccess access) {
+ CgIrRecorder* r = rec_of(t);
+ CgIrInst* in = emit(r, CG_IR_AGG_SET);
+ CgIrAggAux* aux = AUX_NEW(r, CgIrAggAux);
+ Operand ops[2] = {dst_addr, byte_value};
+ aux->access = access;
+ set_ops(r, in, ops, 2);
+ in->extra.aux = aux;
+}
+
+static void rec_bitfield_load(CgTarget* t, Operand dst, Operand record_addr,
+ BitFieldAccess access) {
+ CgIrRecorder* r = rec_of(t);
+ CgIrInst* in = emit(r, CG_IR_BITFIELD_LOAD);
+ CgIrBitFieldAux* aux = AUX_NEW(r, CgIrBitFieldAux);
+ Operand ops[2] = {dst, record_addr};
+ aux->access = access;
+ set_ops(r, in, ops, 2);
+ in->extra.aux = aux;
+}
+
+static void rec_bitfield_store(CgTarget* t, Operand record_addr, Operand src,
+ BitFieldAccess access) {
+ CgIrRecorder* r = rec_of(t);
+ CgIrInst* in = emit(r, CG_IR_BITFIELD_STORE);
+ CgIrBitFieldAux* aux = AUX_NEW(r, CgIrBitFieldAux);
+ Operand ops[2] = {record_addr, src};
+ aux->access = access;
+ set_ops(r, in, ops, 2);
+ in->extra.aux = aux;
+}
+
+static void rec_binop(CgTarget* t, BinOp op, Operand dst, Operand a,
+ Operand b) {
+ CgIrInst* in = emit(rec_of(t), CG_IR_BINOP);
+ Operand ops[3] = {dst, a, b};
+ set_ops(rec_of(t), in, ops, 3);
+ in->extra.imm = (i64)op;
+}
+
+static void rec_unop(CgTarget* t, UnOp op, Operand dst, Operand a) {
+ CgIrInst* in = emit(rec_of(t), CG_IR_UNOP);
+ Operand ops[2] = {dst, a};
+ set_ops(rec_of(t), in, ops, 2);
+ in->extra.imm = (i64)op;
+}
+
+static void rec_cmp(CgTarget* t, CmpOp op, Operand dst, Operand a, Operand b) {
+ CgIrInst* in = emit(rec_of(t), CG_IR_CMP);
+ Operand ops[3] = {dst, a, b};
+ set_ops(rec_of(t), in, ops, 3);
+ in->extra.imm = (i64)op;
+}
+
+static void rec_convert(CgTarget* t, ConvKind op, Operand dst, Operand src) {
+ CgIrInst* in = emit(rec_of(t), CG_IR_CONVERT);
+ Operand ops[2] = {dst, src};
+ set_ops(rec_of(t), in, ops, 2);
+ in->extra.imm = (i64)op;
+}
+
+static void rec_call(CgTarget* t, const CGCallDesc* desc) {
+ CgIrRecorder* r = rec_of(t);
+ CgIrInst* in = emit(r, CG_IR_CALL);
+ CgIrCallAux* aux = AUX_NEW(r, CgIrCallAux);
+ aux->desc = cg_ir_dup_call_desc(require_func(r)->arena, desc);
+ in->extra.aux = aux;
+}
+
+static const char* rec_tail_call_unrealizable_reason(CgTarget* t,
+ const CGCallDesc* desc) {
+ CgIrRecorder* r = rec_of(t);
+ if (r->tail_call_unrealizable_reason) {
+ CgIrFunc* f = require_func(r);
+ return r->tail_call_unrealizable_reason(r->user, &f->desc, desc);
+ }
+ return NULL;
+}
+
+static void rec_ret(CgTarget* t, const CGLocal* values, u32 nvalues) {
+ CgIrRecorder* r = rec_of(t);
+ CgIrInst* in = emit(r, CG_IR_RET);
+ CgIrRetAux* aux = AUX_NEW(r, CgIrRetAux);
+ aux->values = cg_ir_dup_locals(require_func(r)->arena, values, nvalues);
+ aux->nvalues = nvalues;
+ in->extra.aux = aux;
+}
+
+static void rec_alloca(CgTarget* t, Operand dst, Operand size, u32 align) {
+ CgIrInst* in = emit(rec_of(t), CG_IR_ALLOCA);
+ Operand ops[2] = {dst, size};
+ set_ops(rec_of(t), in, ops, 2);
+ in->extra.imm = (i64)align;
+}
+
+static void rec_va_start(CgTarget* t, Operand ap_addr) {
+ CgIrInst* in = emit(rec_of(t), CG_IR_VA_START);
+ set_ops(rec_of(t), in, &ap_addr, 1);
+}
+
+static void rec_va_arg(CgTarget* t, Operand dst, Operand ap_addr,
+ CfreeCgTypeId type) {
+ CgIrInst* in = emit(rec_of(t), CG_IR_VA_ARG);
+ Operand ops[2] = {dst, ap_addr};
+ set_ops(rec_of(t), in, ops, 2);
+ in->extra.imm = (i64)type;
+}
+
+static void rec_va_end(CgTarget* t, Operand ap_addr) {
+ CgIrInst* in = emit(rec_of(t), CG_IR_VA_END);
+ set_ops(rec_of(t), in, &ap_addr, 1);
+}
+
+static void rec_va_copy(CgTarget* t, Operand dst_ap_addr, Operand src_ap_addr) {
+ CgIrInst* in = emit(rec_of(t), CG_IR_VA_COPY);
+ Operand ops[2] = {dst_ap_addr, src_ap_addr};
+ set_ops(rec_of(t), in, ops, 2);
+}
+
+static void rec_atomic_load(CgTarget* t, Operand dst, Operand addr,
+ MemAccess mem, MemOrder order) {
+ CgIrRecorder* r = rec_of(t);
+ CgIrInst* in = emit(r, CG_IR_ATOMIC_LOAD);
+ CgIrAtomicAux* aux = AUX_NEW(r, CgIrAtomicAux);
+ Operand ops[2] = {dst, addr};
+ aux->mem = mem;
+ aux->order = order;
+ set_ops(r, in, ops, 2);
+ in->extra.aux = aux;
+}
+
+static void rec_atomic_store(CgTarget* t, Operand addr, Operand src,
+ MemAccess mem, MemOrder order) {
+ CgIrRecorder* r = rec_of(t);
+ CgIrInst* in = emit(r, CG_IR_ATOMIC_STORE);
+ CgIrAtomicAux* aux = AUX_NEW(r, CgIrAtomicAux);
+ Operand ops[2] = {addr, src};
+ aux->mem = mem;
+ aux->order = order;
+ set_ops(r, in, ops, 2);
+ in->extra.aux = aux;
+}
+
+static void rec_atomic_rmw(CgTarget* t, AtomicOp op, Operand dst, Operand addr,
+ Operand val, MemAccess mem, MemOrder order) {
+ CgIrRecorder* r = rec_of(t);
+ CgIrInst* in = emit(r, CG_IR_ATOMIC_RMW);
+ CgIrAtomicAux* aux = AUX_NEW(r, CgIrAtomicAux);
+ Operand ops[3] = {dst, addr, val};
+ aux->mem = mem;
+ aux->order = order;
+ aux->op = op;
+ set_ops(r, in, ops, 3);
+ in->extra.aux = aux;
+}
+
+static void rec_atomic_cas(CgTarget* t, Operand prior, Operand ok, Operand addr,
+ Operand expected, Operand desired, MemAccess mem,
+ MemOrder success, MemOrder failure) {
+ CgIrRecorder* r = rec_of(t);
+ CgIrInst* in = emit(r, CG_IR_ATOMIC_CAS);
+ CgIrAtomicAux* aux = AUX_NEW(r, CgIrAtomicAux);
+ Operand ops[5] = {prior, ok, addr, expected, desired};
+ aux->mem = mem;
+ aux->order = success;
+ aux->failure = failure;
+ set_ops(r, in, ops, 5);
+ in->extra.aux = aux;
+}
+
+static void rec_fence(CgTarget* t, MemOrder order) {
+ CgIrInst* in = emit(rec_of(t), CG_IR_FENCE);
+ in->extra.imm = (i64)order;
+}
+
+static void rec_intrinsic(CgTarget* t, IntrinKind kind, Operand* dsts, u32 ndst,
+ const Operand* args, u32 narg) {
+ CgIrRecorder* r = rec_of(t);
+ CgIrInst* in = emit(r, CG_IR_INTRINSIC);
+ CgIrIntrinsicAux* aux = AUX_NEW(r, CgIrIntrinsicAux);
+ aux->kind = kind;
+ aux->dsts = cg_ir_dup_operands(require_func(r)->arena, dsts, ndst);
+ aux->args = cg_ir_dup_operands(require_func(r)->arena, args, narg);
+ aux->ndst = ndst;
+ aux->narg = narg;
+ in->extra.aux = aux;
+}
+
+static void rec_asm_block(CgTarget* t, const char* tmpl,
+ const AsmConstraint* outs, u32 nout, Operand* out_ops,
+ const AsmConstraint* ins, u32 nin,
+ const Operand* in_ops, const Sym* clobbers,
+ u32 nclob) {
+ CgIrRecorder* r = rec_of(t);
+ CgIrFunc* f = require_func(r);
+ CgIrInst* in = emit(r, CG_IR_ASM_BLOCK);
+ CgIrAsmAux* aux = AUX_NEW(r, CgIrAsmAux);
+ aux->tmpl = cg_ir_dup_cstr(f->arena, tmpl);
+ aux->outs = cg_ir_dup_asm_constraints(f->arena, outs, nout);
+ aux->out_ops = cg_ir_dup_operands(f->arena, out_ops, nout);
+ aux->ins = cg_ir_dup_asm_constraints(f->arena, ins, nin);
+ aux->in_ops = cg_ir_dup_operands(f->arena, in_ops, nin);
+ if (nclob) {
+ aux->clobbers = arena_array(f->arena, Sym, nclob);
+ memcpy(aux->clobbers, clobbers, sizeof(*aux->clobbers) * nclob);
+ }
+ aux->nout = nout;
+ aux->nin = nin;
+ aux->nclob = nclob;
+ in->extra.aux = aux;
+}
+
+static void rec_file_scope_asm(CgTarget* t, const char* src, size_t len) {
+ (void)t;
+ (void)src;
+ (void)len;
+}
+
+static void rec_set_loc(CgTarget* t, SrcLoc loc) { rec_of(t)->loc = loc; }
+
+static void rec_finalize(CgTarget* t) {
+ CgIrRecorder* r = rec_of(t);
+ if (r->cur) rec_panic(r, "finalize with open function");
+ if (r->finalize_recorded) r->finalize_recorded(r->user, r->module);
+}
+
+static void rec_destroy(CgTarget* t) {
+ CgIrRecorder* r = rec_of(t);
+ if (r->destroy_user) r->destroy_user(r->user);
+}
+
+CgTarget* cg_ir_recorder_new(Compiler* c, ObjBuilder* obj,
+ const CgIrRecorderConfig* cfg) {
+ CgIrRecorder* r;
+ if (!c) return NULL;
+ r = arena_znew(c->tu, CgIrRecorder);
+ if (!r) return NULL;
+ r->base.c = c;
+ r->base.obj = obj;
+ r->magic = CG_IR_RECORDER_MAGIC;
+ r->module = cg_ir_module_new(c);
+ if (!r->module) return NULL;
+ if (cfg) {
+ r->func_recorded = cfg->func_recorded;
+ r->finalize_recorded = cfg->finalize;
+ r->destroy_user = cfg->destroy;
+ r->local_static_data_begin = cfg->local_static_data_begin;
+ r->tail_call_unrealizable_reason = cfg->tail_call_unrealizable_reason;
+ r->user = cfg->user;
+ }
+
+ r->base.func_begin = rec_func_begin;
+ r->base.func_end = rec_func_end;
+ r->base.alias = rec_alias;
+ r->base.local = rec_local;
+ r->base.local_addr = rec_local_addr;
+ r->base.param = rec_param;
+ r->base.label_new = rec_label_new;
+ r->base.label_place = rec_label_place;
+ r->base.jump = rec_jump;
+ r->base.cmp_branch = rec_cmp_branch;
+ r->base.switch_ = rec_switch;
+ r->base.indirect_branch = rec_indirect_branch;
+ r->base.load_label_addr = rec_load_label_addr;
+ r->base.local_static_data_begin = rec_local_static_data_begin;
+ r->base.local_static_data_write = rec_local_static_data_write;
+ r->base.local_static_data_label_addr = rec_local_static_data_label_addr;
+ r->base.local_static_data_end = rec_local_static_data_end;
+ r->base.data_label_addr_unsupported_msg = rec_data_label_addr_unsupported_msg;
+ r->base.scope_begin = rec_scope_begin;
+ r->base.scope_else = rec_scope_else;
+ r->base.scope_end = rec_scope_end;
+ r->base.break_to = rec_break_to;
+ r->base.continue_to = rec_continue_to;
+ r->base.load_imm = rec_load_imm;
+ r->base.load_const = rec_load_const;
+ r->base.copy = rec_copy;
+ r->base.load = rec_load;
+ r->base.store = rec_store;
+ r->base.addr_of = rec_addr_of;
+ r->base.tls_addr_of = rec_tls_addr_of;
+ r->base.copy_bytes = rec_copy_bytes;
+ r->base.set_bytes = rec_set_bytes;
+ r->base.bitfield_load = rec_bitfield_load;
+ r->base.bitfield_store = rec_bitfield_store;
+ r->base.binop = rec_binop;
+ r->base.unop = rec_unop;
+ r->base.cmp = rec_cmp;
+ r->base.convert = rec_convert;
+ r->base.call = rec_call;
+ r->base.tail_call_unrealizable_reason = rec_tail_call_unrealizable_reason;
+ r->base.ret = rec_ret;
+ r->base.alloca_ = rec_alloca;
+ r->base.va_start_ = rec_va_start;
+ r->base.va_arg_ = rec_va_arg;
+ r->base.va_end_ = rec_va_end;
+ r->base.va_copy_ = rec_va_copy;
+ r->base.atomic_load = rec_atomic_load;
+ r->base.atomic_store = rec_atomic_store;
+ r->base.atomic_rmw = rec_atomic_rmw;
+ r->base.atomic_cas = rec_atomic_cas;
+ r->base.fence = rec_fence;
+ r->base.intrinsic = rec_intrinsic;
+ r->base.asm_block = rec_asm_block;
+ r->base.file_scope_asm = rec_file_scope_asm;
+ r->base.set_loc = rec_set_loc;
+ r->base.finalize = rec_finalize;
+ r->base.destroy = rec_destroy;
+ return &r->base;
+}
+
+CgIrRecorder* cg_ir_recorder_from_target(CgTarget* t) {
+ CgIrRecorder* r = t ? rec_of(t) : NULL;
+ return r && r->magic == CG_IR_RECORDER_MAGIC ? r : NULL;
+}
+
+const CgIrModule* cg_ir_recorder_module(const CgTarget* t) {
+ const CgIrRecorder* r = (const CgIrRecorder*)t;
+ return r && r->magic == CG_IR_RECORDER_MAGIC ? r->module : NULL;
+}
+
+CgIrFunc* cg_ir_recorder_current_func(const CgTarget* t) {
+ const CgIrRecorder* r = (const CgIrRecorder*)t;
+ return r && r->magic == CG_IR_RECORDER_MAGIC ? r->cur : NULL;
+}
diff --git a/src/cg/ir_recorder.h b/src/cg/ir_recorder.h
@@ -0,0 +1,24 @@
+#ifndef CFREE_CG_IR_RECORDER_H
+#define CFREE_CG_IR_RECORDER_H
+
+#include "cg/ir.h"
+
+typedef struct CgIrRecorder CgIrRecorder;
+
+typedef struct CgIrRecorderConfig {
+ void (*func_recorded)(void* user, CgIrFunc* func);
+ void (*finalize)(void* user, const CgIrModule* module);
+ void (*destroy)(void* user);
+ int (*local_static_data_begin)(void* user, const CGLocalStaticDataDesc* desc);
+ const char* (*tail_call_unrealizable_reason)(void* user,
+ const CGFuncDesc* caller,
+ const CGCallDesc* call);
+ void* user;
+} CgIrRecorderConfig;
+
+CgTarget* cg_ir_recorder_new(Compiler*, ObjBuilder*, const CgIrRecorderConfig*);
+CgIrRecorder* cg_ir_recorder_from_target(CgTarget*);
+const CgIrModule* cg_ir_recorder_module(const CgTarget*);
+CgIrFunc* cg_ir_recorder_current_func(const CgTarget*);
+
+#endif
diff --git a/test/cg/ir_recorder_test.c b/test/cg/ir_recorder_test.c
@@ -0,0 +1,343 @@
+#include "cg/ir_recorder.h"
+
+#include <cfree/core.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "core/pool.h"
+
+static void* h_alloc(CfreeHeap* h, size_t n, size_t a) {
+ (void)h;
+ (void)a;
+ return n ? malloc(n) : NULL;
+}
+
+static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) {
+ (void)h;
+ (void)o;
+ (void)a;
+ return realloc(p, n);
+}
+
+static void h_free(CfreeHeap* h, void* p, size_t n) {
+ (void)h;
+ (void)n;
+ free(p);
+}
+
+static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL};
+static int g_fails;
+static int g_checks;
+
+static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc,
+ const char* fmt, va_list ap) {
+ static const char* names[] = {"note", "warning", "error", "fatal"};
+ (void)s;
+ (void)loc;
+ fprintf(stderr, "%s: ", names[k]);
+ vfprintf(stderr, fmt, ap);
+ fputc('\n', stderr);
+}
+
+static CfreeDiagSink g_diag = {diag_emit, NULL, 0, 0};
+
+#define EXPECT(cond, ...) \
+ do { \
+ ++g_checks; \
+ if (!(cond)) { \
+ ++g_fails; \
+ fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
+ fprintf(stderr, __VA_ARGS__); \
+ fputc('\n', stderr); \
+ } \
+ } while (0)
+
+typedef struct TestCtx {
+ CfreeContext ctx;
+ Compiler* c;
+ CfreeCgTypeId i32;
+ CfreeCgTypeId ptr;
+} TestCtx;
+
+static void tc_init(TestCtx* tc) {
+ CfreeTarget target;
+ CfreeCgBuiltinTypes b;
+ memset(tc, 0, sizeof *tc);
+ tc->ctx.heap = &g_heap;
+ tc->ctx.diag = &g_diag;
+ tc->ctx.now = -1;
+ memset(&target, 0, sizeof target);
+ target.arch = CFREE_ARCH_X86_64;
+ target.os = CFREE_OS_LINUX;
+ target.obj = CFREE_OBJ_ELF;
+ target.ptr_size = 8;
+ target.ptr_align = 8;
+ if (cfree_compiler_new(target, &tc->ctx, (CfreeCompiler**)&tc->c) !=
+ CFREE_OK ||
+ !tc->c) {
+ fprintf(stderr, "fatal: compiler allocation failed\n");
+ abort();
+ }
+ b = cfree_cg_builtin_types(tc->c);
+ tc->i32 = b.id[CFREE_CG_BUILTIN_I32];
+ tc->ptr = cfree_cg_type_ptr(tc->c, b.id[CFREE_CG_BUILTIN_VOID], 0);
+}
+
+static void tc_fini(TestCtx* tc) {
+ cfree_compiler_free(tc->c);
+ tc->c = NULL;
+}
+
+static Operand op_local(CGLocal local, CfreeCgTypeId type) {
+ Operand o;
+ memset(&o, 0, sizeof o);
+ o.kind = OPK_LOCAL;
+ o.type = type;
+ o.v.local = local;
+ return o;
+}
+
+static Operand op_imm(i64 value, CfreeCgTypeId type) {
+ Operand o;
+ memset(&o, 0, sizeof o);
+ o.kind = OPK_IMM;
+ o.type = type;
+ o.v.imm = value;
+ return o;
+}
+
+static Operand op_global(ObjSymId sym, CfreeCgTypeId type) {
+ Operand o;
+ memset(&o, 0, sizeof o);
+ o.kind = OPK_GLOBAL;
+ o.type = type;
+ o.v.global.sym = sym;
+ return o;
+}
+
+static CGLocal local_new(CgTarget* t, CfreeCgTypeId type, const char* name) {
+ CGLocalDesc d;
+ memset(&d, 0, sizeof d);
+ d.type = type;
+ d.name = name ? pool_intern_slice(t->c->global, cfree_slice_cstr(name)) : 0;
+ d.size = 4;
+ d.align = 4;
+ return t->local(t, &d);
+}
+
+static CGFuncDesc fn_desc(TestCtx* tc) {
+ CGFuncDesc fd;
+ CfreeCgFuncSig sig;
+ memset(&fd, 0, sizeof fd);
+ memset(&sig, 0, sizeof sig);
+ sig.ret = tc->i32;
+ sig.call_conv = CFREE_CG_CC_TARGET_C;
+ fd.fn_type = cfree_cg_type_func(tc->c, sig);
+ fd.loc.line = 3;
+ fd.loc.col = 1;
+ return fd;
+}
+
+typedef struct CallbackState {
+ u32 count;
+ CgIrFunc* last;
+} CallbackState;
+
+static void on_func(void* user, CgIrFunc* func) {
+ CallbackState* s = user;
+ ++s->count;
+ s->last = func;
+}
+
+static CgTarget* make_recorder(TestCtx* tc, CallbackState* cb) {
+ CgIrRecorderConfig cfg;
+ memset(&cfg, 0, sizeof cfg);
+ cfg.func_recorded = on_func;
+ cfg.user = cb;
+ return cg_ir_recorder_new(tc->c, NULL, &cfg);
+}
+
+static void test_records_basic_function_shape(void) {
+ TestCtx tc;
+ CallbackState cb;
+ CgTarget* t;
+ CGFuncDesc fd;
+ CGLocal a, b, dst;
+ const CgIrModule* m;
+ CgIrFunc* f;
+ CgIrRetAux* ret;
+ memset(&cb, 0, sizeof cb);
+ tc_init(&tc);
+ t = make_recorder(&tc, &cb);
+ fd = fn_desc(&tc);
+ t->func_begin(t, &fd);
+ t->set_loc(t, (SrcLoc){.file_id = 9, .line = 7, .col = 5});
+ a = local_new(t, tc.i32, "a");
+ b = local_new(t, tc.i32, "b");
+ dst = local_new(t, tc.i32, "dst");
+ t->load_imm(t, op_local(a, tc.i32), 40);
+ t->load_imm(t, op_local(b, tc.i32), 2);
+ t->binop(t, BO_IADD, op_local(dst, tc.i32), op_local(a, tc.i32),
+ op_local(b, tc.i32));
+ t->ret(t, &dst, 1);
+ t->func_end(t);
+
+ m = cg_ir_recorder_module(t);
+ EXPECT(m && m->nfuncs == 1, "expected one recorded function");
+ f = m->funcs[0];
+ EXPECT(f == cb.last && cb.count == 1, "callback should observe func_end");
+ EXPECT(f->complete, "function should be complete");
+ EXPECT(f->nlocals == 3, "expected 3 locals, got %u", f->nlocals);
+ EXPECT(f->ninsts == 4, "expected 4 insts, got %u", f->ninsts);
+ EXPECT(f->insts[0].op == CG_IR_LOAD_IMM && f->insts[0].extra.imm == 40,
+ "first inst should be load_imm 40");
+ EXPECT(f->insts[2].op == CG_IR_BINOP && f->insts[2].opnds[0].v.local == dst &&
+ f->insts[2].opnds[1].v.local == a &&
+ f->insts[2].opnds[2].v.local == b &&
+ f->insts[2].extra.imm == BO_IADD,
+ "binop should preserve semantic local operands");
+ EXPECT(f->insts[2].loc.file_id == 9 && f->insts[2].loc.line == 7,
+ "sticky source location should be stamped on insts");
+ ret = (CgIrRetAux*)f->insts[3].extra.aux;
+ EXPECT(ret && ret->nvalues == 1 && ret->values[0] == dst,
+ "return should preserve semantic result locals");
+ tc_fini(&tc);
+}
+
+static void test_deep_copies_call_switch_and_const_payloads(void) {
+ TestCtx tc;
+ CallbackState cb;
+ CgTarget* t;
+ CGFuncDesc fd;
+ CGLocal arg, result;
+ CGCallDesc call;
+ CGLocal call_args[1];
+ CGLocal call_results[1];
+ CGSwitchDesc sw;
+ CGSwitchCase cases[2];
+ u8 bytes[4] = {1, 2, 3, 4};
+ ConstBytes cbv;
+ Label l0, l1;
+ CgIrFunc* f;
+ CgIrCallAux* call_aux;
+ CgIrSwitchAux* switch_aux;
+ memset(&cb, 0, sizeof cb);
+ tc_init(&tc);
+ t = make_recorder(&tc, &cb);
+ fd = fn_desc(&tc);
+ t->func_begin(t, &fd);
+ arg = local_new(t, tc.i32, "arg");
+ result = local_new(t, tc.i32, "result");
+ l0 = t->label_new(t);
+ l1 = t->label_new(t);
+
+ memset(&call, 0, sizeof call);
+ call_args[0] = arg;
+ call_results[0] = result;
+ call.fn_type = fd.fn_type;
+ call.callee = op_global(12, tc.ptr);
+ call.args = call_args;
+ call.results = call_results;
+ call.nargs = 1;
+ call.nresults = 1;
+ t->call(t, &call);
+ call_args[0] = 999;
+ call_results[0] = 1000;
+
+ memset(&sw, 0, sizeof sw);
+ cases[0].value = 4;
+ cases[0].label = l0;
+ cases[1].value = 9;
+ cases[1].label = l1;
+ sw.selector = op_local(arg, tc.i32);
+ sw.selector_type = tc.i32;
+ sw.default_label = l1;
+ sw.cases = cases;
+ sw.ncases = 2;
+ sw.hint = 7;
+ t->switch_(t, &sw);
+ cases[0].value = 400;
+ cases[0].label = 400;
+
+ memset(&cbv, 0, sizeof cbv);
+ cbv.type = tc.i32;
+ cbv.bytes = bytes;
+ cbv.size = sizeof bytes;
+ cbv.align = 4;
+ t->load_const(t, op_local(result, tc.i32), cbv);
+ bytes[0] = 99;
+ t->func_end(t);
+
+ f = cb.last;
+ EXPECT(f->ninsts == 3, "expected call, switch, load_const");
+ call_aux = (CgIrCallAux*)f->insts[0].extra.aux;
+ EXPECT(call_aux && call_aux->desc.args[0] == arg &&
+ call_aux->desc.results[0] == result,
+ "call descriptor should be deep-copied");
+ switch_aux = (CgIrSwitchAux*)f->insts[1].extra.aux;
+ EXPECT(switch_aux && switch_aux->ncases == 2 &&
+ switch_aux->cases[0].value == 4 &&
+ switch_aux->cases[0].label == l0 &&
+ switch_aux->cases[1].label == l1,
+ "switch cases should be deep-copied");
+ EXPECT(f->insts[2].extra.cbytes.bytes[0] == 1 &&
+ f->insts[2].extra.cbytes.size == 4,
+ "const bytes should be deep-copied");
+ tc_fini(&tc);
+}
+
+static void test_labels_scopes_and_address_taken_locals(void) {
+ TestCtx tc;
+ CallbackState cb;
+ CgTarget* t;
+ CGFuncDesc fd;
+ CGLocal ptr, value;
+ Label label;
+ CGScope scope;
+ CGScopeDesc sd;
+ CgIrFunc* f;
+ CgIrScopeAux* scope_aux;
+ memset(&cb, 0, sizeof cb);
+ tc_init(&tc);
+ t = make_recorder(&tc, &cb);
+ fd = fn_desc(&tc);
+ t->func_begin(t, &fd);
+ ptr = local_new(t, tc.ptr, "ptr");
+ value = local_new(t, tc.i32, "value");
+ label = t->label_new(t);
+ t->label_place(t, label);
+ t->load_label_addr(t, op_local(ptr, tc.ptr), label);
+ t->addr_of(t, op_local(ptr, tc.ptr), op_local(value, tc.i32));
+ memset(&sd, 0, sizeof sd);
+ sd.kind = SCOPE_IF;
+ sd.cond = op_imm(1, tc.i32);
+ scope = t->scope_begin(t, &sd);
+ t->break_to(t, scope);
+ t->scope_end(t, scope);
+ t->func_end(t);
+
+ f = cb.last;
+ EXPECT(f->nlabels == 1 && f->labels[0].nplaces == 1,
+ "label placement should be tracked");
+ EXPECT(f->insts[0].op == CG_IR_LABEL && f->insts[0].extra.imm == label,
+ "label placement should be a linear IR inst");
+ EXPECT(f->locals[value - 1u].address_taken,
+ "addr_of local should mark the local address-taken");
+ EXPECT(f->nscopes == 1 && f->scopes[0].id == scope,
+ "scope table should preserve semantic scope ids");
+ scope_aux = (CgIrScopeAux*)f->insts[3].extra.aux;
+ EXPECT(scope_aux && scope_aux->scope == scope &&
+ scope_aux->desc.kind == SCOPE_IF,
+ "scope_begin inst should carry scope metadata");
+ tc_fini(&tc);
+}
+
+int main(void) {
+ test_records_basic_function_shape();
+ test_deep_copies_call_switch_and_const_payloads();
+ test_labels_scopes_and_address_taken_locals();
+ fprintf(stderr, "ir-recorder: %d checks, %d failures\n", g_checks, g_fails);
+ return g_fails ? 1 : 0;
+}
diff --git a/test/test.mk b/test/test.mk
@@ -51,6 +51,7 @@ TEST_TARGETS = \
test-dwarf \
test-elf \
test-emu \
+ test-ir-recorder \
test-isa \
test-lib-deps \
test-libc \
@@ -231,6 +232,7 @@ $(EMU_RV64_TEST_BIN): test/emu/rv64_smoke_test.c $(LIB_AR)
CG_API_TEST_BIN = build/test/cg_api_test
CG_SWITCH_TEST_BIN = build/test/cg_switch_test
ABI_CLASSIFY_TEST_BIN = build/test/abi_classify_test
+IR_RECORDER_TEST_BIN = build/test/ir_recorder_test
test-cg-api: $(CG_API_TEST_BIN) $(CG_SWITCH_TEST_BIN)
$(CG_API_TEST_BIN)
@@ -251,6 +253,13 @@ $(ABI_CLASSIFY_TEST_BIN): test/api/abi_classify_test.c $(LIB_OBJS)
@mkdir -p $(dir $@)
$(CC) $(TEST_HOST_CFLAGS) -Isrc test/api/abi_classify_test.c $(LIB_OBJS) -o $@
+test-ir-recorder: $(IR_RECORDER_TEST_BIN)
+ $(IR_RECORDER_TEST_BIN)
+
+$(IR_RECORDER_TEST_BIN): test/cg/ir_recorder_test.c $(LIB_OBJS)
+ @mkdir -p $(dir $@)
+ $(CC) $(TEST_HOST_CFLAGS) -Isrc test/cg/ir_recorder_test.c $(LIB_OBJS) -o $@
+
test-toy: bin
@CFREE=$(abspath $(BIN)) test/toy/run.sh