C frontend + lua — cross-target / ESP feature coverage
- Type: feature (test coverage) — Track C (+ A for any backend gap found)
- Status: done
- Owner: fable-c
- Opened: 2026-06-27
2026-06-29 — empirical cross-C test + first layer fixed
Confirmed C was x86-64-only: every C program (even int main(void){return 42;})
crashed on i386/arm32/aarch64/riscv32, while the equivalent Pascal ran. Traced
to layer 1 — the program entry stub: ParseCProgram hand-emitted raw
x86-64 bytes (REX.W mov [m],rsp, syscall, exit_group 231) for the ELF
entry regardless of target, so on i386 the 0x48 REX prefixes decoded as garbage
→ SIGILL/SIGSEGV before main even ran.
Fixed for i386 (commit pending): the entry stub now dispatches on
TargetArch — x86-64 keeps edi/rsi + syscall; i386 uses the cdecl stack
([argc][argv] pushed) + int 0x80 exit_group(252). int main(void){return 42}
now exits 42 on i386 (guard test/ccross_entry.c in make test-i386). The other
backends (arm32/aarch64/riscv32) now raise a clear compile error ("C program
entry stub not implemented for this target yet") instead of silently emitting
x86-64 bytes that crash.
Remaining layers (still open), discovered by the same test:
-
arm32 entry + args — FIXED 2026-06-29. arm32 C entry stub (save sp via inline literal, r0=argc/r1=argv,
bl mainwith a 24-bit offset fixup,svcexit_group 248) + arm32 word-based C param spill (mirrors the Pascal arm32 path: word k in r[k] for k<4 else[fp+8+(pnWords-1-k)*4], Int64/Double = 2 words).return 42, 5-arg calls, recursion, struct+loop (cnoprintf→62) all run on arm32; guardsccross_entry.c+ccross_args.cinmake test-arm32. aarch64 also FIXED 2026-06-29 — entry stub (save sp via inline literal, x0=argc/x1=argv,bl mainimm26 fixup,svcexit_group 94) + aarch64 param spill (x0..x7 then[x29+16+(i-8)*8], mirrors the Pascal aarch64 path).return 42, args, recursion, cnoprintf→62 run on aarch64; guards inmake test-aarch64. riscv32 also FIXED 2026-06-30 — entry stub (save sp viaEmitLoadGlobAddrRISCV32, a0=argc/a1=argv,jal rapatched viaEncodeRISCVJAL,ecallexit_group 94) + riscv32 word-based param spill (a0..a7, mirrors the Pascal riscv32 path).return 42, args, recursion, cnoprintf→62 run on riscv32; guards inmake test-riscv32. The earlier "near-empty binary (procs=3)" was a misdiagnosis — riscv32 C lowering is fine; the small binary is just libc-free/minimal C. Non-varargs C now runs on ALL desktop/cross targets: x86-64 + i386 + arm32 + aarch64 + riscv32. -
C function call arg-passing on i386 — FIXED 2026-06-29. The C param-spill in
ParseCSubroutinewas hardcoded x86-64 (spill rdi/rsi/xmm…); it now has an i386 branch that copies the cdecl stack args ([ebp+8+sz], leftmost deepest) into the param slots, mirroring the Pascal i386 spill.id(42),add, 5-arg calls, recursion (fib), structs + loops (cnoprintf→ 62) all run on i386 now. Guardtest/ccross_args.cinmake test-i386. x86-64 unaffected; self-host byte-identical. Original diagnosis below for reference:cnoprintf.creturned 7 (= onlyp.x+p.y, struct fields ok;sum_to(10)returns 0). Minimal reproint id(int a){return a;} int main(){return id(42);}→ 0 on i386 (42 on x86-64); no-arg call + local arith are fine, so it is purely argument passing to C functions. Disassembly: the call site correctly does cdecl (mov eax,42; push eax; call id), butid's prologue spillsedi(mov [ebp-4], edi) — i.e. it reads the first param from the x86-64 first-arg register, and the param was given a negative (local-style) frame offset instead of[ebp+8]. So the i386 callee prologue uses the register-arg convention (correct for Pascal i386, which passes args in regs and spills them) while the cdecl call site passed on the stack → mismatch, param reads garbage/0. Fix must make C (cdecl) functions on i386 bind params from the stack ([ebp+8],[ebp+12], …) — touches the shared param-offset assignment- the i386 prologue, with self-host risk (Pascal i386 must stay green). Ties
to
feature-cdecl-indirect-cross-targets(cdecl honored only on x86-64).
- the i386 prologue, with self-host risk (Pascal i386 must stay green). Ties
to
-
C varargs call cross — printf-style
f(fmt, ...)fails to compile on i386/arm32/aarch64 withcall argument count mismatch (defaults not supported yet)(ir_codegen386.inc:2446— the call site rejects nArgs != ParamCount). The variadic call path is x86-64-only. i386 design note (2026-06-29): two coupled problems make this non-trivial. (a) Theva_listmodel inlib/crtl/include/stdarg.hkeys on a 176-byte SysV register-save area (gp/fp_offset) — i386 has no arg registers, all args are on the stack, so the model differs. (b) PXX's i386 calling convention pushes args left-to-right (leftmost deepest), the opposite of standard i386 cdecl (right-to-left, arg0 lowest), so variadic args land at decreasing addresses whileva_argwalks up — a simple stack-walk won't line up. Fixing i386 varargs likely needs either a right-to-left push for variadic C funcs (+ an i386va_startthat points at the first variadic stack slot and walks up, gp/fp_offset forced to the overflow path so__pxx_va_arg_*reuse the existing stack branch), or a dedicated i386 va model. The x86-64 variadic register-save prologue (cparser.inc) is also x86-64-only and needs an i386 no-op. Deferred — a dedicated piece.
Each is a separate Track-A backend gap; fix + guard incrementally per target.
Problem
The C frontend's bring-up — pointer model, double/float value model, va_arg,
struct-by-value returns, goto/labels, the whole lua arc — was verified almost
entirely on x86-64 only. The C make test entries and the pxx-compiled lua
smoke run against the x86-64 oracle. We have no evidence the same C programs
(and lua) produce correct results on the 32-bit and ESP targets.
Body lowering goes through shared IR, so cross should hold — but the generator/for-in work already proved x86-64-only testing misses real cross regressions ([[feature-c-desktop-lua-sqlite-path]] testing note). Float/double, va_arg FP-save-area, struct-by-value return slots, and pointer-width assumptions are exactly the areas where i386/arm32 (32-bit pairs) and xtensa/riscv32 (soft float, windowed/Call0 ABI) diverge.
Scope
- Run the existing C test programs (
test/c*.c,test/cnested_*, the value-model bN tests) under the cross harness: i386, arm32, aarch64, riscv32, and the ESP bare/QEMU path (tools/esp_run_bare.sh), each diffed vs the x86-64 oracle. - Run pxx-compiled lua (the functional script set — control flow, closures, varargs, generic-for, string lib, table.sort, metatables, pcall, float) on each target where it can run; at minimum the 32-bit + aarch64 native/QEMU.
- File a per-target Track A backend ticket for each gap (do not bloat this one).
Acceptance
- A
make-driven C cross matrix (analogous to the Pascal cross harness) that compiles + runs the C suite on i386/arm32/aarch64/riscv32 and diffs the x86-64 oracle; ESP via the bare/QEMU harness. - lua float + core script set verified on ≥ the 32-bit and aarch64 targets.
- Gaps found are filed as backend tickets and linked here.
Notes
- Deliberately deferred (2026-06-27, user): not blocking the sqlite milestone; Track C+A is proceeding to sqlite (M5) first. File-and-park.
- Related value-model landmines already mapped: 32-bit pair widths, FP save area for va_arg(double), struct/union-with-double byval return (r10), float negate = sign-bit flip, cmp NaN. See the done C double-value-model tickets.
Log
- 2026-06-27 - Filed while wrapping the FPC-seed fix. C/lua proven on x86-64 only; cross + ESP coverage is an open gap. Park behind the sqlite push.
- 2026-07-08 — resolved, commit b385a381.