From a5919ed8dabe96a4e65ba08a927c6ae95b040fce Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 14 May 2026 19:12:20 +0900 Subject: [PATCH] w6c+selfhost: cg_widen_tagged_store basereg + N_ASSIGN tagged field (closes #26) Extended cg_widen_tagged_store (cstage) / cgwidentaggedstore (wwstage) to take a base_reg/basereg parameter so the primitive supports non-BP destinations. Cstage extends body in-place via via_outer gate + spill+scratch+copy-out; wwstage splits into wrapper (non-BP) + cgwidentaggedstorebp (BP-only) to dodge the no-goto constraint. New N_ASSIGN field TY_TAGGED branch routes through the primitive for all rhs shapes. Scope-adjacent: fieldsize recurses through N_TTAGGED via slotsize and TNAME-aliased-to-tagged via aliaslookup. Needed for the test fixtures. Wwstage read-side N_DOT-of-tagged-field source is filed as task #28; test rows use mark-canary verification until that lands. --- cmd/w6c/cgen.c | 193 +++++++++++++++------------ selfhost/cmd/w6c/main.combined.ww | 167 +++++++++++++++++++++-- selfhost/cmd/wcc/cgendecl.ww | 55 ++++++++ selfhost/cmd/wcc/cgenexpr.ww | 31 ++++- selfhost/cmd/wcc/cgenstmt.ww | 6 +- selfhost/cmd/wcc/cgenutil.ww | 75 ++++++++++- selfhost/cmd/wwdump/main.combined.ww | 167 +++++++++++++++++++++-- test/wcc/681_arr_elem_field_write.c | 66 +++++++++ 8 files changed, 644 insertions(+), 116 deletions(-) diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index c7b16b1b..c5d2b9d9 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -862,7 +862,7 @@ localfind(Local *head, const char *name) static void cgexpr(Cg*, Node*, Local*); 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); +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 @@ -913,9 +913,21 @@ cg_widen_tag_remap(Cg *c, Type *du, Type *su, int slot_off) } /* cg_widen_tagged_store — write the tagged-union slot bytes for `src` - * into BP+slot_off, sized to `sz` (8 for nullable fold, else 16/24+). - * Used by call-site widening (via cg_widen_tagged_push) and by the - * let/assign/return/struct-field-init paths. + * into base_reg+slot_off, sized to `sz` (8 for nullable fold, else + * 16/24+). Used by call-site widening (via cg_widen_tagged_push) and + * by the let/assign/return/struct-field-init paths. + * + * base_reg picks the addressing root for every write: + * - D_BP: function-frame slot. The original layout — callers pass + * a BP-relative slot_off and the function writes directly. + * - else (e.g. D_BX for a *struct field, D_CX for a top-level + * struct field): pointer-rooted dst. cgexpr inside this function + * trashes every GPR, so we can't carry base_reg across — instead + * we route every write through a fresh BP-rooted scratch slot, + * reload base_reg from a temp spill at the end, and word-copy + * scratch → (base_reg, slot_off). Caller is responsible for + * loading base_reg with the dst address before the call; the + * function preserves it across cgexpr via the spill. * * Branches by source shape (tagged_arg_size > 0 source counts as a * tagged subset — possibly with different variant indices): @@ -929,13 +941,34 @@ cg_widen_tag_remap(Cg *c, Type *du, Type *su, int slot_off) * - scalar: cgexpr leaves AX; store at +8 with zero pad. */ static void cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src, - int slot_off, int sz) + int base_reg, int slot_off, int sz) { + /* For pointer-rooted dst, materialise into a BP-rooted scratch + * slot — body writes via `amem(D_BP, write_off + k)` — then copy + * out. Spill base_reg first so cgexpr can clobber freely. */ + int via_outer = (base_reg != D_BP); + int base_spill = 0; + int write_off = slot_off; + if (via_outer) { + const char *spname = mklabel(c, "tagbase"); + base_spill = local_alloc(c, locals_p, spname, 8, cg_frame); + ins2(c, A_MOVQ, areg(base_reg), amem(D_BP, base_spill)); + const char *scname = mklabel(c, "tagscr"); + write_off = local_alloc(c, locals_p, scname, sz, cg_frame); + /* Pre-zero so str/scalar branches (which leave high words + * untouched when sz exceeds the variant's footprint) still + * deliver a clean slot to the copy-out. */ + ins2(c, A_XORQ, areg(D_AX), areg(D_AX)); + for (int k = 0; k < sz; k += 8) + ins2(c, A_MOVQ, areg(D_AX), + amem(D_BP, write_off + k)); + } Type *du = (dst && dst->kind == TY_NAMED) ? dst->under : dst; if (du == NULL || du->kind != TY_TAGGED) return; if (du->nullable) { cgexpr(c, src, *locals_p); - ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, slot_off + 0)); + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, write_off + 0)); + if (via_outer) goto copy_out; return; } /* `expr: TaggedAlias` where the cast's destination IS the union @@ -970,29 +1003,30 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src, ins2(c, A_MOVQ, amem(D_BP, soff + k), areg(D_AX)); ins2(c, A_MOVQ, areg(D_AX), - amem(D_BP, slot_off + k)); + amem(D_BP, write_off + k)); } } else { cgexpr(c, src, *locals_p); ins2(c, A_MOVQ, areg(D_AX), - amem(D_BP, slot_off + 0)); + amem(D_BP, write_off + 0)); if (ssz > 8) ins2(c, A_MOVQ, areg(D_DX), - amem(D_BP, slot_off + 8)); + amem(D_BP, write_off + 8)); if (ssz > 16) ins2(c, A_MOVQ, areg(D_CX), - amem(D_BP, slot_off + 16)); + amem(D_BP, write_off + 16)); if (ssz > 24) ins2(c, A_MOVQ, areg(D_R8), - amem(D_BP, slot_off + 24)); + amem(D_BP, write_off + 24)); } if (ssz < sz) { ins2(c, A_XORQ, areg(D_AX), areg(D_AX)); for (int k = ssz; k < sz; k += 8) ins2(c, A_MOVQ, areg(D_AX), - amem(D_BP, slot_off + k)); + amem(D_BP, write_off + k)); } - cg_widen_tag_remap(c, du, su, slot_off); + cg_widen_tag_remap(c, du, su, write_off); + if (via_outer) goto copy_out; return; } /* Struct payload: zero the whole slot, then write fields/words @@ -1002,7 +1036,7 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src, ins2(c, A_XORQ, areg(D_AX), areg(D_AX)); for (int k = 0; k < sz; k += 8) ins2(c, A_MOVQ, areg(D_AX), - amem(D_BP, slot_off + k)); + amem(D_BP, write_off + k)); int tag = cg_tag_for_variant(du, st); if (src->kind == N_IDENT) { int soff = localfind(*locals_p, src->str); @@ -1012,7 +1046,7 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src, ins2(c, A_MOVQ, amem(D_BP, soff + k), areg(D_AX)); ins2(c, A_MOVQ, areg(D_AX), - amem(D_BP, slot_off + 8 + k)); + amem(D_BP, write_off + 8 + k)); k += 8; } if (k < ssz) { @@ -1026,7 +1060,7 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src, ins2(c, lop, amem(D_BP, soff + k), areg(D_AX)); ins2(c, lop, areg(D_AX), - amem(D_BP, slot_off + 8 + k)); + amem(D_BP, write_off + 8 + k)); } } else if (src->kind == N_STRUCTLIT) { for (Node *f = src->list; f; f = f->next) { @@ -1046,37 +1080,39 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src, if (fld_isfloat(ftype, &sl_isf32)) { int mov = sl_isf32 ? A_MOVSS : A_MOVSD; ins2(c, mov, areg(D_X0), - amem(D_BP, slot_off + 8 + (int)foff)); + amem(D_BP, write_off + 8 + (int)foff)); continue; } Type *fu = (ftype && ftype->kind == TY_NAMED) ? ftype->under : ftype; if (fu && fu->kind == TY_STR) { ins2(c, A_MOVQ, areg(D_AX), - amem(D_BP, slot_off + 8 + (int)foff + 0)); + amem(D_BP, write_off + 8 + (int)foff + 0)); ins2(c, A_MOVQ, areg(D_BX), - amem(D_BP, slot_off + 8 + (int)foff + 8)); + amem(D_BP, write_off + 8 + (int)foff + 8)); 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, slot_off + 8 + (int)foff)); + amem(D_BP, write_off + 8 + (int)foff)); } } ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag), - amem(D_BP, slot_off + 0)); + amem(D_BP, write_off + 0)); + if (via_outer) goto copy_out; return; } /* str payload: AX=ptr, BX=len from cgexpr. */ if (type_isstr(st) || (su && su->kind == TY_STR)) { cgexpr(c, src, *locals_p); - ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, slot_off + 8)); - ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, slot_off + 16)); + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, write_off + 8)); + ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, write_off + 16)); int tag = cg_tag_for_variant(du, st); ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag), - amem(D_BP, slot_off + 0)); + amem(D_BP, write_off + 0)); + if (via_outer) goto copy_out; return; } /* Slice payload: cgexpr leaves (AX=ptr, BX=len, CX=cap). The @@ -1084,12 +1120,13 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src, * destination tagged-union slot be at least 32B. */ if (type_isslice(st) || (su && su->kind == TY_SLICE)) { cgexpr(c, src, *locals_p); - ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, slot_off + 8)); - ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, slot_off + 16)); - ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, slot_off + 24)); + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, write_off + 8)); + ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, write_off + 16)); + ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, write_off + 24)); int tag = cg_tag_for_variant(du, st); ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag), - amem(D_BP, slot_off + 0)); + amem(D_BP, write_off + 0)); + if (via_outer) goto copy_out; return; } /* Scalar / pointer / etc. The high slot word (when sz > 16) is @@ -1099,9 +1136,21 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src, * cg_widen_tagged_push pre-zeroes the scratch slot before * calling us, so the call-site push still sees clean pad. */ cgexpr(c, src, *locals_p); - ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, slot_off + 8)); + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, write_off + 8)); int tag = cg_tag_for_variant(du, st); - ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag), amem(D_BP, slot_off + 0)); + ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag), amem(D_BP, write_off + 0)); +copy_out: + if (via_outer) { + /* cgexpr above clobbered base_reg — reload from spill, then + * word-copy scratch → caller's (base_reg, slot_off). */ + ins2(c, A_MOVQ, amem(D_BP, base_spill), areg(base_reg)); + for (int k = 0; k < sz; k += 8) { + ins2(c, A_MOVQ, amem(D_BP, write_off + k), + areg(D_AX)); + ins2(c, A_MOVQ, areg(D_AX), + amem(base_reg, slot_off + k)); + } + } } /* cg_widen_tagged_push — call-site widening. For shapes where cgexpr @@ -1168,7 +1217,7 @@ cg_widen_tagged_push(Cg *c, Local **locals_p, Type *dst, Node *src, int sz) ins2(c, A_XORQ, areg(D_AX), areg(D_AX)); for (int k = 0; k < sz; k += 8) ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, scr + k)); - cg_widen_tagged_store(c, locals_p, dst, src, scr, sz); + cg_widen_tagged_store(c, locals_p, dst, src, D_BP, scr, sz); int nwords = sz / 8; for (int k = nwords - 1; k >= 0; k--) { ins2(c, A_MOVQ, amem(D_BP, scr + k * 8), areg(D_AX)); @@ -1862,63 +1911,41 @@ cgexpr(Cg *c, Node *n, Local *locals) Type *ft = f->type; Type *fu = (ft && ft->kind == TY_NAMED) ? ft->under : ft; + /* Tagged-union field — full slot rewrite via the + * shared widener so every rhs shape (whole-tagged + * ident or expr with tag-remap, concrete-variant + * widening of str/slice/struct/scalar/void) lands + * the right tag + payload bytes. The pre-#26 + * branch synthesised a single tag from + * cg_tag_for_variant and stored only AX at +8, so + * whole-tagged rhs (vt == fu, no concrete tag) + * silently wrote tag 0 and dropped trailing words. + * cg_widen_tagged_store handles every shape by + * branching on the source's resolved type. */ if (fu && fu->kind == TY_TAGGED && n->op == TK_ASSIGN) { int boff = localfind(locals, base->str); int is_global = (boff == 0 && !via_ptr && let_islet(base->str)); int foff = (int)f->offset; - Type *vt = n->rhs ? n->rhs->type : NULL; - int tag = cg_tag_for_variant(fu, vt); - int addr_in_bx = via_ptr; - if (via_ptr) - ins2(c, A_MOVQ, amem(D_BP, boff), + int fsz = (int)fu->size; + if (via_ptr) { + ins2(c, A_MOVQ, + amem(D_BP, boff), areg(D_BX)); - cgexpr(c, n->rhs, locals); - if (is_global) + cg_widen_tagged_store(c, &locals, + fu, n->rhs, D_BX, foff, fsz); + } else if (is_global) { ins2(c, A_LEAQ, masym(c, base->str), - areg(D_CX)); - if (type_isstr(vt)) { - if (addr_in_bx) { - ins2(c, A_MOVQ, areg(D_AX), - amem(D_BX, foff + 8)); - ins2(c, A_MOVQ, areg(D_BX), - amem(D_BX, foff + 16)); - } else if (is_global) { - ins2(c, A_MOVQ, areg(D_AX), - amem(D_CX, foff + 8)); - ins2(c, A_MOVQ, areg(D_BX), - amem(D_CX, foff + 16)); - } else { - ins2(c, A_MOVQ, areg(D_AX), - amem(D_BP, boff + foff + 8)); - ins2(c, A_MOVQ, areg(D_BX), - amem(D_BP, boff + foff + 16)); - } + areg(D_BX)); + cg_widen_tagged_store(c, &locals, + fu, n->rhs, D_BX, foff, fsz); } else { - if (addr_in_bx) - ins2(c, A_MOVQ, areg(D_AX), - amem(D_BX, foff + 8)); - else if (is_global) - ins2(c, A_MOVQ, areg(D_AX), - amem(D_CX, foff + 8)); - else - ins2(c, A_MOVQ, areg(D_AX), - amem(D_BP, boff + foff + 8)); + cg_widen_tagged_store(c, &locals, + fu, n->rhs, D_BP, + boff + foff, fsz); } - if (addr_in_bx) - ins2(c, A_MOVQ, - aimm(tag < 0 ? 0 : tag), - amem(D_BX, foff + 0)); - else if (is_global) - ins2(c, A_MOVQ, - aimm(tag < 0 ? 0 : tag), - amem(D_CX, foff + 0)); - else - ins2(c, A_MOVQ, - aimm(tag < 0 ? 0 : tag), - amem(D_BP, boff + foff + 0)); break; } int fsz = (int)(f->type ? f->type->size : 8); @@ -2781,7 +2808,7 @@ cgexpr(Cg *c, Node *n, Local *locals) ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, scr + k)); cg_widen_tagged_store(c, &locals, esubu, - n->rhs, scr, ssz); + n->rhs, D_BP, scr, ssz); /* Compute &arr[i] → BX. */ cgexpr(c, n->lhs->rhs, locals); if (ssz > 1) { @@ -2878,7 +2905,7 @@ cgexpr(Cg *c, Node *n, Local *locals) int off = localfind(locals, n->lhs->str); if (off == 0) break; cg_widen_tagged_store(c, &locals, lu, n->rhs, - off, (int)lu->size); + D_BP, off, (int)lu->size); break; } } @@ -3458,7 +3485,7 @@ cgexpr(Cg *c, Node *n, Local *locals) if (v_is_tagged) { cg_widen_tagged_store(c, &locals, velem, - a, slot, esz); + a, D_BP, slot, esz); continue; } cgexpr(c, a, locals); @@ -5158,7 +5185,7 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) * literal — field-by-field at slot+8+field_off), str payload, * and scalar payload (with zero-pad to the slot size). */ if (n->rhs && lu && lu->kind == TY_TAGGED) { - cg_widen_tagged_store(c, locals, lu, n->rhs, off, sz); + cg_widen_tagged_store(c, locals, lu, n->rhs, D_BP, off, sz); break; } /* slice expression initialiser: build a {ptr, len, cap} header @@ -5272,7 +5299,7 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) ? ft->under : ft; if (fu && fu->kind == TY_TAGGED) { cg_widen_tagged_store(c, locals, fu, - f->lhs, off + (int)foff, + f->lhs, D_BP, off + (int)foff, (int)fu->size); continue; } @@ -5470,7 +5497,7 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, scr + k)); cg_widen_tagged_store(c, locals, rt, - n->lhs, scr, sz); + n->lhs, D_BP, scr, sz); ins2(c, A_MOVQ, amem(D_BP, scr + 0), areg(D_AX)); if (sz > 8) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 64dcea5c..6ccf4c43 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -5744,7 +5744,7 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { emitline("(BP)\n"); zz += 8; }; - cgwidentaggedstore(c, ptype, arg, scroff, widensz); + cgwidentaggedstore(c, ptype, arg, "BP", scroff, widensz); let pp: i32 = widensz - 8; for (pp >= 0) { emitline("\tMOVQ\t"); @@ -6938,6 +6938,7 @@ fn slotsize(c: *cgen, typn: *node) i32 = { fn fieldsize(c: *cgen, tnode: *node) i32 = { if (tnode == nil) { return 8; }; let k: nkind = tnode.kind; + if (k == nkind.N_TTAGGED){ return slotsize(c, tnode); }; if (k == nkind.N_TNAME) { let nm: str = tnode.str; if (streq(nm, "str")) { return 16; }; @@ -6957,6 +6958,13 @@ fn fieldsize(c: *cgen, tnode: *node) i32 = { }; return 4; // default storage is i32 }; + // Type alias to a tagged-union — recurse through aliaslookup + // so `e: ev` (where `ev = (i64 | i32)`) takes 16B in the + // containing struct rather than the 8B default. + if (c != nil) { + let aliased: *node = aliaslookup(c, nm); + if (aliased != nil) { return fieldsize(c, aliased); }; + }; return 8; }; if (k == nkind.N_TPTR) { return 8; }; @@ -7633,9 +7641,20 @@ fn rhstaggedabicall(c: *cgen, src: *node) bool = { return false; }; -// cgwidentaggedstore — write tagged-union slot bytes for `src` into the -// slot at BP+slot_off, sized to slot_sz. Mirrors cg_widen_tagged_store -// in cmd/w6c/cgen.c. Branches by source shape: +// cgwidentaggedstore — write tagged-union slot bytes for `src` into +// the slot at `basereg`+slot_off, sized to slot_sz. Mirrors +// cg_widen_tagged_store in cmd/w6c/cgen.c. +// +// `basereg` selects the addressing root: +// - "BP": function-frame slot (let / assign / return / structlit / +// array-elem scratch). Body writes straight to slot_off(BP). +// - else (e.g. "BX" for *struct field, top-level struct LEAQ +// base): pointer-rooted dst. cgexpr inside trashes every GPR, +// so we route through a fresh BP-rooted scratch slot, spill +// basereg before the body, reload after, then word-copy +// scratch → (basereg, slot_off). +// +// Branches by source shape: // - nullable dst (8B slot): cgexpr → AX → slot+0. // - tagged src ident: copy slot words, zero-pad, tag-remap. // - tagged src via AX/DX/CX ABI (call / tagged-arr index): cgexpr, @@ -7646,7 +7665,53 @@ fn rhstaggedabicall(c: *cgen, src: *node) bool = { // tag last. // - str src: tag@+0, ptr@+8, len@+16. // - scalar src: tag@+0, value@+8. -fn cgwidentaggedstore(c: *cgen, dst: *node, src: *node, slot_off: i32, slot_sz: i32) void = { +fn cgwidentaggedstore(c: *cgen, dst: *node, src: *node, + basereg: str, slot_off: i32, slot_sz: i32) void = { + if (streq(basereg, "BP")) { + cgwidentaggedstorebp(c, dst, src, slot_off, slot_sz); + return; + }; + // Pointer-rooted dst: spill basereg (cgexpr will trash it), + // materialise into a BP-rooted scratch via the BP path, then + // reload basereg and word-copy scratch → caller's slot. + let bspill: i32 = localadd(c, "@tagbase", 8, nil); + emitline("\tMOVQ\t"); + emitline(basereg); + emitline(", "); + emitoff(bspill: i64); + emitline("(BP)\n"); + let scr: i32 = localadd(c, "@tagscr", slot_sz, nil); + emitline("\tXORQ\tAX, AX\n"); + let z: i32 = 0; + for (z < slot_sz) { + emitline("\tMOVQ\tAX, "); + emitoff((scr + z): i64); + emitline("(BP)\n"); + z += 8; + }; + cgwidentaggedstorebp(c, dst, src, scr, slot_sz); + emitline("\tMOVQ\t"); + emitoff(bspill: i64); + emitline("(BP), "); + emitline(basereg); + emitline("\n"); + let k: i32 = 0; + for (k < slot_sz) { + emitline("\tMOVQ\t"); + emitoff((scr + k): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tAX, "); + emitdispreg((slot_off + k): i64, basereg); + emitline("\n"); + k += 8; + }; +}; + +// cgwidentaggedstorebp — BP-rooted body. Called via cgwidentaggedstore +// for the natural "BP" case and via the wrapper's scratch path for +// pointer-rooted dst. Direct callers exist only in case of future +// inlined uses inside this file; new code should call the wrapper. +fn cgwidentaggedstorebp(c: *cgen, dst: *node, src: *node, slot_off: i32, slot_sz: i32) void = { let dt: *node = resolvetagged(c, dst); if (dt == nil) { return; }; // Nullable fold: one 8B word holding the pointer (or 0 for void). @@ -10793,7 +10858,7 @@ fn cgcall(c: *cgen, n: *node) void = { let slot: i32 = doff + j * esz; if (velemtagged) { cgwidentaggedstore(c, varp.lhs, - aa2, slot, esz); + aa2, "BP", slot, esz); } else { if (velemstr) { cgexpr(c, aa2); emitline("\tMOVQ\tAX, "); @@ -11049,7 +11114,7 @@ fn cgassign(c: *cgen, n: *node) void = { if (istaggedtype(c, lc.tnode)) { let lsz: i32 = slotsize(c, lc.tnode); cgwidentaggedstore(c, lc.tnode, - n.rhs, lc.off, lsz); + n.rhs, "BP", lc.off, lsz); return; }; }; @@ -11273,7 +11338,7 @@ fn cgassign(c: *cgen, n: *node) void = { zz += 8; }; cgwidentaggedstore(c, elemtn, n.rhs, - scroff, slot_sz); + "BP", scroff, slot_sz); cgexpr(c, idx); if (slot_sz > 1) { emitline("\tMOVQ\t$"); @@ -11619,6 +11684,20 @@ fn cgassign(c: *cgen, n: *node) void = { for (fi != nil) { let fn_: str = fi.fname; if (streq(fn_, fld)) { + // Tagged-union field via *struct base — full slot + // rewrite via cgwidentaggedstore basereg="BX". Pre-#26 + // fell through to the scalar store and dropped tag + // + payload. + if (n.op == tkind.TK_ASSIGN + && istaggedtype(c, fi.tnode)) { + let fsz: i32 = slotsize(c, fi.tnode); + emitline("\tMOVQ\t"); + emitoff(lc.off: i64); + emitline("(BP), BX\n"); + cgwidentaggedstore(c, fi.tnode, + n.rhs, "BX", fi.foff, fsz); + return; + }; // struct-typed field, struct-ident rhs through // *struct base: cgexpr can't materialise a whole // struct value, so word-copy from the rhs slot @@ -11769,6 +11848,17 @@ fn cgassign(c: *cgen, n: *node) void = { for (fi != nil) { let fn_: str = fi.fname; if (streq(fn_, fld)) { + // Tagged-union field in a direct struct local — + // full slot rewrite at (lc.off + fi.foff)(BP) + // via cgwidentaggedstore basereg="BP". Pre-#26 + // fell through and dropped tag + payload. + if (n.op == tkind.TK_ASSIGN + && istaggedtype(c, fi.tnode)) { + let fsz: i32 = slotsize(c, fi.tnode); + cgwidentaggedstore(c, fi.tnode, + n.rhs, "BP", lc.off + fi.foff, fsz); + return; + }; // struct-typed field, struct-ident rhs: // word-copy direct, skipping cgexpr (no // register convention for a whole struct @@ -12978,8 +13068,8 @@ fn cgreturn(c: *cgen, n: *node) void = { emitline("(BP)\n"); zz += 8; }; - cgwidentaggedstore(c, c.fnret, rhs, scroff, - rsz); + cgwidentaggedstore(c, c.fnret, rhs, "BP", + scroff, rsz); emitline("\tMOVQ\t"); emitoff(scroff: i64); emitline("(BP), AX\n"); @@ -13101,7 +13191,7 @@ fn cglet(c: *cgen, n: *node) void = { // ABI call), struct payload (literal/ident), str payload, // scalar payload — with tag remap for tagged-subset widening. if (istaggedtype(c, tn)) { - cgwidentaggedstore(c, tn, rhs, off, sz); + cgwidentaggedstore(c, tn, rhs, "BP", off, sz); c.lastwasreturn = 0; return; }; @@ -14118,6 +14208,61 @@ fn scanlocals(c: *cgen, n: *node) i32 = { }; }; }; + // Tagged-union struct-field write: `s.f = v` or `(*p).f = v` + // where f is a tagged-union field. cgassign delegates to + // cgwidentaggedstore; for pointer-rooted dst the wrapper + // allocates @tagbase (8B) and @tagscr (slot_sz). Both names + // dedup with other tagged scratch users in the same function. + if (n.kind == nkind.N_ASSIGN) { + let alhs: *node = n.lhs; + if (alhs != nil) { + if (alhs.kind == nkind.N_DOT) { + let abase: *node = alhs.lhs; + if (abase != nil) { + if (abase.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, abase.str); + let btn: *node = nil; + if (lc != nil) { btn = lc.tnode; } + else { btn = letvartnode(c, abase.str); }; + let isptr: bool = false; + let stype: *node = nil; + if (btn != nil) { + if (btn.kind == nkind.N_TPTR) { + isptr = true; + stype = btn.lhs; + }; + if (btn.kind == nkind.N_TNAME) { stype = btn; }; + }; + if (stype != nil) { + if (stype.kind == nkind.N_TNAME) { + let si: *structinfo = structlookup(c, stype.str); + if (si != nil) { + let fi: *fieldinfo = si.fields; + for (fi != nil) { + if (streq(fi.fname, alhs.str)) { + if (istaggedtype(c, fi.tnode)) { + if (isptr) { + if (!scanseenmark(c, "@tagbase")) { + total += 8; + }; + if (!scanseenmark(c, "@tagscr")) { + total += 24; + }; + }; + }; + fi = nil; + } else { + fi = fi.finext; + }; + }; + }; + }; + }; + }; + }; + }; + }; + }; // Tagged-union return with struct payload or tagged-subset // source — cgreturn materialises in @tagscr then loads // AX/DX/CX. Detect via the same rhsstructpayload predicate diff --git a/selfhost/cmd/wcc/cgendecl.ww b/selfhost/cmd/wcc/cgendecl.ww index e10fc09f..7af3b97f 100644 --- a/selfhost/cmd/wcc/cgendecl.ww +++ b/selfhost/cmd/wcc/cgendecl.ww @@ -204,6 +204,61 @@ fn scanlocals(c: *cgen, n: *node) i32 = { }; }; }; + // Tagged-union struct-field write: `s.f = v` or `(*p).f = v` + // where f is a tagged-union field. cgassign delegates to + // cgwidentaggedstore; for pointer-rooted dst the wrapper + // allocates @tagbase (8B) and @tagscr (slot_sz). Both names + // dedup with other tagged scratch users in the same function. + if (n.kind == nkind.N_ASSIGN) { + let alhs: *node = n.lhs; + if (alhs != nil) { + if (alhs.kind == nkind.N_DOT) { + let abase: *node = alhs.lhs; + if (abase != nil) { + if (abase.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, abase.str); + let btn: *node = nil; + if (lc != nil) { btn = lc.tnode; } + else { btn = letvartnode(c, abase.str); }; + let isptr: bool = false; + let stype: *node = nil; + if (btn != nil) { + if (btn.kind == nkind.N_TPTR) { + isptr = true; + stype = btn.lhs; + }; + if (btn.kind == nkind.N_TNAME) { stype = btn; }; + }; + if (stype != nil) { + if (stype.kind == nkind.N_TNAME) { + let si: *structinfo = structlookup(c, stype.str); + if (si != nil) { + let fi: *fieldinfo = si.fields; + for (fi != nil) { + if (streq(fi.fname, alhs.str)) { + if (istaggedtype(c, fi.tnode)) { + if (isptr) { + if (!scanseenmark(c, "@tagbase")) { + total += 8; + }; + if (!scanseenmark(c, "@tagscr")) { + total += 24; + }; + }; + }; + fi = nil; + } else { + fi = fi.finext; + }; + }; + }; + }; + }; + }; + }; + }; + }; + }; // Tagged-union return with struct payload or tagged-subset // source — cgreturn materialises in @tagscr then loads // AX/DX/CX. Detect via the same rhsstructpayload predicate diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 4ee5f668..43000aad 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -2745,7 +2745,7 @@ fn cgcall(c: *cgen, n: *node) void = { let slot: i32 = doff + j * esz; if (velemtagged) { cgwidentaggedstore(c, varp.lhs, - aa2, slot, esz); + aa2, "BP", slot, esz); } else { if (velemstr) { cgexpr(c, aa2); emitline("\tMOVQ\tAX, "); @@ -3001,7 +3001,7 @@ fn cgassign(c: *cgen, n: *node) void = { if (istaggedtype(c, lc.tnode)) { let lsz: i32 = slotsize(c, lc.tnode); cgwidentaggedstore(c, lc.tnode, - n.rhs, lc.off, lsz); + n.rhs, "BP", lc.off, lsz); return; }; }; @@ -3225,7 +3225,7 @@ fn cgassign(c: *cgen, n: *node) void = { zz += 8; }; cgwidentaggedstore(c, elemtn, n.rhs, - scroff, slot_sz); + "BP", scroff, slot_sz); cgexpr(c, idx); if (slot_sz > 1) { emitline("\tMOVQ\t$"); @@ -3571,6 +3571,20 @@ fn cgassign(c: *cgen, n: *node) void = { for (fi != nil) { let fn_: str = fi.fname; if (streq(fn_, fld)) { + // Tagged-union field via *struct base — full slot + // rewrite via cgwidentaggedstore basereg="BX". Pre-#26 + // fell through to the scalar store and dropped tag + // + payload. + if (n.op == tkind.TK_ASSIGN + && istaggedtype(c, fi.tnode)) { + let fsz: i32 = slotsize(c, fi.tnode); + emitline("\tMOVQ\t"); + emitoff(lc.off: i64); + emitline("(BP), BX\n"); + cgwidentaggedstore(c, fi.tnode, + n.rhs, "BX", fi.foff, fsz); + return; + }; // struct-typed field, struct-ident rhs through // *struct base: cgexpr can't materialise a whole // struct value, so word-copy from the rhs slot @@ -3721,6 +3735,17 @@ fn cgassign(c: *cgen, n: *node) void = { for (fi != nil) { let fn_: str = fi.fname; if (streq(fn_, fld)) { + // Tagged-union field in a direct struct local — + // full slot rewrite at (lc.off + fi.foff)(BP) + // via cgwidentaggedstore basereg="BP". Pre-#26 + // fell through and dropped tag + payload. + if (n.op == tkind.TK_ASSIGN + && istaggedtype(c, fi.tnode)) { + let fsz: i32 = slotsize(c, fi.tnode); + cgwidentaggedstore(c, fi.tnode, + n.rhs, "BP", lc.off + fi.foff, fsz); + return; + }; // struct-typed field, struct-ident rhs: // word-copy direct, skipping cgexpr (no // register convention for a whole struct diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index 3f14e232..dc5d56c0 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -188,8 +188,8 @@ fn cgreturn(c: *cgen, n: *node) void = { emitline("(BP)\n"); zz += 8; }; - cgwidentaggedstore(c, c.fnret, rhs, scroff, - rsz); + cgwidentaggedstore(c, c.fnret, rhs, "BP", + scroff, rsz); emitline("\tMOVQ\t"); emitoff(scroff: i64); emitline("(BP), AX\n"); @@ -311,7 +311,7 @@ fn cglet(c: *cgen, n: *node) void = { // ABI call), struct payload (literal/ident), str payload, // scalar payload — with tag remap for tagged-subset widening. if (istaggedtype(c, tn)) { - cgwidentaggedstore(c, tn, rhs, off, sz); + cgwidentaggedstore(c, tn, rhs, "BP", off, sz); c.lastwasreturn = 0; return; }; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 8153cbd0..0d76b4dd 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -174,7 +174,7 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { emitline("(BP)\n"); zz += 8; }; - cgwidentaggedstore(c, ptype, arg, scroff, widensz); + cgwidentaggedstore(c, ptype, arg, "BP", scroff, widensz); let pp: i32 = widensz - 8; for (pp >= 0) { emitline("\tMOVQ\t"); @@ -1368,6 +1368,7 @@ fn slotsize(c: *cgen, typn: *node) i32 = { fn fieldsize(c: *cgen, tnode: *node) i32 = { if (tnode == nil) { return 8; }; let k: nkind = tnode.kind; + if (k == nkind.N_TTAGGED){ return slotsize(c, tnode); }; if (k == nkind.N_TNAME) { let nm: str = tnode.str; if (streq(nm, "str")) { return 16; }; @@ -1387,6 +1388,13 @@ fn fieldsize(c: *cgen, tnode: *node) i32 = { }; return 4; // default storage is i32 }; + // Type alias to a tagged-union — recurse through aliaslookup + // so `e: ev` (where `ev = (i64 | i32)`) takes 16B in the + // containing struct rather than the 8B default. + if (c != nil) { + let aliased: *node = aliaslookup(c, nm); + if (aliased != nil) { return fieldsize(c, aliased); }; + }; return 8; }; if (k == nkind.N_TPTR) { return 8; }; @@ -2063,9 +2071,20 @@ fn rhstaggedabicall(c: *cgen, src: *node) bool = { return false; }; -// cgwidentaggedstore — write tagged-union slot bytes for `src` into the -// slot at BP+slot_off, sized to slot_sz. Mirrors cg_widen_tagged_store -// in cmd/w6c/cgen.c. Branches by source shape: +// cgwidentaggedstore — write tagged-union slot bytes for `src` into +// the slot at `basereg`+slot_off, sized to slot_sz. Mirrors +// cg_widen_tagged_store in cmd/w6c/cgen.c. +// +// `basereg` selects the addressing root: +// - "BP": function-frame slot (let / assign / return / structlit / +// array-elem scratch). Body writes straight to slot_off(BP). +// - else (e.g. "BX" for *struct field, top-level struct LEAQ +// base): pointer-rooted dst. cgexpr inside trashes every GPR, +// so we route through a fresh BP-rooted scratch slot, spill +// basereg before the body, reload after, then word-copy +// scratch → (basereg, slot_off). +// +// Branches by source shape: // - nullable dst (8B slot): cgexpr → AX → slot+0. // - tagged src ident: copy slot words, zero-pad, tag-remap. // - tagged src via AX/DX/CX ABI (call / tagged-arr index): cgexpr, @@ -2076,7 +2095,53 @@ fn rhstaggedabicall(c: *cgen, src: *node) bool = { // tag last. // - str src: tag@+0, ptr@+8, len@+16. // - scalar src: tag@+0, value@+8. -fn cgwidentaggedstore(c: *cgen, dst: *node, src: *node, slot_off: i32, slot_sz: i32) void = { +fn cgwidentaggedstore(c: *cgen, dst: *node, src: *node, + basereg: str, slot_off: i32, slot_sz: i32) void = { + if (streq(basereg, "BP")) { + cgwidentaggedstorebp(c, dst, src, slot_off, slot_sz); + return; + }; + // Pointer-rooted dst: spill basereg (cgexpr will trash it), + // materialise into a BP-rooted scratch via the BP path, then + // reload basereg and word-copy scratch → caller's slot. + let bspill: i32 = localadd(c, "@tagbase", 8, nil); + emitline("\tMOVQ\t"); + emitline(basereg); + emitline(", "); + emitoff(bspill: i64); + emitline("(BP)\n"); + let scr: i32 = localadd(c, "@tagscr", slot_sz, nil); + emitline("\tXORQ\tAX, AX\n"); + let z: i32 = 0; + for (z < slot_sz) { + emitline("\tMOVQ\tAX, "); + emitoff((scr + z): i64); + emitline("(BP)\n"); + z += 8; + }; + cgwidentaggedstorebp(c, dst, src, scr, slot_sz); + emitline("\tMOVQ\t"); + emitoff(bspill: i64); + emitline("(BP), "); + emitline(basereg); + emitline("\n"); + let k: i32 = 0; + for (k < slot_sz) { + emitline("\tMOVQ\t"); + emitoff((scr + k): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tAX, "); + emitdispreg((slot_off + k): i64, basereg); + emitline("\n"); + k += 8; + }; +}; + +// cgwidentaggedstorebp — BP-rooted body. Called via cgwidentaggedstore +// for the natural "BP" case and via the wrapper's scratch path for +// pointer-rooted dst. Direct callers exist only in case of future +// inlined uses inside this file; new code should call the wrapper. +fn cgwidentaggedstorebp(c: *cgen, dst: *node, src: *node, slot_off: i32, slot_sz: i32) void = { let dt: *node = resolvetagged(c, dst); if (dt == nil) { return; }; // Nullable fold: one 8B word holding the pointer (or 0 for void). diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 927a6fe5..e736c250 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -5744,7 +5744,7 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { emitline("(BP)\n"); zz += 8; }; - cgwidentaggedstore(c, ptype, arg, scroff, widensz); + cgwidentaggedstore(c, ptype, arg, "BP", scroff, widensz); let pp: i32 = widensz - 8; for (pp >= 0) { emitline("\tMOVQ\t"); @@ -6938,6 +6938,7 @@ fn slotsize(c: *cgen, typn: *node) i32 = { fn fieldsize(c: *cgen, tnode: *node) i32 = { if (tnode == nil) { return 8; }; let k: nkind = tnode.kind; + if (k == nkind.N_TTAGGED){ return slotsize(c, tnode); }; if (k == nkind.N_TNAME) { let nm: str = tnode.str; if (streq(nm, "str")) { return 16; }; @@ -6957,6 +6958,13 @@ fn fieldsize(c: *cgen, tnode: *node) i32 = { }; return 4; // default storage is i32 }; + // Type alias to a tagged-union — recurse through aliaslookup + // so `e: ev` (where `ev = (i64 | i32)`) takes 16B in the + // containing struct rather than the 8B default. + if (c != nil) { + let aliased: *node = aliaslookup(c, nm); + if (aliased != nil) { return fieldsize(c, aliased); }; + }; return 8; }; if (k == nkind.N_TPTR) { return 8; }; @@ -7633,9 +7641,20 @@ fn rhstaggedabicall(c: *cgen, src: *node) bool = { return false; }; -// cgwidentaggedstore — write tagged-union slot bytes for `src` into the -// slot at BP+slot_off, sized to slot_sz. Mirrors cg_widen_tagged_store -// in cmd/w6c/cgen.c. Branches by source shape: +// cgwidentaggedstore — write tagged-union slot bytes for `src` into +// the slot at `basereg`+slot_off, sized to slot_sz. Mirrors +// cg_widen_tagged_store in cmd/w6c/cgen.c. +// +// `basereg` selects the addressing root: +// - "BP": function-frame slot (let / assign / return / structlit / +// array-elem scratch). Body writes straight to slot_off(BP). +// - else (e.g. "BX" for *struct field, top-level struct LEAQ +// base): pointer-rooted dst. cgexpr inside trashes every GPR, +// so we route through a fresh BP-rooted scratch slot, spill +// basereg before the body, reload after, then word-copy +// scratch → (basereg, slot_off). +// +// Branches by source shape: // - nullable dst (8B slot): cgexpr → AX → slot+0. // - tagged src ident: copy slot words, zero-pad, tag-remap. // - tagged src via AX/DX/CX ABI (call / tagged-arr index): cgexpr, @@ -7646,7 +7665,53 @@ fn rhstaggedabicall(c: *cgen, src: *node) bool = { // tag last. // - str src: tag@+0, ptr@+8, len@+16. // - scalar src: tag@+0, value@+8. -fn cgwidentaggedstore(c: *cgen, dst: *node, src: *node, slot_off: i32, slot_sz: i32) void = { +fn cgwidentaggedstore(c: *cgen, dst: *node, src: *node, + basereg: str, slot_off: i32, slot_sz: i32) void = { + if (streq(basereg, "BP")) { + cgwidentaggedstorebp(c, dst, src, slot_off, slot_sz); + return; + }; + // Pointer-rooted dst: spill basereg (cgexpr will trash it), + // materialise into a BP-rooted scratch via the BP path, then + // reload basereg and word-copy scratch → caller's slot. + let bspill: i32 = localadd(c, "@tagbase", 8, nil); + emitline("\tMOVQ\t"); + emitline(basereg); + emitline(", "); + emitoff(bspill: i64); + emitline("(BP)\n"); + let scr: i32 = localadd(c, "@tagscr", slot_sz, nil); + emitline("\tXORQ\tAX, AX\n"); + let z: i32 = 0; + for (z < slot_sz) { + emitline("\tMOVQ\tAX, "); + emitoff((scr + z): i64); + emitline("(BP)\n"); + z += 8; + }; + cgwidentaggedstorebp(c, dst, src, scr, slot_sz); + emitline("\tMOVQ\t"); + emitoff(bspill: i64); + emitline("(BP), "); + emitline(basereg); + emitline("\n"); + let k: i32 = 0; + for (k < slot_sz) { + emitline("\tMOVQ\t"); + emitoff((scr + k): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tAX, "); + emitdispreg((slot_off + k): i64, basereg); + emitline("\n"); + k += 8; + }; +}; + +// cgwidentaggedstorebp — BP-rooted body. Called via cgwidentaggedstore +// for the natural "BP" case and via the wrapper's scratch path for +// pointer-rooted dst. Direct callers exist only in case of future +// inlined uses inside this file; new code should call the wrapper. +fn cgwidentaggedstorebp(c: *cgen, dst: *node, src: *node, slot_off: i32, slot_sz: i32) void = { let dt: *node = resolvetagged(c, dst); if (dt == nil) { return; }; // Nullable fold: one 8B word holding the pointer (or 0 for void). @@ -10793,7 +10858,7 @@ fn cgcall(c: *cgen, n: *node) void = { let slot: i32 = doff + j * esz; if (velemtagged) { cgwidentaggedstore(c, varp.lhs, - aa2, slot, esz); + aa2, "BP", slot, esz); } else { if (velemstr) { cgexpr(c, aa2); emitline("\tMOVQ\tAX, "); @@ -11049,7 +11114,7 @@ fn cgassign(c: *cgen, n: *node) void = { if (istaggedtype(c, lc.tnode)) { let lsz: i32 = slotsize(c, lc.tnode); cgwidentaggedstore(c, lc.tnode, - n.rhs, lc.off, lsz); + n.rhs, "BP", lc.off, lsz); return; }; }; @@ -11273,7 +11338,7 @@ fn cgassign(c: *cgen, n: *node) void = { zz += 8; }; cgwidentaggedstore(c, elemtn, n.rhs, - scroff, slot_sz); + "BP", scroff, slot_sz); cgexpr(c, idx); if (slot_sz > 1) { emitline("\tMOVQ\t$"); @@ -11619,6 +11684,20 @@ fn cgassign(c: *cgen, n: *node) void = { for (fi != nil) { let fn_: str = fi.fname; if (streq(fn_, fld)) { + // Tagged-union field via *struct base — full slot + // rewrite via cgwidentaggedstore basereg="BX". Pre-#26 + // fell through to the scalar store and dropped tag + // + payload. + if (n.op == tkind.TK_ASSIGN + && istaggedtype(c, fi.tnode)) { + let fsz: i32 = slotsize(c, fi.tnode); + emitline("\tMOVQ\t"); + emitoff(lc.off: i64); + emitline("(BP), BX\n"); + cgwidentaggedstore(c, fi.tnode, + n.rhs, "BX", fi.foff, fsz); + return; + }; // struct-typed field, struct-ident rhs through // *struct base: cgexpr can't materialise a whole // struct value, so word-copy from the rhs slot @@ -11769,6 +11848,17 @@ fn cgassign(c: *cgen, n: *node) void = { for (fi != nil) { let fn_: str = fi.fname; if (streq(fn_, fld)) { + // Tagged-union field in a direct struct local — + // full slot rewrite at (lc.off + fi.foff)(BP) + // via cgwidentaggedstore basereg="BP". Pre-#26 + // fell through and dropped tag + payload. + if (n.op == tkind.TK_ASSIGN + && istaggedtype(c, fi.tnode)) { + let fsz: i32 = slotsize(c, fi.tnode); + cgwidentaggedstore(c, fi.tnode, + n.rhs, "BP", lc.off + fi.foff, fsz); + return; + }; // struct-typed field, struct-ident rhs: // word-copy direct, skipping cgexpr (no // register convention for a whole struct @@ -12978,8 +13068,8 @@ fn cgreturn(c: *cgen, n: *node) void = { emitline("(BP)\n"); zz += 8; }; - cgwidentaggedstore(c, c.fnret, rhs, scroff, - rsz); + cgwidentaggedstore(c, c.fnret, rhs, "BP", + scroff, rsz); emitline("\tMOVQ\t"); emitoff(scroff: i64); emitline("(BP), AX\n"); @@ -13101,7 +13191,7 @@ fn cglet(c: *cgen, n: *node) void = { // ABI call), struct payload (literal/ident), str payload, // scalar payload — with tag remap for tagged-subset widening. if (istaggedtype(c, tn)) { - cgwidentaggedstore(c, tn, rhs, off, sz); + cgwidentaggedstore(c, tn, rhs, "BP", off, sz); c.lastwasreturn = 0; return; }; @@ -14118,6 +14208,61 @@ fn scanlocals(c: *cgen, n: *node) i32 = { }; }; }; + // Tagged-union struct-field write: `s.f = v` or `(*p).f = v` + // where f is a tagged-union field. cgassign delegates to + // cgwidentaggedstore; for pointer-rooted dst the wrapper + // allocates @tagbase (8B) and @tagscr (slot_sz). Both names + // dedup with other tagged scratch users in the same function. + if (n.kind == nkind.N_ASSIGN) { + let alhs: *node = n.lhs; + if (alhs != nil) { + if (alhs.kind == nkind.N_DOT) { + let abase: *node = alhs.lhs; + if (abase != nil) { + if (abase.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, abase.str); + let btn: *node = nil; + if (lc != nil) { btn = lc.tnode; } + else { btn = letvartnode(c, abase.str); }; + let isptr: bool = false; + let stype: *node = nil; + if (btn != nil) { + if (btn.kind == nkind.N_TPTR) { + isptr = true; + stype = btn.lhs; + }; + if (btn.kind == nkind.N_TNAME) { stype = btn; }; + }; + if (stype != nil) { + if (stype.kind == nkind.N_TNAME) { + let si: *structinfo = structlookup(c, stype.str); + if (si != nil) { + let fi: *fieldinfo = si.fields; + for (fi != nil) { + if (streq(fi.fname, alhs.str)) { + if (istaggedtype(c, fi.tnode)) { + if (isptr) { + if (!scanseenmark(c, "@tagbase")) { + total += 8; + }; + if (!scanseenmark(c, "@tagscr")) { + total += 24; + }; + }; + }; + fi = nil; + } else { + fi = fi.finext; + }; + }; + }; + }; + }; + }; + }; + }; + }; + }; // Tagged-union return with struct payload or tagged-subset // source — cgreturn materialises in @tagscr then loads // AX/DX/CX. Detect via the same rhsstructpayload predicate diff --git a/test/wcc/681_arr_elem_field_write.c b/test/wcc/681_arr_elem_field_write.c index c53a6636..f873e78f 100644 --- a/test/wcc/681_arr_elem_field_write.c +++ b/test/wcc/681_arr_elem_field_write.c @@ -423,6 +423,72 @@ static const struct row rows[] = { " return 42;\n" "};\n", 42 }, + /* Whole-tagged rhs to struct tagged-union field (task #26). cgen + * N_ASSIGN had a TY_TAGGED branch that called cg_tag_for_variant + * with the rhs type — when the rhs was the tagged union itself + * (not a concrete variant), the function returned -1 and the + * branch wrote literal 0 as the tag plus only AX as the payload, + * dropping the original tag and trailing payload words. Fixed + * by routing through cg_widen_tagged_store, which copies all slot + * words from the source's tagged slot and runs cg_widen_tag_remap. + * + * Verification: the write must not bleed into the trailing `mark` + * field. Direct struct local. Reading x.e back via match is a + * separate code path (gated on a wwstage read-side bug for + * tagged N_DOT — filed below as a follow-up); these rows pin the + * write-side bug and let mark act as a canary for stray stores + * past the field boundary. */ + { "tagged_field_value_write_local_mark_canary", + "type ev = (i64 | i32);\n" + "type holder = struct { e: ev, mark: i32 };\n" + "fn main() i32 = {\n" + " let v: ev = 33: i32;\n" + " let x: holder;\n" + " x.mark = 0x3a3a3a3a;\n" + " x.e = v;\n" + " if (x.mark != 0x3a3a3a3a) { return 1; };\n" + " return 42;\n" + "};\n", + 42 }, + /* Same shape, concrete-variant str widening. The str payload + * occupies +8/+16 (ptr/len); a missing store to +16 (the pre-#26 + * shape that wrote only AX) would leave .len at whatever the + * neighbouring memory held. mark canary at +24 catches a stray + * AX-only path that wrote past 16B into the mark slot. */ + { "tagged_field_value_write_str_variant_mark_canary", + "type ev = (i32 | str);\n" + "type holder = struct { e: ev, mark: i32 };\n" + "fn main() i32 = {\n" + " let x: holder;\n" + " x.mark = 0x5e5e5e5e;\n" + " x.e = (\"hello\": ev);\n" + " if (x.mark != 0x5e5e5e5e) { return 1; };\n" + " return 42;\n" + "};\n", + 42 }, + /* Pointer-rooted dst — `(*p).e = v` exercises the base-reg + * parameterisation added to cg_widen_tagged_store in #26. The + * wrapper spills BX (the *struct pointer), routes the body + * through a BP-rooted scratch, reloads BX, and word-copies the + * scratch to (BX, foff). Without that path the via_ptr arm of + * the new TY_TAGGED branch would either trample BX during cgexpr + * or write to a stale slot address. mark canary at +24 catches + * a stray spill that wrote past the e field. */ + { "tagged_field_value_write_via_ptr_mark_canary", + "type ev = (i64 | i32);\n" + "type holder = struct { e: ev, mark: i32 };\n" + "fn fill(h: *holder) void = {\n" + " let v: ev = 33: i32;\n" + " h.mark = 0x77777777;\n" + " h.e = v;\n" + "};\n" + "fn main() i32 = {\n" + " let x: holder;\n" + " fill(&x);\n" + " if (x.mark != 0x77777777) { return 1; };\n" + " return 42;\n" + "};\n", + 42 }, }; static int