← board

C varargs (va_list / va_start / va_arg) — implementation design

Current state (investigated 2026-06-26)

Chosen approach — System V save-area, fully contained

Gate EVERYTHING to variadic C functions so the Pascal self-host stays byte-identical (Pascal never emits C variadic functions; non-variadic prologues are untouched). NO change to the call site is needed.

1. va_list type (Track C)

Make va_list a real 24-byte struct (System V): struct { unsigned gp_offset, fp_offset; void *overflow_arg_area, *reg_save_area; } Pass it BY VALUE between functions — it carries reg_save_area (a pointer into the ORIGINATING function's frame, which is alive for the duration of the call), so a callee (lua_pushvfstring) can va_arg on its copy and read the originating function's saved registers. This matches lua's usage (luaL_error builds argp, hands it to lua_pushvfstring, never re-reads it).

2. Frontend parse (Track C)

3. Variadic prologue (Track A backend, ADDITIVE, x86-64)

Only when ProcVariadic: reserve a 176-byte save area on the frame and, at the top of the prologue, store the 6 GP arg regs (rdi,rsi,rdx,rcx,r8,r9 -> +0..+40) and, UNCONDITIONALLY, the 8 XMM regs (xmm0..7 -> +48..+168). Saving XMM unconditionally means the al register (vector-arg count) is irrelevant, so NO call-site change / no al-setup is needed — this is what keeps it contained.

4. va_start / va_arg codegen (Track A backend)

Why not the alternatives (all rejected)

Verification

Self-host byte-identical (changes gated to ProcVariadic). Test a variadic adder / printf-style consumer == gcc for int/long/ptr/char AND double args, and a va_list passed into a second function (lua's pattern). Then re-survey lua: lapi/lauxlib/ ldebug/lobject should reach parse-clean (29 -> 33). Linking/amalgamation remains separate.

Implementation progress (2026-06-26) — foundation laid + architecture proven

COMMITTED (safe, compiler unchanged -> self-host byte-identical):

So the codegen surface collapses to a MINIMAL, well-scoped remainder:

  1. FRONTEND (Track C): a ProcVariadic flag (set when a trailing ... param is seen, counting named GP/XMM params); and in ParseCPrimary's call path intercept the three builtins and DESUGAR them to existing AST:
    • __builtin_va_arg(ap, type) -> *(type*)( __pxx_va_arg_gp(&ap) ) for integer/pointer types, __pxx_va_arg_fp for float/double. (Parse the type arg like sizeof; choose gp vs fp by TypeIsFloat.)
    • __builtin_va_start(ap, last) -> field stores: ap.reg_save_area = &__va_save; ap.overflow_arg_area = <first stack arg>; ap.gp_offset = numNamedGP8; ap.fp_offset = 48 + numNamedXMM16; where __va_save is a hidden 176-byte local declared in the variadic function. (&__va_save is plain C — no intrinsic.)
    • __builtin_va_end(ap) -> no-op (0).
  2. BACKEND (Track A, ADDITIVE, gated to ProcVariadic, x86-64 only): right after the prologue, store the 6 GP arg regs (rdi,rsi,rdx,rcx,r8,r9) at &__va_save+0..40 and the 8 XMM regs at +48..+168. ~14 mov/movsd instructions. The frame just needs the 176-byte __va_save local reserved (the frontend declares it, so the frame sizing is automatic); the only special emission is the register stores. The overflow_arg_area (first stack arg, rbp+16) only matters for >6-GP-arg calls; lua's errors stay under that, but set it via a tiny lea for correctness.

Net: va_arg logic = done (C helper). va_start = field stores + one address-of a local (frontend). Only the register-save store sequence is new backend codegen, and it's purely additive/gated, so the self-host gate stays byte-identical.

Completion log