diff --git a/Makefile b/Makefile index 4e9cc1b7..1f15c5a0 100644 --- a/Makefile +++ b/Makefile @@ -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) diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 3ee1b5a7..f0a03089 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -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 diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 91576ddc..438832c8 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -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). diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 1dd4a709..557c6a19 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -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 — diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index 3ea5017f..c2d806ff 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -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). diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index c7027400..00784f87 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -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. diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 34b57dbb..b9744939 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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). diff --git a/selfhost/test/smoke.combined.ww b/selfhost/test/smoke.combined.ww index fa83f16d..7068cd6e 100644 --- a/selfhost/test/smoke.combined.ww +++ b/selfhost/test/smoke.combined.ww @@ -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 diff --git a/test/wcc/701_cgassign_struct.c b/test/wcc/701_cgassign_struct.c new file mode 100644 index 00000000..b583eda7 --- /dev/null +++ b/test/wcc/701_cgassign_struct.c @@ -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 +#include +#include +#include +#include +#include + +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; +}