kit

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

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:
Mlang/c/parse/cg_adapter.c | 3+--
Mlang/cpp/pp/pp.c | 37+++++++++++++++++++++++++++++--------
Mlang/cpp/pp/pp_directive.c | 6+++---
Mlang/cpp/pp/pp_expand.c | 11+++++++++++
Mmk/bootstrap.mk | 21++++++++-------------
Ascripts/linux_bootstrap.sh | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/arch/c_target/c_emit.c | 16++++++++++------
Atest/toy/cases/122_data_entsize.objdump.elf | 4++++
Atest/toy/cases/127_switch_forced_jump_table.objdump.elf | 2++
Atest/toy/cases/36_musttail_sret.cbackend.gcc.skip | 1+
Atest/toy/cases/37_tail_sret.cbackend.gcc.skip | 1+
Atest/toy/cases/62_decl_data_attrs.objdump.elf | 5+++++
Mtest/toy/run.sh | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++------
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"