kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

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:
Mdoc/CBACKEND.md | 48+++++++++++++++++++++++++++++++++++++++++-------
Msrc/arch/c_target/emit.c | 777++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Msrc/arch/c_target/internal.h | 12++++++++++++
Msrc/arch/c_target/target.c | 62+++++++++++++++++---------------------------------------------
Mtest/parse/run.sh | 5+++++
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"