From a917533fbf9e5e5befe6c6f90ece3283c1cb5e79 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Wed, 27 May 2026 23:31:19 +0900 Subject: [PATCH] wcc: struct-param float fields via SSE arg regs (#165) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit struct params were passed GP-only, so a struct{f64,f64} argument landed in DI/SI instead of X0/X1 — value-correct for internal ww calls (the bits round-trip) but not SysV register-class conformant. Add a per-eightbyte classifier (struct_float_class) routing a qualifying struct's float eightbytes through the SSE arg cursor, reusing #163's dual-cursor plumbing and #164's field classification. A struct qualifies only when every eightbyte is pure-integer or a lone f64 exactly filling it (and >=1 f64); anything else — any f32, multiple floats per eightbyte, a straddling or aggregate field — falls back to the unchanged GP path (f32 sub-eightbyte packing deferred #165b). Both stages' predicates are alias-aware and identical in coverage. Gate-blind and value-correct either way, so the discriminator is the callee's receive instruction (MOVSD vs MOVQ), scoped per-function — covered by probe 946. --- Makefile | 6 + cmd/w6c/cgen.c | 165 +++++++++++++- selfhost/cmd/w6c/main.combined.ww | 214 +++++++++++++++++-- selfhost/cmd/wcc/cgendecl.ww | 46 ++++ selfhost/cmd/wcc/cgenexpr.ww | 91 ++++++-- selfhost/cmd/wcc/cgenutil.ww | 77 +++++++ selfhost/cmd/wwdump/main.combined.ww | 214 +++++++++++++++++-- test/wcc/946_structparam_run.c | 308 +++++++++++++++++++++++++++ 8 files changed, 1048 insertions(+), 73 deletions(-) create mode 100644 test/wcc/946_structparam_run.c diff --git a/Makefile b/Makefile index 4a28dfad..c5e000be 100644 --- a/Makefile +++ b/Makefile @@ -354,6 +354,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_f32arg_run \ $(BIN)/test_tuprecv_f64_run \ $(BIN)/test_tupparam_run \ + $(BIN)/test_structparam_run \ $(BIN)/test_floats_run \ $(BIN)/test_size_type_run \ $(BIN)/test_types_sizelim_run \ @@ -1272,6 +1273,11 @@ $(BIN)/test_tupparam_run: test/wcc/905_tupparam_run.c $(BIN)/ww \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_structparam_run: test/wcc/946_structparam_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 fcb0c820..e26ed29c 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -432,6 +432,68 @@ struct_arg_size(Type *t) return (int)t->size; } +/* struct_float_class — SysV per-eightbyte classification for the #165 + * float-bearing-struct param case (the param twin of #171's struct + * return, classifying per-eightbyte rather than #163's per-element). + * Fills cls[e] = 1 (SSE) / 0 (INTEGER) for each of the struct's 1-2 + * eightbytes and returns the eightbyte count, but ONLY for a qualifying + * struct: every eightbyte is either pure-INTEGER or a lone f64 exactly + * filling it, AND at least one is f64. Returns 0 (caller keeps the all- + * GP transport, which is correct + byte-identical for those) when the + * type is not a <=16B struct, has an all-integer layout (no float to + * route), carries an f32 field, packs >1 float into an eightbyte, has a + * float straddling the 8-byte SysV eightbyte boundary, or holds an + * aggregate field (SysV would recurse — out of scope here). f32 / sub- + * eightbyte packing is deferred (#165b). */ +static int +struct_float_class(Type *t, int *cls) +{ + if (t == NULL) return 0; + if (t->kind == TY_NAMED) t = t->under; + if (t == NULL || t->kind != TY_STRUCT) return 0; + int sz = (int)t->size; + if (sz <= 0 || sz > 16) return 0; + /* SysV classifies aggregates in 8-byte eightbytes (§3.2.3); 8 is + * the eightbyte stride, not a type footprint. */ + int nb = (sz > 8) ? 2 : 1; + int nflt[2], nint[2]; + nflt[0] = nflt[1] = nint[0] = nint[1] = 0; + for (Tfield *f = t->fields; f; f = f->next) { + Type *fu = (f->type && f->type->kind == TY_NAMED) + ? f->type->under : f->type; + if (fu == NULL) return 0; + int foff = (int)f->offset; + int fsz = (int)fu->size; + int e = foff / 8; + if (e < 0 || e >= nb) return 0; + int f32; + if (fld_isfloat(f->type, &f32)) { + if (f32) return 0; + if (foff % 8 != 0 || fsz != 8) return 0; + nflt[e]++; + } else { + if (fu->kind == TY_STRUCT || fu->kind == TY_ARRAY + || fu->kind == TY_SLICE || fu->kind == TY_STR + || fu->kind == TY_TAGGED || fu->kind == TY_TUPLE) + return 0; + if (fsz > 8 || (foff + fsz - 1) / 8 != e) return 0; + nint[e]++; + } + } + int hasfloat = 0; + for (int e = 0; e < nb; e++) { + if (nflt[e] == 1 && nint[e] == 0) { + cls[e] = 1; + hasfloat = 1; + } else if (nflt[e] == 0) { + cls[e] = 0; + } else { + return 0; + } + } + return hasfloat ? nb : 0; +} + /* Tagged-union arg byte size: 16 (8B variants) or 24 (16B variants). * Nullable-folded `(*T | void)` collapses to 8 bytes (just the * pointer). Returns 0 if not a tagged union or too large to pass @@ -5645,13 +5707,59 @@ cgexpr(Cg *c, Node *n, Local *locals) stackslots++; } } else if (node_isstructarg(args[i])) { - int sz = struct_arg_size(args[i]->type); - int eb = (sz > 8) ? 2 : 1; - for (int k = 0; k < eb; k++) { - if (ii < 6) - ins1(c, A_POPQ, areg(sysv_argregs[ii++])); - else - stackslots++; + int sclass[2], snb; + /* SSE-drain only for an ident arg: the struct push + * stages raw slot words for an N_IDENT only (non- + * ident struct args are a pre-existing >8B-push gap, + * out of scope). Gating here keeps cstage byte-id + * with wwstage, whose type lookup is ident-keyed. */ + if (args[i]->kind == N_IDENT + && (snb = struct_float_class(args[i]->type, + sclass)) > 0) { + /* #165: float-bearing struct arg — drain by + * SysV eightbyte class: a lone-f64 eightbyte + * MOVSD off (SP) into the next XMM (X0..X7), a + * pure-INT eightbyte POPQ into the next INTEGER + * arg reg (DI/SI/..). The struct-ident push + * staged raw words (class-independent); only the + * drain differs. Gated to qualifying floats; + * all-int + f32-packed keep the all-GP pop + * below. Reg overflow loud-stops (rule 7), the + * partial-spill stitch out of scope (#163 twin). */ + for (int e = 0; e < snb; e++) { + if (sclass[e]) { + if (fi >= 8) + fatal("float struct arg " + "eightbyte overflows SSE " + "arg regs (X0..X7); stitch " + "out of scope, see #165"); + ins2(c, A_MOVSD, + amem(D_SP, 0), + areg(sysv_fargregs[fi])); + ins2(c, A_ADDQ, aimm(8), + areg(D_SP)); + fi++; + } else { + if (ii >= 6) + fatal("float struct arg " + "eightbyte overflows " + "integer arg regs (DI/SI/" + "DX/CX/R8/R9); stitch out " + "of scope, see #165"); + ins1(c, A_POPQ, + areg(sysv_argregs[ii++])); + } + } + } else { + int sz = struct_arg_size(args[i]->type); + int eb = (sz > 8) ? 2 : 1; + for (int k = 0; k < eb; k++) { + if (ii < 6) + ins1(c, A_POPQ, + areg(sysv_argregs[ii++])); + else + stackslots++; + } } } else if (node_istaggedarg(args[i])) { int sz = tagged_arg_size(args[i]->type); @@ -8762,6 +8870,49 @@ cgfn(Cg *c, FILE *out, Node *fn) continue; } + /* #165: float-bearing struct PARAM receive (param twin of + * #163's tuple). Classify each SysV eightbyte; a lone-f64 + * eightbyte reads its XMM (X0..X7), a pure-INT eightbyte its + * INTEGER arg reg (DI/SI/..), stored into the param's frame + * slot at the 8-byte eightbyte stride. Gated to qualifying + * structs by struct_float_class — all-int + f32-packed keep + * the GP transport below (byte-id / #165b). Placed before the + * single-class eightbyte logic, which can't model a mixed + * GP/SSE aggregate. Reg overflow loud-stops (rule 7). */ + if (is_struct) { + int sclass[2], snb; + if ((snb = struct_float_class(pt, sclass)) > 0) { + int sz = (int)pu->size; + int off = localoff(c, &locals, p->str, sz, &frame); + for (int e = 0; e < snb; e++) { + if (sclass[e]) { + if (fargi >= 8) + fatal("float struct param " + "eightbyte overflows SSE " + "arg regs (X0..X7); stitch " + "out of scope, see #165"); + ins2(c, A_MOVSD, + areg(sysv_fargregs[fargi]), + amem(D_BP, off + e * 8)); + fargi++; + } else { + if (argi >= 6) + fatal("float struct param " + "eightbyte overflows " + "integer arg regs (DI/SI/" + "DX/CX/R8/R9); stitch out " + "of scope, see #165"); + ins2(c, A_MOVQ, + areg(sysv_argregs[argi]), + amem(D_BP, off + e * 8)); + argi++; + } + } + if (tp) tp = tp->next; + continue; + } + } + /* Args overflowing register classes live at positive offsets * from BP (16 + i*8). We register them as Locals at those * offsets, no spill needed. */ diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index b3328885..402fbc61 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -15748,6 +15748,83 @@ fn structparamsize(c: *cgen, t: *node) i32 = { return si.totsize; }; +// structfloatclass — SysV per-eightbyte classification for the #165 +// float-bearing-struct param case (param twin of #171's struct return; +// classifies per-eightbyte, not #163's per-element). Returns 0 when the +// struct does NOT qualify — the caller keeps the all-GP transport, which +// is correct + byte-identical there — for: not a <=16B struct; an all- +// integer layout (no float to route); an f32 field; >1 float packed in +// one eightbyte; a float straddling the 8-byte SysV eightbyte boundary; +// or an aggregate field (SysV would recurse, out of scope). Otherwise a +// packed result whose low bits hold the eightbyte count nb (1|2) and bit +// (4+e) marks eightbyte e SSE-class (a lone f64). Qualifies iff every +// eightbyte is pure-INT or a lone f64 AND at least one is f64. f32 / +// sub-eightbyte packing deferred (#165b). Mirrors cstage +// struct_float_class (cmd/w6c/cgen.c). +fn structfloatclass(c: *cgen, t: *node) i32 = { + if (c == nil) { return 0; }; + let r: *node = resolvetype(c, t); + if (r == nil) { return 0; }; + if (r.kind != nkind.N_TNAME) { return 0; }; + let nm: str = r.str; + if (streq(nm, "str")) { return 0; }; + if (primsize(nm) > 0) { return 0; }; + let si: *structinfo = structlookup(c, nm); + if (si == nil) { return 0; }; + if (si.totsize <= 0) { return 0; }; + if (si.totsize > 16) { return 0; }; + // SysV classifies aggregates in 8-byte eightbytes; 8 is the + // eightbyte stride, not a type footprint. + let nb: i32 = 1; + if (si.totsize > 8) { nb = 2; }; + let nflt0: i32 = 0; let nflt1: i32 = 0; + let nint0: i32 = 0; let nint1: i32 = 0; + let fi: *fieldinfo = si.fields; + for (fi != nil) { + let foff: i32 = fi.foff; + let fsz: i32 = fi.fsz; + let e: i32 = foff / 8; + if (e < 0) { return 0; }; + if (e >= nb) { return 0; }; + if (isfloattype(c, fi.tnode)) { + if (isf32type(c, fi.tnode)) { return 0; }; + if ((foff & 7) != 0) { return 0; }; + if (fsz != 8) { return 0; }; + if (e == 0) { nflt0 += 1; } else { nflt1 += 1; }; + } else { + if (isslicetype(c, fi.tnode)) { return 0; }; + if (isstrtype(c, fi.tnode)) { return 0; }; + if (istaggedtype(c, fi.tnode)) { return 0; }; + if (structparamsize(c, fi.tnode) > 0) { return 0; }; + // Alias-aware array/tuple reject, mirroring cstage's + // NAMED-peeled TY_ARRAY/TY_TUPLE (cgen.c struct_float_class). + // A direct-AST-kind N_TARRAY test misses an aliased array + // and every tuple field; the fsz>8 guard below also lets a + // <=8B one slip, so such a struct would wrongly SSE-route on + // this stage but stay GP on cstage (a #165b leak). + let rf: *node = resolvetype(c, fi.tnode); + if (rf != nil) { + if (rf.kind == nkind.N_TARRAY) { return 0; }; + if (rf.kind == nkind.N_TTUPLE) { return 0; }; + }; + if (fsz > 8) { return 0; }; + if ((foff + fsz - 1) / 8 != e) { return 0; }; + if (e == 0) { nint0 += 1; } else { nint1 += 1; }; + }; + fi = fi.finext; + }; + let enc: i32 = nb; + let hasfloat: bool = false; + if (nflt0 == 1 && nint0 == 0) { enc += 16; hasfloat = true; } + else { if (nflt0 != 0) { return 0; }; }; + if (nb == 2) { + if (nflt1 == 1 && nint1 == 0) { enc += 32; hasfloat = true; } + else { if (nflt1 != 0) { return 0; }; }; + }; + if (!hasfloat) { return 0; }; + return enc; +}; + // istaggedtype — alias-aware. Reads stamped tinfo so `T`, // `type alias = (A|B)`, `type error = !(invalid|overflow)` all // resolve to TY_TAGGED — tinfofornode handles the N_TBANG unwrap @@ -21192,29 +21269,76 @@ fn cgcall(c: *cgen, n: *node) void = { p = p.next; }; } else { - let extra: i32 = 0; - // str IS []u8: 3-word arg, same as slice (#1/Phase 3). - if (nodeisstr(c, a)) { extra = 2; }; - if (nodeisslice(c, a)) { extra = 2; }; - // #21: tagged-CALL arg was pushed AX/DX/CX/R8 high→low - // by pushargsrev; size the per-arg pop to match so the - // next arg's POPQ doesn't land on residual tag/payload - // words and shift intidx out of sync. - let tcs: i32 = taggedcallslot(c, a); - if (tcs > 0) { extra = tcs / 8 - 1; }; - let words: i32 = 1 + extra; - let w: i32 = 0; - for (w < words) { - if (intidx < 6) { - emitline("\tPOPQ\t"); - emitline(argregname(intidx)); - emitline("\n"); - intidx += 1; - } else { - stackslots += 1; + let stfc: i32 = 0; + if (a.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, a.str); + if (lc != nil) { stfc = structfloatclass(c, lc.tnode); }; + }; + if (stfc != 0) { + // #165: float-bearing struct arg — drain by SysV + // eightbyte class: a lone-f64 eightbyte MOVSD off + // (SP) into the next XMM (X0..X7), a pure-INT + // eightbyte POPQ into the next INTEGER arg reg + // (DI/SI/..). The struct-ident push staged raw slot + // words (class-independent); only the drain differs. + // Gated to qualifying floats; all-int + f32-packed + // keep the generic pop below. Reg overflow loud- + // stops (rule 7), the partial-spill stitch out of + // scope (#163 twin). + let nb: i32 = stfc & 15; + let e: i32 = 0; + for (e < nb) { + let issse: bool = (stfc & (16 << e)) != 0; + if (issse) { + if (fpidx >= 8) { + let msg: str = "float struct arg eightbyte overflows SSE arg regs (X0..X7); stitch out of scope, see #165\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + emitline("\tMOVSD\t(SP), "); + emitline(fargregname(fpidx)); + emitline("\n"); + emitline("\tADDQ\t$8, SP\n"); + fpidx += 1; + } else { + if (intidx >= 6) { + let msg: str = "float struct arg eightbyte overflows integer arg regs (DI/SI/DX/CX/R8/R9); stitch out of scope, see #165\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + emitline("\tPOPQ\t"); + emitline(argregname(intidx)); + emitline("\n"); + intidx += 1; + }; + popped += 1; + e += 1; + }; + } else { + let extra: i32 = 0; + // str IS []u8: 3-word arg, same as slice (#1/Phase 3). + if (nodeisstr(c, a)) { extra = 2; }; + if (nodeisslice(c, a)) { extra = 2; }; + // #21: tagged-CALL arg was pushed AX/DX/CX/R8 high→low + // by pushargsrev; size the per-arg pop to match so the + // next arg's POPQ doesn't land on residual tag/payload + // words and shift intidx out of sync. + let tcs: i32 = taggedcallslot(c, a); + if (tcs > 0) { extra = tcs / 8 - 1; }; + let words: i32 = 1 + extra; + let w: i32 = 0; + for (w < words) { + if (intidx < 6) { + emitline("\tPOPQ\t"); + emitline(argregname(intidx)); + emitline("\n"); + intidx += 1; + } else { + stackslots += 1; + }; + popped += 1; + w += 1; }; - popped += 1; - w += 1; }; }; }; @@ -26361,6 +26485,52 @@ fn cgfnparams(c: *cgen, params: *node) void = { p = p.next; continue; }; + let sfc: i32 = structfloatclass(c, p.lhs); + if (sfc != 0) { + // #165: float-bearing struct PARAM receive (param + // twin of #163's tuple). Classify each SysV + // eightbyte; a lone-f64 eightbyte reads its XMM + // (X0..X7), a pure-INT eightbyte its INTEGER arg reg + // (DI/SI/..), stored into the param slot at the + // 8-byte eightbyte stride. Gated to qualifying + // structs by structfloatclass — all-int + f32-packed + // fall through to the GP struct arm below (byte-id / + // #165b). Reg overflow loud-stops (rule 7). + let off: i32 = localadd(c, nm, structparamsize(c, p.lhs), p.lhs); + let nb: i32 = sfc & 15; + let e: i32 = 0; + for (e < nb) { + let issse: bool = (sfc & (16 << e)) != 0; + if (issse) { + if (fidx >= 8) { + let msg: str = "float struct param eightbyte overflows SSE arg regs (X0..X7); stitch out of scope, see #165\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + emitline("\tMOVSD\t"); + emitline(fargregname(fidx)); + emitline(", "); + emitoff((off + e*8): i64); + emitline("(BP)\n"); + fidx += 1; + } else { + if (idx >= 6) { + let msg: str = "float struct param eightbyte overflows integer arg regs (DI/SI/DX/CX/R8/R9); stitch out of scope, see #165\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff((off + e*8): i64); + emitline("(BP)\n"); + idx += 1; + }; + e += 1; + }; + p = p.next; + continue; + }; if (istaggedtype(c, p.lhs)) { let slot: i32 = slotsize(c, p.lhs); let nw: i32 = slot / 8; diff --git a/selfhost/cmd/wcc/cgendecl.ww b/selfhost/cmd/wcc/cgendecl.ww index 8937ae1d..bcf7f7c9 100644 --- a/selfhost/cmd/wcc/cgendecl.ww +++ b/selfhost/cmd/wcc/cgendecl.ww @@ -180,6 +180,52 @@ fn cgfnparams(c: *cgen, params: *node) void = { p = p.next; continue; }; + let sfc: i32 = structfloatclass(c, p.lhs); + if (sfc != 0) { + // #165: float-bearing struct PARAM receive (param + // twin of #163's tuple). Classify each SysV + // eightbyte; a lone-f64 eightbyte reads its XMM + // (X0..X7), a pure-INT eightbyte its INTEGER arg reg + // (DI/SI/..), stored into the param slot at the + // 8-byte eightbyte stride. Gated to qualifying + // structs by structfloatclass — all-int + f32-packed + // fall through to the GP struct arm below (byte-id / + // #165b). Reg overflow loud-stops (rule 7). + let off: i32 = localadd(c, nm, structparamsize(c, p.lhs), p.lhs); + let nb: i32 = sfc & 15; + let e: i32 = 0; + for (e < nb) { + let issse: bool = (sfc & (16 << e)) != 0; + if (issse) { + if (fidx >= 8) { + let msg: str = "float struct param eightbyte overflows SSE arg regs (X0..X7); stitch out of scope, see #165\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + emitline("\tMOVSD\t"); + emitline(fargregname(fidx)); + emitline(", "); + emitoff((off + e*8): i64); + emitline("(BP)\n"); + fidx += 1; + } else { + if (idx >= 6) { + let msg: str = "float struct param eightbyte overflows integer arg regs (DI/SI/DX/CX/R8/R9); stitch out of scope, see #165\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff((off + e*8): i64); + emitline("(BP)\n"); + idx += 1; + }; + e += 1; + }; + p = p.next; + continue; + }; if (istaggedtype(c, p.lhs)) { let slot: i32 = slotsize(c, p.lhs); let nw: i32 = slot / 8; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 906cc479..a8c906ff 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -3980,29 +3980,76 @@ fn cgcall(c: *cgen, n: *node) void = { p = p.next; }; } else { - let extra: i32 = 0; - // str IS []u8: 3-word arg, same as slice (#1/Phase 3). - if (nodeisstr(c, a)) { extra = 2; }; - if (nodeisslice(c, a)) { extra = 2; }; - // #21: tagged-CALL arg was pushed AX/DX/CX/R8 high→low - // by pushargsrev; size the per-arg pop to match so the - // next arg's POPQ doesn't land on residual tag/payload - // words and shift intidx out of sync. - let tcs: i32 = taggedcallslot(c, a); - if (tcs > 0) { extra = tcs / 8 - 1; }; - let words: i32 = 1 + extra; - let w: i32 = 0; - for (w < words) { - if (intidx < 6) { - emitline("\tPOPQ\t"); - emitline(argregname(intidx)); - emitline("\n"); - intidx += 1; - } else { - stackslots += 1; + let stfc: i32 = 0; + if (a.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, a.str); + if (lc != nil) { stfc = structfloatclass(c, lc.tnode); }; + }; + if (stfc != 0) { + // #165: float-bearing struct arg — drain by SysV + // eightbyte class: a lone-f64 eightbyte MOVSD off + // (SP) into the next XMM (X0..X7), a pure-INT + // eightbyte POPQ into the next INTEGER arg reg + // (DI/SI/..). The struct-ident push staged raw slot + // words (class-independent); only the drain differs. + // Gated to qualifying floats; all-int + f32-packed + // keep the generic pop below. Reg overflow loud- + // stops (rule 7), the partial-spill stitch out of + // scope (#163 twin). + let nb: i32 = stfc & 15; + let e: i32 = 0; + for (e < nb) { + let issse: bool = (stfc & (16 << e)) != 0; + if (issse) { + if (fpidx >= 8) { + let msg: str = "float struct arg eightbyte overflows SSE arg regs (X0..X7); stitch out of scope, see #165\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + emitline("\tMOVSD\t(SP), "); + emitline(fargregname(fpidx)); + emitline("\n"); + emitline("\tADDQ\t$8, SP\n"); + fpidx += 1; + } else { + if (intidx >= 6) { + let msg: str = "float struct arg eightbyte overflows integer arg regs (DI/SI/DX/CX/R8/R9); stitch out of scope, see #165\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + emitline("\tPOPQ\t"); + emitline(argregname(intidx)); + emitline("\n"); + intidx += 1; + }; + popped += 1; + e += 1; + }; + } else { + let extra: i32 = 0; + // str IS []u8: 3-word arg, same as slice (#1/Phase 3). + if (nodeisstr(c, a)) { extra = 2; }; + if (nodeisslice(c, a)) { extra = 2; }; + // #21: tagged-CALL arg was pushed AX/DX/CX/R8 high→low + // by pushargsrev; size the per-arg pop to match so the + // next arg's POPQ doesn't land on residual tag/payload + // words and shift intidx out of sync. + let tcs: i32 = taggedcallslot(c, a); + if (tcs > 0) { extra = tcs / 8 - 1; }; + let words: i32 = 1 + extra; + let w: i32 = 0; + for (w < words) { + if (intidx < 6) { + emitline("\tPOPQ\t"); + emitline(argregname(intidx)); + emitline("\n"); + intidx += 1; + } else { + stackslots += 1; + }; + popped += 1; + w += 1; }; - popped += 1; - w += 1; }; }; }; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 016d519e..4433b5a1 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -1926,6 +1926,83 @@ fn structparamsize(c: *cgen, t: *node) i32 = { return si.totsize; }; +// structfloatclass — SysV per-eightbyte classification for the #165 +// float-bearing-struct param case (param twin of #171's struct return; +// classifies per-eightbyte, not #163's per-element). Returns 0 when the +// struct does NOT qualify — the caller keeps the all-GP transport, which +// is correct + byte-identical there — for: not a <=16B struct; an all- +// integer layout (no float to route); an f32 field; >1 float packed in +// one eightbyte; a float straddling the 8-byte SysV eightbyte boundary; +// or an aggregate field (SysV would recurse, out of scope). Otherwise a +// packed result whose low bits hold the eightbyte count nb (1|2) and bit +// (4+e) marks eightbyte e SSE-class (a lone f64). Qualifies iff every +// eightbyte is pure-INT or a lone f64 AND at least one is f64. f32 / +// sub-eightbyte packing deferred (#165b). Mirrors cstage +// struct_float_class (cmd/w6c/cgen.c). +fn structfloatclass(c: *cgen, t: *node) i32 = { + if (c == nil) { return 0; }; + let r: *node = resolvetype(c, t); + if (r == nil) { return 0; }; + if (r.kind != nkind.N_TNAME) { return 0; }; + let nm: str = r.str; + if (streq(nm, "str")) { return 0; }; + if (primsize(nm) > 0) { return 0; }; + let si: *structinfo = structlookup(c, nm); + if (si == nil) { return 0; }; + if (si.totsize <= 0) { return 0; }; + if (si.totsize > 16) { return 0; }; + // SysV classifies aggregates in 8-byte eightbytes; 8 is the + // eightbyte stride, not a type footprint. + let nb: i32 = 1; + if (si.totsize > 8) { nb = 2; }; + let nflt0: i32 = 0; let nflt1: i32 = 0; + let nint0: i32 = 0; let nint1: i32 = 0; + let fi: *fieldinfo = si.fields; + for (fi != nil) { + let foff: i32 = fi.foff; + let fsz: i32 = fi.fsz; + let e: i32 = foff / 8; + if (e < 0) { return 0; }; + if (e >= nb) { return 0; }; + if (isfloattype(c, fi.tnode)) { + if (isf32type(c, fi.tnode)) { return 0; }; + if ((foff & 7) != 0) { return 0; }; + if (fsz != 8) { return 0; }; + if (e == 0) { nflt0 += 1; } else { nflt1 += 1; }; + } else { + if (isslicetype(c, fi.tnode)) { return 0; }; + if (isstrtype(c, fi.tnode)) { return 0; }; + if (istaggedtype(c, fi.tnode)) { return 0; }; + if (structparamsize(c, fi.tnode) > 0) { return 0; }; + // Alias-aware array/tuple reject, mirroring cstage's + // NAMED-peeled TY_ARRAY/TY_TUPLE (cgen.c struct_float_class). + // A direct-AST-kind N_TARRAY test misses an aliased array + // and every tuple field; the fsz>8 guard below also lets a + // <=8B one slip, so such a struct would wrongly SSE-route on + // this stage but stay GP on cstage (a #165b leak). + let rf: *node = resolvetype(c, fi.tnode); + if (rf != nil) { + if (rf.kind == nkind.N_TARRAY) { return 0; }; + if (rf.kind == nkind.N_TTUPLE) { return 0; }; + }; + if (fsz > 8) { return 0; }; + if ((foff + fsz - 1) / 8 != e) { return 0; }; + if (e == 0) { nint0 += 1; } else { nint1 += 1; }; + }; + fi = fi.finext; + }; + let enc: i32 = nb; + let hasfloat: bool = false; + if (nflt0 == 1 && nint0 == 0) { enc += 16; hasfloat = true; } + else { if (nflt0 != 0) { return 0; }; }; + if (nb == 2) { + if (nflt1 == 1 && nint1 == 0) { enc += 32; hasfloat = true; } + else { if (nflt1 != 0) { return 0; }; }; + }; + if (!hasfloat) { return 0; }; + return enc; +}; + // istaggedtype — alias-aware. Reads stamped tinfo so `T`, // `type alias = (A|B)`, `type error = !(invalid|overflow)` all // resolve to TY_TAGGED — tinfofornode handles the N_TBANG unwrap diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 73fc8aad..455e2654 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -15748,6 +15748,83 @@ fn structparamsize(c: *cgen, t: *node) i32 = { return si.totsize; }; +// structfloatclass — SysV per-eightbyte classification for the #165 +// float-bearing-struct param case (param twin of #171's struct return; +// classifies per-eightbyte, not #163's per-element). Returns 0 when the +// struct does NOT qualify — the caller keeps the all-GP transport, which +// is correct + byte-identical there — for: not a <=16B struct; an all- +// integer layout (no float to route); an f32 field; >1 float packed in +// one eightbyte; a float straddling the 8-byte SysV eightbyte boundary; +// or an aggregate field (SysV would recurse, out of scope). Otherwise a +// packed result whose low bits hold the eightbyte count nb (1|2) and bit +// (4+e) marks eightbyte e SSE-class (a lone f64). Qualifies iff every +// eightbyte is pure-INT or a lone f64 AND at least one is f64. f32 / +// sub-eightbyte packing deferred (#165b). Mirrors cstage +// struct_float_class (cmd/w6c/cgen.c). +fn structfloatclass(c: *cgen, t: *node) i32 = { + if (c == nil) { return 0; }; + let r: *node = resolvetype(c, t); + if (r == nil) { return 0; }; + if (r.kind != nkind.N_TNAME) { return 0; }; + let nm: str = r.str; + if (streq(nm, "str")) { return 0; }; + if (primsize(nm) > 0) { return 0; }; + let si: *structinfo = structlookup(c, nm); + if (si == nil) { return 0; }; + if (si.totsize <= 0) { return 0; }; + if (si.totsize > 16) { return 0; }; + // SysV classifies aggregates in 8-byte eightbytes; 8 is the + // eightbyte stride, not a type footprint. + let nb: i32 = 1; + if (si.totsize > 8) { nb = 2; }; + let nflt0: i32 = 0; let nflt1: i32 = 0; + let nint0: i32 = 0; let nint1: i32 = 0; + let fi: *fieldinfo = si.fields; + for (fi != nil) { + let foff: i32 = fi.foff; + let fsz: i32 = fi.fsz; + let e: i32 = foff / 8; + if (e < 0) { return 0; }; + if (e >= nb) { return 0; }; + if (isfloattype(c, fi.tnode)) { + if (isf32type(c, fi.tnode)) { return 0; }; + if ((foff & 7) != 0) { return 0; }; + if (fsz != 8) { return 0; }; + if (e == 0) { nflt0 += 1; } else { nflt1 += 1; }; + } else { + if (isslicetype(c, fi.tnode)) { return 0; }; + if (isstrtype(c, fi.tnode)) { return 0; }; + if (istaggedtype(c, fi.tnode)) { return 0; }; + if (structparamsize(c, fi.tnode) > 0) { return 0; }; + // Alias-aware array/tuple reject, mirroring cstage's + // NAMED-peeled TY_ARRAY/TY_TUPLE (cgen.c struct_float_class). + // A direct-AST-kind N_TARRAY test misses an aliased array + // and every tuple field; the fsz>8 guard below also lets a + // <=8B one slip, so such a struct would wrongly SSE-route on + // this stage but stay GP on cstage (a #165b leak). + let rf: *node = resolvetype(c, fi.tnode); + if (rf != nil) { + if (rf.kind == nkind.N_TARRAY) { return 0; }; + if (rf.kind == nkind.N_TTUPLE) { return 0; }; + }; + if (fsz > 8) { return 0; }; + if ((foff + fsz - 1) / 8 != e) { return 0; }; + if (e == 0) { nint0 += 1; } else { nint1 += 1; }; + }; + fi = fi.finext; + }; + let enc: i32 = nb; + let hasfloat: bool = false; + if (nflt0 == 1 && nint0 == 0) { enc += 16; hasfloat = true; } + else { if (nflt0 != 0) { return 0; }; }; + if (nb == 2) { + if (nflt1 == 1 && nint1 == 0) { enc += 32; hasfloat = true; } + else { if (nflt1 != 0) { return 0; }; }; + }; + if (!hasfloat) { return 0; }; + return enc; +}; + // istaggedtype — alias-aware. Reads stamped tinfo so `T`, // `type alias = (A|B)`, `type error = !(invalid|overflow)` all // resolve to TY_TAGGED — tinfofornode handles the N_TBANG unwrap @@ -21192,29 +21269,76 @@ fn cgcall(c: *cgen, n: *node) void = { p = p.next; }; } else { - let extra: i32 = 0; - // str IS []u8: 3-word arg, same as slice (#1/Phase 3). - if (nodeisstr(c, a)) { extra = 2; }; - if (nodeisslice(c, a)) { extra = 2; }; - // #21: tagged-CALL arg was pushed AX/DX/CX/R8 high→low - // by pushargsrev; size the per-arg pop to match so the - // next arg's POPQ doesn't land on residual tag/payload - // words and shift intidx out of sync. - let tcs: i32 = taggedcallslot(c, a); - if (tcs > 0) { extra = tcs / 8 - 1; }; - let words: i32 = 1 + extra; - let w: i32 = 0; - for (w < words) { - if (intidx < 6) { - emitline("\tPOPQ\t"); - emitline(argregname(intidx)); - emitline("\n"); - intidx += 1; - } else { - stackslots += 1; + let stfc: i32 = 0; + if (a.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, a.str); + if (lc != nil) { stfc = structfloatclass(c, lc.tnode); }; + }; + if (stfc != 0) { + // #165: float-bearing struct arg — drain by SysV + // eightbyte class: a lone-f64 eightbyte MOVSD off + // (SP) into the next XMM (X0..X7), a pure-INT + // eightbyte POPQ into the next INTEGER arg reg + // (DI/SI/..). The struct-ident push staged raw slot + // words (class-independent); only the drain differs. + // Gated to qualifying floats; all-int + f32-packed + // keep the generic pop below. Reg overflow loud- + // stops (rule 7), the partial-spill stitch out of + // scope (#163 twin). + let nb: i32 = stfc & 15; + let e: i32 = 0; + for (e < nb) { + let issse: bool = (stfc & (16 << e)) != 0; + if (issse) { + if (fpidx >= 8) { + let msg: str = "float struct arg eightbyte overflows SSE arg regs (X0..X7); stitch out of scope, see #165\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + emitline("\tMOVSD\t(SP), "); + emitline(fargregname(fpidx)); + emitline("\n"); + emitline("\tADDQ\t$8, SP\n"); + fpidx += 1; + } else { + if (intidx >= 6) { + let msg: str = "float struct arg eightbyte overflows integer arg regs (DI/SI/DX/CX/R8/R9); stitch out of scope, see #165\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + emitline("\tPOPQ\t"); + emitline(argregname(intidx)); + emitline("\n"); + intidx += 1; + }; + popped += 1; + e += 1; + }; + } else { + let extra: i32 = 0; + // str IS []u8: 3-word arg, same as slice (#1/Phase 3). + if (nodeisstr(c, a)) { extra = 2; }; + if (nodeisslice(c, a)) { extra = 2; }; + // #21: tagged-CALL arg was pushed AX/DX/CX/R8 high→low + // by pushargsrev; size the per-arg pop to match so the + // next arg's POPQ doesn't land on residual tag/payload + // words and shift intidx out of sync. + let tcs: i32 = taggedcallslot(c, a); + if (tcs > 0) { extra = tcs / 8 - 1; }; + let words: i32 = 1 + extra; + let w: i32 = 0; + for (w < words) { + if (intidx < 6) { + emitline("\tPOPQ\t"); + emitline(argregname(intidx)); + emitline("\n"); + intidx += 1; + } else { + stackslots += 1; + }; + popped += 1; + w += 1; }; - popped += 1; - w += 1; }; }; }; @@ -26361,6 +26485,52 @@ fn cgfnparams(c: *cgen, params: *node) void = { p = p.next; continue; }; + let sfc: i32 = structfloatclass(c, p.lhs); + if (sfc != 0) { + // #165: float-bearing struct PARAM receive (param + // twin of #163's tuple). Classify each SysV + // eightbyte; a lone-f64 eightbyte reads its XMM + // (X0..X7), a pure-INT eightbyte its INTEGER arg reg + // (DI/SI/..), stored into the param slot at the + // 8-byte eightbyte stride. Gated to qualifying + // structs by structfloatclass — all-int + f32-packed + // fall through to the GP struct arm below (byte-id / + // #165b). Reg overflow loud-stops (rule 7). + let off: i32 = localadd(c, nm, structparamsize(c, p.lhs), p.lhs); + let nb: i32 = sfc & 15; + let e: i32 = 0; + for (e < nb) { + let issse: bool = (sfc & (16 << e)) != 0; + if (issse) { + if (fidx >= 8) { + let msg: str = "float struct param eightbyte overflows SSE arg regs (X0..X7); stitch out of scope, see #165\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + emitline("\tMOVSD\t"); + emitline(fargregname(fidx)); + emitline(", "); + emitoff((off + e*8): i64); + emitline("(BP)\n"); + fidx += 1; + } else { + if (idx >= 6) { + let msg: str = "float struct param eightbyte overflows integer arg regs (DI/SI/DX/CX/R8/R9); stitch out of scope, see #165\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff((off + e*8): i64); + emitline("(BP)\n"); + idx += 1; + }; + e += 1; + }; + p = p.next; + continue; + }; if (istaggedtype(c, p.lhs)) { let slot: i32 = slotsize(c, p.lhs); let nw: i32 = slot / 8; diff --git a/test/wcc/946_structparam_run.c b/test/wcc/946_structparam_run.c new file mode 100644 index 00000000..312a80e4 --- /dev/null +++ b/test/wcc/946_structparam_run.c @@ -0,0 +1,308 @@ +/* + * 946_structparam_run — runtime + byte-id + asm-pattern net for #165, the + * float-bearing struct-PARAM SysV ABI (the param twin of #171's struct + * RETURN, and the struct counterpart of #163's per-element tuple PARAM). + * + * THE BUG (#165, cs==ww but SysV-non-conformant on master): struct args + * ARE handled but GP-ONLY — the cgcall pop drained every struct eightbyte + * into the INTEGER arg regs (DI/SI/..) and the callee cgfnparams receive + * MOVQ'd them back from the same GP regs. So a `struct { a: f64, b: f64 }` + * arg landed in DI/SI 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. 907_f32arg_run, same situation). The + * genuine defect is SysV register-CLASS conformance, observable only in + * the emitted asm (and at a real ABI boundary). The discriminating + * dimension is therefore the asm pattern (c) below. + * + * THE FIX: per-EIGHTBYTE SysV classification (structs classify per + * eightbyte, unlike #163's per-element tuples). Each 8-byte eightbyte that + * is a lone f64 rides the SSE arg cursor (X0..X7); a pure-INTEGER eightbyte + * rides the INTEGER cursor (DI/SI/..). SEND (cgcall pop) MOVSD's the float + * eightbyte off (SP) into the next XMM; RECV (cgfnparams) MOVSD's the XMM + * into the param slot. Symmetric across cstage (cmd/w6c/cgen.c + * struct_float_class + the cgcall pop arm + cgfnparams) and wwstage + * (cgenutil.ww structfloatclass + cgenexpr.ww cgcall pop + cgendecl.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 #165b); a naive per-field reuse would WRONGLY + * route its two f32 to two SSE regs. 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 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 on the CALLEE function's receive: a lone-f64 eightbyte + * is received `MOVSD Xn, -off(BP)` (XMM -> slot); a GP eightbyte is + * `MOVQ DI, ...`. The marker is scoped to the callee `main.use` text + * (main's float-literal init also emits `MOVSD X0, -off(BP)`, so a + * whole-file grep would not discriminate). PRESENT for the SSE-routed + * rows, ABSENT for the GP / fallback rows. A pre-fix (or GP-regressed) + * build receives every struct eightbyte via MOVQ -> marker 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 = callee must receive an f64 eightbyte via XMM */ +}; + +static const struct row rows[] = { + /* HEADLINE — struct{f64,f64}. Both eightbytes lone f64 -> X0,X1. The + * callee receives `MOVSD X0, slot` + `MOVSD X1, slot`. s.a+s.b = 8.0. */ + { "f64f64_arg", + "package main;\n" + "type pff = struct { a: f64, b: f64 };\n" + "fn use(s: pff) f64 = { return s.a + s.b; };\n" + "export fn main() i32 = {\n" + "\tlet s: pff = pff { a = 3.0, b = 5.0 };\n" + "\tif (use(s) != 8.0) { return 1; };\n" + "\treturn 0;\n" + "};\n", 0, 1 }, + /* struct{f64,i64} — eb0 lone f64 (X0), eb1 pure-INT (DI). Class is + * independent of eightbyte position. (s.a:i64)+s.b = 3+5 = 8. */ + { "f64i64_arg", + "package main;\n" + "type pfi = struct { a: f64, b: i64 };\n" + "fn use(s: pfi) i64 = { return (s.a: i64) + s.b; };\n" + "export fn main() i32 = {\n" + "\tlet s: pfi = pfi { a = 3.0, b = 5 };\n" + "\tif (use(s) != 8) { return 1; };\n" + "\treturn 0;\n" + "};\n", 0, 1 }, + /* struct{i64,f64} — order-swap: eb0 pure-INT (DI), 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) = 3+5 = 8. */ + { "i64f64_arg", + "package main;\n" + "type pif = struct { a: i64, b: f64 };\n" + "fn use(s: pif) i64 = { return s.a + (s.b: i64); };\n" + "export fn main() i32 = {\n" + "\tlet s: pif = pif { a = 3, b = 5.0 };\n" + "\tif (use(s) != 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 GP transport unchanged (the in-tree + * byte-id case). Callee receives via MOVQ DI/SI -> no SSE marker. */ + { "i64i64_arg", + "package main;\n" + "type pii = struct { a: i64, b: i64 };\n" + "fn use(s: pii) i64 = { return s.a + s.b; };\n" + "export fn main() i32 = {\n" + "\tlet s: pii = pii { a = 3, b = 5 };\n" + "\tif (use(s) != 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 + * (received via MOVQ DI). The value still round-trips (the 8B MOVQ + * carries both f32) -> (s.a:i64)+(s.b:i64) = 3+5 = 8. Asserts BOTH the + * correct value AND no SSE receive, proving the gate caught it (NOT + * routed to 2 SSE regs). The 2-f32-per-eightbyte packing is #165b. */ + { "f32f32_arg_fallback", + "package main;\n" + "type pf32 = struct { a: f32, b: f32 };\n" + "fn use(s: pf32) i64 = { return (s.a: i64) + (s.b: i64); };\n" + "export fn main() i32 = {\n" + "\tlet s: pf32 = pf32 { a = 3.0f32, b = 5.0f32 };\n" + "\tif (use(s) != 8) { return 1; };\n" + "\treturn 0;\n" + "};\n", 0, 0 }, + /* CURSOR INDEPENDENCE — i64 scalar + struct{f64,f64}. n->DI (INTEGER + * cursor), s.a->X0, s.b->X1 (SSE cursor), independent counters. + * n + s.a + s.b = 2+3+5 = 10. SSE receive present. */ + { "scalar_plus_f64f64", + "package main;\n" + "type pff = struct { a: f64, b: f64 };\n" + "fn use(n: i64, s: pff) i64 = {\n" + "\treturn n + (s.a: i64) + (s.b: i64);\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet s: pff = pff { a = 3.0, b = 5.0 };\n" + "\tif (use(2, s) != 10) { return 1; };\n" + "\treturn 0;\n" + "};\n", 0, 1 }, + { 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; +} + +/* Does the callee `main.use` receive an f64 eightbyte via an XMM reg? + * Isolate that function's text (its receive of an SSE eightbyte is the + * only `MOVSD Xn, -off(BP)` store-to-slot in it; the body of these probes + * stores no f64 local, and main's float-literal init — also a + * `MOVSD X0, -off(BP)` — lives in a different function), then look for an + * XMM-source store to a negative BP offset. Returns 1/0, or -1 on error. */ +static int +callee_receives_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 *start = strstr(buf, "TEXT main.use"); + if (!start) return -1; + char *end = strstr(start + 1, "\nTEXT "); + if (end) *end = '\0'; + /* `MOVSD\tX0, -` is the receive of the first (or only) f64 eightbyte; + * a GP-routed struct never emits it (MOVQ DI instead). */ + return strstr(start, "MOVSD\tX0, -") != NULL ? 1 : 0; +} + +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, "structparam: 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/wwstp_%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/wwstp_%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/wwstp_%d_%d_cs.s", + getpid(), i); + snprintf(ws_s, sizeof ws_s, "/tmp/wwstp_%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 callee receive. byte-id + * (b) proves ww_s mirrors cs_s, so checking cs_s suffices. */ + int sse = callee_receives_sse(cs_s); + if (sse < 0) { + fprintf(stderr, "row[%s]: cannot scan callee .s\n", + rows[i].label); + fail++; + } else if (sse != rows[i].want_sse) { + fprintf(stderr, + "row[%s]: callee SSE receive %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-param tests failed\n", fail, n); + return 1; + } + printf("structparam: %d/%d ok (cstage run + cs==ww byte-id + " + "SSE-receive asm)\n", n, n); + return 0; +}