w6c: promote f32 arg to double in C-variadic tail (#14)

C99 6.5.2.2p6 requires float->double promotion for floats in the
variadic region of a call; w6c emitted MOVSS (4B), so a C variadic's
va_arg(double) read 8B of garbage. Promote at push (CVTSS2SD) so the
8B slot holds a real double, covering the XMM-reg and >8-float spill
cases uniformly; fi/AL XMM-count is unchanged.

cstage only; the wwstage twin rides C2 (#10), which adds wwstage's
C-variadic-call codegen.
This commit is contained in:
2026-06-21 11:14:14 +09:00
parent 294f4c93fd
commit 8f0ce09f2a
2 changed files with 78 additions and 5 deletions

View File

@@ -9395,6 +9395,23 @@ cgexpr(Cg *c, Node *n, Local *locals)
memarg[i] = memarg[i] =
tagged_memarg_size(args[i]->type); tagged_memarg_size(args[i]->type);
} }
/* C default argument promotion (#14): an f32 passed in the
* variadic region of a C-variadic call must widen to f64 — the
* callee reads it with va_arg(double) (8B). The region is every
* arg past the fixed params (the C `...` adds no Tparam). The
* push promotes via CVTSS2SD and stores a full 8B double; the
* pop then reloads MOVSD and counts it as exactly one SSE reg.
* Fixed params keep their declared width. Ref SysV §3.5.7;
* C99 §6.5.2.2p6. */
int promote_f32[64] = {0};
if (cu && cu->kind == TY_FN && cu->variadic) {
int nfixed = 0;
for (Tparam *p = callee_params; p; p = p->next)
nfixed++;
for (int i = nfixed; i < argcount; i++)
if (args[i] && node_isf32(args[i]))
promote_f32[i] = 1;
}
/* #38b MEMORY-class pre-pass: stage every >48B tagged arg on /* #38b MEMORY-class pre-pass: stage every >48B tagged arg on
* the stack BELOW all register-class words (rightmost-first, * the stack BELOW all register-class words (rightmost-first,
* so the leftmost mem arg lands at the lowest address = the * so the leftmost mem arg lands at the lowest address = the
@@ -9791,8 +9808,16 @@ cgexpr(Cg *c, Node *n, Local *locals)
* float class drives the width per ref/qbe * float class drives the width per ref/qbe
* amd64/emit.c:524 (slot-copy single→movss). The * amd64/emit.c:524 (slot-copy single→movss). The
* slot is 8B either way; the pop reads the same * slot is 8B either way; the pop reads the same
* width back. #143. */ * width back. #143.
int fmov = op_for(args[i], A_MOVSD, A_MOVSS); *
* #14: a variadic-region f32 is promoted to f64
* here (CVTSS2SD in X0) and spilled MOVSD, so the
* slot holds a real 8B double for the pop. */
int fmov = promote_f32[i] ? A_MOVSD
: op_for(args[i], A_MOVSD, A_MOVSS);
if (promote_f32[i])
ins2(c, A_CVTSS2SD, areg(D_X0),
areg(D_X0));
ins2(c, A_SUBQ, aimm(8), areg(D_SP)); ins2(c, A_SUBQ, aimm(8), areg(D_SP));
ins2(c, fmov, areg(D_X0), amem(D_SP, 0)); ins2(c, fmov, areg(D_X0), amem(D_SP, 0));
} else if (node_isstr(args[i])) { } else if (node_isstr(args[i])) {
@@ -10017,9 +10042,11 @@ cgexpr(Cg *c, Node *n, Local *locals)
if (fi < 8) { if (fi < 8) {
/* Reload the spilled f32/f64 at its class /* Reload the spilled f32/f64 at its class
* width — MOVSS for f32, MOVSD for f64 — * width — MOVSS for f32, MOVSD for f64 —
* matching the push above (#143). */ * matching the push above (#143). A #14
int fmov = op_for(args[i], A_MOVSD, * variadic-region f32 was promoted to f64
A_MOVSS); * at push, so its slot reloads MOVSD. */
int fmov = promote_f32[i] ? A_MOVSD
: op_for(args[i], A_MOVSD, A_MOVSS);
ins2(c, fmov, amem(D_SP, 0), ins2(c, fmov, amem(D_SP, 0),
areg(sysv_fargregs[fi])); areg(sysv_fargregs[fi]));
ins2(c, A_ADDQ, aimm(8), areg(D_SP)); ins2(c, A_ADDQ, aimm(8), areg(D_SP));

View File

@@ -79,6 +79,52 @@ static const struct row rows[] = {
" return 1;\n" " return 1;\n"
"};\n", "};\n",
0 }, 0 },
/* 3 f32 args (#14): C default arg promotion widens each to f64, so
* the callee's va_arg(double) reads 1.5+2.5+3.0 == 7.0. Values are
* exact in f32, so the f64 compare is exact. Unpromoted (MOVSS, 4B)
* the high 4B of each 8B slot is stale stack, so va_arg(double)
* pulls garbage and the sum misses 7.0 — this row FAILS pre-fix. */
{ "f32three",
"package main;\n"
"@symbol(\"fixture\") fn fixture(n: i64, ...) f64;\n"
"export fn main() int = {\n"
" let r: f64 = fixture(3, 1.5: f32, 2.5: f32, 3.0: f32);\n"
" if (r == 7.0) { return 0; };\n"
" return 1;\n"
"};\n",
0 },
/* Sharper than f32three: 0.1 is INEXACT in f32, so the f32-rounded
* value differs from the f64 literal 0.1. The expected side `(x: f64)`
* is the SAME f32 var widened at runtime (CVTSS2SD), so equality holds
* iff the variadic arg carried the f32-rounded value promoted to f64 —
* NOT the original f64 literal and NOT stale-high-bit garbage. Both a
* no-promote (MOVSS) and a hypothetical direct-f64 pass would miss it.
* Single f32 arg also exercises the fi==1 boundary. */
{ "f32inexact",
"package main;\n"
"@symbol(\"fixture\") fn fixture(n: i64, ...) f64;\n"
"export fn main() int = {\n"
" let x: f32 = 0.1: f32;\n"
" let r: f64 = fixture(1, x);\n"
" if (r == (x: f64)) { return 0; };\n"
" return 1;\n"
"};\n",
0 },
/* Mixed f32 + f64 in one variadic call: only the f32 args (1.5, 3.5)
* promote; the f64 arg (2.0) passes at its native width and must NOT be
* double-promoted or skewed. 1.5+2.0+3.5 == 7.0, exact in both widths. */
{ "f32mixed",
"package main;\n"
"@symbol(\"fixture\") fn fixture(n: i64, ...) f64;\n"
"export fn main() int = {\n"
" let r: f64 = fixture(3, 1.5: f32, 2.0, 3.5: f32);\n"
" if (r == 7.0) { return 0; };\n"
" return 1;\n"
"};\n",
0 },
}; };
/* run_build — build+run `src` via cstage `driver`, linking libffifix.a from /* run_build — build+run `src` via cstage `driver`, linking libffifix.a from