commit 12bb06edf18181404c60995df67a1dc9755ddae7
parent 1268ac83cff81e2e67f57e50c6e18aeb95acf194
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 20 May 2026 10:55:23 -0700
c_target: land Phase 3 of the C-source backend
Aggregate types, varargs, intrinsics, and indirect/tail calls. Toy path C
goes from 49 to 87 passing; the remaining 40 skips all blame Phase 4
surface (data emission, atomics, inline asm, load_const).
- Composite types (records, arrays, function types) emit as opaque
byte-storage typedefs through a TU-wide typedefs section seeded lazily
by c_typename. Field/element access continues through the existing
(base + byte_offset) deref path, sidestepping C bitfield ABI ambiguity.
- Enums map to their underlying integer base; floats to host
float/double/long double; VARARG_STATE to the host's va_list with a
conditional <stdarg.h>.
- Aggregate call args and aggregate returns lower to direct assignment
(slot_R = f(args); / *(T*)addr = f(args);) -- no sret shim.
- Indirect calls cast the (void*) reg to a function-pointer typedef
derived from the call's fn_type.
- Tail calls become `return f(args);` so the host cc handles TCO; CG
intentionally does not invoke ret afterwards.
- va_start_/va_arg_/va_end_/va_copy_ lower to __builtin_va_*; the
synthesized last-named-param `pN-1` is fed to va_start.
- intrinsic dispatches the full IntrinKind set onto __builtin_{popcount,
ctz, clz, bswap*, memcpy, memmove, memset, prefetch, assume_aligned,
expect, unreachable, trap, *_overflow} plus setjmp/longjmp via a
conditional <setjmp.h>.
- copy_bytes/set_bytes/alloca_ implemented.
- Binop/cmp emission wraps pointer-typed operands `((uintptr_t)op)` so
the source compiles under -Wpedantic -Werror; cfree IR is already
byte-delta shaped semantically.
- Variadic function signatures now emit `, ...)`.
- OPK_IMM with type NONE (cfree_cg_memset's byte value) emits the
literal raw instead of attempting `(NONE)` cast.
Side-fix: test/parse/run.sh now gates path C to opt=0 only, matching the
toy runner -- the C target forces opt_level=0 internally so running it
per opt level just duplicates work.
Diffstat:
5 files changed, 811 insertions(+), 93 deletions(-)
diff --git a/doc/CBACKEND.md b/doc/CBACKEND.md
@@ -475,13 +475,47 @@ Acceptance met: 49 toy cases under path C pass (loops, conditionals,
locals, pointer chasing, recursion); the remaining 78 SKIPs all blame
Phase 3/4 surface (aggregates, intrinsics, atomics, data emission).
-### Phase 3 — aggregates, varargs, intrinsics
-
-- Type emission for records, arrays, function types.
-- Call/return for aggregate types (address-operand emission rule).
-- `va_start_`/`va_arg_`/`va_end_`/`va_copy_`.
-- `intrinsic` for the full IntrinKind set.
-- `copy_bytes`/`set_bytes`.
+### Phase 3 — aggregates, varargs, intrinsics ✅ landed
+
+Implemented: type emission for records / arrays / function types / enums /
+floats / `va_list`; aggregate call args and aggregate returns (address-operand
+emission); indirect call via function-pointer typedef; tail calls (lowered to
+`return f(args)`); `va_start_`/`va_arg_`/`va_end_`/`va_copy_`; `intrinsic` for
+the full `IntrinKind` set; `copy_bytes`/`set_bytes`; `alloca_`.
+
+Other choices that landed in Phase 3:
+
+- **Composite types as opaque byte storage.** Records and arrays are
+ emitted as `typedef struct { _Alignas(A) uint8_t raw[N]; } __ty_N;` —
+ same shape regardless of field layout. CG already speaks in
+ `(base, byte_offset)` for field/element access, so the indirect path
+ `(*(T*)((char*)addr + ofs))` does all the work and the host C compiler
+ never needs to see the original field declarations. Sidesteps C bitfield
+ ABI ambiguity entirely (see §"Type emission" in this doc).
+- **Pointer arithmetic via `uintptr_t`.** Any binop/cmp whose operand type
+ is a pointer is wrapped `((uintptr_t)op)` before the operator so the
+ emitted source compiles cleanly under `-Wpedantic -Werror` (strict C
+ forbids `void* + void*`). cfree IR already carries byte deltas, so
+ arithmetic is integer-shaped semantically; this just makes that syntactic.
+- **Type-emission worklist.** `c_ensure_typedef` lazily emits a typedef
+ the first time `c_typename` encounters a RECORD/ARRAY/FUNC, recursing
+ on field/element/param types so dependencies emit first. Output lands
+ in a TU-wide `typedefs` CBuf flushed between the prologue and forward
+ declarations.
+- **Variadic signature.** Function signatures emit `, ...)` when
+ `func.abi_variadic` is set so the host cc accepts `__builtin_va_start`.
+- **Tail calls become `return f(args);`.** For tail-flagged calls CG
+ doesn't follow with a `ret`, so the C target emits the return itself;
+ gcc handles the actual TCO. Void tail calls add a trailing `return;`.
+- **Aggregate ret storage.** `OPK_LOCAL`/`OPK_INDIRECT` ret storage is
+ spelled as a direct assignment (`slot_R = f(args);` /
+ `(*(T*)addr) = f(args);`). No sret shim — the host cc rebuilds that.
+- **Untyped IMM byte values.** `cfree_cg_memset` passes
+ `OPK_IMM(byte, type=NONE)`; `c_emit_operand` recognizes this and emits
+ the literal raw instead of attempting to cast to `(NONE)`.
+
+Acceptance met: 87 toy cases under path `C` pass; the remaining 40 SKIPs
+all blame Phase 4 surface (data emission, atomics, inline asm).
### Phase 4 — atomics, asm, TLS, exotic features
diff --git a/src/arch/c_target/emit.c b/src/arch/c_target/emit.c
@@ -17,12 +17,19 @@
#include "arch/c_target/internal.h"
+#include <stdio.h>
+
#include "cg/type.h"
#include "core/core.h"
#include "core/heap.h"
#include "core/pool.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);
+
/* === Writer helpers === */
void c_writer_write(CTarget* t, const void* data, size_t n) {
@@ -69,7 +76,179 @@ static u32 c_int_width_for_signedness(CTarget* t, CfreeCgTypeId type) {
return 0;
}
-/* Phase 1: void / bool / sized int / pointer. Aggregates and floats panic. */
+/* === 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(t->c->global, buf, (size_t)n);
+ return pool_str(t->c->global, s, NULL);
+}
+
+/* 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;
+ }
+}
+
+/* Phase 1/3: void / bool / sized int / pointer / float / record / array /
+ * enum / vararg-state. Returns interned C type names; for composites lazily
+ * emits a typedef into t->typedefs and returns the typedef name. */
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);
@@ -82,16 +261,29 @@ static const char* c_typename(CTarget* t, CfreeCgTypeId type) {
return "void";
case CFREE_CG_TYPE_BOOL:
return "int32_t";
- case CFREE_CG_TYPE_INT: {
- const char* s = c_int_type_name_for_width(ty->integer.width, 1);
+ 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: int width %u not yet supported",
- (unsigned)ty->integer.width);
+ 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);
@@ -204,16 +396,58 @@ void c_emit_operand(CTarget* t, Operand op) {
cbuf_puts(&t->body, buf);
return;
case OPK_IMM:
- cbuf_puts(&t->body, "((");
- c_emit_type(t, &t->body, op.type);
- cbuf_puts(&t->body, ")");
- cbuf_put_i64(&t->body, op.v.imm);
- cbuf_puts(&t->body, ")");
+ if (op.type == CFREE_CG_TYPE_NONE) {
+ /* Untyped IMM (e.g. memset byte value): emit the literal raw. */
+ cbuf_putc(&t->body, '(');
+ cbuf_put_i64(&t->body, op.v.imm);
+ cbuf_putc(&t->body, ')');
+ } else {
+ cbuf_puts(&t->body, "((");
+ c_emit_type(t, &t->body, op.type);
+ cbuf_puts(&t->body, ")");
+ cbuf_put_i64(&t->body, op.v.imm);
+ cbuf_puts(&t->body, ")");
+ }
return;
- case OPK_LOCAL:
+ case OPK_LOCAL: {
+ /* If the operand's type matches the slot's declared type (the common
+ * case), emit `slot_N` directly. Otherwise, deref the slot's address
+ * cast to a pointer of the operand type so we read it as the type CG
+ * actually wants. */
c_slot_name(op.v.frame_slot, buf, sizeof buf);
- cbuf_puts(&t->body, buf);
+ u32 idx = (u32)op.v.frame_slot - 1u;
+ CfreeCgTypeId slot_ty = (idx < t->nslots) ? t->slot_type[idx]
+ : (CfreeCgTypeId)0;
+ if (op.type == 0 || slot_ty == 0 ||
+ api_unalias_type(t->c, op.type) ==
+ api_unalias_type(t->c, slot_ty)) {
+ cbuf_puts(&t->body, buf);
+ } else {
+ cbuf_puts(&t->body, "(*(");
+ c_emit_type(t, &t->body, op.type);
+ cbuf_puts(&t->body, "*)&");
+ cbuf_puts(&t->body, buf);
+ cbuf_puts(&t->body, ")");
+ }
return;
+ }
+ case OPK_INDIRECT: {
+ /* Used by call paths to pass aggregates by-address: the operand's type
+ * is the aggregate, the storage is (base + offset). Emit the deref as
+ * a value expression. */
+ cbuf_puts(&t->body, "(*(");
+ c_emit_type(t, &t->body, op.type);
+ cbuf_puts(&t->body, "*)((char*)");
+ char rbuf[24];
+ c_reg_name(op.v.ind.base, rbuf, sizeof rbuf);
+ cbuf_puts(&t->body, rbuf);
+ if (op.v.ind.ofs != 0) {
+ cbuf_puts(&t->body, " + ");
+ cbuf_put_i64(&t->body, (i64)op.v.ind.ofs);
+ }
+ cbuf_puts(&t->body, "))");
+ return;
+ }
case OPK_GLOBAL: {
/* Data globals need an `extern T name;` declaration AND, in the
* single-TU case, the actual data definition — both belong to data
@@ -288,6 +522,57 @@ void c_emit_operand_signed(CTarget* t, Operand op, int signed_) {
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;
+}
+
+/* Returns 1 if `op`'s emitted C expression has pointer type. Beyond the
+ * obvious `op.type` check, CG can hand us a reg whose first-sighting type
+ * was `void*` paired with an integer-typed operand later (the IR reuses ids
+ * across types). The C declaration is locked at first sighting, so the
+ * lvalue we'd emit is still a `void*`. */
+static int c_operand_is_ptr_typed(CTarget* t, Operand op) {
+ if (c_type_is_ptr(t, op.type)) return 1;
+ if (op.kind == OPK_REG && (u32)op.v.reg < t->reg_cap &&
+ t->reg_declared[op.v.reg]) {
+ if (c_type_is_ptr(t, t->reg_type[op.v.reg])) return 1;
+ }
+ if (op.kind == OPK_LOCAL) {
+ u32 idx = (u32)op.v.frame_slot - 1u;
+ if (idx < t->nslots && c_type_is_ptr(t, t->slot_type[idx])) return 1;
+ }
+ return 0;
+}
+
+/* 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)");
+ 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)");
+ 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
@@ -405,7 +690,12 @@ void c_emit_prologue(CTarget* t) {
c_writer_puts(t,
"/* generated by cfree --emit=c */\n"
"#include <stdint.h>\n"
- "\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 === */
@@ -423,7 +713,7 @@ static void c_emit_func_signature(CTarget* t, CBuf* b, const char* name,
cbuf_puts(b, " ");
cbuf_puts(b, name);
cbuf_puts(b, "(");
- if (fty->func.nparams == 0) {
+ if (fty->func.nparams == 0 && !fty->func.abi_variadic) {
cbuf_puts(b, "void");
} else {
for (u32 i = 0; i < fty->func.nparams; ++i) {
@@ -432,6 +722,10 @@ static void c_emit_func_signature(CTarget* t, CBuf* b, const char* name,
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, ")");
}
@@ -662,28 +956,28 @@ void c_binop(CGTarget* T, BinOp op, Operand dst, Operand a, Operand b) {
BinSignCast bsc = binop_sign_kind(op, &lhs_signed);
switch (bsc) {
case BSC_NONE:
- c_emit_operand(t, a);
+ c_emit_operand_arith(t, a);
cbuf_puts(&t->body, " ");
cbuf_puts(&t->body, sym);
cbuf_puts(&t->body, " ");
- c_emit_operand(t, b);
+ c_emit_operand_arith(t, b);
break;
case BSC_SIGNED:
- c_emit_operand_signed(t, a, 1);
+ c_emit_operand_arith_signed(t, a, 1);
cbuf_puts(&t->body, " ");
cbuf_puts(&t->body, sym);
cbuf_puts(&t->body, " ");
- c_emit_operand_signed(t, b, 1);
+ c_emit_operand_arith_signed(t, b, 1);
break;
case BSC_UNSIGNED:
- c_emit_operand_signed(t, a, 0);
+ c_emit_operand_arith_signed(t, a, 0);
cbuf_puts(&t->body, " ");
cbuf_puts(&t->body, sym);
cbuf_puts(&t->body, " ");
- c_emit_operand_signed(t, b, 0);
+ c_emit_operand_arith_signed(t, b, 0);
break;
case BSC_SHIFT_LHS:
- c_emit_operand_signed(t, a, lhs_signed);
+ c_emit_operand_arith_signed(t, a, lhs_signed);
cbuf_puts(&t->body, " ");
cbuf_puts(&t->body, sym);
cbuf_puts(&t->body, " ");
@@ -768,18 +1062,18 @@ static int cmp_signedness(CmpOp op) {
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(t, a);
+ 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(t, b);
+ c_emit_operand_arith(t, b);
} else {
int signed_ = (sg < 0);
- c_emit_operand_signed(t, a, signed_);
+ 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_signed(t, b, signed_);
+ c_emit_operand_arith_signed(t, b, signed_);
}
}
@@ -1046,44 +1340,114 @@ void c_convert(CGTarget* T, ConvKind k, Operand dst, Operand src) {
/* === call === */
+/* Emit an argument expression for a call. The CGABIValue's `type` is the
+ * source (front-end) type; for aggregates the storage is by-address. We
+ * dereference such addresses to recover the value the C callee expects.
+ *
+ * The CG also routes scalar-typed values through OPK_LOCAL (frame-slot
+ * params land there in this target). c_emit_operand already does the right
+ * thing for OPK_LOCAL when the slot's declared type matches; for aggregate
+ * args coming from a frame slot of the aggregate type, the slot was
+ * declared with the aggregate type, and `slot_N` works as a struct value.
+ */
+static void c_emit_call_arg(CTarget* t, const CGABIValue* v) {
+ Operand op = v->storage;
+ /* If the source type is an aggregate but the storage is some address
+ * carrier, the emission already produces the deref via c_emit_operand's
+ * OPK_LOCAL/INDIRECT paths. CGABIValue.type carries the front-end type;
+ * use that to override the operand type so the deref expression uses the
+ * aggregate type. */
+ op.type = v->type;
+ c_emit_operand(t, op);
+}
+
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};
- if (d->callee.kind != OPK_GLOBAL) {
- compiler_panic(t->c, loc, "C target: indirect call not yet supported");
- }
-
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 has_ret = !cg_type_is_void(t->c, ret_type);
-
- if (has_ret) {
- if (d->ret.storage.kind != OPK_REG) {
+ int fn_returns = !cg_type_is_void(t->c, ret_type);
+ int is_tail = (d->flags & CG_CALL_TAIL) != 0;
+ /* When a non-void function is tail-called, CG sets ret.storage to a void
+ * IMM (the caller will not consume the result). We still need to forward
+ * the callee's value with a `return` so the enclosing function returns
+ * something — gcc will tail-call-optimize it on its own. */
+
+ /* === Emit the LHS / open the call statement === */
+ if (is_tail) {
+ cbuf_puts(&t->body, " ");
+ if (fn_returns) cbuf_puts(&t->body, "return ");
+ } else if (fn_returns) {
+ Operand rs = d->ret.storage;
+ if (rs.kind == OPK_REG) {
+ c_ensure_reg(t, rs.v.reg, ret_type, (RegClass)rs.cls);
+ c_emit_reg_assign_open(t, rs.v.reg);
+ } else if (rs.kind == OPK_LOCAL) {
+ char buf[24];
+ c_slot_name(rs.v.frame_slot, buf, sizeof buf);
+ cbuf_puts(&t->body, " ");
+ cbuf_puts(&t->body, buf);
+ cbuf_puts(&t->body, " = ");
+ } else if (rs.kind == OPK_INDIRECT) {
+ cbuf_puts(&t->body, " ");
+ c_emit_addr_deref(t, rs, ret_type);
+ cbuf_puts(&t->body, " = ");
+ } else if (rs.kind == OPK_IMM &&
+ cg_type_is_void(t->c, rs.type)) {
+ /* Result discarded by CG; emit a statement-only call. */
+ cbuf_puts(&t->body, " ");
+ } else {
compiler_panic(t->c, loc,
- "C target: aggregate return not yet supported");
+ "C target: call ret storage kind %d not supported",
+ (int)rs.kind);
}
- c_ensure_reg(t, d->ret.storage.v.reg, ret_type,
- (RegClass)d->ret.storage.cls);
- c_emit_reg_assign_open(t, d->ret.storage.v.reg);
} else {
cbuf_puts(&t->body, " ");
}
- c_ensure_forward_decl(t, d->callee.v.global.sym, d->fn_type);
- cbuf_puts(&t->body, c_sym_name(t, d->callee.v.global.sym));
+
+ /* === Emit the callee expression === */
+ if (d->callee.kind == OPK_GLOBAL) {
+ c_ensure_forward_decl(t, d->callee.v.global.sym, d->fn_type);
+ cbuf_puts(&t->body, c_sym_name(t, d->callee.v.global.sym));
+ } else if (d->callee.kind == OPK_REG) {
+ /* Indirect call: cast the (void*) reg to the right function-pointer
+ * type and call. The typedef machinery makes a function-pointer typedef
+ * for d->fn_type available as __ty_<id>. */
+ const char* fp = c_typedef_name(t, d->fn_type);
+ c_ensure_typedef(t, d->fn_type);
+ cbuf_puts(&t->body, "((");
+ cbuf_puts(&t->body, fp);
+ cbuf_puts(&t->body, ")");
+ c_emit_operand(t, d->callee);
+ cbuf_puts(&t->body, ")");
+ } else {
+ compiler_panic(t->c, loc, "C target: callee kind %d not supported",
+ (int)d->callee.kind);
+ }
+
+ /* === Emit args === */
cbuf_puts(&t->body, "(");
for (u32 i = 0; i < d->nargs; ++i) {
if (i > 0) cbuf_puts(&t->body, ", ");
- c_emit_operand(t, d->args[i].storage);
+ c_emit_call_arg(t, &d->args[i]);
}
cbuf_puts(&t->body, ")");
- if (has_ret) {
+
+ /* === Close the statement === */
+ if (!is_tail && fn_returns && d->ret.storage.kind == OPK_REG) {
c_emit_reg_assign_close(t);
} else {
cbuf_puts(&t->body, ";\n");
+ if (is_tail && !fn_returns) {
+ /* Void tail call. After this CG won't call ret, so add an explicit
+ * return so any non-void enclosing function still has a terminating
+ * statement. (For void-returning enclosing fns this is a no-op.) */
+ cbuf_puts(&t->body, " return;\n");
+ }
}
}
@@ -1206,6 +1570,330 @@ void c_alias(CGTarget* T, ObjSymId alias_sym, ObjSymId target_sym,
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 reg (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_reg(t, dsts[0].v.reg, dsts[0].type, (RegClass)dsts[0].cls);
+ c_emit_reg_assign_open(t, dsts[0].v.reg);
+ 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_reg_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_reg(t, dsts[0].v.reg, dsts[0].type, (RegClass)dsts[0].cls);
+ c_emit_reg_assign_open(t, dsts[0].v.reg);
+ 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_reg_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_reg(t, dsts[0].v.reg, dsts[0].type, (RegClass)dsts[0].cls);
+ c_emit_reg_assign_open(t, dsts[0].v.reg);
+ cbuf_puts(&t->body, fn);
+ cbuf_puts(&t->body, "(");
+ c_emit_operand(t, args[0]);
+ cbuf_puts(&t->body, ")");
+ c_emit_reg_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, ", ");
+ 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 reg, dsts[1] = i1 overflow flag */
+ if (ndst != 2 || narg != 2) {
+ compiler_panic(t->c, loc,
+ "C target: overflow-intrin: bad shape");
+ }
+ const char* fn = c_overflow_builtin(k);
+ c_ensure_reg(t, dsts[0].v.reg, dsts[0].type, (RegClass)dsts[0].cls);
+ c_ensure_reg(t, dsts[1].v.reg, dsts[1].type, (RegClass)dsts[1].cls);
+ char vbuf[24], obuf[24];
+ c_reg_name(dsts[0].v.reg, vbuf, sizeof vbuf);
+ c_reg_name(dsts[1].v.reg, obuf, sizeof obuf);
+ /* gcc/clang's __builtin_*_overflow writes the result through a pointer
+ * and returns the overflow as int. */
+ cbuf_puts(&t->body, " ");
+ 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, "(");
+ c_emit_operand(t, args[0]);
+ cbuf_puts(&t->body, ", ");
+ c_emit_operand(t, args[1]);
+ cbuf_puts(&t->body, ", &");
+ cbuf_puts(&t->body, vbuf);
+ cbuf_puts(&t->body, ");\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_reg(t, dsts[0].v.reg, dsts[0].type, (RegClass)dsts[0].cls);
+ c_emit_reg_assign_open(t, dsts[0].v.reg);
+ cbuf_puts(&t->body, "setjmp(*(jmp_buf*)(");
+ c_emit_operand(t, args[0]);
+ cbuf_puts(&t->body, "))");
+ c_emit_reg_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_REG) {
+ compiler_panic(t->c, loc, "C target: alloca dst must be REG");
+ }
+ c_ensure_reg(t, dst.v.reg, dst.type, (RegClass)dst.cls);
+ c_emit_reg_assign_open(t, dst.v.reg);
+ 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_reg_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 slot.
+ * 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_REG) {
+ compiler_panic(t->c, loc, "C target: va_arg dst must be REG");
+ }
+ c_ensure_reg(t, dst.v.reg, dst.type, (RegClass)dst.cls);
+ c_emit_reg_assign_open(t, dst.v.reg);
+ cbuf_puts(&t->body, "__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_reg_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;
+ cbuf_puts(&t->body, " __builtin_memcpy(");
+ c_emit_operand(t, dst_addr);
+ cbuf_puts(&t->body, ", ");
+ c_emit_operand(t, src_addr);
+ cbuf_puts(&t->body, ", ");
+ cbuf_put_u64(&t->body, (u64)m.size);
+ cbuf_puts(&t->body, ");\n");
+}
+
+void c_set_bytes(CGTarget* T, Operand dst_addr, Operand byte_value,
+ AggregateAccess m) {
+ CTarget* t = (CTarget*)T;
+ cbuf_puts(&t->body, " __builtin_memset(");
+ c_emit_operand(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");
+}
+
/* === set_loc === */
void c_set_loc(CGTarget* T, SrcLoc l) {
@@ -1220,6 +1908,13 @@ void c_finalize(CGTarget* 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");
+ }
if (t->forwards.len) {
c_writer_write(t, t->forwards.data, t->forwards.len);
c_writer_puts(t, "\n");
diff --git a/src/arch/c_target/internal.h b/src/arch/c_target/internal.h
@@ -55,6 +55,18 @@ typedef struct CTarget {
u8* sym_forwarded;
u32 sym_forwarded_cap;
+ /* TU-wide typedefs (records, arrays, function pointers, opaque storage).
+ * Emitted between prologue and forwards. */
+ CBuf typedefs;
+ /* 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;
diff --git a/src/arch/c_target/target.c b/src/arch/c_target/target.c
@@ -44,6 +44,14 @@ 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);
/* Unimplemented stubs panic specifically per the doc's Phase 0 acceptance
* criterion. The method name is encoded in the panic message. */
@@ -124,16 +132,6 @@ static void c_unimpl_tls_addr_of(CGTarget* t, Operand dst, ObjSymId sym,
(void)dst; (void)sym; (void)addend;
C_UNIMPL("tls_addr_of");
}
-static void c_unimpl_copy_bytes(CGTarget* t, Operand a, Operand b,
- AggregateAccess m) {
- (void)a; (void)b; (void)m;
- C_UNIMPL("copy_bytes");
-}
-static void c_unimpl_set_bytes(CGTarget* t, Operand a, Operand b,
- AggregateAccess m) {
- (void)a; (void)b; (void)m;
- C_UNIMPL("set_bytes");
-}
static void c_unimpl_bitfield_load(CGTarget* t, Operand dst, Operand addr,
BitFieldAccess bf) {
(void)dst; (void)addr; (void)bf;
@@ -168,28 +166,6 @@ static void c_unimpl_emit_call_plan(CGTarget* t, const CGCallPlan* p) {
C_UNIMPL("emit_call_plan");
}
-static void c_unimpl_alloca(CGTarget* t, Operand d, Operand s, u32 a) {
- (void)d; (void)s; (void)a;
- C_UNIMPL("alloca_");
-}
-static void c_unimpl_va_start(CGTarget* t, Operand a) {
- (void)a;
- C_UNIMPL("va_start_");
-}
-static void c_unimpl_va_arg(CGTarget* t, Operand d, Operand a,
- CfreeCgTypeId ty) {
- (void)d; (void)a; (void)ty;
- C_UNIMPL("va_arg_");
-}
-static void c_unimpl_va_end(CGTarget* t, Operand a) {
- (void)a;
- C_UNIMPL("va_end_");
-}
-static void c_unimpl_va_copy(CGTarget* t, Operand d, Operand s) {
- (void)d; (void)s;
- C_UNIMPL("va_copy_");
-}
-
static void c_unimpl_atomic_load(CGTarget* t, Operand d, Operand a, MemAccess m,
MemOrder o) {
(void)d; (void)a; (void)m; (void)o;
@@ -216,11 +192,6 @@ static void c_unimpl_fence(CGTarget* t, MemOrder o) {
C_UNIMPL("fence");
}
-static void c_unimpl_intrinsic(CGTarget* t, IntrinKind k, Operand* d, u32 nd,
- const Operand* a, u32 na) {
- (void)k; (void)d; (void)nd; (void)a; (void)na;
- C_UNIMPL("intrinsic");
-}
static void c_unimpl_asm_block(CGTarget* t, const char* tmpl,
const AsmConstraint* outs, u32 no, Operand* oo,
const AsmConstraint* ins, u32 ni,
@@ -241,6 +212,7 @@ CGTarget* c_cgtarget_new(Compiler* c, ObjBuilder* o, CfreeWriter* w) {
x->obj = o;
x->w = w;
cbuf_init(&x->forwards, c->ctx->heap);
+ cbuf_init(&x->typedefs, c->ctx->heap);
cbuf_init(&x->decls, c->ctx->heap);
cbuf_init(&x->body, c->ctx->heap);
@@ -295,8 +267,8 @@ CGTarget* c_cgtarget_new(Compiler* c, ObjBuilder* o, CfreeWriter* w) {
t->store = c_store;
t->addr_of = c_addr_of;
t->tls_addr_of = c_unimpl_tls_addr_of;
- t->copy_bytes = c_unimpl_copy_bytes;
- t->set_bytes = c_unimpl_set_bytes;
+ t->copy_bytes = c_copy_bytes;
+ t->set_bytes = c_set_bytes;
t->bitfield_load = c_unimpl_bitfield_load;
t->bitfield_store = c_unimpl_bitfield_store;
@@ -316,11 +288,11 @@ CGTarget* c_cgtarget_new(Compiler* c, ObjBuilder* o, CfreeWriter* w) {
t->ret = c_ret;
/* ---- alloca / varargs ---- */
- t->alloca_ = c_unimpl_alloca;
- t->va_start_ = c_unimpl_va_start;
- t->va_arg_ = c_unimpl_va_arg;
- t->va_end_ = c_unimpl_va_end;
- t->va_copy_ = c_unimpl_va_copy;
+ 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_unimpl_atomic_load;
@@ -330,7 +302,7 @@ CGTarget* c_cgtarget_new(Compiler* c, ObjBuilder* o, CfreeWriter* w) {
t->fence = c_unimpl_fence;
/* ---- intrinsics / asm ---- */
- t->intrinsic = c_unimpl_intrinsic;
+ t->intrinsic = c_intrinsic;
t->asm_block = c_unimpl_asm_block;
t->resolve_reg_name = NULL;
diff --git a/test/parse/run.sh b/test/parse/run.sh
@@ -520,6 +520,11 @@ run_parse_case() {
# show up as a panic — e.g. emitted-C link errors that need Phase 4
# work (aliases, asm definitions) to fix.
run_c=$RUN_C
+ # Path C forces opt_level=0 internally; running it at every requested
+ # opt level would duplicate identical work. Mirror the toy runner.
+ if [ $run_c -eq 1 ] && [ "$opt" != "0" ]; then
+ run_c=0
+ fi
if [ $run_c -eq 1 ] && [ -e "$TEST_DIR/cases/$base_name.cbackend.skip" ]; then
reason=$(head -n1 "$TEST_DIR/cases/$base_name.cbackend.skip")
emit_event "$event" SKIP "$name/C" "$reason"