Files
ww/test/wcc/946_structret_run.c
Hojun-Cho c882bcf27c wcc: struct-return float fields via SSE return regs (#171a)
The RETURN twin of #165: a qualifying float-struct was returned GP-only
(struct{f64,f64} in AX/DX instead of X0/X1) — value-correct via GP transport
but not SysV register-class conformant. Route each float eightbyte through the
SSE return cursor (X0/X1) and each integer eightbyte through GP (AX/DX) via
independent cursors, at the struct-return SEND and RECV, both stages, reusing
struct_float_class verbatim. Closes the temporary tuple-SSE/struct-GP
divergence opened across #164/#165.

A qualifying struct has >=1 lone f64 so maxalign is 8 and the ABI slot is an
8-multiple — no sub-8 tail — so #169's sized tail is unreachable here and the
integer eightbyte uses a full MOVQ (cstage agrees, proven by the f64i32
cs==ww byte-id). f32 / multi-float-per-eightbyte stays GP (deferred #171b);
>16B stays sret.

Gate-blind and value-correct, so the discriminator is the SEND/RECV register
class (MOVSD X0/X1 vs MOVQ AX/DX) — covered by probe 946_structret_run.
2026-05-28 01:50:28 +09:00

320 lines
11 KiB
C

/*
* 946_structret_run — runtime + byte-id + asm-pattern net for #171a, the
* float-bearing struct-RETURN SysV ABI (the return twin of #165's struct
* PARAM, and the struct counterpart of #164's per-element tuple RETURN).
*
* THE BUG (#171a, cs==ww but SysV-non-conformant on master): a <=16B
* struct returned by value materialised into a zero-padded 24B scratch and
* then loaded unconditionally into the INTEGER return regs (AX/DX/CX); the
* let-init receive MOVQ'd them back from the same GP regs. So a
* `struct { a: f64, b: f64 }` return rode AX/DX instead of X0/X1. For a
* pure INTERNAL ww call (both ends compiled by the same stage) the f64
* bits still round-trip through the GP regs intact, so the runtime VALUE
* was correct AND both stages were symmetric-GP — the cs==ww gate and a
* value check are therefore NECESSARY-NOT-SUFFICIENT here (cf.
* 946_structparam_run, the param twin). The genuine defect is SysV
* register-CLASS conformance, observable only in the emitted asm (and at a
* real ABI boundary). The discriminating dimension is the asm pattern (c).
*
* THE FIX: per-EIGHTBYTE SysV classification (reusing struct_float_class /
* structfloatclass verbatim from #165). Each 8-byte eightbyte that is a
* lone f64 rides the SSE return cursor (X0,X1); a pure-INTEGER eightbyte
* rides the INTEGER return cursor (AX,DX) — on INDEPENDENT counters, so a
* float lands in the next XMM regardless of its positional eightbyte. SEND
* (cgreturn) loads the float eightbyte off the scratch into the next XMM;
* RECV (let-init) stores the XMM into the destination slot. Symmetric
* across cstage (cmd/w6c/cgen.c) and wwstage (selfhost/cmd/wcc/
* cgenstmt.ww).
*
* THE FALLBACK GATE: only a struct whose every eightbyte is pure-INT or a
* lone f64 qualifies. An f32 field packs two f32 into ONE SSE eightbyte
* (needs packing — deferred #171b); struct{f32,f32} therefore STAYS on the
* GP transport (correct + byte-identical current behavior). The f32f32 row
* asserts BOTH the correct round-tripped value AND the ABSENCE of an SSE
* return/receive, proving the gate caught it.
*
* Each row carries THREE dimensions:
* (a) cstage `ww build` + run — exit code (end-to-end round-trip).
* (b) w6c vs w6c_ww `.s` cmp — rule-10 byte-id (both stages identical).
* (c) asm-pattern: the producer `main.mk` returns a lone-f64 eightbyte
* as `MOVSD <off>(BP), Xn` (scratch -> XMM; a GP eightbyte is
* `MOVQ <off>(BP), AX`), and the consumer `main` receives it as
* `MOVSD Xn, -off(BP)` (XMM -> slot). The producer marker is scoped
* to `TEXT main.mk` (its scratch fill loads f64 literals via
* `MOVSD (SP), X0`, never `(BP), X0`); the consumer marker to
* `TEXT main,` (a GP recv MOVQ's instead). PRESENT for the SSE-routed
* rows, ABSENT for the GP / fallback rows. A pre-fix (or GP-regressed)
* build routes every eightbyte via AX/DX/CX -> both markers absent,
* which is exactly what (c) catches.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
static int
runwait(const char *cmd)
{
int rc = system(cmd);
if (rc == -1) return -1;
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
return -1;
}
struct row {
const char *label;
const char *src;
int want_exit;
int want_sse; /* 1 = struct return rides an XMM eightbyte */
};
static const struct row rows[] = {
/* HEADLINE — struct{f64,f64}. Both eightbytes lone f64 -> X0,X1. mk
* returns `MOVSD ..,X0` + `MOVSD ..,X1`; main receives `MOVSD X0,..`
* + `MOVSD X1,..`. s.a + s.b = 8.0. */
{ "f64f64_ret",
"package main;\n"
"type pff = struct { a: f64, b: f64 };\n"
"fn mk() pff = { return pff { a = 3.0, b = 5.0 }; };\n"
"export fn main() i32 = {\n"
"\tlet s: pff = mk();\n"
"\tif ((s.a: i64) + (s.b: i64) != 8) { return 1; };\n"
"\treturn 0;\n"
"};\n", 0, 1 },
/* CURSOR INDEPENDENCE — struct{f64,i32}: eb0 lone f64 (X0), eb1
* pure-INT (the i32 rides AX, NOT DX — the GP cursor starts at 0
* regardless of the float ahead of it). (s.a:i64)+(s.b:i64) = 8. */
{ "f64i32_ret",
"package main;\n"
"type pfi = struct { a: f64, b: i32 };\n"
"fn mk() pfi = { return pfi { a = 3.0, b = 5 }; };\n"
"export fn main() i32 = {\n"
"\tlet s: pfi = mk();\n"
"\tif ((s.a: i64) + (s.b: i64) != 8) { return 1; };\n"
"\treturn 0;\n"
"};\n", 0, 1 },
/* ORDER SWAP — struct{i64,f64}: eb0 pure-INT (AX), eb1 lone f64 (X0,
* the first float still gets X0). Confirms the float lands in the
* next XMM regardless of position. s.a + (s.b:i64) = 8. */
{ "i64f64_ret",
"package main;\n"
"type pif = struct { a: i64, b: f64 };\n"
"fn mk() pif = { return pif { a = 3, b = 5.0 }; };\n"
"export fn main() i32 = {\n"
"\tlet s: pif = mk();\n"
"\tif (s.a + (s.b: i64) != 8) { return 1; };\n"
"\treturn 0;\n"
"};\n", 0, 1 },
/* GP REGRESSION — struct{i64,i64}: every eightbyte pure-INT, no float
* to route, so it STAYS on the AX/DX/CX transport unchanged (the
* in-tree byte-id case). No SSE return/receive marker. */
{ "i64i64_ret",
"package main;\n"
"type pii = struct { a: i64, b: i64 };\n"
"fn mk() pii = { return pii { a = 3, b = 5 }; };\n"
"export fn main() i32 = {\n"
"\tlet s: pii = mk();\n"
"\tif (s.a + s.b != 8) { return 1; };\n"
"\treturn 0;\n"
"};\n", 0, 0 },
/* FALLBACK GATE — struct{f32,f32}: two f32 pack into ONE SSE eightbyte
* (8B struct). The gate rejects f32, so the struct stays GP-routed
* (returned/received via AX). The value still round-trips (the 8B MOVQ
* carries both f32) -> (s.a:i64)+(s.b:i64) = 8. Asserts BOTH the
* correct value AND no SSE return/receive, proving the gate caught it
* (NOT routed to an SSE reg). The 2-f32-per-eightbyte packing is
* #171b. */
{ "f32f32_ret_fallback",
"package main;\n"
"type pf32 = struct { a: f32, b: f32 };\n"
"fn mk() pf32 = { return pf32 { a = 3.0f32, b = 5.0f32 }; };\n"
"export fn main() i32 = {\n"
"\tlet s: pf32 = mk();\n"
"\tif ((s.a: i64) + (s.b: i64) != 8) { return 1; };\n"
"\treturn 0;\n"
"};\n", 0, 0 },
{ NULL, NULL, 0, 0 }
};
static int
slurp_eq(const char *a, const char *b)
{
FILE *fa = fopen(a, "rb");
FILE *fb = fopen(b, "rb");
if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return -1; }
int rc = 0;
for (;;) {
int ca = fgetc(fa);
int cb = fgetc(fb);
if (ca != cb) { rc = -1; break; }
if (ca == EOF) break;
}
fclose(fa); fclose(fb);
return rc;
}
/* Isolate one function's TEXT block: from `head` to the next `\nTEXT `.
* Returns a malloc'd copy the caller frees, or NULL. */
static char *
isolate(const char *buf, const char *head)
{
const char *start = strstr(buf, head);
if (!start) return NULL;
const char *end = strstr(start + 1, "\nTEXT ");
size_t len = end ? (size_t)(end - start) : strlen(start);
char *out = malloc(len + 1);
if (!out) return NULL;
memcpy(out, start, len);
out[len] = '\0';
return out;
}
/* Does the struct return ride an XMM on BOTH ends?
* - producer `main.mk`: a lone-f64 eightbyte is loaded scratch -> XMM as
* `MOVSD\t<off>(BP), X0` (the only `(BP), X0` in mk; its f64-literal
* fill loads via `MOVSD (SP), X0`). A GP-routed return is `MOVQ ..,AX`.
* - consumer `main`: the eightbyte is stored XMM -> slot as
* `MOVSD\tX0, -<off>(BP)`. A GP recv is `MOVQ AX, ..`.
* Returns 1 iff both markers present, 0 iff both absent, -1 on a split or
* scan error (which would itself be a bug). */
static int
ret_rides_sse(const char *path)
{
FILE *f = fopen(path, "rb");
if (!f) return -1;
static char buf[1 << 18];
size_t n = fread(buf, 1, sizeof buf - 1, f);
fclose(f);
buf[n] = '\0';
char *mk = isolate(buf, "TEXT main.mk");
char *mn = isolate(buf, "TEXT main,");
if (!mk || !mn) { free(mk); free(mn); return -1; }
int send = strstr(mk, "(BP), X0") != NULL;
int recv = strstr(mn, "MOVSD\tX0, -") != NULL;
free(mk); free(mn);
if (send != recv) return -1; /* SEND and RECV must agree */
return send;
}
int
main(void)
{
const char *bin = getenv("BIN");
if (!bin) bin = "out/bin";
char absbin[2200];
if (bin[0] != '/') {
char cwd[1024];
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
bin = absbin;
}
char w6c[2300], w6c_ww[2300];
snprintf(w6c, sizeof w6c, "%s/w6c", bin);
snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin);
if (access(w6c_ww, X_OK) != 0) {
fprintf(stderr, "structret: w6c_ww missing — cannot run the "
"cs==ww byte-id gate\n");
return 1;
}
int n = 0, fail = 0;
for (int i = 0; rows[i].src; i++, n++) {
char src[64];
snprintf(src, sizeof src, "/tmp/wwsrt_%d_%d.ww", getpid(), i);
FILE *f = fopen(src, "wb");
if (f == NULL) { fail++; continue; }
fputs(rows[i].src, f);
fclose(f);
char cmd[4096];
/* (a) cstage build + run in a scratch dir. */
char tmpdir[64];
snprintf(tmpdir, sizeof tmpdir, "/tmp/wwsrt_%d_d_%d",
getpid(), i);
mkdir(tmpdir, 0755);
snprintf(cmd, sizeof cmd, "cd %s && %s/ww build %s",
tmpdir, bin, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: cstage build failed\n",
rows[i].label);
fail++;
unlink(src); rmdir(tmpdir);
continue;
}
char outbin[128];
const char *base = strrchr(src, '/');
base = base ? base + 1 : src;
snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base);
char *dot = strrchr(outbin, '.');
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
int got = runwait(outbin);
if (got != rows[i].want_exit) {
fprintf(stderr, "row[%s]: cstage exit %d, want %d\n",
rows[i].label, got, rows[i].want_exit);
fail++;
}
unlink(outbin); rmdir(tmpdir);
/* (b) cs==ww byte-id gate: emit .s from both stages, cmp. */
char cs_s[64], ws_s[64];
snprintf(cs_s, sizeof cs_s, "/tmp/wwsrt_%d_%d_cs.s",
getpid(), i);
snprintf(ws_s, sizeof ws_s, "/tmp/wwsrt_%d_%d_ww.s",
getpid(), i);
snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null",
w6c, cs_s, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: w6c failed\n", rows[i].label);
fail++; unlink(src); continue;
}
snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null",
w6c_ww, ws_s, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: w6c_ww failed\n",
rows[i].label);
fail++; unlink(src); unlink(cs_s); continue;
}
if (slurp_eq(cs_s, ws_s) != 0) {
fprintf(stderr,
"row[%s]: cstage/wwstage .s DIFFER (rule-10 "
"byte-id violation)\n", rows[i].label);
fail++;
}
/* (c) asm-pattern discriminator on the return/receive. byte-id
* (b) proves ww_s mirrors cs_s, so checking cs_s suffices. */
int sse = ret_rides_sse(cs_s);
if (sse < 0) {
fprintf(stderr, "row[%s]: cannot scan .s (or SEND/RECV "
"disagree)\n", rows[i].label);
fail++;
} else if (sse != rows[i].want_sse) {
fprintf(stderr,
"row[%s]: struct return SSE %s, want %s "
"(register-class discriminator)\n",
rows[i].label, sse ? "present" : "absent",
rows[i].want_sse ? "present" : "absent");
fail++;
}
unlink(src); unlink(cs_s); unlink(ws_s);
}
if (fail) {
fprintf(stderr, "%d/%d struct-return tests failed\n", fail, n);
return 1;
}
printf("structret: %d/%d ok (cstage run + cs==ww byte-id + "
"SSE-return asm)\n", n, n);
return 0;
}