From 9d03e0288176bf17782c09c0e67eea9290af57a3 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 15 May 2026 18:14:33 +0900 Subject: [PATCH] cstage+selfhost+test: fix nested STRUCTLIT silent zero in BP-relative fills MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-existing landmine surfaced by #5. For a struct literal whose field value is itself an N_STRUCTLIT of a struct-typed field, the inline field-walk did `cgexpr(field.lhs); store-AX-sized`. cgexpr has no whole-struct-in-register convention, so the nested literal landed AX = first qword and the trailing bytes silently stayed zero (or stack garbage). Three BP-relative sites in each stage hit it: N_LET, N_ASSIGN N_IDENT-lhs, and N_RETURN N_STRUCTLIT. Fix: shared cg_structlit_fill_bp (cstage) / cgstructlitfillbp (wwstage) helper handles TK_ELLIPSIS autofill, tagged-field widening, float vs scalar store dispatch, AND recurses on struct-typed N_STRUCTLIT field values at bp_off + field_off. All 3 sites in each stage now call the helper instead of the inline walk. Scalar store dispatch is the explicit {1->MOVB, 4->MOVL, else MOVQ} shape (not fieldstoreop, which would emit MOVW for fsz==2) to stay byte-identical with cstage pending task #13. Sister N_ASSIGN N_DOT structlit walks (via_ptr / global / BP-relative-through-N_DOT) keep their inline walk and still drop nested-STRUCTLIT silently — tracked as task #18. 703 covers 6 rows: let_nested_i64, let_nested_3deep, let_nested_i32, let_nested_middle (i64; switch to i32 once #15 lands), assign_ident_nested, return_nested. 995_self_rebuild byte-identity preserved. --- Makefile | 7 + cmd/w6c/cgen.c | 304 ++++++++++-------------- selfhost/cmd/w6c/main.combined.ww | 340 ++++++++++++++------------- selfhost/cmd/wcc/cgenexpr.ww | 73 +----- selfhost/cmd/wcc/cgenstmt.ww | 121 ++-------- selfhost/cmd/wcc/cgenutil.ww | 146 ++++++++++++ selfhost/cmd/wwdump/main.combined.ww | 340 ++++++++++++++------------- test/wcc/703_nested_structlit.c | 290 +++++++++++++++++++++++ 8 files changed, 932 insertions(+), 689 deletions(-) create mode 100644 test/wcc/703_nested_structlit.c diff --git a/Makefile b/Makefile index 089e7c9b..09670254 100644 --- a/Makefile +++ b/Makefile @@ -229,6 +229,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_cgreturn_struct \ $(BIN)/test_cgassign_struct \ $(BIN)/test_dot_explicit_deref \ + $(BIN)/test_nested_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 \ @@ -388,6 +389,12 @@ $(BIN)/test_dot_explicit_deref: test/wcc/702_dot_explicit_deref.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_nested_structlit: test/wcc/703_nested_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 5b5a1dd0..5701413a 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -864,6 +864,7 @@ 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); +static void cg_structlit_fill_bp(Cg*, Local**, Type*, Node*, int); static void cgexpr_int(Cg *c, long long v) @@ -1225,6 +1226,100 @@ 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). + * + * 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. + * + * 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. + * + * 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. */ +static void +cg_structlit_fill_bp(Cg *c, Local **locals_p, Type *lu, Node *lit, int bp_off) +{ + int sz = (int)lu->size; + 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). */ + 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, bp_off + zi)); + zi += 8; + } + while (zi + 4 <= sz) { + ins2(c, A_MOVL, areg(D_AX), + amem(D_BP, bp_off + zi)); + zi += 4; + } + while (zi < sz) { + ins2(c, A_MOVB, areg(D_AX), + amem(D_BP, bp_off + zi)); + zi += 1; + } + } + for (Node *f = lit->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_p, fu, f->lhs, + D_BP, bp_off + (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 + * 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); + continue; + } + cgexpr(c, f->lhs, *locals_p); + 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)); + 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)); + } +} + static void cgexpr(Cg *c, Node *n, Local *locals) { @@ -3454,76 +3549,14 @@ cgexpr(Cg *c, Node *n, Local *locals) 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; - } - } - /* Tagged-union field: delegate to the - * shared widening writer. Handles - * str, scalar, struct literal/ident - * payload, and tagged-subset - * forwarding (with tag remap). */ - 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)); - } + /* Delegate to the shared BP-relative + * structlit fill helper. Handles + * TK_ELLIPSIS autofill, tagged fields, + * float/scalar stores, AND nested + * struct-typed structlit values via + * recursion (#17 silent-zero fix). */ + cg_structlit_fill_bp(c, &locals, lu, + n->rhs, off); break; } if (n->rhs && n->rhs->kind == N_CALL @@ -5888,74 +5921,16 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off + 16)); break; } - /* struct literal initialiser: field-by-field store. The - * literal carries op == TK_ELLIPSIS when the source ends in - * `..., ...` — in that case zero-fill the entire slot first, - * so unmentioned fields read as 0. */ + /* struct literal initialiser: field-by-field store via the + * shared cg_structlit_fill_bp helper. The literal carries + * op == TK_ELLIPSIS when the source ends in `..., ...` — + * helper zero-fills the slot first so unmentioned fields + * read as 0. Nested struct-typed structlit field values + * recurse into the helper at the correct offset instead of + * landing AX = first-qword via cgexpr (#17 silent zero). */ if (n->rhs && n->rhs->kind == N_STRUCTLIT && lu && lu->kind == TY_STRUCT) { - if (n->rhs->op == TK_ELLIPSIS) { - u64 sz = lu->size; - /* AX = 0 once, then store from AX. w6a doesn't - * accept MOVB imm,mem — use register stores. */ - ins2(c, A_XORQ, areg(D_AX), areg(D_AX)); - u64 i = 0; - while (i + 8 <= sz) { - ins2(c, A_MOVQ, areg(D_AX), - amem(D_BP, off + (int)i)); - i += 8; - } - while (i + 4 <= sz) { - ins2(c, A_MOVL, areg(D_AX), - amem(D_BP, off + (int)i)); - i += 4; - } - while (i < sz) { - ins2(c, A_MOVB, areg(D_AX), - amem(D_BP, off + (int)i)); - i += 1; - } - } - for (Node *f = n->rhs->list; f; f = f->next) { - /* find offset of this field */ - 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; - } - } - /* Tagged-union field: delegate to the shared - * widening writer. Handles str, scalar, struct - * literal/ident payload, and tagged-subset - * forwarding (with tag remap). The field's slot - * starts at off+foff inside the struct slot. */ - 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)); - } + cg_structlit_fill_bp(c, locals, lu, n->rhs, off); break; } /* Whole-struct receive for sizes <=24B (call-result rhs). @@ -6224,55 +6199,14 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, scr + 16)); if (n->lhs->kind == N_STRUCTLIT) { - for (Node *f = n->lhs->list; f; - f = f->next) { - u64 foff = 0; - int fsz = 8; - Type *ft = NULL; - for (Tfield *fl = rt->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, - scr + (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, - scr + (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, - scr + (int)foff)); - } + /* Delegate to the shared BP-relative + * fill helper. Same store sequence the + * inline pre-#17 walk emitted, plus + * nested struct-typed structlit values + * recurse instead of dropping the + * trailing bytes. */ + cg_structlit_fill_bp(c, locals, rt, + n->lhs, scr); } else { /* N_IDENT: word-copy rhs slot into * scratch. Whole 8B words via MOVQ; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 7bce6030..e72fe0a1 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -8656,6 +8656,152 @@ 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. +// +// 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. +// +// 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. +// +// 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. +// +// 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 = { + if (si == nil) { return; }; + 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; + emitline("\tXORQ\tAX, AX\n"); + let zi: i32 = 0; + for (zi + 8 <= total) { + emitline("\tMOVQ\tAX, "); + emitoff((bpoff + zi): i64); + emitline("(BP)\n"); + zi += 8; + }; + for (zi + 4 <= total) { + emitline("\tMOVL\tAX, "); + emitoff((bpoff + zi): i64); + emitline("(BP)\n"); + zi += 4; + }; + for (zi < total) { + emitline("\tMOVB\tAX, "); + emitoff((bpoff + zi): i64); + emitline("(BP)\n"); + zi += 1; + }; + }; + let fieldnode: *node = lit.list; + for (fieldnode != nil) { + if (fieldnode.kind == nkind.N_FIELD) { + let fname: str = fieldnode.str; + let fi: *fieldinfo = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fname)) { + // 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). + if (istaggedtype(c, fi.tnode)) { + cgwidentaggedstore(c, fi.tnode, + fieldnode.lhs, "BP", + bpoff + 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. + let nested: bool = false; + if (fieldnode.lhs != nil) { + if (fieldnode.lhs.kind == nkind.N_STRUCTLIT) { + if (fi.tnode != nil) { + if (fi.tnode.kind == nkind.N_TNAME) { + if (primsize(fi.tnode.str) == 0) { + let isi: *structinfo = structlookup(c, fi.tnode.str); + if (isi != nil) { + cgstructlitfillbp(c, isi, + fieldnode.lhs, + bpoff + fi.foff); + nested = true; + }; + }; + }; + }; + }; + }; + if (nested) { + fi = nil; + } else { + cgexpr(c, fieldnode.lhs); + 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"); + 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. + 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((bpoff + fi.foff): i64); + emitline("(BP)\n"); + fi = nil; + }; + }; + }; + } else { + fi = fi.finext; + }; + }; + }; + fieldnode = fieldnode.next; + }; +}; + // MODULE: wcc // selfhost/cmd/wcc/cgenexpr.ww — split out of cgen.ww. // @@ -14149,67 +14295,18 @@ fn cgassign(c: *cgen, n: *node) void = { 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; - }; + // Delegate to the shared BP-relative + // structlit fill helper. Handles + // TK_ELLIPSIS autofill + per-field + // walk; nested struct-typed values + // recurse via the helper (#17 fix). + // Helper uses the explicit {1→MOVB, + // 4→MOVL, else MOVQ} sized-store + // dispatch (NOT fieldstoreop) to stay + // byte-identical with cstage pending + // #13 (fsz==2 MOVW divergence). See + // cgstructlitfillbp docstring. + cgstructlitfillbp(c, lcsi, n.rhs, off); return; }; if (n.rhs != nil @@ -14677,48 +14774,14 @@ fn cgreturn(c: *cgen, n: *node) void = { emitoff((scroff + 16): i64); emitline("(BP)\n"); if (rhs.kind == nkind.N_STRUCTLIT) { - let fn_: *node = rhs.list; - for (fn_ != nil) { - if (fn_.kind == nkind.N_FIELD) { - let fi: *fieldinfo = rsi.fields; - for (fi != nil) { - if (streq(fi.fname, fn_.str)) { - if (istaggedtype(c, fi.tnode)) { - cgwidentaggedstore(c, - fi.tnode, fn_.lhs, - "BP", - scroff + fi.foff, - fi.fsz); - fi = nil; - } else { - cgexpr(c, fn_.lhs); - if (isfloattype(c, fi.tnode)) { - let mov: str = "MOVSD"; - if (isf32type(c, fi.tnode)) { - mov = "MOVSS"; - }; - emitline("\t"); - emitline(mov); - emitline("\tX0, "); - emitoff((scroff + fi.foff): i64); - emitline("(BP)\n"); - } else { - let sop: str = fieldstoreop(c, fi); - emitline("\t"); - emitline(sop); - emitline("\tAX, "); - emitoff((scroff + fi.foff): i64); - emitline("(BP)\n"); - }; - fi = nil; - }; - } else { - fi = fi.finext; - }; - }; - }; - fn_ = fn_.next; - }; + // Delegate to the shared BP-relative + // structlit fill helper. Same store + // sequence the inline pre-#17 walk + // emitted (tagged + float + scalar), + // plus nested struct-typed structlit + // values recurse instead of dropping + // trailing bytes. + cgstructlitfillbp(c, rsi, rhs, scroff); } else { // N_IDENT: word-copy from rhs slot // to scratch. Whole 8B words via @@ -14951,12 +15014,11 @@ fn cglet(c: *cgen, n: *node) void = { return; }; // Struct literal init: `let p: point = point{x=..., y=...};`. - // For each field in the lit, evaluate its value and store at - // the field's offset within the slot. Field-name → offset - // from the struct registry. When the literal carries - // op == tkind.TK_ELLIPSIS (autofill marker from the parser), the - // entire slot is zero-filled first so unmentioned fields - // read as 0. + // Delegates to the shared cgstructlitfillbp helper: TK_ELLIPSIS + // autofill + per-field walk, with nested struct-typed structlit + // values recursing into the helper instead of landing only AX + // (the #17 silent-zero fix). Mirror of cstage cgen.c N_LET + // structlit branch. if (rhs.kind == nkind.N_STRUCTLIT) { let trefn: *node = rhs.lhs; let sname: str; @@ -14967,65 +15029,7 @@ fn cglet(c: *cgen, n: *node) void = { }; let si: *structinfo = structlookup(c, sname); if (si != nil) { - if (rhs.op == tkind.TK_ELLIPSIS) { - let total: i32 = si.totsize; - emitline("\tXORQ\tAX, AX\n"); - let zi: i32 = 0; - for (zi + 8 <= total) { - emitline("\tMOVQ\tAX, "); - emitoff((off + zi): i64); - emitline("(BP)\n"); - zi += 8; - }; - for (zi + 4 <= total) { - emitline("\tMOVL\tAX, "); - emitoff((off + zi): i64); - emitline("(BP)\n"); - zi += 4; - }; - for (zi < total) { - emitline("\tMOVB\tAX, "); - emitoff((off + zi): i64); - emitline("(BP)\n"); - zi += 1; - }; - }; - let fieldnode: *node = rhs.list; - for (fieldnode != nil) { - if (fieldnode.kind == nkind.N_FIELD) { - let fname: str = fieldnode.str; - let fi: *fieldinfo = si.fields; - for (fi != nil) { - let fn_: str = fi.fname; - if (streq(fn_, fname)) { - cgexpr(c, fieldnode.lhs); - // f64/f32 struct-literal field init: cgexpr left - // the value in X0, store via MOVSD/MOVSS. - 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 sop: str = fieldstoreop(c, fi); - emitline("\t"); - emitline(sop); - emitline("\tAX, "); - emitoff((off + fi.foff): i64); - emitline("(BP)\n"); - fi = nil; - }; - } else { - fi = fi.finext; - }; - }; - }; - fieldnode = fieldnode.next; - }; + cgstructlitfillbp(c, si, rhs, off); c.lastwasreturn = 0; return; }; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index b2f38bba..a62547e8 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -5490,67 +5490,18 @@ fn cgassign(c: *cgen, n: *node) void = { 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; - }; + // Delegate to the shared BP-relative + // structlit fill helper. Handles + // TK_ELLIPSIS autofill + per-field + // walk; nested struct-typed values + // recurse via the helper (#17 fix). + // Helper uses the explicit {1→MOVB, + // 4→MOVL, else MOVQ} sized-store + // dispatch (NOT fieldstoreop) to stay + // byte-identical with cstage pending + // #13 (fsz==2 MOVW divergence). See + // cgstructlitfillbp docstring. + cgstructlitfillbp(c, lcsi, n.rhs, off); return; }; if (n.rhs != nil diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index c2d806ff..dd98aee1 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -295,48 +295,14 @@ fn cgreturn(c: *cgen, n: *node) void = { emitoff((scroff + 16): i64); emitline("(BP)\n"); if (rhs.kind == nkind.N_STRUCTLIT) { - let fn_: *node = rhs.list; - for (fn_ != nil) { - if (fn_.kind == nkind.N_FIELD) { - let fi: *fieldinfo = rsi.fields; - for (fi != nil) { - if (streq(fi.fname, fn_.str)) { - if (istaggedtype(c, fi.tnode)) { - cgwidentaggedstore(c, - fi.tnode, fn_.lhs, - "BP", - scroff + fi.foff, - fi.fsz); - fi = nil; - } else { - cgexpr(c, fn_.lhs); - if (isfloattype(c, fi.tnode)) { - let mov: str = "MOVSD"; - if (isf32type(c, fi.tnode)) { - mov = "MOVSS"; - }; - emitline("\t"); - emitline(mov); - emitline("\tX0, "); - emitoff((scroff + fi.foff): i64); - emitline("(BP)\n"); - } else { - let sop: str = fieldstoreop(c, fi); - emitline("\t"); - emitline(sop); - emitline("\tAX, "); - emitoff((scroff + fi.foff): i64); - emitline("(BP)\n"); - }; - fi = nil; - }; - } else { - fi = fi.finext; - }; - }; - }; - fn_ = fn_.next; - }; + // Delegate to the shared BP-relative + // structlit fill helper. Same store + // sequence the inline pre-#17 walk + // emitted (tagged + float + scalar), + // plus nested struct-typed structlit + // values recurse instead of dropping + // trailing bytes. + cgstructlitfillbp(c, rsi, rhs, scroff); } else { // N_IDENT: word-copy from rhs slot // to scratch. Whole 8B words via @@ -569,12 +535,11 @@ fn cglet(c: *cgen, n: *node) void = { return; }; // Struct literal init: `let p: point = point{x=..., y=...};`. - // For each field in the lit, evaluate its value and store at - // the field's offset within the slot. Field-name → offset - // from the struct registry. When the literal carries - // op == tkind.TK_ELLIPSIS (autofill marker from the parser), the - // entire slot is zero-filled first so unmentioned fields - // read as 0. + // Delegates to the shared cgstructlitfillbp helper: TK_ELLIPSIS + // autofill + per-field walk, with nested struct-typed structlit + // values recursing into the helper instead of landing only AX + // (the #17 silent-zero fix). Mirror of cstage cgen.c N_LET + // structlit branch. if (rhs.kind == nkind.N_STRUCTLIT) { let trefn: *node = rhs.lhs; let sname: str; @@ -585,65 +550,7 @@ fn cglet(c: *cgen, n: *node) void = { }; let si: *structinfo = structlookup(c, sname); if (si != nil) { - if (rhs.op == tkind.TK_ELLIPSIS) { - let total: i32 = si.totsize; - emitline("\tXORQ\tAX, AX\n"); - let zi: i32 = 0; - for (zi + 8 <= total) { - emitline("\tMOVQ\tAX, "); - emitoff((off + zi): i64); - emitline("(BP)\n"); - zi += 8; - }; - for (zi + 4 <= total) { - emitline("\tMOVL\tAX, "); - emitoff((off + zi): i64); - emitline("(BP)\n"); - zi += 4; - }; - for (zi < total) { - emitline("\tMOVB\tAX, "); - emitoff((off + zi): i64); - emitline("(BP)\n"); - zi += 1; - }; - }; - let fieldnode: *node = rhs.list; - for (fieldnode != nil) { - if (fieldnode.kind == nkind.N_FIELD) { - let fname: str = fieldnode.str; - let fi: *fieldinfo = si.fields; - for (fi != nil) { - let fn_: str = fi.fname; - if (streq(fn_, fname)) { - cgexpr(c, fieldnode.lhs); - // f64/f32 struct-literal field init: cgexpr left - // the value in X0, store via MOVSD/MOVSS. - 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 sop: str = fieldstoreop(c, fi); - emitline("\t"); - emitline(sop); - emitline("\tAX, "); - emitoff((off + fi.foff): i64); - emitline("(BP)\n"); - fi = nil; - }; - } else { - fi = fi.finext; - }; - }; - }; - fieldnode = fieldnode.next; - }; + cgstructlitfillbp(c, si, rhs, off); c.lastwasreturn = 0; return; }; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 00784f87..2be9ad4e 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -2783,3 +2783,149 @@ 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. +// +// 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. +// +// 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. +// +// 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. +// +// 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 = { + if (si == nil) { return; }; + 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; + emitline("\tXORQ\tAX, AX\n"); + let zi: i32 = 0; + for (zi + 8 <= total) { + emitline("\tMOVQ\tAX, "); + emitoff((bpoff + zi): i64); + emitline("(BP)\n"); + zi += 8; + }; + for (zi + 4 <= total) { + emitline("\tMOVL\tAX, "); + emitoff((bpoff + zi): i64); + emitline("(BP)\n"); + zi += 4; + }; + for (zi < total) { + emitline("\tMOVB\tAX, "); + emitoff((bpoff + zi): i64); + emitline("(BP)\n"); + zi += 1; + }; + }; + let fieldnode: *node = lit.list; + for (fieldnode != nil) { + if (fieldnode.kind == nkind.N_FIELD) { + let fname: str = fieldnode.str; + let fi: *fieldinfo = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fname)) { + // 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). + if (istaggedtype(c, fi.tnode)) { + cgwidentaggedstore(c, fi.tnode, + fieldnode.lhs, "BP", + bpoff + 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. + let nested: bool = false; + if (fieldnode.lhs != nil) { + if (fieldnode.lhs.kind == nkind.N_STRUCTLIT) { + if (fi.tnode != nil) { + if (fi.tnode.kind == nkind.N_TNAME) { + if (primsize(fi.tnode.str) == 0) { + let isi: *structinfo = structlookup(c, fi.tnode.str); + if (isi != nil) { + cgstructlitfillbp(c, isi, + fieldnode.lhs, + bpoff + fi.foff); + nested = true; + }; + }; + }; + }; + }; + }; + if (nested) { + fi = nil; + } else { + cgexpr(c, fieldnode.lhs); + 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"); + 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. + 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((bpoff + fi.foff): i64); + emitline("(BP)\n"); + fi = nil; + }; + }; + }; + } else { + fi = fi.finext; + }; + }; + }; + fieldnode = fieldnode.next; + }; +}; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index a10af267..0bc81c08 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -8656,6 +8656,152 @@ 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. +// +// 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. +// +// 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. +// +// 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. +// +// 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 = { + if (si == nil) { return; }; + 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; + emitline("\tXORQ\tAX, AX\n"); + let zi: i32 = 0; + for (zi + 8 <= total) { + emitline("\tMOVQ\tAX, "); + emitoff((bpoff + zi): i64); + emitline("(BP)\n"); + zi += 8; + }; + for (zi + 4 <= total) { + emitline("\tMOVL\tAX, "); + emitoff((bpoff + zi): i64); + emitline("(BP)\n"); + zi += 4; + }; + for (zi < total) { + emitline("\tMOVB\tAX, "); + emitoff((bpoff + zi): i64); + emitline("(BP)\n"); + zi += 1; + }; + }; + let fieldnode: *node = lit.list; + for (fieldnode != nil) { + if (fieldnode.kind == nkind.N_FIELD) { + let fname: str = fieldnode.str; + let fi: *fieldinfo = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fname)) { + // 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). + if (istaggedtype(c, fi.tnode)) { + cgwidentaggedstore(c, fi.tnode, + fieldnode.lhs, "BP", + bpoff + 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. + let nested: bool = false; + if (fieldnode.lhs != nil) { + if (fieldnode.lhs.kind == nkind.N_STRUCTLIT) { + if (fi.tnode != nil) { + if (fi.tnode.kind == nkind.N_TNAME) { + if (primsize(fi.tnode.str) == 0) { + let isi: *structinfo = structlookup(c, fi.tnode.str); + if (isi != nil) { + cgstructlitfillbp(c, isi, + fieldnode.lhs, + bpoff + fi.foff); + nested = true; + }; + }; + }; + }; + }; + }; + if (nested) { + fi = nil; + } else { + cgexpr(c, fieldnode.lhs); + 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"); + 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. + 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((bpoff + fi.foff): i64); + emitline("(BP)\n"); + fi = nil; + }; + }; + }; + } else { + fi = fi.finext; + }; + }; + }; + fieldnode = fieldnode.next; + }; +}; + // MODULE: wcc // selfhost/cmd/wcc/cgenexpr.ww — split out of cgen.ww. // @@ -14149,67 +14295,18 @@ fn cgassign(c: *cgen, n: *node) void = { 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; - }; + // Delegate to the shared BP-relative + // structlit fill helper. Handles + // TK_ELLIPSIS autofill + per-field + // walk; nested struct-typed values + // recurse via the helper (#17 fix). + // Helper uses the explicit {1→MOVB, + // 4→MOVL, else MOVQ} sized-store + // dispatch (NOT fieldstoreop) to stay + // byte-identical with cstage pending + // #13 (fsz==2 MOVW divergence). See + // cgstructlitfillbp docstring. + cgstructlitfillbp(c, lcsi, n.rhs, off); return; }; if (n.rhs != nil @@ -14677,48 +14774,14 @@ fn cgreturn(c: *cgen, n: *node) void = { emitoff((scroff + 16): i64); emitline("(BP)\n"); if (rhs.kind == nkind.N_STRUCTLIT) { - let fn_: *node = rhs.list; - for (fn_ != nil) { - if (fn_.kind == nkind.N_FIELD) { - let fi: *fieldinfo = rsi.fields; - for (fi != nil) { - if (streq(fi.fname, fn_.str)) { - if (istaggedtype(c, fi.tnode)) { - cgwidentaggedstore(c, - fi.tnode, fn_.lhs, - "BP", - scroff + fi.foff, - fi.fsz); - fi = nil; - } else { - cgexpr(c, fn_.lhs); - if (isfloattype(c, fi.tnode)) { - let mov: str = "MOVSD"; - if (isf32type(c, fi.tnode)) { - mov = "MOVSS"; - }; - emitline("\t"); - emitline(mov); - emitline("\tX0, "); - emitoff((scroff + fi.foff): i64); - emitline("(BP)\n"); - } else { - let sop: str = fieldstoreop(c, fi); - emitline("\t"); - emitline(sop); - emitline("\tAX, "); - emitoff((scroff + fi.foff): i64); - emitline("(BP)\n"); - }; - fi = nil; - }; - } else { - fi = fi.finext; - }; - }; - }; - fn_ = fn_.next; - }; + // Delegate to the shared BP-relative + // structlit fill helper. Same store + // sequence the inline pre-#17 walk + // emitted (tagged + float + scalar), + // plus nested struct-typed structlit + // values recurse instead of dropping + // trailing bytes. + cgstructlitfillbp(c, rsi, rhs, scroff); } else { // N_IDENT: word-copy from rhs slot // to scratch. Whole 8B words via @@ -14951,12 +15014,11 @@ fn cglet(c: *cgen, n: *node) void = { return; }; // Struct literal init: `let p: point = point{x=..., y=...};`. - // For each field in the lit, evaluate its value and store at - // the field's offset within the slot. Field-name → offset - // from the struct registry. When the literal carries - // op == tkind.TK_ELLIPSIS (autofill marker from the parser), the - // entire slot is zero-filled first so unmentioned fields - // read as 0. + // Delegates to the shared cgstructlitfillbp helper: TK_ELLIPSIS + // autofill + per-field walk, with nested struct-typed structlit + // values recursing into the helper instead of landing only AX + // (the #17 silent-zero fix). Mirror of cstage cgen.c N_LET + // structlit branch. if (rhs.kind == nkind.N_STRUCTLIT) { let trefn: *node = rhs.lhs; let sname: str; @@ -14967,65 +15029,7 @@ fn cglet(c: *cgen, n: *node) void = { }; let si: *structinfo = structlookup(c, sname); if (si != nil) { - if (rhs.op == tkind.TK_ELLIPSIS) { - let total: i32 = si.totsize; - emitline("\tXORQ\tAX, AX\n"); - let zi: i32 = 0; - for (zi + 8 <= total) { - emitline("\tMOVQ\tAX, "); - emitoff((off + zi): i64); - emitline("(BP)\n"); - zi += 8; - }; - for (zi + 4 <= total) { - emitline("\tMOVL\tAX, "); - emitoff((off + zi): i64); - emitline("(BP)\n"); - zi += 4; - }; - for (zi < total) { - emitline("\tMOVB\tAX, "); - emitoff((off + zi): i64); - emitline("(BP)\n"); - zi += 1; - }; - }; - let fieldnode: *node = rhs.list; - for (fieldnode != nil) { - if (fieldnode.kind == nkind.N_FIELD) { - let fname: str = fieldnode.str; - let fi: *fieldinfo = si.fields; - for (fi != nil) { - let fn_: str = fi.fname; - if (streq(fn_, fname)) { - cgexpr(c, fieldnode.lhs); - // f64/f32 struct-literal field init: cgexpr left - // the value in X0, store via MOVSD/MOVSS. - 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 sop: str = fieldstoreop(c, fi); - emitline("\t"); - emitline(sop); - emitline("\tAX, "); - emitoff((off + fi.foff): i64); - emitline("(BP)\n"); - fi = nil; - }; - } else { - fi = fi.finext; - }; - }; - }; - fieldnode = fieldnode.next; - }; + cgstructlitfillbp(c, si, rhs, off); c.lastwasreturn = 0; return; }; diff --git a/test/wcc/703_nested_structlit.c b/test/wcc/703_nested_structlit.c new file mode 100644 index 00000000..ae1b8f1c --- /dev/null +++ b/test/wcc/703_nested_structlit.c @@ -0,0 +1,290 @@ +/* + * 703_nested_structlit — silent zero of nested STRUCTLIT fields + * (task #17). + * + * Pre-existing landmine surfaced by worker-cgnassign during #5. + * For a struct literal whose field value is itself an N_STRUCTLIT + * of a struct-typed field, the outer field-walk did + * `cgexpr(field_value); store-AX-sized` — cgexpr has no whole- + * struct-in-register convention, so for a nested literal it lands + * AX = first qword and the trailing bytes silently stay zero (or + * stack garbage in the hostile case). The same shape applies at + * three sites in each stage: + * - N_LET N_STRUCTLIT — `let o: outer = outer { i = inner{...} }` + * - N_ASSIGN N_IDENT-lhs N_STRUCTLIT — `o = outer { i = inner{...} }` + * - N_RETURN N_STRUCTLIT — `return outer { i = inner{...} }` + * + * Workaround used in 701_cgassign_struct rows that need nested + * shape: `let o: outer; o.f = ...` (write-by-field). Avoids the + * bug; doesn't fix it. + * + * Fix: a shared helper `cg_structlit_fill_bp` (cstage) / + * `cgstructlitfillbp` (wwstage) factors the field-walk, recursing + * when a field's value is itself an N_STRUCTLIT for a struct-typed + * field. Bp-relative addressing only; the N_ASSIGN N_DOT-lhs + * structlit walks (via_ptr / global) keep their inline field-walk + * and still drop nested-STRUCTLIT silently — separate follow-up + * task ("cgen: nested STRUCTLIT in N_ASSIGN N_DOT structlit"). + * + * 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[] = { + /* N_LET nested structlit, all i64 fields. Pre-fix: only o.i.a + * (first qword) was stored; o.i.b silently stayed 0. Want: + * 7 + 8 + 12 = 27. */ + { "let_nested_i64", + "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 = 7i64, b = 8i64 }, t = 12i64 };\n" + " return (o.i.a + o.i.b + o.t): i32;\n" + "};\n", + 27 }, + /* N_LET nested structlit, multi-level (3-deep). Pre-fix: + * only o.m.in.a was stored; b and t and outer.x silently 0. + * Want: 1+2+3+4 = 10. */ + { "let_nested_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" + "fn main() i32 = {\n" + " let o: outer = outer {\n" + " m = middle {\n" + " in = leaf { a = 1i64, b = 2i64 },\n" + " t = 3i64\n" + " },\n" + " x = 4i64\n" + " };\n" + " return (o.m.in.a + o.m.in.b + o.m.t + o.x): i32;\n" + "};\n", + 10 }, + /* N_LET nested structlit, narrow i32 fields. Pins sized-store + * dispatch in the helper — MOVL not MOVQ. Want: 4+5+6+7 = 22. */ + { "let_nested_i32", + "type inner = struct { a: i32, b: i32 };\n" + "type outer = struct { i: inner, x: i32, y: i32 };\n" + "fn main() i32 = {\n" + " let o: outer = outer { i = inner { a = 4, b = 5 }, x = 6, y = 7 };\n" + " return o.i.a + o.i.b + o.x + o.y;\n" + "};\n", + 22 }, + /* N_ASSIGN N_IDENT-lhs nested structlit. Pre-fix: o.i.b + * stayed 0 after the reassign (only first qword stored). + * Want: 9+11+30 = 50. */ + { "assign_ident_nested", + "type inner = struct { a: i64, b: i64 };\n" + "type outer = struct { i: inner, t: i64 };\n" + "fn main() i32 = {\n" + " let o: outer;\n" + " o = outer { i = inner { a = 9i64, b = 11i64 }, t = 30i64 };\n" + " return (o.i.a + o.i.b + o.t): i32;\n" + "};\n", + 50 }, + /* N_RETURN nested structlit. Pre-fix: only b's first qword + * (its .a) landed in the receive slot; b's .b silently 0. + * Want: 13+17+25 = 55. */ + { "return_nested", + "type inner = struct { a: i64, b: i64 };\n" + "type outer = struct { i: inner, t: i64 };\n" + "fn mko() outer = {\n" + " return outer { i = inner { a = 13i64, b = 17i64 }, t = 25i64 };\n" + "};\n" + "fn main() i32 = {\n" + " let r: outer = mko();\n" + " return (r.i.a + r.i.b + r.t): i32;\n" + "};\n", + 55 }, + /* Mixed: inner struct surrounded by other scalar fields, with + * the inner appearing in the MIDDLE of the outer (non-zero + * outer foff for the recursion target). Pins that the helper + * threads `bpoff + outer_foff` correctly. All fields are i64 so + * the wwstage/cstage struct-layout alignment divergence (task + * #15, mixed i32/struct fields) doesn't bite — switch to i32 + * once #15 lands. Sum: 100+1+2+50 = 153. */ + { "let_nested_middle", + "type inner = struct { a: i64, b: i64 };\n" + "type outer = struct { pre: i64, i: inner, post: i64 };\n" + "fn main() i32 = {\n" + " let o: outer = outer {\n" + " pre = 100i64,\n" + " i = inner { a = 1i64, b = 2i64 },\n" + " post = 50i64\n" + " };\n" + " return (o.pre + o.i.a + o.i.b + o.post): i32;\n" + "};\n", + 153 }, +}; + +/* run_driver — compile r->src via the given driver and exec; return + * the process exit code. Mirror of 701/702. */ +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/wcns_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/wcns_%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 701/702. */ +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/wcns_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/wcns_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/wcns_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, "nested_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, + "nested_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, + "nested_structlit: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("nested_structlit: %d/%d ok\n", total, total); + return 0; +}