From 99a68a6a573cca75b7cdea163f31f0180175958e Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 15 May 2026 18:45:21 +0900 Subject: [PATCH] cstage+selfhost+test: extend structlit-fill helper to N_ASSIGN N_DOT lhs (4 flavors) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sister fix to #17. The BP-rel helper from #17 covered N_LET / N_ASSIGN N_IDENT-lhs / N_RETURN; the four N_ASSIGN N_DOT-lhs structlit walks still went through the inline `cgexpr(field.lhs); store-AX-sized` shape and silently dropped trailing bytes when a struct-typed field's value was itself an N_STRUCTLIT. Affected dot flavors: single-dot via_ptr / global / BP-rel and the chained-dot walker (depth >= 2, all three root flavors). Extend `cg_structlit_fill_bp` / `cgstructlitfillbp` into `cg_structlit_fill` / `cgstructlitfill` taking a destination mode (DST_BP / DST_PTR_LOCAL / DST_GLOBAL = 0/1/2), srcoff (PTR_LOCAL), srcname (GLOBAL), and disp accumulator. `disp` grows by foff on descent; srcoff/srcname stay constant across the call tree. The pre-#17 wrappers are preserved byte-identically by delegating with mode=DST_BP — 995_self_rebuild byte-identity holds for the no- nested-STRUCTLIT case that selfhost source actually uses. The non-BP modes reload BX before the ELLIPSIS zero-fill loop AND before every field store (tagged, scalar, and the cgexpr leaf). This is correctness-by-construction — cgexpr clobbers BX between fields, and the redundant reload only fires on shapes that didn't compile before. The four dot-flavor sites in each stage now compute their dst mode + disp and call the shared helper (reducing each from ~80-130 inline lines to ~5-12 lines of dispatch). Stage signature asymmetry: cstage threads Local** for cgexpr; ww- stage takes explicit totsize because #15 (split totsize into naturalsize + slotsize) is still pending and the dot sites need structnaturalsize while the BP-rel sites need si.totsize. Both asymmetries are documented in the helper docstrings. 704 covers 8 rows (24 checks: 8 cstage exits, 8 wwstage exits, 8 cstage-vs-wwstage .s byte-identity diffs): 6 dst-flavors (single- dot local/ptr/global, chained-dot local/ptr/global) plus single- local 3-deep and single-ptr 3-deep to pin disp threading through the helper's recursion and through DST_PTR_LOCAL BX reloads. The nested struct-typed CALL rhs in field-walks has the same shape as the STRUCTLIT bug fixed here but the helper only handles STRUCTLIT — tracked as task #20. --- Makefile | 7 + cmd/w6c/cgen.c | 407 ++++++------------ selfhost/cmd/w6c/main.combined.ww | 588 +++++++++------------------ selfhost/cmd/wcc/cgenexpr.ww | 382 +++-------------- selfhost/cmd/wcc/cgenutil.ww | 206 +++++++--- selfhost/cmd/wwdump/main.combined.ww | 588 +++++++++------------------ test/wcc/704_dot_structlit.c | 342 ++++++++++++++++ 7 files changed, 1070 insertions(+), 1450 deletions(-) create mode 100644 test/wcc/704_dot_structlit.c diff --git a/Makefile b/Makefile index 09670254..dbd98669 100644 --- a/Makefile +++ b/Makefile @@ -230,6 +230,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_cgassign_struct \ $(BIN)/test_dot_explicit_deref \ $(BIN)/test_nested_structlit \ + $(BIN)/test_dot_structlit \ $(BIN)/test_use_promote_alias \ $(BIN)/test_field_signed $(BIN)/test_frame_argcount \ $(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \ @@ -395,6 +396,12 @@ $(BIN)/test_nested_structlit: test/wcc/703_nested_structlit.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_dot_structlit: test/wcc/704_dot_structlit.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 5701413a..a4098133 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -864,6 +864,13 @@ static void cgstmt(Cg*, Node*, Local**, int*); static void cg_widen_tagged_push(Cg*, Local**, Type*, Node*, int); static void cg_widen_tagged_store(Cg*, Local**, Type*, Node*, int, int, int); static void cg_widen_tag_remap(Cg*, Type*, Type*, int); +/* cg_structlit_fill modes — see helper docstring. */ +enum { + DST_BP = 0, + DST_PTR_LOCAL = 1, + DST_GLOBAL = 2, +}; +static void cg_structlit_fill(Cg*, Local**, Type*, Node*, int, int, const char*, int); static void cg_structlit_fill_bp(Cg*, Local**, Type*, Node*, int); static void @@ -1226,53 +1233,81 @@ cg_widen_tagged_push(Cg *c, Local **locals_p, Type *dst, Node *src, int sz) } } -/* cg_structlit_fill_bp — fill a struct-typed slot from an N_STRUCTLIT - * value into BP-relative memory at `bp_off`. Used by N_LET, N_ASSIGN - * N_IDENT-lhs, and N_RETURN N_STRUCTLIT sites where the dst is BP- - * relative (BP-rel locals, or a return-side scratch slot). +/* cg_structlit_fill — fill a struct-typed slot from an N_STRUCTLIT + * value into one of three destination flavors. Used by N_LET, N_ASSIGN + * N_IDENT-lhs, N_RETURN N_STRUCTLIT (BP-rel), and N_ASSIGN N_DOT-lhs + * (BP-rel / via *struct local / via struct global) at single-dot and + * chained-dot sites. + * + * Destination modes: + * DST_BP — base = BP, no reload. Stores at disp+i(BP). + * srcoff/name unused. + * DST_PTR_LOCAL — base = BX, reloaded from srcoff(BP) before the + * ELLIPSIS zero-fill loop and before EVERY field + * store (cgexpr clobbers BX between fields). + * Stores at disp+i(BX). name unused. + * DST_GLOBAL — base = BX, reloaded via `LEAQ name(SB), BX` with + * the same reload cadence as DST_PTR_LOCAL. + * srcoff unused. + * + * Param semantics (locked in here so the recursion contract is clear): + * - `disp` is the per-recursion accumulator — grows by `foff` as + * we descend into a nested struct-typed structlit field. + * - `srcoff` (DST_PTR_LOCAL) and `name` (DST_GLOBAL) are *constant* + * across the whole call tree — they identify the root dst, which + * doesn't change with depth. Recursion passes them through. * * Why a helper? The inline field-walk at each call site previously * did `cgexpr(f->lhs); store AX (sized)`. For struct-typed fields * whose value is itself a nested N_STRUCTLIT, cgexpr has no whole- * struct-in-register convention — it lands AX = first qword and the - * trailing bytes silently stay zero (or stack garbage). Selfhost - * source uses write-by-field as a fixture-level workaround; with - * this helper, nested literals recurse cleanly and the workaround - * is no longer load-bearing. + * trailing bytes silently stay zero (or stack garbage). #17 fixed + * the BP-rel sites; #18 extends the same recursion to the four + * N_ASSIGN N_DOT-lhs structlit walks (single-dot via_ptr/global/ + * local + chained depth>=2). * - * Sister branches (N_ASSIGN N_DOT-lhs structlit — via_ptr / global / - * BP-relative) keep their inline field-walk for v1 since their - * addressing has additional BX-reload concerns; nested-STRUCTLIT - * silent-zero still drops on those paths — separate follow-up. + * The non-BP modes emit a redundant BX reload at the start of each + * recursive nested zero-fill / each recursive scalar store — this is + * correctness-by-construction (BX is always freshly loaded right + * before use), and the redundancy only fires on the nested-STRUCTLIT + * shapes that didn't compile before. Byte-identity for the no-nested + * case (the only shape selfhost source uses today) is preserved + * because the existing inline code's reload-before-each-store pattern + * matches the helper's per-store reload exactly. * - * Output is byte-identical to the prior inline code for any input - * that has no nested-STRUCTLIT field (the only shape selfhost - * actually compiles today), preserving 995_self_rebuild byte- - * identity. */ + * The scalar store dispatch stays at the explicit {1->MOVB, 4->MOVL, + * else MOVQ} shape (not fieldstoreop, which emits MOVW for fsz==2) to + * stay byte-identical with cstage pending task #13. */ static void -cg_structlit_fill_bp(Cg *c, Local **locals_p, Type *lu, Node *lit, int bp_off) +cg_structlit_fill(Cg *c, Local **locals_p, Type *lu, Node *lit, + int mode, int srcoff, const char *name, int disp) { int sz = (int)lu->size; + int base_reg = (mode == DST_BP) ? D_BP : D_BX; if (lit->op == TK_ELLIPSIS) { /* `..., ...` autofill — zero the entire slot first so - * unmentioned fields read as 0. Sized stores: 8/4/1 - * (matches the inline pre-#17 pattern at N_LET / N_ASSIGN / - * N_RETURN). */ + * unmentioned fields read as 0. Sized stores: 8/4/1. For + * non-BP modes, reload BX once before the loop (cgexpr-free + * region between iterations, so one reload is enough). */ ins2(c, A_XORQ, areg(D_AX), areg(D_AX)); + if (mode == DST_PTR_LOCAL) + ins2(c, A_MOVQ, amem(D_BP, srcoff), areg(D_BX)); + else if (mode == DST_GLOBAL) + ins2(c, A_LEAQ, masym(c, name), areg(D_BX)); int zi = 0; while (zi + 8 <= sz) { ins2(c, A_MOVQ, areg(D_AX), - amem(D_BP, bp_off + zi)); + amem(base_reg, disp + zi)); zi += 8; } while (zi + 4 <= sz) { ins2(c, A_MOVL, areg(D_AX), - amem(D_BP, bp_off + zi)); + amem(base_reg, disp + zi)); zi += 4; } while (zi < sz) { ins2(c, A_MOVB, areg(D_AX), - amem(D_BP, bp_off + zi)); + amem(base_reg, disp + zi)); zi += 1; } } @@ -1290,36 +1325,59 @@ cg_structlit_fill_bp(Cg *c, Local **locals_p, Type *lu, Node *lit, int bp_off) } Type *fu = (ft && ft->kind == TY_NAMED) ? ft->under : ft; if (fu && fu->kind == TY_TAGGED) { + /* Tagged store: reload BX first (if non-BP) so the + * widener sees a valid base reg. The widener itself + * preserves base_reg through its internal cgexpr. */ + if (mode == DST_PTR_LOCAL) + ins2(c, A_MOVQ, amem(D_BP, srcoff), + areg(D_BX)); + else if (mode == DST_GLOBAL) + ins2(c, A_LEAQ, masym(c, name), areg(D_BX)); cg_widen_tagged_store(c, locals_p, fu, f->lhs, - D_BP, bp_off + (int)foff, (int)fu->size); + base_reg, disp + (int)foff, (int)fu->size); continue; } /* Nested struct-typed structlit value: recurse at the - * field's offset so all inner fields land. Pre-#17 the + * field's offset so all inner fields land. Pre-#17/#18 the * cgexpr-then-store below would land AX = first qword and * the rest silently stayed zero. */ if (fu && fu->kind == TY_STRUCT && f->lhs && f->lhs->kind == N_STRUCTLIT) { - cg_structlit_fill_bp(c, locals_p, fu, f->lhs, - bp_off + (int)foff); + cg_structlit_fill(c, locals_p, fu, f->lhs, + mode, srcoff, name, disp + (int)foff); continue; } cgexpr(c, f->lhs, *locals_p); + /* For non-BP modes, cgexpr just clobbered BX; reload it + * before the store. */ + if (mode == DST_PTR_LOCAL) + ins2(c, A_MOVQ, amem(D_BP, srcoff), areg(D_BX)); + else if (mode == DST_GLOBAL) + ins2(c, A_LEAQ, masym(c, name), areg(D_BX)); 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, bp_off + (int)foff)); + amem(base_reg, disp + (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, bp_off + (int)foff)); + amem(base_reg, disp + (int)foff)); } } +/* Thin wrapper preserving the BP-rel call shape used by N_LET, + * N_ASSIGN N_IDENT-lhs, and N_RETURN. Byte-identical to the pre-#18 + * helper. */ +static void +cg_structlit_fill_bp(Cg *c, Local **locals_p, Type *lu, Node *lit, int bp_off) +{ + cg_structlit_fill(c, locals_p, lu, lit, DST_BP, 0, NULL, bp_off); +} + static void cgexpr(Cg *c, Node *n, Local *locals) { @@ -2176,137 +2234,21 @@ cgexpr(Cg *c, Node *n, Local *locals) 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; + /* Delegate to the shared structlit fill + * helper. For via_ptr/is_global, helper + * reloads BX before zero-fill loop + each + * field store. For local BP-rel, helper + * stores direct off BP. AND nested struct- + * typed structlit values recurse instead + * of silently dropping trailing bytes + * (#18 fix). */ + int mode = via_ptr ? DST_PTR_LOCAL + : is_global ? DST_GLOBAL : DST_BP; + int disp = (mode == DST_BP) + ? (boff + foff) : foff; + cg_structlit_fill(c, &locals, str_fu, + n->rhs, mode, boff, + is_global ? base->str : NULL, disp); break; } if (n->op == TK_ASSIGN && str_fu @@ -2974,135 +2916,22 @@ cgexpr(Cg *c, Node *n, Local *locals) } 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; + /* Delegate to the shared structlit fill + * helper. For via_cx (ptr_root | is_global), + * helper reloads BX before zero-fill loop + + * each field store. For local through chain, + * helper stores direct off BP. AND nested + * struct-typed structlit values recurse + * instead of silently dropping trailing + * bytes (#18 fix). */ + int dst_mode = ptr_root ? DST_PTR_LOCAL + : is_global ? DST_GLOBAL : DST_BP; + int dst_disp = (dst_mode == DST_BP) + ? (base_disp + total_off) : total_off; + cg_structlit_fill(c, &locals, fu, + n->rhs, dst_mode, base_disp, + is_global ? cur->str : NULL, + dst_disp); break; } if (fu && fu->kind == TY_STRUCT diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index e72fe0a1..41ea38ea 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -8656,59 +8656,113 @@ export fn dotchainresolve(c: *cgen, n: *node, return false; }; -// cgstructlitfillbp — fill a struct-typed slot from an N_STRUCTLIT -// value into BP-relative memory at `bpoff`. Mirror of cstage cgen.c's -// cg_structlit_fill_bp. Used by cglet, cgreturn N_STRUCTLIT, and -// cgassign N_IDENT-lhs N_STRUCTLIT sites where the dst is BP- -// relative. +// cgstructlitfill — fill a struct-typed slot from an N_STRUCTLIT +// value into one of three destination flavors. Mirror of cstage +// cgen.c's cg_structlit_fill. Used by cglet, cgreturn N_STRUCTLIT, +// cgassign N_IDENT-lhs N_STRUCTLIT (BP-rel) AND cgassign N_DOT-lhs +// N_STRUCTLIT (BP-rel / via *struct local / via struct global) at +// single-dot and chained-dot sites. // -// The inline field-walk previously did `cgexpr(field.lhs); store AX -// sized`. For struct-typed fields whose value is itself a nested -// N_STRUCTLIT, cgexpr has no whole-struct-in-register convention — -// it lands AX = first qword and the trailing bytes silently stay -// zero. Selfhost source used write-by-field as a workaround; this -// helper recurses cleanly so the workaround is no longer required. +// Destination modes: +// 0 = DST_BP — base = BP, no reload. Stores at disp+i(BP). +// srcoff/srcname unused. +// 1 = DST_PTR_LOCAL — base = BX, reloaded from srcoff(BP) before +// the ELLIPSIS zero-fill loop and before EVERY +// field store (cgexpr clobbers BX between +// fields). Stores at disp+i(BX). srcname +// unused. +// 2 = DST_GLOBAL — base = BX, reloaded via `LEAQ srcname(SB), +// BX` with the same cadence as DST_PTR_LOCAL. +// srcoff unused. // -// Sister branches (cgassign single-dot / via_ptr / global structlit -// walks) keep their inline field-walk in v1 since their addressing -// has additional BX-reload concerns; nested-STRUCTLIT silent-zero -// still drops on those paths — separate follow-up task. +// Param semantics (locked in here so the recursion contract is +// clear): +// - `disp` is the per-recursion accumulator — grows by `fi.foff` +// as we descend into a nested struct-typed structlit field. +// - `srcoff` (DST_PTR_LOCAL) and `srcname` (DST_GLOBAL) are +// *constant* across the whole call tree — they identify the +// root dst, which doesn't change with depth. +// - `totsize` is also constant; pass the natural size for dot +// sites (structnaturalsize) and si.totsize for BP-rel sites, +// matching each site's pre-#18 zero-fill bound. // -// Output is byte-identical to the prior inline code for any input -// that has no nested-STRUCTLIT field (the only shape selfhost source -// actually compiles today), preserving 995_self_rebuild byte- -// identity. +// Why a helper? The inline field-walk previously did +// `cgexpr(field.lhs); store AX sized`. For struct-typed fields whose +// value is itself a nested N_STRUCTLIT, cgexpr has no whole-struct- +// in-register convention — it lands AX = first qword and the +// trailing bytes silently stay zero. #17 fixed the BP-rel sites; +// #18 extends the same recursion to the four cgassign N_DOT-lhs +// structlit walks (single-dot via_ptr/global/local + chained +// depth>=2). +// +// The non-BP modes emit a redundant BX reload at the start of each +// recursive nested zero-fill / each recursive scalar store — this is +// correctness-by-construction (BX is always freshly loaded right +// before use), and the redundancy only fires on the nested-STRUCTLIT +// shapes that didn't compile before. Byte-identity for the no- +// nested case (the only shape selfhost source uses today) is +// preserved because the existing inline code's reload-before-each- +// store pattern matches the helper's per-store reload exactly. // // Graduation note (task #13): the scalar store currently uses the // explicit {1→MOVB, 4→MOVL, else MOVQ} dispatch to match cstage // byte-identically — cstage hasn't yet learned MOVW for fsz==2. Once // #13 aligns both stages, the dispatch can switch to fieldstoreop // which already returns MOVW where appropriate. -fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *node, bpoff: i32) void = { +fn cgstructlitfill(c: *cgen, si: *structinfo, lit: *node, + mode: i32, srcoff: i32, srcname: str, + disp: i32, totsize: i32) void = { if (si == nil) { return; }; + let basereg: str = "BP"; + if (mode != 0) { basereg = "BX"; }; if (lit.op == tkind.TK_ELLIPSIS) { // `..., ...` autofill — zero the entire slot first so - // unmentioned fields read as 0. Uses si.totsize (slot-padded) - // to match the inline pre-#17 pattern. - let total: i32 = si.totsize; + // unmentioned fields read as 0. Sized stores: 8/4/1. For + // non-BP modes, reload BX once before the loop (cgexpr-free + // region between iterations, so one reload is enough). emitline("\tXORQ\tAX, AX\n"); + if (mode == 1) { + emitline("\tMOVQ\t"); + emitoff(srcoff: i64); + emitline("(BP), BX\n"); + }; + if (mode == 2) { + emitline("\tLEAQ\t"); + emitsymname(c, srcname); + emitline("(SB), BX\n"); + }; let zi: i32 = 0; - for (zi + 8 <= total) { + for (zi + 8 <= totsize) { emitline("\tMOVQ\tAX, "); - emitoff((bpoff + zi): i64); - emitline("(BP)\n"); + if (mode == 0) { + emitoff((disp + zi): i64); + emitline("(BP)\n"); + } else { + emitdispreg((disp + zi): i64, basereg); + emitline("\n"); + }; zi += 8; }; - for (zi + 4 <= total) { + for (zi + 4 <= totsize) { emitline("\tMOVL\tAX, "); - emitoff((bpoff + zi): i64); - emitline("(BP)\n"); + if (mode == 0) { + emitoff((disp + zi): i64); + emitline("(BP)\n"); + } else { + emitdispreg((disp + zi): i64, basereg); + emitline("\n"); + }; zi += 4; }; - for (zi < total) { + for (zi < totsize) { emitline("\tMOVB\tAX, "); - emitoff((bpoff + zi): i64); - emitline("(BP)\n"); + if (mode == 0) { + emitoff((disp + zi): i64); + emitline("(BP)\n"); + } else { + emitdispreg((disp + zi): i64, basereg); + emitline("\n"); + }; zi += 1; }; }; @@ -8723,23 +8777,29 @@ fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *node, bpoff: i32) void = { // Tagged-union field: delegate to the shared // widening writer (handles str/scalar/struct // literal/ident payload + tagged-subset tag - // remap). Mirrors cstage cg_structlit_fill_bp - // and the pre-existing wwstage cgreturn - // structlit dispatch; cglet's pre-#17 inline - // lacked this branch but selfhost never - // tripped it (no tagged-fields-in-let- - // structlit in tree). + // remap). For non-BP modes, reload BX first so + // the widener sees a valid base reg. if (istaggedtype(c, fi.tnode)) { + if (mode == 1) { + emitline("\tMOVQ\t"); + emitoff(srcoff: i64); + emitline("(BP), BX\n"); + }; + if (mode == 2) { + emitline("\tLEAQ\t"); + emitsymname(c, srcname); + emitline("(SB), BX\n"); + }; cgwidentaggedstore(c, fi.tnode, - fieldnode.lhs, "BP", - bpoff + fi.foff, fi.fsz); + fieldnode.lhs, basereg, + disp + fi.foff, fi.fsz); fi = nil; } else { // Nested struct-typed structlit value: look up // the inner struct's metadata and recurse at the - // field's offset. Pre-#17 the cgexpr-then-store - // below would land AX = first qword and the rest - // silently stayed zero. + // field's offset. Pre-#17/#18 the cgexpr-then- + // store below would land AX = first qword and + // the rest silently stayed zero. let nested: bool = false; if (fieldnode.lhs != nil) { if (fieldnode.lhs.kind == nkind.N_STRUCTLIT) { @@ -8748,9 +8808,17 @@ fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *node, bpoff: i32) void = { if (primsize(fi.tnode.str) == 0) { let isi: *structinfo = structlookup(c, fi.tnode.str); if (isi != nil) { - cgstructlitfillbp(c, isi, + // Nested fill: pick the size + // discipline matching the outer + // site — dot sites pass natural + // size, BP-rel sites pass + // totsize. Mirror it. + let inner_tot: i32 = isi.totsize; + if (mode != 0) { inner_tot = structnaturalsize(isi); }; + cgstructlitfill(c, isi, fieldnode.lhs, - bpoff + fi.foff); + mode, srcoff, srcname, + disp + fi.foff, inner_tot); nested = true; }; }; @@ -8762,24 +8830,39 @@ fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *node, bpoff: i32) void = { fi = nil; } else { cgexpr(c, fieldnode.lhs); + // For non-BP modes, cgexpr just clobbered + // BX; reload it before the store. + if (mode == 1) { + emitline("\tMOVQ\t"); + emitoff(srcoff: i64); + emitline("(BP), BX\n"); + }; + if (mode == 2) { + emitline("\tLEAQ\t"); + emitsymname(c, srcname); + emitline("(SB), BX\n"); + }; if (isfloattype(c, fi.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\tX0, "); - emitoff((bpoff + fi.foff): i64); - emitline("(BP)\n"); + if (mode == 0) { + emitoff((disp + fi.foff): i64); + emitline("(BP)\n"); + } else { + emitdispreg((disp + fi.foff): i64, basereg); + emitline("\n"); + }; fi = nil; } else { // Explicit {1→MOVB, 4→MOVL, else MOVQ} // dispatch (not fieldstoreop) to match - // cstage cg_structlit_fill_bp byte- - // identically. wwstage's fieldstoreop - // would return MOVW for fsz==2 which - // cstage doesn't emit — tracked as task - // #13. Until that lands, the helper - // emits the cstage shape. + // cstage byte-identically. wwstage's + // fieldstoreop would return MOVW for + // fsz==2 which cstage doesn't emit — + // tracked as task #13. let fsz: i32 = fi.fsz; let op: str = "MOVQ"; if (fsz == 1) { op = "MOVB"; }; @@ -8787,8 +8870,13 @@ fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *node, bpoff: i32) void = { emitline("\t"); emitline(op); emitline("\tAX, "); - emitoff((bpoff + fi.foff): i64); - emitline("(BP)\n"); + if (mode == 0) { + emitoff((disp + fi.foff): i64); + emitline("(BP)\n"); + } else { + emitdispreg((disp + fi.foff): i64, basereg); + emitline("\n"); + }; fi = nil; }; }; @@ -8802,6 +8890,14 @@ fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *node, bpoff: i32) void = { }; }; +// Thin wrapper preserving the BP-rel call shape used by cglet, +// cgreturn, and cgassign N_IDENT-lhs N_STRUCTLIT. Byte-identical to +// the pre-#18 cgstructlitfillbp. +fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *node, bpoff: i32) void = { + if (si == nil) { return; }; + cgstructlitfill(c, si, lit, 0, 0, "", bpoff, si.totsize); +}; + // MODULE: wcc // selfhost/cmd/wcc/cgenexpr.ww — split out of cgen.ww. // @@ -12643,6 +12739,11 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; }; + // #18: delegate to cgstructlitfill so a nested struct- + // typed structlit value recurses instead of dropping + // its trailing bytes. mode=1 (DST_PTR_LOCAL) reloads BX + // from lc.off(BP) before zero-fill and before every + // field store. if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT @@ -12652,75 +12753,8 @@ fn cgassign(c: *cgen, n: *node) void = { 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; - }; + cgstructlitfill(c, ssi, n.rhs, 1, lc.off, "", + fi.foff, ssz); return; }; }; @@ -12936,6 +12970,10 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; }; + // #18: delegate to cgstructlitfill so a nested struct- + // typed structlit value recurses instead of dropping + // its trailing bytes. mode=0 (DST_BP) — direct BP-rel, + // no BX reload. if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT @@ -12945,66 +12983,8 @@ fn cgassign(c: *cgen, n: *node) void = { 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; - }; + cgstructlitfill(c, ssi, n.rhs, 0, 0, "", + lc.off + fi.foff, ssz); return; }; }; @@ -13243,87 +13223,25 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; }; - 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; - }; + // #18: delegate to cgstructlitfill so a nested struct- + // typed structlit value recurses instead of dropping + // its trailing bytes. mode=2 (DST_GLOBAL) reloads BX + // via LEAQ bn(SB) before zero-fill and before every + // field store. + 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); + cgstructlitfill(c, ssi, n.rhs, 2, 0, bn, + fi.foff, ssz); + return; }; + }; if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_IDENT @@ -13702,6 +13620,14 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; }; + // #18: delegate to cgstructlitfill so a nested struct- + // typed structlit value recurses instead of dropping + // its trailing bytes. mode picks the dst flavor: + // ptrroot → mode=1 (DST_PTR_LOCAL), reload BX from + // rootoff(BP). + // isglobal → mode=2 (DST_GLOBAL), reload BX via + // LEAQ rootname(SB). + // else → mode=0 (DST_BP), direct BP-rel, no reload. if (n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT && leaffi.tnode != nil @@ -13712,131 +13638,19 @@ fn cgassign(c: *cgen, n: *node) void = { // 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 dmode: i32 = 0; + let ddisp: i32 = rootoff + totaloff; + if (ptrroot) { + dmode = 1; + ddisp = totaloff; }; - 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; + if (isglobal) { + dmode = 2; + ddisp = totaloff; }; + cgstructlitfill(c, lsi, n.rhs, + dmode, rootoff, rootname, + ddisp, lsz); return; }; }; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index a62547e8..1a015503 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -3838,6 +3838,11 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; }; + // #18: delegate to cgstructlitfill so a nested struct- + // typed structlit value recurses instead of dropping + // its trailing bytes. mode=1 (DST_PTR_LOCAL) reloads BX + // from lc.off(BP) before zero-fill and before every + // field store. if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT @@ -3847,75 +3852,8 @@ fn cgassign(c: *cgen, n: *node) void = { 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; - }; + cgstructlitfill(c, ssi, n.rhs, 1, lc.off, "", + fi.foff, ssz); return; }; }; @@ -4131,6 +4069,10 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; }; + // #18: delegate to cgstructlitfill so a nested struct- + // typed structlit value recurses instead of dropping + // its trailing bytes. mode=0 (DST_BP) — direct BP-rel, + // no BX reload. if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT @@ -4140,66 +4082,8 @@ fn cgassign(c: *cgen, n: *node) void = { 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; - }; + cgstructlitfill(c, ssi, n.rhs, 0, 0, "", + lc.off + fi.foff, ssz); return; }; }; @@ -4438,87 +4322,25 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; }; - 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; - }; + // #18: delegate to cgstructlitfill so a nested struct- + // typed structlit value recurses instead of dropping + // its trailing bytes. mode=2 (DST_GLOBAL) reloads BX + // via LEAQ bn(SB) before zero-fill and before every + // field store. + 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); + cgstructlitfill(c, ssi, n.rhs, 2, 0, bn, + fi.foff, ssz); + return; }; + }; if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_IDENT @@ -4897,6 +4719,14 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; }; + // #18: delegate to cgstructlitfill so a nested struct- + // typed structlit value recurses instead of dropping + // its trailing bytes. mode picks the dst flavor: + // ptrroot → mode=1 (DST_PTR_LOCAL), reload BX from + // rootoff(BP). + // isglobal → mode=2 (DST_GLOBAL), reload BX via + // LEAQ rootname(SB). + // else → mode=0 (DST_BP), direct BP-rel, no reload. if (n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT && leaffi.tnode != nil @@ -4907,131 +4737,19 @@ fn cgassign(c: *cgen, n: *node) void = { // 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 dmode: i32 = 0; + let ddisp: i32 = rootoff + totaloff; + if (ptrroot) { + dmode = 1; + ddisp = totaloff; }; - 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; + if (isglobal) { + dmode = 2; + ddisp = totaloff; }; + cgstructlitfill(c, lsi, n.rhs, + dmode, rootoff, rootname, + ddisp, lsz); return; }; }; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 2be9ad4e..b7eca4d0 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -2784,59 +2784,113 @@ export fn dotchainresolve(c: *cgen, n: *node, return false; }; -// cgstructlitfillbp — fill a struct-typed slot from an N_STRUCTLIT -// value into BP-relative memory at `bpoff`. Mirror of cstage cgen.c's -// cg_structlit_fill_bp. Used by cglet, cgreturn N_STRUCTLIT, and -// cgassign N_IDENT-lhs N_STRUCTLIT sites where the dst is BP- -// relative. +// cgstructlitfill — fill a struct-typed slot from an N_STRUCTLIT +// value into one of three destination flavors. Mirror of cstage +// cgen.c's cg_structlit_fill. Used by cglet, cgreturn N_STRUCTLIT, +// cgassign N_IDENT-lhs N_STRUCTLIT (BP-rel) AND cgassign N_DOT-lhs +// N_STRUCTLIT (BP-rel / via *struct local / via struct global) at +// single-dot and chained-dot sites. // -// The inline field-walk previously did `cgexpr(field.lhs); store AX -// sized`. For struct-typed fields whose value is itself a nested -// N_STRUCTLIT, cgexpr has no whole-struct-in-register convention — -// it lands AX = first qword and the trailing bytes silently stay -// zero. Selfhost source used write-by-field as a workaround; this -// helper recurses cleanly so the workaround is no longer required. +// Destination modes: +// 0 = DST_BP — base = BP, no reload. Stores at disp+i(BP). +// srcoff/srcname unused. +// 1 = DST_PTR_LOCAL — base = BX, reloaded from srcoff(BP) before +// the ELLIPSIS zero-fill loop and before EVERY +// field store (cgexpr clobbers BX between +// fields). Stores at disp+i(BX). srcname +// unused. +// 2 = DST_GLOBAL — base = BX, reloaded via `LEAQ srcname(SB), +// BX` with the same cadence as DST_PTR_LOCAL. +// srcoff unused. // -// Sister branches (cgassign single-dot / via_ptr / global structlit -// walks) keep their inline field-walk in v1 since their addressing -// has additional BX-reload concerns; nested-STRUCTLIT silent-zero -// still drops on those paths — separate follow-up task. +// Param semantics (locked in here so the recursion contract is +// clear): +// - `disp` is the per-recursion accumulator — grows by `fi.foff` +// as we descend into a nested struct-typed structlit field. +// - `srcoff` (DST_PTR_LOCAL) and `srcname` (DST_GLOBAL) are +// *constant* across the whole call tree — they identify the +// root dst, which doesn't change with depth. +// - `totsize` is also constant; pass the natural size for dot +// sites (structnaturalsize) and si.totsize for BP-rel sites, +// matching each site's pre-#18 zero-fill bound. // -// Output is byte-identical to the prior inline code for any input -// that has no nested-STRUCTLIT field (the only shape selfhost source -// actually compiles today), preserving 995_self_rebuild byte- -// identity. +// Why a helper? The inline field-walk previously did +// `cgexpr(field.lhs); store AX sized`. For struct-typed fields whose +// value is itself a nested N_STRUCTLIT, cgexpr has no whole-struct- +// in-register convention — it lands AX = first qword and the +// trailing bytes silently stay zero. #17 fixed the BP-rel sites; +// #18 extends the same recursion to the four cgassign N_DOT-lhs +// structlit walks (single-dot via_ptr/global/local + chained +// depth>=2). +// +// The non-BP modes emit a redundant BX reload at the start of each +// recursive nested zero-fill / each recursive scalar store — this is +// correctness-by-construction (BX is always freshly loaded right +// before use), and the redundancy only fires on the nested-STRUCTLIT +// shapes that didn't compile before. Byte-identity for the no- +// nested case (the only shape selfhost source uses today) is +// preserved because the existing inline code's reload-before-each- +// store pattern matches the helper's per-store reload exactly. // // Graduation note (task #13): the scalar store currently uses the // explicit {1→MOVB, 4→MOVL, else MOVQ} dispatch to match cstage // byte-identically — cstage hasn't yet learned MOVW for fsz==2. Once // #13 aligns both stages, the dispatch can switch to fieldstoreop // which already returns MOVW where appropriate. -fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *node, bpoff: i32) void = { +fn cgstructlitfill(c: *cgen, si: *structinfo, lit: *node, + mode: i32, srcoff: i32, srcname: str, + disp: i32, totsize: i32) void = { if (si == nil) { return; }; + let basereg: str = "BP"; + if (mode != 0) { basereg = "BX"; }; if (lit.op == tkind.TK_ELLIPSIS) { // `..., ...` autofill — zero the entire slot first so - // unmentioned fields read as 0. Uses si.totsize (slot-padded) - // to match the inline pre-#17 pattern. - let total: i32 = si.totsize; + // unmentioned fields read as 0. Sized stores: 8/4/1. For + // non-BP modes, reload BX once before the loop (cgexpr-free + // region between iterations, so one reload is enough). emitline("\tXORQ\tAX, AX\n"); + if (mode == 1) { + emitline("\tMOVQ\t"); + emitoff(srcoff: i64); + emitline("(BP), BX\n"); + }; + if (mode == 2) { + emitline("\tLEAQ\t"); + emitsymname(c, srcname); + emitline("(SB), BX\n"); + }; let zi: i32 = 0; - for (zi + 8 <= total) { + for (zi + 8 <= totsize) { emitline("\tMOVQ\tAX, "); - emitoff((bpoff + zi): i64); - emitline("(BP)\n"); + if (mode == 0) { + emitoff((disp + zi): i64); + emitline("(BP)\n"); + } else { + emitdispreg((disp + zi): i64, basereg); + emitline("\n"); + }; zi += 8; }; - for (zi + 4 <= total) { + for (zi + 4 <= totsize) { emitline("\tMOVL\tAX, "); - emitoff((bpoff + zi): i64); - emitline("(BP)\n"); + if (mode == 0) { + emitoff((disp + zi): i64); + emitline("(BP)\n"); + } else { + emitdispreg((disp + zi): i64, basereg); + emitline("\n"); + }; zi += 4; }; - for (zi < total) { + for (zi < totsize) { emitline("\tMOVB\tAX, "); - emitoff((bpoff + zi): i64); - emitline("(BP)\n"); + if (mode == 0) { + emitoff((disp + zi): i64); + emitline("(BP)\n"); + } else { + emitdispreg((disp + zi): i64, basereg); + emitline("\n"); + }; zi += 1; }; }; @@ -2851,23 +2905,29 @@ fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *node, bpoff: i32) void = { // Tagged-union field: delegate to the shared // widening writer (handles str/scalar/struct // literal/ident payload + tagged-subset tag - // remap). Mirrors cstage cg_structlit_fill_bp - // and the pre-existing wwstage cgreturn - // structlit dispatch; cglet's pre-#17 inline - // lacked this branch but selfhost never - // tripped it (no tagged-fields-in-let- - // structlit in tree). + // remap). For non-BP modes, reload BX first so + // the widener sees a valid base reg. if (istaggedtype(c, fi.tnode)) { + if (mode == 1) { + emitline("\tMOVQ\t"); + emitoff(srcoff: i64); + emitline("(BP), BX\n"); + }; + if (mode == 2) { + emitline("\tLEAQ\t"); + emitsymname(c, srcname); + emitline("(SB), BX\n"); + }; cgwidentaggedstore(c, fi.tnode, - fieldnode.lhs, "BP", - bpoff + fi.foff, fi.fsz); + fieldnode.lhs, basereg, + disp + fi.foff, fi.fsz); fi = nil; } else { // Nested struct-typed structlit value: look up // the inner struct's metadata and recurse at the - // field's offset. Pre-#17 the cgexpr-then-store - // below would land AX = first qword and the rest - // silently stayed zero. + // field's offset. Pre-#17/#18 the cgexpr-then- + // store below would land AX = first qword and + // the rest silently stayed zero. let nested: bool = false; if (fieldnode.lhs != nil) { if (fieldnode.lhs.kind == nkind.N_STRUCTLIT) { @@ -2876,9 +2936,17 @@ fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *node, bpoff: i32) void = { if (primsize(fi.tnode.str) == 0) { let isi: *structinfo = structlookup(c, fi.tnode.str); if (isi != nil) { - cgstructlitfillbp(c, isi, + // Nested fill: pick the size + // discipline matching the outer + // site — dot sites pass natural + // size, BP-rel sites pass + // totsize. Mirror it. + let inner_tot: i32 = isi.totsize; + if (mode != 0) { inner_tot = structnaturalsize(isi); }; + cgstructlitfill(c, isi, fieldnode.lhs, - bpoff + fi.foff); + mode, srcoff, srcname, + disp + fi.foff, inner_tot); nested = true; }; }; @@ -2890,24 +2958,39 @@ fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *node, bpoff: i32) void = { fi = nil; } else { cgexpr(c, fieldnode.lhs); + // For non-BP modes, cgexpr just clobbered + // BX; reload it before the store. + if (mode == 1) { + emitline("\tMOVQ\t"); + emitoff(srcoff: i64); + emitline("(BP), BX\n"); + }; + if (mode == 2) { + emitline("\tLEAQ\t"); + emitsymname(c, srcname); + emitline("(SB), BX\n"); + }; if (isfloattype(c, fi.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\tX0, "); - emitoff((bpoff + fi.foff): i64); - emitline("(BP)\n"); + if (mode == 0) { + emitoff((disp + fi.foff): i64); + emitline("(BP)\n"); + } else { + emitdispreg((disp + fi.foff): i64, basereg); + emitline("\n"); + }; fi = nil; } else { // Explicit {1→MOVB, 4→MOVL, else MOVQ} // dispatch (not fieldstoreop) to match - // cstage cg_structlit_fill_bp byte- - // identically. wwstage's fieldstoreop - // would return MOVW for fsz==2 which - // cstage doesn't emit — tracked as task - // #13. Until that lands, the helper - // emits the cstage shape. + // cstage byte-identically. wwstage's + // fieldstoreop would return MOVW for + // fsz==2 which cstage doesn't emit — + // tracked as task #13. let fsz: i32 = fi.fsz; let op: str = "MOVQ"; if (fsz == 1) { op = "MOVB"; }; @@ -2915,8 +2998,13 @@ fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *node, bpoff: i32) void = { emitline("\t"); emitline(op); emitline("\tAX, "); - emitoff((bpoff + fi.foff): i64); - emitline("(BP)\n"); + if (mode == 0) { + emitoff((disp + fi.foff): i64); + emitline("(BP)\n"); + } else { + emitdispreg((disp + fi.foff): i64, basereg); + emitline("\n"); + }; fi = nil; }; }; @@ -2929,3 +3017,11 @@ fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *node, bpoff: i32) void = { fieldnode = fieldnode.next; }; }; + +// Thin wrapper preserving the BP-rel call shape used by cglet, +// cgreturn, and cgassign N_IDENT-lhs N_STRUCTLIT. Byte-identical to +// the pre-#18 cgstructlitfillbp. +fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *node, bpoff: i32) void = { + if (si == nil) { return; }; + cgstructlitfill(c, si, lit, 0, 0, "", bpoff, si.totsize); +}; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 0bc81c08..6b416dfa 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -8656,59 +8656,113 @@ export fn dotchainresolve(c: *cgen, n: *node, return false; }; -// cgstructlitfillbp — fill a struct-typed slot from an N_STRUCTLIT -// value into BP-relative memory at `bpoff`. Mirror of cstage cgen.c's -// cg_structlit_fill_bp. Used by cglet, cgreturn N_STRUCTLIT, and -// cgassign N_IDENT-lhs N_STRUCTLIT sites where the dst is BP- -// relative. +// cgstructlitfill — fill a struct-typed slot from an N_STRUCTLIT +// value into one of three destination flavors. Mirror of cstage +// cgen.c's cg_structlit_fill. Used by cglet, cgreturn N_STRUCTLIT, +// cgassign N_IDENT-lhs N_STRUCTLIT (BP-rel) AND cgassign N_DOT-lhs +// N_STRUCTLIT (BP-rel / via *struct local / via struct global) at +// single-dot and chained-dot sites. // -// The inline field-walk previously did `cgexpr(field.lhs); store AX -// sized`. For struct-typed fields whose value is itself a nested -// N_STRUCTLIT, cgexpr has no whole-struct-in-register convention — -// it lands AX = first qword and the trailing bytes silently stay -// zero. Selfhost source used write-by-field as a workaround; this -// helper recurses cleanly so the workaround is no longer required. +// Destination modes: +// 0 = DST_BP — base = BP, no reload. Stores at disp+i(BP). +// srcoff/srcname unused. +// 1 = DST_PTR_LOCAL — base = BX, reloaded from srcoff(BP) before +// the ELLIPSIS zero-fill loop and before EVERY +// field store (cgexpr clobbers BX between +// fields). Stores at disp+i(BX). srcname +// unused. +// 2 = DST_GLOBAL — base = BX, reloaded via `LEAQ srcname(SB), +// BX` with the same cadence as DST_PTR_LOCAL. +// srcoff unused. // -// Sister branches (cgassign single-dot / via_ptr / global structlit -// walks) keep their inline field-walk in v1 since their addressing -// has additional BX-reload concerns; nested-STRUCTLIT silent-zero -// still drops on those paths — separate follow-up task. +// Param semantics (locked in here so the recursion contract is +// clear): +// - `disp` is the per-recursion accumulator — grows by `fi.foff` +// as we descend into a nested struct-typed structlit field. +// - `srcoff` (DST_PTR_LOCAL) and `srcname` (DST_GLOBAL) are +// *constant* across the whole call tree — they identify the +// root dst, which doesn't change with depth. +// - `totsize` is also constant; pass the natural size for dot +// sites (structnaturalsize) and si.totsize for BP-rel sites, +// matching each site's pre-#18 zero-fill bound. // -// Output is byte-identical to the prior inline code for any input -// that has no nested-STRUCTLIT field (the only shape selfhost source -// actually compiles today), preserving 995_self_rebuild byte- -// identity. +// Why a helper? The inline field-walk previously did +// `cgexpr(field.lhs); store AX sized`. For struct-typed fields whose +// value is itself a nested N_STRUCTLIT, cgexpr has no whole-struct- +// in-register convention — it lands AX = first qword and the +// trailing bytes silently stay zero. #17 fixed the BP-rel sites; +// #18 extends the same recursion to the four cgassign N_DOT-lhs +// structlit walks (single-dot via_ptr/global/local + chained +// depth>=2). +// +// The non-BP modes emit a redundant BX reload at the start of each +// recursive nested zero-fill / each recursive scalar store — this is +// correctness-by-construction (BX is always freshly loaded right +// before use), and the redundancy only fires on the nested-STRUCTLIT +// shapes that didn't compile before. Byte-identity for the no- +// nested case (the only shape selfhost source uses today) is +// preserved because the existing inline code's reload-before-each- +// store pattern matches the helper's per-store reload exactly. // // Graduation note (task #13): the scalar store currently uses the // explicit {1→MOVB, 4→MOVL, else MOVQ} dispatch to match cstage // byte-identically — cstage hasn't yet learned MOVW for fsz==2. Once // #13 aligns both stages, the dispatch can switch to fieldstoreop // which already returns MOVW where appropriate. -fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *node, bpoff: i32) void = { +fn cgstructlitfill(c: *cgen, si: *structinfo, lit: *node, + mode: i32, srcoff: i32, srcname: str, + disp: i32, totsize: i32) void = { if (si == nil) { return; }; + let basereg: str = "BP"; + if (mode != 0) { basereg = "BX"; }; if (lit.op == tkind.TK_ELLIPSIS) { // `..., ...` autofill — zero the entire slot first so - // unmentioned fields read as 0. Uses si.totsize (slot-padded) - // to match the inline pre-#17 pattern. - let total: i32 = si.totsize; + // unmentioned fields read as 0. Sized stores: 8/4/1. For + // non-BP modes, reload BX once before the loop (cgexpr-free + // region between iterations, so one reload is enough). emitline("\tXORQ\tAX, AX\n"); + if (mode == 1) { + emitline("\tMOVQ\t"); + emitoff(srcoff: i64); + emitline("(BP), BX\n"); + }; + if (mode == 2) { + emitline("\tLEAQ\t"); + emitsymname(c, srcname); + emitline("(SB), BX\n"); + }; let zi: i32 = 0; - for (zi + 8 <= total) { + for (zi + 8 <= totsize) { emitline("\tMOVQ\tAX, "); - emitoff((bpoff + zi): i64); - emitline("(BP)\n"); + if (mode == 0) { + emitoff((disp + zi): i64); + emitline("(BP)\n"); + } else { + emitdispreg((disp + zi): i64, basereg); + emitline("\n"); + }; zi += 8; }; - for (zi + 4 <= total) { + for (zi + 4 <= totsize) { emitline("\tMOVL\tAX, "); - emitoff((bpoff + zi): i64); - emitline("(BP)\n"); + if (mode == 0) { + emitoff((disp + zi): i64); + emitline("(BP)\n"); + } else { + emitdispreg((disp + zi): i64, basereg); + emitline("\n"); + }; zi += 4; }; - for (zi < total) { + for (zi < totsize) { emitline("\tMOVB\tAX, "); - emitoff((bpoff + zi): i64); - emitline("(BP)\n"); + if (mode == 0) { + emitoff((disp + zi): i64); + emitline("(BP)\n"); + } else { + emitdispreg((disp + zi): i64, basereg); + emitline("\n"); + }; zi += 1; }; }; @@ -8723,23 +8777,29 @@ fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *node, bpoff: i32) void = { // Tagged-union field: delegate to the shared // widening writer (handles str/scalar/struct // literal/ident payload + tagged-subset tag - // remap). Mirrors cstage cg_structlit_fill_bp - // and the pre-existing wwstage cgreturn - // structlit dispatch; cglet's pre-#17 inline - // lacked this branch but selfhost never - // tripped it (no tagged-fields-in-let- - // structlit in tree). + // remap). For non-BP modes, reload BX first so + // the widener sees a valid base reg. if (istaggedtype(c, fi.tnode)) { + if (mode == 1) { + emitline("\tMOVQ\t"); + emitoff(srcoff: i64); + emitline("(BP), BX\n"); + }; + if (mode == 2) { + emitline("\tLEAQ\t"); + emitsymname(c, srcname); + emitline("(SB), BX\n"); + }; cgwidentaggedstore(c, fi.tnode, - fieldnode.lhs, "BP", - bpoff + fi.foff, fi.fsz); + fieldnode.lhs, basereg, + disp + fi.foff, fi.fsz); fi = nil; } else { // Nested struct-typed structlit value: look up // the inner struct's metadata and recurse at the - // field's offset. Pre-#17 the cgexpr-then-store - // below would land AX = first qword and the rest - // silently stayed zero. + // field's offset. Pre-#17/#18 the cgexpr-then- + // store below would land AX = first qword and + // the rest silently stayed zero. let nested: bool = false; if (fieldnode.lhs != nil) { if (fieldnode.lhs.kind == nkind.N_STRUCTLIT) { @@ -8748,9 +8808,17 @@ fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *node, bpoff: i32) void = { if (primsize(fi.tnode.str) == 0) { let isi: *structinfo = structlookup(c, fi.tnode.str); if (isi != nil) { - cgstructlitfillbp(c, isi, + // Nested fill: pick the size + // discipline matching the outer + // site — dot sites pass natural + // size, BP-rel sites pass + // totsize. Mirror it. + let inner_tot: i32 = isi.totsize; + if (mode != 0) { inner_tot = structnaturalsize(isi); }; + cgstructlitfill(c, isi, fieldnode.lhs, - bpoff + fi.foff); + mode, srcoff, srcname, + disp + fi.foff, inner_tot); nested = true; }; }; @@ -8762,24 +8830,39 @@ fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *node, bpoff: i32) void = { fi = nil; } else { cgexpr(c, fieldnode.lhs); + // For non-BP modes, cgexpr just clobbered + // BX; reload it before the store. + if (mode == 1) { + emitline("\tMOVQ\t"); + emitoff(srcoff: i64); + emitline("(BP), BX\n"); + }; + if (mode == 2) { + emitline("\tLEAQ\t"); + emitsymname(c, srcname); + emitline("(SB), BX\n"); + }; if (isfloattype(c, fi.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\tX0, "); - emitoff((bpoff + fi.foff): i64); - emitline("(BP)\n"); + if (mode == 0) { + emitoff((disp + fi.foff): i64); + emitline("(BP)\n"); + } else { + emitdispreg((disp + fi.foff): i64, basereg); + emitline("\n"); + }; fi = nil; } else { // Explicit {1→MOVB, 4→MOVL, else MOVQ} // dispatch (not fieldstoreop) to match - // cstage cg_structlit_fill_bp byte- - // identically. wwstage's fieldstoreop - // would return MOVW for fsz==2 which - // cstage doesn't emit — tracked as task - // #13. Until that lands, the helper - // emits the cstage shape. + // cstage byte-identically. wwstage's + // fieldstoreop would return MOVW for + // fsz==2 which cstage doesn't emit — + // tracked as task #13. let fsz: i32 = fi.fsz; let op: str = "MOVQ"; if (fsz == 1) { op = "MOVB"; }; @@ -8787,8 +8870,13 @@ fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *node, bpoff: i32) void = { emitline("\t"); emitline(op); emitline("\tAX, "); - emitoff((bpoff + fi.foff): i64); - emitline("(BP)\n"); + if (mode == 0) { + emitoff((disp + fi.foff): i64); + emitline("(BP)\n"); + } else { + emitdispreg((disp + fi.foff): i64, basereg); + emitline("\n"); + }; fi = nil; }; }; @@ -8802,6 +8890,14 @@ fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *node, bpoff: i32) void = { }; }; +// Thin wrapper preserving the BP-rel call shape used by cglet, +// cgreturn, and cgassign N_IDENT-lhs N_STRUCTLIT. Byte-identical to +// the pre-#18 cgstructlitfillbp. +fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *node, bpoff: i32) void = { + if (si == nil) { return; }; + cgstructlitfill(c, si, lit, 0, 0, "", bpoff, si.totsize); +}; + // MODULE: wcc // selfhost/cmd/wcc/cgenexpr.ww — split out of cgen.ww. // @@ -12643,6 +12739,11 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; }; + // #18: delegate to cgstructlitfill so a nested struct- + // typed structlit value recurses instead of dropping + // its trailing bytes. mode=1 (DST_PTR_LOCAL) reloads BX + // from lc.off(BP) before zero-fill and before every + // field store. if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT @@ -12652,75 +12753,8 @@ fn cgassign(c: *cgen, n: *node) void = { 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; - }; + cgstructlitfill(c, ssi, n.rhs, 1, lc.off, "", + fi.foff, ssz); return; }; }; @@ -12936,6 +12970,10 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; }; + // #18: delegate to cgstructlitfill so a nested struct- + // typed structlit value recurses instead of dropping + // its trailing bytes. mode=0 (DST_BP) — direct BP-rel, + // no BX reload. if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT @@ -12945,66 +12983,8 @@ fn cgassign(c: *cgen, n: *node) void = { 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; - }; + cgstructlitfill(c, ssi, n.rhs, 0, 0, "", + lc.off + fi.foff, ssz); return; }; }; @@ -13243,87 +13223,25 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; }; - 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; - }; + // #18: delegate to cgstructlitfill so a nested struct- + // typed structlit value recurses instead of dropping + // its trailing bytes. mode=2 (DST_GLOBAL) reloads BX + // via LEAQ bn(SB) before zero-fill and before every + // field store. + 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); + cgstructlitfill(c, ssi, n.rhs, 2, 0, bn, + fi.foff, ssz); + return; }; + }; if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_IDENT @@ -13702,6 +13620,14 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; }; + // #18: delegate to cgstructlitfill so a nested struct- + // typed structlit value recurses instead of dropping + // its trailing bytes. mode picks the dst flavor: + // ptrroot → mode=1 (DST_PTR_LOCAL), reload BX from + // rootoff(BP). + // isglobal → mode=2 (DST_GLOBAL), reload BX via + // LEAQ rootname(SB). + // else → mode=0 (DST_BP), direct BP-rel, no reload. if (n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT && leaffi.tnode != nil @@ -13712,131 +13638,19 @@ fn cgassign(c: *cgen, n: *node) void = { // 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 dmode: i32 = 0; + let ddisp: i32 = rootoff + totaloff; + if (ptrroot) { + dmode = 1; + ddisp = totaloff; }; - 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; + if (isglobal) { + dmode = 2; + ddisp = totaloff; }; + cgstructlitfill(c, lsi, n.rhs, + dmode, rootoff, rootname, + ddisp, lsz); return; }; }; diff --git a/test/wcc/704_dot_structlit.c b/test/wcc/704_dot_structlit.c new file mode 100644 index 00000000..d350dab7 --- /dev/null +++ b/test/wcc/704_dot_structlit.c @@ -0,0 +1,342 @@ +/* + * 704_dot_structlit — silent zero of nested STRUCTLIT field in + * cgassign N_DOT-lhs (task #18). + * + * Sister bug to #17. #17 fixed the BP-relative sites (N_LET, + * N_ASSIGN N_IDENT-lhs, N_RETURN) via the cg_structlit_fill_bp / + * cgstructlitfillbp helper. The N_ASSIGN N_DOT-lhs structlit walks + * were left inline because their addressing has extra BX-reload + * concerns (the dst addr is a *struct local or a struct global, + * loaded into BX, and cgexpr clobbers BX between fields). All four + * dot-flavors still emitted `cgexpr(field_value); store-AX-sized` + * for the rhs structlit's fields — and for a struct-typed field + * whose value is itself an N_STRUCTLIT, that lands only the first + * qword while the trailing bytes silently stay zero. + * + * Affected dot-flavors (each with the same silent-zero shape): + * 1. single-dot local-base `o.f = outer { i = inner{...}, ... }` + * 2. single-dot ptr-base `p.f = outer { i = inner{...}, ... }` + * 3. single-dot global-base `g.f = outer { i = inner{...}, ... }` + * 4. chained-dot local `o.x.y = outer { i = inner{...}, ... }` + * 5. chained-dot ptrroot `p.x.y = outer { i = inner{...}, ... }` + * 6. chained-dot global `g.x.y = outer { i = inner{...}, ... }` + * + * Fix (#18): the helper is extended into cg_structlit_fill / + * cgstructlitfill that takes a destination context (mode, srcoff, + * name, disp). The old BP-rel wrapper is preserved byte-identically. + * The four dot-flavor sites in each stage now delegate to the + * helper, which recurses on nested struct-typed N_STRUCTLIT values + * at `disp + foff` and reloads BX before each store for the non-BP + * modes. 995_self_rebuild byte-identity is the load-bearing proof + * that the no-nested case is byte-identical to the pre-#18 inline + * walks. + * + * Each row pins: + * - cstage value correctness (process exit code). + * - wwstage value correctness (when ww_ww exists). + * - cstage vs wwstage byte-identical .s output (catches drift). + */ +#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[] = { + /* single-dot local-base: `h.f = outer { i = inner{...}, ... }`. + * Pre-fix: h.f.i.a landed (first qword via cgexpr) but h.f.i.b + * silently stayed 0. Want: 7+8+12 = 27. */ + { "dot_local_lit_nested", + "type inner = struct { a: i64, b: i64 };\n" + "type outer = struct { i: inner, t: i64 };\n" + "type holder = struct { f: outer };\n" + "fn main() i32 = {\n" + " let h: holder;\n" + " h.f = outer { i = inner { a = 7i64, b = 8i64 }, t = 12i64 };\n" + " return (h.f.i.a + h.f.i.b + h.f.t): i32;\n" + "};\n", + 27 }, + /* single-dot local-base, 3-deep recursion (outer.m.in). Pins + * that the disp accumulator threads `foff + middle_foff + + * leaf_foff` correctly. Want: 1+2+3+4+9 = 19. */ + { "dot_local_lit_3deep", + "type leaf = struct { a: i64, b: i64 };\n" + "type middle = struct { in: leaf, t: i64 };\n" + "type outer = struct { m: middle, x: i64 };\n" + "type holder = struct { f: outer, pad: i64 };\n" + "fn main() i32 = {\n" + " let h: holder;\n" + " h.f = outer {\n" + " m = middle {\n" + " in = leaf { a = 1i64, b = 2i64 },\n" + " t = 3i64\n" + " },\n" + " x = 4i64\n" + " };\n" + " h.pad = 9i64;\n" + " return (h.f.m.in.a + h.f.m.in.b + h.f.m.t + h.f.x + h.pad): i32;\n" + "};\n", + 19 }, + /* single-dot ptr-base (auto-deref): `p.f = outer { i = inner{...}, ... }`. + * Exercises DST_PTR_LOCAL mode — helper reloads BX from + * srcoff(BP) before zero-fill and before every store. Want: + * 9+11+30 = 50. */ + { "dot_ptr_lit_nested", + "type inner = struct { a: i64, b: i64 };\n" + "type outer = struct { i: inner, t: i64 };\n" + "type holder = struct { f: outer };\n" + "fn main() i32 = {\n" + " let h: holder;\n" + " let p: *holder = &h;\n" + " p.f = outer { i = inner { a = 9i64, b = 11i64 }, t = 30i64 };\n" + " return (p.f.i.a + p.f.i.b + p.f.t): i32;\n" + "};\n", + 50 }, + /* single-dot global-base: `g.f = outer { i = inner{...}, ... }`. + * Exercises DST_GLOBAL mode — helper reloads BX via LEAQ + * name(SB) before zero-fill and before every store. `let g: T;` + * (no init) per the task #17 module-name-mangle workaround + * pattern. Want: 13+17+25 = 55. */ + { "dot_global_lit_nested", + "type inner = struct { a: i64, b: i64 };\n" + "type outer = struct { i: inner, t: i64 };\n" + "type holder = struct { f: outer };\n" + "let g: holder;\n" + "fn main() i32 = {\n" + " g.f = outer { i = inner { a = 13i64, b = 17i64 }, t = 25i64 };\n" + " return (g.f.i.a + g.f.i.b + g.f.t): i32;\n" + "};\n", + 55 }, + /* chained-dot local (depth-2 lhs): `h.mid.f = outer { i = + * inner{...}, ... }`. Exercises the chained-DOT walker with the + * non-via_cx (local-through-chain) path — disp = base_disp + + * total_off, no BX reload. Want: 4+5+13 = 22. */ + { "dot_chain_lit_nested", + "type inner = struct { a: i64, b: i64 };\n" + "type outer = struct { i: inner, t: i64 };\n" + "type holder = struct { f: outer };\n" + "type h2 = struct { mid: holder, pad: i64 };\n" + "fn main() i32 = {\n" + " let h: h2;\n" + " h.mid.f = outer { i = inner { a = 4i64, b = 5i64 }, t = 13i64 };\n" + " return (h.mid.f.i.a + h.mid.f.i.b + h.mid.f.t): i32;\n" + "};\n", + 22 }, + /* chained-dot ptrroot (depth-2 lhs through *struct root): + * `p.mid.f = outer { i = inner{...}, ... }`. Exercises the + * chained-DOT walker with ptrroot=1 — helper mode=1, reloads BX + * from rootoff(BP). Want: 6+7+14 = 27. */ + { "dot_chain_ptr_lit_nested", + "type inner = struct { a: i64, b: i64 };\n" + "type outer = struct { i: inner, t: i64 };\n" + "type holder = struct { f: outer };\n" + "type h2 = struct { mid: holder, pad: i64 };\n" + "fn main() i32 = {\n" + " let h: h2;\n" + " let p: *h2 = &h;\n" + " p.mid.f = outer { i = inner { a = 6i64, b = 7i64 }, t = 14i64 };\n" + " return (p.mid.f.i.a + p.mid.f.i.b + p.mid.f.t): i32;\n" + "};\n", + 27 }, + /* chained-dot global (depth-2 lhs through struct global): + * `gg.mid.f = outer { i = inner{...}, ... }`. Exercises the + * chained-DOT walker with isglobal=1 — helper mode=2, reloads + * BX via LEAQ gg(SB). Want: 10+20+31 = 61. */ + { "dot_chain_global_lit_nested", + "type inner = struct { a: i64, b: i64 };\n" + "type outer = struct { i: inner, t: i64 };\n" + "type holder = struct { f: outer };\n" + "type h2 = struct { mid: holder, pad: i64 };\n" + "let gg: h2;\n" + "fn main() i32 = {\n" + " gg.mid.f = outer { i = inner { a = 10i64, b = 20i64 }, t = 31i64 };\n" + " return (gg.mid.f.i.a + gg.mid.f.i.b + gg.mid.f.t): i32;\n" + "};\n", + 61 }, + /* single-dot ptr-base 3-deep — pins disp threading through the + * helper's recursion AND through DST_PTR_LOCAL reloads. Want: + * 1+2+3+4 = 10. */ + { "dot_ptr_lit_3deep", + "type leaf = struct { a: i64, b: i64 };\n" + "type middle = struct { in: leaf, t: i64 };\n" + "type outer = struct { m: middle, x: i64 };\n" + "type holder = struct { f: outer };\n" + "fn main() i32 = {\n" + " let h: holder;\n" + " let p: *holder = &h;\n" + " p.f = outer {\n" + " m = middle {\n" + " in = leaf { a = 1i64, b = 2i64 },\n" + " t = 3i64\n" + " },\n" + " x = 4i64\n" + " };\n" + " return (p.f.m.in.a + p.f.m.in.b + p.f.m.t + p.f.x): i32;\n" + "};\n", + 10 }, +}; + +/* run_driver — compile r->src via the given driver and exec; return + * the process exit code. Mirror of 703. */ +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/wcds_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/wcds_%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. Mirror of 703. */ +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/wcds_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/wcds_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/wcds_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; + + for (int d = 0; drivers[d].name; d++) { + if (drivers[d].gated_on_existence + && access(drivers[d].path, X_OK) != 0) { + fprintf(stderr, "dot_structlit: 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, + "dot_structlit[%s][%s]: exit=%d want=%d\n", + drivers[d].name, rows[i].label, + got, rows[i].want); + fail++; + } + } + } + + 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, + "dot_structlit: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("dot_structlit: %d/%d ok\n", total, total); + return 0; +}