From 5bfcd8bd6bf4117ad5435b74e2b9e921c4f7406c Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 29 Jun 2026 15:19:41 +0900 Subject: [PATCH] cgen: convert the struct-store loop arms to a tinfo-native fill (#31 c2) The W1/W2/W5/W4b store / structlit-fill arms still resolved struct field LAYOUT by bare-leaf name (structlookupchain / structlookup / letvarstructinfo) inside the per-field store loop -- the write-side twin of commit-1's read fix. Under a cross-module same-leaf collision (two modules each exporting a `pair`, 16B vs 24B) the field is stored at the wrong offset/width; an interior 2-byte field store silently corrupted neighbours. cstage walks the resolved Type* (never a name) and is correct; this aligns wwstage UP (ww-only change). Convert each store loop to a tinfo-native fill. Two new helpers in cgenutil.ww: sretretsizetn (a verbatim port of cstage cg_sret_retsize -- 4 kind-branches, no TY_BANG, dropping the #129 module-juggling that was pure name-keying compensation, redundant once the lookup walks resolved tinfo) and cgstructlitfilltn (the tinfo-native twin of cgstructlitfill, recursing on tichase(tf.type_).fields with NO name lookup at any nesting level). Supporting twins storeopsz / tfieldlookup / cgdotfieldhardstoptn. Because the fill never bottoms out on a name, the W4b "decisive wall" (a structlit that needed a (sname,smod)-keyed structinfo the stamped leaf-only tinfo couldn't supply) dissolves by construction, with no module-aware tinfo->structinfo bridge. This CLOSES the nine in-loop nested sub-arms (N_CALL / N_STRUCTLIT / N_IDENT field receive) by construction -- the tf walk has no name lookup. The non-reddenable global arms (W5/W4b) are converted-for-construction: a qualified global decl resolves its bare leaf correctly today, so byte-id (cs.s==ww.s) is their net, not a value pin. `!T` is kind-preserving in both stages (resolve_type / tinfofornode), so a `!struct` sret sizes as the inner struct, never TAGGED -- the #129 drop changes no size. Completes the 2-commit #31 arc (RULING R2 / Opt-2, .ai/ken-31-spec.md): the #224 name-keyed receiver-layout cluster is now closed by construction. The residual nested-field surface (the shared cgstructlitfill kept for its non-#31 callers + the cgenstmt/cgenutil let-receive/fill family) is tracked as #32. Pins: test/wcc/797 value-asserts ptr-WRITE (W1) and val-WRITE (W2), each reddening under independent per-arm revert; nestfill_box drives cgstructlitfilltn's nested recursion (bootstrap-uncovered -- the corpus has no struct-literal field store -- so the value pin is its only net). --- selfhost/cmd/wcc/cgenexpr.ww | 374 +++++++------ selfhost/cmd/wcc/cgenutil.ww | 503 ++++++++++++++++++ ...797_xmod_struct_field_layout_collide_run.c | 121 ++++- 3 files changed, 813 insertions(+), 185 deletions(-) diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index e329450f..fc4c6a7f 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -350,6 +350,34 @@ fn cgdotfieldhardstop(c: *cgen, ftn: *syntax.node) void = { }; }; +// cgdotfieldhardstoptn — tinfo twin of cgdotfieldhardstop (#31): the +// single-dot field compound combine speaks integer ABI only, so a +// tagged/str/slice/float field compound loud-stops. Keyed off the +// checker-STAMPED field tinfo (the receiver-layout tf walk), not a +// tnode. Messages verbatim from cgdotfieldhardstop. (#34/rule-7) +fn cgdotfieldhardstoptn(c: *cgen, ft: *syntax.tinfo) void = { + if (syntax.typeistagged(ft)) { + let m: str = "single-dot field compound on tagged field not wired (#34/rule-7)\n"; + os.write(2, m.ptr, m.len: u64); + os.exit(1); + }; + if (syntax.typeisstr(ft)) { + let m: str = "single-dot field compound on str field not wired (#34/rule-7)\n"; + os.write(2, m.ptr, m.len: u64); + os.exit(1); + }; + if (syntax.typeisslice(ft)) { + let m: str = "single-dot field compound on slice field not wired (#34/rule-7)\n"; + os.write(2, m.ptr, m.len: u64); + os.exit(1); + }; + if (syntax.typeisfloat(ft)) { + let m: str = "single-dot field compound on float field not wired (#34/rule-7)\n"; + os.write(2, m.ptr, m.len: u64); + os.exit(1); + }; +}; + fn cgtryunwcursor(c: *cgen, n: *syntax.node, opname: str) void = { let u: *syntax.tinfo = nil; if (n.lhs != nil) { u = n.lhs.type_: *syntax.tinfo; }; @@ -10592,34 +10620,39 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { if (tn != nil) { lkind = tn.kind; }; // Pointer-to-struct: deref then store. if (lkind == syntax.nkind.N_TPTR) { - let inner: *syntax.node = tn.lhs; - let sname: str; - sname.ptr = nil; sname.len = 0; - if (inner != nil) { - if (inner.kind == syntax.nkind.N_TNAME) { sname = inner.str; }; - }; - if (sname.len > 0) { - // structlookupchain (#22) handles the - // alias-chain miss; same shape as the - // cgdot pointer-to-struct read site. - let si: *structinfo = structlookupchain(c, inner); - if (si != nil) { - let fi: *fieldinfo = si.fields; - for (fi != nil) { - let fn_: str = fi.fname; - if (syntax.streq(fn_, fld)) { + // #31: resolve the *struct field OFFSET + type off the + // checker-STAMPED receiver tinfo (tichase(base.type_)->.sub), + // NOT the name-keyed leaf lookup. A cross-module same-leaf + // collision makes the bare leaf mis-resolve to a FOREIGN + // same-leaf struct -> the field is STORED at the wrong + // offset/width (the inferred-local ptr-WRITE row). The + // stamped tinfo carries the right layout; mirror cstage + // type_chase_named(bu->sub)->fields. The in-loop #32 nested + // struct-receive/structlit/ident sub-arms close by + // construction (the tf walk has no name lookup). Non-struct + // pointees fall through to the str/slice arm. + let sti: *syntax.tinfo = tichase(base.type_: *syntax.tinfo); + if (sti != nil && sti.kind == syntax.tykind.TY_PTR) { sti = tichase(sti.sub); }; + if (sti != nil) { + if (sti.kind == syntax.tykind.TY_STRUCT) { + let tf: *syntax.tfield = sti.fields; + for (tf != nil) { + if (syntax.streq(tf.name, fld)) { + let foff: i32 = tf.offset: i32; + let ftraw: *syntax.tinfo = tf.type_; + let fu: *syntax.tinfo = tichase(ftraw); // 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 == syntax.tkind.TK_ASSIGN - && istaggedtype(c, fi.tnode)) { - let fsz: i32 = slotsize(c, fi.tnode); + && syntax.typeistagged(ftraw)) { + let fsz: i32 = fu.size: i32; emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); - cgwidentaggedstore(c, fi.tnode.type_: *syntax.tinfo, - n.rhs, "BX", fi.foff, fsz); + cgwidentaggedstore(c, ftraw, + n.rhs, "BX", foff, fsz); return; }; // #234-tail: via-ptr (`p.f`) sret field STORE. The dest must @@ -10629,7 +10662,7 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { if (n.op == syntax.tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == syntax.nkind.N_CALL - && sretretsize(c, fi.tnode) > 0) { + && sretretsizetn(c, ftraw) > 0) { let m234: str = "#234-tail: over-cap tuple sret store to via-ptr field dest unsupported\n"; os.write(2, m234.ptr, m234.len: u64); os.exit(1); @@ -10654,20 +10687,16 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { && n.rhs != nil && (n.rhs.kind == syntax.nkind.N_CALL || n.rhs.kind == syntax.nkind.N_TRYUNW - || n.rhs.kind == syntax.nkind.N_TRYPROP) - && fi.tnode != nil - && fi.tnode.kind == syntax.nkind.N_TNAME - && aliasprimsize(c, fi.tnode.str) == 0) { - let ssi: *structinfo = structlookup(c, fi.tnode.str); - if (ssi != nil) { - let ssz: i32 = structabisize(ssi); + || n.rhs.kind == syntax.nkind.N_TRYPROP)) { + if (fu != nil && fu.kind == syntax.tykind.TY_STRUCT) { + let ssz: i32 = fu.size: i32; if (ssz <= 24) { // #14: choke-point now stores every in-cap tail; 3/5/6/7 no longer dropped to a lone narrow MOV. cgexpr(c, n.rhs); emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); - cgaggregstore(c, "BX", fi.foff, ssz, false); + cgaggregstore(c, "BX", foff, ssz, false); return; }; }; @@ -10679,26 +10708,18 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { // field store. if (n.op == syntax.tkind.TK_ASSIGN && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_STRUCTLIT - && fi.tnode != nil - && fi.tnode.kind == syntax.nkind.N_TNAME - && aliasprimsize(c, fi.tnode.str) == 0) { - let ssi: *structinfo = structlookup(c, fi.tnode.str); - if (ssi != nil) { - cgstructlitfill(c, ssi, n.rhs, 1, lc.off, "", - fi.foff); + && n.rhs.kind == syntax.nkind.N_STRUCTLIT) { + if (fu != nil && fu.kind == syntax.tykind.TY_STRUCT) { + cgstructlitfilltn(c, fu, n.rhs, 1, lc.off, "", + foff); return; }; }; if (n.op == syntax.tkind.TK_ASSIGN && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_IDENT - && fi.tnode != nil - && fi.tnode.kind == syntax.nkind.N_TNAME - && aliasprimsize(c, fi.tnode.str) == 0) { - let ssi: *structinfo = structlookup(c, fi.tnode.str); + && n.rhs.kind == syntax.nkind.N_IDENT) { let srhs: *local = localfindnode(c, n.rhs.str); - if (ssi != nil) { if (srhs != nil) { + if (fu != nil && fu.kind == syntax.tykind.TY_STRUCT) { if (srhs != nil) { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); @@ -10709,7 +10730,7 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); - emitdispreg((fi.foff + k): i64, "BX"); + emitdispreg((foff + k): i64, "BX"); emitline("\n"); k += 8; }; @@ -10718,7 +10739,7 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVL\tAX, "); - emitdispreg((fi.foff + k): i64, "BX"); + emitdispreg((foff + k): i64, "BX"); emitline("\n"); k += 4; }; @@ -10727,7 +10748,7 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVW\tAX, "); - emitdispreg((fi.foff + k): i64, "BX"); + emitdispreg((foff + k): i64, "BX"); emitline("\n"); k += 2; }; @@ -10736,7 +10757,7 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVB\tAX, "); - emitdispreg((fi.foff + k): i64, "BX"); + emitdispreg((foff + k): i64, "BX"); emitline("\n"); k += 1; }; @@ -10748,11 +10769,11 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); - let lop: str = fieldloadop(c, fi); + let lop: str = loadopsz(syntax.typeissigned(ftraw), fu.size: i32); emitline("\t"); emitline(lop); emitline("\t"); - emitdispreg(fi.foff: i64, "BX"); + emitdispreg(foff: i64, "BX"); emitline(", BX\n"); emitline("\tPUSHQ\tBX\n"); }; @@ -10762,9 +10783,9 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { // PLUSEQ is commutative; MINUSEQ // needs lhs - rhs (BX is old lhs, // AX is rhs). - cgdotfieldhardstop(c, fi.tnode); + cgdotfieldhardstoptn(c, ftraw); let uns34: bool = false; - if (fi.tnode != nil) { if (fi.tnode.type_ != nil) { uns34 = syntax.typeisunsigned(fi.tnode.type_: *syntax.tinfo); }; }; + uns34 = syntax.typeisunsigned(ftraw); cgdotfieldcombine(c, n.op, uns34); }; if (n.op == syntax.tkind.TK_ASSIGN) { @@ -10772,33 +10793,33 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { // store the full 3-word {ptr,len,cap} from (AX,BX,CX). // CX holds cap, so stage the struct addr in DX and // store at foff/+8/+16 (#1/Phase 3). - if (isstrtype(c, fi.tnode) || isslicetype(c, fi.tnode)) { + if (syntax.typeisstr(ftraw) || syntax.typeisslice(ftraw)) { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), DX\n"); emitline("\tMOVQ\tAX, "); - emitdispreg(fi.foff: i64, "DX"); + emitdispreg(foff: i64, "DX"); emitline("\n"); emitline("\tMOVQ\tBX, "); - emitdispreg((fi.foff + 8): i64, "DX"); + emitdispreg((foff + 8): i64, "DX"); emitline("\n"); emitline("\tMOVQ\tCX, "); - emitdispreg((fi.foff + 16): i64, "DX"); + emitdispreg((foff + 16): i64, "DX"); emitline("\n"); return; }; // f64/f32 plain `=` via *struct: cgexpr left the // value in X0. Reload struct ptr and MOVSD/MOVSS. - if (isfloattype(c, fi.tnode)) { + if (syntax.typeisfloat(ftraw)) { let mov: str = "MOVSD"; - if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; + if (syntax.typeisf32(ftraw)) { mov = "MOVSS"; }; emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); emitline("\t"); emitline(mov); emitline("\tX0, "); - emitdispreg(fi.foff: i64, "BX"); + emitdispreg(foff: i64, "BX"); emitline("\n"); return; }; @@ -10806,38 +10827,48 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); - let sop: str = fieldstoreop(c, fi); + let sop: str = storeopsz(fu.size: i32); emitline("\t"); emitline(sop); emitline("\tAX, "); - emitdispreg(fi.foff: i64, "BX"); + emitdispreg(foff: i64, "BX"); emitline("\n"); return; }; - fi = fi.finext; + tf = tf.tnext; }; }; }; }; // Direct struct local: store at off+foff. if (lkind == syntax.nkind.N_TNAME) { - // structlookupchain (#22) — same shape - // as the cgdot direct-local read site. - let si: *structinfo = structlookupchain(c, tn); - if (si != nil) { - let fi: *fieldinfo = si.fields; - for (fi != nil) { - let fn_: str = fi.fname; - if (syntax.streq(fn_, fld)) { + // #31: resolve the value-struct field OFFSET + type off the + // checker-STAMPED receiver tinfo (tichase(base.type_)), NOT + // the name-keyed leaf lookup. A cross-module same-leaf + // collision makes the bare leaf mis-resolve to a FOREIGN + // same-leaf struct -> the field is STORED at the wrong + // offset/width (the inferred-local val-WRITE row, ww=107). + // The stamped tinfo carries the right layout; mirror cstage + // type_chase_named(base->type)->fields. The in-loop #32 + // nested struct-receive/structlit/ident sub-arms close by + // construction (the tf walk has no name lookup). + let sti: *syntax.tinfo = tichase(base.type_: *syntax.tinfo); + if (sti != nil && sti.kind == syntax.tykind.TY_STRUCT) { + let tf: *syntax.tfield = sti.fields; + for (tf != nil) { + if (syntax.streq(tf.name, fld)) { + let foff: i32 = tf.offset: i32; + let ftraw: *syntax.tinfo = tf.type_; + let fu: *syntax.tinfo = tichase(ftraw); // Tagged-union field in a direct struct local — - // full slot rewrite at (lc.off + fi.foff)(BP) + // full slot rewrite at (lc.off + foff)(BP) // via cgwidentaggedstore basereg="BP". Pre-#26 // fell through and dropped tag + payload. if (n.op == syntax.tkind.TK_ASSIGN - && istaggedtype(c, fi.tnode)) { - let fsz: i32 = slotsize(c, fi.tnode); - cgwidentaggedstore(c, fi.tnode.type_: *syntax.tinfo, - n.rhs, "BP", lc.off + fi.foff, fsz); + && syntax.typeistagged(ftraw)) { + let fsz: i32 = fu.size: i32; + cgwidentaggedstore(c, ftraw, + n.rhs, "BP", lc.off + foff, fsz); return; }; // #234: over-cap sret STORE into a LOCAL struct field — @@ -10845,14 +10876,14 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { // (sretretsize > 0: a >24B struct OR an over-cap tuple). // STORE-twin of the Fold-B sret RECEIVE (a937d67): point // the callee's hidden RDI dest at the field slot - // (c.sretdestoff = lc.off + fi.foff) so it writes the + // (c.sretdestoff = lc.off + foff) so it writes the // WHOLE value there, never the truncating generic store // below. Mirror of cstage cgen.c (#234) field local arm. if (n.op == syntax.tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == syntax.nkind.N_CALL - && sretretsize(c, fi.tnode) > 0) { - c.sretdestoff = lc.off + fi.foff; + && sretretsizetn(c, ftraw) > 0) { + c.sretdestoff = lc.off + foff; cgexpr(c, n.rhs); c.sretdestoff = 0; return; @@ -10862,9 +10893,9 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { // added with #5; closes #27 marker here): // N_IDENT: word-copy from rhs slot. // N_CALL: cgexpr → AX/DX/CX; sized stores - // directly at (lc.off+fi.foff)(BP). + // directly at (lc.off+foff)(BP). // N_STRUCTLIT: field-walk; each inner - // field stored at +fi.foff+inner_foff(BP). + // field stored at +foff+inner_foff(BP). // BP-rel direct, no addr scratch needed. #12: an // unwrap `b.f = mk()!` rides the same {AX,DX,CX} // shape (producer shift) — admit it alongside @@ -10873,17 +10904,13 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { && n.rhs != nil && (n.rhs.kind == syntax.nkind.N_CALL || n.rhs.kind == syntax.nkind.N_TRYUNW - || n.rhs.kind == syntax.nkind.N_TRYPROP) - && fi.tnode != nil - && fi.tnode.kind == syntax.nkind.N_TNAME - && aliasprimsize(c, fi.tnode.str) == 0) { - let ssi: *structinfo = structlookup(c, fi.tnode.str); - if (ssi != nil) { - let ssz: i32 = structabisize(ssi); + || n.rhs.kind == syntax.nkind.N_TRYPROP)) { + if (fu != nil && fu.kind == syntax.tykind.TY_STRUCT) { + let ssz: i32 = fu.size: i32; if (ssz <= 24) { // #14: choke-point now stores every in-cap tail; 3/5/6/7 no longer dropped to a lone narrow MOV. cgexpr(c, n.rhs); - cgaggregstore(c, "BP", lc.off + fi.foff, ssz, false); + cgaggregstore(c, "BP", lc.off + foff, ssz, false); return; }; }; @@ -10894,26 +10921,18 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { // no BX reload. if (n.op == syntax.tkind.TK_ASSIGN && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_STRUCTLIT - && fi.tnode != nil - && fi.tnode.kind == syntax.nkind.N_TNAME - && aliasprimsize(c, fi.tnode.str) == 0) { - let ssi: *structinfo = structlookup(c, fi.tnode.str); - if (ssi != nil) { - cgstructlitfill(c, ssi, n.rhs, 0, 0, "", - lc.off + fi.foff); + && n.rhs.kind == syntax.nkind.N_STRUCTLIT) { + if (fu != nil && fu.kind == syntax.tykind.TY_STRUCT) { + cgstructlitfilltn(c, fu, n.rhs, 0, 0, "", + lc.off + foff); return; }; }; if (n.op == syntax.tkind.TK_ASSIGN && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_IDENT - && fi.tnode != nil - && fi.tnode.kind == syntax.nkind.N_TNAME - && aliasprimsize(c, fi.tnode.str) == 0) { - let ssi: *structinfo = structlookup(c, fi.tnode.str); + && n.rhs.kind == syntax.nkind.N_IDENT) { let srhs: *local = localfindnode(c, n.rhs.str); - if (ssi != nil) { if (srhs != nil) { + if (fu != nil && fu.kind == syntax.tykind.TY_STRUCT) { if (srhs != nil) { let ssz: i32 = copysrcnatsize(c, n.rhs); // #71: natural source size, not slot-padded totsize let k: i32 = 0; for (k + 8 <= ssz) { @@ -10921,7 +10940,7 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); - emitoff((lc.off + fi.foff + k): i64); + emitoff((lc.off + foff + k): i64); emitline("(BP)\n"); k += 8; }; @@ -10930,7 +10949,7 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVL\tAX, "); - emitoff((lc.off + fi.foff + k): i64); + emitoff((lc.off + foff + k): i64); emitline("(BP)\n"); k += 4; }; @@ -10939,7 +10958,7 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVW\tAX, "); - emitoff((lc.off + fi.foff + k): i64); + emitoff((lc.off + foff + k): i64); emitline("(BP)\n"); k += 2; }; @@ -10948,7 +10967,7 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVB\tAX, "); - emitoff((lc.off + fi.foff + k): i64); + emitoff((lc.off + foff + k): i64); emitline("(BP)\n"); k += 1; }; @@ -10960,11 +10979,11 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { // scalar field: load current → push // → eval rhs → combine → store // (mirror cgen.c:3477 local arm). - let lop: str = fieldloadop(c, fi); + let lop: str = loadopsz(syntax.typeissigned(ftraw), fu.size: i32); emitline("\t"); emitline(lop); emitline("\t"); - emitoff((lc.off + fi.foff): i64); + emitoff((lc.off + foff): i64); emitline("(BP), BX\n"); emitline("\tPUSHQ\tBX\n"); }; @@ -10973,47 +10992,47 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { emitline("\tPOPQ\tBX\n"); // PLUSEQ commutes; MINUSEQ needs // lhs-rhs (BX old lhs, AX rhs). - cgdotfieldhardstop(c, fi.tnode); + cgdotfieldhardstoptn(c, ftraw); let uns34: bool = false; - if (fi.tnode != nil) { if (fi.tnode.type_ != nil) { uns34 = syntax.typeisunsigned(fi.tnode.type_: *syntax.tinfo); }; }; + uns34 = syntax.typeisunsigned(ftraw); cgdotfieldcombine(c, n.op, uns34); }; // str/slice field direct: str IS []u8, so both store the // full 3-word {ptr,len,cap} from (AX,BX,CX) at +0/+8/+16. // BP base, no scratch reload needed; the generic fldstoreop // below would write only AX, dropping .len/.cap (#1/Phase 3). - if (isstrtype(c, fi.tnode) || isslicetype(c, fi.tnode)) { + if (syntax.typeisstr(ftraw) || syntax.typeisslice(ftraw)) { emitline("\tMOVQ\tAX, "); - emitoff((lc.off + fi.foff): i64); + emitoff((lc.off + foff): i64); emitline("(BP)\n"); emitline("\tMOVQ\tBX, "); - emitoff((lc.off + fi.foff + 8): i64); + emitoff((lc.off + foff + 8): i64); emitline("(BP)\n"); emitline("\tMOVQ\tCX, "); - emitoff((lc.off + fi.foff + 16): i64); + emitoff((lc.off + foff + 16): i64); emitline("(BP)\n"); return; }; // f64/f32 direct struct local store: route via X0. - if (isfloattype(c, fi.tnode)) { + if (syntax.typeisfloat(ftraw)) { let mov: str = "MOVSD"; - if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; + if (syntax.typeisf32(ftraw)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\tX0, "); - emitoff((lc.off + fi.foff): i64); + emitoff((lc.off + foff): i64); emitline("(BP)\n"); return; }; - let sop: str = fieldstoreop(c, fi); + let sop: str = storeopsz(fu.size: i32); emitline("\t"); emitline(sop); emitline("\tAX, "); - emitoff((lc.off + fi.foff): i64); + emitoff((lc.off + foff): i64); emitline("(BP)\n"); return; }; - fi = fi.finext; + tf = tf.tnext; }; }; }; @@ -11108,11 +11127,22 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { if (base.kind == syntax.nkind.N_IDENT) { let bn: str = base.str; if (localfindnode(c, bn) == nil) { - let si: *structinfo = letvarstructinfo(c, bn); - if (si != nil) { - let fi: *fieldinfo = si.fields; - for (fi != nil) { - if (syntax.streq(fi.fname, fld)) { + // #31: resolve the global value-struct field OFFSET + type + // off the checker-STAMPED receiver tinfo (tichase(base.type_)), + // NOT the name-keyed global-struct leaf lookup. A global decl + // is qualified -> non-reddenable; converted for close-by- + // construction (byte-id). Mirror cstage type-keyed N_DOT global + // arm. The in-loop #32 nested struct-receive/structlit/ident + // sub-arms close by construction (the tf walk has no name + // lookup). + let sti: *syntax.tinfo = tichase(base.type_: *syntax.tinfo); + if (sti != nil && sti.kind == syntax.tykind.TY_STRUCT) { + let tf: *syntax.tfield = sti.fields; + for (tf != nil) { + if (syntax.streq(tf.name, fld)) { + let foff: i32 = tf.offset: i32; + let ftraw: *syntax.tinfo = tf.type_; + let fu: *syntax.tinfo = tichase(ftraw); // #234-tail: GLOBAL (`g.f`) sret field STORE. c.sretdestoff is // BP-relative only and can't name a global slot; the runtime // RDI-pointer dest variant is deferred. HARD-STOP loud, never @@ -11120,7 +11150,7 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { if (n.op == syntax.tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == syntax.nkind.N_CALL - && sretretsize(c, fi.tnode) > 0) { + && sretretsizetn(c, ftraw) > 0) { let m234: str = "#234-tail: over-cap tuple sret store to global field dest unsupported\n"; os.write(2, m234.ptr, m234.len: u64); os.exit(1); @@ -11134,20 +11164,16 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { // N_STRUCTLIT: field-walk; reload BX per store. if (n.op == syntax.tkind.TK_ASSIGN && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_CALL - && fi.tnode != nil - && fi.tnode.kind == syntax.nkind.N_TNAME - && aliasprimsize(c, fi.tnode.str) == 0) { - let ssi: *structinfo = structlookup(c, fi.tnode.str); - if (ssi != nil) { - let ssz: i32 = structabisize(ssi); + && n.rhs.kind == syntax.nkind.N_CALL) { + if (fu != nil && fu.kind == syntax.tykind.TY_STRUCT) { + let ssz: i32 = fu.size: i32; if (ssz <= 24) { // #14: choke-point now stores every in-cap tail; 3/5/6/7 no longer dropped to a lone narrow MOV. cgexpr(c, n.rhs); emitline("\tLEAQ\t"); emitsymname(c, bn); emitline("(SB), BX\n"); - cgaggregstore(c, "BX", fi.foff, ssz, false); + cgaggregstore(c, "BX", foff, ssz, false); return; }; }; @@ -11159,26 +11185,18 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { // field store. if (n.op == syntax.tkind.TK_ASSIGN && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_STRUCTLIT - && fi.tnode != nil - && fi.tnode.kind == syntax.nkind.N_TNAME - && aliasprimsize(c, fi.tnode.str) == 0) { - let ssi: *structinfo = structlookup(c, fi.tnode.str); - if (ssi != nil) { - cgstructlitfill(c, ssi, n.rhs, 2, 0, bn, - fi.foff); + && n.rhs.kind == syntax.nkind.N_STRUCTLIT) { + if (fu != nil && fu.kind == syntax.tykind.TY_STRUCT) { + cgstructlitfilltn(c, fu, n.rhs, 2, 0, bn, + foff); return; }; }; if (n.op == syntax.tkind.TK_ASSIGN && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_IDENT - && fi.tnode != nil - && fi.tnode.kind == syntax.nkind.N_TNAME - && aliasprimsize(c, fi.tnode.str) == 0) { - let ssi: *structinfo = structlookup(c, fi.tnode.str); + && n.rhs.kind == syntax.nkind.N_IDENT) { let srhs: *local = localfindnode(c, n.rhs.str); - if (ssi != nil) { if (srhs != nil) { + if (fu != nil && fu.kind == syntax.tykind.TY_STRUCT) { if (srhs != nil) { emitline("\tLEAQ\t"); emitsymname(c, bn); emitline("(SB), BX\n"); @@ -11189,7 +11207,7 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); - emitdispreg((fi.foff + k): i64, "BX"); + emitdispreg((foff + k): i64, "BX"); emitline("\n"); k += 8; }; @@ -11198,7 +11216,7 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVL\tAX, "); - emitdispreg((fi.foff + k): i64, "BX"); + emitdispreg((foff + k): i64, "BX"); emitline("\n"); k += 4; }; @@ -11207,7 +11225,7 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVW\tAX, "); - emitdispreg((fi.foff + k): i64, "BX"); + emitdispreg((foff + k): i64, "BX"); emitline("\n"); k += 2; }; @@ -11216,7 +11234,7 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVB\tAX, "); - emitdispreg((fi.foff + k): i64, "BX"); + emitdispreg((foff + k): i64, "BX"); emitline("\n"); k += 1; }; @@ -11230,19 +11248,19 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { // below truncates to 1 word, silently dropping tag // and payload. if (n.op == syntax.tkind.TK_ASSIGN - && istaggedtype(c, fi.tnode)) { - let fsz: i32 = slotsize(c, fi.tnode); + && syntax.typeistagged(ftraw)) { + let fsz: i32 = fu.size: i32; emitline("\tLEAQ\t"); emitsymname(c, bn); emitline("(SB), BX\n"); cgwidentaggedstore(c, - fi.tnode.type_: *syntax.tinfo, - n.rhs, "BX", fi.foff, fsz); + ftraw, + n.rhs, "BX", foff, fsz); return; }; if (n.op == syntax.tkind.TK_ASSIGN) { cgexpr(c, n.rhs); - if (isstrtype(c, fi.tnode) || isslicetype(c, fi.tnode)) { + if (syntax.typeisstr(ftraw) || syntax.typeisslice(ftraw)) { // str OR slice field: str IS []u8, so // both store the full {ptr,len,cap} // header (cstage cgen.c:5055 gates @@ -11256,39 +11274,39 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { emitsymname(c, bn); emitline("(SB), DX\n"); emitline("\tMOVQ\tAX, "); - emitdispreg(fi.foff: i64, "DX"); + emitdispreg(foff: i64, "DX"); emitline("\n"); emitline("\tMOVQ\tBX, "); - emitdispreg((fi.foff + 8): i64, "DX"); + emitdispreg((foff + 8): i64, "DX"); emitline("\n"); emitline("\tMOVQ\tCX, "); - emitdispreg((fi.foff + 16): i64, "DX"); + emitdispreg((foff + 16): i64, "DX"); emitline("\n"); return; }; // f64/f32 plain `=` on global struct field: value is // in X0; LEAQ the base into BX and MOVSD/MOVSS. - if (isfloattype(c, fi.tnode)) { + if (syntax.typeisfloat(ftraw)) { let mov: str = "MOVSD"; - if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; + if (syntax.typeisf32(ftraw)) { mov = "MOVSS"; }; emitline("\tLEAQ\t"); emitsymname(c, bn); emitline("(SB), BX\n"); emitline("\t"); emitline(mov); emitline("\tX0, "); - emitdispreg(fi.foff: i64, "BX"); + emitdispreg(foff: i64, "BX"); emitline("\n"); return; }; - let sop: str = fieldstoreop(c, fi); + let sop: str = storeopsz(fu.size: i32); emitline("\tLEAQ\t"); emitsymname(c, bn); emitline("(SB), BX\n"); emitline("\t"); emitline(sop); emitline("\tAX, "); - emitdispreg(fi.foff: i64, "BX"); + emitdispreg(foff: i64, "BX"); emitline("\n"); return; }; @@ -11296,34 +11314,34 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { // → push → eval rhs → combine → // store. cgexpr clobbers BX, so // re-LEAQ for the store. - let lop: str = fieldloadop(c, fi); + let lop: str = loadopsz(syntax.typeissigned(ftraw), fu.size: i32); emitline("\tLEAQ\t"); emitsymname(c, bn); emitline("(SB), BX\n"); emitline("\t"); emitline(lop); emitline("\t"); - emitdispreg(fi.foff: i64, "BX"); + emitdispreg(foff: i64, "BX"); emitline(", BX\n"); emitline("\tPUSHQ\tBX\n"); cgexpr(c, n.rhs); emitline("\tPOPQ\tBX\n"); - cgdotfieldhardstop(c, fi.tnode); + cgdotfieldhardstoptn(c, ftraw); let uns34: bool = false; - if (fi.tnode != nil) { if (fi.tnode.type_ != nil) { uns34 = syntax.typeisunsigned(fi.tnode.type_: *syntax.tinfo); }; }; + uns34 = syntax.typeisunsigned(ftraw); cgdotfieldcombine(c, n.op, uns34); - let sop: str = fieldstoreop(c, fi); + let sop: str = storeopsz(fu.size: i32); emitline("\tLEAQ\t"); emitsymname(c, bn); emitline("(SB), BX\n"); emitline("\t"); emitline(sop); emitline("\tAX, "); - emitdispreg(fi.foff: i64, "BX"); + emitdispreg(foff: i64, "BX"); emitline("\n"); return; }; - fi = fi.finext; + tf = tf.tnext; }; }; }; @@ -12305,18 +12323,20 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { || gau.kind == syntax.tykind.TY_ARRAY || gau.kind == syntax.tykind.TY_TUPLE) { if (n.rhs != nil && n.rhs.kind == syntax.nkind.N_STRUCTLIT) { - let gsi49: *structinfo = nil; - if (lvftn.kind == syntax.nkind.N_TNAME) { - gsi49 = structlookup(c, lvftn.str); - }; - if (gsi49 == nil) { + // #31: fill off the stamped struct tinfo (gau = + // tichase(lvftn.type_)), NOT structlookup(lvftn.str). A + // global decl is qualified -> non-reddenable; converted + // for close-by-construction (byte-id). The W4b nested + // in-loop structlookup sub-arms close by construction + // (cgstructlitfilltn recurses on tichase(tf.type_)). + if (gau.kind != syntax.tykind.TY_STRUCT) { // wwstage-only bail (anonymous // type; the @placescr precedent). let m49d: str = "assign: structlit layout unresolved (rule-7)\n"; os.write(2, m49d.ptr, m49d.len: u64); os.exit(1); }; - cgstructlitfill(c, gsi49, n.rhs, 2, 0, nm, 0); + cgstructlitfilltn(c, gau, n.rhs, 2, 0, nm, 0); return; }; if (n.rhs != nil && n.rhs.kind == syntax.nkind.N_CALL) { diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 02b8a1e3..31ff63cb 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -1695,6 +1695,19 @@ fn loadopsz(sigd: bool, sz: i32) str = { return "MOVQ"; }; +// storeopsz — store op when the size has already been resolved +// upstream off a stamped tinfo (the #31 receiver-layout store choke- +// point). Size-only twin of fieldstoreop's body {1->MOVB, 2->MOVW, +// 4->MOVL, else MOVQ}; the store side carries no signedness. Preserves +// the #15 sub-8 guard (a 2B tail field stores MOVW, not a slot-wide +// MOVQ into saved BP). +fn storeopsz(sz: i32) str = { + if (sz == 1) { return "MOVB"; }; + if (sz == 2) { return "MOVW"; }; + if (sz == 4) { return "MOVL"; }; + return "MOVQ"; +}; + // localloadop — read instruction for a scalar local/let load. Same // dispatch as fieldloadop, but keyed on the value's own tnode.type_. // Lets the caller emit MOVSXD/MOVSWQ/MOVSBQ on a signed-narrow slot @@ -2214,6 +2227,80 @@ export fn sretretsize(c: *cgen, t: *syntax.node) i32 = { return n; }; +// tfieldlookup — the named field `fld` in struct tinfo `sti`'s stamped +// field chain, else nil. The #31 receiver-layout store choke-point: the +// tfield twin of the name-keyed `for (fi != nil)` fieldinfo walk. A +// cross-module same-leaf collision that mis-resolved structlookupchain +// cannot reach a stamped tinfo, so every field-OFFSET/width/kind keyed +// off the returned tf is collision-immune. +fn tfieldlookup(sti: *syntax.tinfo, fld: str) *syntax.tfield = { + if (sti == nil) { return nil; }; + let tf: *syntax.tfield = sti.fields; + for (tf != nil) { + if (syntax.streq(tf.name, fld)) { return tf; }; + tf = tf.tnext; + }; + return nil; +}; + +// sretretsizetn — verbatim port of cstage cg_sret_retsize (cmd/w6c/ +// cgen.c:380) keyed on the checker-STAMPED *tinfo. The tinfo twin of +// sretretsize, the #31 store-loop choke-point (the sret hard-stop / +// sret-receive field sub-arms). Walks resolved tinfo, so the #129 +// aliassamemod/module-juggling sretretsize carries — pure name-keying +// compensation for structlookup's any-module fallback — is NOT needed +// and absent: tichase(t) peels NAMED then falls through all four kind +// tests to 0, exactly what #129 reconstructed by name (cstage sees a +// resolved Type*, never a name). There is NO TY_BANG tinfo kind — `!T` +// folds into TY_TAGGED's per-variant iserror (typ.ww:82-90), so there +// is no error-peel branch; mirror cstage's four branches exactly. `c` +// kept for call-site symmetry with sretretsize (unused: tinfo carries +// the module-independent layout). +fn sretretsizetn(c: *cgen, t: *syntax.tinfo) i32 = { + let u: *syntax.tinfo = tichase(t); + if (u == nil) { return 0; }; + if (u.kind == syntax.tykind.TY_STRUCT) { + if (u.size: i32 <= 24) { return 0; }; + return u.size: i32; + }; + // #38: a tagged union rides AX(tag)+DX/CX/R8 = TUPLE_GPCAP + // eightbytes; nullable folds to one ptr word. Mirrors cstage + // cg_sret_retsize TY_TAGGED arm. + if (u.kind == syntax.tykind.TY_TAGGED) { + if (u.nullable != 0) { return 0; }; + if (u.size: i32 <= TUPLE_GPCAP * 8) { return 0; }; + return u.size: i32; + }; + // #267: arrays ride the struct-return ABI (≤24 reg / >24 sret). + if (u.kind == syntax.tykind.TY_ARRAY) { + if (u.size: i32 <= 24) { return 0; }; + return u.size: i32; + }; + // #10: over-cap tuple → sret. Walk the positional element chain + // over the SAME caps the SEND/receive use (a float = 1 SSE + // eightbyte; str/slice/tagged ride their box-rounded GP stride via + // tupeslot; a scalar 1 GP word). Mirrors cstage cg_sret_retsize + // TY_TUPLE arm; tupleelems is the tinfo positional chain. + if (u.kind == syntax.tykind.TY_TUPLE) { + let gptotal: i32 = 0; + let ssecount: i32 = 0; + let te: *syntax.ttupleelem = u.tupleelems; + for (te != nil) { + if (syntax.typeisfloat(te.type_)) { + ssecount = ssecount + 1; + } else { + gptotal = gptotal + tupeslot(te.type_) / 8; + }; + te = te.tnext; + }; + if (gptotal > TUPLE_GPCAP || ssecount > TUPLE_SSECAP) { + return u.size: i32; + }; + return 0; + }; + return 0; +}; + // callsretsize — if N_CALL `n`'s callee returns a plain TY_STRUCT // > 24B, return its natural size; else 0. Wraps sretretsize over the // callee's resolved return type, used by cglet / cgassign receive @@ -5738,3 +5825,419 @@ fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *syntax.node, bpoff: i32) v if (si == nil) { return; }; cgstructlitfill(c, si, lit, 0, 0, "", bpoff); }; + +// cgstructlitfilltn — tinfo-native twin of cgstructlitfill, the #31 +// receiver-layout fill choke-point. Walks the struct's checker-STAMPED +// tfield chain (lu.fields) and keys every per-field decision off the +// resolved tf.type_ (kind / size / signedness) — NO structlookup, NO +// fi.tnode names. Nested struct fields recurse on tichase(tf.type_), so +// a cross-module same-leaf collision (the #224 cluster) cannot reach a +// name lookup at any nesting level. Faithful mirror of cstage +// cg_structlit_fill, which recurses on type_chase_named(fl->type) +// (cgen.c:3179) over resolved Type*. Emission (mode reloads, register +// staging, store ordering) is byte-identical to cgstructlitfill; only +// the LAYOUT source flips from si/fieldinfo to lu/tfield. tinfo.size +// equals structabisize/fi.fsz for a struct field (check.ww:2468 ABI +// formula; NAMED.size==under.size at :2234), so the size-keyed stores +// match. Modes: 0=DST_BP, 1=DST_PTR_LOCAL(srcoff), 2=DST_GLOBAL(srcname), +// 3=DST_PTR_SP. Routed by the W1/W2/W5 N_STRUCTLIT sub-arms + W4b. +fn cgstructlitfilltn(c: *cgen, lu: *syntax.tinfo, lit: *syntax.node, + mode: i32, srcoff: i32, srcname: str, + disp: i32) void = { + let s: *syntax.tinfo = tichase(lu); + if (s == nil) { return; }; + if (s.kind != syntax.tykind.TY_STRUCT) { return; }; + let basereg: str = "BP"; + if (mode != 0) { basereg = "BX"; }; + let totsize: i32 = s.size: i32; + if (lit.op == syntax.tkind.TK_ELLIPSIS) { + // `..., ...` autofill — zero the entire slot first so + // unmentioned fields read as 0. Sized stores: 8/4/1. For + // non-BP modes, reload BX once before the loop. + emitline("\tXORQ\tAX, AX\n"); + if (mode == 1) { + emitline("\tMOVQ\t"); + emitoff(srcoff: i64); + emitline("(BP), BX\n"); + }; + if (mode == 2) { + emitline("\tLEAQ\t"); + emitsymname(c, srcname); + emitline("(SB), BX\n"); + }; + if (mode == 3) { + emitline("\tMOVQ\t(SP), BX\n"); + }; + let zi: i32 = 0; + for (zi + 8 <= totsize) { + emitline("\tMOVQ\tAX, "); + if (mode == 0) { + emitoff((disp + zi): i64); + emitline("(BP)\n"); + } else { + emitdispreg((disp + zi): i64, basereg); + emitline("\n"); + }; + zi += 8; + }; + for (zi + 4 <= totsize) { + emitline("\tMOVL\tAX, "); + if (mode == 0) { + emitoff((disp + zi): i64); + emitline("(BP)\n"); + } else { + emitdispreg((disp + zi): i64, basereg); + emitline("\n"); + }; + zi += 4; + }; + for (zi < totsize) { + emitline("\tMOVB\tAX, "); + if (mode == 0) { + emitoff((disp + zi): i64); + emitline("(BP)\n"); + } else { + emitdispreg((disp + zi): i64, basereg); + emitline("\n"); + }; + zi += 1; + }; + }; + let fieldnode: *syntax.node = lit.list; + for (fieldnode != nil) { + if (fieldnode.kind == syntax.nkind.N_FIELD) { + let fname: str = fieldnode.str; + let tf: *syntax.tfield = tfieldlookup(s, fname); + if (tf != nil) { + let foff: i32 = tf.offset: i32; + let ftraw: *syntax.tinfo = tf.type_; + let fu: *syntax.tinfo = tichase(ftraw); + let fsz: i32 = 0; + if (fu != nil) { fsz = fu.size: i32; }; + // Tagged-union field: delegate to the shared + // widening writer (str/scalar/struct-lit/ident + // payload + tagged-subset tag remap). For non-BP + // modes, reload BX first. + if (syntax.typeistagged(ftraw)) { + if (mode == 1) { + emitline("\tMOVQ\t"); + emitoff(srcoff: i64); + emitline("(BP), BX\n"); + }; + if (mode == 2) { + emitline("\tLEAQ\t"); + emitsymname(c, srcname); + emitline("(SB), BX\n"); + }; + if (mode == 3) { + emitline("\tMOVQ\t(SP), BX\n"); + }; + cgwidentaggedstore(c, ftraw, + fieldnode.lhs, basereg, + disp + foff, fsz); + } else { + // Nested struct-typed structlit value: recurse at + // the field's offset off the resolved field tinfo + // (NO structlookup). Pre-#17/#18 the cgexpr-then- + // store tail would land AX = first qword and the + // rest silently stayed zero. + let nested: bool = false; + if (fieldnode.lhs != nil) { + if (fieldnode.lhs.kind == syntax.nkind.N_STRUCTLIT) { + if (fu != nil) { + if (fu.kind == syntax.tykind.TY_STRUCT) { + cgstructlitfilltn(c, fu, + fieldnode.lhs, + mode, srcoff, srcname, + disp + foff); + nested = true; + }; + }; + }; + }; + // Nested struct-typed CALL value (#20): cgexpr + // leaves AX/DX/CX per #4's cgreturn ABI; the + // choke-point stores full 8B chunks + a sized tail. + // Guard fu.size <= 24 (#14: every in-cap tail incl. + // 3/5/6/7); >24B falls through (sret deferred). + // INVARIANT: between cgexpr(N_CALL) and the AX/DX/CX + // stores, only the BX reload may intervene. + let callwhole: bool = false; + if (!nested) { + if (fieldnode.lhs != nil) { + if (fieldnode.lhs.kind == syntax.nkind.N_CALL) { + if (fu != nil) { + if (fu.kind == syntax.tykind.TY_STRUCT) { + let cfsz: i32 = fu.size: i32; + if (cfsz <= 24) { + cgexpr(c, fieldnode.lhs); + if (mode == 1) { + emitline("\tMOVQ\t"); + emitoff(srcoff: i64); + emitline("(BP), BX\n"); + }; + if (mode == 2) { + emitline("\tLEAQ\t"); + emitsymname(c, srcname); + emitline("(SB), BX\n"); + }; + if (mode == 3) { + emitline("\tMOVQ\t(SP), BX\n"); + }; + cgaggregstore(c, basereg, disp + foff, cfsz, false); + callwhole = true; + }; + }; + }; + }; + }; + }; + if (nested) { + // recursed above + } else if (callwhole) { + // stored above + } else if (syntax.typeisstr(ftraw) || syntax.typeisslice(ftraw)) { + // str IS []u8: 3-word {ptr,len,cap}. cgexpr + // leaves AX/BX/CX; for non-BP modes the dst + // base goes in DX to dodge BX=len / CX=cap. + cgexpr(c, fieldnode.lhs); + if (mode == 0) { + emitline("\tMOVQ\tAX, "); + emitoff((disp + foff): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tBX, "); + emitoff((disp + foff + 8): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tCX, "); + emitoff((disp + foff + 16): i64); + emitline("(BP)\n"); + } else { + if (mode == 1) { + emitline("\tMOVQ\t"); + emitoff(srcoff: i64); + emitline("(BP), DX\n"); + } else { if (mode == 3) { + emitline("\tMOVQ\t(SP), DX\n"); + } else { + emitline("\tLEAQ\t"); + emitsymname(c, srcname); + emitline("(SB), DX\n"); + }; }; + emitline("\tMOVQ\tAX, "); + emitdispreg((disp + foff): i64, "DX"); + emitline("\n"); + emitline("\tMOVQ\tBX, "); + emitdispreg((disp + foff + 8): i64, "DX"); + emitline("\n"); + emitline("\tMOVQ\tCX, "); + emitdispreg((disp + foff + 16): i64, "DX"); + emitline("\n"); + }; + } else if (fu != nil && fu.kind == syntax.tykind.TY_ARRAY + && fieldnode.lhs != nil + && fieldnode.lhs.kind == syntax.nkind.N_ARRLIT) { + // #249: array field from an N_ARRLIT. element- + // wise store at disp+foff+i*esz off the array + // element tinfo (fu.sub). int/float elements + // only; str/slice/struct/tagged elements are the + // N_LET multi-word gap — loud rule-7 (cstage + // cg_structlit_fill array arm twin). + let eti: *syntax.tinfo = tichase(fu.sub); + let isstructel: bool = false; + if (eti != nil) { + if (eti.kind == syntax.tykind.TY_STRUCT) { isstructel = true; }; + }; + if (syntax.typeisstr(fu.sub) || syntax.typeisslice(fu.sub) + || isstructel || syntax.typeistagged(fu.sub)) { + let e1: str = "ww: struct-literal array field '"; + os.write(2, e1.ptr, e1.len: u64); + os.write(2, fname.ptr, fname.len: u64); + let e2: str = "' has a str/slice/struct/tagged element — multi-word element store out of #249 scope (N_LET array-init gap)\n"; + os.write(2, e2.ptr, e2.len: u64); + os.exit(1); + }; + let esz: i32 = 8; + if (eti != nil) { esz = eti.size: i32; }; + let mop: str = storeopsz(esz); + let isfloatel: bool = syntax.typeisfloat(fu.sub); + let fmov: str = "MOVSD"; + if (syntax.typeisf32(fu.sub)) { fmov = "MOVSS"; }; + let idx: i32 = 0; + let repeat: bool = false; + let e: *syntax.node = fieldnode.lhs.list; + for (e != nil) { + let isellip: bool = false; + if (e.kind == syntax.nkind.N_FIELD) { + if (syntax.streq(e.str, "...")) { + repeat = true; + isellip = true; + }; + }; + if (isellip) { + e = nil; + } else { + cgexpr(c, e); + if (mode == 1) { + emitline("\tMOVQ\t"); + emitoff(srcoff: i64); + emitline("(BP), BX\n"); + }; + if (mode == 2) { + emitline("\tLEAQ\t"); + emitsymname(c, srcname); + emitline("(SB), BX\n"); + }; + if (mode == 3) { + emitline("\tMOVQ\t(SP), BX\n"); + }; + let eoff: i32 = disp + foff + idx * esz; + if (isfloatel) { + emitline("\t"); + emitline(fmov); + emitline("\tX0, "); + } else { + emitline("\t"); + emitline(mop); + emitline("\tAX, "); + }; + if (mode == 0) { + emitoff(eoff: i64); + emitline("(BP)\n"); + } else { + emitdispreg(eoff: i64, basereg); + emitline("\n"); + }; + idx += 1; + e = e.next; + }; + }; + if (repeat) { + let total: i32 = fu.alen: i32; + for (idx < total) { + if (mode == 1) { + emitline("\tMOVQ\t"); + emitoff(srcoff: i64); + emitline("(BP), BX\n"); + }; + if (mode == 2) { + emitline("\tLEAQ\t"); + emitsymname(c, srcname); + emitline("(SB), BX\n"); + }; + if (mode == 3) { + emitline("\tMOVQ\t(SP), BX\n"); + }; + let eoff: i32 = disp + foff + idx * esz; + if (isfloatel) { + emitline("\t"); + emitline(fmov); + emitline("\tX0, "); + } else { + emitline("\t"); + emitline(mop); + emitline("\tAX, "); + }; + if (mode == 0) { + emitoff(eoff: i64); + emitline("(BP)\n"); + } else { + emitdispreg(eoff: i64, basereg); + emitline("\n"); + }; + idx += 1; + }; + }; + } else if (fu != nil + && (fu.kind == syntax.tykind.TY_STRUCT + || fu.kind == syntax.tykind.TY_ARRAY + || fu.kind == syntax.tykind.TY_TUPLE)) { + // #49: aggregate (struct/array/tuple) field from + // an ADDRESSABLE source expr — `outer{.., r = r}`. + // Funnel: source addr via aggargsrcaddr (SI), + // field addr via LEAQ/ADDQ (BX, AFTER the source + // walk clobbers BX/AX), then aggcopy. Width = the + // resolved field tinfo size. Non-addressable + // sources die loud (rule 7). cstage #49 arm twin. + if (!aggargsrcaddr(c, fieldnode.lhs, "SI")) { + let m49g: str = "structlit fill: aggregate field '"; + os.write(2, m49g.ptr, m49g.len: u64); + os.write(2, fname.ptr, fname.len: u64); + let m49h: str = "' from a non-addressable source unwired (task #49/rule-7)\n"; + os.write(2, m49h.ptr, m49h.len: u64); + os.exit(1); + }; + if (mode == 0) { + emitline("\tLEAQ\t"); + emitoff((disp + foff): i64); + emitline("(BP), BX\n"); + } else { + if (mode == 1) { + emitline("\tMOVQ\t"); + emitoff(srcoff: i64); + emitline("(BP), BX\n"); + } else { if (mode == 3) { + emitline("\tMOVQ\t(SP), BX\n"); + } else { + emitline("\tLEAQ\t"); + emitsymname(c, srcname); + emitline("(SB), BX\n"); + }; }; + if (disp + foff != 0) { + emitline("\tADDQ\t$"); + emitint((disp + foff): i64); + emitline(", BX\n"); + }; + }; + aggcopy(c, fsz); + } else { + cgexpr(c, fieldnode.lhs); + // For non-BP modes, cgexpr just clobbered BX; + // reload it before the store. + if (mode == 1) { + emitline("\tMOVQ\t"); + emitoff(srcoff: i64); + emitline("(BP), BX\n"); + }; + if (mode == 2) { + emitline("\tLEAQ\t"); + emitsymname(c, srcname); + emitline("(SB), BX\n"); + }; + if (mode == 3) { + emitline("\tMOVQ\t(SP), BX\n"); + }; + if (syntax.typeisfloat(ftraw)) { + let mov: str = "MOVSD"; + if (syntax.typeisf32(ftraw)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\tX0, "); + if (mode == 0) { + emitoff((disp + foff): i64); + emitline("(BP)\n"); + } else { + emitdispreg((disp + foff): i64, basereg); + emitline("\n"); + }; + } else { + // Full field-width store {1->MOVB, 2->MOVW, + // 4->MOVL, else MOVQ}; the #15 sub-8 guard. + let op: str = storeopsz(fsz); + emitline("\t"); + emitline(op); + emitline("\tAX, "); + if (mode == 0) { + emitoff((disp + foff): i64); + emitline("(BP)\n"); + } else { + emitdispreg((disp + foff): i64, basereg); + emitline("\n"); + }; + }; + }; + }; + }; + }; + fieldnode = fieldnode.next; + }; +}; diff --git a/test/wcc/797_xmod_struct_field_layout_collide_run.c b/test/wcc/797_xmod_struct_field_layout_collide_run.c index f880ffd0..59523125 100644 --- a/test/wcc/797_xmod_struct_field_layout_collide_run.c +++ b/test/wcc/797_xmod_struct_field_layout_collide_run.c @@ -41,12 +41,27 @@ * letcopy_t16 | pair{u64,u16} 16B | pair 24B | C1 | 107 (value coincides; byte-id is the net) * addrptr_pq | pq{u64,u64} 16B | pq 24B | A1 | 10 * addrval_pq | pq{u64,u64} 16B | pq 24B | A2 | 10 + * ptrwrite_t16 | pair{u64,u16} 16B | pair 24B | W1 | 109 (commit-2 STORE) + * valwrite_t16 | pair{u64,u16} 16B | pair 24B | W2 | 109 (commit-2 STORE) + * nestfill_box | box{tag,ir:nst} 32B | box 24B | W2 | 66 (commit-2 cgstructlitfilltn nested recursion) + * + * Commit-2 (RULING R2 / Opt-2) converts the W1/W2/W5/W4b store-loop arms + * off the name-keyed receiver walk to a tinfo-native fill (cgstructlitfilltn + * + sretretsizetn), closing the in-loop #32 nested struct-receive/structlit/ + * ident sub-arms by construction. The two scalar WRITE rows STORE through the + * mis-resolved offset: at commit-1 (R1 read already fixed) wwstage stored at + * the FOREIGN offset 16 while reading off 8 -> 100+7=107 WRONG vs cstage 109. + * They reach only the scalar storeopsz arm, NOT cgstructlitfilltn; the + * nestfill_box row is the cgstructlitfilltn fill + nested-recursion value pin + * (a struct-literal stored into a struct-typed field, whose own field is a + * nested struct literal) — the riskiest new helper, which no bootstrap + * construct exercises. * * Each row reddens under an INDEPENDENT revert of its arm (verified - * impl-side: R1 ww=101, C1 cs!=ww OOB, A1/A2 ww=8). cstage `ww build - * --sep` + run pins runtime; the wwstage binary is run too (the bug WAS a - * wrong wwstage runtime value / OOB); raw cs.s vs ww.s over the - * driver-produced per-package asm pins rule-10 byte-id. + * impl-side: R1 ww=101, C1 cs!=ww OOB, A1/A2 ww=8, W1/W2 ww=107 cs!=ww). + * cstage `ww build --sep` + run pins runtime; the wwstage binary is run too + * (the bug WAS a wrong wwstage runtime value / OOB); raw cs.s vs ww.s over + * the driver-produced per-package asm pins rule-10 byte-id. * * NOTE the addr rows use an all-u64 `pq` (offset-only collision): the * canonical {u64,u16} field would, after `&p.lo`, force a `*q = v:u16` @@ -188,11 +203,101 @@ static const struct file addrval_files[] = { { NULL, NULL } }; +/* ---- ptrwrite_t16 (W1, commit-2): inferred-let *struct field STORE. + * `p.lo = 9: u16` resolved the field OFFSET via the name-keyed lookup -> + * the FOREIGN 24B pair (lo @ off 16, u64) -> the store landed at off 16 + * (a u64 MOVQ into g's DATA neighbour) while the read (R1, fixed in + * commit-1) reads off 8 -> p.lo stays 7 -> 100+7=107 WRONG (cstage 109, + * cs!=ww). The {u64,u16} field stores MOVW in both stages (a direct + * field store, NOT the addr-of `*q=v:u16` narrow-deref-store divergence + * the A1/A2 pq rows dodge), so it isolates the #31 W1 offset/width fix. */ +static const struct file ptrwrite_files[] = { + { "m1.ww", m1_src }, + { "m2.ww", m2_src }, + { "main.ww", + "package main;\n" + "import m1;\n" + "import m2;\n" + "fn main() i32 = {\n" + " let dummy: m2.pair;\n" + " dummy.hi = 0: u64;\n" + " let p = m1.mkp();\n" + " p.lo = 9: u16;\n" + " return (p.hi + (p.lo: u64)): i32;\n" + "};\n" }, + { NULL, NULL } +}; + +/* ---- valwrite_t16 (W2, commit-2): inferred-let value-struct field STORE. + * `s.lo = 9: u16` resolved the OFFSET via the FOREIGN 24B pair -> stored + * 8 bytes at off 16 (past s's 16B slot) while s.lo (off 8) stays 7 -> + * 100+7=107 WRONG (cstage 109). The write-twin #21 missed. */ +static const struct file valwrite_files[] = { + { "m1.ww", m1_src }, + { "m2.ww", m2_src }, + { "main.ww", + "package main;\n" + "import m1;\n" + "import m2;\n" + "fn main() i32 = {\n" + " let dummy: m2.pair;\n" + " dummy.hi = 0: u64;\n" + " let s = m1.mk();\n" + " s.lo = 9: u16;\n" + " return (s.hi + (s.lo: u64)): i32;\n" + "};\n" }, + { NULL, NULL } +}; + +/* ---- nestfill_box (W2 -> cgstructlitfilltn, commit-2): the nested- + * recursion VALUE pin for the new fill helper. `s.ir = nst{...}` stores a + * struct-LITERAL into a struct-typed field of a value-struct local, + * driving cgstructlitfilltn (the W2 N_STRUCTLIT sub-arm, mode 0); the + * literal's `dp = deep{...}` field is itself a struct literal, so + * cgstructlitfilltn RECURSES — cgstructlitfilltn(tichase(tf.type_)) at + * the inner offset (box.ir.dp @ +16). This is the riskiest new code and + * is exercised by NO bootstrap construct (the corpus has zero `x.f = + * Y{...}` struct-lit field stores) and by NO other 797 row (they all + * store scalars/idents/calls, hitting only the storeopsz scalar arm). + * Want = i0 + dp.d0 + dp.d1 = 11 + 22 + 33 = 66; a broken recursion + * drops dp -> 11, a mis-offset -> a wrong sum. + * + * COLLISION-FREE by necessity (NOT an oversight): a cross-module + * same-leaf box collision CANNOT drive this pin because TWO pre-existing + * wwstage CHECKER gaps (orthogonal to #31, which is cgen-only) block the + * wwstage readback under collision — (a) the checker mis-resolves a + * nested value-struct field read to the FOREIGN field type (`u64 -> nst + * not assignable`); (b) a chained `s.ir.dp.d0` read trips `asserttyped: + * dot`. So the value is read back via single-dot-through-param helpers + * (sumnst/sumdeep), the only nested-read shape wwstage accepts. The pin + * still catches every cgstructlitfilltn fill/recursion miscompile (cs == + * ww runtime + cs.s == ww.s byte-id); the collision surface for the + * SCALAR store stays covered by ptrwrite/valwrite above. */ +static const struct file nestfill_files[] = { + { "main.ww", + "package main;\n" + "type deep = struct { d0: u64, d1: u64 };\n" + "type nst = struct { i0: u64, dp: deep };\n" + "type box = struct { tag: u64, ir: nst };\n" + "fn mkbox() box = { return box { tag = 0: u64, ir = nst { i0 = 0: u64, dp = deep { d0 = 0: u64, d1 = 0: u64 } } }; };\n" + "fn sumdeep(d: deep) u64 = { return d.d0 + d.d1; };\n" + "fn sumnst(n: nst) u64 = { return n.i0 + sumdeep(n.dp); };\n" + "fn main() i32 = {\n" + " let s = mkbox();\n" + " s.ir = nst { i0 = 11: u64, dp = deep { d0 = 22: u64, d1 = 33: u64 } };\n" + " return sumnst(s.ir): i32;\n" + "};\n" }, + { NULL, NULL } +}; + static const struct scenario scenarios[] = { - { "ptrread_t16", ptrread_files, 107 }, - { "letcopy_t16", letcopy_files, 107 }, - { "addrptr_pq", addrptr_files, 10 }, - { "addrval_pq", addrval_files, 10 }, + { "ptrread_t16", ptrread_files, 107 }, + { "letcopy_t16", letcopy_files, 107 }, + { "addrptr_pq", addrptr_files, 10 }, + { "addrval_pq", addrval_files, 10 }, + { "ptrwrite_t16", ptrwrite_files, 109 }, + { "valwrite_t16", valwrite_files, 109 }, + { "nestfill_box", nestfill_files, 66 }, }; static int