wwstage: C-FFI variadic call codegen parity with cstage (#10)
Mirror cstage's C-variadic call handling in the ww self-host: parse a bare `...` param (decl.ww), skip param-keyed desugar for it to avoid a nil-deref (check.ww), and emit AL = XMM-reg count plus CVTSS2SD promotion of f32 args in the variadic tail (cgenutil.ww, cgenexpr.ww). Closes the cat-A wwstage silent miscompile (AL=0, unpromoted f32 tail). Parse/check/cgen are one atomic align-up (parse alone miscompiles, so not bisect-splittable). 989_ffivariadic now runs dual-stage (cstage ww + wwstage ww_ww), 12/12; w6c==w6c_ww byte-identical. Byte-id alone is blind here (the bootstrap calls no float-bearing C variadic), so the ww_ww runtime rows are the real net.
This commit is contained in:
9
Makefile
9
Makefile
@@ -835,8 +835,9 @@ $(BIN)/test_idxarg_run: test/wcc/989_idxarg_run.c \
|
||||
# fixture is built self-contained (zero relocs + zero undef syms, verified via
|
||||
# readelf -r / nm) so w6l links it with no libc, then wrapped in libffifix.a
|
||||
# under $(OUT)/ffivariadic (the harness finds it via $(BIN)/../ffivariadic).
|
||||
# cstage `ww` ONLY — wwstage AL is C2. Fixture flags are fixed (NOT $(CFLAGS),
|
||||
# which is -O0/-std=c99) to match ken's verified self-contained build.
|
||||
# Runs on BOTH cstage `ww` and wwstage `ww_ww` (C2). Fixture flags are fixed
|
||||
# (NOT $(CFLAGS), which is -O0/-std=c99) to match ken's verified
|
||||
# self-contained build.
|
||||
$(OUT)/ffivariadic/libffifix.a: test/wcc/data/ffivariadic/fixture.c
|
||||
@mkdir -p $(OUT)/ffivariadic
|
||||
$(CC) -O1 -fno-stack-protector -fno-asynchronous-unwind-tables \
|
||||
@@ -845,7 +846,9 @@ $(OUT)/ffivariadic/libffifix.a: test/wcc/data/ffivariadic/fixture.c
|
||||
|
||||
$(BIN)/test_ffivariadic_run: test/wcc/989_ffivariadic_run.c \
|
||||
$(OUT)/ffivariadic/libffifix.a \
|
||||
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
|
||||
$(BIN)/ww $(BIN)/ww_ww \
|
||||
$(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
|
||||
$(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \
|
||||
$(LIB)/libwwrt.a | $(BIN)
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
|
||||
@@ -119,6 +119,18 @@ fn parseparams(p: *parser) *node = {
|
||||
let pf: str = p.curfile;
|
||||
let pl: i32 = p.curline;
|
||||
let pc: i32 = p.curcol;
|
||||
// C-style FFI variadic: a bare `...` param (no name/type),
|
||||
// the last param. Tagged by str == "..." (distinct from the
|
||||
// Hare-style `T...` marked on n.op below). Mirrors cmd/wcc/
|
||||
// parse.c:113-122; check.c keys cu->variadic on this form.
|
||||
if (p.curkind == tkind.TK_ELLIPSIS) {
|
||||
advance(p);
|
||||
let cv: *node = newnode(nkind.N_PARAM, pf, pl, pc);
|
||||
cv.str = "...";
|
||||
if (head == nil) { head = cv; tail = cv; }
|
||||
else { tail.next = cv; tail = cv; };
|
||||
break;
|
||||
};
|
||||
let n: *node = newnode(nkind.N_PARAM, pf, pl, pc);
|
||||
// Param form: (IDENT|'_') ':' type. Anonymous-type-only params
|
||||
// (used in fn type expressions) aren't yet wired here.
|
||||
|
||||
@@ -7774,12 +7774,22 @@ fn cgcall(c: *cgen, n: *syntax.node) void = {
|
||||
};
|
||||
};
|
||||
};
|
||||
// C-variadic (bare `...`) detection: drives the SysV §3.5.7 AL=
|
||||
// XMM-count emit (below, before CALL) and the f32→f64 variadic-
|
||||
// tail promotion in pushargsrev (#14). cvarnfixed is the fixed-
|
||||
// param count, or -1 when the callee is not C-variadic. Mirror of
|
||||
// cstage's `cu && cu->kind == TY_FN && cu->variadic` gate.
|
||||
let cvarnfixed: i32 = -1;
|
||||
{
|
||||
let nfx: i32 = 0;
|
||||
if (calleecvariadic(c, callee, &nfx)) { cvarnfixed = nfx; };
|
||||
};
|
||||
// #38b: two-phase push — MEMORY-class (>48B tagged) args staged
|
||||
// first so they sit BELOW every register-class word; the pop loop
|
||||
// drains a strict prefix and never touches them. memwords feeds
|
||||
// the caller-cleanup ADDQ (with the mix guard below).
|
||||
let memwords: i32 = pushargsrev(c, n.list, calleeparams, true);
|
||||
let nargs: i32 = pushargsrev(c, n.list, calleeparams, false);
|
||||
let memwords: i32 = pushargsrev(c, n.list, calleeparams, true, 0, cvarnfixed);
|
||||
let nargs: i32 = pushargsrev(c, n.list, calleeparams, false, 0, cvarnfixed);
|
||||
// sret call (#23): callee returns plain TY_STRUCT > 24B. The
|
||||
// dest pointer lands in RDI; start intidx at 1 to skip RDI in
|
||||
// the user-arg pop loop and emit `LEAQ off(BP), DI` AFTER all
|
||||
@@ -7818,7 +7828,13 @@ fn cgcall(c: *cgen, n: *syntax.node) void = {
|
||||
let dparam: *syntax.node = calleeparams;
|
||||
let popped: i32 = 0;
|
||||
let stackslots: i32 = 0;
|
||||
let argidx: i32 = 0;
|
||||
for (a != nil) {
|
||||
// argidx is this arg's 0-based position; captured before any
|
||||
// continue so the C-variadic f32-promotion check below tracks
|
||||
// the push-side argidx for every arg shape (#14).
|
||||
let curargidx: i32 = argidx;
|
||||
argidx += 1;
|
||||
// #38b: MEMORY-class arg — its words sit below the pop
|
||||
// region and stay on the stack for the callee; nothing to
|
||||
// drain. Same param-keyed-else-arg-keyed detection as
|
||||
@@ -7879,6 +7895,12 @@ fn cgcall(c: *cgen, n: *syntax.node) void = {
|
||||
if (fk != 0) {
|
||||
let mov: str = "MOVSD";
|
||||
if (fk == 1) { mov = "MOVSS"; };
|
||||
// #14: a C-variadic-tail f32 was promoted to f64 at push
|
||||
// (CVTSS2SD + MOVSD), so its slot reloads MOVSD. Mirror
|
||||
// cstage cmd/w6c/cgen.c promote_f32 pop.
|
||||
if (cvarnfixed >= 0 && curargidx >= cvarnfixed && fk == 1) {
|
||||
mov = "MOVSD";
|
||||
};
|
||||
if (fpidx < 8) {
|
||||
emitline("\t");
|
||||
emitline(mov);
|
||||
@@ -8200,6 +8222,23 @@ fn cgcall(c: *cgen, n: *syntax.node) void = {
|
||||
emitline("(BP), DI\n");
|
||||
};};
|
||||
};
|
||||
// SysV §3.5.7: a C-variadic call sets AL to the number of vector
|
||||
// (XMM) regs used to pass the variable float args — the callee
|
||||
// gates its xmm-save-area stores on `test %al,%al`, so a wrong AL
|
||||
// makes va_arg(double) read garbage. fpidx is the XMM cursor
|
||||
// (capped at 8 in the pop loop). Emitted after any sret-RDI LEAQ,
|
||||
// right before CALL. Mirror cstage cmd/w6c/cgen.c (the imm→reg MOVQ
|
||||
// idiom carries AL since MOVL-imm has no w6a encoding; AL = low byte,
|
||||
// fpidx <= 8). Ref ref/qbe/amd64/sysv.c:384.
|
||||
if (cvarnfixed >= 0) {
|
||||
if (fpidx > 0) {
|
||||
emitline("\tMOVQ\t$");
|
||||
emitint(fpidx: i64);
|
||||
emitline(", AX\n");
|
||||
} else {
|
||||
emitline("\tXORQ\tAX, AX\n");
|
||||
};
|
||||
};
|
||||
if (isfnptrcall) {
|
||||
// Load fn-ptr field value into AX; CALL AX. We emit the
|
||||
// load AFTER the args have been popped (so AX/BX/etc
|
||||
|
||||
@@ -83,6 +83,46 @@ fn callee_variadic_param(c: *cgen, callee: *syntax.node, nfixed_out: *i32) *synt
|
||||
return findvariadicparam(ps, nfixed_out);
|
||||
};
|
||||
|
||||
// calleecvariadic — true when the callee is a C-style variadic fn: a
|
||||
// bare `...` param (str == "...", distinct from the Hare-style `T...`
|
||||
// findvariadicparam keys on op == TK_ELLIPSIS). nfixed_out gets the
|
||||
// count of fixed params before the `...`. Mirror of cstage's
|
||||
// `cu->variadic` gate (cmd/w6c/cgen.c cgcall), which check.c:902 sets
|
||||
// ONLY for the bare-`...` form — the bare `...` adds no Tparam, so
|
||||
// nfixed is the fixed-param count. Drives the SysV §3.5.7 AL=XMM-count
|
||||
// emit and the C-default-promotion of an f32 variadic-tail arg to f64
|
||||
// (#14). nfixed_out cannot be nil.
|
||||
fn calleecvariadic(c: *cgen, callee: *syntax.node, nfixed_out: *i32) bool = {
|
||||
*nfixed_out = 0;
|
||||
if (callee == nil) { return false; };
|
||||
let ps: *syntax.node = nil;
|
||||
if (callee.kind == syntax.nkind.N_IDENT) {
|
||||
if (callee.str.len == 0) { return false; };
|
||||
ps = fnparamslookup(c, callee.str);
|
||||
} else { if (callee.kind == syntax.nkind.N_DOT) {
|
||||
if (callee.str.len == 0) { return false; };
|
||||
let cmod: str;
|
||||
cmod.ptr = nil; cmod.len = 0;
|
||||
if (callee.lhs != nil) {
|
||||
if (callee.lhs.kind == syntax.nkind.N_IDENT) {
|
||||
cmod = callee.lhs.str;
|
||||
};
|
||||
};
|
||||
ps = fnparamslookupmod(c, callee.str, cmod);
|
||||
}; };
|
||||
let p: *syntax.node = ps;
|
||||
for (p != nil) {
|
||||
if (p.kind == syntax.nkind.N_PARAM) {
|
||||
if (syntax.streq(p.str, "...")) {
|
||||
return true;
|
||||
};
|
||||
*nfixed_out += 1;
|
||||
};
|
||||
p = p.next;
|
||||
};
|
||||
return false;
|
||||
};
|
||||
|
||||
// ---- expression cgen -------------------------------------------------
|
||||
|
||||
// pushargsrev — recursively walks the arg list, evaluates rightmost
|
||||
@@ -155,11 +195,17 @@ fn argtaggedwidensz(c: *cgen, arg0: *syntax.node, param: *syntax.node) i32 = {
|
||||
return pslot;
|
||||
};
|
||||
|
||||
fn pushargsrev(c: *cgen, arg: *syntax.node, param: *syntax.node, memphase: bool) i32 = {
|
||||
// argidx is this arg's 0-based position in the call's arg list;
|
||||
// cvarnfixed is the fixed-param count of a C-variadic callee (-1 when
|
||||
// the callee is not C-variadic). An f32 arg in the variadic tail
|
||||
// (argidx >= cvarnfixed) is promoted to f64 here (#14), so its 8B stack
|
||||
// slot holds a real double for the pop side and the callee's
|
||||
// va_arg(double).
|
||||
fn pushargsrev(c: *cgen, arg: *syntax.node, param: *syntax.node, memphase: bool, argidx: i32, cvarnfixed: i32) i32 = {
|
||||
if (arg == nil) { return 0; };
|
||||
let nextparam: *syntax.node = nil;
|
||||
if (param != nil) { nextparam = param.next; };
|
||||
let rest: i32 = pushargsrev(c, arg.next, nextparam, memphase);
|
||||
let rest: i32 = pushargsrev(c, arg.next, nextparam, memphase, argidx + 1, cvarnfixed);
|
||||
// Family C (#35): peel tagged→tagged casts FIRST so every gate
|
||||
// below keys on the operand — an identity cast reduces to the
|
||||
// ident fast path, a widening cast trips the widen branch with
|
||||
@@ -992,6 +1038,14 @@ fn pushargsrev(c: *cgen, arg: *syntax.node, param: *syntax.node, memphase: bool)
|
||||
cgexpr(c, arg);
|
||||
let mov: str = "MOVSD";
|
||||
if (fk == 1) { mov = "MOVSS"; };
|
||||
// #14: an f32 in the C-variadic tail widens to f64 (C default
|
||||
// argument promotion) — CVTSS2SD in X0, then MOVSD spills a
|
||||
// full 8B double; the pop reloads MOVSD and counts it as one
|
||||
// SSE reg. Mirror cstage cmd/w6c/cgen.c promote_f32 push.
|
||||
if (cvarnfixed >= 0 && argidx >= cvarnfixed && fk == 1) {
|
||||
emitline("\tCVTSS2SD\tX0, X0\n");
|
||||
mov = "MOVSD";
|
||||
};
|
||||
emitline("\tSUBQ\t$8, SP\n");
|
||||
emitline("\t");
|
||||
emitline(mov);
|
||||
|
||||
@@ -5610,6 +5610,18 @@ fn desugarcallargs(c: *checker, n: *syntax.node) void = {
|
||||
let nexta: *syntax.node = a.next;
|
||||
if (param != nil) {
|
||||
if (param.kind == syntax.nkind.N_PARAM) {
|
||||
// C-style FFI variadic (bare `...`): str=="...",
|
||||
// op != TK_ELLIPSIS, lhs == nil. The `...` absorbs
|
||||
// every remaining arg untyped — mirror cstage
|
||||
// check.c:1860 `if (!u->variadic) ...; continue`.
|
||||
// MANDATORY: this param's lhs is nil, so the per-arg
|
||||
// coercerunelit / checkarrlitfits / desugararrayslice
|
||||
// calls below would deref nil on a rune-literal or
|
||||
// array-literal variadic arg.
|
||||
if (syntax.streq(param.str, "...")
|
||||
&& param.op != syntax.tkind.TK_ELLIPSIS) {
|
||||
break;
|
||||
};
|
||||
if (param.op != syntax.tkind.TK_ELLIPSIS) {
|
||||
let atype: *syntax.node = exprtype(c, a, nil);
|
||||
// #29: a rune literal narrowing into an integer param
|
||||
|
||||
@@ -13,8 +13,9 @@
|
||||
* RUNTIME gate (byte-id can never see AL correctness): each row builds a ww
|
||||
* caller that calls the C fixture `double fixture(long n, ...)` (a
|
||||
* va_arg(double) summer, test/wcc/data/ffivariadic/fixture.c, linked from
|
||||
* libffifix.a) and asserts the returned sum. cstage `ww` ONLY — wwstage AL
|
||||
* is C2.
|
||||
* libffifix.a) and asserts the returned sum. Runs on BOTH the cstage `ww`
|
||||
* and wwstage `ww_ww` drivers (C2): AL is byte-id-blind, so a wwstage fi /
|
||||
* f32-promotion divergence is caught only by a wrong runtime sum here.
|
||||
*
|
||||
* NON-VACUITY DEVIATION (reported to lead): the spec's `fixture(2,1.0,2.0)`
|
||||
* is VACUOUS on this box — with AL=0 the two skipped xmm slots happen to
|
||||
@@ -174,22 +175,45 @@ main(void)
|
||||
bin = absbin;
|
||||
}
|
||||
|
||||
char cdrv[1024], libdir[1024];
|
||||
char cdrv[1024], wdrv[1024], libdir[1024];
|
||||
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
|
||||
snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin);
|
||||
/* libffifix.a lives beside $(BIN) under $(OUT)/ffivariadic — the
|
||||
* Makefile builds it there as a prereq of this test binary. */
|
||||
snprintf(libdir, sizeof libdir, "%s/../ffivariadic", bin);
|
||||
|
||||
/* C2: run each row on BOTH the cstage `ww` and the wwstage `ww_ww`
|
||||
* driver. AL correctness is byte-id-blind, so a wwstage fi/promotion
|
||||
* divergence is invisible to the 990-997 gates but caught here as a
|
||||
* wrong sum (nonzero exit). wwstage is access-gated like the other
|
||||
* dual-stage runtime tests (989_chainidx_run) so a cstage-only tree
|
||||
* still runs the cstage rows. */
|
||||
struct { const char *name; const char *drv; int gated; }
|
||||
drivers[] = {
|
||||
{ "cstage", cdrv, 0 },
|
||||
{ "wwstage", wdrv, 1 },
|
||||
{ NULL, NULL, 0 },
|
||||
};
|
||||
|
||||
int n = (int)(sizeof rows / sizeof rows[0]);
|
||||
int total = 0, fail = 0;
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
total++;
|
||||
int got = run_build(cdrv, libdir, &rows[i], i);
|
||||
if (got != rows[i].want_exit) {
|
||||
fprintf(stderr, "ffivariadic[%s]: exit=%d want=%d\n",
|
||||
rows[i].label, got, rows[i].want_exit);
|
||||
fail++;
|
||||
for (int d = 0; drivers[d].name; d++) {
|
||||
if (drivers[d].gated && access(drivers[d].drv, X_OK) != 0) {
|
||||
fprintf(stderr, "ffivariadic: skip %s (no %s)\n",
|
||||
drivers[d].name, drivers[d].drv);
|
||||
continue;
|
||||
}
|
||||
for (int i = 0; i < n; i++) {
|
||||
total++;
|
||||
int got = run_build(drivers[d].drv, libdir, &rows[i], i);
|
||||
if (got != rows[i].want_exit) {
|
||||
fprintf(stderr,
|
||||
"ffivariadic[%s][%s]: exit=%d want=%d\n",
|
||||
drivers[d].name, rows[i].label, got,
|
||||
rows[i].want_exit);
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user