← board

C frontend + lua — cross-target / ESP feature coverage

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:

  1. arm32 entry + args — FIXED 2026-06-29. arm32 C entry stub (save sp via inline literal, r0=argc/r1=argv, bl main with a 24-bit offset fixup, svc exit_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; guards ccross_entry.c + ccross_args.c in make test-arm32. aarch64 also FIXED 2026-06-29 — entry stub (save sp via inline literal, x0=argc/x1=argv, bl main imm26 fixup, svc exit_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 in make test-aarch64. riscv32 also FIXED 2026-06-30 — entry stub (save sp via EmitLoadGlobAddrRISCV32, a0=argc/a1=argv, jal ra patched via EncodeRISCVJAL, ecall exit_group 94) + riscv32 word-based param spill (a0..a7, mirrors the Pascal riscv32 path). return 42, args, recursion, cnoprintf→62 run on riscv32; guards in make 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.

  2. C function call arg-passing on i386 — FIXED 2026-06-29. The C param-spill in ParseCSubroutine was 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. Guard test/ccross_args.c in make test-i386. x86-64 unaffected; self-host byte-identical. Original diagnosis below for reference:

    cnoprintf.c returned 7 (= only p.x+p.y, struct fields ok; sum_to(10) returns 0). Minimal repro int 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), but id's prologue spills edi (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).
  3. C varargs call cross — printf-style f(fmt, ...) fails to compile on i386/arm32/aarch64 with call 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) The va_list model in lib/crtl/include/stdarg.h keys 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 while va_arg walks 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 i386 va_start that 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

Acceptance

Notes

Log