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

@@ -4830,6 +4830,20 @@ fn declmod(file: *node, d: *node) str = {
// installdecl — install the top-level decl's name into the top scope.
// We don't compute its type yet (that's the resolve pass) — just bind
// the name so forward references resolve.
//
// Architectural note: wwstage uses COEXISTENCE rather than the cstage
// promote-SK_USE-in-place approach in cmd/wcc/check.c. SK_USE and any
// same-leaf SK_TYPE/SK_FN/SK_DEF/SK_VAR live as separate entries in
// the same scope-bucket, distinguished by `sym.mod`. The dot-prefix
// lookup in resolvewalk + scopelookupinmodule's mod-filter already
// disambiguate `fnmatch.flag` against an `fn fnmatch(...)` of the same
// leaf — no `use_alias` flag needed. So the cstage L1722-class bug
// (promotion missing use_alias) is structurally non-reachable here.
// Don't port the use_alias flag from cstage without first re-reading
// the architecture: adding a field to `sym` changes its size and risks
// the wwstage cgen amalloc-undersize trap (rob-pike). #11 (wwstage
// checkfile pass) will reconsider this when wwstage grows a real check
// pass on the cgen path.
fn installdecl(c: *checker, file: *node, d: *node) void = {
if (d == nil) { return; };
let k: nkind = d.kind;
@@ -13671,6 +13685,146 @@ fn cgreturn(c: *cgen, n: *node) void = {
c.lastwasreturn = 1;
return;
};
// Whole-struct return for sizes <= 24B. ABI: AX=bytes[0..7],
// DX=bytes[8..15], CX=bytes[16..23]. Mirrors cstage cgen.c
// N_RETURN TY_STRUCT branch. Two rhs shapes are wired:
// N_IDENT (word-copy from rhs local slot) and N_STRUCTLIT
// (field-by-field store at scratch+foff, with tagged fields
// delegated to cgwidentaggedstore). Call-result chain return
// is deferred to #5's receive side. Sizes > 24B fall through
// to the scalar path below (only AX gets the first qword),
// pending sret.
let rname: str;
rname.ptr = nil; rname.len = 0;
if (c.fnret != nil) {
if (c.fnret.kind == nkind.N_TNAME) {
rname = c.fnret.str;
};
};
if (rname.len > 0) {
let rsi: *structinfo = structlookup(c, rname);
if (rsi != nil) {
let rsz: i32 = rsi.totsize;
if (rsz <= 24) {
let okrhs: bool = false;
if (rhs.kind == nkind.N_IDENT) {
okrhs = true;
};
if (rhs.kind == nkind.N_STRUCTLIT) {
okrhs = true;
};
if (okrhs) {
let scroff: i32 = localadd(c,
"@retscr", 24, nil);
emitline("\tXORQ\tAX, AX\n");
emitline("\tMOVQ\tAX, ");
emitoff(scroff: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tAX, ");
emitoff((scroff + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tAX, ");
emitoff((scroff + 16): i64);
emitline("(BP)\n");
if (rhs.kind == nkind.N_STRUCTLIT) {
let fn_: *node = rhs.list;
for (fn_ != nil) {
if (fn_.kind == nkind.N_FIELD) {
let fi: *fieldinfo = rsi.fields;
for (fi != nil) {
if (streq(fi.fname, fn_.str)) {
if (istaggedtype(c, fi.tnode)) {
cgwidentaggedstore(c,
fi.tnode, fn_.lhs,
"BP",
scroff + fi.foff,
fi.fsz);
fi = nil;
} else {
cgexpr(c, fn_.lhs);
if (isfloattype(c, fi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, fi.tnode)) {
mov = "MOVSS";
};
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff((scroff + fi.foff): i64);
emitline("(BP)\n");
} else {
let sop: str = fieldstoreop(c, fi);
emitline("\t");
emitline(sop);
emitline("\tAX, ");
emitoff((scroff + fi.foff): i64);
emitline("(BP)\n");
};
fi = nil;
};
} else {
fi = fi.finext;
};
};
};
fn_ = fn_.next;
};
} else {
// N_IDENT: word-copy from rhs slot
// to scratch. Whole 8B words via
// MOVQ; tail via MOVL/MOVB so we
// read no further than the source
// slot's declared size.
let rl: *local = localfindnode(c, rhs.str);
if (rl != nil) {
let k: i32 = 0;
for (k + 8 <= rsz) {
emitline("\tMOVQ\t");
emitoff((rl.off + k): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((scroff + k): i64);
emitline("(BP)\n");
k += 8;
};
for (k + 4 <= rsz) {
emitline("\tMOVL\t");
emitoff((rl.off + k): i64);
emitline("(BP), AX\n");
emitline("\tMOVL\tAX, ");
emitoff((scroff + k): i64);
emitline("(BP)\n");
k += 4;
};
for (k < rsz) {
emitline("\tMOVB\t");
emitoff((rl.off + k): i64);
emitline("(BP), AX\n");
emitline("\tMOVB\tAX, ");
emitoff((scroff + k): i64);
emitline("(BP)\n");
k += 1;
};
};
};
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");
c.lastwasreturn = 1;
return;
};
};
};
};
cgexpr(c, rhs);
} else {
// Bare `return;` from a tagged-union-returning fn is
@@ -14844,6 +14998,40 @@ fn scanlocals(c: *cgen, n: *node) i32 = {
};
};
};
// Whole-struct return for sizes <= 24B uses @retscr — when
// the function's return type is a registered TY_STRUCT of
// size <= 24 and the return rhs is N_IDENT or N_STRUCTLIT,
// cgreturn materialises in @retscr then loads AX/DX/CX.
// Mirrors cstage cgen.c which allocates the scratch slot
// inline; here we must pre-reserve so the prologue SUBQ
// reserves enough frame.
if (n.kind == nkind.N_RETURN) {
if (c.fnret != nil) {
if (c.fnret.kind == nkind.N_TNAME) {
let rname: str = c.fnret.str;
let rsi: *structinfo = structlookup(c, rname);
if (rsi != nil) {
if (rsi.totsize <= 24) {
let rhs: *node = n.lhs;
let okrhs: bool = false;
if (rhs != nil) {
if (rhs.kind == nkind.N_IDENT) {
okrhs = true;
};
if (rhs.kind == nkind.N_STRUCTLIT) {
okrhs = true;
};
};
if (okrhs) {
if (!scanseenmark(c, "@retscr")) {
total += 24;
};
};
};
};
};
};
};
// Call-site struct-payload widening uses @tagscr — when the
// arg is a struct literal/ident and the callee's param is
// tagged, pushargsrev materialises in scratch and pushes.

View File

@@ -302,6 +302,40 @@ fn scanlocals(c: *cgen, n: *node) i32 = {
};
};
};
// Whole-struct return for sizes <= 24B uses @retscr — when
// the function's return type is a registered TY_STRUCT of
// size <= 24 and the return rhs is N_IDENT or N_STRUCTLIT,
// cgreturn materialises in @retscr then loads AX/DX/CX.
// Mirrors cstage cgen.c which allocates the scratch slot
// inline; here we must pre-reserve so the prologue SUBQ
// reserves enough frame.
if (n.kind == nkind.N_RETURN) {
if (c.fnret != nil) {
if (c.fnret.kind == nkind.N_TNAME) {
let rname: str = c.fnret.str;
let rsi: *structinfo = structlookup(c, rname);
if (rsi != nil) {
if (rsi.totsize <= 24) {
let rhs: *node = n.lhs;
let okrhs: bool = false;
if (rhs != nil) {
if (rhs.kind == nkind.N_IDENT) {
okrhs = true;
};
if (rhs.kind == nkind.N_STRUCTLIT) {
okrhs = true;
};
};
if (okrhs) {
if (!scanseenmark(c, "@retscr")) {
total += 24;
};
};
};
};
};
};
};
// Call-site struct-payload widening uses @tagscr — when the
// arg is a struct literal/ident and the callee's param is
// tagged, pushargsrev materialises in scratch and pushes.

View File

@@ -253,6 +253,146 @@ fn cgreturn(c: *cgen, n: *node) void = {
c.lastwasreturn = 1;
return;
};
// Whole-struct return for sizes <= 24B. ABI: AX=bytes[0..7],
// DX=bytes[8..15], CX=bytes[16..23]. Mirrors cstage cgen.c
// N_RETURN TY_STRUCT branch. Two rhs shapes are wired:
// N_IDENT (word-copy from rhs local slot) and N_STRUCTLIT
// (field-by-field store at scratch+foff, with tagged fields
// delegated to cgwidentaggedstore). Call-result chain return
// is deferred to #5's receive side. Sizes > 24B fall through
// to the scalar path below (only AX gets the first qword),
// pending sret.
let rname: str;
rname.ptr = nil; rname.len = 0;
if (c.fnret != nil) {
if (c.fnret.kind == nkind.N_TNAME) {
rname = c.fnret.str;
};
};
if (rname.len > 0) {
let rsi: *structinfo = structlookup(c, rname);
if (rsi != nil) {
let rsz: i32 = rsi.totsize;
if (rsz <= 24) {
let okrhs: bool = false;
if (rhs.kind == nkind.N_IDENT) {
okrhs = true;
};
if (rhs.kind == nkind.N_STRUCTLIT) {
okrhs = true;
};
if (okrhs) {
let scroff: i32 = localadd(c,
"@retscr", 24, nil);
emitline("\tXORQ\tAX, AX\n");
emitline("\tMOVQ\tAX, ");
emitoff(scroff: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tAX, ");
emitoff((scroff + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tAX, ");
emitoff((scroff + 16): i64);
emitline("(BP)\n");
if (rhs.kind == nkind.N_STRUCTLIT) {
let fn_: *node = rhs.list;
for (fn_ != nil) {
if (fn_.kind == nkind.N_FIELD) {
let fi: *fieldinfo = rsi.fields;
for (fi != nil) {
if (streq(fi.fname, fn_.str)) {
if (istaggedtype(c, fi.tnode)) {
cgwidentaggedstore(c,
fi.tnode, fn_.lhs,
"BP",
scroff + fi.foff,
fi.fsz);
fi = nil;
} else {
cgexpr(c, fn_.lhs);
if (isfloattype(c, fi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, fi.tnode)) {
mov = "MOVSS";
};
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff((scroff + fi.foff): i64);
emitline("(BP)\n");
} else {
let sop: str = fieldstoreop(c, fi);
emitline("\t");
emitline(sop);
emitline("\tAX, ");
emitoff((scroff + fi.foff): i64);
emitline("(BP)\n");
};
fi = nil;
};
} else {
fi = fi.finext;
};
};
};
fn_ = fn_.next;
};
} else {
// N_IDENT: word-copy from rhs slot
// to scratch. Whole 8B words via
// MOVQ; tail via MOVL/MOVB so we
// read no further than the source
// slot's declared size.
let rl: *local = localfindnode(c, rhs.str);
if (rl != nil) {
let k: i32 = 0;
for (k + 8 <= rsz) {
emitline("\tMOVQ\t");
emitoff((rl.off + k): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((scroff + k): i64);
emitline("(BP)\n");
k += 8;
};
for (k + 4 <= rsz) {
emitline("\tMOVL\t");
emitoff((rl.off + k): i64);
emitline("(BP), AX\n");
emitline("\tMOVL\tAX, ");
emitoff((scroff + k): i64);
emitline("(BP)\n");
k += 4;
};
for (k < rsz) {
emitline("\tMOVB\t");
emitoff((rl.off + k): i64);
emitline("(BP), AX\n");
emitline("\tMOVB\tAX, ");
emitoff((scroff + k): i64);
emitline("(BP)\n");
k += 1;
};
};
};
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");
c.lastwasreturn = 1;
return;
};
};
};
};
cgexpr(c, rhs);
} else {
// Bare `return;` from a tagged-union-returning fn is

View File

@@ -4830,6 +4830,20 @@ fn declmod(file: *node, d: *node) str = {
// installdecl — install the top-level decl's name into the top scope.
// We don't compute its type yet (that's the resolve pass) — just bind
// the name so forward references resolve.
//
// Architectural note: wwstage uses COEXISTENCE rather than the cstage
// promote-SK_USE-in-place approach in cmd/wcc/check.c. SK_USE and any
// same-leaf SK_TYPE/SK_FN/SK_DEF/SK_VAR live as separate entries in
// the same scope-bucket, distinguished by `sym.mod`. The dot-prefix
// lookup in resolvewalk + scopelookupinmodule's mod-filter already
// disambiguate `fnmatch.flag` against an `fn fnmatch(...)` of the same
// leaf — no `use_alias` flag needed. So the cstage L1722-class bug
// (promotion missing use_alias) is structurally non-reachable here.
// Don't port the use_alias flag from cstage without first re-reading
// the architecture: adding a field to `sym` changes its size and risks
// the wwstage cgen amalloc-undersize trap (rob-pike). #11 (wwstage
// checkfile pass) will reconsider this when wwstage grows a real check
// pass on the cgen path.
fn installdecl(c: *checker, file: *node, d: *node) void = {
if (d == nil) { return; };
let k: nkind = d.kind;
@@ -13671,6 +13685,146 @@ fn cgreturn(c: *cgen, n: *node) void = {
c.lastwasreturn = 1;
return;
};
// Whole-struct return for sizes <= 24B. ABI: AX=bytes[0..7],
// DX=bytes[8..15], CX=bytes[16..23]. Mirrors cstage cgen.c
// N_RETURN TY_STRUCT branch. Two rhs shapes are wired:
// N_IDENT (word-copy from rhs local slot) and N_STRUCTLIT
// (field-by-field store at scratch+foff, with tagged fields
// delegated to cgwidentaggedstore). Call-result chain return
// is deferred to #5's receive side. Sizes > 24B fall through
// to the scalar path below (only AX gets the first qword),
// pending sret.
let rname: str;
rname.ptr = nil; rname.len = 0;
if (c.fnret != nil) {
if (c.fnret.kind == nkind.N_TNAME) {
rname = c.fnret.str;
};
};
if (rname.len > 0) {
let rsi: *structinfo = structlookup(c, rname);
if (rsi != nil) {
let rsz: i32 = rsi.totsize;
if (rsz <= 24) {
let okrhs: bool = false;
if (rhs.kind == nkind.N_IDENT) {
okrhs = true;
};
if (rhs.kind == nkind.N_STRUCTLIT) {
okrhs = true;
};
if (okrhs) {
let scroff: i32 = localadd(c,
"@retscr", 24, nil);
emitline("\tXORQ\tAX, AX\n");
emitline("\tMOVQ\tAX, ");
emitoff(scroff: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tAX, ");
emitoff((scroff + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tAX, ");
emitoff((scroff + 16): i64);
emitline("(BP)\n");
if (rhs.kind == nkind.N_STRUCTLIT) {
let fn_: *node = rhs.list;
for (fn_ != nil) {
if (fn_.kind == nkind.N_FIELD) {
let fi: *fieldinfo = rsi.fields;
for (fi != nil) {
if (streq(fi.fname, fn_.str)) {
if (istaggedtype(c, fi.tnode)) {
cgwidentaggedstore(c,
fi.tnode, fn_.lhs,
"BP",
scroff + fi.foff,
fi.fsz);
fi = nil;
} else {
cgexpr(c, fn_.lhs);
if (isfloattype(c, fi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, fi.tnode)) {
mov = "MOVSS";
};
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff((scroff + fi.foff): i64);
emitline("(BP)\n");
} else {
let sop: str = fieldstoreop(c, fi);
emitline("\t");
emitline(sop);
emitline("\tAX, ");
emitoff((scroff + fi.foff): i64);
emitline("(BP)\n");
};
fi = nil;
};
} else {
fi = fi.finext;
};
};
};
fn_ = fn_.next;
};
} else {
// N_IDENT: word-copy from rhs slot
// to scratch. Whole 8B words via
// MOVQ; tail via MOVL/MOVB so we
// read no further than the source
// slot's declared size.
let rl: *local = localfindnode(c, rhs.str);
if (rl != nil) {
let k: i32 = 0;
for (k + 8 <= rsz) {
emitline("\tMOVQ\t");
emitoff((rl.off + k): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((scroff + k): i64);
emitline("(BP)\n");
k += 8;
};
for (k + 4 <= rsz) {
emitline("\tMOVL\t");
emitoff((rl.off + k): i64);
emitline("(BP), AX\n");
emitline("\tMOVL\tAX, ");
emitoff((scroff + k): i64);
emitline("(BP)\n");
k += 4;
};
for (k < rsz) {
emitline("\tMOVB\t");
emitoff((rl.off + k): i64);
emitline("(BP), AX\n");
emitline("\tMOVB\tAX, ");
emitoff((scroff + k): i64);
emitline("(BP)\n");
k += 1;
};
};
};
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");
c.lastwasreturn = 1;
return;
};
};
};
};
cgexpr(c, rhs);
} else {
// Bare `return;` from a tagged-union-returning fn is
@@ -14844,6 +14998,40 @@ fn scanlocals(c: *cgen, n: *node) i32 = {
};
};
};
// Whole-struct return for sizes <= 24B uses @retscr — when
// the function's return type is a registered TY_STRUCT of
// size <= 24 and the return rhs is N_IDENT or N_STRUCTLIT,
// cgreturn materialises in @retscr then loads AX/DX/CX.
// Mirrors cstage cgen.c which allocates the scratch slot
// inline; here we must pre-reserve so the prologue SUBQ
// reserves enough frame.
if (n.kind == nkind.N_RETURN) {
if (c.fnret != nil) {
if (c.fnret.kind == nkind.N_TNAME) {
let rname: str = c.fnret.str;
let rsi: *structinfo = structlookup(c, rname);
if (rsi != nil) {
if (rsi.totsize <= 24) {
let rhs: *node = n.lhs;
let okrhs: bool = false;
if (rhs != nil) {
if (rhs.kind == nkind.N_IDENT) {
okrhs = true;
};
if (rhs.kind == nkind.N_STRUCTLIT) {
okrhs = true;
};
};
if (okrhs) {
if (!scanseenmark(c, "@retscr")) {
total += 24;
};
};
};
};
};
};
};
// Call-site struct-payload widening uses @tagscr — when the
// arg is a struct literal/ident and the callee's param is
// tagged, pushargsrev materialises in scratch and pushes.