← board

C corpus: chess engine — perft as a compiler-independent oracle

Why chess / why perft

A chess engine is pure math + bit tests + deep recursion + near-zero I/O — a different muscle than lua/sqlite/tcc (parsers, VMs, string churn). The killer feature is the oracle: perft (move-path enumeration to depth N) has canonical known-answer values that are compiler-independent — no gcc oracle build needed, the numbers ARE truth. It also cross-validates the Rust frontend: test_rust_chess_perft.rs already computes perft (20/400/8902) through the Rust path; a C engine hitting the same numbers triangulates C and Rust against one ground truth.

Canonical perft values (chessprogramming.org — DO NOT re-derive)

Depth budget: 1..5 for all six is a few seconds native; startpos perft(6)=119M is the heaviest single run (seconds). Keep the default gate to perft(1..4/5); perft(6) as an opt-in deep check.

The plan (mirror zlib/tcc)

  1. Vendor a compact, portable-C, perft-capable engine via tools/install_lib_candidates.sh (gitignored source + PROVENANCE.md w/ upstream commit). Candidates, best-fit first:
    • VICE (bluefever tutorial engine, GPL) — ships perft + the standard positions; portable C, mailbox 0x88-ish, no libc math surprises. First choice.
    • TSCP (~2500 LOC classic) — clean but perft not shipped; add a ~30-line movegen-walk driver.
    • micro-Max (one file, ~130 lines) — ultra-dense C, great stress but hard to debug; keep as a torture follow-up, not the first target. Prefer an engine whose movegen is legal-move (or that you drive to legal perft), so the counts match the canonical values exactly (pseudo-legal perft gives different numbers).
  2. Runner + oracle: test/chess/perft_main.c (or use the engine's own perft entry) that sets each FEN, runs perft(depth), prints pos depth count. make test-chess-perft compiles it with $(COMPILER) -Ilib/crtl/include -Ilib/crtl/src, runs, and compares every count to the canonical table above (byte/number-exact). NO gcc oracle needed (but a gcc build is a cheap sanity cross-check for the harness itself).
  3. Blocker loop (same as tcc): compile → run → any wrong perft count = a movegen / bit-op / recursion miscompile. Bisect to the construct, minimal repro vs the canonical number (or vs gcc for the isolated snippet), fix ONE thing, add a bXXX regression test wired into test-core, land green. Expect bit-shift/rotate, 64-bit mask, signed/unsigned edge, and array-of-struct movelist bugs — exactly the class sqlite/lua don't stress.

Gate

make test-chess-perft = every canonical perft count matches (startpos + Kiwipete + pos 3-6, depth 1..5). If the frontend/IR changed: make test + self-host byte-identical, then make stabilize && make pin (verify VERSION advanced). Cross targets confirm via Track T (watcher) — perft is deterministic, so a cross red = a real per-target bit bug. Land only green; regression tests per fix.

Landmines (from the tcc/00216 arc — DO NOT relearn)

[[feature-c-corpus-expansion]] · [[feature-c-corpus-duktape]] · [[project_c_compound_literals_done_00216_residual]]

COPY-PASTE KICKOFF PROMPT (fresh session)

You are Track A+C (compiler core + C frontend), on master, sole-A confirmed (you may self-resolve shared-internals changes). Task: land the next C-corpus rung — a chess engine perft corpus — per devdocs/progress/backlog/feature-c-corpus-chess.md (READ IT FIRST; the canonical perft values, engine choice, and loop are settled there — do not re-derive them).

VERIFIED CONTEXT (do not re-check): c-testsuite is 220/220/0, zlib/cjson/lua/sqlite green byte-identical, tcc self-compiles and the pxx/gcc lineages converge (g3==p3) — the C frontend is strong. Pinned binary is current (VERSION >= 189). The corpus ladder's next rung is chess because perft gives a COMPILER-INDEPENDENT known-answer oracle (the numbers in the ticket ARE truth) and it cross-validates the existing Rust chess perft.

Do this loop:

  1. git pull --rebase. Confirm 220/220 still (make test-c-conformance) and pinned binary current (cat stable_linux_amd64/default/VERSION).
  2. Vendor a compact, portable-C, perft-capable engine (VICE first choice; TSCP+own perft driver; micro-Max as torture follow-up) via tools/install_lib_candidates.sh — gitignored source + PROVENANCE.md (upstream commit). Prefer LEGAL-move perft so the counts match the canonical table exactly.
  3. Wire test/chess/perft_main.c (or the engine's perft entry) + make test-chess-perft: compile with ./compiler/pascal26 -Ilib/crtl/include -Ilib/crtl/src, run each FEN's perft(1..5), compare every count to the canonical table in the ticket. NO gcc oracle needed; a gcc build is just a harness sanity check.
  4. Any wrong perft count = a pxx miscompile (movegen/bit-op/64-bit-mask/recursion/ array-of-struct movelist), NEVER the engine (canonical numbers are proven). Use divide-perft (per-root-move subtotals) to bisect which move diverges → minimal repro vs the canonical number (or gcc for the isolated snippet) → fix ONE primitive → add test/cchess_*_bNNN.c (exit 42) wired into test-core → land green.
  5. GATE per fix: repro matches; make test-chess-perft advances; tools/testmgr.py --tier quick + self-host byte-identical (compiler changed → make stabilize then make pin, verify VERSION advanced + compiler==pinned); test-lua green; cross via Track T (tools/twatch.py --status). Commit each fix separately with its regression test; push each unit; update this ticket's log; board-md.

LANDMINES (from the tcc/00216 arc — in the ticket, do not relearn): no literal { or } in { } Pascal comments (self-host lexer desync); no ErrOutput/writeln debug in the byte-identical build; a 2-step converge = the comment-brace landmine not a reseed; make stabilize alone runs test-core (background it). When perft mismatches, instrument the divide, don't guess.

After chess lands, the next rung is [[feature-c-corpus-duktape]] (JS engine, GC + float).

RESOLUTION — 2026-07-09 (Track A+C, sole-A)

Landed GREEN, zero compiler fixes needed. The C frontend compiles the VICE engine's full legal-move perft path correctly on the first build — every canonical perft count matches, so the blocker loop never fired. Chess exercised a genuinely new muscle (64-bit bitboard masks, bit-shift/rotate movegen, deep recursion, array-of-struct movelists) and pxx got them all right out of the box.

Next rung: [[feature-c-corpus-duktape]].

Log