From c882bcf27c4bc6a52a36eb9cff71f218fe6e63e1 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 28 May 2026 01:50:28 +0900 Subject: [PATCH] wcc: struct-return float fields via SSE return regs (#171a) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Makefile | 6 + cmd/w6c/cgen.c | 78 ++++++- selfhost/cmd/w6c/main.combined.ww | 103 ++++++++- selfhost/cmd/wcc/cgenstmt.ww | 103 ++++++++- selfhost/cmd/wwdump/main.combined.ww | 103 ++++++++- test/wcc/946_structret_run.c | 319 +++++++++++++++++++++++++++ 6 files changed, 679 insertions(+), 33 deletions(-) create mode 100644 test/wcc/946_structret_run.c diff --git a/Makefile b/Makefile index c5e000be..386b2866 100644 --- a/Makefile +++ b/Makefile @@ -355,6 +355,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_tuprecv_f64_run \ $(BIN)/test_tupparam_run \ $(BIN)/test_structparam_run \ + $(BIN)/test_structret_run \ $(BIN)/test_floats_run \ $(BIN)/test_size_type_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) $(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: @sh tools/sizelint diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index e26ed29c..fd9c1504 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -7717,6 +7717,40 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) * excludes them so they fall through to the existing scalar * path rather than emit a stomping MOVQ tail. Sizes >24B also * 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 && lu->kind == TY_STRUCT && sz <= 24 && (sz % 8 == 0 || sz % 8 == 1 @@ -8290,12 +8324,44 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) k += 1; } } - 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)); + /* #171a: float-bearing struct RETURN (the return + * twin of #165's param recv). A qualifying struct's + * float eightbytes ride the SSE return row (X0,X1 = + * tuple_sse_seq), its INT eightbytes the INTEGER + * return row (AX,DX = tuple_rseq), on INDEPENDENT + * 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)); ins1(c, A_POPQ, areg(D_BP)); ins0(c, A_RET); diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 8a106c1d..aa36eb71 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -25061,15 +25061,57 @@ fn cgreturn(c: *cgen, n: *node) void = { }; }; }; - 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"); + // #171a: float-bearing struct RETURN (return + // twin of #165's param recv). A qualifying + // struct's float eightbytes ride the SSE return + // row (X0,X1 = tupsse), its INT eightbytes the + // INTEGER return row (AX,DX = tupreg), on + // INDEPENDENT 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 + // 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("\tPOPQ\tBP\n"); emitline("\tRET\n"); @@ -25539,6 +25581,49 @@ fn cglet(c: *cgen, n: *node) void = { // stomping MOVQ tail. Sizes >24B also fall through (sret // deferred, same constraint as #4). Mirrors the cstage // 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) { let sname: str; sname.ptr = nil; sname.len = 0; diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index a542bd9c..ecd3184d 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -728,15 +728,57 @@ fn cgreturn(c: *cgen, n: *node) void = { }; }; }; - 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"); + // #171a: float-bearing struct RETURN (return + // twin of #165's param recv). A qualifying + // struct's float eightbytes ride the SSE return + // row (X0,X1 = tupsse), its INT eightbytes the + // INTEGER return row (AX,DX = tupreg), on + // INDEPENDENT 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 + // 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("\tPOPQ\tBP\n"); emitline("\tRET\n"); @@ -1206,6 +1248,49 @@ fn cglet(c: *cgen, n: *node) void = { // stomping MOVQ tail. Sizes >24B also fall through (sret // deferred, same constraint as #4). Mirrors the cstage // 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) { let sname: str; sname.ptr = nil; sname.len = 0; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 9802f0f9..f4aa03b3 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -25061,15 +25061,57 @@ fn cgreturn(c: *cgen, n: *node) void = { }; }; }; - 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"); + // #171a: float-bearing struct RETURN (return + // twin of #165's param recv). A qualifying + // struct's float eightbytes ride the SSE return + // row (X0,X1 = tupsse), its INT eightbytes the + // INTEGER return row (AX,DX = tupreg), on + // INDEPENDENT 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 + // 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("\tPOPQ\tBP\n"); emitline("\tRET\n"); @@ -25539,6 +25581,49 @@ fn cglet(c: *cgen, n: *node) void = { // stomping MOVQ tail. Sizes >24B also fall through (sret // deferred, same constraint as #4). Mirrors the cstage // 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) { let sname: str; sname.ptr = nil; sname.len = 0; diff --git a/test/wcc/946_structret_run.c b/test/wcc/946_structret_run.c new file mode 100644 index 00000000..6a2c5eee --- /dev/null +++ b/test/wcc/946_structret_run.c @@ -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 (BP), Xn` (scratch -> XMM; a GP eightbyte is + * `MOVQ (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 +#include +#include +#include +#include +#include + +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(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, -(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; +}