← board

C corpus: bring up tcc (TinyCC) as a real-world multi-file C target

Goal

Compile a meaningful subset of TinyCC with pxx (libc-free, single-TU unity build) and run it, diffing against a gcc oracle — the next real-world C project after zlib. tcc is itself a C compiler, so "tcc compiles a hello.c" is a strong E2E bar.

Setup (to do)

  1. Add fetch_tcc to tools/install_lib_candidates.sh (github.com/TinyCC/tinycc, pin a release commit; vendor under library_candidates/tcc, gitignored like the others).
  2. tcc needs a generated config.h / tccdefs_.h — either vendor a prebuilt one or generate with the host toolchain once; document in PROVENANCE.md.
  3. Write test/tcc/runner.c that #includes the tcc core .c files as one TU (mirror test/zlib/runner.c), plus the crtl shims. Watch for the SAME unity-build macro leaks zlib hit — #undef any private macro that collides with a later file's identifier (zlib needed #undef COPY). Audit gzguts-style guardless headers.
  4. Add make test-tcc: gcc oracle (compile tcc's own files separately) vs the pxx unity runner; start with tcc compiling a trivial hello.c and comparing the emitted output / exit.

Method (proven on zlib)

test-tcc diff → each mismatch line = one bug → printf-instrument the vendored .c (throwaway, restore after) → trace to the exact byte/value → minimal repro vs gcc → isolate to ONE compiler primitive → fix in cparser/ir/ir_codegen with a regression (bXXX) → self-host byte-identical → drop/advance. Expect a cascade of general cfront bugs (declarators, initializers, macro corners) like zlib surfaced.

Gate

make test-tcc advances (tcc builds + runs a hello.c to a correct result); file each compiler bug it surfaces as its own Track C/A ticket with a minimal repro.

Started 2026-07-07 — fetch wired, first blocker found

Blocker 2 (2026-07-07): libtcc.c:11810

After va_copy landed, parse advances to :11810: unexpected token near sizeof file filename — a sizeof/declarator corner (looks like sizeof(<type>) where the operand tokenizes oddly, or an array-of-struct member). Reduce to a minimal repro and file as its own Track C ticket; then continue the cascade.

Blocker 3 (2026-07-07): libtcc.c:12370 — ldexpl (crtl/library gap)

After the unparenthesized-sizeof-field fix (b179), parse advances to :12370: call to undeclared function: ldexpl. This is a LIBRARY gap, not a cfront parse bug: ldexpl (ldexp for long double) is missing from lib/crtl math. tcc uses long-double float parsing. Track B: add ldexpl (+ likely other *l long-double math: strtold, etc.) to lib/crtl, or stub if long double is mapped to double. File as a Track B crtl ticket. The cfront cascade is now hitting crtl breadth gaps interleaved with parse bugs.

Blocker 4 (2026-07-07): libtcc.c:14377 — codegen ICE IR_UNSUPPORTED

After the long-double aliases, parse advances to :14377: Unsupported linear node in IR codegen! Kind=10 node=47 (Kind 10 = IR_UNSUPPORTED). A C construct lowers to IR_UNSUPPORTED — a C->IR lowering gap (Track C/A), deeper than the parse fixes. Reduce: bisect libtcc.c around that token region to the construct, minimal repro, file a Track C/A ticket. CLUE: the IR_UNSUPPORTED node has IRA=1 (=AN_INT_LIT), IRB=-1, IRC=-1, IRIVal=0 — i.e. a childless integer-literal 0 reached codegen as unsupported, which is odd (int literals always lower). Suspect a synthesized/ placeholder node from an unmodelled construct (tccgen.c IS in libtcc.c's TU and uses computed goto &&label / goto * — a GCC extension pxx doesn't lower; check if that's what emits the stray node). Needs instrumentation: print the AST/IR node origin, or bisect the source region. (Progress: tcc parse went 10545 -> 11810 -> 12370 -> 14377 via 3 cfront fixes + crtl aliases; now at a codegen lowering gap.)

Blocker 5 (2026-07-07): libtcc.c:14395 — ELF64_ST_VISIBILITY undeclared

After the multi-declarator-global fix (b180), parse advances to :14395: call to undeclared function: ELF64_ST_VISIBILITY. ELF64_ST_VISIBILITY is a MACRO from <elf.h> (#define ELF64_ST_VISIBILITY(o) ((o)&0x3)). tcc uses its own elf.h; pxx either doesn't find/parse it or the macro isn't defined in the TU, so the call looks like an undeclared function. Check tcc's elf.h include + whether pxx's cpreproc picked up the ELF*ST* function-like macros; IS bug-c-preproc-paste-rescan (NOT a new bug): ELF64_ST_VISIBILITY comes from ELFW(ST_VISIBILITY)(x) where ELFW(type)=ELF##64##_##type (tcc.h:397) — the paste result must rescan and consume the trailing (x). So tcc's next blocker is the parked paste-rescan rework (prio raised to 60). Progress: tcc parse 10545 -> 11810 -> 12370 -> 14377 -> 14395 (4 cfront fixes + crtl aliases).

After paste-rescan landed (bug-c-preproc-paste-rescan done), one session cleared blockers 6..N in a cascade; compiler/pascal26 -Ilibrary_candidates/tcc library_candidates/tcc/tcc.c out now produces a 1.75MB binary that prints tcc version 0.9.28rc (x86_64 Linux). Fixes, in order hit:

cfront (cparser.inc, regression b184 covers all):

cpreproc.inc:

crtl/PAL (Track B files):

NEXT WALL: tcc -v works; tcc_bin -c hello.c SEGFAULTS

Runtime arc, not parse. Suspects, in order:

  1. mmap/mprotect are stubs (mmap returns MAP_FAILED) — tcc_relocate needs real anonymous exec mappings. PAL has SYS_mmap already; bridge it (Track B) and make mprotect real.
  2. struct jmp_buf passed BY VALUE where tcc treats jmp_buf as array→pointer (main_jb into _tcc_setjmp) — needs the array-typedef fix.
  3. environ is referenced (char **envp = environ;) — resolved how? verify.
  4. Any of the ~30 fresh crtl paths (fdopen etc.) or a genuine miscompile — instrument with the zlib printf-diff method once 1-3 are clean.

Benchmark anchors (2026-07-07, same unity TU, this machine)

MILESTONE 2026-07-07 (session 2, later): pxx-built tcc COMPILES C

Two deep miscompiles found via tcc's TOK_GET macro (token stream = int* walked through int** and *(&p)):

NEXT WALL: linking — "error: invalid archive" on libtcc1.a AND

/usr/lib/.../libc_nonshared.a. tcc's archive loader (tcc_load_archive / ld_add_file magic check "!<arch>\n") misbehaves in the pxx build — suspect crtl fread/fseek on the .a or another miscompile in its header walk. After that: tcc -run, then the self-compile benchmark (anchors above).

MILESTONE: tcc self-compiles under pxx — chains converge byte-identical (2026-07-07, fable-ac)

Four compiler primitives fixed (b189-b192, commit 116230b1):

  1. b189 — local aggregate {0}/partial init never zero-filled the remainder (C99 6.7.8p21). struct scope f = {0} → garbage cl.s → goto-cleanup crash.
  2. b190 — &floatField kept the pointee's float tk; C float→int-param truncation cvttsd2si'd the ADDRESS (write_ldouble got s=0).
  3. b191 — struct int nb, *lv;: starred later declarator over non-pointer base → pointee unknown → 8-byte loads of int elements → sym_versions[2^32]
    • wrong glibc symbol versions (stdin@GLIBC_2.3).
  4. b192 — narrow int casts were retag-only; c == (char)c (tcc's imm8-fit check) true for 0x80 → cmp $0x80 encoded as sign-extended imm8 → miscompiled everything tcc built. Now truncate + re-extend (C 6.3.1.3).

Result chain (method: gdb bt → TU line via --dump-cpp → minimal repro vs gcc → fix ONE primitive → bXXX test):

Benchmarks (2026-07-07, 8-core box, watcher active — background load):

Log