C corpus: bring up tcc (TinyCC) as a real-world multi-file C target
- Type: feature (C frontend corpus). Track C.
- Opened: 2026-07-07, after zlib landed byte-identical to gcc.
- Depends on: the zlib bring-up method (feature-c-corpus-zlib, done).
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)
- Add
fetch_tccto tools/install_lib_candidates.sh (github.com/TinyCC/tinycc, pin a release commit; vendor under library_candidates/tcc, gitignored like the others). - tcc needs a generated
config.h/tccdefs_.h— either vendor a prebuilt one or generate with the host toolchain once; document in PROVENANCE.md. - 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 —
#undefany private macro that collides with a later file's identifier (zlib needed#undef COPY). Audit gzguts-style guardless headers. - 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
- fetch_tcc added to tools/install_lib_candidates.sh (TCC_URL/COMMIT pinned to
a338258d, TinyCC mob). Setup that works:
./configure(config.h) +make tccdefs_.h(c2str.exe from conftest.c) — both run at fetch time with host gcc. - pxx PARSES most of libtcc.c (the amalgam core, 10k+ lines) with
-Ilib/crtl/include -Ilib/crtl/src -Ilibrary_candidates/tcc. - FIRST BLOCKER:
libtcc.c:10545: call to undeclared function: __builtin_va_copy. pxx supports __builtin_va_start/va_arg/va_end but not __builtin_va_copy (copy a va_list). Likely a small add mirroring the va_start handling in ParseCPrimary (cparser.inc). File as its own Track C ticket + minimal repro, then continue the parse to surface the next blockers (expect a cascade like zlib). - NO runner/oracle yet: next is test/tcc/runner.c (unity include of the core .c)
- make test-tcc (gcc oracle: tcc compiling a hello.c). Watch for unity macro leaks (#undef as needed, cf. zlib COPY).
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).
MILESTONE 2026-07-07 (session 2): tcc COMPILES, LINKS, and RUNS -v
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):
- :22215
sizeof ((Stab_Sym*)0)->n_value— postfix->field/.fieldafter a parenthesized sizeof operand belongs to the operand (C 6.5.3). - :29062
TCCSem static rt_sem;— storage class / qualifier AFTER the type name (static/extern/inline/volatile/register/restrict/_Noreturn accepted anywhere in the specifier run). - :29139
(tcc_enter_state(s1),_tcc_error_noabort)("...")— comma expr yields the callee; CNodeProcSig recurses into AN_COMMA's right arm, callee stays the whole comma node so left-arm side effects run (AN_CALL_IND evaluates it). - :29181
int (*prog_main)(int,...), ret;— sibling declarators after an inline fn-ptr declarator (fall through into the multi-declarator loop; sibling base = the fn-ptr's return specifier). - :29232
__pxx_setjmp(&(_tcc_setjmp(...)))—&(pointer-valued call)(only our setjmp macro can produce it) yields the call value = glibc array-decay semantics. NOTE: proper fix is array typedefs (jmp_buf aslong[16]) — typedef array dimension is LOST today (sizeof=8 not 128); file separately. - :29264
} while (++p, f);— do-while condition is a full C expression (ParseCCommaExpr).
cpreproc.inc:
#undefnow kills ALL stacked entries of a name (repeated#define _tcc_error use_tcc_error_noabortfrom tcc.h's per-file re-include survived one tombstone and renamed the real_tcc_errorDEFINITION → "undefined symbol: _tcc_error").
crtl/PAL (Track B files):
- getcwd: full chain — SYS_getcwd in all 5 posix arch tables + PalBackendGetcwd (+ ESP stub) + PalGetcwd + __pxx_getcwd + unistd.h/unistd.c veneer.
- unlink (rides __pxx_remove), fdopen + fileno (stdio), mprotect (no-op stub next to the stub mmap), realpath (identity copy, no symlink walk), execvp (link-only stub, ENOENT), assert.c (__pxx_assert_fail had NO impl anywhere), signal.h grown a POSIX surface (sigset_t/siginfo_t/stack_t/struct sigaction + sigemptyset/sigaddset/sigprocmask/sigaction/sigaltstack — bit-ops real, registration stubs) + crtl-own sys/ucontext.h (x86-64 glibc gregs layout; stops the /usr/include host-header leak).
NEXT WALL: tcc -v works; tcc_bin -c hello.c SEGFAULTS
Runtime arc, not parse. Suspects, in order:
- 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.
- struct jmp_buf passed BY VALUE where tcc treats jmp_buf as array→pointer (main_jb into _tcc_setjmp) — needs the array-typedef fix.
- environ is referenced (
char **envp = environ;) — resolved how? verify. - 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)
- tcc self-compile: 0.09s (and SELF-HOSTS: gen2->gen3 byte-identical, so a pxx-built tcc has a hard correctness fixpoint to hit)
- gcc -O0: 1.4s · gcc -O2: 8.1s · pxx: 27.9s (10.4s of that = cpreproc alone; Pascal path compiles 88k lines in 6.8s, so the C FRONT END is the cost)
- note: pxx also assembles+links AND compiles the whole libc-free runtime (crtl/PAL/softfloat) from source per invocation — others link prebuilt libs.
- Target benchmark once tcc runs: tcc-by-pxx vs tcc-by-gcc SELF-COMPILE time (measures generated-code quality, not compile speed).
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)):
- ir.inc IRPointerStride: no AN_DEREF case —
++*(p)(p T**) and*(&x)stepped 1 BYTE. Every function-macro expansion read misaligned tokens (first as segfault in sym_link via garbage v; after block-scope fix, as silent bad tokens 0x5000005 / "got '(null)'"). - cparser CNodePointeeTk: no AN_DEREF case —
**(&mp)loaded 8 bytes; comparisons saw garbage high half; printf %d truncated it away (heisenbug). Also en route: C block scope (b186) after tcc's struct_decl shadow clobbered symbols; blue-paint markers (b185); >1KB printf; label-owns-statement. RESULT:tcc -Ematches gcc-built tcc byte-for-byte;tcc -cemits an ELF object BYTE-IDENTICAL to gcc-built tcc's for the same input. Regressions b185/b186/b187.
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):
- 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. - b190 —
&floatFieldkept the pointee's float tk; C float→int-param truncation cvttsd2si'd the ADDRESS (write_ldouble got s=0). - 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).
- b192 — narrow int casts were retag-only;
c == (char)c(tcc's imm8-fit check) true for 0x80 →cmp $0x80encoded 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):
- tcc-by-pxx self-compiles tcc.c: rc=0; its output (gen2) is a WORKING compiler (mini.c → exit 42).
- gen3 == gen4 byte-identical (pxx-lineage fixedpoint).
- g2 == p2 byte-identical: the gcc-lineage and pxx-lineage self-compile chains CONVERGE to the same binary.
- Known residual: gen2-level 52-byte diff (g1 vs p1) from the host
long-double model (pxx
long double= 8 bytes, gcc = 80-bit x87) — affects one .data.ro constant + layout, functionally nil (both gen2s produce identical gen3). Goes away only if pxx grows 80-bit long double.
Benchmarks (2026-07-07, 8-core box, watcher active — background load):
- tcc-by-gcc self-compile: 0.168s
- tcc-by-pxx self-compile: 0.156s ← generated-code quality on par
- pxx compiling tcc.c: 57.2s under load (prior quiet-box anchor: 27.9s) Verify recipe: g1==tcc_ref(tcc.c); p1==tcc_pxx(tcc.c); gN+1 = gN(tcc.c); cmp g2 p2.
Log
- 2026-07-07 — resolved, commit 116230b1.