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.
This commit is contained in:
2026-05-28 01:50:28 +09:00
parent d4e500f61c
commit c882bcf27c
6 changed files with 679 additions and 33 deletions

View File

@@ -355,6 +355,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
$(BIN)/test_tuprecv_f64_run \ $(BIN)/test_tuprecv_f64_run \
$(BIN)/test_tupparam_run \ $(BIN)/test_tupparam_run \
$(BIN)/test_structparam_run \ $(BIN)/test_structparam_run \
$(BIN)/test_structret_run \
$(BIN)/test_floats_run \ $(BIN)/test_floats_run \
$(BIN)/test_size_type_run \ $(BIN)/test_size_type_run \
$(BIN)/test_types_sizelim_run \ $(BIN)/test_types_sizelim_run \
@@ -1278,6 +1279,11 @@ $(BIN)/test_structparam_run: test/wcc/946_structparam_run.c $(BIN)/ww \
$(LIB)/libwwrt.a | $(BIN) $(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $< $(CC) $(CFLAGS) -o $@ $<
$(BIN)/test_structret_run: test/wcc/946_structret_run.c $(BIN)/ww \
$(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l \
$(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
sizelint: sizelint:
@sh tools/sizelint @sh tools/sizelint

View File

@@ -7717,6 +7717,40 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
* excludes them so they fall through to the existing scalar * excludes them so they fall through to the existing scalar
* path rather than emit a stomping MOVQ tail. Sizes >24B also * path rather than emit a stomping MOVQ tail. Sizes >24B also
* fall through (sret deferred, same constraint as #4). */ * fall through (sret deferred, same constraint as #4). */
/* #171a: float-bearing struct RECEIVE (the return twin of
* #165's param recv). cgexpr leaves each float eightbyte in
* its SSE return reg (X0,X1 = tuple_sse_seq) and each INT
* eightbyte in its INTEGER return reg (AX,DX = tuple_rseq),
* on INDEPENDENT cursors per SysV (ref/qbe/amd64/sysv.c retr)
* — so a float is read from the next XMM regardless of its
* positional eightbyte (struct{f64,i32}: e0←X0, e1←AX). A
* qualifying struct's size is maxalign-rounded to a multiple
* of 8 (an f64 forces align 8), so every eightbyte is a full
* word — the #169 sized tail (MOVL/MOVB) is unreachable here.
* struct_float_class gates to qualifying structs; all-int +
* f32 fall through to the GP recv below (byte-id / #171b). */
if (n->rhs && n->rhs->kind == N_CALL && lu
&& lu->kind == TY_STRUCT) {
int sclass[2], snb;
if ((snb = struct_float_class(lu, sclass)) > 0) {
cgexpr(c, n->rhs, *locals);
int gpcur = 0, ssecur = 0;
for (int e = 0; e < snb; e++) {
if (sclass[e]) {
ins2(c, A_MOVSD,
areg(tuple_sse_seq[ssecur]),
amem(D_BP, off + e * 8));
ssecur++;
} else {
ins2(c, A_MOVQ,
areg(tuple_rseq[gpcur]),
amem(D_BP, off + e * 8));
gpcur++;
}
}
break;
}
}
if (n->rhs && n->rhs->kind == N_CALL && lu if (n->rhs && n->rhs->kind == N_CALL && lu
&& lu->kind == TY_STRUCT && sz <= 24 && lu->kind == TY_STRUCT && sz <= 24
&& (sz % 8 == 0 || sz % 8 == 1 && (sz % 8 == 0 || sz % 8 == 1
@@ -8290,12 +8324,44 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
k += 1; k += 1;
} }
} }
ins2(c, A_MOVQ, amem(D_BP, scr + 0), /* #171a: float-bearing struct RETURN (the return
areg(D_AX)); * twin of #165's param recv). A qualifying struct's
ins2(c, A_MOVQ, amem(D_BP, scr + 8), * float eightbytes ride the SSE return row (X0,X1 =
areg(D_DX)); * tuple_sse_seq), its INT eightbytes the INTEGER
ins2(c, A_MOVQ, amem(D_BP, scr + 16), * return row (AX,DX = tuple_rseq), on INDEPENDENT
areg(D_CX)); * cursors per SysV (ref/qbe/amd64/sysv.c retr) — so a
* float lands in the next XMM regardless of its
* positional eightbyte (struct{f64,i32}: e0→X0, e1→AX,
* NOT DX). The scr slot is zero-padded to 24B, so a
* full MOVQ on a trailing INT eightbyte reads no
* garbage — the #169 sized tail is a RECV-only concern.
* struct_float_class gates to qualifying structs (>=1
* f64, every eightbyte lone-f64 or pure-INT); all-int +
* f32 keep the AX/DX/CX transport (byte-id / #171b). */
int sclass[2], snb;
if ((snb = struct_float_class(rt, sclass)) > 0) {
int gpcur = 0, ssecur = 0;
for (int e = 0; e < snb; e++) {
if (sclass[e]) {
ins2(c, A_MOVSD,
amem(D_BP, scr + e * 8),
areg(tuple_sse_seq[ssecur]));
ssecur++;
} else {
ins2(c, A_MOVQ,
amem(D_BP, scr + e * 8),
areg(tuple_rseq[gpcur]));
gpcur++;
}
}
} else {
ins2(c, A_MOVQ, amem(D_BP, scr + 0),
areg(D_AX));
ins2(c, A_MOVQ, amem(D_BP, scr + 8),
areg(D_DX));
ins2(c, A_MOVQ, amem(D_BP, scr + 16),
areg(D_CX));
}
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP)); ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
ins1(c, A_POPQ, areg(D_BP)); ins1(c, A_POPQ, areg(D_BP));
ins0(c, A_RET); ins0(c, A_RET);

View File

@@ -25061,15 +25061,57 @@ fn cgreturn(c: *cgen, n: *node) void = {
}; };
}; };
}; };
emitline("\tMOVQ\t"); // #171a: float-bearing struct RETURN (return
emitoff(scroff: i64); // twin of #165's param recv). A qualifying
emitline("(BP), AX\n"); // struct's float eightbytes ride the SSE return
emitline("\tMOVQ\t"); // row (X0,X1 = tupsse), its INT eightbytes the
emitoff((scroff + 8): i64); // INTEGER return row (AX,DX = tupreg), on
emitline("(BP), DX\n"); // INDEPENDENT cursors per SysV (ref/qbe/amd64/
emitline("\tMOVQ\t"); // sysv.c retr) — so a float lands in the next
emitoff((scroff + 16): i64); // XMM regardless of its positional eightbyte
emitline("(BP), CX\n"); // (struct{f64,i32}: e0→X0, e1→AX, NOT DX). The
// scratch is zero-padded to 24B so a full MOVQ
// on a trailing INT eightbyte reads no garbage
// (the #169 sized tail is a RECV concern).
// structfloatclass gates to qualifying structs;
// all-int + f32 keep the AX/DX/CX transport
// (byte-id / #171b).
let sfc: i32 = structfloatclass(c, c.fnret);
if (sfc != 0) {
let nb: i32 = sfc & 15;
let gpcur: i32 = 0;
let ssecur: i32 = 0;
let e: i32 = 0;
for (e < nb) {
let issse: bool = (sfc & (16 << e)) != 0;
if (issse) {
emitline("\tMOVSD\t");
emitoff((scroff + e*8): i64);
emitline("(BP), ");
emitline(tupsse(ssecur));
emitline("\n");
ssecur += 1;
} else {
emitline("\tMOVQ\t");
emitoff((scroff + e*8): i64);
emitline("(BP), ");
emitline(tupreg(gpcur));
emitline("\n");
gpcur += 1;
};
e += 1;
};
} else {
emitline("\tMOVQ\t");
emitoff(scroff: i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\t");
emitoff((scroff + 8): i64);
emitline("(BP), DX\n");
emitline("\tMOVQ\t");
emitoff((scroff + 16): i64);
emitline("(BP), CX\n");
};
emitline("\tMOVQ\tBP, SP\n"); emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n"); emitline("\tPOPQ\tBP\n");
emitline("\tRET\n"); emitline("\tRET\n");
@@ -25539,6 +25581,49 @@ fn cglet(c: *cgen, n: *node) void = {
// stomping MOVQ tail. Sizes >24B also fall through (sret // stomping MOVQ tail. Sizes >24B also fall through (sret
// deferred, same constraint as #4). Mirrors the cstage // deferred, same constraint as #4). Mirrors the cstage
// cgen.c N_LET receive branch. // cgen.c N_LET receive branch.
// #171a: float-bearing struct RECEIVE (return twin of #165's
// param recv). cgexpr leaves each float eightbyte in its SSE
// return reg (X0,X1 = tupsse) and each INT eightbyte in its
// INTEGER return reg (AX,DX = tupreg), on INDEPENDENT cursors
// per SysV (ref/qbe/amd64/sysv.c retr) — so a float is read
// from the next XMM regardless of its positional eightbyte
// (struct{f64,i32}: e0←X0, e1←AX). A qualifying struct's
// abisize is maxalign-rounded to a multiple of 8 (an f64
// forces align 8), so every eightbyte is a full word — the
// #169 sized tail is unreachable here. structfloatclass gates
// to qualifying structs; all-int + f32 fall to the GP recv
// below (byte-id / #171b).
if (rhs.kind == nkind.N_CALL && tn != nil) {
let sfc: i32 = structfloatclass(c, tn);
if (sfc != 0) {
cgexpr(c, rhs);
let nb: i32 = sfc & 15;
let gpcur: i32 = 0;
let ssecur: i32 = 0;
let e: i32 = 0;
for (e < nb) {
let issse: bool = (sfc & (16 << e)) != 0;
if (issse) {
emitline("\tMOVSD\t");
emitline(tupsse(ssecur));
emitline(", ");
emitoff((off + e*8): i64);
emitline("(BP)\n");
ssecur += 1;
} else {
emitline("\tMOVQ\t");
emitline(tupreg(gpcur));
emitline(", ");
emitoff((off + e*8): i64);
emitline("(BP)\n");
gpcur += 1;
};
e += 1;
};
c.lastwasreturn = 0;
return;
};
};
if (rhs.kind == nkind.N_CALL) { if (rhs.kind == nkind.N_CALL) {
let sname: str; let sname: str;
sname.ptr = nil; sname.len = 0; sname.ptr = nil; sname.len = 0;

View File

@@ -728,15 +728,57 @@ fn cgreturn(c: *cgen, n: *node) void = {
}; };
}; };
}; };
emitline("\tMOVQ\t"); // #171a: float-bearing struct RETURN (return
emitoff(scroff: i64); // twin of #165's param recv). A qualifying
emitline("(BP), AX\n"); // struct's float eightbytes ride the SSE return
emitline("\tMOVQ\t"); // row (X0,X1 = tupsse), its INT eightbytes the
emitoff((scroff + 8): i64); // INTEGER return row (AX,DX = tupreg), on
emitline("(BP), DX\n"); // INDEPENDENT cursors per SysV (ref/qbe/amd64/
emitline("\tMOVQ\t"); // sysv.c retr) — so a float lands in the next
emitoff((scroff + 16): i64); // XMM regardless of its positional eightbyte
emitline("(BP), CX\n"); // (struct{f64,i32}: e0→X0, e1→AX, NOT DX). The
// scratch is zero-padded to 24B so a full MOVQ
// on a trailing INT eightbyte reads no garbage
// (the #169 sized tail is a RECV concern).
// structfloatclass gates to qualifying structs;
// all-int + f32 keep the AX/DX/CX transport
// (byte-id / #171b).
let sfc: i32 = structfloatclass(c, c.fnret);
if (sfc != 0) {
let nb: i32 = sfc & 15;
let gpcur: i32 = 0;
let ssecur: i32 = 0;
let e: i32 = 0;
for (e < nb) {
let issse: bool = (sfc & (16 << e)) != 0;
if (issse) {
emitline("\tMOVSD\t");
emitoff((scroff + e*8): i64);
emitline("(BP), ");
emitline(tupsse(ssecur));
emitline("\n");
ssecur += 1;
} else {
emitline("\tMOVQ\t");
emitoff((scroff + e*8): i64);
emitline("(BP), ");
emitline(tupreg(gpcur));
emitline("\n");
gpcur += 1;
};
e += 1;
};
} else {
emitline("\tMOVQ\t");
emitoff(scroff: i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\t");
emitoff((scroff + 8): i64);
emitline("(BP), DX\n");
emitline("\tMOVQ\t");
emitoff((scroff + 16): i64);
emitline("(BP), CX\n");
};
emitline("\tMOVQ\tBP, SP\n"); emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n"); emitline("\tPOPQ\tBP\n");
emitline("\tRET\n"); emitline("\tRET\n");
@@ -1206,6 +1248,49 @@ fn cglet(c: *cgen, n: *node) void = {
// stomping MOVQ tail. Sizes >24B also fall through (sret // stomping MOVQ tail. Sizes >24B also fall through (sret
// deferred, same constraint as #4). Mirrors the cstage // deferred, same constraint as #4). Mirrors the cstage
// cgen.c N_LET receive branch. // cgen.c N_LET receive branch.
// #171a: float-bearing struct RECEIVE (return twin of #165's
// param recv). cgexpr leaves each float eightbyte in its SSE
// return reg (X0,X1 = tupsse) and each INT eightbyte in its
// INTEGER return reg (AX,DX = tupreg), on INDEPENDENT cursors
// per SysV (ref/qbe/amd64/sysv.c retr) — so a float is read
// from the next XMM regardless of its positional eightbyte
// (struct{f64,i32}: e0←X0, e1←AX). A qualifying struct's
// abisize is maxalign-rounded to a multiple of 8 (an f64
// forces align 8), so every eightbyte is a full word — the
// #169 sized tail is unreachable here. structfloatclass gates
// to qualifying structs; all-int + f32 fall to the GP recv
// below (byte-id / #171b).
if (rhs.kind == nkind.N_CALL && tn != nil) {
let sfc: i32 = structfloatclass(c, tn);
if (sfc != 0) {
cgexpr(c, rhs);
let nb: i32 = sfc & 15;
let gpcur: i32 = 0;
let ssecur: i32 = 0;
let e: i32 = 0;
for (e < nb) {
let issse: bool = (sfc & (16 << e)) != 0;
if (issse) {
emitline("\tMOVSD\t");
emitline(tupsse(ssecur));
emitline(", ");
emitoff((off + e*8): i64);
emitline("(BP)\n");
ssecur += 1;
} else {
emitline("\tMOVQ\t");
emitline(tupreg(gpcur));
emitline(", ");
emitoff((off + e*8): i64);
emitline("(BP)\n");
gpcur += 1;
};
e += 1;
};
c.lastwasreturn = 0;
return;
};
};
if (rhs.kind == nkind.N_CALL) { if (rhs.kind == nkind.N_CALL) {
let sname: str; let sname: str;
sname.ptr = nil; sname.len = 0; sname.ptr = nil; sname.len = 0;

View File

@@ -25061,15 +25061,57 @@ fn cgreturn(c: *cgen, n: *node) void = {
}; };
}; };
}; };
emitline("\tMOVQ\t"); // #171a: float-bearing struct RETURN (return
emitoff(scroff: i64); // twin of #165's param recv). A qualifying
emitline("(BP), AX\n"); // struct's float eightbytes ride the SSE return
emitline("\tMOVQ\t"); // row (X0,X1 = tupsse), its INT eightbytes the
emitoff((scroff + 8): i64); // INTEGER return row (AX,DX = tupreg), on
emitline("(BP), DX\n"); // INDEPENDENT cursors per SysV (ref/qbe/amd64/
emitline("\tMOVQ\t"); // sysv.c retr) — so a float lands in the next
emitoff((scroff + 16): i64); // XMM regardless of its positional eightbyte
emitline("(BP), CX\n"); // (struct{f64,i32}: e0→X0, e1→AX, NOT DX). The
// scratch is zero-padded to 24B so a full MOVQ
// on a trailing INT eightbyte reads no garbage
// (the #169 sized tail is a RECV concern).
// structfloatclass gates to qualifying structs;
// all-int + f32 keep the AX/DX/CX transport
// (byte-id / #171b).
let sfc: i32 = structfloatclass(c, c.fnret);
if (sfc != 0) {
let nb: i32 = sfc & 15;
let gpcur: i32 = 0;
let ssecur: i32 = 0;
let e: i32 = 0;
for (e < nb) {
let issse: bool = (sfc & (16 << e)) != 0;
if (issse) {
emitline("\tMOVSD\t");
emitoff((scroff + e*8): i64);
emitline("(BP), ");
emitline(tupsse(ssecur));
emitline("\n");
ssecur += 1;
} else {
emitline("\tMOVQ\t");
emitoff((scroff + e*8): i64);
emitline("(BP), ");
emitline(tupreg(gpcur));
emitline("\n");
gpcur += 1;
};
e += 1;
};
} else {
emitline("\tMOVQ\t");
emitoff(scroff: i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\t");
emitoff((scroff + 8): i64);
emitline("(BP), DX\n");
emitline("\tMOVQ\t");
emitoff((scroff + 16): i64);
emitline("(BP), CX\n");
};
emitline("\tMOVQ\tBP, SP\n"); emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n"); emitline("\tPOPQ\tBP\n");
emitline("\tRET\n"); emitline("\tRET\n");
@@ -25539,6 +25581,49 @@ fn cglet(c: *cgen, n: *node) void = {
// stomping MOVQ tail. Sizes >24B also fall through (sret // stomping MOVQ tail. Sizes >24B also fall through (sret
// deferred, same constraint as #4). Mirrors the cstage // deferred, same constraint as #4). Mirrors the cstage
// cgen.c N_LET receive branch. // cgen.c N_LET receive branch.
// #171a: float-bearing struct RECEIVE (return twin of #165's
// param recv). cgexpr leaves each float eightbyte in its SSE
// return reg (X0,X1 = tupsse) and each INT eightbyte in its
// INTEGER return reg (AX,DX = tupreg), on INDEPENDENT cursors
// per SysV (ref/qbe/amd64/sysv.c retr) — so a float is read
// from the next XMM regardless of its positional eightbyte
// (struct{f64,i32}: e0←X0, e1←AX). A qualifying struct's
// abisize is maxalign-rounded to a multiple of 8 (an f64
// forces align 8), so every eightbyte is a full word — the
// #169 sized tail is unreachable here. structfloatclass gates
// to qualifying structs; all-int + f32 fall to the GP recv
// below (byte-id / #171b).
if (rhs.kind == nkind.N_CALL && tn != nil) {
let sfc: i32 = structfloatclass(c, tn);
if (sfc != 0) {
cgexpr(c, rhs);
let nb: i32 = sfc & 15;
let gpcur: i32 = 0;
let ssecur: i32 = 0;
let e: i32 = 0;
for (e < nb) {
let issse: bool = (sfc & (16 << e)) != 0;
if (issse) {
emitline("\tMOVSD\t");
emitline(tupsse(ssecur));
emitline(", ");
emitoff((off + e*8): i64);
emitline("(BP)\n");
ssecur += 1;
} else {
emitline("\tMOVQ\t");
emitline(tupreg(gpcur));
emitline(", ");
emitoff((off + e*8): i64);
emitline("(BP)\n");
gpcur += 1;
};
e += 1;
};
c.lastwasreturn = 0;
return;
};
};
if (rhs.kind == nkind.N_CALL) { if (rhs.kind == nkind.N_CALL) {
let sname: str; let sname: str;
sname.ptr = nil; sname.len = 0; sname.ptr = nil; sname.len = 0;

View File

@@ -0,0 +1,319 @@
/*
* 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;
}