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.
320 lines
11 KiB
C
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;
|
|
}
|