wcc: struct-param float fields via SSE arg regs (#165)

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.
This commit is contained in:
2026-05-27 23:31:19 +09:00
parent 0465c423c1
commit a917533fbf
8 changed files with 1048 additions and 73 deletions

View File

@@ -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. */