cstage+selfhost+test: cgreturn TY_STRUCT <=24B via AX/DX/CX

Whole-struct return ABI for sizes <=24B. Both stages materialise rhs
into a zero-padded 24B @retscr scratch slot, then load AX=bytes[0..7],
DX=bytes[8..15], CX=bytes[16..23] unconditionally — three MOVQs
regardless of declared struct size, so the receive side (landing in
task #5) can read all three words and mask by the declared size. R8
stays reserved for the tagged-return 4th word; the uniform-MOVQ shape
is cheap over a size-conditional partial-load and keeps the producer
diff vs the existing tagged-return AX/DX/CX/R8 path minimal.

Two rhs shapes wired this pass: N_IDENT (word-copy from rhs local slot,
MOVQ pairs + MOVL/MOVB tail bounded by declared struct size) and
N_STRUCTLIT (field-walk; tagged fields delegate to the existing tagged
widening helper, float fields go through X0, int fields use MOVQ/MOVL/
MOVB by field size). Sizes >24B fall through to the existing scalar
path (only AX gets the first qword), pending sret in a future task.
N_CALL chain-return (`return otherfn()`) is deferred to task #5's
receive side — until that lands the call-result lives in caller regs.

The wwstage mirror in cgenstmt.ww matches cgen.c byte-for-byte on the
new branch; cgendecl.ww's scanlocals pre-reserves 24B for @retscr under
the same predicate (N_RETURN, fnret is N_TNAME, structlookup hit,
totsize<=24, rhs is N_IDENT|N_STRUCTLIT) since wwstage writes its
prologue SUBQ from the upfront frame total — cstage patches SUBQ at fn
end so it can allocate inline.

Latent fsz==2 MOVW divergence between stages (cstage structlit int-
branch only special-cases fsz 1/4, wwstage's fieldstoreop also returns
MOVW for fsz==2) tracked as task #13; not exercised by the new fixtures
or by any current selfhost <=24B struct return.

main.combined.ww files also pick up worker-checkfix's wwstage
architectural comment from 7f60ebb (auto-regen ran after that commit).
This commit is contained in:
2026-05-15 15:19:38 +09:00
parent 1d5ff201ee
commit aee8149754
7 changed files with 937 additions and 0 deletions

View File

@@ -5640,6 +5640,130 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
break;
}
}
/* Whole-struct return for sizes ≤24B. ABI: AX=bytes[0..7],
* DX=bytes[8..15], CX=bytes[16..23]. Sizes >24B are not
* wired (sret deferred); they fall through to the scalar
* path below and return only AX. Materialise rhs into a
* zero-padded 24B scratch slot, then emit AX/DX/CX loads
* unconditionally so the instruction shape is constant
* regardless of declared struct size. The receive side
* masks via the dst slot's declared size. Two rhs shapes
* are wired: N_IDENT (word-copy from rhs local slot) and
* N_STRUCTLIT (field-by-field store at scratch+foff). Call-
* result chain return is deferred to #5's receive side. */
if (n->lhs && cg_ret_type) {
Type *rt = cg_ret_type;
if (rt->kind == TY_NAMED) rt = rt->under;
if (rt && rt->kind == TY_STRUCT && rt->size <= 24
&& (n->lhs->kind == N_IDENT
|| n->lhs->kind == N_STRUCTLIT)) {
int sz = (int)rt->size;
const char *scrn = mklabel(c, "retscr");
int scr = local_alloc(c, locals, scrn, 24,
cg_frame);
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, scr + 0));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, scr + 8));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, scr + 16));
if (n->lhs->kind == N_STRUCTLIT) {
for (Node *f = n->lhs->list; f;
f = f->next) {
u64 foff = 0;
int fsz = 8;
Type *ft = NULL;
for (Tfield *fl = rt->fields;
fl; fl = fl->next) {
if (strcmp(fl->name,
f->str) == 0) {
foff = fl->offset;
fsz = (int)(fl->type
? fl->type->size
: 8);
ft = fl->type;
break;
}
}
Type *fu = (ft && ft->kind
== TY_NAMED)
? ft->under : ft;
if (fu && fu->kind
== TY_TAGGED) {
cg_widen_tagged_store(c,
locals, fu, f->lhs,
D_BP,
scr + (int)foff,
(int)fu->size);
continue;
}
cgexpr(c, f->lhs, *locals);
int sl_isf32 = 0;
if (fld_isfloat(ft,
&sl_isf32)) {
int mov = sl_isf32
? A_MOVSS
: A_MOVSD;
ins2(c, mov,
areg(D_X0),
amem(D_BP,
scr + (int)foff));
continue;
}
int op = A_MOVQ;
if (fsz == 1) op = A_MOVB;
else if (fsz == 4) op = A_MOVL;
ins2(c, op, areg(D_AX),
amem(D_BP,
scr + (int)foff));
}
} else {
/* N_IDENT: word-copy rhs slot into
* scratch. Whole 8B words via MOVQ;
* trailing partial word via MOVL/MOVB
* so we read no further than the
* source slot's declared size. */
int rhsoff = localfind(*locals,
n->lhs->str);
int k = 0;
while (k + 8 <= sz) {
ins2(c, A_MOVQ,
amem(D_BP, rhsoff + k),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, scr + k));
k += 8;
}
while (k + 4 <= sz) {
ins2(c, A_MOVL,
amem(D_BP, rhsoff + k),
areg(D_AX));
ins2(c, A_MOVL, areg(D_AX),
amem(D_BP, scr + k));
k += 4;
}
while (k < sz) {
ins2(c, A_MOVB,
amem(D_BP, rhsoff + k),
areg(D_AX));
ins2(c, A_MOVB, areg(D_AX),
amem(D_BP, scr + k));
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));
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
ins1(c, A_POPQ, areg(D_BP));
ins0(c, A_RET);
break;
}
}
if (n->lhs && node_isstr(n->lhs)) {
cgexpr(c, n->lhs, *locals); /* AX=ptr, BX=len */
ins2(c, A_MOVQ, areg(D_BX), areg(D_DX));