commit 704d2a298814839a5fdc17620b078f5e12dc34e3
parent 632517297055ab3a43bccd588d04211a315c97d1
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Mon, 8 Jun 2026 17:12:08 -0700
bootstrap: aarch64-linux self-host (musl + glibc) + Toy ELF fixes
Reach the byte-identical bootstrap fixed point on aarch64-linux at -O0 and -O1
for both musl and glibc, run natively inside an arm64 Linux container
(scripts/linux_bootstrap.sh; make bootstrap-linux[-glibc]).
Build:
- bootstrap.mk: drive kit-as-compiler (stages 2/3) with -lc on every host, so
kit resolves its hosted libc/sysroot via its own profile + host probe
(replaces Darwin's -isysroot; unifies linux/freebsd/macos).
- scripts/linux_bootstrap.sh: container runner (alpine=musl, debian=glibc).
C frontend / preprocessor (glibc + Linux-UAPI headers; musl's ISO-C headers
never exercised these):
- pp: predefine __{INT,UINT}{64,MAX}_C_SUFFIX__ so kit-compiled TUs get
correctly-typed 64-bit constants from rt/include/stdint.h.
- pp: erase __extension__ and map __signed__/__volatile__/__const__ to the
canonical keywords (unconditional; were Windows-only).
- pp: support GNU named variadic macro params `args...` (<linux/stddef.h>).
- pp_directive: parse_pp_int accumulates in u64 (signed overflow was UB).
- cg_adapter: fix a 1-byte OOB write in pcg_aux_clear (pad[5] on u8 pad[5]).
C backend + Toy harness (ELF / host-cc portability):
- c_target: emit `va_list x = {0}`, not `= 0` (va_list is a struct/array on
aarch64 / x86-64 Linux).
- toy/run.sh: L lane links hosted (kit cc -lc) on non-Darwin (ELF needs a crt
_start); prefer .objdump.elf goldens on non-Darwin; skip musttail+sret C
cases on gcc hosts (gcc rejects musttail on sret returns; clang accepts);
add -Wno-tentative-definition-incomplete-type (valid C; clang-14 pedantry).
- new fixtures: 3 .objdump.elf goldens + 2 .cbackend.gcc.skip.
Verified: glibc(clang) Toy 1378/2/39, musl(gcc) 1375/3/41. Remaining fails are
the tracked JIT-TLS .tdata-init (141_threadlocal_mutate) and a rare
archive-iterator heisenbug; both deliberately left as follow-ups.
Diffstat:
13 files changed, 217 insertions(+), 38 deletions(-)
diff --git a/lang/c/parse/cg_adapter.c b/lang/c/parse/cg_adapter.c
@@ -47,8 +47,7 @@ static void pcg_aux_clear(PcgLvAux* a) {
a->bit_signed = 0;
a->base_kind = PCG_LV_BASE_LOCAL;
a->is_subobject = 0;
- a->pad[0] = a->pad[1] = a->pad[2] = a->pad[3] = 0;
- a->pad[4] = a->pad[5] = 0;
+ a->pad[0] = a->pad[1] = a->pad[2] = a->pad[3] = a->pad[4] = 0;
}
static void pcg_stack_grow(Parser* p, u32 want) {
diff --git a/lang/cpp/pp/pp.c b/lang/cpp/pp/pp.c
@@ -336,6 +336,21 @@ static void pp_register_static_predefined(Pp* pp) {
pp_define(pp, "__ATOMIC_RELEASE", "3");
pp_define(pp, "__ATOMIC_ACQ_REL", "4");
pp_define(pp, "__ATOMIC_SEQ_CST", "5");
+ /* GNU `__extension__` is a pedantic-quiet prefix on non-standard constructs
+ * (statement exprs, anonymous structs, `long long` in C89, ...). kit's parser
+ * is permissive about those already and the keyword has no effect on parsing,
+ * so erase it. Needed on every OS, not just mingw: glibc's headers (e.g.
+ * <stdlib.h>'s `__extension__ typedef struct { ... } lldiv_t;`) use it
+ * pervasively, and musl's cleaner ISO-C headers simply never tripped it. */
+ pp_define(pp, "__extension__", "");
+ /* GCC keyword spellings of the C qualifiers/`signed`. GNU libc and the Linux
+ * kernel UAPI headers use these directly in GCC mode (e.g.
+ * <asm-generic/int-ll64.h>'s `typedef __signed__ char __s8;`). kit parses the
+ * canonical keywords; map the GCC spellings onto them. (`__restrict` is
+ * additionally a parser keyword alias for the configs that #undef the macro.) */
+ pp_define(pp, "__volatile__", "volatile");
+ pp_define(pp, "__const__", "const");
+ pp_define(pp, "__signed__", "signed");
}
/* OS-keyed predefined macros: the single place to extend per operating system.
@@ -419,19 +434,14 @@ static void pp_register_os_predefined(Pp* pp, KitTargetSpec target) {
* still needs to handle the syntax if/when the macro is removed.)
*/
pp_define(pp, "__declspec(x)", "");
- /* GNU `__extension__` is a pedantic-quiet wrapper around
- * non-standard constructs (statement exprs, anonymous structs).
- * kit's parser is permissive about those already; the keyword
- * has no effect on parsing, so we erase it. */
- pp_define(pp, "__extension__", "");
+ /* __extension__ is erased unconditionally in pp_register_static_predefined. */
/* __restrict / __restrict__: GCC-flavored alternates to the C99
* `restrict` keyword. kit parses `restrict` already; map the
* GCC spellings onto it. */
pp_define(pp, "__restrict", "restrict");
pp_define(pp, "__restrict__", "restrict");
- pp_define(pp, "__volatile__", "volatile");
- pp_define(pp, "__const__", "const");
- pp_define(pp, "__signed__", "signed");
+ /* __volatile__/__const__/__signed__ erased->canonical unconditionally in
+ * pp_register_static_predefined (GNU spellings glibc/UAPI headers use). */
/* MSVC calling-convention attributes. On x86_64 they're no-ops
* (every function uses the Win64 ABI) and on ARM64 likewise; on
* i386 they actually mean something but kit doesn't target it.
@@ -636,6 +646,13 @@ static void pp_register_target_predefined(Pp* pp) {
pp_define(pp, "__UINT64_C(c)", "c ## UL");
pp_define(pp, "__INTMAX_C(c)", "c ## L");
pp_define(pp, "__UINTMAX_C(c)", "c ## UL");
+ /* Suffix tokens (the form <stdint.h> uses to build INT64_C/etc.). GCC and
+ * clang predefine these; kit must too so kit-compiled TUs that include its
+ * freestanding <stdint.h> get the right-typed 64-bit constants. */
+ pp_define(pp, "__INT64_C_SUFFIX__", "L");
+ pp_define(pp, "__UINT64_C_SUFFIX__", "UL");
+ pp_define(pp, "__INTMAX_C_SUFFIX__", "L");
+ pp_define(pp, "__UINTMAX_C_SUFFIX__", "UL");
} else {
pp_define(pp, "__INTMAX_TYPE__", "long long");
pp_define(pp, "__UINTMAX_TYPE__", "unsigned long long");
@@ -645,6 +662,10 @@ static void pp_register_target_predefined(Pp* pp) {
pp_define(pp, "__UINT64_C(c)", "c ## ULL");
pp_define(pp, "__INTMAX_C(c)", "c ## LL");
pp_define(pp, "__UINTMAX_C(c)", "c ## ULL");
+ pp_define(pp, "__INT64_C_SUFFIX__", "LL");
+ pp_define(pp, "__UINT64_C_SUFFIX__", "ULL");
+ pp_define(pp, "__INTMAX_C_SUFFIX__", "LL");
+ pp_define(pp, "__UINTMAX_C_SUFFIX__", "ULL");
}
pp_define(pp, "__WCHAR_MAX__", wchar16 ? "65535" : "2147483647");
diff --git a/lang/cpp/pp/pp_directive.c b/lang/cpp/pp/pp_directive.c
@@ -62,7 +62,7 @@ void read_directive_line(Pp* pp, Tok** out_toks, u32* out_n) {
static i64 parse_pp_int(const char* s, size_t n) {
int base = 10;
size_t i = 0;
- i64 val = 0;
+ u64 val = 0; /* unsigned: #if arithmetic wraps on overflow, signed would be UB */
if (n >= 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) {
base = 16;
i = 2;
@@ -82,9 +82,9 @@ static i64 parse_pp_int(const char* s, size_t n) {
else
break;
if (d >= base) break;
- val = val * (i64)base + (i64)d;
+ val = val * (u64)base + (u64)d;
}
- return val;
+ return (i64)val;
}
/* Pre-pass: replace `defined X` / `defined ( X )` with a 0/1 pp-number,
diff --git a/lang/cpp/pp/pp_expand.c b/lang/cpp/pp/pp_expand.c
@@ -192,6 +192,17 @@ void do_define(Pp* pp, const Tok* line, u32 n) {
}
params[pn++] = line[i].v.ident;
++i;
+ /* GNU named variadic: `args...` — the named parameter itself collects
+ * the trailing arguments (the body refers to it by name rather than
+ * __VA_ARGS__). The variadic arg-collection below is positional on the
+ * last param, so we just mark the macro variadic and eat the ellipsis;
+ * the "'...' must be last" check still fires if a comma follows. Linux
+ * UAPI headers use this (e.g. <linux/stddef.h>'s __struct_group). */
+ if (i < n && line[i].kind == TOK_PUNCT &&
+ line[i].v.punct == P_ELLIPSIS) {
+ m->is_variadic = 1;
+ ++i;
+ }
} else {
compiler_panic(pp->c, line[i].loc, "#define: bad parameter list");
}
diff --git a/mk/bootstrap.mk b/mk/bootstrap.mk
@@ -22,20 +22,15 @@ BOOTSTRAP_TOOLS = cc ld ar ranlib as
BOOTSTRAP_HOST_MODE_CFLAGS =
BOOTSTRAP_HOST_MODE_LDFLAGS =
-# When kit itself is the compiler (stages 2/3) on a native Linux or FreeBSD
-# host, the hosted libc include + library dirs are only wired up once libc is
-# requested, so -lc must reach both the compile and link steps (it makes kit
-# resolve the hosted profile and add /usr/include + the libc search path).
-# macOS instead gets its system headers via the -isysroot in
-# HOST_SYSROOT_{C,LD}FLAGS, so this is POSIX-non-Darwin only; left empty
-# elsewhere the stage sub-makes re-derive the host default unclobbered.
-BOOTSTRAP_KIT_TOOLCHAIN_FLAGS =
-ifeq ($(HOST_OS),linux)
+# When kit itself is the compiler (stages 2/3), drive its hosted profile with a
+# plain -lc on both the compile and link steps, on every host. -lc makes kit
+# resolve the hosted libc and add its include + library dirs (the system
+# headers + libc search path), which is exactly what stages 2/3 need. This
+# replaces the host CC's HOST_SYSROOT_{C,LD}FLAGS (e.g. Darwin's -isysroot): kit
+# discovers the SDK / sysroot via its own hosted profile + host probe rather
+# than an -isysroot it doesn't interpret the same way. Stage1 is built by the
+# host CC (the outer make) and keeps env.mk's HOST_SYSROOT_* untouched.
BOOTSTRAP_KIT_TOOLCHAIN_FLAGS = HOST_SYSROOT_CFLAGS=-lc HOST_SYSROOT_LDFLAGS=-lc
-endif
-ifeq ($(HOST_OS),freebsd)
-BOOTSTRAP_KIT_TOOLCHAIN_FLAGS = HOST_SYSROOT_CFLAGS=-lc HOST_SYSROOT_LDFLAGS=-lc
-endif
# Portable SHA-256 display: macOS uses shasum, FreeBSD/Linux have sha256sum.
BOOTSTRAP_SHASUM = shasum -a 256
diff --git a/scripts/linux_bootstrap.sh b/scripts/linux_bootstrap.sh
@@ -0,0 +1,88 @@
+#!/usr/bin/env bash
+# Run the three-stage self-build (mk/bootstrap.mk) for aarch64-linux inside a
+# Linux container, from a non-Linux dev host (e.g. an Apple-silicon Mac).
+#
+# The bootstrap is a NATIVE build: `make bootstrap` keys off the container's
+# own uname (HOST_OS=linux + aarch64), so it selects the aarch64-linux ELF
+# toolchain and reaches its fixed point entirely inside the container. No
+# cross-compilation is involved -- the host only supplies podman + the repo.
+#
+# We run on the same arm64 Linux container family used by the hosted test
+# suite: alpine (musl) or debian (glibc). The container needs a seed C
+# compiler (clang) to build stage1; stages 2 and 3 are built by kit itself.
+#
+# usage: scripts/linux_bootstrap.sh [libc] [chain]
+# libc musl (default, alpine) | glibc (debian)
+# chain both (default) | debug (-O0) | release (-O1)
+#
+# Env overrides:
+# KIT_LINUX_BOOT_IMAGE container image (defaults per libc, below)
+# KIT_LINUX_BOOT_PLATFORM podman --platform (default linux/arm64)
+# KIT_LINUX_BOOT_TOY=1 also run the Toy corpus through the stage3 compiler
+#
+# The stage tree lands under build/linux-boot/<libc>/ on the host (gitignored),
+# so artifacts survive the run for inspection / per-object diffing.
+
+set -eu
+
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+LIBC="${1:-musl}"
+CHAIN="${2:-both}"
+PLATFORM="${KIT_LINUX_BOOT_PLATFORM:-linux/arm64}"
+
+case "$LIBC" in
+ musl) DEF_IMAGE="docker.io/library/alpine:3.23" ;;
+ glibc) DEF_IMAGE="docker.io/arm64v8/debian:bookworm-slim" ;;
+ *) echo "linux_bootstrap: unknown libc '$LIBC' (want musl|glibc)" >&2; exit 2 ;;
+esac
+IMAGE="${KIT_LINUX_BOOT_IMAGE:-$DEF_IMAGE}"
+
+case "$CHAIN" in
+ both) TARGET="bootstrap" ;;
+ debug) TARGET="bootstrap-debug" ;;
+ release) TARGET="bootstrap-release" ;;
+ *) echo "linux_bootstrap: unknown chain '$CHAIN' (want both|debug|release)" >&2; exit 2 ;;
+esac
+
+TOY="${KIT_LINUX_BOOT_TOY:-0}"
+BUILD_DIR="build/linux-boot/$LIBC"
+
+# In-container provisioning + build. The package sets give: a seed clang/lld,
+# make, the libc dev headers, binutils (ar/ranlib used by the host stage1
+# link), the ASan/UBSan runtime + unwinder for the -O0 stage1, and perl (the
+# `shasum` the bootstrap recipe prints with). detect_leaks=0 because kit is
+# arena-allocated and never frees -- LeakSanitizer (absent on the macOS
+# reference host, which is why this only bites on Linux) would otherwise abort
+# every stage1 cc invocation.
+case "$LIBC" in
+ musl)
+ PROVISION='apk add --no-cache clang lld make musl-dev binutils compiler-rt libgcc perl-utils bash >/dev/null'
+ ;;
+ glibc)
+ PROVISION='export DEBIAN_FRONTEND=noninteractive; apt-get update -qq >/dev/null && apt-get install -y -qq clang lld make libc6-dev binutils perl >/dev/null'
+ ;;
+esac
+
+read -r -d '' REMOTE <<EOF || true
+set -eu
+$PROVISION
+cd /work
+echo "=== linux_bootstrap: \$(uname -m) / $LIBC / $TARGET ==="
+clang --version | head -1
+make $TARGET BUILD_DIR='$BUILD_DIR' CC=clang AR=ar
+if [ "$TOY" = "1" ]; then
+ for m in debug release; do
+ s3="$BUILD_DIR/\$m/bootstrap/stage3/kit"
+ [ -x "\$s3" ] || continue
+ echo "=== Toy corpus through \$m stage3 ==="
+ KIT="\$(pwd)/\$s3" test/toy/run.sh || true
+ done
+fi
+echo "=== linux_bootstrap: DONE $TARGET ==="
+EOF
+
+exec podman run --rm --platform "$PLATFORM" \
+ -e ASAN_OPTIONS=halt_on_error=1:abort_on_error=1:detect_leaks=0 \
+ -e UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 \
+ -v "$ROOT":/work:Z \
+ "$IMAGE" sh -c "$REMOTE"
diff --git a/src/arch/c_target/c_emit.c b/src/arch/c_target/c_emit.c
@@ -392,14 +392,18 @@ static void c_grow_local_table(CTarget* t, u32 needed) {
}
/* Emit the trailing `__attribute__((unused)) = INIT;` for a local decl of
- * type `ty`. Scalars get `= 0` (readable); aggregates get `= {0}` (which is
- * the only form that compiles for record/array). */
+ * type `ty`. Scalars get `= 0` (readable); aggregates get `= {0}` (the only
+ * form that compiles for record/array). va_list also takes `= {0}`: the host's
+ * <stdarg.h> va_list is an aggregate (struct/array) on common ABIs (aarch64,
+ * x86-64 SysV) where `= 0` is invalid, and `= {0}` is also valid for the
+ * pointer form (e.g. Apple), so it is the portable choice. */
static void c_emit_zero_init(CTarget* t, KitCgTypeId ty) {
const CgType* cgt = ty ? cg_type_get(t->c, api_unalias_type(t->c, ty)) : NULL;
- int is_aggregate = cgt && (cgt->kind == KIT_CG_TYPE_RECORD ||
- cgt->kind == KIT_CG_TYPE_ARRAY);
- cbuf_puts(&t->decls, is_aggregate ? " __attribute__((unused)) = {0};\n"
- : " __attribute__((unused)) = 0;\n");
+ int braced = cgt && (cgt->kind == KIT_CG_TYPE_RECORD ||
+ cgt->kind == KIT_CG_TYPE_ARRAY ||
+ cgt->kind == KIT_CG_TYPE_VARARG_STATE);
+ cbuf_puts(&t->decls, braced ? " __attribute__((unused)) = {0};\n"
+ : " __attribute__((unused)) = 0;\n");
}
void c_ensure_local(CTarget* t, CLocal r, KitCgTypeId type) {
diff --git a/test/toy/cases/122_data_entsize.objdump.elf b/test/toy/cases/122_data_entsize.objdump.elf
@@ -0,0 +1,4 @@
+.rodata.kit.merge
+MERGE
+STRINGS
+entsize=1
diff --git a/test/toy/cases/127_switch_forced_jump_table.objdump.elf b/test/toy/cases/127_switch_forced_jump_table.objdump.elf
@@ -0,0 +1,2 @@
+.rodata
+.Lkit_jt
diff --git a/test/toy/cases/36_musttail_sret.cbackend.gcc.skip b/test/toy/cases/36_musttail_sret.cbackend.gcc.skip
@@ -0,0 +1 @@
+gcc tail-call checker rejects __attribute__((musttail)) on an sret-returning call ('return value used after call'); clang honors it. C backend emit is clang-portable only here.
diff --git a/test/toy/cases/37_tail_sret.cbackend.gcc.skip b/test/toy/cases/37_tail_sret.cbackend.gcc.skip
@@ -0,0 +1 @@
+gcc tail-call checker rejects __attribute__((musttail)) on an sret-returning call ('return value used after call'); clang honors it. C backend emit is clang-portable only here.
diff --git a/test/toy/cases/62_decl_data_attrs.objdump.elf b/test/toy/cases/62_decl_data_attrs.objdump.elf
@@ -0,0 +1,5 @@
+2**4
+w F .text.hot
+decorated_alias
+g C *UND*
+tentative
diff --git a/test/toy/run.sh b/test/toy/run.sh
@@ -70,6 +70,27 @@ TOY_OPT_LEVELS="${KIT_OPT_LEVELS:-0 1}"
HOST_CC="${CC:-cc}"
PAR="${KIT_TOY_PARALLEL:-1}"
+# Is the host C compiler gcc (vs clang)? GCC's tail-call checker rejects the
+# c_target's `__attribute__((musttail)) return <sret-call>` ("cannot tail-call:
+# return value used after call") where clang honors it, so musttail+sret cases
+# carry a `.cbackend.gcc.skip` sidecar that the C lane respects only on gcc.
+case "$($HOST_CC --version 2>&1 | head -n1)" in
+ *clang*) TOY_HOST_CC_IS_GCC=0 ;;
+ *) TOY_HOST_CC_IS_GCC=1 ;;
+esac
+
+# Lane L links the kit object and runs the result natively. A freestanding
+# `kit ld` (no crt) is runnable where the format drives the entry to `main`
+# directly (Mach-O's LC_MAIN), but a native ELF executable needs a crt-provided
+# `_start`, so on ELF hosts we drive the link through `kit cc -lc` (kit's hosted
+# profile pulls the crt that calls main). kit ld is still exercised -- cc just
+# invokes it with the crt/libc inputs. Override with KIT_TOY_L_HOSTED=0|1.
+case "$(uname -s)" in
+ Darwin) TOY_L_HOSTED_DEFAULT=0 ;;
+ *) TOY_L_HOSTED_DEFAULT=1 ;;
+esac
+TOY_L_HOSTED="${KIT_TOY_L_HOSTED:-$TOY_L_HOSTED_DEFAULT}"
+
# The engine's KIT_TEST_FILTER drives discovery; honor the positional filter.
export KIT_TEST_FILTER="$FILTER"
@@ -128,6 +149,13 @@ kit_lane_L() {
local cc_err="$KIT_WORK/cc.err" ld_err="$KIT_WORK/ld.err"
local dump="$KIT_WORK/objdump.out" dump_err="$KIT_WORK/objdump.err"
local dump_exp="${KIT_SRC%.toy}.objdump"
+ # The .objdump goldens encode section/symbol spellings, which differ by
+ # object format (Mach-O `__TEXT,__const` vs ELF `.rodata`...). On non-Darwin
+ # (ELF) hosts, prefer a `.objdump.elf` sidecar when present; fall back to the
+ # Mach-O golden otherwise so cases without an ELF variant still run.
+ if [ "$(uname -s)" != "Darwin" ] && [ -f "${KIT_SRC%.toy}.objdump.elf" ]; then
+ dump_exp="${KIT_SRC%.toy}.objdump.elf"
+ fi
local link_skip="${KIT_SRC%.toy}.link.skip"
local pattern missing
@@ -173,13 +201,21 @@ kit_lane_L() {
fi
fi
- if ! "$KIT" ld "$obj" -o "$exe" > "$KIT_WORK/ld.out" 2> "$ld_err"; then
- kit_fail "$label" "kit ld failed"
- sed 's/^/ | /' "$ld_err"
- return
+ if [ "$TOY_L_HOSTED" = "1" ]; then
+ if ! "$KIT" cc "$obj" -lc -o "$exe" > "$KIT_WORK/ld.out" 2> "$ld_err"; then
+ kit_fail "$label" "kit cc -lc link failed"
+ sed 's/^/ | /' "$ld_err"
+ return
+ fi
+ else
+ if ! "$KIT" ld "$obj" -o "$exe" > "$KIT_WORK/ld.out" 2> "$ld_err"; then
+ kit_fail "$label" "kit ld failed"
+ sed 's/^/ | /' "$ld_err"
+ return
+ fi
fi
if [ -s "$ld_err" ]; then
- kit_fail "$label" "kit ld wrote stderr"
+ kit_fail "$label" "kit link wrote stderr"
sed 's/^/ | /' "$ld_err"
return
fi
@@ -476,6 +512,11 @@ kit_lane_C() {
kit_skip "$label" "$(head -n1 "$cbackend_skip")"
return
fi
+ local cbackend_gcc_skip="${KIT_SRC%.toy}.cbackend.gcc.skip"
+ if [ "$TOY_HOST_CC_IS_GCC" = 1 ] && [ -e "$cbackend_gcc_skip" ]; then
+ kit_skip "$label" "$(head -n1 "$cbackend_gcc_skip")"
+ return
+ fi
local out_c="$KIT_WORK/$KIT_BASE.kit.c"
local out_bin="$KIT_WORK/$KIT_BASE.cbackend.bin"
local emit_err="$KIT_WORK/c.emit.err"
@@ -499,7 +540,14 @@ kit_lane_C() {
fi
# Fixtures wrap their i64 main in an `fn main(): i32` thunk so the emitted
# `int32_t main(void)` satisfies the host C compiler's main-return-type check.
- if ! $HOST_CC -std=gnu99 -Wall -Wextra -Werror "$out_c" -o "$out_bin" \
+ # -Wno-tentative-definition-incomplete-type: the c_target forward-declares a
+ # data global as `static const struct __kit_data_g g;` before defining that
+ # per-global struct later in the same TU. It is valid C (g is completed by
+ # end of TU; clang>=15 and gcc accept it), but clang 14's pedantic warning
+ # rejects the forward decl under -Werror. clang has the flag; gcc ignores the
+ # unknown -Wno- silently, so this is safe across host toolchains.
+ if ! $HOST_CC -std=gnu99 -Wall -Wextra -Werror \
+ -Wno-tentative-definition-incomplete-type "$out_c" -o "$out_bin" \
> "$KIT_WORK/c.cc.out" 2> "$cc_err"; then
kit_fail "$label" "host cc rejected emitted source"
sed 's/^/ | /' "$cc_err"