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

@@ -227,6 +227,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
$(BIN)/test_modtype_leaf_collision \
$(BIN)/test_samemod_prefer \
$(BIN)/test_cgreturn_struct \
$(BIN)/test_cgassign_struct \
$(BIN)/test_use_promote_alias \
$(BIN)/test_field_signed $(BIN)/test_frame_argcount \
$(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \
@@ -374,6 +375,12 @@ $(BIN)/test_cgreturn_struct: test/wcc/698_cgreturn_struct.c \
$(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
$(BIN)/test_cgassign_struct: test/wcc/701_cgassign_struct.c \
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \
$(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
$(BIN)/test_use_promote_alias: test/wcc/699_use_promote_alias.c \
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(LIB)/libwwrt.a | $(BIN)

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

View File

@@ -476,6 +476,32 @@ export fn dup(s: str) str = {
return r;
};
// freeall — release every str element in `s` (those that were
// individually allocated) plus the slice's backing storage. Mirrors
// Hare's strings::freeall — the natural disposer for any function
// returning a fresh `[]str` of dup'd elements (e.g. shlex.split).
//
// Each element is freed via os.free at its own length; the slice
// header storage is freed at `cap * 16` bytes (one str = 16B). Empty
// elements (`{nil, 0}` from a zero-length dup) are skipped — calling
// os.free on a nil pointer at len 0 would tickle the rt_free guard
// that the runtime treats as a logic bug.
//
// `cap == 0` means the slice was never grown (empty `[]str` with no
// backing allocation); skip the header free in that case too.
export fn freeall(s: []str) void = {
let i: i32 = 0;
for (i < s.len) {
if (s[i].len > 0) {
os.free(s[i].ptr: *void, s[i].len: u64);
};
i += 1;
};
if (s.cap > 0) {
os.free(s.ptr: *void, (s.cap: u64) * 16u64);
};
};
// rbyteindex — last byte position of `needle` in `s`. Mirrors Hare's
// strings::rbyteindex. Rune needle scans for the byte that encodes it
// (ASCII only); str needle scans for the substring. Empty str needle
@@ -6891,6 +6917,31 @@ fn nodeprimwidth(c: *cgen, n: *node) i32 = {
// ---- type-driven slot sizing ----------------------------------------
// structnaturalsize — type-natural size of `si`, i.e. max(foff +
// fsz) across declared fields. Mirrors cstage's `lu->size` for a
// TY_STRUCT (rounded only to the struct's maxalign).
//
// NOTE: si.totsize is mis-named — it's actually the *slot-padded*
// size (rounded up to 8 for stack-slot use; see registerstruct's
// tail `if ((off & 7) != 0) ...`). Frame allocation, [N]foo stride,
// and similar consumers want that slot-padded number. The
// receive-side ABI (#5) and any future "TYPE size, not slot size"
// query wants the natural size. Until si.totsize is split into
// si.naturalsize + si.slotsize (tracked as the wwstage-sizing
// follow-up task), recover the type-natural size from the field
// chain here.
fn structnaturalsize(si: *structinfo) i32 = {
if (si == nil) { return 0; };
let n: i32 = 0;
let fi: *fieldinfo = si.fields;
for (fi != nil) {
let end: i32 = fi.foff + fi.fsz;
if (end > n) { n = end; };
fi = fi.finext;
};
return n;
};
fn structlookup(c: *cgen, name: str) *structinfo = {
// Exact match first: bare-from-source struct names and already-
// leafed lookups hit here directly.
@@ -12250,11 +12301,148 @@ fn cgassign(c: *cgen, n: *node) void = {
n.rhs, "BX", fi.foff, fsz);
return;
};
// struct-typed field, struct-ident rhs through
// *struct base: cgexpr can't materialise a whole
// struct value, so word-copy from the rhs slot
// to *(struct_ptr + fi.foff). Other rhs shapes
// (call result, struct literal) tracked as #27.
// struct-typed field via *struct base — three
// rhs shapes (call/structlit added with #5;
// closes #27 marker here):
// N_IDENT: word-copy from rhs slot.
// N_CALL: cgexpr → AX/DX/CX per #4's cgreturn
// ABI; load *struct ptr into BX after the
// call, sized stores per natural struct size.
// N_STRUCTLIT: field-walk; reload BX before
// each store so cgexpr can clobber AX/BX.
// si.totsize is slot-padded; use
// structnaturalsize for the type-size query.
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_CALL
&& fi.tnode != nil
&& fi.tnode.kind == nkind.N_TNAME
&& primsize(fi.tnode.str) == 0) {
let ssi: *structinfo = structlookup(c, fi.tnode.str);
if (ssi != nil) {
let ssz: i32 = structnaturalsize(ssi);
if (ssz <= 24) {
let tlm: i32 = ssz - (ssz / 8) * 8;
if (tlm == 0 || tlm == 1
|| tlm == 2 || tlm == 4) {
cgexpr(c, n.rhs);
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
let full: i32 = ssz / 8;
let i: i32 = 0;
for (i < full) {
let reg: str = "AX";
if (i == 1) { reg = "DX"; };
if (i == 2) { reg = "CX"; };
emitline("\tMOVQ\t");
emitline(reg);
emitline(", ");
emitdispreg((fi.foff + i * 8): i64, "BX");
emitline("\n");
i += 1;
};
if (tlm > 0) {
let top: str = "MOVB";
if (tlm == 4) { top = "MOVL"; };
if (tlm == 2) { top = "MOVW"; };
let treg: str = "AX";
if (full == 1) { treg = "DX"; };
if (full == 2) { treg = "CX"; };
emitline("\t");
emitline(top);
emitline("\t");
emitline(treg);
emitline(", ");
emitdispreg((fi.foff + full * 8): i64, "BX");
emitline("\n");
};
return;
};
};
};
};
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_STRUCTLIT
&& fi.tnode != nil
&& fi.tnode.kind == nkind.N_TNAME
&& primsize(fi.tnode.str) == 0) {
let ssi: *structinfo = structlookup(c, fi.tnode.str);
if (ssi != nil) {
let ssz: i32 = structnaturalsize(ssi);
if (n.rhs.op == tkind.TK_ELLIPSIS) {
emitline("\tXORQ\tAX, AX\n");
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
let zi: i32 = 0;
for (zi + 8 <= ssz) {
emitline("\tMOVQ\tAX, ");
emitdispreg((fi.foff + zi): i64, "BX");
emitline("\n");
zi += 8;
};
for (zi + 4 <= ssz) {
emitline("\tMOVL\tAX, ");
emitdispreg((fi.foff + zi): i64, "BX");
emitline("\n");
zi += 4;
};
for (zi < ssz) {
emitline("\tMOVB\tAX, ");
emitdispreg((fi.foff + zi): i64, "BX");
emitline("\n");
zi += 1;
};
};
let fldn: *node = n.rhs.list;
for (fldn != nil) {
if (fldn.kind == nkind.N_FIELD) {
let fnm: str = fldn.str;
let ifi: *fieldinfo = ssi.fields;
for (ifi != nil) {
if (streq(ifi.fname, fnm)) {
cgexpr(c, fldn.lhs);
if (isfloattype(c, ifi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, ifi.tnode)) {
mov = "MOVSS";
};
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitdispreg((fi.foff + ifi.foff): i64, "BX");
emitline("\n");
ifi = nil;
} else {
let ifsz: i32 = ifi.fsz;
let op: str = "MOVQ";
if (ifsz == 1) { op = "MOVB"; };
if (ifsz == 4) { op = "MOVL"; };
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
emitline("\t");
emitline(op);
emitline("\tAX, ");
emitdispreg((fi.foff + ifi.foff): i64, "BX");
emitline("\n");
ifi = nil;
};
} else {
ifi = ifi.finext;
};
};
};
fldn = fldn.next;
};
return;
};
};
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_IDENT
@@ -12411,10 +12599,134 @@ fn cgassign(c: *cgen, n: *node) void = {
n.rhs, "BP", lc.off + fi.foff, fsz);
return;
};
// struct-typed field, struct-ident rhs:
// word-copy direct, skipping cgexpr (no
// register convention for a whole struct
// value). #27 covers non-ident rhs.
// struct-typed field on a direct struct
// local — three rhs shapes (call/structlit
// added with #5; closes #27 marker here):
// N_IDENT: word-copy from rhs slot.
// N_CALL: cgexpr → AX/DX/CX; sized stores
// directly at (lc.off+fi.foff)(BP).
// N_STRUCTLIT: field-walk; each inner
// field stored at +fi.foff+inner_foff(BP).
// BP-rel direct, no addr scratch needed.
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_CALL
&& fi.tnode != nil
&& fi.tnode.kind == nkind.N_TNAME
&& primsize(fi.tnode.str) == 0) {
let ssi: *structinfo = structlookup(c, fi.tnode.str);
if (ssi != nil) {
let ssz: i32 = structnaturalsize(ssi);
if (ssz <= 24) {
let tlm: i32 = ssz - (ssz / 8) * 8;
if (tlm == 0 || tlm == 1
|| tlm == 2 || tlm == 4) {
cgexpr(c, n.rhs);
let full: i32 = ssz / 8;
let i: i32 = 0;
for (i < full) {
let reg: str = "AX";
if (i == 1) { reg = "DX"; };
if (i == 2) { reg = "CX"; };
emitline("\tMOVQ\t");
emitline(reg);
emitline(", ");
emitoff((lc.off + fi.foff + i * 8): i64);
emitline("(BP)\n");
i += 1;
};
if (tlm > 0) {
let top: str = "MOVB";
if (tlm == 4) { top = "MOVL"; };
if (tlm == 2) { top = "MOVW"; };
let treg: str = "AX";
if (full == 1) { treg = "DX"; };
if (full == 2) { treg = "CX"; };
emitline("\t");
emitline(top);
emitline("\t");
emitline(treg);
emitline(", ");
emitoff((lc.off + fi.foff + full * 8): i64);
emitline("(BP)\n");
};
return;
};
};
};
};
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_STRUCTLIT
&& fi.tnode != nil
&& fi.tnode.kind == nkind.N_TNAME
&& primsize(fi.tnode.str) == 0) {
let ssi: *structinfo = structlookup(c, fi.tnode.str);
if (ssi != nil) {
let ssz: i32 = structnaturalsize(ssi);
if (n.rhs.op == tkind.TK_ELLIPSIS) {
emitline("\tXORQ\tAX, AX\n");
let zi: i32 = 0;
for (zi + 8 <= ssz) {
emitline("\tMOVQ\tAX, ");
emitoff((lc.off + fi.foff + zi): i64);
emitline("(BP)\n");
zi += 8;
};
for (zi + 4 <= ssz) {
emitline("\tMOVL\tAX, ");
emitoff((lc.off + fi.foff + zi): i64);
emitline("(BP)\n");
zi += 4;
};
for (zi < ssz) {
emitline("\tMOVB\tAX, ");
emitoff((lc.off + fi.foff + zi): i64);
emitline("(BP)\n");
zi += 1;
};
};
let fldn: *node = n.rhs.list;
for (fldn != nil) {
if (fldn.kind == nkind.N_FIELD) {
let fnm: str = fldn.str;
let ifi: *fieldinfo = ssi.fields;
for (ifi != nil) {
if (streq(ifi.fname, fnm)) {
cgexpr(c, fldn.lhs);
if (isfloattype(c, ifi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, ifi.tnode)) {
mov = "MOVSS";
};
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff((lc.off + fi.foff + ifi.foff): i64);
emitline("(BP)\n");
ifi = nil;
} else {
let ifsz: i32 = ifi.fsz;
let op: str = "MOVQ";
if (ifsz == 1) { op = "MOVB"; };
if (ifsz == 4) { op = "MOVL"; };
emitline("\t");
emitline(op);
emitline("\tAX, ");
emitoff((lc.off + fi.foff + ifi.foff): i64);
emitline("(BP)\n");
ifi = nil;
};
} else {
ifi = ifi.finext;
};
};
};
fldn = fldn.next;
};
return;
};
};
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_IDENT
@@ -12593,10 +12905,144 @@ fn cgassign(c: *cgen, n: *node) void = {
let fi: *fieldinfo = si.fields;
for (fi != nil) {
if (streq(fi.fname, fld)) {
// struct-typed field, struct-ident rhs on global
// struct base: word-copy direct. Skips cgexpr —
// no register convention for a whole struct value.
// #27 covers non-ident rhs.
// struct-typed field on a global struct base —
// three rhs shapes (call/structlit added with
// #5; closes #27 marker here):
// N_IDENT: word-copy from rhs slot.
// N_CALL: cgexpr → AX/DX/CX; LEAQ base into BX
// after call, sized stores per natural size.
// N_STRUCTLIT: field-walk; reload BX per store.
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_CALL
&& fi.tnode != nil
&& fi.tnode.kind == nkind.N_TNAME
&& primsize(fi.tnode.str) == 0) {
let ssi: *structinfo = structlookup(c, fi.tnode.str);
if (ssi != nil) {
let ssz: i32 = structnaturalsize(ssi);
if (ssz <= 24) {
let tlm: i32 = ssz - (ssz / 8) * 8;
if (tlm == 0 || tlm == 1
|| tlm == 2 || tlm == 4) {
cgexpr(c, n.rhs);
emitline("\tLEAQ\t");
emitsymname(c, bn);
emitline("(SB), BX\n");
let full: i32 = ssz / 8;
let i: i32 = 0;
for (i < full) {
let reg: str = "AX";
if (i == 1) { reg = "DX"; };
if (i == 2) { reg = "CX"; };
emitline("\tMOVQ\t");
emitline(reg);
emitline(", ");
emitdispreg((fi.foff + i * 8): i64, "BX");
emitline("\n");
i += 1;
};
if (tlm > 0) {
let top: str = "MOVB";
if (tlm == 4) { top = "MOVL"; };
if (tlm == 2) { top = "MOVW"; };
let treg: str = "AX";
if (full == 1) { treg = "DX"; };
if (full == 2) { treg = "CX"; };
emitline("\t");
emitline(top);
emitline("\t");
emitline(treg);
emitline(", ");
emitdispreg((fi.foff + full * 8): i64, "BX");
emitline("\n");
};
return;
};
};
};
};
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_STRUCTLIT
&& fi.tnode != nil
&& fi.tnode.kind == nkind.N_TNAME
&& primsize(fi.tnode.str) == 0) {
let ssi: *structinfo = structlookup(c, fi.tnode.str);
if (ssi != nil) {
let ssz: i32 = structnaturalsize(ssi);
if (n.rhs.op == tkind.TK_ELLIPSIS) {
emitline("\tXORQ\tAX, AX\n");
emitline("\tLEAQ\t");
emitsymname(c, bn);
emitline("(SB), BX\n");
let zi: i32 = 0;
for (zi + 8 <= ssz) {
emitline("\tMOVQ\tAX, ");
emitdispreg((fi.foff + zi): i64, "BX");
emitline("\n");
zi += 8;
};
for (zi + 4 <= ssz) {
emitline("\tMOVL\tAX, ");
emitdispreg((fi.foff + zi): i64, "BX");
emitline("\n");
zi += 4;
};
for (zi < ssz) {
emitline("\tMOVB\tAX, ");
emitdispreg((fi.foff + zi): i64, "BX");
emitline("\n");
zi += 1;
};
};
let fldn: *node = n.rhs.list;
for (fldn != nil) {
if (fldn.kind == nkind.N_FIELD) {
let fnm: str = fldn.str;
let ifi: *fieldinfo = ssi.fields;
for (ifi != nil) {
if (streq(ifi.fname, fnm)) {
cgexpr(c, fldn.lhs);
if (isfloattype(c, ifi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, ifi.tnode)) {
mov = "MOVSS";
};
emitline("\tLEAQ\t");
emitsymname(c, bn);
emitline("(SB), BX\n");
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitdispreg((fi.foff + ifi.foff): i64, "BX");
emitline("\n");
ifi = nil;
} else {
let ifsz: i32 = ifi.fsz;
let op: str = "MOVQ";
if (ifsz == 1) { op = "MOVB"; };
if (ifsz == 4) { op = "MOVL"; };
emitline("\tLEAQ\t");
emitsymname(c, bn);
emitline("(SB), BX\n");
emitline("\t");
emitline(op);
emitline("\tAX, ");
emitdispreg((fi.foff + ifi.foff): i64, "BX");
emitline("\n");
ifi = nil;
};
} else {
ifi = ifi.finext;
};
};
};
fldn = fldn.next;
};
return;
};
};
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_IDENT
@@ -12883,11 +13329,236 @@ fn cgassign(c: *cgen, n: *node) void = {
};
return;
};
// TY_STRUCT terminal: word-copy the rhs slot onto
// base+totaloff. Only N_IDENT rhs is wired — other
// shapes (call result, struct literal) route through
// #27. cgexpr is skipped (no whole-struct register
// convention); reads come straight from the rhs slot.
// TY_STRUCT terminal: three rhs shapes:
// - N_IDENT: word-copy from the rhs local slot
// (cgexpr is skipped — no whole-struct register
// convention for an arbitrary local).
// - 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.
// cgreturn touches only AX/DX/CX so for
// ptrroot/global we load 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; for ptrroot/global the dst addr is
// reloaded into BX before each store so cgexpr
// can clobber AX/BX between fields.
if (n.rhs != nil
&& n.rhs.kind == nkind.N_CALL
&& leaffi.tnode != nil
&& leaffi.tnode.kind == nkind.N_TNAME
&& primsize(leaffi.tnode.str) == 0) {
let lsi: *structinfo = structlookup(c, leaffi.tnode.str);
if (lsi != nil) {
// si.totsize is slot-padded (rounded to 8);
// receive ABI needs the TYPE's natural size.
let lsz: i32 = structnaturalsize(lsi);
if (lsz <= 24) {
let tlm: i32 = lsz - (lsz / 8) * 8;
if (tlm == 0 || tlm == 1
|| tlm == 2 || tlm == 4) {
cgexpr(c, n.rhs);
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), BX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), BX\n");
};
};
let full: i32 = lsz / 8;
let i: i32 = 0;
for (i < full) {
let reg: str = "AX";
if (i == 1) { reg = "DX"; };
if (i == 2) { reg = "CX"; };
if (viacx) {
emitline("\tMOVQ\t");
emitline(reg);
emitline(", ");
emitdispreg((totaloff + i * 8): i64, "BX");
emitline("\n");
} else {
emitline("\tMOVQ\t");
emitline(reg);
emitline(", ");
emitoff((rootoff + totaloff + i * 8): i64);
emitline("(BP)\n");
};
i += 1;
};
if (tlm > 0) {
let top: str = "MOVB";
if (tlm == 4) { top = "MOVL"; };
if (tlm == 2) { top = "MOVW"; };
let treg: str = "AX";
if (full == 1) { treg = "DX"; };
if (full == 2) { treg = "CX"; };
if (viacx) {
emitline("\t");
emitline(top);
emitline("\t");
emitline(treg);
emitline(", ");
emitdispreg((totaloff + full * 8): i64, "BX");
emitline("\n");
} else {
emitline("\t");
emitline(top);
emitline("\t");
emitline(treg);
emitline(", ");
emitoff((rootoff + totaloff + full * 8): i64);
emitline("(BP)\n");
};
};
return;
};
};
};
};
if (n.rhs != nil
&& n.rhs.kind == nkind.N_STRUCTLIT
&& leaffi.tnode != nil
&& leaffi.tnode.kind == nkind.N_TNAME
&& primsize(leaffi.tnode.str) == 0) {
let lsi: *structinfo = structlookup(c, leaffi.tnode.str);
if (lsi != nil) {
// si.totsize is slot-padded (rounded to 8);
// receive ABI needs the TYPE's natural size.
let lsz: i32 = structnaturalsize(lsi);
if (n.rhs.op == tkind.TK_ELLIPSIS) {
emitline("\tXORQ\tAX, AX\n");
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), BX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), BX\n");
};
let zi: i32 = 0;
for (zi + 8 <= lsz) {
emitline("\tMOVQ\tAX, ");
emitdispreg((totaloff + zi): i64, "BX");
emitline("\n");
zi += 8;
};
for (zi + 4 <= lsz) {
emitline("\tMOVL\tAX, ");
emitdispreg((totaloff + zi): i64, "BX");
emitline("\n");
zi += 4;
};
for (zi < lsz) {
emitline("\tMOVB\tAX, ");
emitdispreg((totaloff + zi): i64, "BX");
emitline("\n");
zi += 1;
};
} else {
let zi: i32 = 0;
for (zi + 8 <= lsz) {
emitline("\tMOVQ\tAX, ");
emitoff((rootoff + totaloff + zi): i64);
emitline("(BP)\n");
zi += 8;
};
for (zi + 4 <= lsz) {
emitline("\tMOVL\tAX, ");
emitoff((rootoff + totaloff + zi): i64);
emitline("(BP)\n");
zi += 4;
};
for (zi < lsz) {
emitline("\tMOVB\tAX, ");
emitoff((rootoff + totaloff + zi): i64);
emitline("(BP)\n");
zi += 1;
};
};
};
let fldn: *node = n.rhs.list;
for (fldn != nil) {
if (fldn.kind == nkind.N_FIELD) {
let fnm: str = fldn.str;
let fi: *fieldinfo = lsi.fields;
for (fi != nil) {
if (streq(fi.fname, fnm)) {
cgexpr(c, fldn.lhs);
if (isfloattype(c, fi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, fi.tnode)) {
mov = "MOVSS";
};
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), BX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), BX\n");
};
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitdispreg((totaloff + fi.foff): i64, "BX");
emitline("\n");
} else {
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff((rootoff + totaloff + fi.foff): i64);
emitline("(BP)\n");
};
fi = nil;
} else {
let fsz: i32 = fi.fsz;
let op: str = "MOVQ";
if (fsz == 1) { op = "MOVB"; };
if (fsz == 4) { op = "MOVL"; };
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), BX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), BX\n");
};
emitline("\t");
emitline(op);
emitline("\tAX, ");
emitdispreg((totaloff + fi.foff): i64, "BX");
emitline("\n");
} else {
emitline("\t");
emitline(op);
emitline("\tAX, ");
emitoff((rootoff + totaloff + fi.foff): i64);
emitline("(BP)\n");
};
fi = nil;
};
} else {
fi = fi.finext;
};
};
};
fldn = fldn.next;
};
return;
};
};
if (n.rhs != nil
&& n.rhs.kind == nkind.N_IDENT
&& leaffi.tnode != nil
@@ -13306,6 +13977,150 @@ fn cgassign(c: *cgen, n: *node) void = {
lcf = isfloattype(c, lcn.tnode);
lcf32 = isf32type(c, lcn.tnode);
};
// Struct-typed local reassignment: `s = expr;` where s
// is a TY_STRUCT local of size <=24B. Two rhs shapes
// (mirrors cglet's N_STRUCTLIT and the call-result
// receive branch):
// - N_STRUCTLIT: walk fields, store at off+foff
// directly. ASYMMETRY-safe (no register copy from
// the caller; values come from cgexpr).
// - N_CALL: cgexpr → AX/DX/CX, sized stores per the
// declared struct size — MOVQ for full 8B chunks
// plus MOVL/MOVW/MOVB tail. See cglet receive
// site for the ASYMMETRY rationale.
// Struct-IDENT word-copy rhs (s = p) is left unwired;
// #5 is scoped to receive-side of #4 (calls + literals).
// fsz dispatch uses the explicit {1→MOVB, 4→MOVL, else
// MOVQ} pattern (not fieldstoreop) to match cstage
// cgen.c N_ASSIGN byte-identically — wwstage's
// fieldstoreop returns MOVW for fsz==2 which cstage
// doesn't emit (tracked separately as the cstage/
// wwstage MOVW divergence task).
if (lcn != nil) {
let lctn: *node = lcn.tnode;
let lcsname: str;
lcsname.ptr = nil; lcsname.len = 0;
if (lctn != nil) {
if (lctn.kind == nkind.N_TNAME) {
lcsname = lctn.str;
};
};
if (lcsname.len > 0) {
let lcsi: *structinfo = structlookup(c, lcsname);
if (lcsi != nil) {
// si.totsize is slot-padded (rounded to 8);
// receive ABI needs the TYPE's natural size.
let lcnsz: i32 = structnaturalsize(lcsi);
if (n.op == tkind.TK_ASSIGN) {
if (n.rhs != nil
&& n.rhs.kind == nkind.N_STRUCTLIT) {
let lcsz: i32 = lcnsz;
if (n.rhs.op == tkind.TK_ELLIPSIS) {
emitline("\tXORQ\tAX, AX\n");
let zi: i32 = 0;
for (zi + 8 <= lcsz) {
emitline("\tMOVQ\tAX, ");
emitoff((off + zi): i64);
emitline("(BP)\n");
zi += 8;
};
for (zi + 4 <= lcsz) {
emitline("\tMOVL\tAX, ");
emitoff((off + zi): i64);
emitline("(BP)\n");
zi += 4;
};
for (zi < lcsz) {
emitline("\tMOVB\tAX, ");
emitoff((off + zi): i64);
emitline("(BP)\n");
zi += 1;
};
};
let fldn: *node = n.rhs.list;
for (fldn != nil) {
if (fldn.kind == nkind.N_FIELD) {
let fnm: str = fldn.str;
let fi: *fieldinfo = lcsi.fields;
for (fi != nil) {
if (streq(fi.fname, fnm)) {
cgexpr(c, fldn.lhs);
if (isfloattype(c, fi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, fi.tnode)) {
mov = "MOVSS";
};
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff((off + fi.foff): i64);
emitline("(BP)\n");
fi = nil;
} else {
let fsz: i32 = fi.fsz;
let op: str = "MOVQ";
if (fsz == 1) { op = "MOVB"; };
if (fsz == 4) { op = "MOVL"; };
emitline("\t");
emitline(op);
emitline("\tAX, ");
emitoff((off + fi.foff): i64);
emitline("(BP)\n");
fi = nil;
};
} else {
fi = fi.finext;
};
};
};
fldn = fldn.next;
};
return;
};
if (n.rhs != nil
&& n.rhs.kind == nkind.N_CALL) {
let lcsz: i32 = lcnsz;
if (lcsz <= 24) {
let tlm: i32 = lcsz - (lcsz / 8) * 8;
if (tlm == 0 || tlm == 1
|| tlm == 2 || tlm == 4) {
cgexpr(c, n.rhs);
let full: i32 = lcsz / 8;
let i: i32 = 0;
for (i < full) {
let reg: str = "AX";
if (i == 1) { reg = "DX"; };
if (i == 2) { reg = "CX"; };
emitline("\tMOVQ\t");
emitline(reg);
emitline(", ");
emitoff((off + i * 8): i64);
emitline("(BP)\n");
i += 1;
};
if (tlm > 0) {
let top: str = "MOVB";
if (tlm == 4) { top = "MOVL"; };
if (tlm == 2) { top = "MOVW"; };
let treg: str = "AX";
if (full == 1) { treg = "DX"; };
if (full == 2) { treg = "CX"; };
emitline("\t");
emitline(top);
emitline("\t");
emitline(treg);
emitline(", ");
emitoff((off + full * 8): i64);
emitline("(BP)\n");
};
return;
};
};
};
};
};
};
};
// Float-typed local: rhs lands in X0; store via MOVSD/
// MOVSS, no AX shuffle. Compound (+= -= *= /=) loads
// slot into X1, combines into X1, stores X1 back —
@@ -14080,6 +14895,79 @@ fn cglet(c: *cgen, n: *node) void = {
return;
};
};
// 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 writes 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} (unreachable under WW struct
// alignment rules — field aligns force size%align==0) fall
// through to the generic scalar store rather than emit a
// stomping MOVQ tail. Sizes >24B also fall through (sret
// deferred, same constraint as #4). Mirrors the cstage
// cgen.c N_LET receive branch.
if (rhs.kind == nkind.N_CALL) {
let sname: str;
sname.ptr = nil; sname.len = 0;
if (tn != nil) {
if (tn.kind == nkind.N_TNAME) {
sname = tn.str;
};
};
if (sname.len > 0) {
let lsi: *structinfo = structlookup(c, sname);
if (lsi != nil) {
// si.totsize is slot-padded (rounded to 8) for
// stack-slot use; the receive ABI needs the
// TYPE's natural size — see structnaturalsize.
let lsz: i32 = structnaturalsize(lsi);
let tlm: i32 = lsz - (lsz / 8) * 8;
if (lsz <= 24) {
if (tlm == 0 || tlm == 1
|| tlm == 2 || tlm == 4) {
cgexpr(c, rhs);
let full: i32 = lsz / 8;
let i: i32 = 0;
for (i < full) {
let reg: str = "AX";
if (i == 1) { reg = "DX"; };
if (i == 2) { reg = "CX"; };
emitline("\tMOVQ\t");
emitline(reg);
emitline(", ");
emitoff((off + i * 8): i64);
emitline("(BP)\n");
i += 1;
};
if (tlm > 0) {
let top: str = "MOVB";
if (tlm == 4) { top = "MOVL"; };
if (tlm == 2) { top = "MOVW"; };
let treg: str = "AX";
if (full == 1) { treg = "DX"; };
if (full == 2) { treg = "CX"; };
emitline("\t");
emitline(top);
emitline("\t");
emitline(treg);
emitline(", ");
emitoff((off + full * 8): i64);
emitline("(BP)\n");
};
c.lastwasreturn = 0;
return;
};
};
};
};
};
cgexpr(c, rhs);
// Float local: cgexpr leaves the value in X0. Spill via
// MOVSS (f32, 4B) or MOVSD (f64, 8B).

View File

@@ -3745,11 +3745,148 @@ fn cgassign(c: *cgen, n: *node) void = {
n.rhs, "BX", fi.foff, fsz);
return;
};
// struct-typed field, struct-ident rhs through
// *struct base: cgexpr can't materialise a whole
// struct value, so word-copy from the rhs slot
// to *(struct_ptr + fi.foff). Other rhs shapes
// (call result, struct literal) tracked as #27.
// struct-typed field via *struct base — three
// rhs shapes (call/structlit added with #5;
// closes #27 marker here):
// N_IDENT: word-copy from rhs slot.
// N_CALL: cgexpr → AX/DX/CX per #4's cgreturn
// ABI; load *struct ptr into BX after the
// call, sized stores per natural struct size.
// N_STRUCTLIT: field-walk; reload BX before
// each store so cgexpr can clobber AX/BX.
// si.totsize is slot-padded; use
// structnaturalsize for the type-size query.
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_CALL
&& fi.tnode != nil
&& fi.tnode.kind == nkind.N_TNAME
&& primsize(fi.tnode.str) == 0) {
let ssi: *structinfo = structlookup(c, fi.tnode.str);
if (ssi != nil) {
let ssz: i32 = structnaturalsize(ssi);
if (ssz <= 24) {
let tlm: i32 = ssz - (ssz / 8) * 8;
if (tlm == 0 || tlm == 1
|| tlm == 2 || tlm == 4) {
cgexpr(c, n.rhs);
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
let full: i32 = ssz / 8;
let i: i32 = 0;
for (i < full) {
let reg: str = "AX";
if (i == 1) { reg = "DX"; };
if (i == 2) { reg = "CX"; };
emitline("\tMOVQ\t");
emitline(reg);
emitline(", ");
emitdispreg((fi.foff + i * 8): i64, "BX");
emitline("\n");
i += 1;
};
if (tlm > 0) {
let top: str = "MOVB";
if (tlm == 4) { top = "MOVL"; };
if (tlm == 2) { top = "MOVW"; };
let treg: str = "AX";
if (full == 1) { treg = "DX"; };
if (full == 2) { treg = "CX"; };
emitline("\t");
emitline(top);
emitline("\t");
emitline(treg);
emitline(", ");
emitdispreg((fi.foff + full * 8): i64, "BX");
emitline("\n");
};
return;
};
};
};
};
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_STRUCTLIT
&& fi.tnode != nil
&& fi.tnode.kind == nkind.N_TNAME
&& primsize(fi.tnode.str) == 0) {
let ssi: *structinfo = structlookup(c, fi.tnode.str);
if (ssi != nil) {
let ssz: i32 = structnaturalsize(ssi);
if (n.rhs.op == tkind.TK_ELLIPSIS) {
emitline("\tXORQ\tAX, AX\n");
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
let zi: i32 = 0;
for (zi + 8 <= ssz) {
emitline("\tMOVQ\tAX, ");
emitdispreg((fi.foff + zi): i64, "BX");
emitline("\n");
zi += 8;
};
for (zi + 4 <= ssz) {
emitline("\tMOVL\tAX, ");
emitdispreg((fi.foff + zi): i64, "BX");
emitline("\n");
zi += 4;
};
for (zi < ssz) {
emitline("\tMOVB\tAX, ");
emitdispreg((fi.foff + zi): i64, "BX");
emitline("\n");
zi += 1;
};
};
let fldn: *node = n.rhs.list;
for (fldn != nil) {
if (fldn.kind == nkind.N_FIELD) {
let fnm: str = fldn.str;
let ifi: *fieldinfo = ssi.fields;
for (ifi != nil) {
if (streq(ifi.fname, fnm)) {
cgexpr(c, fldn.lhs);
if (isfloattype(c, ifi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, ifi.tnode)) {
mov = "MOVSS";
};
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitdispreg((fi.foff + ifi.foff): i64, "BX");
emitline("\n");
ifi = nil;
} else {
let ifsz: i32 = ifi.fsz;
let op: str = "MOVQ";
if (ifsz == 1) { op = "MOVB"; };
if (ifsz == 4) { op = "MOVL"; };
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
emitline("\t");
emitline(op);
emitline("\tAX, ");
emitdispreg((fi.foff + ifi.foff): i64, "BX");
emitline("\n");
ifi = nil;
};
} else {
ifi = ifi.finext;
};
};
};
fldn = fldn.next;
};
return;
};
};
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_IDENT
@@ -3906,10 +4043,134 @@ fn cgassign(c: *cgen, n: *node) void = {
n.rhs, "BP", lc.off + fi.foff, fsz);
return;
};
// struct-typed field, struct-ident rhs:
// word-copy direct, skipping cgexpr (no
// register convention for a whole struct
// value). #27 covers non-ident rhs.
// struct-typed field on a direct struct
// local — three rhs shapes (call/structlit
// added with #5; closes #27 marker here):
// N_IDENT: word-copy from rhs slot.
// N_CALL: cgexpr → AX/DX/CX; sized stores
// directly at (lc.off+fi.foff)(BP).
// N_STRUCTLIT: field-walk; each inner
// field stored at +fi.foff+inner_foff(BP).
// BP-rel direct, no addr scratch needed.
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_CALL
&& fi.tnode != nil
&& fi.tnode.kind == nkind.N_TNAME
&& primsize(fi.tnode.str) == 0) {
let ssi: *structinfo = structlookup(c, fi.tnode.str);
if (ssi != nil) {
let ssz: i32 = structnaturalsize(ssi);
if (ssz <= 24) {
let tlm: i32 = ssz - (ssz / 8) * 8;
if (tlm == 0 || tlm == 1
|| tlm == 2 || tlm == 4) {
cgexpr(c, n.rhs);
let full: i32 = ssz / 8;
let i: i32 = 0;
for (i < full) {
let reg: str = "AX";
if (i == 1) { reg = "DX"; };
if (i == 2) { reg = "CX"; };
emitline("\tMOVQ\t");
emitline(reg);
emitline(", ");
emitoff((lc.off + fi.foff + i * 8): i64);
emitline("(BP)\n");
i += 1;
};
if (tlm > 0) {
let top: str = "MOVB";
if (tlm == 4) { top = "MOVL"; };
if (tlm == 2) { top = "MOVW"; };
let treg: str = "AX";
if (full == 1) { treg = "DX"; };
if (full == 2) { treg = "CX"; };
emitline("\t");
emitline(top);
emitline("\t");
emitline(treg);
emitline(", ");
emitoff((lc.off + fi.foff + full * 8): i64);
emitline("(BP)\n");
};
return;
};
};
};
};
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_STRUCTLIT
&& fi.tnode != nil
&& fi.tnode.kind == nkind.N_TNAME
&& primsize(fi.tnode.str) == 0) {
let ssi: *structinfo = structlookup(c, fi.tnode.str);
if (ssi != nil) {
let ssz: i32 = structnaturalsize(ssi);
if (n.rhs.op == tkind.TK_ELLIPSIS) {
emitline("\tXORQ\tAX, AX\n");
let zi: i32 = 0;
for (zi + 8 <= ssz) {
emitline("\tMOVQ\tAX, ");
emitoff((lc.off + fi.foff + zi): i64);
emitline("(BP)\n");
zi += 8;
};
for (zi + 4 <= ssz) {
emitline("\tMOVL\tAX, ");
emitoff((lc.off + fi.foff + zi): i64);
emitline("(BP)\n");
zi += 4;
};
for (zi < ssz) {
emitline("\tMOVB\tAX, ");
emitoff((lc.off + fi.foff + zi): i64);
emitline("(BP)\n");
zi += 1;
};
};
let fldn: *node = n.rhs.list;
for (fldn != nil) {
if (fldn.kind == nkind.N_FIELD) {
let fnm: str = fldn.str;
let ifi: *fieldinfo = ssi.fields;
for (ifi != nil) {
if (streq(ifi.fname, fnm)) {
cgexpr(c, fldn.lhs);
if (isfloattype(c, ifi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, ifi.tnode)) {
mov = "MOVSS";
};
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff((lc.off + fi.foff + ifi.foff): i64);
emitline("(BP)\n");
ifi = nil;
} else {
let ifsz: i32 = ifi.fsz;
let op: str = "MOVQ";
if (ifsz == 1) { op = "MOVB"; };
if (ifsz == 4) { op = "MOVL"; };
emitline("\t");
emitline(op);
emitline("\tAX, ");
emitoff((lc.off + fi.foff + ifi.foff): i64);
emitline("(BP)\n");
ifi = nil;
};
} else {
ifi = ifi.finext;
};
};
};
fldn = fldn.next;
};
return;
};
};
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_IDENT
@@ -4088,10 +4349,144 @@ fn cgassign(c: *cgen, n: *node) void = {
let fi: *fieldinfo = si.fields;
for (fi != nil) {
if (streq(fi.fname, fld)) {
// struct-typed field, struct-ident rhs on global
// struct base: word-copy direct. Skips cgexpr —
// no register convention for a whole struct value.
// #27 covers non-ident rhs.
// struct-typed field on a global struct base —
// three rhs shapes (call/structlit added with
// #5; closes #27 marker here):
// N_IDENT: word-copy from rhs slot.
// N_CALL: cgexpr → AX/DX/CX; LEAQ base into BX
// after call, sized stores per natural size.
// N_STRUCTLIT: field-walk; reload BX per store.
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_CALL
&& fi.tnode != nil
&& fi.tnode.kind == nkind.N_TNAME
&& primsize(fi.tnode.str) == 0) {
let ssi: *structinfo = structlookup(c, fi.tnode.str);
if (ssi != nil) {
let ssz: i32 = structnaturalsize(ssi);
if (ssz <= 24) {
let tlm: i32 = ssz - (ssz / 8) * 8;
if (tlm == 0 || tlm == 1
|| tlm == 2 || tlm == 4) {
cgexpr(c, n.rhs);
emitline("\tLEAQ\t");
emitsymname(c, bn);
emitline("(SB), BX\n");
let full: i32 = ssz / 8;
let i: i32 = 0;
for (i < full) {
let reg: str = "AX";
if (i == 1) { reg = "DX"; };
if (i == 2) { reg = "CX"; };
emitline("\tMOVQ\t");
emitline(reg);
emitline(", ");
emitdispreg((fi.foff + i * 8): i64, "BX");
emitline("\n");
i += 1;
};
if (tlm > 0) {
let top: str = "MOVB";
if (tlm == 4) { top = "MOVL"; };
if (tlm == 2) { top = "MOVW"; };
let treg: str = "AX";
if (full == 1) { treg = "DX"; };
if (full == 2) { treg = "CX"; };
emitline("\t");
emitline(top);
emitline("\t");
emitline(treg);
emitline(", ");
emitdispreg((fi.foff + full * 8): i64, "BX");
emitline("\n");
};
return;
};
};
};
};
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_STRUCTLIT
&& fi.tnode != nil
&& fi.tnode.kind == nkind.N_TNAME
&& primsize(fi.tnode.str) == 0) {
let ssi: *structinfo = structlookup(c, fi.tnode.str);
if (ssi != nil) {
let ssz: i32 = structnaturalsize(ssi);
if (n.rhs.op == tkind.TK_ELLIPSIS) {
emitline("\tXORQ\tAX, AX\n");
emitline("\tLEAQ\t");
emitsymname(c, bn);
emitline("(SB), BX\n");
let zi: i32 = 0;
for (zi + 8 <= ssz) {
emitline("\tMOVQ\tAX, ");
emitdispreg((fi.foff + zi): i64, "BX");
emitline("\n");
zi += 8;
};
for (zi + 4 <= ssz) {
emitline("\tMOVL\tAX, ");
emitdispreg((fi.foff + zi): i64, "BX");
emitline("\n");
zi += 4;
};
for (zi < ssz) {
emitline("\tMOVB\tAX, ");
emitdispreg((fi.foff + zi): i64, "BX");
emitline("\n");
zi += 1;
};
};
let fldn: *node = n.rhs.list;
for (fldn != nil) {
if (fldn.kind == nkind.N_FIELD) {
let fnm: str = fldn.str;
let ifi: *fieldinfo = ssi.fields;
for (ifi != nil) {
if (streq(ifi.fname, fnm)) {
cgexpr(c, fldn.lhs);
if (isfloattype(c, ifi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, ifi.tnode)) {
mov = "MOVSS";
};
emitline("\tLEAQ\t");
emitsymname(c, bn);
emitline("(SB), BX\n");
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitdispreg((fi.foff + ifi.foff): i64, "BX");
emitline("\n");
ifi = nil;
} else {
let ifsz: i32 = ifi.fsz;
let op: str = "MOVQ";
if (ifsz == 1) { op = "MOVB"; };
if (ifsz == 4) { op = "MOVL"; };
emitline("\tLEAQ\t");
emitsymname(c, bn);
emitline("(SB), BX\n");
emitline("\t");
emitline(op);
emitline("\tAX, ");
emitdispreg((fi.foff + ifi.foff): i64, "BX");
emitline("\n");
ifi = nil;
};
} else {
ifi = ifi.finext;
};
};
};
fldn = fldn.next;
};
return;
};
};
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_IDENT
@@ -4378,11 +4773,236 @@ fn cgassign(c: *cgen, n: *node) void = {
};
return;
};
// TY_STRUCT terminal: word-copy the rhs slot onto
// base+totaloff. Only N_IDENT rhs is wired — other
// shapes (call result, struct literal) route through
// #27. cgexpr is skipped (no whole-struct register
// convention); reads come straight from the rhs slot.
// TY_STRUCT terminal: three rhs shapes:
// - N_IDENT: word-copy from the rhs local slot
// (cgexpr is skipped — no whole-struct register
// convention for an arbitrary local).
// - 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.
// cgreturn touches only AX/DX/CX so for
// ptrroot/global we load 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; for ptrroot/global the dst addr is
// reloaded into BX before each store so cgexpr
// can clobber AX/BX between fields.
if (n.rhs != nil
&& n.rhs.kind == nkind.N_CALL
&& leaffi.tnode != nil
&& leaffi.tnode.kind == nkind.N_TNAME
&& primsize(leaffi.tnode.str) == 0) {
let lsi: *structinfo = structlookup(c, leaffi.tnode.str);
if (lsi != nil) {
// si.totsize is slot-padded (rounded to 8);
// receive ABI needs the TYPE's natural size.
let lsz: i32 = structnaturalsize(lsi);
if (lsz <= 24) {
let tlm: i32 = lsz - (lsz / 8) * 8;
if (tlm == 0 || tlm == 1
|| tlm == 2 || tlm == 4) {
cgexpr(c, n.rhs);
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), BX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), BX\n");
};
};
let full: i32 = lsz / 8;
let i: i32 = 0;
for (i < full) {
let reg: str = "AX";
if (i == 1) { reg = "DX"; };
if (i == 2) { reg = "CX"; };
if (viacx) {
emitline("\tMOVQ\t");
emitline(reg);
emitline(", ");
emitdispreg((totaloff + i * 8): i64, "BX");
emitline("\n");
} else {
emitline("\tMOVQ\t");
emitline(reg);
emitline(", ");
emitoff((rootoff + totaloff + i * 8): i64);
emitline("(BP)\n");
};
i += 1;
};
if (tlm > 0) {
let top: str = "MOVB";
if (tlm == 4) { top = "MOVL"; };
if (tlm == 2) { top = "MOVW"; };
let treg: str = "AX";
if (full == 1) { treg = "DX"; };
if (full == 2) { treg = "CX"; };
if (viacx) {
emitline("\t");
emitline(top);
emitline("\t");
emitline(treg);
emitline(", ");
emitdispreg((totaloff + full * 8): i64, "BX");
emitline("\n");
} else {
emitline("\t");
emitline(top);
emitline("\t");
emitline(treg);
emitline(", ");
emitoff((rootoff + totaloff + full * 8): i64);
emitline("(BP)\n");
};
};
return;
};
};
};
};
if (n.rhs != nil
&& n.rhs.kind == nkind.N_STRUCTLIT
&& leaffi.tnode != nil
&& leaffi.tnode.kind == nkind.N_TNAME
&& primsize(leaffi.tnode.str) == 0) {
let lsi: *structinfo = structlookup(c, leaffi.tnode.str);
if (lsi != nil) {
// si.totsize is slot-padded (rounded to 8);
// receive ABI needs the TYPE's natural size.
let lsz: i32 = structnaturalsize(lsi);
if (n.rhs.op == tkind.TK_ELLIPSIS) {
emitline("\tXORQ\tAX, AX\n");
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), BX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), BX\n");
};
let zi: i32 = 0;
for (zi + 8 <= lsz) {
emitline("\tMOVQ\tAX, ");
emitdispreg((totaloff + zi): i64, "BX");
emitline("\n");
zi += 8;
};
for (zi + 4 <= lsz) {
emitline("\tMOVL\tAX, ");
emitdispreg((totaloff + zi): i64, "BX");
emitline("\n");
zi += 4;
};
for (zi < lsz) {
emitline("\tMOVB\tAX, ");
emitdispreg((totaloff + zi): i64, "BX");
emitline("\n");
zi += 1;
};
} else {
let zi: i32 = 0;
for (zi + 8 <= lsz) {
emitline("\tMOVQ\tAX, ");
emitoff((rootoff + totaloff + zi): i64);
emitline("(BP)\n");
zi += 8;
};
for (zi + 4 <= lsz) {
emitline("\tMOVL\tAX, ");
emitoff((rootoff + totaloff + zi): i64);
emitline("(BP)\n");
zi += 4;
};
for (zi < lsz) {
emitline("\tMOVB\tAX, ");
emitoff((rootoff + totaloff + zi): i64);
emitline("(BP)\n");
zi += 1;
};
};
};
let fldn: *node = n.rhs.list;
for (fldn != nil) {
if (fldn.kind == nkind.N_FIELD) {
let fnm: str = fldn.str;
let fi: *fieldinfo = lsi.fields;
for (fi != nil) {
if (streq(fi.fname, fnm)) {
cgexpr(c, fldn.lhs);
if (isfloattype(c, fi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, fi.tnode)) {
mov = "MOVSS";
};
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), BX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), BX\n");
};
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitdispreg((totaloff + fi.foff): i64, "BX");
emitline("\n");
} else {
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff((rootoff + totaloff + fi.foff): i64);
emitline("(BP)\n");
};
fi = nil;
} else {
let fsz: i32 = fi.fsz;
let op: str = "MOVQ";
if (fsz == 1) { op = "MOVB"; };
if (fsz == 4) { op = "MOVL"; };
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), BX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), BX\n");
};
emitline("\t");
emitline(op);
emitline("\tAX, ");
emitdispreg((totaloff + fi.foff): i64, "BX");
emitline("\n");
} else {
emitline("\t");
emitline(op);
emitline("\tAX, ");
emitoff((rootoff + totaloff + fi.foff): i64);
emitline("(BP)\n");
};
fi = nil;
};
} else {
fi = fi.finext;
};
};
};
fldn = fldn.next;
};
return;
};
};
if (n.rhs != nil
&& n.rhs.kind == nkind.N_IDENT
&& leaffi.tnode != nil
@@ -4801,6 +5421,150 @@ fn cgassign(c: *cgen, n: *node) void = {
lcf = isfloattype(c, lcn.tnode);
lcf32 = isf32type(c, lcn.tnode);
};
// Struct-typed local reassignment: `s = expr;` where s
// is a TY_STRUCT local of size <=24B. Two rhs shapes
// (mirrors cglet's N_STRUCTLIT and the call-result
// receive branch):
// - N_STRUCTLIT: walk fields, store at off+foff
// directly. ASYMMETRY-safe (no register copy from
// the caller; values come from cgexpr).
// - N_CALL: cgexpr → AX/DX/CX, sized stores per the
// declared struct size — MOVQ for full 8B chunks
// plus MOVL/MOVW/MOVB tail. See cglet receive
// site for the ASYMMETRY rationale.
// Struct-IDENT word-copy rhs (s = p) is left unwired;
// #5 is scoped to receive-side of #4 (calls + literals).
// fsz dispatch uses the explicit {1→MOVB, 4→MOVL, else
// MOVQ} pattern (not fieldstoreop) to match cstage
// cgen.c N_ASSIGN byte-identically — wwstage's
// fieldstoreop returns MOVW for fsz==2 which cstage
// doesn't emit (tracked separately as the cstage/
// wwstage MOVW divergence task).
if (lcn != nil) {
let lctn: *node = lcn.tnode;
let lcsname: str;
lcsname.ptr = nil; lcsname.len = 0;
if (lctn != nil) {
if (lctn.kind == nkind.N_TNAME) {
lcsname = lctn.str;
};
};
if (lcsname.len > 0) {
let lcsi: *structinfo = structlookup(c, lcsname);
if (lcsi != nil) {
// si.totsize is slot-padded (rounded to 8);
// receive ABI needs the TYPE's natural size.
let lcnsz: i32 = structnaturalsize(lcsi);
if (n.op == tkind.TK_ASSIGN) {
if (n.rhs != nil
&& n.rhs.kind == nkind.N_STRUCTLIT) {
let lcsz: i32 = lcnsz;
if (n.rhs.op == tkind.TK_ELLIPSIS) {
emitline("\tXORQ\tAX, AX\n");
let zi: i32 = 0;
for (zi + 8 <= lcsz) {
emitline("\tMOVQ\tAX, ");
emitoff((off + zi): i64);
emitline("(BP)\n");
zi += 8;
};
for (zi + 4 <= lcsz) {
emitline("\tMOVL\tAX, ");
emitoff((off + zi): i64);
emitline("(BP)\n");
zi += 4;
};
for (zi < lcsz) {
emitline("\tMOVB\tAX, ");
emitoff((off + zi): i64);
emitline("(BP)\n");
zi += 1;
};
};
let fldn: *node = n.rhs.list;
for (fldn != nil) {
if (fldn.kind == nkind.N_FIELD) {
let fnm: str = fldn.str;
let fi: *fieldinfo = lcsi.fields;
for (fi != nil) {
if (streq(fi.fname, fnm)) {
cgexpr(c, fldn.lhs);
if (isfloattype(c, fi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, fi.tnode)) {
mov = "MOVSS";
};
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff((off + fi.foff): i64);
emitline("(BP)\n");
fi = nil;
} else {
let fsz: i32 = fi.fsz;
let op: str = "MOVQ";
if (fsz == 1) { op = "MOVB"; };
if (fsz == 4) { op = "MOVL"; };
emitline("\t");
emitline(op);
emitline("\tAX, ");
emitoff((off + fi.foff): i64);
emitline("(BP)\n");
fi = nil;
};
} else {
fi = fi.finext;
};
};
};
fldn = fldn.next;
};
return;
};
if (n.rhs != nil
&& n.rhs.kind == nkind.N_CALL) {
let lcsz: i32 = lcnsz;
if (lcsz <= 24) {
let tlm: i32 = lcsz - (lcsz / 8) * 8;
if (tlm == 0 || tlm == 1
|| tlm == 2 || tlm == 4) {
cgexpr(c, n.rhs);
let full: i32 = lcsz / 8;
let i: i32 = 0;
for (i < full) {
let reg: str = "AX";
if (i == 1) { reg = "DX"; };
if (i == 2) { reg = "CX"; };
emitline("\tMOVQ\t");
emitline(reg);
emitline(", ");
emitoff((off + i * 8): i64);
emitline("(BP)\n");
i += 1;
};
if (tlm > 0) {
let top: str = "MOVB";
if (tlm == 4) { top = "MOVL"; };
if (tlm == 2) { top = "MOVW"; };
let treg: str = "AX";
if (full == 1) { treg = "DX"; };
if (full == 2) { treg = "CX"; };
emitline("\t");
emitline(top);
emitline("\t");
emitline(treg);
emitline(", ");
emitoff((off + full * 8): i64);
emitline("(BP)\n");
};
return;
};
};
};
};
};
};
};
// Float-typed local: rhs lands in X0; store via MOVSD/
// MOVSS, no AX shuffle. Compound (+= -= *= /=) loads
// slot into X1, combines into X1, stores X1 back —

View File

@@ -648,6 +648,79 @@ fn cglet(c: *cgen, n: *node) void = {
return;
};
};
// 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 writes 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} (unreachable under WW struct
// alignment rules — field aligns force size%align==0) fall
// through to the generic scalar store rather than emit a
// stomping MOVQ tail. Sizes >24B also fall through (sret
// deferred, same constraint as #4). Mirrors the cstage
// cgen.c N_LET receive branch.
if (rhs.kind == nkind.N_CALL) {
let sname: str;
sname.ptr = nil; sname.len = 0;
if (tn != nil) {
if (tn.kind == nkind.N_TNAME) {
sname = tn.str;
};
};
if (sname.len > 0) {
let lsi: *structinfo = structlookup(c, sname);
if (lsi != nil) {
// si.totsize is slot-padded (rounded to 8) for
// stack-slot use; the receive ABI needs the
// TYPE's natural size — see structnaturalsize.
let lsz: i32 = structnaturalsize(lsi);
let tlm: i32 = lsz - (lsz / 8) * 8;
if (lsz <= 24) {
if (tlm == 0 || tlm == 1
|| tlm == 2 || tlm == 4) {
cgexpr(c, rhs);
let full: i32 = lsz / 8;
let i: i32 = 0;
for (i < full) {
let reg: str = "AX";
if (i == 1) { reg = "DX"; };
if (i == 2) { reg = "CX"; };
emitline("\tMOVQ\t");
emitline(reg);
emitline(", ");
emitoff((off + i * 8): i64);
emitline("(BP)\n");
i += 1;
};
if (tlm > 0) {
let top: str = "MOVB";
if (tlm == 4) { top = "MOVL"; };
if (tlm == 2) { top = "MOVW"; };
let treg: str = "AX";
if (full == 1) { treg = "DX"; };
if (full == 2) { treg = "CX"; };
emitline("\t");
emitline(top);
emitline("\t");
emitline(treg);
emitline(", ");
emitoff((off + full * 8): i64);
emitline("(BP)\n");
};
c.lastwasreturn = 0;
return;
};
};
};
};
};
cgexpr(c, rhs);
// Float local: cgexpr leaves the value in X0. Spill via
// MOVSS (f32, 4B) or MOVSD (f64, 8B).

View File

@@ -1148,6 +1148,31 @@ fn nodeprimwidth(c: *cgen, n: *node) i32 = {
// ---- type-driven slot sizing ----------------------------------------
// structnaturalsize — type-natural size of `si`, i.e. max(foff +
// fsz) across declared fields. Mirrors cstage's `lu->size` for a
// TY_STRUCT (rounded only to the struct's maxalign).
//
// NOTE: si.totsize is mis-named — it's actually the *slot-padded*
// size (rounded up to 8 for stack-slot use; see registerstruct's
// tail `if ((off & 7) != 0) ...`). Frame allocation, [N]foo stride,
// and similar consumers want that slot-padded number. The
// receive-side ABI (#5) and any future "TYPE size, not slot size"
// query wants the natural size. Until si.totsize is split into
// si.naturalsize + si.slotsize (tracked as the wwstage-sizing
// follow-up task), recover the type-natural size from the field
// chain here.
fn structnaturalsize(si: *structinfo) i32 = {
if (si == nil) { return 0; };
let n: i32 = 0;
let fi: *fieldinfo = si.fields;
for (fi != nil) {
let end: i32 = fi.foff + fi.fsz;
if (end > n) { n = end; };
fi = fi.finext;
};
return n;
};
fn structlookup(c: *cgen, name: str) *structinfo = {
// Exact match first: bare-from-source struct names and already-
// leafed lookups hit here directly.

View File

@@ -476,6 +476,32 @@ export fn dup(s: str) str = {
return r;
};
// freeall — release every str element in `s` (those that were
// individually allocated) plus the slice's backing storage. Mirrors
// Hare's strings::freeall — the natural disposer for any function
// returning a fresh `[]str` of dup'd elements (e.g. shlex.split).
//
// Each element is freed via os.free at its own length; the slice
// header storage is freed at `cap * 16` bytes (one str = 16B). Empty
// elements (`{nil, 0}` from a zero-length dup) are skipped — calling
// os.free on a nil pointer at len 0 would tickle the rt_free guard
// that the runtime treats as a logic bug.
//
// `cap == 0` means the slice was never grown (empty `[]str` with no
// backing allocation); skip the header free in that case too.
export fn freeall(s: []str) void = {
let i: i32 = 0;
for (i < s.len) {
if (s[i].len > 0) {
os.free(s[i].ptr: *void, s[i].len: u64);
};
i += 1;
};
if (s.cap > 0) {
os.free(s.ptr: *void, (s.cap: u64) * 16u64);
};
};
// rbyteindex — last byte position of `needle` in `s`. Mirrors Hare's
// strings::rbyteindex. Rune needle scans for the byte that encodes it
// (ASCII only); str needle scans for the substring. Empty str needle
@@ -6891,6 +6917,31 @@ fn nodeprimwidth(c: *cgen, n: *node) i32 = {
// ---- type-driven slot sizing ----------------------------------------
// structnaturalsize — type-natural size of `si`, i.e. max(foff +
// fsz) across declared fields. Mirrors cstage's `lu->size` for a
// TY_STRUCT (rounded only to the struct's maxalign).
//
// NOTE: si.totsize is mis-named — it's actually the *slot-padded*
// size (rounded up to 8 for stack-slot use; see registerstruct's
// tail `if ((off & 7) != 0) ...`). Frame allocation, [N]foo stride,
// and similar consumers want that slot-padded number. The
// receive-side ABI (#5) and any future "TYPE size, not slot size"
// query wants the natural size. Until si.totsize is split into
// si.naturalsize + si.slotsize (tracked as the wwstage-sizing
// follow-up task), recover the type-natural size from the field
// chain here.
fn structnaturalsize(si: *structinfo) i32 = {
if (si == nil) { return 0; };
let n: i32 = 0;
let fi: *fieldinfo = si.fields;
for (fi != nil) {
let end: i32 = fi.foff + fi.fsz;
if (end > n) { n = end; };
fi = fi.finext;
};
return n;
};
fn structlookup(c: *cgen, name: str) *structinfo = {
// Exact match first: bare-from-source struct names and already-
// leafed lookups hit here directly.
@@ -12250,11 +12301,148 @@ fn cgassign(c: *cgen, n: *node) void = {
n.rhs, "BX", fi.foff, fsz);
return;
};
// struct-typed field, struct-ident rhs through
// *struct base: cgexpr can't materialise a whole
// struct value, so word-copy from the rhs slot
// to *(struct_ptr + fi.foff). Other rhs shapes
// (call result, struct literal) tracked as #27.
// struct-typed field via *struct base — three
// rhs shapes (call/structlit added with #5;
// closes #27 marker here):
// N_IDENT: word-copy from rhs slot.
// N_CALL: cgexpr → AX/DX/CX per #4's cgreturn
// ABI; load *struct ptr into BX after the
// call, sized stores per natural struct size.
// N_STRUCTLIT: field-walk; reload BX before
// each store so cgexpr can clobber AX/BX.
// si.totsize is slot-padded; use
// structnaturalsize for the type-size query.
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_CALL
&& fi.tnode != nil
&& fi.tnode.kind == nkind.N_TNAME
&& primsize(fi.tnode.str) == 0) {
let ssi: *structinfo = structlookup(c, fi.tnode.str);
if (ssi != nil) {
let ssz: i32 = structnaturalsize(ssi);
if (ssz <= 24) {
let tlm: i32 = ssz - (ssz / 8) * 8;
if (tlm == 0 || tlm == 1
|| tlm == 2 || tlm == 4) {
cgexpr(c, n.rhs);
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
let full: i32 = ssz / 8;
let i: i32 = 0;
for (i < full) {
let reg: str = "AX";
if (i == 1) { reg = "DX"; };
if (i == 2) { reg = "CX"; };
emitline("\tMOVQ\t");
emitline(reg);
emitline(", ");
emitdispreg((fi.foff + i * 8): i64, "BX");
emitline("\n");
i += 1;
};
if (tlm > 0) {
let top: str = "MOVB";
if (tlm == 4) { top = "MOVL"; };
if (tlm == 2) { top = "MOVW"; };
let treg: str = "AX";
if (full == 1) { treg = "DX"; };
if (full == 2) { treg = "CX"; };
emitline("\t");
emitline(top);
emitline("\t");
emitline(treg);
emitline(", ");
emitdispreg((fi.foff + full * 8): i64, "BX");
emitline("\n");
};
return;
};
};
};
};
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_STRUCTLIT
&& fi.tnode != nil
&& fi.tnode.kind == nkind.N_TNAME
&& primsize(fi.tnode.str) == 0) {
let ssi: *structinfo = structlookup(c, fi.tnode.str);
if (ssi != nil) {
let ssz: i32 = structnaturalsize(ssi);
if (n.rhs.op == tkind.TK_ELLIPSIS) {
emitline("\tXORQ\tAX, AX\n");
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
let zi: i32 = 0;
for (zi + 8 <= ssz) {
emitline("\tMOVQ\tAX, ");
emitdispreg((fi.foff + zi): i64, "BX");
emitline("\n");
zi += 8;
};
for (zi + 4 <= ssz) {
emitline("\tMOVL\tAX, ");
emitdispreg((fi.foff + zi): i64, "BX");
emitline("\n");
zi += 4;
};
for (zi < ssz) {
emitline("\tMOVB\tAX, ");
emitdispreg((fi.foff + zi): i64, "BX");
emitline("\n");
zi += 1;
};
};
let fldn: *node = n.rhs.list;
for (fldn != nil) {
if (fldn.kind == nkind.N_FIELD) {
let fnm: str = fldn.str;
let ifi: *fieldinfo = ssi.fields;
for (ifi != nil) {
if (streq(ifi.fname, fnm)) {
cgexpr(c, fldn.lhs);
if (isfloattype(c, ifi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, ifi.tnode)) {
mov = "MOVSS";
};
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitdispreg((fi.foff + ifi.foff): i64, "BX");
emitline("\n");
ifi = nil;
} else {
let ifsz: i32 = ifi.fsz;
let op: str = "MOVQ";
if (ifsz == 1) { op = "MOVB"; };
if (ifsz == 4) { op = "MOVL"; };
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
emitline("\t");
emitline(op);
emitline("\tAX, ");
emitdispreg((fi.foff + ifi.foff): i64, "BX");
emitline("\n");
ifi = nil;
};
} else {
ifi = ifi.finext;
};
};
};
fldn = fldn.next;
};
return;
};
};
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_IDENT
@@ -12411,10 +12599,134 @@ fn cgassign(c: *cgen, n: *node) void = {
n.rhs, "BP", lc.off + fi.foff, fsz);
return;
};
// struct-typed field, struct-ident rhs:
// word-copy direct, skipping cgexpr (no
// register convention for a whole struct
// value). #27 covers non-ident rhs.
// struct-typed field on a direct struct
// local — three rhs shapes (call/structlit
// added with #5; closes #27 marker here):
// N_IDENT: word-copy from rhs slot.
// N_CALL: cgexpr → AX/DX/CX; sized stores
// directly at (lc.off+fi.foff)(BP).
// N_STRUCTLIT: field-walk; each inner
// field stored at +fi.foff+inner_foff(BP).
// BP-rel direct, no addr scratch needed.
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_CALL
&& fi.tnode != nil
&& fi.tnode.kind == nkind.N_TNAME
&& primsize(fi.tnode.str) == 0) {
let ssi: *structinfo = structlookup(c, fi.tnode.str);
if (ssi != nil) {
let ssz: i32 = structnaturalsize(ssi);
if (ssz <= 24) {
let tlm: i32 = ssz - (ssz / 8) * 8;
if (tlm == 0 || tlm == 1
|| tlm == 2 || tlm == 4) {
cgexpr(c, n.rhs);
let full: i32 = ssz / 8;
let i: i32 = 0;
for (i < full) {
let reg: str = "AX";
if (i == 1) { reg = "DX"; };
if (i == 2) { reg = "CX"; };
emitline("\tMOVQ\t");
emitline(reg);
emitline(", ");
emitoff((lc.off + fi.foff + i * 8): i64);
emitline("(BP)\n");
i += 1;
};
if (tlm > 0) {
let top: str = "MOVB";
if (tlm == 4) { top = "MOVL"; };
if (tlm == 2) { top = "MOVW"; };
let treg: str = "AX";
if (full == 1) { treg = "DX"; };
if (full == 2) { treg = "CX"; };
emitline("\t");
emitline(top);
emitline("\t");
emitline(treg);
emitline(", ");
emitoff((lc.off + fi.foff + full * 8): i64);
emitline("(BP)\n");
};
return;
};
};
};
};
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_STRUCTLIT
&& fi.tnode != nil
&& fi.tnode.kind == nkind.N_TNAME
&& primsize(fi.tnode.str) == 0) {
let ssi: *structinfo = structlookup(c, fi.tnode.str);
if (ssi != nil) {
let ssz: i32 = structnaturalsize(ssi);
if (n.rhs.op == tkind.TK_ELLIPSIS) {
emitline("\tXORQ\tAX, AX\n");
let zi: i32 = 0;
for (zi + 8 <= ssz) {
emitline("\tMOVQ\tAX, ");
emitoff((lc.off + fi.foff + zi): i64);
emitline("(BP)\n");
zi += 8;
};
for (zi + 4 <= ssz) {
emitline("\tMOVL\tAX, ");
emitoff((lc.off + fi.foff + zi): i64);
emitline("(BP)\n");
zi += 4;
};
for (zi < ssz) {
emitline("\tMOVB\tAX, ");
emitoff((lc.off + fi.foff + zi): i64);
emitline("(BP)\n");
zi += 1;
};
};
let fldn: *node = n.rhs.list;
for (fldn != nil) {
if (fldn.kind == nkind.N_FIELD) {
let fnm: str = fldn.str;
let ifi: *fieldinfo = ssi.fields;
for (ifi != nil) {
if (streq(ifi.fname, fnm)) {
cgexpr(c, fldn.lhs);
if (isfloattype(c, ifi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, ifi.tnode)) {
mov = "MOVSS";
};
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff((lc.off + fi.foff + ifi.foff): i64);
emitline("(BP)\n");
ifi = nil;
} else {
let ifsz: i32 = ifi.fsz;
let op: str = "MOVQ";
if (ifsz == 1) { op = "MOVB"; };
if (ifsz == 4) { op = "MOVL"; };
emitline("\t");
emitline(op);
emitline("\tAX, ");
emitoff((lc.off + fi.foff + ifi.foff): i64);
emitline("(BP)\n");
ifi = nil;
};
} else {
ifi = ifi.finext;
};
};
};
fldn = fldn.next;
};
return;
};
};
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_IDENT
@@ -12593,10 +12905,144 @@ fn cgassign(c: *cgen, n: *node) void = {
let fi: *fieldinfo = si.fields;
for (fi != nil) {
if (streq(fi.fname, fld)) {
// struct-typed field, struct-ident rhs on global
// struct base: word-copy direct. Skips cgexpr —
// no register convention for a whole struct value.
// #27 covers non-ident rhs.
// struct-typed field on a global struct base —
// three rhs shapes (call/structlit added with
// #5; closes #27 marker here):
// N_IDENT: word-copy from rhs slot.
// N_CALL: cgexpr → AX/DX/CX; LEAQ base into BX
// after call, sized stores per natural size.
// N_STRUCTLIT: field-walk; reload BX per store.
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_CALL
&& fi.tnode != nil
&& fi.tnode.kind == nkind.N_TNAME
&& primsize(fi.tnode.str) == 0) {
let ssi: *structinfo = structlookup(c, fi.tnode.str);
if (ssi != nil) {
let ssz: i32 = structnaturalsize(ssi);
if (ssz <= 24) {
let tlm: i32 = ssz - (ssz / 8) * 8;
if (tlm == 0 || tlm == 1
|| tlm == 2 || tlm == 4) {
cgexpr(c, n.rhs);
emitline("\tLEAQ\t");
emitsymname(c, bn);
emitline("(SB), BX\n");
let full: i32 = ssz / 8;
let i: i32 = 0;
for (i < full) {
let reg: str = "AX";
if (i == 1) { reg = "DX"; };
if (i == 2) { reg = "CX"; };
emitline("\tMOVQ\t");
emitline(reg);
emitline(", ");
emitdispreg((fi.foff + i * 8): i64, "BX");
emitline("\n");
i += 1;
};
if (tlm > 0) {
let top: str = "MOVB";
if (tlm == 4) { top = "MOVL"; };
if (tlm == 2) { top = "MOVW"; };
let treg: str = "AX";
if (full == 1) { treg = "DX"; };
if (full == 2) { treg = "CX"; };
emitline("\t");
emitline(top);
emitline("\t");
emitline(treg);
emitline(", ");
emitdispreg((fi.foff + full * 8): i64, "BX");
emitline("\n");
};
return;
};
};
};
};
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_STRUCTLIT
&& fi.tnode != nil
&& fi.tnode.kind == nkind.N_TNAME
&& primsize(fi.tnode.str) == 0) {
let ssi: *structinfo = structlookup(c, fi.tnode.str);
if (ssi != nil) {
let ssz: i32 = structnaturalsize(ssi);
if (n.rhs.op == tkind.TK_ELLIPSIS) {
emitline("\tXORQ\tAX, AX\n");
emitline("\tLEAQ\t");
emitsymname(c, bn);
emitline("(SB), BX\n");
let zi: i32 = 0;
for (zi + 8 <= ssz) {
emitline("\tMOVQ\tAX, ");
emitdispreg((fi.foff + zi): i64, "BX");
emitline("\n");
zi += 8;
};
for (zi + 4 <= ssz) {
emitline("\tMOVL\tAX, ");
emitdispreg((fi.foff + zi): i64, "BX");
emitline("\n");
zi += 4;
};
for (zi < ssz) {
emitline("\tMOVB\tAX, ");
emitdispreg((fi.foff + zi): i64, "BX");
emitline("\n");
zi += 1;
};
};
let fldn: *node = n.rhs.list;
for (fldn != nil) {
if (fldn.kind == nkind.N_FIELD) {
let fnm: str = fldn.str;
let ifi: *fieldinfo = ssi.fields;
for (ifi != nil) {
if (streq(ifi.fname, fnm)) {
cgexpr(c, fldn.lhs);
if (isfloattype(c, ifi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, ifi.tnode)) {
mov = "MOVSS";
};
emitline("\tLEAQ\t");
emitsymname(c, bn);
emitline("(SB), BX\n");
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitdispreg((fi.foff + ifi.foff): i64, "BX");
emitline("\n");
ifi = nil;
} else {
let ifsz: i32 = ifi.fsz;
let op: str = "MOVQ";
if (ifsz == 1) { op = "MOVB"; };
if (ifsz == 4) { op = "MOVL"; };
emitline("\tLEAQ\t");
emitsymname(c, bn);
emitline("(SB), BX\n");
emitline("\t");
emitline(op);
emitline("\tAX, ");
emitdispreg((fi.foff + ifi.foff): i64, "BX");
emitline("\n");
ifi = nil;
};
} else {
ifi = ifi.finext;
};
};
};
fldn = fldn.next;
};
return;
};
};
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_IDENT
@@ -12883,11 +13329,236 @@ fn cgassign(c: *cgen, n: *node) void = {
};
return;
};
// TY_STRUCT terminal: word-copy the rhs slot onto
// base+totaloff. Only N_IDENT rhs is wired — other
// shapes (call result, struct literal) route through
// #27. cgexpr is skipped (no whole-struct register
// convention); reads come straight from the rhs slot.
// TY_STRUCT terminal: three rhs shapes:
// - N_IDENT: word-copy from the rhs local slot
// (cgexpr is skipped — no whole-struct register
// convention for an arbitrary local).
// - 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.
// cgreturn touches only AX/DX/CX so for
// ptrroot/global we load 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; for ptrroot/global the dst addr is
// reloaded into BX before each store so cgexpr
// can clobber AX/BX between fields.
if (n.rhs != nil
&& n.rhs.kind == nkind.N_CALL
&& leaffi.tnode != nil
&& leaffi.tnode.kind == nkind.N_TNAME
&& primsize(leaffi.tnode.str) == 0) {
let lsi: *structinfo = structlookup(c, leaffi.tnode.str);
if (lsi != nil) {
// si.totsize is slot-padded (rounded to 8);
// receive ABI needs the TYPE's natural size.
let lsz: i32 = structnaturalsize(lsi);
if (lsz <= 24) {
let tlm: i32 = lsz - (lsz / 8) * 8;
if (tlm == 0 || tlm == 1
|| tlm == 2 || tlm == 4) {
cgexpr(c, n.rhs);
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), BX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), BX\n");
};
};
let full: i32 = lsz / 8;
let i: i32 = 0;
for (i < full) {
let reg: str = "AX";
if (i == 1) { reg = "DX"; };
if (i == 2) { reg = "CX"; };
if (viacx) {
emitline("\tMOVQ\t");
emitline(reg);
emitline(", ");
emitdispreg((totaloff + i * 8): i64, "BX");
emitline("\n");
} else {
emitline("\tMOVQ\t");
emitline(reg);
emitline(", ");
emitoff((rootoff + totaloff + i * 8): i64);
emitline("(BP)\n");
};
i += 1;
};
if (tlm > 0) {
let top: str = "MOVB";
if (tlm == 4) { top = "MOVL"; };
if (tlm == 2) { top = "MOVW"; };
let treg: str = "AX";
if (full == 1) { treg = "DX"; };
if (full == 2) { treg = "CX"; };
if (viacx) {
emitline("\t");
emitline(top);
emitline("\t");
emitline(treg);
emitline(", ");
emitdispreg((totaloff + full * 8): i64, "BX");
emitline("\n");
} else {
emitline("\t");
emitline(top);
emitline("\t");
emitline(treg);
emitline(", ");
emitoff((rootoff + totaloff + full * 8): i64);
emitline("(BP)\n");
};
};
return;
};
};
};
};
if (n.rhs != nil
&& n.rhs.kind == nkind.N_STRUCTLIT
&& leaffi.tnode != nil
&& leaffi.tnode.kind == nkind.N_TNAME
&& primsize(leaffi.tnode.str) == 0) {
let lsi: *structinfo = structlookup(c, leaffi.tnode.str);
if (lsi != nil) {
// si.totsize is slot-padded (rounded to 8);
// receive ABI needs the TYPE's natural size.
let lsz: i32 = structnaturalsize(lsi);
if (n.rhs.op == tkind.TK_ELLIPSIS) {
emitline("\tXORQ\tAX, AX\n");
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), BX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), BX\n");
};
let zi: i32 = 0;
for (zi + 8 <= lsz) {
emitline("\tMOVQ\tAX, ");
emitdispreg((totaloff + zi): i64, "BX");
emitline("\n");
zi += 8;
};
for (zi + 4 <= lsz) {
emitline("\tMOVL\tAX, ");
emitdispreg((totaloff + zi): i64, "BX");
emitline("\n");
zi += 4;
};
for (zi < lsz) {
emitline("\tMOVB\tAX, ");
emitdispreg((totaloff + zi): i64, "BX");
emitline("\n");
zi += 1;
};
} else {
let zi: i32 = 0;
for (zi + 8 <= lsz) {
emitline("\tMOVQ\tAX, ");
emitoff((rootoff + totaloff + zi): i64);
emitline("(BP)\n");
zi += 8;
};
for (zi + 4 <= lsz) {
emitline("\tMOVL\tAX, ");
emitoff((rootoff + totaloff + zi): i64);
emitline("(BP)\n");
zi += 4;
};
for (zi < lsz) {
emitline("\tMOVB\tAX, ");
emitoff((rootoff + totaloff + zi): i64);
emitline("(BP)\n");
zi += 1;
};
};
};
let fldn: *node = n.rhs.list;
for (fldn != nil) {
if (fldn.kind == nkind.N_FIELD) {
let fnm: str = fldn.str;
let fi: *fieldinfo = lsi.fields;
for (fi != nil) {
if (streq(fi.fname, fnm)) {
cgexpr(c, fldn.lhs);
if (isfloattype(c, fi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, fi.tnode)) {
mov = "MOVSS";
};
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), BX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), BX\n");
};
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitdispreg((totaloff + fi.foff): i64, "BX");
emitline("\n");
} else {
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff((rootoff + totaloff + fi.foff): i64);
emitline("(BP)\n");
};
fi = nil;
} else {
let fsz: i32 = fi.fsz;
let op: str = "MOVQ";
if (fsz == 1) { op = "MOVB"; };
if (fsz == 4) { op = "MOVL"; };
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), BX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), BX\n");
};
emitline("\t");
emitline(op);
emitline("\tAX, ");
emitdispreg((totaloff + fi.foff): i64, "BX");
emitline("\n");
} else {
emitline("\t");
emitline(op);
emitline("\tAX, ");
emitoff((rootoff + totaloff + fi.foff): i64);
emitline("(BP)\n");
};
fi = nil;
};
} else {
fi = fi.finext;
};
};
};
fldn = fldn.next;
};
return;
};
};
if (n.rhs != nil
&& n.rhs.kind == nkind.N_IDENT
&& leaffi.tnode != nil
@@ -13306,6 +13977,150 @@ fn cgassign(c: *cgen, n: *node) void = {
lcf = isfloattype(c, lcn.tnode);
lcf32 = isf32type(c, lcn.tnode);
};
// Struct-typed local reassignment: `s = expr;` where s
// is a TY_STRUCT local of size <=24B. Two rhs shapes
// (mirrors cglet's N_STRUCTLIT and the call-result
// receive branch):
// - N_STRUCTLIT: walk fields, store at off+foff
// directly. ASYMMETRY-safe (no register copy from
// the caller; values come from cgexpr).
// - N_CALL: cgexpr → AX/DX/CX, sized stores per the
// declared struct size — MOVQ for full 8B chunks
// plus MOVL/MOVW/MOVB tail. See cglet receive
// site for the ASYMMETRY rationale.
// Struct-IDENT word-copy rhs (s = p) is left unwired;
// #5 is scoped to receive-side of #4 (calls + literals).
// fsz dispatch uses the explicit {1→MOVB, 4→MOVL, else
// MOVQ} pattern (not fieldstoreop) to match cstage
// cgen.c N_ASSIGN byte-identically — wwstage's
// fieldstoreop returns MOVW for fsz==2 which cstage
// doesn't emit (tracked separately as the cstage/
// wwstage MOVW divergence task).
if (lcn != nil) {
let lctn: *node = lcn.tnode;
let lcsname: str;
lcsname.ptr = nil; lcsname.len = 0;
if (lctn != nil) {
if (lctn.kind == nkind.N_TNAME) {
lcsname = lctn.str;
};
};
if (lcsname.len > 0) {
let lcsi: *structinfo = structlookup(c, lcsname);
if (lcsi != nil) {
// si.totsize is slot-padded (rounded to 8);
// receive ABI needs the TYPE's natural size.
let lcnsz: i32 = structnaturalsize(lcsi);
if (n.op == tkind.TK_ASSIGN) {
if (n.rhs != nil
&& n.rhs.kind == nkind.N_STRUCTLIT) {
let lcsz: i32 = lcnsz;
if (n.rhs.op == tkind.TK_ELLIPSIS) {
emitline("\tXORQ\tAX, AX\n");
let zi: i32 = 0;
for (zi + 8 <= lcsz) {
emitline("\tMOVQ\tAX, ");
emitoff((off + zi): i64);
emitline("(BP)\n");
zi += 8;
};
for (zi + 4 <= lcsz) {
emitline("\tMOVL\tAX, ");
emitoff((off + zi): i64);
emitline("(BP)\n");
zi += 4;
};
for (zi < lcsz) {
emitline("\tMOVB\tAX, ");
emitoff((off + zi): i64);
emitline("(BP)\n");
zi += 1;
};
};
let fldn: *node = n.rhs.list;
for (fldn != nil) {
if (fldn.kind == nkind.N_FIELD) {
let fnm: str = fldn.str;
let fi: *fieldinfo = lcsi.fields;
for (fi != nil) {
if (streq(fi.fname, fnm)) {
cgexpr(c, fldn.lhs);
if (isfloattype(c, fi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, fi.tnode)) {
mov = "MOVSS";
};
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff((off + fi.foff): i64);
emitline("(BP)\n");
fi = nil;
} else {
let fsz: i32 = fi.fsz;
let op: str = "MOVQ";
if (fsz == 1) { op = "MOVB"; };
if (fsz == 4) { op = "MOVL"; };
emitline("\t");
emitline(op);
emitline("\tAX, ");
emitoff((off + fi.foff): i64);
emitline("(BP)\n");
fi = nil;
};
} else {
fi = fi.finext;
};
};
};
fldn = fldn.next;
};
return;
};
if (n.rhs != nil
&& n.rhs.kind == nkind.N_CALL) {
let lcsz: i32 = lcnsz;
if (lcsz <= 24) {
let tlm: i32 = lcsz - (lcsz / 8) * 8;
if (tlm == 0 || tlm == 1
|| tlm == 2 || tlm == 4) {
cgexpr(c, n.rhs);
let full: i32 = lcsz / 8;
let i: i32 = 0;
for (i < full) {
let reg: str = "AX";
if (i == 1) { reg = "DX"; };
if (i == 2) { reg = "CX"; };
emitline("\tMOVQ\t");
emitline(reg);
emitline(", ");
emitoff((off + i * 8): i64);
emitline("(BP)\n");
i += 1;
};
if (tlm > 0) {
let top: str = "MOVB";
if (tlm == 4) { top = "MOVL"; };
if (tlm == 2) { top = "MOVW"; };
let treg: str = "AX";
if (full == 1) { treg = "DX"; };
if (full == 2) { treg = "CX"; };
emitline("\t");
emitline(top);
emitline("\t");
emitline(treg);
emitline(", ");
emitoff((off + full * 8): i64);
emitline("(BP)\n");
};
return;
};
};
};
};
};
};
};
// Float-typed local: rhs lands in X0; store via MOVSD/
// MOVSS, no AX shuffle. Compound (+= -= *= /=) loads
// slot into X1, combines into X1, stores X1 back —
@@ -14080,6 +14895,79 @@ fn cglet(c: *cgen, n: *node) void = {
return;
};
};
// 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 writes 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} (unreachable under WW struct
// alignment rules — field aligns force size%align==0) fall
// through to the generic scalar store rather than emit a
// stomping MOVQ tail. Sizes >24B also fall through (sret
// deferred, same constraint as #4). Mirrors the cstage
// cgen.c N_LET receive branch.
if (rhs.kind == nkind.N_CALL) {
let sname: str;
sname.ptr = nil; sname.len = 0;
if (tn != nil) {
if (tn.kind == nkind.N_TNAME) {
sname = tn.str;
};
};
if (sname.len > 0) {
let lsi: *structinfo = structlookup(c, sname);
if (lsi != nil) {
// si.totsize is slot-padded (rounded to 8) for
// stack-slot use; the receive ABI needs the
// TYPE's natural size — see structnaturalsize.
let lsz: i32 = structnaturalsize(lsi);
let tlm: i32 = lsz - (lsz / 8) * 8;
if (lsz <= 24) {
if (tlm == 0 || tlm == 1
|| tlm == 2 || tlm == 4) {
cgexpr(c, rhs);
let full: i32 = lsz / 8;
let i: i32 = 0;
for (i < full) {
let reg: str = "AX";
if (i == 1) { reg = "DX"; };
if (i == 2) { reg = "CX"; };
emitline("\tMOVQ\t");
emitline(reg);
emitline(", ");
emitoff((off + i * 8): i64);
emitline("(BP)\n");
i += 1;
};
if (tlm > 0) {
let top: str = "MOVB";
if (tlm == 4) { top = "MOVL"; };
if (tlm == 2) { top = "MOVW"; };
let treg: str = "AX";
if (full == 1) { treg = "DX"; };
if (full == 2) { treg = "CX"; };
emitline("\t");
emitline(top);
emitline("\t");
emitline(treg);
emitline(", ");
emitoff((off + full * 8): i64);
emitline("(BP)\n");
};
c.lastwasreturn = 0;
return;
};
};
};
};
};
cgexpr(c, rhs);
// Float local: cgexpr leaves the value in X0. Spill via
// MOVSS (f32, 4B) or MOVSD (f64, 8B).

View File

@@ -368,6 +368,32 @@ export fn dup(s: str) str = {
return r;
};
// freeall — release every str element in `s` (those that were
// individually allocated) plus the slice's backing storage. Mirrors
// Hare's strings::freeall — the natural disposer for any function
// returning a fresh `[]str` of dup'd elements (e.g. shlex.split).
//
// Each element is freed via os.free at its own length; the slice
// header storage is freed at `cap * 16` bytes (one str = 16B). Empty
// elements (`{nil, 0}` from a zero-length dup) are skipped — calling
// os.free on a nil pointer at len 0 would tickle the rt_free guard
// that the runtime treats as a logic bug.
//
// `cap == 0` means the slice was never grown (empty `[]str` with no
// backing allocation); skip the header free in that case too.
export fn freeall(s: []str) void = {
let i: i32 = 0;
for (i < s.len) {
if (s[i].len > 0) {
os.free(s[i].ptr: *void, s[i].len: u64);
};
i += 1;
};
if (s.cap > 0) {
os.free(s.ptr: *void, (s.cap: u64) * 16u64);
};
};
// rbyteindex — last byte position of `needle` in `s`. Mirrors Hare's
// strings::rbyteindex. Rune needle scans for the byte that encodes it
// (ASCII only); str needle scans for the substring. Empty str needle

View File

@@ -0,0 +1,435 @@
/*
* 701_cgassign_struct — whole-struct receive ABI for sizes <= 24B
* (receive side of #4's cgreturn, task #5 #27).
*
* #4 wired the producer side: `return s;` materialises bytes into
* AX (bytes 0..7), DX (8..15), CX (16..23), zero-padded to 24B. The
* #4 test (698_cgreturn_struct.c) only pins that cgreturn doesn't
* crash; the result is discarded by the caller. #5 wires the
* receive side end-to-end:
*
* - `let s: foo = bar()` — N_LET call-result rhs lands AX/DX/CX
* into the slot with sized stores (MOVQ for full 8B chunks,
* MOVL/MOVB/MOVW tail by *declared* struct size — the receiver
* must NOT mirror the sender's three uniform MOVQs, else
* trailing 1..7 bytes overrun the next slot).
* - `let s: foo = foo{...}` — already wired by #4's predecessor;
* this test pins the value path.
* - `s = bar()` reassign — symmetric N_ASSIGN N_IDENT-lhs.
* - `s = foo{...}` reassign — symmetric N_STRUCTLIT shape.
*
* What this test pins:
* - cstage and wwstage emit byte-identical asm for every fixture
* (catches any drift in the receive-side store sequence).
* - END-TO-END VALUE CORRECTNESS: each fixture's main returns a
* deterministic exit code derived from the received struct's
* fields. A regression in the receive ABI (e.g., DX→+0 instead
* of +8, or an over-wide tail MOVQ overrunning the slot) shows
* up as a `got != want` exit-code mismatch, not a no-crash
* pass.
*
* Coverage:
* - 8B (one_i64): smallest struct, only AX is meaningful.
* - 16B (pair_i64): two-word AX/DX.
* - 20B (five_i32): three-word AX/DX/CX with MOVL tail — the
* headline ASYMMETRY case. A naïve MOVQ tail here would stomp
* 4 bytes past the slot.
* - 24B (trip_i64): three-word, no tail.
* - 24B mix (i32+i32+i64+i64): registerstruct-packed shape.
*
* Each shape covers two rhs forms (call-result, struct-literal)
* across two lvalue forms (let-init, reassign).
*
* Sizes >24B are OUT OF SCOPE — sret is deferred (same constraint
* as #4). Tail chunks in {3,5,6,7} are unreachable under WW
* struct alignment rules (field aligns force size%align == 0); the
* receive branches guard those out and fall through.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
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; };
static const struct row rows[] = {
/* 8B, call-result let-init. exit = s.v (= 7). */
{ "one_i64_let_call",
"type one = struct { v: i64 };\n"
"fn mk() one = { return one { v = 7i64 }; };\n"
"fn main() i32 = {\n"
" let s: one = mk();\n"
" return s.v: i32;\n"
"};\n",
7 },
/* 8B, struct-literal let-init. exit = s.v (= 11). */
{ "one_i64_let_lit",
"type one = struct { v: i64 };\n"
"fn main() i32 = {\n"
" let s: one = one { v = 11i64 };\n"
" return s.v: i32;\n"
"};\n",
11 },
/* 8B, call-result reassign. exit = s.v (= 13). */
{ "one_i64_reassign_call",
"type one = struct { v: i64 };\n"
"fn mk() one = { return one { v = 13i64 }; };\n"
"fn main() i32 = {\n"
" let s: one = one { v = 0i64 };\n"
" s = mk();\n"
" return s.v: i32;\n"
"};\n",
13 },
/* 8B, struct-literal reassign. exit = s.v (= 17). */
{ "one_i64_reassign_lit",
"type one = struct { v: i64 };\n"
"fn main() i32 = {\n"
" let s: one = one { v = 0i64 };\n"
" s = one { v = 17i64 };\n"
" return s.v: i32;\n"
"};\n",
17 },
/* 16B, call-result let-init. exit = a + b (= 3 + 5 = 8).
* Pins that DX lands at +8 and AX at +0. */
{ "pair_i64_let_call",
"type pair = struct { a: i64, b: i64 };\n"
"fn mk() pair = { return pair { a = 3i64, b = 5i64 }; };\n"
"fn main() i32 = {\n"
" let s: pair = mk();\n"
" return (s.a + s.b): i32;\n"
"};\n",
8 },
/* 16B, struct-literal reassign. exit = a + b (= 4 + 6 = 10). */
{ "pair_i64_reassign_lit",
"type pair = struct { a: i64, b: i64 };\n"
"fn main() i32 = {\n"
" let s: pair = pair { a = 0i64, b = 0i64 };\n"
" s = pair { a = 4i64, b = 6i64 };\n"
" return (s.a + s.b): i32;\n"
"};\n",
10 },
/* 20B (five-i32), call-result let-init — the ASYMMETRY
* headline case. AX bytes 0..7 hold a,b. DX bytes 8..15 hold
* c,d. CX low 4 bytes hold e; CX upper 4 bytes are pad zero.
* The receiver MUST store CX as MOVL (4B), not MOVQ (8B);
* a MOVQ would overrun into the next local slot. Sum check:
* 1+2+3+4+5 = 15. */
{ "five_i32_let_call",
"type five = struct { a: i32, b: i32, c: i32, d: i32, e: i32 };\n"
"fn mk() five = {\n"
" return five { a = 1, b = 2, c = 3, d = 4, e = 5 };\n"
"};\n"
"fn main() i32 = {\n"
" let s: five = mk();\n"
" return s.a + s.b + s.c + s.d + s.e;\n"
"};\n",
15 },
/* 20B (five-i32), struct-literal reassign. Same tail-MOVL
* pin as above but exercises the structlit-field-walk path. */
{ "five_i32_reassign_lit",
"type five = struct { a: i32, b: i32, c: i32, d: i32, e: i32 };\n"
"fn main() i32 = {\n"
" let s: five = five { a = 0, b = 0, c = 0, d = 0, e = 0 };\n"
" s = five { a = 2, b = 4, c = 6, d = 8, e = 10 };\n"
" return s.a + s.b + s.c + s.d + s.e;\n"
"};\n",
30 },
/* 24B, call-result let-init — the headline three-word ABI.
* AX/DX/CX each carry one full word, no tail. Sum check:
* 11+22+33 = 66. */
{ "trip_i64_let_call",
"type trip = struct { x: i64, y: i64, z: i64 };\n"
"fn mk() trip = { return trip { x = 11i64, y = 22i64, z = 33i64 }; };\n"
"fn main() i32 = {\n"
" let s: trip = mk();\n"
" return (s.x + s.y + s.z): i32;\n"
"};\n",
66 },
/* 24B, struct-literal reassign. Same word-shape as above. */
{ "trip_i64_reassign_lit",
"type trip = struct { x: i64, y: i64, z: i64 };\n"
"fn main() i32 = {\n"
" let s: trip = trip { x = 0i64, y = 0i64, z = 0i64 };\n"
" s = trip { x = 14i64, y = 28i64, z = 42i64 };\n"
" return (s.x + s.y + s.z): i32;\n"
"};\n",
84 },
/* 24B mix (registerstruct-packed): {i32, i32, i64, i64}.
* Layout: a@+0, b@+4, c@+8, d@+16 (totsize 24). AX carries
* a+b packed, DX = c, CX = d. Sum check: 1+2+3+4 = 10. */
{ "mix_let_call",
"type mix = struct { a: i32, b: i32, c: i64, d: i64 };\n"
"fn mk() mix = { return mix { a = 1, b = 2, c = 3i64, d = 4i64 }; };\n"
"fn main() i32 = {\n"
" let s: mix = mk();\n"
" return (s.a + s.b) + (s.c + s.d): i32;\n"
"};\n",
10 },
/* dst.field = call() — single-dot struct-typed field via
* local struct base. Exercises the cgen.c:1996+ N_DOT.N_IDENT
* struct-field branch (cstage) and cgenexpr.ww:~4046 single-
* dot local-base site (wwstage). Sum check: 3+4+0 = 7. */
{ "field_local_call",
"type inner = struct { a: i64, b: i64 };\n"
"type outer = struct { i: inner, t: i64 };\n"
"fn mki() inner = { return inner { a = 3i64, b = 4i64 }; };\n"
"fn main() i32 = {\n"
" let o: outer = outer { i = inner { a = 0i64, b = 0i64 }, t = 0i64 };\n"
" o.i = mki();\n"
" return (o.i.a + o.i.b + o.t): i32;\n"
"};\n",
7 },
/* dst.field = T{...} — same single-dot site, struct-literal
* rhs. Sum check: 11+22+5 = 38. */
{ "field_local_lit",
"type inner = struct { a: i64, b: i64 };\n"
"type outer = struct { i: inner, t: i64 };\n"
"fn main() i32 = {\n"
" let o: outer = outer { i = inner { a = 0i64, b = 0i64 }, t = 5i64 };\n"
" o.i = inner { a = 11i64, b = 22i64 };\n"
" return (o.i.a + o.i.b + o.t): i32;\n"
"};\n",
38 },
/* dst.field = call() via *struct base (auto-deref through
* pointer). Exercises cgenexpr.ww:~3752 single-dot ptr-base
* site (wwstage). Sum check: 30+40+100 = 170. */
{ "field_ptr_call",
"type inner = struct { a: i64, b: i64 };\n"
"type outer = struct { i: inner, t: i64 };\n"
"fn mki() inner = { return inner { a = 30i64, b = 40i64 }; };\n"
"fn main() i32 = {\n"
" let o: outer = outer { i = inner { a = 0i64, b = 0i64 }, t = 100i64 };\n"
" let p: *outer = &o;\n"
" p.i = mki();\n"
" return (o.i.a + o.i.b + o.t): i32;\n"
"};\n",
170 },
/* dst.field = T{...} via *struct base, struct-literal rhs.
* Sum check: 7+8+50 = 65. */
{ "field_ptr_lit",
"type inner = struct { a: i64, b: i64 };\n"
"type outer = struct { i: inner, t: i64 };\n"
"fn main() i32 = {\n"
" let o: outer = outer { i = inner { a = 0i64, b = 0i64 }, t = 50i64 };\n"
" let p: *outer = &o;\n"
" p.i = inner { a = 7i64, b = 8i64 };\n"
" return (o.i.a + o.i.b + o.t): i32;\n"
"};\n",
65 },
/* dst.field = call() via top-level let-global base.
* Exercises cgenexpr.ww:~4352 single-dot global-base site
* (wwstage) and the corresponding cstage is_global path.
* `let g: T;` (no init) avoids the module-name-mangle path
* that initialised globals hit (cf. task #17). Sum check:
* 70+80+100 = 250 (≤ 255, fits in process exit code). */
{ "field_global_call",
"type inner = struct { a: i64, b: i64 };\n"
"type outer = struct { i: inner, t: i64 };\n"
"let g: outer;\n"
"fn mki() inner = { return inner { a = 70i64, b = 80i64 }; };\n"
"fn main() i32 = {\n"
" g.t = 100i64;\n"
" g.i = mki();\n"
" return (g.i.a + g.i.b + g.t): i32;\n"
"};\n",
250 },
/* Chained `s.f.g = call()` — depth-2 dot lhs. Exercises the
* cstage chained walker (cgen.c:~2626) and wwstage walker
* (cgenexpr.ww:~4381). The initialiser uses explicit field
* writes (not a nested struct-literal), since cgen's nested-
* STRUCTLIT-as-field-value path is a separate gap. Bare
* `let o: outer;` zero-fills the slot. Sums sized to fit a
* process exit code (8 bits): 5+6+10+20 = 41. */
{ "field_chain_call",
"type inner = struct { a: i64, b: i64 };\n"
"type middle = struct { in: inner, t: i64 };\n"
"type outer = struct { m: middle, x: i64 };\n"
"fn mki() inner = { return inner { a = 5i64, b = 6i64 }; };\n"
"fn main() i32 = {\n"
" let o: outer;\n"
" o.m.t = 10i64;\n"
" o.x = 20i64;\n"
" o.m.in = mki();\n"
" return (o.m.in.a + o.m.in.b + o.m.t + o.x): i32;\n"
"};\n",
41 },
/* Chained `s.f.g = T{...}` — depth-2 struct-literal rhs.
* Sum check: 11+22+10+20 = 63. */
{ "field_chain_lit",
"type inner = struct { a: i64, b: i64 };\n"
"type middle = struct { in: inner, t: i64 };\n"
"type outer = struct { m: middle, x: i64 };\n"
"fn main() i32 = {\n"
" let o: outer;\n"
" o.m.t = 10i64;\n"
" o.x = 20i64;\n"
" o.m.in = inner { a = 11i64, b = 22i64 };\n"
" return (o.m.in.a + o.m.in.b + o.m.t + o.x): i32;\n"
"};\n",
63 },
};
static int
run_driver(const char *driver, const struct row *r, int i)
{
char src[64], tmpdir[64], cmd[1024];
snprintf(src, sizeof src, "/tmp/wcas_%d_%d.ww", getpid(), i);
snprintf(tmpdir, sizeof tmpdir, "/tmp/wcas_%d_d_%d", getpid(), i);
FILE *f = fopen(src, "wb");
if (!f) return -1;
fputs(r->src, f);
fclose(f);
mkdir(tmpdir, 0755);
snprintf(cmd, sizeof cmd, "cd %s && %s build %s",
tmpdir, driver, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: build via %s failed\n",
r->label, driver);
unlink(src); rmdir(tmpdir);
return -1;
}
const char *base = strrchr(src, '/');
base = base ? base + 1 : src;
char outbin[128];
snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base);
char *dot = strrchr(outbin, '.');
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
int got = runwait(outbin);
unlink(src); unlink(outbin); rmdir(tmpdir);
return got;
}
/* asm_byte_identical — generate .s via cstage's w6c and wwstage's
* w6c_ww and diff. Catches receive-side codegen drift between the
* stages, which 995_self_rebuild covers globally but doesn't surface
* as a focused-fixture failure. */
static int
asm_byte_identical(const char *bin, const struct row *r, int i)
{
char src[64], cs[64], ws[64], cmd[1024];
snprintf(src, sizeof src, "/tmp/wcas_asm_%d_%d.ww", getpid(), i);
snprintf(cs, sizeof cs, "/tmp/wcas_asm_%d_%d_c.s", getpid(), i);
snprintf(ws, sizeof ws, "/tmp/wcas_asm_%d_%d_w.s", getpid(), i);
FILE *f = fopen(src, "wb");
if (!f) return -1;
fputs(r->src, f);
fclose(f);
snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s 2>/dev/null", bin, cs, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: w6c errored\n", r->label);
unlink(src);
return -1;
}
snprintf(cmd, sizeof cmd, "%s/w6c_ww -o %s %s 2>/dev/null",
bin, ws, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: w6c_ww errored\n", r->label);
unlink(src); unlink(cs);
return -1;
}
FILE *fc = fopen(cs, "rb");
FILE *fw = fopen(ws, "rb");
int rc = 0;
if (!fc || !fw) {
rc = -1;
} else {
for (;;) {
int a = fgetc(fc);
int b = fgetc(fw);
if (a != b) { rc = -1; break; }
if (a == EOF) break;
}
}
if (fc) fclose(fc);
if (fw) fclose(fw);
if (rc != 0)
fprintf(stderr, "row[%s]: cstage vs wwstage asm differs\n",
r->label);
unlink(src); unlink(cs); unlink(ws);
return rc;
}
int
main(void)
{
const char *bin = getenv("BIN");
if (!bin) bin = "out/bin";
char absbin[1024];
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 cdrv[1024];
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
char wdrv[1024];
snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin);
struct { const char *name; const char *path; int gated_on_existence; }
drivers[] = {
{ "cstage", cdrv, 0 },
{ "wwstage", wdrv, 1 },
{ NULL, NULL, 0 },
};
int n = (int)(sizeof rows / sizeof rows[0]);
int total = 0, fail = 0;
/* Compile+run for each (driver, row). */
for (int d = 0; drivers[d].name; d++) {
if (drivers[d].gated_on_existence
&& access(drivers[d].path, X_OK) != 0) {
fprintf(stderr, "cgassign_struct: skip %s (no %s)\n",
drivers[d].name, drivers[d].path);
continue;
}
for (int i = 0; i < n; i++) {
int got = run_driver(drivers[d].path, &rows[i], i);
total++;
if (got != rows[i].want) {
fprintf(stderr,
"cgassign_struct[%s][%s]: exit=%d want=%d\n",
drivers[d].name, rows[i].label,
got, rows[i].want);
fail++;
}
}
}
/* Asm byte-identity diff, only when both stages exist. */
if (access(wdrv, X_OK) == 0) {
for (int i = 0; i < n; i++) {
total++;
if (asm_byte_identical(bin, &rows[i], i) != 0)
fail++;
}
}
if (fail) {
fprintf(stderr,
"cgassign_struct: %d/%d fixtures failed\n", fail, total);
return 1;
}
printf("cgassign_struct: %d/%d ok\n", total, total);
return 0;
}