← board

C desktop path — compile real portable C (tiny-regex → lua → sqlite)

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.

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.

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__).

M5 update (2026-06-27, session 8): getpid + ternary middle comma

Two small sqlite-advance fixes:

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.

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 ].

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.

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.

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.

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.

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:

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)

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

Testing

Landmines

Log

2026-06-26 (cont) — struct-by-value fixed; lua RUNS non-IO code

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:

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:

Real bugs banked from the investigation (independent of the false alarm):

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.

  1. 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.
  2. 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 as argN, body's xTask undeclared. Now uses CTypeFnPtrName.
  3. 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, so db->xProgress(...) couldn't resolve. Now laid out as full storage units; only anonymous bit-fields still opaque. test cstruct_bitfield_b96.
  4. call through fn-ptr CAST ((RET(*)(params))e)(args) (was 30088). sqlite's syscall-table osOpen == ((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.