commit 88093f7a2ed4025863be2de9409e7f42934cedee
parent 0c5222c1d9651c72759c16bd0e19caa8806a50c0
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 20 May 2026 13:15:09 -0700
c_target: emit C that compiles under -Wall -Wextra -Werror
Six warning classes the host cc flagged on emitted source:
- unused-but-set-variable / unused-variable: vN and slot_N decls now
get __attribute__((unused)) = {0}. The zero-init also silences
-Wsometimes-uninitialized on paths clang can't reason through.
- unused-label: label placement adds __attribute__((unused)).
- unused-const-variable: static data definitions get
__attribute__((unused)).
- implicitly-unsigned-literal: INT64_MIN is now spelled
(-9223372036854775807LL - 1) via a c_emit_imm_literal helper.
- void-pointer-to-int-cast: CV_TRUNC and CV_SEXT/ZEXT through a
pointer-typed reg (CG reuses reg ids across types) now bridge
through (uintptr_t).
Tighten the host-cc compile in test/parse/run.sh and test/toy/run.sh
to -Wall -Wextra -Werror so any regression in emitter quality fails
the suite. 432/432 parse cases and 124/124 toy cases that emit
successfully now pass; the 31 + 3 SKIPs are pre-existing
phased-rollout gaps (i128/ldbl128/Mach-O TLS).
Diffstat:
3 files changed, 50 insertions(+), 12 deletions(-)
diff --git a/src/arch/c_target/emit.c b/src/arch/c_target/emit.c
@@ -29,6 +29,7 @@
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 int c_operand_is_ptr_typed(CTarget* t, Operand op);
/* === Writer helpers === */
@@ -385,7 +386,23 @@ void c_ensure_reg(CTarget* t, Reg r, CfreeCgTypeId type, RegClass cls) {
char buf[24];
c_reg_name(r, buf, sizeof buf);
cbuf_puts(&t->decls, buf);
- cbuf_puts(&t->decls, ";\n");
+ /* `= {0}` zeroes scalars and aggregates uniformly. Kills
+ * -Wsometimes-uninitialized for control flow clang can't reason through;
+ * the host C compiler DSEs the init when a real assignment dominates. */
+ cbuf_puts(&t->decls, " __attribute__((unused)) = {0};\n");
+}
+
+/* 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);
}
void c_emit_operand(CTarget* t, Operand op) {
@@ -400,13 +417,13 @@ void c_emit_operand(CTarget* t, Operand op) {
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);
+ 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, ")");
- cbuf_put_i64(&t->body, op.v.imm);
+ c_emit_imm_literal(t, op.v.imm);
cbuf_puts(&t->body, ")");
}
return;
@@ -551,9 +568,16 @@ void c_emit_operand_signed(CTarget* t, Operand op, int signed_) {
c_emit_operand(t, op);
return;
}
+ /* If the operand's C declaration is pointer-typed (reg-id reuse across
+ * types), bridge through uintptr_t so the narrow int cast doesn't trip
+ * -Wvoid-pointer-to-int-cast. */
+ int via_uptr = c_operand_is_ptr_typed(t, op);
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, ")");
}
@@ -951,7 +975,8 @@ FrameSlot c_frame_slot(CGTarget* T, const FrameSlotDesc* fsd) {
char buf[24];
c_slot_name(id, buf, sizeof buf);
cbuf_puts(&t->decls, buf);
- cbuf_puts(&t->decls, ";\n");
+ /* See c_ensure_reg — same `= {0}` reasoning. */
+ cbuf_puts(&t->decls, " __attribute__((unused)) = {0};\n");
return id;
}
@@ -994,7 +1019,7 @@ void c_load_imm(CGTarget* T, Operand dst, i64 imm) {
}
c_ensure_reg(t, dst.v.reg, dst.type, (RegClass)dst.cls);
c_emit_reg_assign_open(t, dst.v.reg);
- cbuf_put_i64(&t->body, imm);
+ c_emit_imm_literal(t, imm);
c_emit_reg_assign_close(t);
}
@@ -1252,10 +1277,11 @@ void c_label_place(CGTarget* T, Label l) {
CTarget* t = (CTarget*)T;
char buf[24];
c_label_name(l, buf, sizeof buf);
- /* `Lk: ;` — the empty statement keeps it valid even at end-of-block. */
+ /* `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, ": ;\n");
+ cbuf_puts(&t->body, ": __attribute__((unused));\n");
}
void c_jump(CGTarget* T, Label l) {
@@ -1459,6 +1485,13 @@ void c_convert(CGTarget* T, ConvKind k, Operand dst, Operand src) {
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);
@@ -2764,7 +2797,7 @@ static void c_emit_data_symbol(CTarget* t, ObjSymId id, const ObjSym* os) {
/* Common — uninitialized, with explicit alignment. Emit as
* tentative-definition (`uint8_t name[size];` at file scope), which C
* treats as a common-style definition under -fcommon. */
- cbuf_puts(b, "_Alignas(");
+ cbuf_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);
@@ -2782,6 +2815,10 @@ static void c_emit_data_symbol(CTarget* t, ObjSymId id, const ObjSym* os) {
if (os->bind == SB_LOCAL) cbuf_puts(b, "static ");
if (is_tls) cbuf_puts(b, "_Thread_local ");
c_emit_link_attrs(b, os);
+ /* `__attribute__((unused))` so -Wunused-const-variable doesn't fire when
+ * the upstream use got folded away (CG can emit the data sym before the
+ * frontend decides to inline its load). */
+ cbuf_puts(b, "__attribute__((unused)) ");
/* `const` only when the section is RODATA and no relocs patch it (we
* still drop const if there are relocs because the constructor will
* write through this storage). */
diff --git a/test/parse/run.sh b/test/parse/run.sh
@@ -602,7 +602,7 @@ run_parse_case() {
else
emit_event "$event" FAIL "$name/C (parse-runner --emit-c failed; see $work/c.emit.err)"
fi
- elif ! $CC -std=c11 "$c_src" "$C_WRAPPER_OBJ" -o "$c_bin" \
+ elif ! $CC -std=c11 -Wall -Wextra -Werror "$c_src" "$C_WRAPPER_OBJ" -o "$c_bin" \
>"$work/c.cc.out" 2>"$work/c.cc.err"; then
dt=$(( $(now_ms) - t0 ))
emit_event "$event" TIME C "$dt"
diff --git a/test/toy/run.sh b/test/toy/run.sh
@@ -8,8 +8,9 @@
# C cfree cc --emit=c case.toy -> host cc -> native exec. Exercises the
# --emit=c C-source backend driven by a non-C frontend (validates that
# the CGTarget seam is frontend-agnostic). Phased-rollout panics from
-# the C target report as SKIP. Host cc runs without -Werror so the
-# i64 toy main type doesn't trigger -Wmain-return-type as an error.
+# the C target report as SKIP. Host cc runs under
+# -Wall -Wextra -Werror with -Wno-main-return-type (toy's main returns
+# i64, and `int64_t main` would otherwise fire -Wmain-return-type).
#
# Sidecars:
# <name>.expected expected process exit code, default 0
@@ -338,7 +339,7 @@ run_case_emit_c() {
# hard error ("'main' must return 'int'"). The emitted C only needs
# the stdint typedefs, so we compile under -std=gnu99 + the relax
# flag — gnu99's main-return-type check is a warning, not an error.
- if ! $HOST_CC -std=gnu99 -Wno-main-return-type "$out_c" -o "$out_bin" \
+ if ! $HOST_CC -std=gnu99 -Wall -Wextra -Werror -Wno-main-return-type "$out_c" -o "$out_bin" \
> "$work/c.cc.out" 2> "$cc_err"; then
note_fail "$label"
printf ' host cc rejected emitted source\n'