C desktop path — compile real portable C (tiny-regex → lua → sqlite)
- Type: feature (track-C milestone path)
- Status: DONE — resolved 2026-07-11. Whole M0–M5 arc complete. The
"only floating-point remains broken" note below is STALE (2026-06-28,
superseded); all float bug tickets landed in
done/and both lua and sqlite now run float correctly. - Reconciliation 2026-07-11 (Track E ticket maintenance — verified native,
pinned compiler
stable_linux_amd64/default/pinned):- M4 lua — GREEN.
make test-luaall 6 corpus programs PASS incl.numeric.lua(float:3.14,1.5+2.5,2^10,7/2,%.2f,math.sqrt, stdev). Fetchedlua-5.4.7viatools/install_lib_candidates.sh lua. - M5 sqlite — GREEN.
test/csqlite_extended_test.ccompiles + runs: CREATE/INDEX, plain + prepared-statement INSERT, transactions, float columns (balance 1500.5 → 2000.75), NULL handling, SELECT. Fetchedsqlite-3.46.0amalgamation; built libc-free againstlib/crtl. - Linked bringup tickets
task-sqlite-libc-free-runtime-bringupandbug-c-sqlite-suite-runtime-segfaultalready indone/; everybug-c-*float*ticket indone/. - Residual (tracked elsewhere, NOT blocking this ticket): sqlite
--threadsafeonly on x86-64/i386 (PAL atomics/clone not yet on arm32/aarch64/riscv32) — seetest-sqlite-threadsskip note in Makefile. File any new cross-target gap as its own Track A ticket.
- M4 lua — GREEN.
- Prior status (historical): backlog (active arc — pxx-compiled lua is FUNCTIONAL: control flow, recursion, closures, generic-for, string lib, table.sort, metatables + operator overloading, pcall/error).
- Session 2026-06-28 cleanup (Track A+B+C) — SQLite in-memory extended smoke
runs. The stale VdbeCursor/bitfield crash diagnosis was disproved by direct
full-amalgamation layout probes: PXX and GCC agree on
VdbeCursor(sizeof=120,uc=40,pKeyInfo=48) and nearby VDBE structs. The actual aggregate-query segfault was missing support for inline nested aggregate pointer fields (struct AggInfo_func { ... } *aFunc;) inParseCStructInto; the parser skipped*aFunc, sopAggInfo->aFuncresolved at offset 0 and loaded a bogus pointer. Fixed by parsing stars in that inline-aggregate branch and recording pointer-to-nested-record metadata. Guards:test/cinline_struct_ptr_field_b129.c;test/csqlite_extended_test.cnow completesCREATE/INSERT/SELECT/UPDATE/DELETE/COUNT/SUM/AVG/close. Also removed a leftoversqlite3RunParserdebug printf from the vendored candidate and addedlib/crtl/src/string.cleaf helpers (test/crtl_string_leaf_b130.c), so the SQLite unity binary no longer imports CRTL string helpers. Remaining dynamic imports are OS/VFS calls; tracked in [[task-sqlite-libc-free-runtime-bringup]]. - Session 2026-06-27e (Track A+C) — generic-for fixed; lua essentially
complete (minus float). Two more fixes (self-host byte-identical,
make testgreen):- FIX 7 (
cf53d915) —sizeof(*p)returned the pointer size (8), not the pointed-at type size. Thesizeofoperand starting with*fell through to the default. Added atkStarbranch (ParseCSizeof) resolving the pointee size (record→RecSize(PtrElemRec), else TypeSize(PtrElemTk)). lua'sOP_TFORCALLdoesmemcpy(ra+4, ra, 3*sizeof(*ra))(ra a 16-byte StackValue*); with sizeof mis-sized to 8 it copied 24 of 48 bytes, dropping the generic-for state+control →ipairs/pairs"bad argument to 'for iterator'". Test b79. - FIX 8 (
5071599f) — integer arithmetic dropped unsignedness.CBinResultTkcollapsed every non-float/non-Int64 result to signedtyInteger, so an inlinei - 1uwas tagged signed and(i - 1u) < asizecompiled to a SIGNED compare (0xFFFFFFFF read as -1 → wrongly< asize). Now follows C usual-arith-conversions (unsigned64 > signed64 > unsigned32 > signed32; char/short promote to signed int). This was lua'sfindindexbounds testi - 1u < asize, which loopedpairsforever. Test b80. - VERIFIED WORKING (pxx-compiled lua, comprehensive): numeric for / while /
if-else, recursion (fib(25)=75025), closures + upvalues, varargs
(
{...}),ipairs/pairsgeneric-for, string methods (:upper/:len/:sub/:gsub),table.sort+table.concat, **metatables__index/__addoperator overloading**,pcall/error,string.format(integer/string/hex),string.gmatch. All 10 stdlibs open.
- ONLY REMAINING GAP — floating point. Every float op yields the same
garbage denormal
3.95e-323(≈ bit pattern 8):print(3.14),1.5+2.5,2^10,%.2f,math.sqrt, float compares all wrong. The double VALUE is stored correctly in memory (a union.nread shows0x40091EB851…for 3.14), but probing shows*(long*)&dblreads only 32 bits (pointer-cast deref width), and a struct-by-value return containing adoublesegfaults — so the C-frontend float handling (double through casts / by-value aggregates / the lua_Number TValue union read-write) is the multi-faceted remaining workstream. Ties to the long-standing float gaps (bug-c-float-int-cast-and-spill,%fmath). Distinct from everything fixed this arc; pick up here next. - Also still open (minor): unsigned integer LITERAL suffix (
5u) tagged signed (lexer consumes suffix but records no unsignedness) — needs a token flag; lua unaffected (bounds use unsigned vars).
- FIX 7 (
M5 update (2026-06-27, session 5): global fn-ptr cast initializer fixed
Banked the in-lane sqlite runtime wall. A file-scope struct-array initializer
with a function-pointer cast field, e.g. sqlite's syscall table shape
{ "open", (syscall_ptr)posixOpen, ... }, now records the casted function name
as the same proc-address PendingInit used by a bare function-name initializer.
- FIX 5 (this commit) — global struct-array fn-ptr cast field init.
ParseCGlobalVarDecl's aggregate walker already handled pointer fields initialized from a bare function identifier (PendingInitKind=2), but(typedef_fnptr)function_namefell into the skip path and left the field zero/garbage. AddedCConsumeCastProcInitto recognize the cast wrapper and record the proc address. Testcglobal_struct_array_fnptr_cast_b98. - Verification:
make compiler/pascal26self-host byte-identical; b97 still passes; b98 passes. - sqlite rerun:
./compiler/pascal26 -Ilibrary_candidates/sqlite library_candidates/sqlite/sqlite3.c /tmp/sqlite3now reaches the existing header wall:pascal26:31615: error: call to undeclared function: fsync (). [[bug-c-crtl-missing-unistd-syscalls]] is now the next active wall; user has explicitly allowed taking this Track B/CRTL header ticket.
M5 update (2026-06-27, session 6): CRTL unistd fsync/sysconf prototypes
Took the now-authorized CRTL header wall. Added sqlite-needed declarations to
lib/crtl/include/unistd.h: fsync, sysconf, _SC_PAGESIZE, and
_SC_PAGE_SIZE. This is the faithful header-side fix; no C89 implicit
declaration policy change.
- FIX 6 (this commit) — missing unistd prototypes for sqlite.
Header prototypes now register libc extern imports for
fsyncandsysconf. Testcrtl_unistd_fsync_b99verifies the header resolves both names and that_SC_PAGE_SIZEaliases_SC_PAGESIZE. - Verification: b99 passes.
- sqlite rerun: sqlite advances past
fsyncat 31615 andsysconf(_SC_PAGESIZE)at 42642, then stops at a separate preprocessor conditional wall:pascal26:32926: error: unexpected tokenwith contextpLockingStyle posixIoMethods defined. Filed [[bug-c-preprocessor-defined-expression-sqlite]].
M5 update (2026-06-27, session 7): preprocessor directive-join fixed
The presumed defined(...) wall was a directive-joining bug, not a missing
defined evaluator. CPProcessText joined normal C lines with unbalanced
parentheses across following physical lines, including lines that began with
#if / #endif. sqlite's valid conditional fragment inside
if( pLockingStyle == &posixIoMethods ... ) became one invalid C line containing
#if defined(__APPLE__).
- FIX 7 (this commit) — don't join C continuation lines across directives.
When the next physical line starts with
#, the preprocessor now emits the current source line, processes the directive immediately, and lets conditional state handle following lines. Testcpreproc_defined_directive_join_b100covers both sqlite's shape andX && defined(Y)/!defined(Y). - Verification:
make compiler/pascal26self-host byte-identical; b100 passes;--dump-cppshows thepLockingStyle == &posixIoMethodscondition no longer contains the#if defined(...)text. - sqlite rerun: sqlite now advances to
pascal26:33288: error: call to undeclared function: getpid (). Filed [[bug-c-crtl-missing-getpid]].
M5 update (2026-06-27, session 8): getpid + ternary middle comma
Two small sqlite-advance fixes:
- FIX 8 (this commit) —
unistd.hdeclaresgetpid. Addedint getpid(void);to the CRTL header so sqlite's unix VFS resolves the libc import. Testcrtl_unistd_getpid_b101. - FIX 9 (this commit) — ternary middle arm allows comma expression. C's
?:middle arm is a full expression, so sqlite's macro-expandedcond ? (store), 1 : callmust parse the comma as part of the true arm. The parser now usesParseCCommaExprfor the middle arm. Testcternary_middle_comma_b102. - Verification:
make compiler/pascal26self-host byte-identical; b101 and b102 pass. - sqlite rerun: sqlite now advances to
Unsupported linear node in IR codegen! Kind=10 ... IRA=67;IRA=67isAN_TERNARY. Filed [[bug-c-sqlite-unsupported-ternary-ir]].
M5 update (2026-06-27, session 9): ternary pointer-array indexing
SQLite's unsupported AN_TERNARY IR wall reduced to:
MemPage *pOld = (nNew>nOld ? apNew : apOld)[nOld-1];
Both ternary arms are local arrays of MemPage *; C decays the selected arm to
a pointer value, then indexes that value. The old IR address path tried to take
the address of the AN_TERNARY expression itself and emitted
IR_UNSUPPORTED.
- FIX 10 (this commit) — computed pointer-value indexing. In C mode,
IRLowerAddress(AN_INDEX)now handles computed pointer bases such as ternary, call, assignment, and inc/dec expressions by usingIRLowerAST(base)as the base pointer value andIRPointerStride(base)for scaling. The C parser now preserves pointer depth/base/pointee metadata through pointer-valued ternary nodes so downstream indexing and->field resolution keep the right type. Testcternary_pointer_array_index_b103. - Verification:
make compiler/pascal26self-host byte-identical; b103 passes. - sqlite rerun: sqlite now advances past the btree
balance_nonrootternary and stops at anoffsetof-style array bound:char saveBuf[(sizeof(Parse)-((size_t)&(((Parse *)0)->sLastToken)))];withpascal26:91408: error: unexpected token. Filed [[bug-c-sqlite-offsetof-style-field-address-array-bound]].
M5 update (2026-06-27, session 10): offsetof-style const array bound
SQLite's next wall was the macro-expanded offsetof(Parse,sLastToken) in an
automatic array bound:
char saveBuf[(sizeof(Parse)-((size_t)&(((Parse *)0)->sLastToken)))];
CEvalConstExpr already folded sizeof(...) in array dimensions, but unary
& was unsupported in the constant-expression folder. It returned 0 without
consuming the field-address tokens, so the declarator still saw sLastToken
while expecting ].
- FIX 11 (this commit) — fold macro-expanded offsetof field addresses.
CEvalConstPrimarynow handles unary&for the narrowoffsetofmacro shape&(((T *)0)->field), resolvesTthroughParseCDeclType, and returnsRecFieldOffset(T, field)while restoring the parser's CType globals. Testcoffsetof_constexpr_array_b104. - Verification:
make compiler/pascal26self-host byte-identical; b20, b55, and b104 pass. - sqlite rerun: sqlite advances to
pascal26:105031: error: call to undeclared function: sqlite3OsDlSym (). The preprocessed source has declarations and a definition ofstatic void (*sqlite3OsDlSym(sqlite3_vfs *, void *, const char *))(void);, so the likely next wall is parsing/registering a C function whose return type is a function pointer. Filed [[bug-c-function-returning-function-pointer-prototype-sqlite]].
M5 update (2026-06-27, session 11): functions returning function pointers
SQLite's sqlite3OsDlSym declaration is a real function returning a function
pointer:
static void (*sqlite3OsDlSym(sqlite3_vfs *, void *, const char *))(void);
The C frontend's function-pointer declarator support covered variables,
parameters, fields, typedefs, and casts, but collapsed ret (*fn(params))(...)
into the same bucket as a function-pointer variable declaration. The top-level
scanner then skipped it, so later calls to sqlite3OsDlSym(...) were
undeclared.
- FIX 12 (this commit) — register
ret (*fn(params))(...)as a function.ParseCDeclTypenow distinguishesret (*var)(args)fromret (*fn(params))(args)and passes the real function's name + parameter metadata toParseCSubroutine.CTopLevelIsFuncroutes that shape through the subroutine path. Testcfn_return_fnptr_b105. - Verification:
make compiler/pascal26self-host byte-identical; b95, b97, and b105 pass. - sqlite rerun: sqlite advances to
pascal26:33764: error: @ on external routine not supported; wrap it in a local routine (). The triggering source isx = (void(*(*)(void*,const char*))(void))dlsym;inunixDlSym, wheredlsymis a libc import used as a function-pointer value. Filed [[bug-c-external-function-address-dlsym-sqlite]].
M5 update (2026-06-27, session 12): external function address values
SQLite's unixDlSym stores the imported libc routine dlsym in a function
pointer variable before calling through it:
void (*(*x)(void*,const char*))(void);
x = (void(*(*)(void*,const char*))(void))dlsym;
return (*x)(p, zSym);
The C frontend already represented the bare function name as a proc-address
node, but x86-64 codegen rejected @ on external routines because external
calls normally jump through a dynamic GOT slot without exposing that slot's
resolved value as an expression.
- FIX 13 (this commit) — load external proc addresses from the dynamic GOT.
IR_PROCADDRnow permits imported routines on x86-64.EmitExternalProcAddrregisters/reuses the external's GOT slot and emits a patchedmov rax, qword ptr [absolute address], sharing the existing dynamic-call fixup table. Non-x86-64 targets keep the old explicit rejection until their backends grow equivalent address-load sequences. Testcexternal_func_addr_b106. - Verification:
make compiler/pascal26self-host byte-identical; b106 passes; fullmake testpasses including fixed-point self-host and threadsafe self-host. - sqlite rerun: sqlite advances past
unixDlSymand now reachespascal26:139609: error: expected C expression ()insqlite3_completeat a block-scopestatic const u8 trans[8][8] = { ... };. Filed [[bug-c-local-static-const-multidim-array-init-sqlite]].
M5 update (2026-06-27, session 13): local static 2D initializer
SQLite's sqlite3_complete uses a block-scope transition table:
static const u8 trans[8][8] = {
{ 1, 0, 2, 3, 4, 2, 2, 2, },
...
};
state = trans[state][token];
The local declaration parser consumed only the first [], leaving the second
dimension in the statement stream. After dimension parsing, the nested brace
rows also needed flattening, and chained symbol subscripts needed to lower to one
flat N-D index.
- FIX 14 (this commit) — local C multidimensional ordinal arrays.
ParseCLocalDeclASTnow consumes all local array dimensions, allocates flattened row-major storage, recordsSymArrNDimsmetadata, and flattens nested brace initializers for ordinal arrays.ParseCPostfixnow foldsa[i][j]on symbol-backed N-D arrays through the existingBuildFlatNDIndexhelper. Testclocal_static_const_2d_init_b107. - Verification:
make compiler/pascal26self-host byte-identical; b107 passes. - sqlite rerun: sqlite advances past
sqlite3_completeand now reachespascal26:140250: error: unexpected token ()atLOGFUNC_t xLog = __builtin_va_arg(ap, LOGFUNC_t);, whereLOGFUNC_tis a block-scope function-pointer typedef. Filed [[bug-c-va-arg-local-fnptr-typedef-sqlite]].
M5 update (2026-06-27, session 14): local typedef in va_arg
SQLite works around compilers that reject function-pointer types directly inside
va_arg by declaring a block-scope typedef:
case 16: {
typedef void(*LOGFUNC_t)(void*,int,const char*);
LOGFUNC_t xLog = __builtin_va_arg(ap, LOGFUNC_t);
...
}
The file-scope typedef parser already registered function-pointer typedefs, and
__builtin_va_arg already parses its second argument with ParseCDeclType.
The missing piece was that block-scope typedef declarations were not treated
as declarations/statements, so LOGFUNC_t was never registered.
-
FIX 15 (this commit) — block-scope typedef statements.
ParseCStatementASTnow recognizes localtypedef, callsParseCTypedef, and emits an empty statement node. That reuses the existing function-pointer typedef metadata path, sova_arg(ap, LOGFUNC_t)parses as a pointer-sized value with a callable signature. Testcva_arg_local_fnptr_typedef_b108. -
Verification:
make compiler/pascal26self-host byte-identical; b108 passes. -
sqlite rerun: sqlite advances past
sqlite3_configcase 16 and now reachesUnsupported linear node in IR codegen! Kind=10 ... IRA=1atreturn openDatabase(zFilename, ppDb, 0x00000002 | 0x00000004, 0);.IRA=1isAN_INT_LIT; the likely issue is literal0passed to theconst char *zVfsparameter being lowered as an address instead of a null pointer value. Filed [[bug-c-null-pointer-literal-call-arg-sqlite]]. -
Session 2026-06-27d (Track A+C) — control flow + lexer fixed; lua runs real programs. Two more fixes (self-host byte-identical,
make testgreen):- FIX 5 (
62c88498) — global ordinal array with constant-EXPRESSION elements was zero-initialized.CBraceFlatIntInitCountAt(cparser.inc) bailed on the firsttkLParen/tkIdent, so an init list whose elements were const expressions (parens, shifts, enum/macro constants) fell through to BSS-zero. The materializer already folds withCEvalConstExpr; only the gate was too strict — relaxed to accept const-expr tokens (still bail on tkBegin / tkString). This was lua'sconst lu_byte luaP_opmodes[] = { opmode(...), ...}(each elem =(((mm)<<7)|...|(iABC))). Zeroed →getOpMode/testTModereturned 0 for every opcode →getjumpcontrolmis-identified jump controls →negatecondition'sSETARG_kcleared bit 15 of freshOP_JMPs, corrupting their sJ offset and crashing lua's compiler on every real conditional. (This was the exact bit-15 corruption traced in 27c.) Test b77. Unblocked if/else + while. - FIX 6 (
e6459310) — C lexer didn't decode\a \b \f \vescapes, falling through to the literal letter ('\f'=='f'== 102, not 12). lua's lexercase '\f':then matched the letterf, eating the leadingfof every Lua identifier starting with it:"for"lexed as"or"→ reserved table → TK_OR → all numeric for-loops broke (the "near 'or'"/"near '='" errors). Added \a=7 \b=8 \f=12 \v=11 (+ cross-quote) to both clexer escape handlers (clexer.inc string @~318, char @~348). Test b78. Unblocked for / false / function. - VERIFIED WORKING (pxx-compiled lua,
luaL_dostring):print, int arithmetic,if/else,while, numericfor(sum 1..10 = 55), recursion (fib(20) = 6765), table set/get/constructor/#,string.gmatchgeneric-for,string.format(%d %s %x). 10 stdlibs open. - NEXT BLOCKER — 3-value generic-for protocol (
ipairs/pairs).for i,v in ipairs({8,9}) do … end→ runtimebad argument #2 to 'for iterator' (number expected, got nil);pairs→#1 … (table expected, got string). The generic-forf(s, ctl)call passes the wrong values as the states/ controlctlregisters — they're shifted/garbage.gmatchworks because its closure ignores s/ctl. So it's the OP_TFORCALL/OP_TFORLOOP register setup (lparserforlist/forbodyor lvm) miscompiled by pxx — a register-window / call-arg layout bug, distinct from the prior fixes. Also still open: float2^8/%fgarbage (known float gap).
- FIX 5 (
-
Session 2026-06-27c (Track A+C) — pxx-compiled lua RUNS real Lua. Committed
b00ecae5. The GC allgc-cycle blocker from 27b was the(*p)->fielddouble-deref bug: FIX 4 —*p(anAN_DEREFwhose value is a pointer, e.g.pof typeT**) was not recognized as a pointer base, so(*p)->flowered as a.on the lvalue*pand applied the field offset to the slot holding*p(read/addressedp's own storage); non-zero field offsets collapsed to 0. AddedAN_DEREFcases toCNodeIsPointer(deref is a pointer iff its pointee tag = tyPointer) andCNodePtrElemRec(elem rec = the inner pointer's base record). That fixed lua's GC list-walkfor (p=&g->allgc; *p!=o; p=&(*p)->next){}(was the infinite loop inluaC_checkfinalizer). Testcderef_arrow_field_b76. Self-host byte-identical.- WORKS now (verified via a
luaL_dostringharness inpxx_hostamalg.c, one TU):print, integer arithmetic (+ - * // ^int), string concat..,string.format(%d %s %x), table constructor + index +#,tostring,math.floor/max,if-without-else,do … end, multiple sequential chunks.luaL_openlibsloads all 10 stdlibs clean. - NEXT BLOCKER — lua's own bytecode compiler crashes on real conditionals.
if … else … end,while cond do … end,for i=…all fail. Precise: the failure is atluaL_loadstring(COMPILE time), not run —if 3>2 then a else b endsegfaults inside lua's parser/lcodebefore execution.while false do end(constant-folded cond) compiles+runs but leaves latent state corruption that segfaults the NEXTloadstring. Constructs that work share "no real jump back-patch"; the broken ones all go throughluaK_patchlist/luaK_concat/patchtestreg(jump-list patching) +enterblock/leaveblock. Ruled OUT: instruction arg encoding —GETARG/SETARG_sBx,GETARG/SETARG_sJ,CREATE_sJ, and theNO_JUMP=-1sentinel all round-trip correctly in isolation (tested 0/±small/±large at the realPOS_sJ=7,SIZE_sJ=25/POS_Bx=15,SIZE_Bx=17). So next: instrument lualcode.cjump-patch +lparser.cifstat/whilestat/enterblockunder pxx to find which op corrupts. Likely another pointer/struct-on-C-stack codegen bug (BlockCnt linked viafs->bl, or a Proto/jump-list issue).- DEEP TRACE (27c, narrowed to one bit). For
if 3>2 then return 1 else return 2 end, the compiledcode[]is:0:OP_EQI 1:OP_LOADI 2:? 3:OP_JMP 4:OP_LOADI 5:? 6:OP_JMP. The false-jump list head = pc 3.code[3]is a freshOP_JMPwhose offset should beNO_JUMP=-1but reads -257;code[6](also OP_JMP) correctly reads -1. getjump(3) → -253 (=4+(-257)) → patchlistaux walks to pc -253 →fixjump/getjumpOOB → segfault. Comparing raw words:code[3]=0x7FFF7F38, but a correctCREATE_sJ(OP_JMP, NO_JUMP+OFFSET_sJ=0xFFFFFE, k=0)=0x7FFFFF38. Only bit 15 differs (cleared). Bit 15 =POS_k(= POS_A+SIZE_A+1? no: POS_k=15) and is inside the 25-bit sJ field (sJ bit 8 = value 256; -257 vs -1 is exactly a 256 delta). So a single bit (POS_k/sJ-bit-8) of this JMP is getting cleared. RULED OUT:CREATE_sJ(*,0xFFFFFE,0)round-trips to -1 in isolation (opcode value doesn't matter); expdesc union layout is correct (sz=24, t@16, f@20, no overlap). Odd clue: instrumentingluaK_code's write to dump anyiwithGET_OPCODE(i)==56did NOT fire for these JMPs — so the JMP is either NOT created via the normalluaK_codepath, or is born with non-56 opcode bits and acquires op 56 + a cleared bit-15 later (a stray write tocode[3], e.g. a mis-targetedSETARG_*/fixjump, or aluaM_growvectorrealloc copy issue). NEXT: bisect who writescode[3]between birth and the patch — instrument everyfs->f->code[pc] = …/SETARG_sJ/SETARG_kfor pc==3.
- DEEP TRACE (27c, narrowed to one bit). For
- Also still open: float
2^8/%fprint garbage (known float gap, separate from control flow). - Harness:
pxx_hostamalg.cmainnow runs a list ofluaL_dostringchunks viarunchunk(load/L, call/Cmarkers). pxx Cmaingets no argv (argc=0) and lua file IO returns empty (fopen/fread non-functional), so iterate viadostringchunks + rebuild, not a script file. Debug__pxx_writemarkers remain in gitignored lua scratch.
- WORKS now (verified via a
-
Session 2026-06-27b (Track A+C) — lua now runs through ALL of
luaL_openlibsand executes the script; next blocker is a GC allgc linked-list cycle. Committed57e3e73b(3 front-end fixes, self-host byte-identical,make test+make test-nilpygreen). Build/run recipe unchanged (pxx_hostamalg.c→/tmp/luah_pxx /tmp/t.lua).- FIX 1 — global
const char *[]init dropped the whole array when any element was not a plain string literal. The C-frontend array-of-pointer scan (ParseCGlobalVarDecl, cparser.inc ~2319) aborted (nArrElems:=-1) on the first non-tkStringelement, leaving every slot NULL — so lua'sluaT_typenames_(which holds the array-nameudatatypenameamong string literals) was all-NULL andluaT_objtypenamereturned(null). Now an identifier element lowers to the address of that global (new PendingInit encodingFOff=-4, materialized asarr[i] := @gin parser.incCompilePendingGlobalInits); an integer/NULLelement lowers to its literal pointer bits (FOff=-1). Testcglobal_strptr_array_decay_b73. - FIX 2 —
(void)expr;/((void)expr);statements were silently dropped. The cast lowering wrapped the operand in anAN_PTR_CASTretag tagged void, which codegen pruned as a dead value — so lua'slua_pushglobaltable(= ((void)lua_rawgeti(...))) was a no-op, leaving a string on the stack where the globals table was expected →attempt to index a string valuePANIC at the firstluaL_requiref. Now a void cast (depth 0) returns the operand unwrapped, preserving the call side effect (ParseCUnary, cparser.inc ~700). Testcvoid_cast_call_stmt_b74. This was THE keystone — it unblocked the entire_G/base-lib load. - FIX 3 — scalar
const char *p = "literal";globals stayed NULL. OnlyT *p = &g;was handled; a string-literal initializer fell through to the skip path. Added a scalar string-literal branch (cparser.inc ~2653, reusesFOff=-3string-span withElem=-1; adjacent literals concatenate). lua'sconst char *const CLIBS = "_CLIBS"was NULL →lua_getfield(registry, CLIBS)dereffed a null key → segfault increateclibstable. Testcglobal_scalar_strptr_b75. - NEXT BLOCKER — GC
allgclist cycle at first__gcfinalizer. TracingluaC_checkfinalizer(lgc.c): on the first call (registering the CLIBS table's__gcmetatable, fromlua_setmetatableincreateclibstable) the search loopfor (p=&g->allgc; *p!=o; p=&(*p)->next){}never terminates (>=200000 nodes, no NULL,onever matched) -> hang. Later finalizer registrations (package/string/... libs) terminate normally. Sog->allgcis cyclic/corrupt specifically at that point, or the CLIBS table GCObject is not linked intoallgc. Strongly smells like a GCObjectCommonHeader.nextlink/offset bug or aluaC_newobj/correctpointerscodegen issue — a NEW, distinct bug from the global-init/cast cluster. (A temporary guardedbreakin the loop let lua run to completion but with heap corruption, printing a garbled string — proves the rest of the pipeline works once past this.) - MINOR gap noted (cosmetic):
static const char ud[] = "userdata"andchar buf[] = "hello"— a char ARRAY initialized FROM a string literal — is not filled (stays zero/empty). Only 3 such globals in lua core (udatatypename,lua_ident,Outputin unlinked luac.c), all error-message / version strings, so non-blocking. Fix would emit per-byte PendingInits for awasArrchar/byte base with atkStringinitializer. - Local lua scratch (gitignored
library_candidates/) still has debug markers in lstate/lauxlib/lapi/lbaselib/loadlib/linit.c (B C D E,req:,ob*,P*,cc*, etc.). lgc.c restored clean. Strip before any "clean run" timing.
- FIX 1 — global
-
Session 2026-06-27 (Track C) — Lua startup reaches
F:open-done; current failure is post-startup runaway stack/error formatting, not the earlier bad allocator pointer. VerifiedHEAD == origin/master == 66494acbbefore edits; working tree already contained local compiler/test changes from the Lua C frontend bring-up. Compile still succeeds with:./compiler/pascal26 -g -Ilib/crtl/include -Ilib/crtl/src -Ilibrary_candidates/lua/src library_candidates/lua/src/pxx_hostamalg.c /tmp/luah_pxx. Runtime with normal stack still exits 139 after stderrB C D E F:open-done; runtime withulimit -s unlimitedtimes out after the same markers. gdb shows the normal-stack crash at stack guard in theaddnum2buff/luaO_pushvfstringformatting path, so the next bug is likely an infinite loop/recursion or bad error path after libraries open.- Fixed C fixed-array decay in pointer arithmetic (
nodes + 2) and pointer increment/decrement stride through array-decay metadata. - Fixed partial 2-D row decay double scaling for
G(L)->strcache[i]: the row address path emits a raw byte offset and must not be scaled again as element index arithmetic. - Fixed pointer typedef metadata for declarations such as
typedef StackValue *StkId; StkId o; o++, preserving element record/stride. - Fixed C pointer/null ternary typing so
return (*p == NULL) ? NULL : pdoes not truncate the pointer through an integer hidden temp. - Added/passed focused tests
b61-b64:cptr_array_decay_stride_b61.c,cfield_2d_row_decay_b62.c,ctypedef_ptr_stride_b63.c,cternary_ptr_null_b64.c. Earlierb51-b60remain part of the local Lua bring-up test set. - New debug-tool note for future agents:
agents/debug-tools.md. ptrace/gdb now works and was decisive;rris installed but currently blocked byperf_event_paranoid=4.
- Fixed C fixed-array decay in pointer arithmetic (
-
Session 2026-06-26 round 20 (Track A+C) — 2D array struct field + double-pointer struct record; gate-green + self-host byte-identical. Two fixes that close the
luaS_new/strcacheblocker filed last round:- Multidim array struct field (
cparser.inc+symtab.inc): C array fields now recordUFldArrNDims/per-dim spans. A FULL indexm[i][j]flattens to oneAN_INDEX(field, Horner(i,j))with the real element stride (the old per-subscript path mis-strided the outer index —CNodePointeeTk(AN_FIELD)defaulted to Integer/4-byte, a compressed-but-injective layout that round-tripped by luck on low addresses). A PARTIAL indexm[i]decays to the ROW ADDRESS&field + i*rowStrideas a raw pointer, soT** p = m[i]works. Closesbug-c-multidim-array-field-partial-row. - Double-pointer-to-struct keeps its record (
cparser.incParseCDeclType):T** phadPtrElemRec = REC_NONE, sop[i]->fieldresolved against no record (offset 0). Now the base record is retained (immediate element stays a pointer for stride; the resolver consultsPtrElemRecat theAN_DEREF(AN_INDEX)two- level point). lua'sgetstr(p[j]) == p[j]->contentsnow resolves. RESULT: lua runs past string interning + strcache;luaS_newresolves. Crash now deeper insideluaT_init(next thread — re-instrument the boot path).
- Multidim array struct field (
-
Session 2026-06-26 round 19 (Track A+C) — lua runs past STRING INTERNING into luaT_init; 3 compiler fixes (commit d84d9164), gate-green + self-host byte-identical. The round-18
internshrstrcrash wasgetshrstr(ts) == ts->contentsreturning 0 — a C fixed-array struct field read as a value loaded the array's bytes instead of decaying to the field ADDRESS. Three fixes:- array-field decay (ir.inc, CProgramMode-gated): a C
char contents[N]field used as a value now decays to its address, like an array variable. - array brace-init materialization (cparser.inc): C
T a[]={e0,e1,..}local/static arrays now generate reala[k]=ekassignments at the decl point (reuses C expr lowering -> string-literal /&/ cast elements work) and size an unsized[]from the element count. Was lua'sluaT_eventname[]tag-method-name table reading stack garbage. *constdeclarator (cparser.inc): a pointer-levelconst(char *const p) is skipped after the stars; previously the declarator name was never reached -> variable undeclared (read as 0). lua's table isstatic const char *const luaT_eventname[], so this was load-bearing. RESULT: lua runs stack_init -> registry -> string interning (was the crash) ->luaT_initwithluaT_eventname[0]=="__index"materialized correctly. NEXT BLOCKER (filedbug-c-multidim-array-field-partial-row):luaS_new'sTString **p = G(L)->strcache[i]— a 2-D array struct field. Full 2-Dcache[i][j]works; the partial-index ROW DECAYcache[i]does not (loads the element instead of the row address). Larger multidim-field feature; the C frontend's thin per-node pointer-type model makes the clean fix multi-session.
- array-field decay (ir.inc, CProgramMode-gated): a C
-
Session 2026-06-26 round 18 (Track A+C) — lua COMPILES, LINKS, and EXECUTES into newstate; setjmp/longjmp codegen landed. Built lua-5.4.7 as a one-TU amalgamation (crtl + 33 core/lib .c) against the libc-free crtl; it now runs through stack_init, init_registry, object allocation, and into the string table before a remaining lua-core miscompile. Commits 11a4a95f, 863c044c (+the earlier round-17 chain), all gate-green + self-host byte-identical:
- setjmp/longjmp codegen (the M3 keystone): EmitCSetjmpStubs emits x86-64
machine-code stubs registered as __pxx_setjmp/__pxx_longjmp (save/restore
callee regs + caller rsp + return addr). setjmp.h maps the macros and makes
jmp_buf a STRUCT — a
typedef long jmp_buf[16]array-typedef LOSES its dimension in the frontend (sized as one long), so a struct field of it underflows the frame and rsp lands inside the local jmp_buf, corrupting setjmp's own return. Verified: value passing, nested-frame longjmp, local + struct-field jmp_buf. lua's whole error model rides this. - libc-free libm via math.pas: crtl math.c bridges to lib/rtl/math.pas;
most C names bind case-insensitively (sqrt->Sqrt). FIX (compiler): a Pascal
unit's
usesnow searches the unit's OWN dir first, so pxxcio'suses mathbinds lib/rtl/math.pas, NOT -Ilib/crtl/include/math.h. pow routes via exp/ln (Power is overloaded). frexp/ldexp loop-based (the (ulong)&double pun is unreliable). - libc-free allocator: malloc/free/realloc -> the Pascal mmap heap (PXXAlloc/PXXFree/PXXRealloc) via pxxcio; exit -> exit_group. + locale (localeconv "."), stdlib (atoi/strtod/qsort), seed-only time.
- lua run state: setjmp + math + allocator all verified working inside lua; luaC_newobj returns valid memory; createstrobj completes. Crash is now a NULL/offset miscompile in the string-INTERNING tail (internshrstr after createstrobj) — a lua-core struct-layout bug, the next thread to pull.
- REMAINING for a printing lua: the internshrstr miscompile, then the rest
of the lua-core bring-up tail (openlibs registration tables = global
array-of-struct init, still BSS-zeroed; bug-c-vararg-overflow-area for 6+ args).
Build recipe: one-TU
#includeamalg of crtl/src + lua/src (luac.c excluded),-Ilib/crtl/include -Ilibrary_candidates/lua/src. lua viatools/install_lib_candidates.sh lua(gitignored).
- setjmp/longjmp codegen (the M3 keystone): EmitCSetjmpStubs emits x86-64
machine-code stubs registered as __pxx_setjmp/__pxx_longjmp (save/restore
callee regs + caller rsp + return addr). setjmp.h maps the macros and makes
jmp_buf a STRUCT — a
-
Session 2026-06-26 round 17 (Track A+C combined) — C STDIO RUNS LIBC-FREE VIA THE PASCAL PAL; printf formatting works. Five commits, all gate-green + self-host byte-identical:
- libc-free byte sink (39f00929): new
lib/rtl/pxxcio.pasexports__pxx_write/__pxx_readwrappingPalWrite/PalRead(cross-platform PAL, posix/ESP-IDF). ParseCProgram auto-pulls it for every C program (guarded like the Pascal default-RTL pull), emitted after the entry stub. cparser extern decl: a prototype resolving to an already-bodied proc stays internal (binds to the Pascal wrapper, not a libc import). Closes the track-a-c-stdio-needs-pascal-import-and-data-relocs blockers #1 (C imports + links a Pascal routine) and #2 (libc-free byte sink). The chosen design is RTL-reuse, NOT a raw-syscall intrinsic and NOT libc COPY-reloc. - address-of-global static init (a28e17e3):
FILE *stdout = &__crtl_stdout(blocker #3) — PendingInit gains an address-of-sym variant (Elem=-2, target sym in Val) emittingp:=@gat main entry. Also fixed Cstderrcolliding with the PascalStdErrconst (a C global now shadows a CI-only const hit). - ternary string-literal segfault (1bf28aaf): ir.inc AN_TERNARY tyString
arm now stays a
tyPointerin CProgramMode (not managed AnsiString). This was THE printf-engine keystone — stdio.c's(k=='X')?"0X":"0x"corrupted__crtl_vformat's frame -> snprintf re-entered ~7400x to stack overflow. - double vararg (56de8d5b): float
va_argroutes through the GP helper (the internal all-GP variadic convention saves floats in the GP area, never the FP area).va_arg(ap,double)now receives the value. - RESULT:
fwrite/fputs/puts/fputc(lua'slua_writestringpath) ANDprintf/fprintf%d/%x/%s/%c/%p+ width/precision all run libc-free (statically linked, no NEEDED libc) and match gcc. - REMAINING for running lua with IO: (a)
%f/%gdecimal formatting — the vararg double arrives, but the engine's float->decimal MATH is blocked by bug-c-float-int-cast-and-spill:(int)42.5==0 (C numeric float<->int cast is a bit-reinterpret AN_PTR_CAST, must route to a real cvttsd2si/cvtsi2sd conversion — per-backend, Track A) AND a computed double subtract/compare in a loop spills wrong (xmm liveness across branches, Track A). (b) global array-of-struct init (luaL_Reg registration tables) — blocker #4, still BSS-zeroed; Track A. lua not staged in this checkout (gitignored vendor src) — re-fetch lua-5.4.7 to retest end-to-end.
- libc-free byte sink (39f00929): new
-
Session 2026-06-26 round 16 (Track C), gate-green + byte-identical, lua 27 -> 29: inline anonymous struct/union as a TYPE (f5ef8ec) —
struct { .. }/union { .. }with an inline body in type position (global var / param / field) was not laid out (the body dangled, the var stayed opaque); ParseCDeclType now lays it out as a record via CParseInlineAggBody, and IsBareStructDecl correctly treatsstruct { .. } g;as a VARIABLE decl (route to ParseCGlobalVarDecl), not a bare type decl. Unblocked lparser + lstrlib (parse). The C frontend's parse/preprocessor/struct-layout surface is now COMPLETE for the lua core. The sole remaining core parse blocker is VARARGS (lapi/lauxlib/ldebug/lobject —va_arg(ap, type)/va_start/va_list): System V AMD64 va_list ABI = variadic prologue register-save area + va_arg lowering (gp_offset/overflow) + theal-register call convention. The parse half is Track C, the codegen half is the shared backend (Track A) and risks the byte-identical self-host gate. luac is a SEPARATE tool (not the interpreter core). TWO remaining correctness gaps beyond parse: (1) global= { .. }initializer DATA is still skipped (pre-existing; affects ALL initialised globals — lparser priority[] / lstrlib nativeendian read zero at runtime; fix = PendingInit-based materialisation, pure Track C); (2) multi-file LINKING (combine 34 objects into one executable) — infrastructure the single-file C frontend lacks. So: parse is ~done, but a runnable Lua needs varargs codegen (Track A) + global-init data + linking. -
Session 2026-06-26 rounds 14-15 (Track C), gate-green + byte-identical, lua 25 -> 27: C assignment-as-value (call arg / chained /
(p=x)->field; ldo'sisLua(ci = ci->previous)), and the big one — nested anonymous union/struct member layout (ffb1a73). A struct containing an inlineunion {..}/struct {..}member was laid out as an opaque pointer -> field access read garbage; lua CallInfo/GCUnion/TValue use nested unions pervasively, so this was a WIDESPREAD silent miscompile and the emergent root behind ldo/ltm. Fixed by laying the nested body out as a sub-record AND buffering the parent's field descriptors, appending them to the UFld pool contiguously after the sub-records (the obstacle was the contiguous [base,count) FindUField model). Unblocked ldo + ltm. The parse/preprocessor/struct-layout layer is now essentially complete. REMAINING 7 files, all substantial features: (A) varargs__builtin_va_start/va_arg/va_list— lapi, lauxlib, ldebug, lobject (System V AMD64 va_list ABI; Track A / codegen). (B) global-array DATA materialisation + anon-struct array element — lparser (static const struct {lu_byte left,right;} priority[] = {..}thenpriority[op].left), lstrlib; need the{..}initializer laid into the data segment as a real sized array. (C) luac — a SEPARATE tool (the bytecode compiler, not the interpreter core);#define S(x) (int)(x),SS(x)(comma macro expanding to a printf arg list) hits AN_COMMA in IRLowerAddress; low priority. -
Session 2026-06-26 rounds 11-13 (Track C), gate-green + byte-identical, lua 22 -> 25: object-macro-alias-of-function-macro re-expansion (unblocked lvm);
(type)cast in constant expressions (ltable — root was the bare-funcname=Result landmine:Result := CEvalConstPrimaryneeded());#stringize operator (lundump); aggregate initializer= {...}on a non-array local (lcode); and the pointer-pointer subtraction codegen fix (ptrdiff — was a silent miscompile; tkDiv not tkSlash). Fixtures b38-b42. The PARSE/PREPROCESSOR layer is now essentially complete; the 9 remaining files are blocked on three deeper things: (A) varargs__builtin_va_start/va_arg/va_list— lapi, lauxlib, ldebug, lobject (System V AMD64 va_list ABI: variadic prologue saves the register-save area, va_arg walks gp_offset/overflow_arg_area; Track A / codegen). (B) global array DATA materialisation + anonymous-struct member access — lparser (static const struct {lu_byte left,right;} priority[] = {...}indexedpriority[op].left), lstrlib; globals are not yet real arrays with their initializer data laid into the data segment (the brackets/init are skipped). (C) IRLowerAddress of rvalue/compound in the VM/GC core — ldo (AN_ASSIGN), ltm (AN_BINOP, the emergent setobj block over&(p++)->val/&(p+i)->val), luac (AN_COMMA); several are address-of-rvalue (verify vs gcc, some UB). These are feature/codegen work, partly Track A (varargs), spanning multiple sessions — not the isolated parse fixes that took the core 0 -> 74%. -
Session 2026-06-26 rounds 7-10 (Track C), all gate-green + byte-identical, lua 14 -> 22: f()->field on pointer-returning calls + global-struct record id (silent miscompile — global struct fields were all offset 0);
##token-paste + macro arg whitespace trim; C goto + labels; C float-literal lexing; constant-expr/and%; comma operator in if/while/for conditions; array-syntax function paramsT name[]decay to pointers (silent miscompile); object-macro alias of a function macro re-expands with source args (lua setsvalue2n/setobj2n -> unblocked lvm + lundump partially). Fixtures b31-b38. REMAINING 12 files, each deep or cross-track: (a) varargs__builtin_va_start(lapi/lauxlib/ldebug — va_list ABI, Track A). (b) emergent codegen combinations in the VM/GC core (ltm/ldo:setobjs2s(L, top.p++, func.p + i)= setobj block over&(p++)->val/&(p+i)->val— every isolated piece compiles == gcc, the macro-expanded block does not; reduce with the full-file-bisection method as was done for the multi-declarator root cause). (c) IRLowerAddress of rvalue/compound (lparser AN_INT_LIT, luac AN_COMMA — several are UB, verify vs gcc). (d)(type)cast in a constant expr (ltable MAXABITS — finicky, see bug-c-const-cast-in-array-dim). (e) float codegen conversions (lstrlib/lobject — see bug-c-float-int-cast-and-spill). (f) lcode field-name. (g) multi-file linking. METHOD NOTE that keeps working: shrink a failing real-file snippet (drop macro, drop types, drop block) to a plain-C minimal repro; emergent "works in isolation" failures are usually a shared root (multi-declarator, object-macro-alias). -
Session 2026-06-26 round 6 (Track C), gate-green + byte-identical, lua 14 -> 15 + VM-core path unblocked: ROOT-CAUSE fix — multi-declarator pointers
T *a, *b;(5d3bc2f). ParseCLocalDeclAST folded the FIRST declarator's*into the base type and applied it to every name, soint *p, *q;dropped/mistyped q andTValue *io1, *io2;siblings lost pointer-ness -> deref hit IR_UNSUPPORTED. This was the real cause of the round-5 'emergent setobj/setsvalue' VM-core failures (every isolated piece compiled; the trigger was simply two pointers in one declaration). Now each declarator parses its own stars (a literal star redistributes; a typedef pointer applies whole). lapi advanced past setobj to varargs. NOTE: the bisection method that finally cracked it — keep shrinking the failing snippet (drop macro, drop types, drop block) until a plain-C minimal repro remains; hereint *p,*q; *p=*q;was the whole bug. REMAINING (each deep / multi-session): (a) varargs__builtin_va_start— now the top named blocker, 3 files (lapi/lauxlib/ldebug), lua's luaL_error; real va_list ABI = Track A. (b)IRLowerAddressof rvalue/compound exprs — AN_CALL (liolib/lobject/ lparser:localeconv()->decimal_point[0], field/index of a call result), AN_ASSIGN (ldo), AN_BINOP (ltm), AN_COMMA (luac); general fix = materialise the rvalue to a hidden temp and address that (verify each vs gcc; some are UB). (c) lvm setobj/lundump setsvalue still 'undeclared' (a genuinely emergent macro-expansion case, distinct from the multi-declarator one). (d) multi-file linking. lua.c (interpreter main) compiles to an object. -
Session 2026-06-26 round 5 (Track C), gate-green + byte-identical, lua core 7 -> 14: global array decls no longer cascade + balanced-brace aggregate-init skip (3843307),
sizeof((l)[0])balanced-paren operand skip — unblocked the whole luaL_newlibtable sizeof cluster, +6 files (ced768d),signal()declared via a__sighandler_ttypedef (the function-returning-fn-pointer declarator was unparseable) + undeclared-call error now names the function (2f5e285). REMAINING blockers: (a) emergent-combination bugs — in big files (lapi/lvm/lgc) anIRLowerAddresshits an int-literal / AN_CALL / AN_ASSIGN where every isolated piece (setobj, isvalid, luaC_barrier, the ternary) compiles fine; the failure only appears with the whole accumulated file. Smells like STATE ACCUMULATION (recycled symtab slots / node-pool — cf. the Alloc* parallel-array landmine), NOT a per-construct bug; bisection can't isolate it because the minimal repro doesn't carry the accumulated state. Next: instrument which sym/node slot is recycled with stale ASTSOffset/Kind when the int-lit address is emitted. (b) varargs__builtin_va_start(va_list — lua's luaL_error; Track A/ABI). (c) setobj/setsvalue undeclared in lvm (same emergent class). (d) multi-file linking. -
Opened: 2026-06-25
-
Session 2026-06-25 (Track C) delivered, all gate-green + self-host byte-identical, on
feat/cfront: function pointers (4d36da6), typedef-of- struct-tag record aliasing (b65d617), forward-record field-base re-anchor (65f3fcd), ternary?:/AN_TERNARY(b68c5be), integer literal suffixes (a64d316), bitwise-~const-eval (cd5996d, closed bug-c-const-eval-bitwise-not). lua-5.4.7 staged inlibrary_candidates/lua(gitignored). Remaining blockers triaged below; lua does NOT compile yet (multi-session, spans Track C + A). Session also added: parenthesized declarator names(name)(params)(50e1626's predecessor d4c9b9f — unblocked the whole lua_* API prototype cluster) and Cswitch/case/defaultwith fallthrough + break-only scope (50e1626, AN_SWITCH, target-independent IR). lua core: 0 → 4/34 files parse clean. End-of-session error landscape across the 34 core files: 21unexpected token(a LONG TAIL of varied expression-parse bugs — NOT one cause; e.g. a string- literal/sizeofmacro expansion in lobject.c, a+=on a->field in lmem.c, a call in lstate.c — each needs individual bisection), 6call to undeclared function(residual macros +__builtin_offsetof+ lstring's deep cast chain which bottoms out atbug-c-const-eval-bitwise-not-adjacent macro re-scan), 2Unsupported linear node in IR codegen(ldebug.c, lparser.c — an IR/codegen gap to isolate), 1expected C expression. NEXT: pick off theunexpected tokentail incrementally (Track C), isolate the 2 IR-codegen gaps, thensetjmp/longjmp(Track A) + multi-file linking for an actual lua build. -
2026-06-26 (round 4) — full-file-bisection harness in use; 7 more fixes, lua core 5 -> 7 files parse clean, all gate-green + self-host byte-identical. Bisecting lapi.c's
index2valuesurfaced a chain of high-leverage bugs: array-of-struct element STRIDE (a SILENT miscompile —a[i]/p[i]used pointer size not RecSize;bug-c-struct-pointer-index-stridedone),(p+i)->field(bug-c-field-on-pointer-arithmeticdone), re-expansion of the same macro inside its own arguments (lua'scheck_expwithincheck_expviagco2ccl; the active-macro guard was too aggressive), multi-line macro/call arguments (the preprocessor was line-based; now joins continuation lines while parens are unbalanced), and++/--as a VALUE (new AN_INCDEC: postfix yields the old value via a temp, prefix the new; pointer base supported fors2v(top.p++);bug-c-postincrement-as-rvaluedone). Fixtures b23–b27. KEY: the bisection harness (build progressively larger prefixes of the real .c with its real includes) + thenear:locator is the working method — minimal repros no longer reproduce these emergent/cumulative-state bugs. Current landscape: 9Unsupported linear node(IRLowerAddress gaps for compound/rvalue exprs —&(call),&(int-lit), etc.; mostly address-of-rvalue, several are UB so verify against gcc before "fixing"), 8unexpected token, 7 clean, 5expected C expression, 4call to undeclared function. setjmp/longjmp (Track A) + multi-file linking still remain for a full build. -
2026-06-25 (round 3) — 6 more Track C fixes, all gate-green + byte-identical: adjacent string-literal concatenation (
"a" "b", lua'slua_pushliteral); string-literal-to-pointer store now lands on char 0 not the Pascal length prefix (closedbug-c-string-literal-to-pointer-prefix);sizeofin constant expressions / array dimensions (char b[3*sizeof(size_t)]);&function(AN_PROCADDR in IRLowerAddress); parenthesized comma EXPRESSION(a,b)(newAN_COMMA— lua'sapi_check/lua_lock=((void)l, expr)); plus the libc header growth (math.h + string/stdio/stdlib) and recursive#if. Fixtures b18–b22. lua core still 5/34 parse-clean by COUNT, but individual files advance several blockers each (lapi/lstate/ldo now fail much deeper). KEY FINDING for the next worker: the remainingunexpected token/expected C expressionfailures are EMERGENT from the cumulative full-file preprocessor/macro state — every construct extracted in isolation (api_check, index2stack, the sizeof/comma/cast forms) now COMPILES, but the full file still fails. So per-file progress now needs bisection WITHIN the real file (build progressively larger prefixes of the actual .c with its real includes), not minimal repros. Thenear:locator (cd30d0c) gives the token; pair it with a prefix-bisect harness. -
2026-06-25 (final survey) — the remaining lua blockers are now predominantly CROSS-TRACK, not Track C. With the type system fixed, the
call to undeclared functioncluster resolves to: (a) libc functions with no crtl declaration —fabs/frexp(there is NOlib/crtl/include/math.hat all),strerror(string.h doesn't declare it),fwrite,system,signal. Growing the crtl header/library surface is Track B (lib/crtl/**, the M2 milestone). (b) a few residual macro re-scan bugs (cast/cast_byte/novariant— Track C, deep and context-dependent). The other big remaining gates —setjmp/longjmp(codegen;compiler/exception_emit.inchas the Pascal exception path but C setjmp/longjmp is unverified) and multi-file linking — are Track A / infrastructure. CONCLUSION: Track C has been pushed about as far as it can take lua ALONE; reaching a working lua build now needs Track B (libc surface) and Track A (setjmp) in tandem, plus the residual Track C parse tail (16unexpected token, diagnosable via the newnear:locator). -
2026-06-25 (even later) — recursive
#ifmacro expansion fixed (0fa88d1) — foundational.#ifevaluated a macro atom by reading its body as a literal number, so a chained object macro resolved wrong: lua'sLUA_INT_TYPE → LUA_INT_DEFAULT → LUA_INT_LONGLONG → 3made#if LUA_INT_TYPE == LUA_INT_LONGLONGFALSE, soLUA_INTEGER/lua_Integer/lua_Unsignedwere NEVER defined under the real headers. CPExprAtom now recursively evaluates a macro body as a sub-expression (depth-guarded). Also added LLONG_MAX/MIN + ULLONG_MAX tolib/crtl/include/limits.h(luaconf gates the long-long path on#if defined(LLONG_MAX)). lua_Integer/lua_Unsigned now register. NOTE: lua must be compiled WITH-Ilib/crtl/includeon the path so<limits.h>/<stddef.h>resolve. Post-fix error landscape (with that include path): 16unexpected token, 10call to undeclared function(more files now reach real libc calls — the M2 crtl/extern surface), 3expected C expression, 5 parse-clean. The type-system foundation is now correct; the tail is libc surface + residual per-file parse bugs + setjmp (Track A) + multi-file linking. -
2026-06-25 (later) — session continued; lua core now 5/34 parse clean. Added beyond the above: a permanent readable
near:source-context on unexpected-token errors (cd30d0c — makes the tail diagnosable WITHOUT an instrumented rebuild; use it), indirect call through(*expr)(args)/ dereferenced fn-pointer (e4a991a — lua's(*g->frealloc)(...)), and an IRLowerAddress&(array-field)collapse (007d14f — unblocked lmem.c). Filedbug-c-sizeof-string-literalandbug-c-addr-of-unsupported-ir(the latter partially fixed;&s->v[0]element-via-arrow remains). Current error landscape over 34 files: 20unexpected token(still a long tail of DISTINCT per-file causes — e.g. ltable's(lua_Unsigned)icast-vs-paren disambiguation, lstate'ssizeof(size_t); thenear:context now pinpoints each), 7call to undeclared function, 2expected C expression. Grind the tail with the locator; thensetjmp/longjmp(Track A) + multi-file linking remain for an actual lua build. -
2026-06-25 —
__builtin_expecthandled (2f62c2e): reduces to its first arg (lua's pervasive l_likely/l_unlikely). Diverse-tail confirmed by windowed bisect: the 21unexpected tokenfiles each have a DIFFERENT context- dependent cause that does NOT reproduce in isolation (every minimal repro passes) and the lexer SrcPos sits ahead of the parse point, making them slow to pin. Concrete findings so far: lobject.c usessizeof("string")/sizeof(char)→ filedbug-c-sizeof-string-literal(pxx returns 8 not len+1; a VALUE bug, not the parse blocker); ltable.c fails inside a deeply-nested macro cast chain ((...Integer)( limit + 1)))))->tt_)) & 0x0F)); lmem.c parses its whole body but still errors (SrcPos at end-of-function — the real fault is elsewhere in the token stream). RECOMMENDATION for the next session: add a PERMANENT, precise C-parse error locator (print the failing token's own source offset + a readable window, not the lexer SrcPos) — without it, eachunexpected tokencosts an instrumented rebuild to locate. Then grind the tail file-by-file. -
Track: C (C frontend) — isolated worktree
../frankonpiler-cfront, branch -
Type: feature (track-D milestone path)
-
Status: backlog
-
Opened: 2026-06-25
-
Track: D (C frontend) — isolated worktree
../frankonpiler-cfront, branchfeat/cfront. Lands tomasteronly whenmake test+ self-host fixedpoint stay green (C-body codegen edits the compiler binary → reseed). -
Builds on:
feature-c-source-frontend(slices A–F = the mechanics: lexer fidelity, C expr parser, statements, multi-function/globals, fn-macros, embedded layout). This ticket is the roadmap that drives those slices toward portable desktop C, not embedded/Arduino. -
Relation:
feature-c-regex-library-devtest(tiny-regex-c + freebsd-regex staged inlibrary_candidates/) = the warmup workload.feature-c-runtime-library(lib/crtl) = the libc surface these need.
Why a separate path ticket
feature-c-source-frontend's north-star is Arduino/ESP — slices E (function
macros) and F (packed/bitfield/volatile layout) serve hardware structs. The
desktop path needs A–D solid, a real libc (lib/crtl), and two things the
existing ticket lists as non-goals:
- setjmp/longjmp — lua's default error model (
LUAI_THROW/LUAI_TRY= setjmp/longjmp in plain C; only C++ builds use exceptions). No lua without it. - varargs definition — calling
printfworks today (cdecl push). lua defines vararg functions (luaL_error,lua_pushfstring) → needsva_list/va_start/va_arg/va_end. Calling-vararg ≠ defining-vararg.
Embedded layout (slice F) is deferred here — desktop structs are naturally-aligned; packed/bitfield/volatile not on the lua/sqlite path.
Leverage (why this is tractable)
C frontend emits the same IR the Pascal frontend does → all 6 backends,
ELF, ABI come free. C extern maps straight to the existing
dynamic-link/external-symbol path → printf/malloc/fopen resolve to libc
(proven: test/hello.c compiles + runs against pinned). The declaration half
(header import: typedef/struct/union/enum, POD layout+alignment, extern decls,
integer macros) is already mature. Remaining work = the body half.
Milestones
Each milestone = a runnable workload that proves the prior frontend slices. gcc/tcc stdout-equality oracle throughout (deterministic int/string output).
M0 — small fixtures (drives slices A–C)
Per-slice .c fixtures in test/: expressions (C precedence, pointer
arithmetic scaled by element size, casts, sizeof), control flow
(if/while/do/for/switch + break/continue/fallthrough), local decls with
initializers. Each matches gcc stdout. Gate: make c-interop-devtest
fixtures green; header-import regression suite still green (slice A risk).
M1 — tiny-regex-c warmup (drives slice D)
library_candidates/tiny-regex-c/re.c compiles + a small driver matches known
patterns vs a gcc-built oracle. Today stops at undefined variable (re_matchp)
inside re_match → the multi-function/globals gap. Smallest real multi-function
C program; ideal slice-D proof. Drop-in (no upstream edits) preferred.
M2 — libc surface (lib/crtl)
Grow lib/crtl to what lua needs: string.h (mem*/str*), ctype.h,
stdlib.h (malloc/free/realloc/qsort/strtod/atoi), stdio.h
(printf-family/f*), math.h, setjmp.h, stdarg.h. Prefer thin wrappers over
the host libc via the extern path; own implementations only where needed
(already have string.c, ctype.c). Gate: crtl header+src smoke green.
M3 — setjmp/longjmp + varargs-define (the two scoped non-goals)
setjmp/longjmp: real save/restore of callee-saved regs + SP + return addr. Per-target (start x86-64, then cross). Either intrinsic-lowered or a tiny asm crtl primitive — decide at implementation; keep lowering in shared IR where possible.va_list/va_start/va_arg/va_end: SysV varargs ABI on the callee side. Gate: a.cthat longjmps out of a nested call, and one that defines + consumes a vararg fn, both match gcc.
M4 — lua
Build the lua interpreter (lua-5.4.x, C89/C99 portable core) from upstream
source, staged under library_candidates/lua first. Run lua against its own
test suite where deterministic; smoke print, arithmetic, tables, functions,
closures, error handling (the setjmp path). Gate: lua REPL + a script set
match a gcc-built lua's stdout. Drop-in preferred; record any edit.
M5 — sqlite
sqlite amalgamation (sqlite3.c single file — no multi-file link, but densest
macro/feature surface). Compile, then run a deterministic SQL script
(CREATE/INSERT/SELECT) and diff vs a gcc-built sqlite3 shell. Expect new
pressure: heavy macros, VFS, integer-width assumptions. File follow-ups per
gap rather than bloating this ticket.
Non-goals
- Embedded layout (packed/bitfield/volatile) — stays in
feature-c-source-frontendslice F. - C++ subset — separate follow-on.
- Full optimizer;
goto/labels, VLAs,_Generic, C11 atomics — defer unless a target forces it (note: sqlite may usegoto— revisit at M5). - Non-Linux calling conventions.
Testing
- Oracle = gcc (and/or tcc) stdout-equality; deterministic int/string output.
- Cross-bootstrap: body lowering goes through shared IR → run the multi-target harness (i386/arm32/aarch64/riscv32/xtensa) so cross regressions surface (x86-64 alone missed them in the generator/for-in work).
- Header-import regression suite stays green after every lexer change.
Landmines
- Slice A lexer is shared with header import; collapsed multi-char operators are
relied on by
CEvalConstExpr(<<as twotkLt,&/|bitwise) — update the const-evaluator in the same slice. ->→tkDotmapping is intentional; keep it.- MAX_UCLASS / MAX_UFIELD pressure — C structs share Pascal's tables; preserve opaque-fallback guards.
- Keep body lowering in shared IR, not per-target codegen (cross landmines).
- lua's error model is a hard dependency on setjmp/longjmp — do not start M4 before M3.
Log
- 2026-06-25 — opened (Track D, worktree
feat/cfront). Path = portable desktop C: tiny-regex warmup → lua → sqlite. Pulls slices A–D fromfeature-c-source-frontend; adds setjmp/longjmp + varargs-define (its non-goals) because lua requires both; defers embedded layout (slice F). - 2026-06-25 — Slice A (clexer operator fidelity) DONE. Multi-char C
operators now lex to distinct tokens:
++ -- += -= *= /= %= &= |= ^= <<= >>=,<<=tkShl>>=tkShr,&=tkAmp(bitwise) vs&&=tkAnd(logical),|=tkPipe vs||=tkOr,^=tkXor,?=tkQuestion,:=tkColon (was unlexed → also activates the bitfield→opaque guard at CStructBodyIsSimple).->→tkDot kept.CEvalConstExprrewritten to the new tokens in the same commit (added a bit-XOR precedence level) + the RegisterCMacroConsts guard; enum/macro const eval unaffected. New enum tokens appended at end ofTTokenKind(no ordinal shift). Self-host byte-identical (make bootstrap). Header-import regression: c_interop devtest identical to pinned; new fixturetest/cslicea_lib.c+test_c_slicea.pas(<< >> & | ^+ precedence) matches gcc, wired into the C-import suite. Filedbug-c-const-eval-bitwise-not(pre-existing~typing quirk, omitted from the fixture). Next: Slice B (real C expression compiler). - 2026-06-25 — Slice B (C expression compiler) increment 1 DONE. Real
recursive-descent C expression parser in cparser.inc (
ParseCExpr+ParseCBinExprprecedence-climber +ParseCUnary+ParseCPrimary), replacing the Pascal-ParseExprborrow inreturn. Emits the shared AST (AN_BINOP/AN_NEG/AN_NOT/AN_INT_LIT/AN_IDENT/AN_CALL/AN_ARG/AN_ASSIGN/ AN_STR_LIT) so all IR/backends apply. Full C precedence:* / % | + - | << >> | rel | eq | & | ^ | bit-| | && | ||, unary- + ! ~, assignment + compound-assign (right-assoc), function calls with arg chains, paren grouping, int/char/string literals, const-fold of imported enum/#define names. C-op -> AST-op mapping:/->tkDiv,>>->tkIdent(shr),&->tkAnd,|->tkOr (bitwise);&&/||tagged tyBoolean with operands normalised via(e!=0)for canonical 0/1. Added distincttkLogNotfor!(was collapsed with~-> bitwise);~stays tkNot.return <expr>from a top-level C main now exits with the value (IR: value-bearing CurProc<0 AN_EXIT routes through the Halt terminate path; Pascal program Exit never carries a value, so self-host byte-identical holds). LANDMINE (cost ~an hour): paramless self-recursion —op := ParseCUnaryreads the function's own Result (pxx/FPC bare-funcname rule), not a recursive call; must beParseCUnary(). Fixed in ParseCUnary (x3) and ParseCExpr. Verified: 20-expr differential sweep + fixturetest/cexpr_b.c(=89) all match gcc; full C-import suite still green; self-host byte-identical. Deferred to increment 2: ternary?:, comma operator, pointer/lvalue unary (* & ++ --), cast, sizeof, postfix[] . -> ++ --. Next: Slice C statements (locals+if/while/for/switch) to unlock multi-statement bodies. - 2026-06-25 — Slice C (statements) increment 1 DONE. ParseCStatementAST now
dispatches: local declarations with initialisers (
int x=…, y=…;— AllocVar per name, init lowered to AN_ASSIGN; main-scope locals land in BSS since CurProc<0, function locals get stack slots), expression statements (assignment/compound-assign/call),if/else,while,for,break,continue, empty;, nested blocks. Prefix + postfix++/--added (lowered tolv = lv ± 1, read side CloneAST'd to avoid aliasing).breakkeyword remapped tkHalt→tkBreak; addedcontinue→tkContinue.fordesugars to a while loop; with a post-expression it uses a first-iteration flag so acontinuestill runspostbefore re-checking the condition (the naiveinit;while(cond){body;post}desugar HANGS on continue — post is skipped so the counter never advances). REGRESSION fix: a stricter body parser broke test_c_macro_soup (a deliberately self-referential macro leaves an undefined identifier the old parser silently skipped) — an unresolved identifier now degrades to a0literal (best-effort frontend; undeclared calls still error). Verified: ~20 Slice-C differential programs (locals/loops/if/break/ continue/fib/factorial) match gcc exit codes; new fixturetest/cstmt_c.c(=82) wired into the suite; full C-import regression green; self-host byte-identical. Next: Slice D (compile ALL functions + globals + inter-fn calls) — merge the ParseCProgram/ParseCSubroutine drivers. - 2026-06-25 — Slice D (multi-function + globals) DONE. ParseCProgram is now
a two-pass driver over the one token stream: pass 1 (CHeaderMode) registers
EVERY function signature and reserves every global, skipping bodies; pass 2
compiles each function body via the existing ParseCSubroutine machinery
(prologue/params/frame/epilogue). Forward + mutual inter-function calls
resolve through ApplyCallFixups. Entry stub (x86-64, matching the prior
convention which was already x86-64 machine code):
mov [rsp-save]; call main; exit_group(eax)so main's int return is the process exit code; the call is a rel32 patched to main's body once compiled.CTopLevelIsFuncpeeks type+declarator for a(and rewinds (TokPos save/restore) to classify function vs global. Globals reserved as zero-init BSS (CurProc<0 => skGlobal); non-zero/constant global initialisers deferred. LANDMINE: linkage across the two passes — addedCProgramMode; a bodied function in a program is a LOCAL definition so pass 1 marks it ProcExternal=False (else a caller compiled before the callee's definition emits an EXTERNAL call -> "undefined symbol"), and pass 2's prototype;path must NOT re-mark external (it would clobber pass-1's definition-wins linkage before the body recompiles). Verified: forward calls, mutual recursion (even/odd), deep recursion (fib), 6-arg calls, shared globals all match gcc; new fixturetest/cmulti_d.c(=104) wired in; full C-import regression green; self-host byte-identical. tiny-regexre.cnow passes the old multi-function blocker (was "undefined variable (re_matchp)") — it reaches "main function not found" because it is a library (no main), the correct result. M1 (running regex) additionally needs pointers/arrays. Next: Slice B increment 2 — pointer/lvalue unary (* &), postfix[] . ->, casts, sizeof — the real blocker for regex/lua/sqlite. - 2026-06-25 — Slice B increment 2a (pointers + arrays) DONE. Unary
*(deref, rvalue+lvalue) and&(address-of) in ParseCUnary; postfix subscript[]in ParseCPostfix (AN_INDEX). Pointer arithmeticp+iis scaled by the IR automatically (it keys on operand type + IRPointerStride). Local decls now carry pointer element type (Syms.PtrElemTk/PtrElemRec from the captured CTypeElem* globals) and support fixed arraysT a[N]via AllocArray (N is a constant expression — literal/enum/#define). LANDMINE: pointer PARAMETERS also need their pointed-at type threaded (else*a/a[i]inside the body use the wrong width —swap(int*,int*)silently no-ops); added per-param pelemtk/ pelemrec capture + PtrElemTk set after AllocParam in ParseCSubroutine. All in Track D's lane (clexer/cparser only) — reuses existing AN_DEREF/AN_ADDR/ AN_INDEX, no shared-IR edits. Verified vs gcc: deref read/write, fixed arrays, array+pointer subscript, pointer arithmetic, pointer params, char* + string literal (strlen("hello")=5 — NUL-terminated literals work),swapidiom; new fixturetest/cptr_b2.c(=122) wired in; full C-import regression green; self-host byte-identical. Deferred: struct field access (. ->), casts, sizeof, multi-dim arrays, array initialisers, mixedint *p, qdeclarators. Next: struct field access + casts (then char-string libc surface toward M1). - 2026-06-25 — Slice B increment 2b (struct field access) DONE.
.and->field access (both lex to tkDot since->→tkDot is intentional): ParseCPostfix builds AN_FIELD, and disambiguates by base type — a pointer base auto-derefs (AN_DEREF) sop->f=(*p).f, a value base takes the field directly. Uses the existing ResolveNodeRec (handles AN_DEREF-of-pointer-to-record and AN_FIELD chains) + RecFieldType. Struct-by-value locals now set RecName (LastTypeRecId := CTypeBaseRec before AllocVar) so AllocVar reserves RecSize and fields resolve. KEY FIX: top-levelstruct/union/enum/typedefDEFINITIONS were never laid out in program mode (only the header-import path ParseCUnit did it) — so every field resolved to offset 0 (p.xandp.yaliased). Wired the existing ParseCTypedef/ParseCEnumDecl/ParseCStructDecl into the program driver: pass 1 lays out types (once) + signatures + globals; pass 2 compiles bodies and skips type decls/globals via SkipCDeclToSemi (balanced-brace skip to the depth-0;). All in Track D's lane (cparser only) — reuses existing AN_FIELD/ AN_DEREF, no shared-IR edits. Verified vs gcc: value., pointer->, struct-pointer params, nested structs, typedef structs, linked-list walk; new fixturetest/cstruct_b3.c(=62) wired in; full C-import regression green; self-host byte-identical. Deferred: casts, sizeof, struct-by-value params, combinedstruct X {..} v;, array/struct initialisers. Next: casts + sizeof, then char-string libc surface (M2) toward running tiny-regex (M1). - 2026-06-25 — casts + sizeof + do/while + comma DONE (slice B inc2c/2d).
(type)exprcasts (AN_PTR_CAST reinterpret-retag; cast-vs-paren disambiguated by peeking a type after(),sizeof(type|expr|var)-> compile-time int,do/while(first-iteration-flag desugar so break/continue keep C semantics), and the comma operator in statement / for-init / for-post positions. All in Track D's lane (cparser only), reusing existing nodes. Fixtures test/ccast_b4.c (=102), test/cloop_b5.c (=28). Self-host byte-identical. - 2026-06-25 — ROADMAP REFRAMED after empirically probing the frontend. The
frontend is more capable than the M0–M4 milestones implied: a bubble-sort over
an array of structs (struct assignment,
a[j].key, pointer params, nested loops) already matches gcc. The genuine remaining gaps to compile real C (lua/sqlite) are mostly C language features, NOT "M2 libc surface" (libc resolves via the extern/host path that already works — printf):- switch/case (+ fallthrough) — biggest lua/sqlite user; a fully-correct desugar needs break-only scope, but the IR's loop stack couples break and continue, so a clean switch likely wants a shared break-only-scope IR primitive -> Track A candidate. (Common break-terminated switches could desugar to do{}while(0)+matched-flag, but continue-in-switch would mis-bind.)
- ternary
?:— needs an AN_TERNARY node -> Track A (already flagged in track-a-c-frontend-shared-ir-touchpoints). - function pointers (decl + indirect call), global/
static constinitialisers (currently zero-init), multi-dim arrays, array/struct initialisers — Track C. - M3:
setjmp/longjmpneeds register-save/restore codegen -> Track A; varargs define (callee SysV ABI) -> Track C. initialisers — Track D. - M3:
setjmp/longjmpneeds register-save/restore codegen -> Track A; varargs define (callee SysV ABI) -> Track D. Also: lua/tiny-regex sources are not staged in this worktree (library_candidates/is absent here; it lives in the master checkout), so M1/M4 cannot be compiled here until staged.
- 2026-06-25 — PARKED for cross-track merge (Track A/B/C sync). Branch is in steady, green, self-host-byte-identical state. One shared-IR touch (ir.inc AN_EXIT->Halt) is documented in track-a-c-frontend-shared-ir-touchpoints for the sister agents to reconcile.
- 2026-06-25 — LUA STAGED + first real blocker located (empirical).
Fetched
lua-5.4.7intolibrary_candidates/lua(gitignored — vendor source, not committed). The live compiler already parses several core files:lctype.creaches "main function not found" = full parse OK (a library, no main, like tiny-regexre.c).lzio.c/lmem.c/lobject.cfail. Reduced the failure to a minimal no-header reproducer: function pointers.BinOp f = add; f(3,4);→pascal26: error: call to undeclared function.c.op(5,6)(indirect call through a fn-ptr struct field) — same family.- lua hits this immediately:
lua.hdefineslua_CFunction/lua_Reader/… asret (*Name)(args), and the core calls through fn-ptr fields (z->reader(L, z->data, &size)in lzio.c is the exact first failure). Root cause: C fn-ptr typedefs register as plaintyPointerwith the argument signature not modelled (cparser.inc ~2094), soAN_CALL_INDhas no signatureProcs[]index to marshal with, and a bare function name decays to0instead of its address. The IR primitives already exist and are proven on the Pascal side:AN_CALL_IND(parser.inc ParseProcVarCallAST),AN_PROCADDR,SymProcSig/UFldProcSig(defs.inc:772, symtab.inc:509), and the C-ABIProcCdeclflag. Next slice (Track C, binary-editing —make test+ self-host + cross gate): function pointers —
- fn-ptr typedef
ret (*Name)(params)→ synthesize a signatureProcs[]entry (RegisterProc+BodyAddr:=-1+ProcCdecl:=True); store its index on the typedef (newCTypedefProcSigslot). - var/field of that typedef → set
SymProcSig/UFldProcSigfrom the typedef. - call sites in cparser ParseCPrimary/ParseCPostfix: bare function name (not
followed by
() →AN_PROCADDR;name(args)wherenameis a var withSymProcSig>=0→AN_CALL_IND; postfixfield(args)withUFldProcSig>=0→AN_CALL_IND. Remaining lua blockers after fn-pointers (from the reframe): switch/case + ternary + setjmp/longjmp (Track A, shared-IR), varargs-define (Track C), and multi-file linking (lua core = 34.c; no upstream amalgamation — build a one-unit include shim or add object linking).
- 2026-06-25 — Function pointers DONE (Slice B inc3). Typedef'd fn-ptr types,
indirect calls through a variable / struct field / parameter, and bare
function-name decay to address — all four forms match gcc. Implementation:
ParseCDeclType's existing
(*name)(params)declarator-skip now also captures the declarator name + builds a signatureProcs[]entry (RegisterProc+BodyAddr:=-1+ProcCdecl:=Truefor the System V ABI), exposed via new globalsCTypeProcSig/CTypeFnPtrName. ParseCTypedef registers the typedef with that signature (CTypedefProcSigslot); local-decl / struct-field / param sites thread it ontoSymProcSig/UFldProcSig; ParseCPrimary lowers a bare function name toAN_PROCADDRandvar(args)toAN_CALL_IND, and ParseCPostfix lowersrec.field(args)/p->field(args)toAN_CALL_INDvia the newRecFieldProcSigaccessor. All shared IR (AN_CALL_IND/AN_PROCADDRalready proven on the Pascal side) — no codegen edits. LANDMINE (cost ~30 min): the recursive param-type parse must beParseCDeclType()with parens — bareParseCDeclTypereads the function's own Result (pxx bare-funcname rule) → infinite loop (the exact landmine this ticket logged for Slice B inc1). Gate:make testgreen, self-host byte-identical, all 8 C fixtures match gcc; new fixturetest/cfnptr_b6.c(=91) wired intoc-interop-devtest. NB this was NOT lzio.c's first blocker — lzio/lmem/lobject still stop at an earlierunexpected token ((a different construct; SrcPos shows a NUL between newlines — a preprocessor artifact to investigate next). lua.h's fn-ptr typedefs (lua_CFunction/lua_Reader/…) now model correctly regardless. - 2026-06-25 — typedef of a struct tag now aliases the record (Slice B inc4).
typedef struct Zio ZIO;(no body, with a tag) was registered as an opaquetyPointer, dropping the record id — soZIO *z; z->fieldresolved against REC_NONE (field offset 0, fn-ptr field calls unrecognised →unexpected token (). Now the no-body aggregate-typedef branch aliases the tag's (possibly forward) record viaFindOrForwardCTag:typedef struct T X;→Xis the record;typedef struct T *X;→ pointer to it; tagless stays an opaque pointer. A laterstruct T { ... }body fills the same forward record, soz->field(incl. the lua ZIO reader fn-ptr field) resolves. This is the lua pattern (typedef struct Zio ZIO;,typedef struct lua_State lua_State;then the body in lstate.h — L->top now resolvable). Gate:make testgreen, self-host byte-identical, all C fixtures match gcc; fixturetest/ctypedef_struct_b7.c(=51). - 2026-06-25 — forward-record field base re-anchored (Slice B inc5). The real
lzio.c
z->reader(...)still failed after the alias fix; root cause was NOT the#include/preproc path but a contiguous-field-range bug exposed by the alias fix.FindUFieldscans a record's fields as a flat blockUFldNOff[UClsFBase[ci] .. +UClsFCount[ci]]. A forward record (typedef struct Zio ZIO;) hadUClsFBasefixed at forward-declaration time; lua's lzio.h then lays out an interveningtypedef struct Mbuffer {…} Mbuffer;BEFORE thestruct Zio { … }body, so Zio's body fields land past its recorded base andFindUField('reader')returns -1 (field — and thus its fn-ptr sig — invisible; every other field silently aliased offset 0). Fix: ParseCStructInto re-anchorsUClsFBase[ci] := UFldCountwhen it begins laying out a body that has no fields yet (UClsFCount[ci]=0) — a no-op for freshly-created records, correct for forward ones. Diagnosis landmine: the symptom looked like a fn-ptr-sig/#includebug (UFldProcSig read as -1) but was a field-LOOKUP failure (FindUField=-1); only a minimal bisect (a 3-field intervening struct typedef) exposed it — single intervening fields didn't shift the base enough to interleave. Gate:make testgreen, self-host byte-identical, all 9 C fixtures match gcc; fixturetest/cstruct_fwd_interleave_b8.c(=42). lzio.c now parses pastluaZ_filland reachesluaZ_read. - 2026-06-25 — ternary
?:DONE (newAN_TERNARYnode). Real lzio.c stopped inluaZ_readatm = (n <= z->n) ? n : z->n;. Implemented the C conditional operator end-to-end: newAN_TERNARYAST node (Left=cond, Right=AN_PAIR(then, else), ASTTk=then-branch type); ParseCExpr parses it between logical-or and assignment, right-associative; IR lowering is fully target-independent — a hidden temp (AllocVarduring lowering, same idiom as IRLowerClassMatch) +IR_JUMP_IF_FALSE/labels/IR_STORE_SYM/IR_LOAD_SYM, so only the taken branch is evaluated and NO per-backend codegen was needed. Logged intrack-a-c-frontend-shared-ir-touchpoints(a new shared node) for A to reconcile. Self-host byte-identical (AN_TERNARY is C-frontend-only; the Pascal self-compile never emits it);make testgreen; fixturetest/cternary_b9.c(=37, nested + only-taken-branch side effect proves no double-eval). lzio.c now compiles clean (full parse — it is a library, no main). - 2026-06-25 — LUA CORE SURVEY: 3 / 34 files parse clean (lctype, lzio, + 1),
with the remaining blockers triaged by instrumenting the "undeclared call" name:
- Function-like macro expansion bugs — the biggest cluster.
call to undeclared functionis mostly an UNEXPANDED function-like macro parsed as a call:cast(#define cast(t,exp) ((t)(exp))in llimits.h),novariant, etc. IMPORTANT: function-like macros ALREADY work in general (cpreproc.inchasCPExpandFunction+ arg parsing) — lzio.c'scast_uchar(...)compiled clean, AND an isolated repro of the full cast family (cast/cast_int/cast_uint/cast_uchar, nested) expands correctly and runs. So lstring.c'scast-reaches-parser failure is context-specific, NOT one common cause — the cluster is probably several distinct preprocessor edge cases (token-paste##, stringize#, a multi-token type arg in a particular position, aCPMacroIsActiverecursion-guard interaction, or a macro that re-defines/undefs). Next step: bisect ONE failing file (lstring.c) down to the exact unexpanded use, then widen. Still likely the highest-leverage Track C cluster, but expect a handful of small fixes rather than a single feature.- UPDATE — first two fixed/traced. (a) Integer literal SUFFIXES
(
U/L/UL/LL/ULL) were never consumed by the C lexer, leaving e.g.ULas a dangling identifier that broke const expressions like(0xffffffffffffffffUL / sizeof(t))(DONE — clexer consumes the suffix; fixturetest/cint_suffix_b10.c=42). (b) lstring.c'scastfailure then traced NOT to the suffix but to realMAX_SIZET = ((size_t)(~(size_t)0))— the~bitwise-NOT const-eval, already filed asbug-c-const-eval-bitwise-not(Track C). So the macro "cluster" is indeed several independent small const-eval/lexer bugs, confirmed. Next: fixbug-c-const-eval-bitwise-not, then re-survey.
- UPDATE — first two fixed/traced. (a) Integer literal SUFFIXES
(
__builtin_offsetof(lfunc.c) — gcc builtin foroffsetof; map to a compile-time field offset (Track C).switch/case(lgc.c parsesswitchas a call) — Track A (break-only- scope shared-IR primitive per the reframe).Unsupported linear node in IR codegen(ldebug.c) — an IR/codegen gap to isolate (could be Track A).- A few
unexpected token (/expected C expression(lparser.c, lobject.c, ltable.c) — residual C constructs to bisect after macros land (many may be downstream of the missing macro expansion). RECOMMENDED ORDER: (1) function-like macro expansion [Track C, big, unblocks most], (2)__builtin_offsetof[Track C, small], (3)switch/case[Track A], (4) re-survey, thensetjmp/longjmp[Track A] + multi-file linking (no upstream amalgamation; build a one-unit include shim or add object linking) for M4.
- Function-like macro expansion bugs — the biggest cluster.
2026-06-26 — BREAKTHROUGH: varargs done; lua compiles + links as a whole
- C varargs implemented (commits ~76-79): ProcVariadic detection, hidden __va_save register-save area + additive variadic-gated prologue, stdarg.h with pure-C _pxx_va* helpers, __builtin_va_start/va_arg/va_end desugaring, and the IR_CALL excess-arg fix (pop all pushed args into SysV regs, not just ParamCount). Self-host byte-identical throughout. Int/long/pointer/string varargs verified == gcc.
- lua core parse: 29 -> 33/34 (lapi/lauxlib/ldebug/lobject now parse; only luac.c left, on an unrelated "Unsupported linear node" codegen gap).
- lua TEST-COMPILES AS A WHOLE via amalgamation (one TU = every core .c + lua.c): a 744 KB dynamic ELF, DT_NEEDED libc.so.6 + libm.so.6, all 86 libc/libm imports resolve (commit 80 defaults C externs to libc/libm sonames). Recipe in library_candidates/lua/BUILD-pxx.md.
- Remaining for a RUNNING lua (3 filed/known): bug-c-large-record-byval-param (24-byte va_list by value -> luaO_pushvfstring segfault at startup), bug-c-double-vararg (%f reads 0), and luac.c's codegen node. No multi-file linker needed — amalgamation covers it.
2026-06-26 (cont) — struct-by-value fixed; lua RUNS non-IO code
- bug-c-large-record-byval-param CLOSED: C struct-by-value params of any size (isRef pointer slot + caller copy, CProgramMode-gated, byte-identical). va_list passing now works (was the luaO_pushvfstring segfault).
- pxx-compiled lua now COMPILES + LINKS + RUNS non-IO Lua (rc 0).
print/IO still segfaults -> bug-c-libc-data-symbol-stdio (stdout/stderr/stdin are libc DATA symbols, not imported; need a COPY relocation). That + bug-c-double-vararg- global-array-init are the remaining run blockers.
M5 (sqlite) — kickoff 2026-06-27
sqlite 3.46.0 amalgamation fetched + wired into tools/install_lib_candidates.sh
(sqlite target, pinned SHA, gitignored vendor). First-compile walls:
- Wall 1 (capacity, cleared):
token overflow—MAX_TOKENS512K too small for the 257k-line TU. Bumped to 2M. Tracked in [[chore-sqlite-static-capacity-bumps]]; proper fix [[feature-dynamic-compiler-tables]]. - Wall 2 (open bug):
invalid symbol in lea(ir.inc:296verifier) — anIR_LEAwith an out-of-range sym index. Not capacity (SymCount guarded), not the plain string-pointer-array path (repro negative), reported line stale. Filed [[bug-c-invalid-symbol-in-lea-sqlite]] — next focused debug task.
lua remains functional (incl. float). Cross/ESP coverage of C+lua filed as [[feature-c-cross-target-feature-coverage]].
M5 (sqlite) — session 3 (2026-06-27): lea wall was operator error
The "invalid symbol in lea" wall (session 2) was not a compiler bug — sqlite
was compiled without -Ilib/crtl/include, so <stdarg.h> resolved to the
system header and va_arg did not hit pxx's __builtin_va_arg desugar (the
already-fixed [[track-c-va-arg-nonint-lea]] shape). Rejected the ticket.
Tooling added: --dump-cpp flag (prints the C-preprocessed source and exits)
— invaluable for this; used it to confirm azCompileOpt[] is all clean strings.
With -Ilib/crtl/include sqlite advances much further, to:
- Next wall:
pascal26:19490: error: unexpected tokennearsqlite3Config/int (*xAltLocaltime)(const void*,void*)— a function-pointer struct field (member ofstruct Sqlite3Config). Likely pxx can't parse a function-pointer member declarationT (*name)(args)inside a struct. NEXT TASK.
Real bugs banked from the investigation (independent of the false alarm):
- [[bug-c-sizeof-array-yields-element-size]] —
sizeof(arr)= element size. - [[bug-c-addr-of-global-array-element-const-index-wrong-offset]] —
&g[const]wrong offset (7/221 vs 204). - [[bug-capital-write-undefined-in-compiler-selfbuild]] — capital
Writegap.
LESSON / possible feature: C compiles require -Ilib/crtl/include. Consider
auto-prepending pxx's crtl include dir for .c inputs (cf. the Pascal
default-PAL-dir behaviour), so real C programs build without the manual -I.
M5 update: default crtl include path DONE
The -Ilib/crtl/include requirement is resolved — [[feature-c-default-crtl-include-path]] auto-searches pxx's crtl headers for .c inputs (ExeDir+CWD anchored), -nostdinc opts out. sqlite + the C tests now build with no manual -I. The real M5 wall is the function-pointer struct field (xAltLocaltime) — NEXT.
M5 update: fn-ptr struct member (layout + call) DONE
Inline RET (*name)(params) struct members now lay out + call correctly (both [[bug-c-function-pointer-struct-member]] and [[bug-c-call-inline-function-pointer-struct-member]] done). sqlite passes the xAltLocaltime call; next wall: pascal26:20679: error: expected C expression.
M5 update (2026-06-27, session 4): four fn-ptr/bit-field walls cleared
Banked, each with a regression test, self-host byte-identical, full make test
green. sqlite advances 20679 -> 26103 -> 30088 -> 31615.
- fn-ptr LOCAL variable
RET (*name)(params) = init;(was pascal26:20679). [[bug-c-function-pointer-local-variable]] DONE. ParseCLocalDeclAST allocates the local under CTypeFnPtrName as a callable pointer. test cfnptr_local_b95. - fn-ptr PARAMETER
RET (*name)(params)(sqlite3ThreadCreate's xTask, was 25444 "call to undeclared function: xTask"). ParseCSubroutine read the param name via the ident-read, which ParseCDeclType had already consumed -> param registered asargN, body'sxTaskundeclared. Now uses CTypeFnPtrName. - bit-fields (was 26103). A struct with a bit-field — incl. via the nested
struct sqlite3InitInfo {...:1;} init;— fell back to an opaque pointer, dropping every field incl. sibling fn-ptrs, sodb->xProgress(...)couldn't resolve. Now laid out as full storage units; only anonymous bit-fields still opaque. test cstruct_bitfield_b96. - call through fn-ptr CAST
((RET(*)(params))e)(args)(was 30088). sqlite's syscall-tableosOpen == ((int(*)(...))aSyscall[0].pCurrent)in an if-condition. Abstract(*)(params)declarators now get a CTypeProcSig; the AN_PTR_CAST carries it (ASTRight) and CNodeProcSig reads it -> indirect C-ABI call. test cfnptr_cast_call_b97.
Next wall: pascal26:31615: error: call to undeclared function: fsync — a
libc syscall sqlite calls expecting <unistd.h> to declare it; pxx's crtl
headers don't. Filed [[bug-c-crtl-missing-unistd-syscalls]].
Runtime wall banked for later: a GLOBAL struct-array initializer with a
fn-ptr cast field (aSyscall[0].pCurrent = (syscall_ptr)posixOpen at file
scope) stores garbage -> segfault on the indirect call; runtime-assigned works.
Filed [[bug-c-global-struct-array-fnptr-cast-init]]. sqlite's aSyscall table will
hit this once it links.