← 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

2026-08-09 (Track B): the -c wall is CLEARED — and it was suspect #2

The wall recorded above — "tcc -v works; tcc_bin -c hello.c SEGFAULTS" — no longer reproduces. Verified end to end against the pinned compiler:

$ pxx -Ilib/crtl/include -Ilib/crtl/src -Ilibrary_candidates/tcc tcc.c -> tcc_bin
$ ./tcc_bin -v
tcc version 0.9.28rc (x86_64 Linux)
$ ./tcc_bin -c -I.../include -o hello.o hello.c     # a loop summing 1..10
$ gcc -o hello_linked hello.o && ./hello_linked
sum=55

So a pxx-built tcc compiles C to a valid object file, and gcc links it into a working binary.

It was suspect #2 on that list: "struct jmp_buf passed BY VALUE where tcc treats jmp_buf as array→pointer (main_jb into _tcc_setjmp)". The mechanism was narrower than "needs the array-typedef fix", though: lib/crtl declared longjmp as a function-like MACRO only, so tcc's setjmp(_tcc_setjmp(s1, jb, f, longjmp)) — which passes longjmp as a function POINTER — hit an undeclared identifier. Real longjmp/_longjmp/siglongjmp functions now exist (lib/crtl/src/setjmp.c), forwarding to __pxx_longjmp. By-value is safe because longjmp only READS the buffer. C 7.13 required this anyway: setjmp may be a macro, longjmp shall be a function.

Suspect #3 answered. The list asked of environ: "resolved how? verify". It is NOT resolved — the build emits

warning: undeclared identifier 'environ' used as value (treated as 0)

so char **envp = environ; silently becomes NULL. Recorded as a live crtl gap on [[feature-crtl-implement-libc-assumptions]]. It did not stop -c from working, but anything reading the environment through it gets nothing.

Still open: the second recorded wall, linking — tcc: error: file 'libtcc1.a' not found. That is tcc's own runtime archive, a bootstrap artefact it builds from lib/*.c; the -c path above deliberately sidesteps it. Suspect #1 (mmap/mprotect stubs for tcc_relocate) is untested and only matters for -run.

Second wall CLEARED the same day: linking works, libtcc1.a builds

The other recorded wall — "linking — 'error: invalid archive' on libtcc1.a" — is also gone. libtcc1.a is not something we were missing; it is tcc's own runtime archive, and the pxx-built tcc builds it itself:

$ for f in libtcc1 stdatomic builtin va_list dsohandle; do tcc_bin -c lib/$f.c; done
$ for f in alloca alloca-bt atomic;                     do tcc_bin -c lib/$f.S; done
$ ar rcs libtcc1.a *.o          # 8 objects, 39 KB

Note it assembled the .S files too, not just the C.

Then the full compile-AND-LINK path:

$ ./tcc_bin -B<tccdir> -I<tccdir>/include -o hello_full hello.c
$ ./hello_full
sum=55
$ file hello_full
ELF 64-bit LSB executable, x86-64, dynamically linked

And a harder program through the same path — qsort with a comparator function pointer, strcpy, sprintf into a buffer, puts:

sorted: 1 3 5 7 9

So both walls this ticket recorded are cleared, and a tcc built by pxx is a working C compiler end to end: preprocess, compile, assemble, link, run.

libtcc1.a is a BUILD ARTEFACT and lives in the gitignored library_candidates/tcc/ beside the sources — nothing entered the repo.

Remaining, untested rather than known-broken: tcc -run (suspect #1 on the list above, mmap/mprotect stubs for tcc_relocate), and the self-host fixpoint the benchmark note names — a pxx-built tcc compiling tcc, gen2 vs gen3 byte-identical, which is the hard correctness target.

-run (suspect #1) — mmap fixed, but -run is limited by symbol resolution

The mmap/mprotect stubs were real and are fixed (PAL PalMmapAnonProt / PalMprotect, crtl serving anonymous mappings; test/cmman_jit_exec_pages.c pins the JIT shape against gcc). With that in, tcc -run executes: -run ret7.c exits 7, -run say.c exits 3.

What it cannot do is call host libc. Measured: write(1,...) prints; puts/printf/fputs/fwrite are all silent while RETURNING success; a genuinely undefined symbol errors properly.

Cause, and it is not ours to fix in crtl:

So -run has almost nothing to bind against, and write works only because tcc emits it as a direct syscall. Making -run generally useful needs pxx to emit a symbol table (a Track A/C ELF question), or tcc built with the dynamic-linking path.

-c and full -o linking are unaffected and both work end to end.