From bacbf4b8455a79a61044e1ebea6822598c8b9ea2 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 15 May 2026 00:45:14 +0900 Subject: [PATCH] w6c+selfhost: cgdot N_DOT tagged-field source ABI (closes #28) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cgdot of a tagged-union struct field previously dropped the AX/DX/CX/R8 payload-register convention used by tagged-union returns: cstage's direct-struct branch stopped at CX (size > 16) and never loaded R8 (slice-payload variants, slot 32B); the via_ptr branch had no TY_TAGGED handler at all, falling through to fldloadop and yielding only the tag in AX. The N_DOT scrutinee fallback in N_MATCH similarly stored only AX into the spill slot. Wwstage cgdot had no TY_TAGGED branch in any of the direct, *struct, or top-level-global field-load paths, and cgmatch's non-ident scrutinee branch didn't recognise N_DOT — dispatch always computed want = 0 and the spill scratch was hardcoded 24B. The combined effect: any code reading `s.taggedfield` and consuming more than one quadword of the payload saw garbage in the upper halves. Cstage: extended the direct-struct TY_TAGGED branch with an R8 load for size > 24 (CX still loaded last so global LEAQ-into-CX rooting survives), added a parallel TY_TAGGED handler to the via_ptr (TY_PTR inner TY_STRUCT) field branch, and extended the N_DOT scrutinee spill fallback in N_MATCH to write DX/CX/R8 alongside AX. Wwstage: new cgloadtaggedfield helper emits the four-register load with CX-last ordering, and dotfieldtnode resolves a field's declared type node for a local-ident or *struct base. cgdot grew three TY_TAGGED branches (direct local, *struct deref staging in BX, top-level global through CX). cgmatch's non-ident-scrutinee branch grew an N_DOT type- extraction path mirroring the N_CALL / N_INDEX shapes and now sizes the @match_spill slot from slotsize(scrutt) so slice-payload variants don't overflow the historical 24B alloc. rhstaggedabicall accepts N_DOT so `let copy: ev = h.e;` and tagged-arg call sites pass through the tagged-source spill branch of cgwidentaggedstore. Out of scope for #28 and left as separate latents: wwstage's match-arm bind for a TY_STRUCT-typed variant copies only 8B (cstage falls back to bu->size; wwstage's bsz=8 default), and the variant-index lookup for an i64 literal in (i32 | i64) picks the wrong tag on the write side. Both surface in struct-payload tagged unions and merit their own tasks; the new test rows steer clear so #28's fix verifies end-to-end on scalar / str / slice payloads. Test 693_dot_tagged_source — three variant shapes (16B i64, 24B str, 32B slice) read from direct local, *struct param, top-level global, and let-init round-trip. The 32B-slice rows verify v.cap (R8 / +24) so dropping the upper-word load isn't masked by len-only checks; the top-level-global row routes the write through *p because the direct global-LHS tagged store is a separate wwstage gap (followup). Three negative controls (untagged i32 / str / slice fields) keep the new TY_TAGGED guard from shadowing the existing field-load paths. Wired into make test; 37 tests total. Bootstrap ww2 == ww3 == ww4 byte-identical. --- Makefile | 7 + cmd/w6c/cgen.c | 62 ++++- selfhost/cmd/w6c/main.combined.ww | 177 +++++++++++- selfhost/cmd/wcc/cgenexpr.ww | 85 +++++- selfhost/cmd/wcc/cgenutil.ww | 92 ++++++- selfhost/cmd/wwdump/main.combined.ww | 177 +++++++++++- test/wcc/693_dot_tagged_source.c | 395 +++++++++++++++++++++++++++ 7 files changed, 948 insertions(+), 47 deletions(-) create mode 100644 test/wcc/693_dot_tagged_source.c diff --git a/Makefile b/Makefile index 7b628a75..f8dfb5d2 100644 --- a/Makefile +++ b/Makefile @@ -221,6 +221,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_arr_elem_field_write \ $(BIN)/test_dot_str_chained_arg \ $(BIN)/test_dot_slice_arg \ + $(BIN)/test_dot_tagged_source \ $(BIN)/test_field_signed $(BIN)/test_frame_argcount \ $(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \ $(BIN)/test_w6c_ww $(BIN)/test_ww_ww $(BIN)/test_self_rebuild \ @@ -326,6 +327,12 @@ $(BIN)/test_dot_slice_arg: test/wcc/691_dot_slice_arg.c $(BIN)/ww \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_dot_tagged_source: test/wcc/693_dot_tagged_source.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_field_signed: test/wcc/660_field_signed.c $(BIN)/ww \ $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 69f76a81..e6a2bae9 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -3875,12 +3875,27 @@ cgexpr(Cg *c, Node *n, Local *locals) int boff = localfind(locals, s->lhs->str); sl_off = boff + (int)f->offset; } else { - /* fall back to spill */ + /* fall back to spill — `match (h.e)` where + * h is *struct. cgexpr → cgdot now leaves the + * AX=tag, DX=val0, CX=val1[, R8=val2] shape + * (task #28), so spill all words the variant + * may carry. Pre-#28 only AX landed and the + * dispatch fired on a stale slot. */ sl_off = localoff(c, &locals, "@match_spill", slot_size, cg_frame); cgexpr(c, s, locals); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, sl_off + 0)); + if (!is_nullable) { + ins2(c, A_MOVQ, areg(D_DX), + amem(D_BP, sl_off + 8)); + if (slot_size > 16) + ins2(c, A_MOVQ, areg(D_CX), + amem(D_BP, sl_off + 16)); + if (slot_size > 24) + ins2(c, A_MOVQ, areg(D_R8), + amem(D_BP, sl_off + 24)); + } } } else { /* Spill non-ident scrutinees (e.g. `match (foo()?)`) into @@ -4648,10 +4663,13 @@ cgexpr(Cg *c, Node *n, Local *locals) for (Tfield *f = u->fields; f; f = f->next) { if (strcmp(f->name, n->str) != 0) continue; /* tagged-union field: load AX=tag, DX=val0, - * CX=val1 (last, since for globals CX is also - * the base addr). Mirrors the tagged-return - * ABI so the let-init / match dispatch shapes - * just work. */ + * CX=val1, R8=val2 (CX last, since for globals + * CX is also the base addr; load R8 before CX + * so the base address survives the +24 read). + * Mirrors the tagged-return ABI so the let-init + * / match dispatch shapes just work. The val2 + * word fires for slice-variant tagged-unions + * (slot = 8 tag + 24 slice header = 32B). */ Type *tag_fu = (f->type && f->type->kind == TY_NAMED) ? f->type->under : f->type; if (tag_fu && tag_fu->kind == TY_TAGGED) { @@ -4660,6 +4678,10 @@ cgexpr(Cg *c, Node *n, Local *locals) amem(base_reg, fo + 0), areg(D_AX)); ins2(c, A_MOVQ, amem(base_reg, fo + 8), areg(D_DX)); + if (tag_fu->size > 24) + ins2(c, A_MOVQ, + amem(base_reg, fo + 24), + areg(D_R8)); if (tag_fu->size > 16) ins2(c, A_MOVQ, amem(base_reg, fo + 16), @@ -4744,6 +4766,36 @@ cgexpr(Cg *c, Node *n, Local *locals) ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX)); for (Tfield *f = inner->fields; f; f = f->next) { if (strcmp(f->name, n->str) != 0) continue; + /* tagged-union field through *struct: BX + * already holds the *struct pointer. Load + * the four payload regs from (BX, f->offset) + * — BX is not a target (AX/DX/CX/R8), so + * load order is harmless. Mirrors the direct- + * struct branch above so consumers see the + * same tagged-return register shape + * regardless of pointer rooting. Pre-#28 fell + * through to fldloadop and dropped the + * payload words. */ + Type *ptag_fu = (f->type && f->type->kind == TY_NAMED) + ? f->type->under : f->type; + if (ptag_fu && ptag_fu->kind == TY_TAGGED) { + int fo = (int)f->offset; + ins2(c, A_MOVQ, + amem(D_BX, fo + 0), + areg(D_AX)); + ins2(c, A_MOVQ, + amem(D_BX, fo + 8), + areg(D_DX)); + if (ptag_fu->size > 16) + ins2(c, A_MOVQ, + amem(D_BX, fo + 16), + areg(D_CX)); + if (ptag_fu->size > 24) + ins2(c, A_MOVQ, + amem(D_BX, fo + 24), + areg(D_R8)); + break; + } /* str field through *struct: read len into a * scratch first (it's at +8) so loading ptr * into AX last leaves (AX=ptr, BX=len). We diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 78a4b1f1..07a7fde0 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -7680,10 +7680,50 @@ fn rhstaggedident(c: *cgen, src: *node) *node = { return resolvetagged(c, tn); }; +// dotfieldtnode — for an N_DOT src whose base is a local ident or +// *struct, return the declared type node of the named field, or nil +// if the shape doesn't resolve (e.g. enum-member access, pseudo- +// field `.len`, top-level global). Used by rhstaggedabicall and +// related predicates to walk into the field's tagged type. +fn dotfieldtnode(c: *cgen, n: *node) *node = { + if (n == nil) { return nil; }; + if (n.kind != nkind.N_DOT) { return nil; }; + let base: *node = n.lhs; + let fld: str = n.str; + if (base == nil) { return nil; }; + if (base.kind != nkind.N_IDENT) { return nil; }; + let lc: *local = localfindnode(c, base.str); + let btn: *node = nil; + if (lc != nil) { btn = lc.tnode; } + else { btn = letvartnode(c, base.str); }; + if (btn == nil) { return nil; }; + let bk: nkind = btn.kind; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (bk == nkind.N_TPTR) { + let inner: *node = btn.lhs; + if (inner != nil) { + if (inner.kind == nkind.N_TNAME) { sname = inner.str; }; + }; + }; + if (bk == nkind.N_TNAME) { sname = btn.str; }; + if (sname.len == 0) { return nil; }; + let si: *structinfo = structlookup(c, sname); + if (si == nil) { return nil; }; + let fi: *fieldinfo = si.fields; + for (fi != nil) { + if (streq(fi.fname, fld)) { return fi.tnode; }; + fi = fi.finext; + }; + return nil; +}; + // rhstaggedabicall — does `src` produce a tagged value via the AX/DX/CX -// return ABI? True for N_CALL of a tagged-returning fn and N_INDEX of a -// tagged-element base. Used to decide whether cgexpr/spill works for the -// tagged-source branch of cgwidentaggedstore. +// return ABI? True for N_CALL of a tagged-returning fn, N_INDEX of a +// tagged-element base, and N_DOT of a tagged-typed struct field (after +// #28's cgdot fix loads AX/DX/CX/R8 from the field's slot). Used to +// decide whether cgexpr/spill works for the tagged-source branch of +// cgwidentaggedstore. fn rhstaggedabicall(c: *cgen, src: *node) bool = { if (src == nil) { return false; }; if (src.kind == nkind.N_CALL) { @@ -7725,9 +7765,55 @@ fn rhstaggedabicall(c: *cgen, src: *node) bool = { }; }; }; + // N_DOT of a tagged-typed struct field — cgdot loads + // AX=tag, DX=word0, CX=word1[, R8=word2], so downstream + // spill matches the call/index shapes. + if (src.kind == nkind.N_DOT) { + let ft: *node = dotfieldtnode(c, src); + if (ft != nil) { + if (istaggedtype(c, ft)) { return true; }; + }; + }; return false; }; +// cgloadtaggedfield — load a tagged-union slot at `basereg`+foff +// into the tagged-return ABI registers (AX=tag, DX=word0, CX=word1, +// R8=word2). Slot sizes: 16B = (tag, word0), 24B = + word1, 32B +// = + word2 (slice variant). Mirrors the cstage tagged-field load +// in cmd/w6c/cgen.c (N_DOT TY_STRUCT/TY_PTR branches). +// +// Load order is fixed regardless of basereg: tag, word0, word2, +// word1. CX (word1 target) goes LAST because basereg may itself +// be CX — top-level globals address via LEAQ name(SB), CX — and +// overwriting it earlier would trash the base address for the +// remaining loads. For BP / BX bases the order is harmless. +// Callers must guarantee basereg is one of "BP", "BX", "CX"; the +// only register loaded into that is NOT a target is BX, so AX- +// or DX-rooted callers must spill first. +fn cgloadtaggedfield(c: *cgen, basereg: str, foff: i32, slot_sz: i32) void = { + // tag → AX + emitline("\tMOVQ\t"); + emitdispreg(foff: i64, basereg); + emitline(", AX\n"); + // word0 → DX + emitline("\tMOVQ\t"); + emitdispreg((foff + 8): i64, basereg); + emitline(", DX\n"); + // word2 → R8 (slice variant: slot = 8 tag + 24 payload = 32). + if (slot_sz > 24) { + emitline("\tMOVQ\t"); + emitdispreg((foff + 24): i64, basereg); + emitline(", R8\n"); + }; + // word1 → CX (load LAST; conflicts with CX-base globals). + if (slot_sz > 16) { + emitline("\tMOVQ\t"); + emitdispreg((foff + 16): i64, basereg); + emitline(", CX\n"); + }; +}; + // 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. @@ -9143,17 +9229,20 @@ fn cgmatch(c: *cgen, n: *node) void = { scrutt = resolvetagged(c, lc.tnode); }; } else { - // Non-ident scrutinee (call result, arr[i], ?, etc.). - // Spill into an `@match_spill` scratch slot and - // dispatch off it. Tagged returns (N_CALL) follow the - // AX:DX:CX convention; tagged-element loads (N_INDEX) - // after the cgindex fix produce the same triple. - // Nullable returns are single-word (AX = ptr); only +0 - // is read, so the extra stores are harmless. We - // recover the scrutinee type from fnretlookup (N_CALL) - // or the base local's array element type (N_INDEX) so - // dispatch can compute variant indices. - scrutoff = localalloc(c, "@match_spill", 24, nil); + // Non-ident scrutinee (call result, arr[i], p.field, + // ?, etc.). Spill into an `@match_spill` scratch slot + // and dispatch off it. Tagged returns (N_CALL) follow + // the AX:DX:CX convention; tagged-element loads + // (N_INDEX) and tagged-field loads (N_DOT, fixed by + // #28) produce the same triple. Nullable returns are + // single-word (AX = ptr); only +0 is read, so the + // extra stores are harmless. We recover the scrutinee + // type from fnretlookup (N_CALL), the base local's + // array element type (N_INDEX), or the struct field + // type (N_DOT) so dispatch can compute variant + // indices. Slot size is then derived from the + // scrutinee type so slice-variant tagged-unions + // (32B slot) don't overflow a hardcoded 24B scratch. if (scrut.kind == nkind.N_CALL) { let callee: *node = scrut.lhs; if (callee != nil) { @@ -9188,6 +9277,21 @@ fn cgmatch(c: *cgen, n: *node) void = { }; }; }; + if (scrut.kind == nkind.N_DOT) { + let ft: *node = dotfieldtnode(c, scrut); + if (ft != nil) { + scrutt = resolvetagged(c, ft); + }; + }; + // Size the spill to the scrutinee slot. Default 24B + // preserves the historical alloc for non-tagged or + // unresolved cases (nullable, str-returning, etc.). + let spillsz: i32 = 24; + if (scrutt != nil) { + let resolved: i32 = slotsize(c, scrutt); + if (resolved > spillsz) { spillsz = resolved; }; + }; + scrutoff = localalloc(c, "@match_spill", spillsz, nil); cgexpr(c, scrut); emitline("\tMOVQ\tAX, "); emitoff(scrutoff: i64); @@ -9388,6 +9492,24 @@ fn cgdot(c: *cgen, n: *node) void = { for (fi != nil) { let fn_: str = fi.fname; if (streq(fn_, fld)) { + // tagged-union field via *struct: stage + // the *struct in BX, then load the four + // payload regs via cgloadtaggedfield. + // BX isn't a target (AX/DX/CX/R8), so + // load order doesn't matter. Mirrors + // the direct-local branch above so the + // match / let-init / call-arg consumer + // shape is identical regardless of + // pointer rooting. + if (istaggedtype(c, fi.tnode)) { + let tsz: i32 = slotsize(c, fi.tnode); + emitline("\tMOVQ\t"); + emitoff(lc.off: i64); + emitline("(BP), BX\n"); + cgloadtaggedfield(c, "BX", + fi.foff, tsz); + return; + }; // str field via *struct: load len into a // scratch first (so loading ptr into AX // last leaves (AX=ptr, BX=len)). @@ -9454,6 +9576,21 @@ fn cgdot(c: *cgen, n: *node) void = { for (fi != nil) { let fn_: str = fi.fname; if (streq(fn_, fld)) { + // tagged-union field: emit the AX=tag, + // DX=word0, CX=word1[, R8=word2] load + // sequence so the match / let-init / + // call-arg consumers see the same shape + // as a tagged-returning fn. Pre-#28 fell + // through to the scalar fieldloadop and + // only AX (tag) was loaded — payload + // words came from whatever the caller + // left in DX/CX/R8. + if (istaggedtype(c, fi.tnode)) { + let tsz: i32 = slotsize(c, fi.tnode); + cgloadtaggedfield(c, "BP", + lc.off + fi.foff, tsz); + return; + }; // str field: load both halves so chained // `.ptr` / `.len` see (AX=ptr, BX=len). if (isstrtype(c, fi.tnode)) { @@ -9670,6 +9807,18 @@ fn cgdot(c: *cgen, n: *node) void = { emitline("\tLEAQ\t"); emitsymname(c, lhs.str); emitline("(SB), CX\n"); + // tagged-union field: load via the tagged- + // return ABI off CX. cgloadtaggedfield orders + // the loads so CX (word1 target) is written + // LAST — otherwise the base address would be + // trashed before the +24/R8 (slice variant) + // read could index off it. Pre-#28 fell + // through to fieldloadop and dropped payload. + if (istaggedtype(c, fi.tnode)) { + let tsz: i32 = slotsize(c, fi.tnode); + cgloadtaggedfield(c, "CX", fi.foff, tsz); + return; + }; if (isstrtype(c, fi.tnode)) { emitline("\tMOVQ\t"); emitdispreg(fi.foff: i64, "CX"); diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 0f7c6b0c..0e997785 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -943,17 +943,20 @@ fn cgmatch(c: *cgen, n: *node) void = { scrutt = resolvetagged(c, lc.tnode); }; } else { - // Non-ident scrutinee (call result, arr[i], ?, etc.). - // Spill into an `@match_spill` scratch slot and - // dispatch off it. Tagged returns (N_CALL) follow the - // AX:DX:CX convention; tagged-element loads (N_INDEX) - // after the cgindex fix produce the same triple. - // Nullable returns are single-word (AX = ptr); only +0 - // is read, so the extra stores are harmless. We - // recover the scrutinee type from fnretlookup (N_CALL) - // or the base local's array element type (N_INDEX) so - // dispatch can compute variant indices. - scrutoff = localalloc(c, "@match_spill", 24, nil); + // Non-ident scrutinee (call result, arr[i], p.field, + // ?, etc.). Spill into an `@match_spill` scratch slot + // and dispatch off it. Tagged returns (N_CALL) follow + // the AX:DX:CX convention; tagged-element loads + // (N_INDEX) and tagged-field loads (N_DOT, fixed by + // #28) produce the same triple. Nullable returns are + // single-word (AX = ptr); only +0 is read, so the + // extra stores are harmless. We recover the scrutinee + // type from fnretlookup (N_CALL), the base local's + // array element type (N_INDEX), or the struct field + // type (N_DOT) so dispatch can compute variant + // indices. Slot size is then derived from the + // scrutinee type so slice-variant tagged-unions + // (32B slot) don't overflow a hardcoded 24B scratch. if (scrut.kind == nkind.N_CALL) { let callee: *node = scrut.lhs; if (callee != nil) { @@ -988,6 +991,21 @@ fn cgmatch(c: *cgen, n: *node) void = { }; }; }; + if (scrut.kind == nkind.N_DOT) { + let ft: *node = dotfieldtnode(c, scrut); + if (ft != nil) { + scrutt = resolvetagged(c, ft); + }; + }; + // Size the spill to the scrutinee slot. Default 24B + // preserves the historical alloc for non-tagged or + // unresolved cases (nullable, str-returning, etc.). + let spillsz: i32 = 24; + if (scrutt != nil) { + let resolved: i32 = slotsize(c, scrutt); + if (resolved > spillsz) { spillsz = resolved; }; + }; + scrutoff = localalloc(c, "@match_spill", spillsz, nil); cgexpr(c, scrut); emitline("\tMOVQ\tAX, "); emitoff(scrutoff: i64); @@ -1188,6 +1206,24 @@ fn cgdot(c: *cgen, n: *node) void = { for (fi != nil) { let fn_: str = fi.fname; if (streq(fn_, fld)) { + // tagged-union field via *struct: stage + // the *struct in BX, then load the four + // payload regs via cgloadtaggedfield. + // BX isn't a target (AX/DX/CX/R8), so + // load order doesn't matter. Mirrors + // the direct-local branch above so the + // match / let-init / call-arg consumer + // shape is identical regardless of + // pointer rooting. + if (istaggedtype(c, fi.tnode)) { + let tsz: i32 = slotsize(c, fi.tnode); + emitline("\tMOVQ\t"); + emitoff(lc.off: i64); + emitline("(BP), BX\n"); + cgloadtaggedfield(c, "BX", + fi.foff, tsz); + return; + }; // str field via *struct: load len into a // scratch first (so loading ptr into AX // last leaves (AX=ptr, BX=len)). @@ -1254,6 +1290,21 @@ fn cgdot(c: *cgen, n: *node) void = { for (fi != nil) { let fn_: str = fi.fname; if (streq(fn_, fld)) { + // tagged-union field: emit the AX=tag, + // DX=word0, CX=word1[, R8=word2] load + // sequence so the match / let-init / + // call-arg consumers see the same shape + // as a tagged-returning fn. Pre-#28 fell + // through to the scalar fieldloadop and + // only AX (tag) was loaded — payload + // words came from whatever the caller + // left in DX/CX/R8. + if (istaggedtype(c, fi.tnode)) { + let tsz: i32 = slotsize(c, fi.tnode); + cgloadtaggedfield(c, "BP", + lc.off + fi.foff, tsz); + return; + }; // str field: load both halves so chained // `.ptr` / `.len` see (AX=ptr, BX=len). if (isstrtype(c, fi.tnode)) { @@ -1470,6 +1521,18 @@ fn cgdot(c: *cgen, n: *node) void = { emitline("\tLEAQ\t"); emitsymname(c, lhs.str); emitline("(SB), CX\n"); + // tagged-union field: load via the tagged- + // return ABI off CX. cgloadtaggedfield orders + // the loads so CX (word1 target) is written + // LAST — otherwise the base address would be + // trashed before the +24/R8 (slice variant) + // read could index off it. Pre-#28 fell + // through to fieldloadop and dropped payload. + if (istaggedtype(c, fi.tnode)) { + let tsz: i32 = slotsize(c, fi.tnode); + cgloadtaggedfield(c, "CX", fi.foff, tsz); + return; + }; if (isstrtype(c, fi.tnode)) { emitline("\tMOVQ\t"); emitdispreg(fi.foff: i64, "CX"); diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 3c30f5ac..3adbddff 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -2110,10 +2110,50 @@ fn rhstaggedident(c: *cgen, src: *node) *node = { return resolvetagged(c, tn); }; +// dotfieldtnode — for an N_DOT src whose base is a local ident or +// *struct, return the declared type node of the named field, or nil +// if the shape doesn't resolve (e.g. enum-member access, pseudo- +// field `.len`, top-level global). Used by rhstaggedabicall and +// related predicates to walk into the field's tagged type. +fn dotfieldtnode(c: *cgen, n: *node) *node = { + if (n == nil) { return nil; }; + if (n.kind != nkind.N_DOT) { return nil; }; + let base: *node = n.lhs; + let fld: str = n.str; + if (base == nil) { return nil; }; + if (base.kind != nkind.N_IDENT) { return nil; }; + let lc: *local = localfindnode(c, base.str); + let btn: *node = nil; + if (lc != nil) { btn = lc.tnode; } + else { btn = letvartnode(c, base.str); }; + if (btn == nil) { return nil; }; + let bk: nkind = btn.kind; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (bk == nkind.N_TPTR) { + let inner: *node = btn.lhs; + if (inner != nil) { + if (inner.kind == nkind.N_TNAME) { sname = inner.str; }; + }; + }; + if (bk == nkind.N_TNAME) { sname = btn.str; }; + if (sname.len == 0) { return nil; }; + let si: *structinfo = structlookup(c, sname); + if (si == nil) { return nil; }; + let fi: *fieldinfo = si.fields; + for (fi != nil) { + if (streq(fi.fname, fld)) { return fi.tnode; }; + fi = fi.finext; + }; + return nil; +}; + // rhstaggedabicall — does `src` produce a tagged value via the AX/DX/CX -// return ABI? True for N_CALL of a tagged-returning fn and N_INDEX of a -// tagged-element base. Used to decide whether cgexpr/spill works for the -// tagged-source branch of cgwidentaggedstore. +// return ABI? True for N_CALL of a tagged-returning fn, N_INDEX of a +// tagged-element base, and N_DOT of a tagged-typed struct field (after +// #28's cgdot fix loads AX/DX/CX/R8 from the field's slot). Used to +// decide whether cgexpr/spill works for the tagged-source branch of +// cgwidentaggedstore. fn rhstaggedabicall(c: *cgen, src: *node) bool = { if (src == nil) { return false; }; if (src.kind == nkind.N_CALL) { @@ -2155,9 +2195,55 @@ fn rhstaggedabicall(c: *cgen, src: *node) bool = { }; }; }; + // N_DOT of a tagged-typed struct field — cgdot loads + // AX=tag, DX=word0, CX=word1[, R8=word2], so downstream + // spill matches the call/index shapes. + if (src.kind == nkind.N_DOT) { + let ft: *node = dotfieldtnode(c, src); + if (ft != nil) { + if (istaggedtype(c, ft)) { return true; }; + }; + }; return false; }; +// cgloadtaggedfield — load a tagged-union slot at `basereg`+foff +// into the tagged-return ABI registers (AX=tag, DX=word0, CX=word1, +// R8=word2). Slot sizes: 16B = (tag, word0), 24B = + word1, 32B +// = + word2 (slice variant). Mirrors the cstage tagged-field load +// in cmd/w6c/cgen.c (N_DOT TY_STRUCT/TY_PTR branches). +// +// Load order is fixed regardless of basereg: tag, word0, word2, +// word1. CX (word1 target) goes LAST because basereg may itself +// be CX — top-level globals address via LEAQ name(SB), CX — and +// overwriting it earlier would trash the base address for the +// remaining loads. For BP / BX bases the order is harmless. +// Callers must guarantee basereg is one of "BP", "BX", "CX"; the +// only register loaded into that is NOT a target is BX, so AX- +// or DX-rooted callers must spill first. +fn cgloadtaggedfield(c: *cgen, basereg: str, foff: i32, slot_sz: i32) void = { + // tag → AX + emitline("\tMOVQ\t"); + emitdispreg(foff: i64, basereg); + emitline(", AX\n"); + // word0 → DX + emitline("\tMOVQ\t"); + emitdispreg((foff + 8): i64, basereg); + emitline(", DX\n"); + // word2 → R8 (slice variant: slot = 8 tag + 24 payload = 32). + if (slot_sz > 24) { + emitline("\tMOVQ\t"); + emitdispreg((foff + 24): i64, basereg); + emitline(", R8\n"); + }; + // word1 → CX (load LAST; conflicts with CX-base globals). + if (slot_sz > 16) { + emitline("\tMOVQ\t"); + emitdispreg((foff + 16): i64, basereg); + emitline(", CX\n"); + }; +}; + // 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. diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 900edd32..40ef4076 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -7680,10 +7680,50 @@ fn rhstaggedident(c: *cgen, src: *node) *node = { return resolvetagged(c, tn); }; +// dotfieldtnode — for an N_DOT src whose base is a local ident or +// *struct, return the declared type node of the named field, or nil +// if the shape doesn't resolve (e.g. enum-member access, pseudo- +// field `.len`, top-level global). Used by rhstaggedabicall and +// related predicates to walk into the field's tagged type. +fn dotfieldtnode(c: *cgen, n: *node) *node = { + if (n == nil) { return nil; }; + if (n.kind != nkind.N_DOT) { return nil; }; + let base: *node = n.lhs; + let fld: str = n.str; + if (base == nil) { return nil; }; + if (base.kind != nkind.N_IDENT) { return nil; }; + let lc: *local = localfindnode(c, base.str); + let btn: *node = nil; + if (lc != nil) { btn = lc.tnode; } + else { btn = letvartnode(c, base.str); }; + if (btn == nil) { return nil; }; + let bk: nkind = btn.kind; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (bk == nkind.N_TPTR) { + let inner: *node = btn.lhs; + if (inner != nil) { + if (inner.kind == nkind.N_TNAME) { sname = inner.str; }; + }; + }; + if (bk == nkind.N_TNAME) { sname = btn.str; }; + if (sname.len == 0) { return nil; }; + let si: *structinfo = structlookup(c, sname); + if (si == nil) { return nil; }; + let fi: *fieldinfo = si.fields; + for (fi != nil) { + if (streq(fi.fname, fld)) { return fi.tnode; }; + fi = fi.finext; + }; + return nil; +}; + // rhstaggedabicall — does `src` produce a tagged value via the AX/DX/CX -// return ABI? True for N_CALL of a tagged-returning fn and N_INDEX of a -// tagged-element base. Used to decide whether cgexpr/spill works for the -// tagged-source branch of cgwidentaggedstore. +// return ABI? True for N_CALL of a tagged-returning fn, N_INDEX of a +// tagged-element base, and N_DOT of a tagged-typed struct field (after +// #28's cgdot fix loads AX/DX/CX/R8 from the field's slot). Used to +// decide whether cgexpr/spill works for the tagged-source branch of +// cgwidentaggedstore. fn rhstaggedabicall(c: *cgen, src: *node) bool = { if (src == nil) { return false; }; if (src.kind == nkind.N_CALL) { @@ -7725,9 +7765,55 @@ fn rhstaggedabicall(c: *cgen, src: *node) bool = { }; }; }; + // N_DOT of a tagged-typed struct field — cgdot loads + // AX=tag, DX=word0, CX=word1[, R8=word2], so downstream + // spill matches the call/index shapes. + if (src.kind == nkind.N_DOT) { + let ft: *node = dotfieldtnode(c, src); + if (ft != nil) { + if (istaggedtype(c, ft)) { return true; }; + }; + }; return false; }; +// cgloadtaggedfield — load a tagged-union slot at `basereg`+foff +// into the tagged-return ABI registers (AX=tag, DX=word0, CX=word1, +// R8=word2). Slot sizes: 16B = (tag, word0), 24B = + word1, 32B +// = + word2 (slice variant). Mirrors the cstage tagged-field load +// in cmd/w6c/cgen.c (N_DOT TY_STRUCT/TY_PTR branches). +// +// Load order is fixed regardless of basereg: tag, word0, word2, +// word1. CX (word1 target) goes LAST because basereg may itself +// be CX — top-level globals address via LEAQ name(SB), CX — and +// overwriting it earlier would trash the base address for the +// remaining loads. For BP / BX bases the order is harmless. +// Callers must guarantee basereg is one of "BP", "BX", "CX"; the +// only register loaded into that is NOT a target is BX, so AX- +// or DX-rooted callers must spill first. +fn cgloadtaggedfield(c: *cgen, basereg: str, foff: i32, slot_sz: i32) void = { + // tag → AX + emitline("\tMOVQ\t"); + emitdispreg(foff: i64, basereg); + emitline(", AX\n"); + // word0 → DX + emitline("\tMOVQ\t"); + emitdispreg((foff + 8): i64, basereg); + emitline(", DX\n"); + // word2 → R8 (slice variant: slot = 8 tag + 24 payload = 32). + if (slot_sz > 24) { + emitline("\tMOVQ\t"); + emitdispreg((foff + 24): i64, basereg); + emitline(", R8\n"); + }; + // word1 → CX (load LAST; conflicts with CX-base globals). + if (slot_sz > 16) { + emitline("\tMOVQ\t"); + emitdispreg((foff + 16): i64, basereg); + emitline(", CX\n"); + }; +}; + // 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. @@ -9143,17 +9229,20 @@ fn cgmatch(c: *cgen, n: *node) void = { scrutt = resolvetagged(c, lc.tnode); }; } else { - // Non-ident scrutinee (call result, arr[i], ?, etc.). - // Spill into an `@match_spill` scratch slot and - // dispatch off it. Tagged returns (N_CALL) follow the - // AX:DX:CX convention; tagged-element loads (N_INDEX) - // after the cgindex fix produce the same triple. - // Nullable returns are single-word (AX = ptr); only +0 - // is read, so the extra stores are harmless. We - // recover the scrutinee type from fnretlookup (N_CALL) - // or the base local's array element type (N_INDEX) so - // dispatch can compute variant indices. - scrutoff = localalloc(c, "@match_spill", 24, nil); + // Non-ident scrutinee (call result, arr[i], p.field, + // ?, etc.). Spill into an `@match_spill` scratch slot + // and dispatch off it. Tagged returns (N_CALL) follow + // the AX:DX:CX convention; tagged-element loads + // (N_INDEX) and tagged-field loads (N_DOT, fixed by + // #28) produce the same triple. Nullable returns are + // single-word (AX = ptr); only +0 is read, so the + // extra stores are harmless. We recover the scrutinee + // type from fnretlookup (N_CALL), the base local's + // array element type (N_INDEX), or the struct field + // type (N_DOT) so dispatch can compute variant + // indices. Slot size is then derived from the + // scrutinee type so slice-variant tagged-unions + // (32B slot) don't overflow a hardcoded 24B scratch. if (scrut.kind == nkind.N_CALL) { let callee: *node = scrut.lhs; if (callee != nil) { @@ -9188,6 +9277,21 @@ fn cgmatch(c: *cgen, n: *node) void = { }; }; }; + if (scrut.kind == nkind.N_DOT) { + let ft: *node = dotfieldtnode(c, scrut); + if (ft != nil) { + scrutt = resolvetagged(c, ft); + }; + }; + // Size the spill to the scrutinee slot. Default 24B + // preserves the historical alloc for non-tagged or + // unresolved cases (nullable, str-returning, etc.). + let spillsz: i32 = 24; + if (scrutt != nil) { + let resolved: i32 = slotsize(c, scrutt); + if (resolved > spillsz) { spillsz = resolved; }; + }; + scrutoff = localalloc(c, "@match_spill", spillsz, nil); cgexpr(c, scrut); emitline("\tMOVQ\tAX, "); emitoff(scrutoff: i64); @@ -9388,6 +9492,24 @@ fn cgdot(c: *cgen, n: *node) void = { for (fi != nil) { let fn_: str = fi.fname; if (streq(fn_, fld)) { + // tagged-union field via *struct: stage + // the *struct in BX, then load the four + // payload regs via cgloadtaggedfield. + // BX isn't a target (AX/DX/CX/R8), so + // load order doesn't matter. Mirrors + // the direct-local branch above so the + // match / let-init / call-arg consumer + // shape is identical regardless of + // pointer rooting. + if (istaggedtype(c, fi.tnode)) { + let tsz: i32 = slotsize(c, fi.tnode); + emitline("\tMOVQ\t"); + emitoff(lc.off: i64); + emitline("(BP), BX\n"); + cgloadtaggedfield(c, "BX", + fi.foff, tsz); + return; + }; // str field via *struct: load len into a // scratch first (so loading ptr into AX // last leaves (AX=ptr, BX=len)). @@ -9454,6 +9576,21 @@ fn cgdot(c: *cgen, n: *node) void = { for (fi != nil) { let fn_: str = fi.fname; if (streq(fn_, fld)) { + // tagged-union field: emit the AX=tag, + // DX=word0, CX=word1[, R8=word2] load + // sequence so the match / let-init / + // call-arg consumers see the same shape + // as a tagged-returning fn. Pre-#28 fell + // through to the scalar fieldloadop and + // only AX (tag) was loaded — payload + // words came from whatever the caller + // left in DX/CX/R8. + if (istaggedtype(c, fi.tnode)) { + let tsz: i32 = slotsize(c, fi.tnode); + cgloadtaggedfield(c, "BP", + lc.off + fi.foff, tsz); + return; + }; // str field: load both halves so chained // `.ptr` / `.len` see (AX=ptr, BX=len). if (isstrtype(c, fi.tnode)) { @@ -9670,6 +9807,18 @@ fn cgdot(c: *cgen, n: *node) void = { emitline("\tLEAQ\t"); emitsymname(c, lhs.str); emitline("(SB), CX\n"); + // tagged-union field: load via the tagged- + // return ABI off CX. cgloadtaggedfield orders + // the loads so CX (word1 target) is written + // LAST — otherwise the base address would be + // trashed before the +24/R8 (slice variant) + // read could index off it. Pre-#28 fell + // through to fieldloadop and dropped payload. + if (istaggedtype(c, fi.tnode)) { + let tsz: i32 = slotsize(c, fi.tnode); + cgloadtaggedfield(c, "CX", fi.foff, tsz); + return; + }; if (isstrtype(c, fi.tnode)) { emitline("\tMOVQ\t"); emitdispreg(fi.foff: i64, "CX"); diff --git a/test/wcc/693_dot_tagged_source.c b/test/wcc/693_dot_tagged_source.c new file mode 100644 index 00000000..34d447e3 --- /dev/null +++ b/test/wcc/693_dot_tagged_source.c @@ -0,0 +1,395 @@ +/* + * 693_dot_tagged_source — `s.f` where `f` is a tagged-union field + * (read-side counterpart of 681's tagged-field write rows). Both stages + * previously misread the SysV classification of the source slot: + * + * cstage's direct-struct / global TY_STRUCT N_DOT branch loaded only + * AX (+0=tag), DX (+8=val0), and CX (+16=val1 when size > 16). The + * R8 (+24=val2) word was never read, so slice-payload variants (slot + * 32B) consumed garbage in the high word. + * + * cstage's TY_PTR N_DOT branch (via_ptr) had no TY_TAGGED handler at + * all — fell through to fldloadop, loading only the tag into AX. The + * N_DOT scrutinee spill path in N_MATCH likewise stored only AX. + * + * wwstage's cgdot had no TY_TAGGED handler in the direct, via_ptr, + * or top-level-global branches. cgmatch's non-ident-scrutinee path + * never recognised N_DOT, so the dispatch always computed + * want = 0 and the spill scratch was hardcoded 24B (overflowing for + * slice-payload variants). rhstaggedabicall didn't accept N_DOT, so + * `let copy: ev = h.e;` and call-arg pushes of `h.e` fell into the + * scalar/struct/str/slice branches. + * + * Closed in task #28 by: + * - cstage cgen.c: extending the direct-struct tagged branch to also + * load R8 for size > 24, adding a TY_TAGGED handler in the via_ptr + * branch, and extending the N_DOT scrutinee spill in N_MATCH to + * write DX/CX/R8 alongside AX. + * - wwstage cgenutil.ww: new helpers dotfieldtnode + cgloadtaggedfield, + * and a new N_DOT branch in rhstaggedabicall. + * - wwstage cgenexpr.ww: TY_TAGGED branches in cgdot for direct + * struct local / *struct deref / top-level-global, plus a new + * N_DOT branch in cgmatch's non-ident-scrutinee spill with the + * scratch sized by the resolved scrutinee type. + * + * Coverage — three variant shapes (16B i64, 24B str, 32B slice) read + * from a direct local struct, a *struct param, and a top-level global, + * plus a let-init round-trip that exercises rhstaggedabicall's N_DOT + * acceptance. The non-tagged str/slice/scalar rows pin that the new + * TY_TAGGED branch doesn't shadow the existing field-load paths. + */ +#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[] = { + /* 16B slot, scalar payload, direct local struct. ev = (i64 | i32) + * puts i64 at variant 0 so h.e = 42i64 sets tag = 0 and word0 = + * 42. match on h.e in the i64 arm reads the value. Pre-#28 + * cgdot dropped DX; the match-spill stored a stale DX and the + * bind returned garbage. Now returns 42. */ + { "local_struct_i64_variant", + "type ev = (i64 | i32);\n" + "type holder = struct { e: ev, mark: i32 };\n" + "fn main() i32 = {\n" + " let h: holder;\n" + " h.mark = 99;\n" + " h.e = 42i64;\n" + " match (h.e) {\n" + " case let v: i64 => { return v: i32; };\n" + " case let z: i32 => { return -1; };\n" + " };\n" + "};\n", + 42 }, + /* 24B slot, str payload, direct local struct. ev = (i32 | str) + * — str at variant 1. Reading h.e must leave AX=tag, DX=ptr, + * CX=len for the match dispatch+bind to copy 16B (ptr,len) + * into v. Returns v.len = 5. */ + { "local_struct_str_variant", + "type ev = (i32 | str);\n" + "type holder = struct { e: ev, mark: i32 };\n" + "fn main() i32 = {\n" + " let h: holder;\n" + " h.mark = 99;\n" + " h.e = (\"hello\": ev);\n" + " match (h.e) {\n" + " case let s: str => { return s.len: i32; };\n" + " case let z: i32 => { return -1; };\n" + " };\n" + "};\n", + 5 }, + /* 32B slot, slice payload, direct local struct. ev = (i32 | []u8) + * — slice at variant 1, slot 8 + 24 = 32B. Reading h.e must + * leave AX=tag, DX=ptr, CX=len, R8=cap; pre-#28 R8 was never + * loaded. The match scratch must also be sized to 32B, or the + * R8 spill writes past the allocated slot. The arm verifies + * v.cap (which traces back through R8/spill+24) — testing only + * v.len wouldn't notice an R8 drop because v.len comes from CX + * which was already loaded pre-#28. */ + { "local_struct_slice_variant", + "type ev = (i32 | []u8);\n" + "type holder = struct { e: ev, mark: i32 };\n" + "fn main() i32 = {\n" + " let raw: [8]u8;\n" + " raw[0] = 1u8; raw[1] = 2u8; raw[2] = 3u8;\n" + " let h: holder;\n" + " h.mark = 99;\n" + " h.e = (raw[0:3]: ev);\n" + " match (h.e) {\n" + " case let v: []u8 => {\n" + " if (v.cap != 3) { return 1; };\n" + " return v.len: i32;\n" + " };\n" + " case let z: i32 => { return -1; };\n" + " };\n" + "};\n", + 3 }, + /* *struct base, 16B slot, scalar payload — exercises the via_ptr + * arm of cstage's TY_PTR N_DOT branch (newly added in #28) and + * the wwstage TPTR cgdot branch's tagged handler. The match + * spill path through cgmatch's N_DOT fallback also fires here + * (h: *holder makes `match (h.e)` not an ident scrutinee). */ + { "via_ptr_i64_variant", + "type ev = (i64 | i32);\n" + "type holder = struct { e: ev, mark: i32 };\n" + "fn f(h: *holder) i32 = {\n" + " match (h.e) {\n" + " case let v: i64 => { return v: i32; };\n" + " case let z: i32 => { return -1; };\n" + " };\n" + "};\n" + "fn main() i32 = {\n" + " let h: holder;\n" + " h.mark = 99;\n" + " h.e = 42i64;\n" + " return f(&h);\n" + "};\n", + 42 }, + /* *struct base, 24B slot, str payload — verifies CX is loaded for + * the via_ptr path. */ + { "via_ptr_str_variant", + "type ev = (i32 | str);\n" + "type holder = struct { e: ev, mark: i32 };\n" + "fn f(h: *holder) i32 = {\n" + " match (h.e) {\n" + " case let s: str => { return s.len: i32; };\n" + " case let z: i32 => { return -1; };\n" + " };\n" + "};\n" + "fn main() i32 = {\n" + " let h: holder;\n" + " h.mark = 99;\n" + " h.e = (\"hello\": ev);\n" + " return f(&h);\n" + "};\n", + 5 }, + /* *struct base, 32B slot, slice payload — exercises R8 (cap) load + * in cstage's via_ptr TY_TAGGED branch and the wwstage cgdot + * via_ptr cgloadtaggedfield slice case. The cstage N_DOT-scrutinee + * spill in N_MATCH (the inner else when bu->kind != TY_STRUCT) + * also has to store R8 to spill+24; without it v.cap reads stack + * garbage. v.cap is verified in the arm — testing only v.len would + * mask an R8 drop. */ + { "via_ptr_slice_variant", + "type ev = (i32 | []u8);\n" + "type holder = struct { e: ev, mark: i32 };\n" + "fn f(h: *holder) i32 = {\n" + " match (h.e) {\n" + " case let v: []u8 => {\n" + " if (v.cap != 3) { return 1; };\n" + " return v.len: i32;\n" + " };\n" + " case let z: i32 => { return -1; };\n" + " };\n" + "};\n" + "fn main() i32 = {\n" + " let raw: [8]u8;\n" + " raw[0] = 1u8; raw[1] = 2u8; raw[2] = 3u8;\n" + " let h: holder;\n" + " h.mark = 99;\n" + " h.e = (raw[0:3]: ev);\n" + " return f(&h);\n" + "};\n", + 3 }, + /* let-init round-trip, 16B slot — `let copy = h.e` exercises the + * cgwidentaggedstore tagged-source path. Without rhstaggedabicall + * recognising N_DOT, src would have fallen into the scalar + * branch and only AX (the tag) would have been spilled into + * copy's slot. Variant order matters for the write side too — + * (i64 | i32) makes i64 = variant 0 so the write side picks the + * right tag for the literal i64. */ + { "letinit_roundtrip", + "type ev = (i64 | i32);\n" + "type holder = struct { e: ev, mark: i32 };\n" + "fn main() i32 = {\n" + " let h: holder;\n" + " h.mark = 99;\n" + " h.e = 42i64;\n" + " let copy: ev = h.e;\n" + " match (copy) {\n" + " case let v: i64 => { return v: i32; };\n" + " case let z: i32 => { return -1; };\n" + " };\n" + "};\n", + 42 }, + /* let-init round-trip, 32B slice slot — the only test row that + * actually fires cstage's direct-local cgdot TY_TAGGED branch on + * the 32B path (rows 1-3's `match (h.e)` is bypassed by cstage's + * N_DOT-in-N_IDENT specialization, which reads tag/value directly + * from h's stack slot without invoking cgdot). The let-init forces + * cgexpr → cgdot → AX/DX/CX/R8 register shape; cgwidentaggedstore + * then spills all four into copy's slot. Without the R8 load in + * cgdot, copy.cap is whatever R8 held going in. */ + { "letinit_slice_roundtrip", + "type ev = (i32 | []u8);\n" + "type holder = struct { e: ev, mark: i32 };\n" + "fn main() i32 = {\n" + " let raw: [8]u8;\n" + " raw[0] = 1u8; raw[1] = 2u8; raw[2] = 3u8;\n" + " let h: holder;\n" + " h.mark = 99;\n" + " h.e = (raw[0:3]: ev);\n" + " let copy: ev = h.e;\n" + " match (copy) {\n" + " case let v: []u8 => {\n" + " if (v.cap != 3) { return 1; };\n" + " return v.len: i32;\n" + " };\n" + " case let z: i32 => { return -1; };\n" + " };\n" + "};\n", + 3 }, + /* Top-level global with slice-variant tagged field — exercises + * wwstage cgdot's third new TY_TAGGED branch (top-level global, + * base reg = CX via LEAQ name(SB)) and cstage's direct-struct + * tagged branch with base_reg = CX (the same R8 load with global + * rooting). The let-init copy fires cgdot on g.e in expression + * context — `match (g.e)` directly doesn't address globals in + * cstage's N_DOT-in-N_IDENT match specialization. The write side + * routes through `*p` because the direct global LHS path + * `g.e = (slice: ev)` is a separate pre-existing wwstage gap + * (filed as a follow-up); isolating the read keeps #28's coverage + * intent clean. */ + { "top_level_global_slice", + "type ev = (i32 | []u8);\n" + "type holder = struct { e: ev, mark: i32 };\n" + "let g: holder;\n" + "fn main() i32 = {\n" + " let raw: [8]u8;\n" + " raw[0] = 1u8; raw[1] = 2u8; raw[2] = 3u8;\n" + " let p: *holder = &g;\n" + " p.mark = 99;\n" + " p.e = (raw[0:3]: ev);\n" + " let copy: ev = g.e;\n" + " match (copy) {\n" + " case let v: []u8 => {\n" + " if (v.cap != 3) { return 1; };\n" + " return v.len: i32;\n" + " };\n" + " case let z: i32 => { return -1; };\n" + " };\n" + "};\n", + 3 }, + /* Negative control: untagged scalar struct field — pin that the + * new TY_TAGGED branch doesn't fire on plain i32 reads (would + * pollute DX/CX with random struct bytes). Mirror of the + * scalar-field regression rows from earlier read-side tasks. */ + { "untagged_i32_field_regression", + "type holder = struct { a: i32, b: i32, c: i32 };\n" + "fn main() i32 = {\n" + " let h: holder;\n" + " h.a = 10; h.b = 20; h.c = 12;\n" + " return h.a + h.b + h.c;\n" + "};\n", + 42 }, + /* Negative control: str field, direct local — pin that the + * str-field branch still fires (it sits below the new tagged + * branch in the field-walk; an over-broad istaggedtype guard + * could mask it). Reads len through the existing str-rhs + * convention. */ + { "untagged_str_field_regression", + "type holder = struct { s: str, mark: i32 };\n" + "fn main() i32 = {\n" + " let h: holder;\n" + " h.s = \"hello\";\n" + " return h.s.len: i32;\n" + "};\n", + 5 }, + /* Negative control: slice field via *struct — verifies the new + * tagged via_ptr branch doesn't intercept slice-field reads, + * which already have a dedicated load path. */ + { "untagged_slice_field_via_ptr_regression", + "type holder = struct { rbuf: []u8, mark: i32 };\n" + "fn f(h: *holder) i32 = { return h.rbuf.len: i32; };\n" + "fn main() i32 = {\n" + " let raw: [8]u8;\n" + " raw[0] = 0u8;\n" + " let h: holder;\n" + " h.rbuf = raw[0:5];\n" + " return f(&h);\n" + "};\n", + 5 }, +}; + +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/waew_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/waew_%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; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char cdrv[1024]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + char wdrv[1024]; + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + struct { const char *name; const char *path; int gated_on_existence; } + drivers[] = { + { "cstage", cdrv, 0 }, + { "wwstage", wdrv, 1 }, + { NULL, NULL, 0 }, + }; + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + for (int d = 0; drivers[d].name; d++) { + if (drivers[d].gated_on_existence + && access(drivers[d].path, X_OK) != 0) { + fprintf(stderr, "dot_tagged_source: skip %s (no %s)\n", + drivers[d].name, drivers[d].path); + continue; + } + for (int i = 0; i < n; i++) { + int got = run_driver(drivers[d].path, &rows[i], i); + total++; + if (got != rows[i].want) { + fprintf(stderr, + "dot_tagged_source[%s][%s]: exit=%d want=%d\n", + drivers[d].name, rows[i].label, + got, rows[i].want); + fail++; + } + } + } + if (fail) { + fprintf(stderr, + "dot_tagged_source: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("dot_tagged_source: %d/%d ok\n", total, total); + return 0; +}