cstage+selfhost+test: cgen N_ASSIGN whole-STRUCT (call+structlit, 5 sites)

Receive side of #4's cgreturn ABI (aee8149) for TY_STRUCT lvalues of
size <=24B. Producer materialises rhs into AX=bytes[0..7], DX=[8..15],
CX=[16..23], zero-padded to 24B; receive sites here read the regs and
write only `declared sz` bytes — MOVQ for full 8B chunks plus a sized
tail (MOVL/MOVW/MOVB) by the *declared* struct size. ASYMMETRY: do NOT
mirror the sender's three uniform MOVQs, else trailing 1..7B chunks
overrun the next local slot. Tail chunks in {3,5,6,7} are unreachable
under WW struct align rules (size%align==0) and fall through.

Five sites wired in each stage (cstage cgen.c, wwstage cgenexpr.ww +
cgenstmt.ww), call-result + structlit rhs at each:
  - N_LET   `let s: T = bar()` / `= T{...}`           cgenstmt cglet
  - N_ASSIGN N_IDENT-lhs   `s = bar()` / `= T{...}`   cgenexpr cgassign
  - N_ASSIGN single-DOT local-base   `o.f = ...`
  - N_ASSIGN single-DOT ptr-base auto-deref   `p.f = ...`
  - N_ASSIGN single-DOT global-base   `g.f = ...`
  - N_ASSIGN chained-DOT depth>=2   `o.m.in = ...`
(The four dot-flavors share one shape pattern, hence "5 sites".) Where
the dst addr needs scratch (ptr-base/global-base/via_cx), it is loaded
into BX after the call so CX stays as the third value word; for
structlit field-walks BX is reloaded before each store since cgexpr
clobbers AX/BX between fields.

wwstage needed a new `structnaturalsize(si)` helper (cgenutil.ww):
si.totsize is mis-named — it's slot-padded to 8 by registerstruct for
stack-slot use, while the receive ABI wants the type's natural size
(max(foff+fsz)). Splitting si.totsize into naturalsize + slotsize is
tracked as the wwstage struct sizing follow-up (task #15); until that
lands, the helper recovers the natural size at receive sites.

Test 701_cgassign_struct.c (18 rows, 3 checks each — cstage value,
wwstage value, asm byte-identity), wired in Makefile after 698. The
headline ASYMMETRY case is the 20B `{i32×5}` row: sender pads to 24B
via three MOVQs, receiver writes MOVQ AX +0, MOVQ DX +8, MOVL CX +16.
A regression to a MOVQ tail there overruns 4B past the slot and
flips the exit-code check.

smoke.combined.ww is the auto-regen ride-along of strings.freeall
landing in 714d089 (worker-shlex).

Pre-existing gaps surfaced and tracked separately (not fixed here,
out of scope):
  - task #16: silent drop of `(*p).f = ...` explicit-deref dot lhs.
  - task #17: silent zero of nested STRUCTLIT field in N_LET / N_ASSIGN
    initializer — the field_chain and field_global test rows use
    explicit field writes (`o.m.t = 10i64;`) rather than nested
    literals as a fixture-level workaround.
  - task #9: module-name-mangle for fn labels avoided in the
    field_global_call fixture by `let g: outer;` (no init).

make test: 59/59. 994_w6c_ww + 995_self_rebuild PASS — bootstrap
byte-identity is the load-bearing proof for this commit's scope.
This commit is contained in:
2026-05-15 16:43:07 +09:00
parent 714d089e31
commit f2643a846a
9 changed files with 3689 additions and 63 deletions

View File

@@ -2005,12 +2005,203 @@ cgexpr(Cg *c, Node *n, Local *locals)
}
break;
}
/* struct-typed field, struct-ident rhs: cgexpr cannot
* materialise a whole struct value in registers, so
* word-copy from the rhs slot directly to the dest
* field. Other rhs shapes (struct-returning call,
* struct literal) stay broken at the cgexpr level —
* tracked as #27. */
/* struct-typed field, three rhs shapes:
* - N_IDENT: word-copy from the rhs slot directly
* onto the destination field. cgexpr cannot
* materialise a whole struct value in registers
* for an arbitrary local, so we read field words
* straight from the source slot.
* - N_CALL (added with #5): cgexpr leaves the value
* in AX/DX/CX per #4's cgreturn ABI; sized stores
* write only the declared field size — MOVQ for
* full 8B chunks plus MOVL/MOVW/MOVB tail. See
* the N_LET receive site for the ASYMMETRY
* rationale. cgreturn touches only AX/DX/CX, so
* BX stays free for the dst-addr load after the
* call.
* - N_STRUCTLIT (added with #5): field-by-field
* store; for via_ptr/is_global the dst base addr
* is reloaded into BX before each store so cgexpr
* can clobber AX/BX between fields. */
if (n->op == TK_ASSIGN && str_fu
&& str_fu->kind == TY_STRUCT
&& (int)str_fu->size <= 24
&& n->rhs && n->rhs->kind == N_CALL
&& (str_fu->size % 8 == 0
|| str_fu->size % 8 == 1
|| str_fu->size % 8 == 2
|| str_fu->size % 8 == 4)) {
int ssz = (int)str_fu->size;
cgexpr(c, n->rhs, locals);
int regs[3] = { D_AX, D_DX, D_CX };
int full = ssz / 8;
int tail = ssz % 8;
int base_reg, base_disp;
if (via_ptr || is_global) {
if (via_ptr)
ins2(c, A_MOVQ,
amem(D_BP, boff),
areg(D_BX));
else
ins2(c, A_LEAQ,
masym(c, base->str),
areg(D_BX));
base_reg = D_BX;
base_disp = foff;
} else {
base_reg = D_BP;
base_disp = boff + foff;
}
for (int i = 0; i < full; i++)
ins2(c, A_MOVQ, areg(regs[i]),
amem(base_reg,
base_disp + i * 8));
if (tail > 0) {
int op = (tail == 4) ? A_MOVL
: (tail == 2) ? A_MOVW
: A_MOVB;
ins2(c, op, areg(regs[full]),
amem(base_reg,
base_disp + full * 8));
}
break;
}
if (n->op == TK_ASSIGN && str_fu
&& str_fu->kind == TY_STRUCT
&& n->rhs && n->rhs->kind == N_STRUCTLIT) {
int ssz = (int)str_fu->size;
/* TK_ELLIPSIS autofill: zero-fill the field
* region first so unmentioned inner fields
* read as 0 (mirrors N_LET / N_IDENT-lhs
* structlit branches). */
if (n->rhs->op == TK_ELLIPSIS) {
ins2(c, A_XORQ, areg(D_AX),
areg(D_AX));
int zbase_reg, zbase_disp;
if (via_ptr || is_global) {
if (via_ptr)
ins2(c, A_MOVQ,
amem(D_BP, boff),
areg(D_BX));
else
ins2(c, A_LEAQ,
masym(c, base->str),
areg(D_BX));
zbase_reg = D_BX;
zbase_disp = foff;
} else {
zbase_reg = D_BP;
zbase_disp = boff + foff;
}
int zi = 0;
while (zi + 8 <= ssz) {
ins2(c, A_MOVQ, areg(D_AX),
amem(zbase_reg,
zbase_disp + zi));
zi += 8;
}
while (zi + 4 <= ssz) {
ins2(c, A_MOVL, areg(D_AX),
amem(zbase_reg,
zbase_disp + zi));
zi += 4;
}
while (zi < ssz) {
ins2(c, A_MOVB, areg(D_AX),
amem(zbase_reg,
zbase_disp + zi));
zi += 1;
}
}
for (Node *fn = n->rhs->list; fn;
fn = fn->next) {
u64 inner_foff = 0;
int fsz = 8;
Type *ft = NULL;
for (Tfield *fl = str_fu->fields;
fl; fl = fl->next) {
if (strcmp(fl->name, fn->str) == 0) {
inner_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) {
if (via_ptr || is_global) {
if (via_ptr)
ins2(c, A_MOVQ,
amem(D_BP, boff),
areg(D_BX));
else
ins2(c, A_LEAQ,
masym(c, base->str),
areg(D_BX));
cg_widen_tagged_store(c,
&locals, fu, fn->lhs,
D_BX,
foff + (int)inner_foff,
(int)fu->size);
} else {
cg_widen_tagged_store(c,
&locals, fu, fn->lhs,
D_BP,
boff + foff + (int)inner_foff,
(int)fu->size);
}
continue;
}
cgexpr(c, fn->lhs, locals);
int sl_isf32 = 0;
if (fld_isfloat(ft, &sl_isf32)) {
int mov = sl_isf32
? A_MOVSS : A_MOVSD;
if (via_ptr || is_global) {
if (via_ptr)
ins2(c, A_MOVQ,
amem(D_BP, boff),
areg(D_BX));
else
ins2(c, A_LEAQ,
masym(c, base->str),
areg(D_BX));
ins2(c, mov, areg(D_X0),
amem(D_BX,
foff + (int)inner_foff));
} else {
ins2(c, mov, areg(D_X0),
amem(D_BP,
boff + foff + (int)inner_foff));
}
continue;
}
int op = A_MOVQ;
if (fsz == 1) op = A_MOVB;
else if (fsz == 4) op = A_MOVL;
if (via_ptr || is_global) {
if (via_ptr)
ins2(c, A_MOVQ,
amem(D_BP, boff),
areg(D_BX));
else
ins2(c, A_LEAQ,
masym(c, base->str),
areg(D_BX));
ins2(c, op, areg(D_AX),
amem(D_BX,
foff + (int)inner_foff));
} else {
ins2(c, op, areg(D_AX),
amem(D_BP,
boff + foff + (int)inner_foff));
}
}
(void)ssz;
break;
}
if (n->op == TK_ASSIGN && str_fu
&& str_fu->kind == TY_STRUCT
&& n->rhs && n->rhs->kind == N_IDENT
@@ -2621,9 +2812,192 @@ cgexpr(Cg *c, Node *n, Local *locals)
}
break;
}
/* TY_STRUCT terminal: word-copy the rhs slot onto
* base+total_off. Only N_IDENT rhs is wired —
* other shapes route through #27. */
/* TY_STRUCT terminal in the chained-DOT walker:
* three rhs shapes — mirror of the single-dot
* branch.
* - N_IDENT: word-copy from rhs local slot.
* - N_CALL (added with #5): cgexpr → AX/DX/CX
* per #4's cgreturn ABI; sized stores per
* declared field size. cgreturn touches only
* AX/DX/CX so via_cx loads the dst addr into
* BX (not CX) after the call to keep CX as
* the third value word.
* - N_STRUCTLIT (added with #5): field-by-field
* store; via_cx reloads BX before each store
* so cgexpr can clobber AX/BX between fields.
*/
if (fu && fu->kind == TY_STRUCT
&& fsz <= 24
&& n->rhs && n->rhs->kind == N_CALL
&& (fsz % 8 == 0 || fsz % 8 == 1
|| fsz % 8 == 2 || fsz % 8 == 4)) {
cgexpr(c, n->rhs, locals);
int regs[3] = { D_AX, D_DX, D_CX };
int full = fsz / 8;
int tail = fsz % 8;
int base_reg, base_off;
if (via_cx) {
if (ptr_root)
ins2(c, A_MOVQ,
amem(D_BP, base_disp),
areg(D_BX));
else
ins2(c, A_LEAQ,
masym(c, cur->str),
areg(D_BX));
base_reg = D_BX;
base_off = total_off;
} else {
base_reg = D_BP;
base_off = base_disp + total_off;
}
for (int i = 0; i < full; i++)
ins2(c, A_MOVQ, areg(regs[i]),
amem(base_reg,
base_off + i * 8));
if (tail > 0) {
int op = (tail == 4) ? A_MOVL
: (tail == 2) ? A_MOVW
: A_MOVB;
ins2(c, op, areg(regs[full]),
amem(base_reg,
base_off + full * 8));
}
break;
}
if (fu && fu->kind == TY_STRUCT
&& n->rhs && n->rhs->kind == N_STRUCTLIT) {
int ssz = fsz;
if (n->rhs->op == TK_ELLIPSIS) {
ins2(c, A_XORQ, areg(D_AX),
areg(D_AX));
int zbase_reg, zbase_off;
if (via_cx) {
if (ptr_root)
ins2(c, A_MOVQ,
amem(D_BP, base_disp),
areg(D_BX));
else
ins2(c, A_LEAQ,
masym(c, cur->str),
areg(D_BX));
zbase_reg = D_BX;
zbase_off = total_off;
} else {
zbase_reg = D_BP;
zbase_off = base_disp + total_off;
}
int zi = 0;
while (zi + 8 <= ssz) {
ins2(c, A_MOVQ, areg(D_AX),
amem(zbase_reg,
zbase_off + zi));
zi += 8;
}
while (zi + 4 <= ssz) {
ins2(c, A_MOVL, areg(D_AX),
amem(zbase_reg,
zbase_off + zi));
zi += 4;
}
while (zi < ssz) {
ins2(c, A_MOVB, areg(D_AX),
amem(zbase_reg,
zbase_off + zi));
zi += 1;
}
}
for (Node *fn = n->rhs->list; fn;
fn = fn->next) {
u64 inner_foff = 0;
int ifsz = 8;
Type *ift = NULL;
for (Tfield *fl = fu->fields; fl;
fl = fl->next) {
if (strcmp(fl->name,
fn->str) == 0) {
inner_foff = fl->offset;
ifsz = (int)(fl->type
? fl->type->size : 8);
ift = fl->type;
break;
}
}
Type *ifu = (ift
&& ift->kind == TY_NAMED)
? ift->under : ift;
if (ifu && ifu->kind == TY_TAGGED) {
if (via_cx) {
if (ptr_root)
ins2(c, A_MOVQ,
amem(D_BP, base_disp),
areg(D_BX));
else
ins2(c, A_LEAQ,
masym(c, cur->str),
areg(D_BX));
cg_widen_tagged_store(c,
&locals, ifu, fn->lhs,
D_BX,
total_off + (int)inner_foff,
(int)ifu->size);
} else {
cg_widen_tagged_store(c,
&locals, ifu, fn->lhs,
D_BP,
base_disp + total_off + (int)inner_foff,
(int)ifu->size);
}
continue;
}
cgexpr(c, fn->lhs, locals);
int sf32 = 0;
if (fld_isfloat(ift, &sf32)) {
int mov = sf32
? A_MOVSS : A_MOVSD;
if (via_cx) {
if (ptr_root)
ins2(c, A_MOVQ,
amem(D_BP, base_disp),
areg(D_BX));
else
ins2(c, A_LEAQ,
masym(c, cur->str),
areg(D_BX));
ins2(c, mov, areg(D_X0),
amem(D_BX,
total_off + (int)inner_foff));
} else {
ins2(c, mov, areg(D_X0),
amem(D_BP,
base_disp + total_off + (int)inner_foff));
}
continue;
}
int op = A_MOVQ;
if (ifsz == 1) op = A_MOVB;
else if (ifsz == 4) op = A_MOVL;
if (via_cx) {
if (ptr_root)
ins2(c, A_MOVQ,
amem(D_BP, base_disp),
areg(D_BX));
else
ins2(c, A_LEAQ,
masym(c, cur->str),
areg(D_BX));
ins2(c, op, areg(D_AX),
amem(D_BX,
total_off + (int)inner_foff));
} else {
ins2(c, op, areg(D_AX),
amem(D_BP,
base_disp + total_off + (int)inner_foff));
}
}
(void)ssz;
break;
}
if (fu && fu->kind == TY_STRUCT
&& n->rhs && n->rhs->kind == N_IDENT
&& localfind(locals, n->rhs->str) != 0) {
@@ -3048,6 +3422,115 @@ cgexpr(Cg *c, Node *n, Local *locals)
}
break;
}
/* Struct local reassignment: `s = expr;` where s is
* a TY_STRUCT local of size <=24B. Two rhs shapes,
* mirroring cglet's N_STRUCTLIT and the call-result
* branch above:
* - N_STRUCTLIT: walk fields, store at off+foff
* directly (same shape as the let-init branch).
* - N_CALL: cgexpr → AX/DX/CX, sized stores per the
* same ASYMMETRY rules documented at the N_LET
* receive site (MOVQ for full 8B chunks plus
* MOVL/MOVW/MOVB tail). The struct-IDENT word-copy
* rhs shape (s = p) is left unwired; #5 is scoped to
* the receive side of #4's cgreturn (calls + literals).
* Sizes >24B and non-{0,1,2,4}-byte tails fall through
* to the existing scalar path. */
if (lu && lu->kind == TY_STRUCT
&& (int)lu->size <= 24) {
int off = localfind(locals, n->lhs->str);
if (off != 0) {
int sz = (int)lu->size;
if (n->rhs && n->rhs->kind == N_STRUCTLIT) {
/* TK_ELLIPSIS autofill: zero-fill the
* slot first so unmentioned fields read
* as 0 (mirrors cglet's structlit). */
if (n->rhs->op == TK_ELLIPSIS) {
ins2(c, A_XORQ, areg(D_AX),
areg(D_AX));
int zi = 0;
while (zi + 8 <= sz) {
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, off + zi));
zi += 8;
}
while (zi + 4 <= sz) {
ins2(c, A_MOVL, areg(D_AX),
amem(D_BP, off + zi));
zi += 4;
}
while (zi < sz) {
ins2(c, A_MOVB, areg(D_AX),
amem(D_BP, off + zi));
zi += 1;
}
}
for (Node *f = n->rhs->list; f;
f = f->next) {
u64 foff = 0;
int fsz = 8;
Type *ft = NULL;
for (Tfield *fl = lu->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, off + (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,
off + (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,
off + (int)foff));
}
break;
}
if (n->rhs && n->rhs->kind == N_CALL
&& (sz % 8 == 0 || sz % 8 == 1
|| sz % 8 == 2
|| sz % 8 == 4)) {
cgexpr(c, n->rhs, locals);
int regs[3] = { D_AX, D_DX, D_CX };
int full = sz / 8;
int tail = sz % 8;
for (int i = 0; i < full; i++)
ins2(c, A_MOVQ, areg(regs[i]),
amem(D_BP, off + i * 8));
if (tail > 0) {
int op = (tail == 4) ? A_MOVL
: (tail == 2) ? A_MOVW
: A_MOVB;
ins2(c, op, areg(regs[full]),
amem(D_BP, off + full * 8));
}
break;
}
}
}
}
if (n->lhs->kind == N_IDENT) {
int off = localfind(locals, n->lhs->str);
@@ -5440,6 +5923,43 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
}
break;
}
/* Whole-struct receive for sizes <=24B (call-result rhs).
* Counterpart of #4's cgreturn ABI: cgexpr leaves
* AX=bytes[0..7], DX=bytes[8..15], CX=bytes[16..23], zero-
* padded to 24B by the producer.
*
* ASYMMETRY (do NOT mirror the sender): producer emits three
* uniform MOVQs into a zero-padded 24B scratch slot; the
* receiver must write only `sz` bytes — MOVQ for full 8B
* chunks plus a sized tail (MOVL/MOVW/MOVB) by the *declared*
* struct size. Otherwise a trailing 1..7-byte chunk would
* overrun into the next local slot.
*
* Tail chunks in {3,5,6,7} (would need shift-and-store from
* the register) are unreachable under WW struct alignment
* rules (field aligns force size%align==0); the guard
* 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). */
if (n->rhs && n->rhs->kind == N_CALL && lu
&& lu->kind == TY_STRUCT && sz <= 24
&& (sz % 8 == 0 || sz % 8 == 1
|| sz % 8 == 2 || sz % 8 == 4)) {
cgexpr(c, n->rhs, *locals);
int regs[3] = { D_AX, D_DX, D_CX };
int full = sz / 8;
int tail = sz % 8;
for (int i = 0; i < full; i++)
ins2(c, A_MOVQ, areg(regs[i]),
amem(D_BP, off + i * 8));
if (tail > 0) {
int op = (tail == 4) ? A_MOVL
: (tail == 2) ? A_MOVW : A_MOVB;
ins2(c, op, areg(regs[full]),
amem(D_BP, off + full * 8));
}
break;
}
/* array literal initialiser: `let xs: [N]T = [a, b, c];`.
* Walk elements in declaration order, store each at off + i*esz
* using the right width for the element type. The trailing