From eb28dcd5b7ef66c71fbaad954b8bfd31e8f53142 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 29 Jun 2026 14:01:47 +0900 Subject: [PATCH] cgen: type-key the struct field-layout receiver arms via stamped tinfo (#31 c1) wwstage cgen resolved struct field LAYOUT (offset + field type) by bare-leaf name (structlookupchain / structlookup / letvarstructinfo) at nine field READ / addr-of / let-copy receiver arms whose base is a *struct pointer, a value struct, or a module global. Under a cross-module same-leaf collision (two modules each exporting a `pair`, 16B vs 24B) the bare-leaf lookup first-matches the wrong-size struct -> the field is read / addressed / copied at the wrong offset with the wrong width. cstage type-keys off the stamped node.type_ (type_chase_named(base->type)->fields) and is correct; this aligns wwstage UP to it (ww-only change). Route the nine arms off the stamped receiver tinfo: R1/R2 *struct reads via a new shared choke-point cgptrfieldloadtf (the tinfo twin of cgptrfieldload); A1/A2/A3 addr-of and W3 scalar global-store via tichase(recv.type_)->fields; C1/W4a copy/size via structabisizetn(tichase(.type_)). Mirrors #21 (5ae6e34); the *struct arms peel the pointee with tichase(.type_).sub, the faithful twin of cstage type_chase_named(bu->sub). The global value-struct arms (R2/R3/A3/W3/W4a) are converted-for-construction: a global struct's type is always explicitly qualified, so the bare leaf already resolved correctly and they cannot be reddened -- byte-id (cs.s==ww.s) is their net, not a value pin. Labelled so the absent reddening pin is explained, not silent. Commit 1 of a 2-commit arc (RULING R2 / Opt-2, .ai/ken-31-spec.md): closes the field-LAYOUT read/copy/addr surface. Commit 2 converts the W1/W2/W5/W4b store loops to a tinfo-native fill (cgstructlitfilltn + sretretsizetn) and closes the in-loop nested sub-arms by construction. Part of the #224 name-keyed-cgen cluster retirement. Pin: test/wcc/797 value-asserts R1 ptr-read / C1 let-copy / A1/A2 addr, each reddening under independent per-arm revert (a ratchet proves nothing for this silent-capable class). --- Makefile | 12 + selfhost/cmd/wcc/cgenexpr.ww | 369 +++++++++++------- selfhost/cmd/wcc/cgenstmt.ww | 90 ++--- ...797_xmod_struct_field_layout_collide_run.c | 305 +++++++++++++++ 4 files changed, 578 insertions(+), 198 deletions(-) create mode 100644 test/wcc/797_xmod_struct_field_layout_collide_run.c diff --git a/Makefile b/Makefile index 67e73d7b..2dba1027 100644 --- a/Makefile +++ b/Makefile @@ -431,6 +431,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_type_value_shadow_run \ $(BIN)/test_xmod_alias_struct_collide_run \ $(BIN)/test_xmod_struct_argpush_collide_run \ + $(BIN)/test_xmod_struct_field_layout_collide_run \ $(BIN)/test_xmod_variant_match \ $(BIN)/test_xmod_qualstructlit_run \ $(BIN)/test_spread_variant_match \ @@ -1532,6 +1533,17 @@ $(BIN)/test_xmod_struct_argpush_collide_run: test/wcc/793_xmod_struct_argpush_co $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# 797 #31 (RULING R2 commit-1): inferred-let struct field-LAYOUT receiver +# arms — local *struct READ, let-COPY size, &p.f / &o.f addr-of — keyed +# off the bare leaf that mis-resolves cross-module; fix routes them off the +# stamped tinfo (tichase(recv.type_)->fields / structabisizetn). Direct +# continuation of 793 (#21). Own 2-module fixtures in a private mktemp dir. +$(BIN)/test_xmod_struct_field_layout_collide_run: test/wcc/797_xmod_struct_field_layout_collide_run.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 $@ $< + # #13: cross-module decomposition of an imported union's variants # (`case pkg.a`). Pre-fix the wwstage checker rejected it; cstage built # it. cstage driver build + run pins routing; ww-sep build (w6c_ww must diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 1bd8fcbf..e329450f 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -3403,6 +3403,59 @@ fn cgptrfieldload(c: *cgen, fi: *fieldinfo) void = { emitline(", AX\n"); }; +// cgptrfieldloadtf — tfield twin of cgptrfieldload (#31): load struct +// field `tf` from a *struct base already in BX, resolving offset + field +// type off the checker-STAMPED tfield (tf.offset/tf.type_) instead of the +// name-keyed fieldinfo. The read-side choke-point shared by the R1 (local) +// and R2 (global) *struct field-read arms; a cross-module same-leaf +// collision that mis-resolved structlookupchain(inner) cannot reach a +// stamped tinfo. Emission is byte-identical to cgptrfieldload (the #21 R0 +// substitution table): syntax.typeis*(tf.type_) replaces is*type(fi.tnode), +// tichase(tf.type_).size replaces slotsize/fsz. BX is never a load target, +// so the str/slice triple writes BX (the base) LAST. +fn cgptrfieldloadtf(c: *cgen, tf: *syntax.tfield) void = { + let foff: i32 = tf.offset: i32; + let ftraw: *syntax.tinfo = tf.type_; + if (syntax.typeistagged(ftraw)) { + let ftc: *syntax.tinfo = tichase(ftraw); + let tsz: i32 = 0; + if (ftc != nil) { tsz = ftc.size: i32; }; + cgloadtaggedfield(c, "BX", foff, tsz, false); + return; + }; + if (syntax.typeisstr(ftraw) || syntax.typeisslice(ftraw)) { + emitline("\tMOVQ\t"); + emitdispreg(foff: i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVQ\t"); + emitdispreg((foff + 16): i64, "BX"); + emitline(", CX\n"); + emitline("\tMOVQ\t"); + emitdispreg((foff + 8): i64, "BX"); + emitline(", BX\n"); + return; + }; + if (syntax.typeisfloat(ftraw)) { + let mov: str = "MOVSD"; + if (syntax.typeisf32(ftraw)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t"); + emitdispreg(foff: i64, "BX"); + emitline(", X0\n"); + return; + }; + let ftc: *syntax.tinfo = tichase(ftraw); + let fsz: i32 = 0; + if (ftc != nil) { fsz = ftc.size: i32; }; + let op: str = loadopsz(syntax.typeissigned(ftraw), fsz); + emitline("\t"); + emitline(op); + emitline("\t"); + emitdispreg(foff: i64, "BX"); + emitline(", AX\n"); +}; + // emitchainbase — load the chained-DOT root BASE into CX for the viacx // (global or `*T`-root) spine. Mirrors cstage cgen.c's uniform base // resolution: a global is reached via LEAQ name(SB),CX; a `*struct` @@ -3533,41 +3586,37 @@ fn cgdot(c: *cgen, n: *syntax.node) void = { let lkind: syntax.nkind = tn.kind; // Pointer-to-struct: deref then field load. 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 walks the alias chain on - // a miss so `*tokenizer` where tokenizer is - // a transitively-aliased struct still - // resolves to the underlying fieldinfo (#22). - 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)) { - // Stage the *struct base in BX, then field - // load via cgptrfieldload (shared with the - // #15 global-ptr arm below). BX isn't a load - // target (AX/DX/CX/R8/X0), so order is - // harmless. Mirrors cstage cgen.c N_DOT - // *struct field arm. - emitline("\tMOVQ\t"); - emitoff(lc.off: i64); - emitline("(BP), BX\n"); - cgptrfieldload(c, fi); - return; - }; - fi = fi.finext; + // #31: resolve the *struct field OFFSET + type off the + // checker-STAMPED receiver tinfo (tichase(dotlhs.type_) + // → .sub pointee), NOT the name-keyed + // structlookupchain(inner). Under a cross-module same- + // leaf collision inner is a bare leaf that mis-resolves + // to a FOREIGN same-leaf struct → wrong offset/load-op + // (the inferred-local READ row, ww=28). The stamped tinfo + // carries the right layout; mirror cstage + // type_chase_named(bu->sub)->fields (cgen.c:506-512) + + // the #21 R0 template. Non-struct pointees (*str/*slice/ + // *[N]T) are not TY_STRUCT → fall through to the pseudo- + // field arms below (guard, never a bare-leaf fallback). + // Read choke-point cgptrfieldloadtf shared with the R2 + // global arm. Stage the *struct base in BX (not a load + // target), then field-load. + let sti: *syntax.tinfo = nil; + if (dotlhs != nil) { sti = tichase(dotlhs.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)) { + emitline("\tMOVQ\t"); + emitoff(lc.off: i64); + emitline("(BP), BX\n"); + cgptrfieldloadtf(c, tf); + return; }; + tf = tf.tnext; }; - }; + }; }; }; // Direct struct local: field load at off+foff. if (lkind == syntax.nkind.N_TNAME) { @@ -3855,29 +3904,29 @@ fn cgdot(c: *cgen, n: *syntax.node) void = { if (gtn != nil) { if (gtn.kind == syntax.nkind.N_TPTR) { let inner: *syntax.node = gtn.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) { - let si: *structinfo = structlookupchain(c, inner); - if (si != nil) { - let fi: *fieldinfo = si.fields; - for (fi != nil) { - if (syntax.streq(fi.fname, fld)) { - emitline("\tMOVQ\t"); - emitsymname(c, gnm); - emitline("(SB), BX\n"); - cgptrfieldload(c, fi); - return; - }; - fi = fi.finext; + // #31: stamped-tinfo *struct field resolution + // (tichase(dotlhs.type_)->.sub), the read choke-point + // twin of the R1 local arm -- see cgptrfieldloadtf. A + // global *struct decl is qualified (let gp: *m1.pair) + // -> non-reddenable; converted for close-by-construction + // (byte-id with the name-keyed path). Non-struct + // pointees fall through to the str/slice arm below. + let sti: *syntax.tinfo = nil; + if (dotlhs != nil) { sti = tichase(dotlhs.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)) { + emitline("\tMOVQ\t"); + emitsymname(c, gnm); + emitline("(SB), BX\n"); + cgptrfieldloadtf(c, tf); + return; }; + tf = tf.tnext; }; - }; + }; }; // Pointer to str/slice (`*[]u8`, `*str`): deref // name(SB), then load at delta within the header. let delta: i32 = -1; @@ -4141,65 +4190,64 @@ fn cgdot(c: *cgen, n: *syntax.node) void = { // from the wrong offset). if (lhs != nil) { if (lhs.kind == syntax.nkind.N_IDENT) { - let si: *structinfo = letvarstructinfo(c, lhs.str); - if (si == nil) { si = defvarstructinfo(c, lhs.str); }; - 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 ident tinfo (tichase(lhs.type_)), + // NOT name-keyed letvarstructinfo/defvarstructinfo. A global + // decl is always qualified (let g: m1.pair) -> non-reddenable; + // converted for close-by-construction (byte-id). Covers both + // let- and def-struct globals (both stamp lhs.type_). Mirrors + // cstage type-keyed N_DOT global arm + the #21 R0 template. + let sti: *syntax.tinfo = tichase(lhs.type_: *syntax.tinfo); + 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_; 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, true); + if (syntax.typeistagged(ftraw)) { + let ftc: *syntax.tinfo = tichase(ftraw); + let tsz: i32 = 0; + if (ftc != nil) { tsz = ftc.size: i32; }; + cgloadtaggedfield(c, "CX", foff, tsz, true); return; }; - // str IS []u8 — 3-word {ptr,len,cap}, the local - // slice-field arm (BP) retargeted to the CX global - // base. cap→CX LAST: CX is the base, so .ptr/.len - // must read first. cstage folds local+global in one - // base_reg arm; ww splits them, so this global arm - // carries its own lift (filed divergence task). - if (isstrtype(c, fi.tnode)) { + if (syntax.typeisstr(ftraw)) { emitline("\tMOVQ\t"); - emitdispreg(fi.foff: i64, "CX"); + emitdispreg(foff: i64, "CX"); emitline(", AX\n"); emitline("\tMOVQ\t"); - emitdispreg((fi.foff + 8): i64, "CX"); + emitdispreg((foff + 8): i64, "CX"); emitline(", BX\n"); emitline("\tMOVQ\t"); - emitdispreg((fi.foff + 16): i64, "CX"); + emitdispreg((foff + 16): i64, "CX"); emitline(", CX\n"); - } else { if (isfloattype(c, fi.tnode)) { - // f64/f32 global field: route through X0. + } else { 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("\t"); - emitdispreg(fi.foff: i64, "CX"); + emitdispreg(foff: i64, "CX"); emitline(", X0\n"); } else { - let op: str = fieldloadop(c, fi); + let ftc: *syntax.tinfo = tichase(ftraw); + let fsz: i32 = 0; + if (ftc != nil) { fsz = ftc.size: i32; }; + let op: str = loadopsz(syntax.typeissigned(ftraw), fsz); emitline("\t"); emitline(op); emitline("\t"); - emitdispreg(fi.foff: i64, "CX"); + emitdispreg(foff: i64, "CX"); emitline(", AX\n"); }; }; return; }; - fi = fi.finext; + tf = tf.tnext; }; - }; + }; }; }; }; // `arr[i].field` — element-then-field through a `[N]*S` / `[N]S` @@ -5277,30 +5325,29 @@ fn cgun(c: *cgen, n: *syntax.node) void = { // structlookup head, byte-id by // construction. 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) { - let si: *structinfo = structlookupchain(c, inner); - if (si != nil) { - let fi: *fieldinfo = si.fields; - for (fi != nil) { - if (syntax.streq(fi.fname, fld)) { - emitline("\tMOVQ\t"); - emitoff(lc.off: i64); - emitline("(BP), AX\n"); - emitline("\tLEAQ\t"); - emitdispreg(fi.foff: i64, "AX"); - emitline(", AX\n"); - return; - }; - fi = fi.finext; + // #31: &p.f offset off the stamped *struct tinfo + // (tichase(opnd.lhs.type_)->.sub), NOT structlookupchain(inner). + // Twin of the R1 read arm (inferred-local ADDR row). Non-struct + // pointees fall through to the slice/str pseudo arm below. + let sti: *syntax.tinfo = nil; + if (opnd.lhs != nil) { sti = tichase(opnd.lhs.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; + emitline("\tMOVQ\t"); + emitoff(lc.off: i64); + emitline("(BP), AX\n"); + emitline("\tLEAQ\t"); + emitdispreg(foff: i64, "AX"); + emitline(", AX\n"); + return; }; + tf = tf.tnext; }; - }; + }; }; }; // Value-struct local: &o.f. // #102 review-found sibling: same @@ -5309,19 +5356,24 @@ fn cgun(c: *cgen, n: *syntax.node) void = { // struct fell to the generic route. // Same chase, same construction. if (lkind == syntax.nkind.N_TNAME) { - let si: *structinfo = structlookupchain(c, tn); - if (si != nil) { - let fi: *fieldinfo = si.fields; - for (fi != nil) { - if (syntax.streq(fi.fname, fld)) { + // #31: &o.f offset off the stamped value-struct tinfo + // (tichase(opnd.lhs.type_)), NOT structlookupchain(tn). Non- + // struct (str alias) falls through to the slice/str arm below. + let sti: *syntax.tinfo = nil; + if (opnd.lhs != nil) { sti = tichase(opnd.lhs.type_: *syntax.tinfo); }; + 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; emitline("\tLEAQ\t"); - emitoff((lc.off + fi.foff): i64); + emitoff((lc.off + foff): i64); emitline("(BP), AX\n"); return; }; - fi = fi.finext; + tf = tf.tnext; }; - }; + }; }; }; // Slice/str pseudo-field on a local: // &s.ptr / &s.len / &s.cap. Delta is @@ -5347,22 +5399,28 @@ fn cgun(c: *cgen, n: *syntax.node) void = { // Global root: top-level let, either a // struct or a slice/str. if (isletvar(c, basenm)) { - let gsi: *structinfo = letvarstructinfo(c, basenm); - if (gsi != nil) { - let fi: *fieldinfo = gsi.fields; - for (fi != nil) { - if (syntax.streq(fi.fname, fld)) { + // #31: global &g.f offset off the stamped value-struct tinfo + // (tichase(opnd.lhs.type_)), NOT name-keyed letvarstructinfo. + // Global decls are qualified -> non-reddenable; converted for + // close-by-construction (byte-id). + let gsti: *syntax.tinfo = nil; + if (opnd.lhs != nil) { gsti = tichase(opnd.lhs.type_: *syntax.tinfo); }; + if (gsti != nil) { if (gsti.kind == syntax.tykind.TY_STRUCT) { + let tf: *syntax.tfield = gsti.fields; + for (tf != nil) { + if (syntax.streq(tf.name, fld)) { + let foff: i32 = tf.offset: i32; emitline("\tLEAQ\t"); emitsymname(c, basenm); emitline("(SB), CX\n"); emitline("\tLEAQ\t"); - emitdispreg(fi.foff: i64, "CX"); + emitdispreg(foff: i64, "CX"); emitline(", AX\n"); return; }; - fi = fi.finext; + tf = tf.tnext; }; - }; + }; }; let isstr: bool = letvarisstr(c, basenm); let issl: bool = letvarisslice(c, basenm); if (isstr || issl) { @@ -11310,25 +11368,29 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { let tn: *syntax.node = letvartnode(c, bn); if (tn != nil) { if (tn.kind == syntax.nkind.N_TPTR) { - let inner: *syntax.node = tn.lhs; - let si: *structinfo = structlookupchain(c, inner); - if (si != nil) { - let fi: *fieldinfo = si.fields; - for (fi != nil) { - if (syntax.streq(fi.fname, fld)) { - // scalar only: str/slice/tagged/float/ - // struct fields keep their F6 route. + // #31: scalar global *struct field store -- resolve offset + + // scalar-ness off the stamped *struct tinfo + // (tichase(base.type_)->.sub), NOT structlookupchain(inner). + // Non-scalar fields (str/slice/tagged/float/nested-struct) + // keep their F6 cgplaceaddr route (scalar=false -> fall + // through). Global *struct decls are qualified -> non- + // reddenable; convert for close-by-construction (byte-id). + 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_; + // scalar only: str/slice/tagged/float/struct fields + // keep their F6 route (cstage type-keys the same). let scalar: bool = true; - if (istaggedtype(c, fi.tnode)) { scalar = false; }; - if (isstrtype(c, fi.tnode) || isslicetype(c, fi.tnode)) { scalar = false; }; - if (isfloattype(c, fi.tnode)) { scalar = false; }; - if (fi.tnode != nil) { - if (fi.tnode.kind == syntax.nkind.N_TNAME) { - if (aliasprimsize(c, fi.tnode.str) == 0) { - if (structlookup(c, fi.tnode.str) != nil) { scalar = false; }; - }; - }; - }; + if (syntax.typeistagged(ftraw)) { scalar = false; }; + if (syntax.typeisstr(ftraw) || syntax.typeisslice(ftraw)) { scalar = false; }; + if (syntax.typeisfloat(ftraw)) { scalar = false; }; + let ftc: *syntax.tinfo = tichase(ftraw); + if (ftc != nil) { if (ftc.kind == syntax.tykind.TY_STRUCT) { scalar = false; }; }; if (scalar) { cgexpr(c, n.rhs); emitline("\tPUSHQ\tAX\n"); @@ -11339,18 +11401,23 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { emitdispreg(0i64, "BX"); emitline(", BX\n"); emitline("\tPOPQ\tAX\n"); - let sop: str = fieldstoreop(c, fi); + let fsz: i32 = 0; + if (ftc != nil) { fsz = ftc.size: i32; }; + let sop: str = "MOVQ"; + if (fsz == 1) { sop = "MOVB"; } + else { if (fsz == 2) { sop = "MOVW"; } + else { if (fsz == 4) { sop = "MOVL"; }; }; }; emitline("\t"); emitline(sop); emitline("\tAX, "); - emitdispreg(fi.foff: i64, "BX"); + emitdispreg(foff: i64, "BX"); emitline("\n"); return; }; }; - fi = fi.finext; + tf = tf.tnext; }; - }; + }; }; }; }; }; @@ -12171,8 +12238,10 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { && n.rhs.kind == syntax.nkind.N_CALL && lvftn != nil) { let gsz: i32 = 0; if (lvftn.kind == syntax.nkind.N_TNAME) { - let gsi: *structinfo = structlookup(c, lvftn.str); - if (gsi != nil) { gsz = structabisize(gsi); }; + // #31: size off the stamped tinfo, not structlookup(lvftn.str). + // Non-reddenable (global decl qualified); convert for + // close-by-construction. + gsz = structabisizetn(lvftn.type_: *syntax.tinfo); }; if (lvftn.kind == syntax.nkind.N_TARRAY) { let gat: *syntax.tinfo = lvftn.type_: *syntax.tinfo; diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index 18f9da5c..b8cc7e38 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -2654,55 +2654,49 @@ fn cgletbody(c: *cgen, n: *syntax.node, off: i32) void = { // via the str-init tail) — silent partial copy. Mirrors // cstage cgen.c N_LET struct-ident branch (Task #32). if (rhs.kind == syntax.nkind.N_IDENT) { - let sname: str; - sname.ptr = nil; sname.len = 0; - if (tn != nil) { - if (tn.kind == syntax.nkind.N_TNAME) { sname = tn.str; }; - }; - if (sname.len > 0) { - let lsi: *structinfo = structlookup(c, sname); - if (lsi != nil) { - // memcpy run sizes on the maxalign-rounded ABI - // size — cstage N_LET sets sz = lu->size - // (cgen.c:7533, struct-IDENT branch :7869), - // check.c:760 SSoT. structnaturalsize would - // short struct{i64,i32} (natural 12, ABI 16) - // to MOVQ+MOVL where cstage writes MOVQ+MOVQ. - let lsz: i32 = structabisize(lsi); - if (lsz > 8) { - let lc: *local = localfindnode(c, rhs.str); - if (lc != nil) { - let soff: i32 = lc.off; - let ki: i32 = 0; - for (ki + 8 <= lsz) { - emitline("\tMOVQ\t"); - emitoff((soff + ki): i64); - emitline("(BP), AX\n"); - emitline("\tMOVQ\tAX, "); - emitoff((off + ki): i64); - emitline("(BP)\n"); - ki += 8; - }; - if (ki < lsz) { - let tail: i32 = lsz - ki; - let lop: str = "MOVQ"; - if (tail == 4) { lop = "MOVL"; } - else { if (tail == 1) { lop = "MOVB"; }; }; - emitline("\t"); - emitline(lop); - emitline("\t"); - emitoff((soff + ki): i64); - emitline("(BP), AX\n"); - emitline("\t"); - emitline(lop); - emitline("\tAX, "); - emitoff((off + ki): i64); - emitline("(BP)\n"); - }; - c.lastwasreturn = 0; - return; - }; + // #31: size the local-to-local struct COPY off the COPY + // SOURCE's stamped tinfo (structabisizetn(rhs.type_)), NOT + // structlookup(tn.str). On a cross-module same-leaf collision + // the inferred-let's declared-type leaf (tn.str) mis-resolves + // to a FOREIGN same-leaf struct -> the memcpy run was sized off + // it (a 24B foreign over-reads a 16B source + over-writes the + // dst slot -- silent OOB, cs!=ww). rhs.type_ is the correct + // source struct; byte-id with structabisize on a resolving + // lookup. memcpy run = maxalign-rounded ABI size = cstage N_LET + // sz=lu->size (cgen.c:7533, struct-IDENT :7869, check.c:760 SSoT). + let lsz: i32 = structabisizetn(rhs.type_: *syntax.tinfo); + if (lsz > 8) { + let lc: *local = localfindnode(c, rhs.str); + if (lc != nil) { + let soff: i32 = lc.off; + let ki: i32 = 0; + for (ki + 8 <= lsz) { + emitline("\tMOVQ\t"); + emitoff((soff + ki): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff((off + ki): i64); + emitline("(BP)\n"); + ki += 8; }; + if (ki < lsz) { + let tail: i32 = lsz - ki; + let lop: str = "MOVQ"; + if (tail == 4) { lop = "MOVL"; } + else { if (tail == 1) { lop = "MOVB"; }; }; + emitline("\t"); + emitline(lop); + emitline("\t"); + emitoff((soff + ki): i64); + emitline("(BP), AX\n"); + emitline("\t"); + emitline(lop); + emitline("\tAX, "); + emitoff((off + ki): i64); + emitline("(BP)\n"); + }; + c.lastwasreturn = 0; + return; }; }; }; diff --git a/test/wcc/797_xmod_struct_field_layout_collide_run.c b/test/wcc/797_xmod_struct_field_layout_collide_run.c new file mode 100644 index 00000000..f880ffd0 --- /dev/null +++ b/test/wcc/797_xmod_struct_field_layout_collide_run.c @@ -0,0 +1,305 @@ +/* + * 794_xmod_struct_field_layout_collide_run — project #31 (RULING R2, + * commit-1: the OFFSET/SIZE align-up arms) runtime + byte-id net. The + * direct continuation of 793 (#21): #21 type-keyed the THREE caller-side + * sites the 793 collision drove (push/recv/field-read); #31 closes the + * field-LAYOUT *receiver* arms #21 left name-keyed. Commit-1 covers the + * offset/size half: + * + * - R1 (cgenexpr cgdot, local *struct READ): `p.f` where p is an + * inferred-let *struct resolved the field OFFSET via + * structlookupchain(inner) → the FOREIGN same-leaf struct's layout + * (wrong offset + wrong load-op). Now tichase(dotlhs.type_)->.sub + * ->fields (the #21 R0 tfield template), via the shared read + * choke-point cgptrfieldloadtf. + * - C1 (cgenstmt cgletbody, let-COPY size): `let p2 = p1` sized the + * struct memcpy run via structlookup(tn.str) → the foreign 24B + * struct, OVER-reading a 16B source + OVER-writing p2's slot (a + * SILENT OOB whose *value* coincidentally round-trips — the asm + * diverges, cs!=ww). Now structabisizetn(rhs.type_) (the COPY + * source's stamped tinfo). + * - A1/A2 (cgenexpr cgun, addr-of `&p.f` / `&o.f`): the LEAQ field + * offset came from structlookupchain → wrong offset. Now + * tichase(opnd.lhs.type_) (->.sub for the *struct A1 arm). + * + * cstage is correct at all of them — it keys on type_chase_named(base-> + * type)->fields / lu->size (the checker-STAMPED type), never a name + * re-lookup. #31 aligns wwstage UP (ww-only change; cstage cgen.c is the + * untouched oracle). + * + * GATE-BLIND in the bootstrap (same class as 793/784): the trigger needs + * a SECOND module contributing a same-leaf struct of a DIFFERENT size so + * the name-keyed lookup mis-resolves. The reddening surface is INFERRED + * LOCALS (a global struct decl is always explicitly qualified, so its + * qualifier resolves correctly — non-reddenable; the global value/ptr + * arms R2/R3/A3/W3/W4a are converted-for-construction and ride the + * corpus byte-id, not pinned here). + * + * row | m1 (local) | m2 (foreign) | arm | exit + * ----------------+-------------------+--------------+-----+----- + * ptrread_t16 | pair{u64,u16} 16B | pair 24B | R1 | 107 + * 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 + * + * 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. + * + * 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` + * narrow deref-STORE which wwstage currently emits as MOVQ where cstage + * emits MOVW — a SEPARATE pre-existing wwstage narrow-deref-store-width + * divergence (NOT #31; surfaced + filed by impl-31). An all-u64 store is + * MOVQ in both stages, so the row isolates the #31 offset fix. + */ +#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; +} + +static int +slurp_eq(const char *a, const char *b) +{ + FILE *fa = fopen(a, "rb"); + FILE *fb = fopen(b, "rb"); + if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return -1; } + int rc = 0; + for (;;) { + int ca = fgetc(fa); + int cb = fgetc(fb); + if (ca != cb) { rc = -1; break; } + if (ca == EOF) break; + } + fclose(fa); fclose(fb); + return rc; +} + +struct file { const char *name; const char *src; }; + +struct scenario { + const char *label; + const struct file *files; /* name==NULL terminates */ + int want_exit; +}; + +/* m2 contributes 24B same-leaf `pair` and `pq` — the name-keyed lookup's + * any-module fallback mis-resolves m1's 16B versions to these. */ +static const char m2_src[] = + "package m2;\n" + "export type pair = struct { hi: u64, mid: u64, lo: u64 };\n" + "export type pq = struct { a: u64, b: u64, v: u64 };\n" + "export fn use2(p: pair) i32 = { return (p.hi + p.mid + p.lo): i32; };\n"; + +/* m1: 16B `pair` (sub-8 tail) + 16B all-u64 `pq` (offset-only). */ +static const char m1_src[] = + "package m1;\n" + "export type pair = struct { hi: u64, lo: u16 };\n" + "export type pq = struct { a: u64, v: u64 };\n" + "let g: pair = pair { hi = 100: u64, lo = 7: u16 };\n" + "let gq: pq = pq { a = 1: u64, v = 7: u64 };\n" + "export fn mk() pair = { return pair { hi = 100: u64, lo = 7: u16 }; };\n" + "export fn mkp() *pair = { return &g; };\n" + "export fn mkq() pq = { return pq { a = 1: u64, v = 7: u64 }; };\n" + "export fn mkpq() *pq = { return &gq; };\n"; + +/* ---- ptrread_t16 (R1): inferred-let *struct field read ------------- */ +static const struct file ptrread_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" + " return (p.hi + (p.lo: u64)): i32;\n" + "};\n" }, + { NULL, NULL } +}; + +/* ---- letcopy_t16 (C1): inferred-let local-to-local struct copy ----- */ +static const struct file letcopy_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 p1 = m1.mk();\n" + " let p2 = p1;\n" + " return (p2.hi + (p2.lo: u64)): i32;\n" + "};\n" }, + { NULL, NULL } +}; + +/* ---- addrptr_pq (A1): &p.f over an inferred-let *struct ------------ */ +static const struct file addrptr_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.pq;\n" + " dummy.a = 0: u64;\n" + " let p = m1.mkpq();\n" + " let q = &p.v;\n" + " *q = 9: u64;\n" + " return (p.a + p.v): i32;\n" + "};\n" }, + { NULL, NULL } +}; + +/* ---- addrval_pq (A2): &o.f over an inferred-let value struct ------- */ +static const struct file addrval_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.pq;\n" + " dummy.a = 0: u64;\n" + " let s = m1.mkq();\n" + " let q = &s.v;\n" + " *q = 9: u64;\n" + " return (s.a + s.v): 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 }, +}; + +static int +run_scenario(const char *cdrv, const char *wdrv, const struct scenario *sc) +{ + char dir[] = "/tmp/ww794_XXXXXX"; + if (mkdtemp(dir) == NULL) { + fprintf(stderr, "794[%s]: mkdtemp failed\n", sc->label); + return -1; + } + + char path[1024], cmd[4096]; + int rc = 0; + + for (int i = 0; sc->files[i].name; i++) { + snprintf(path, sizeof path, "%s/%s", dir, sc->files[i].name); + FILE *f = fopen(path, "wb"); + if (!f) { fprintf(stderr, "794[%s]: write %s\n", sc->label, + sc->files[i].name); rc = -1; goto done; } + fputs(sc->files[i].src, f); + fclose(f); + } + + snprintf(cmd, sizeof cmd, + "cd %s && WW_PKGCACHE=%s/pkgc_c %s build --sep -I %s -o %s/main %s/main.ww", + dir, dir, cdrv, dir, dir, dir); + if (runwait(cmd) != 0) { + fprintf(stderr, "794[%s]: cstage build failed\n", sc->label); + rc = -1; goto done; + } + snprintf(path, sizeof path, "%s/main", dir); + int gotc = runwait(path); + if (gotc != sc->want_exit) { + fprintf(stderr, "794[%s]: cstage exit %d, want %d\n", + sc->label, gotc, sc->want_exit); + rc = -1; + } + + snprintf(cmd, sizeof cmd, + "cd %s && WW_PKGCACHE=%s/pkgc_w %s build --sep -I %s -o %s/mainww %s/main.ww", + dir, dir, wdrv, dir, dir, dir); + if (runwait(cmd) != 0) { + fprintf(stderr, "794[%s]: ww_ww build failed\n", sc->label); + rc = -1; goto done; + } + snprintf(path, sizeof path, "%s/mainww", dir); + int gotw = runwait(path); + if (gotw != sc->want_exit) { + fprintf(stderr, "794[%s]: wwstage exit %d, want %d\n", + sc->label, gotw, sc->want_exit); + rc = -1; + } + + char cs_s[1024], ws_s[1024]; + snprintf(cs_s, sizeof cs_s, "%s/all_cs.s", dir); + snprintf(ws_s, sizeof ws_s, "%s/all_ww.s", dir); + snprintf(cmd, sizeof cmd, "cat %s/main.sepwork/*.s > %s 2>/dev/null", + dir, cs_s); if (system(cmd)) {} + snprintf(cmd, sizeof cmd, "cat %s/mainww.sepwork/*.s > %s 2>/dev/null", + dir, ws_s); if (system(cmd)) {} + if (slurp_eq(cs_s, ws_s) != 0) { + fprintf(stderr, "794[%s]: cs.s/ww.s DIFFER (rule-10 byte-id " + "violation — #31 regression)\n", sc->label); + rc = -1; + } + +done: + snprintf(cmd, sizeof cmd, "rm -rf %s", dir); + (void)runwait(cmd); + return rc; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[2048]; + 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[2100], wdrv[2100]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + if (access(wdrv, X_OK) != 0) { + fprintf(stderr, "794: ww_ww missing — cannot run the cs==ww " + "byte-id gate (the whole point of this test)\n"); + return 1; + } + + int n = (int)(sizeof scenarios / sizeof scenarios[0]); + int fail = 0; + for (int i = 0; i < n; i++) { + if (run_scenario(cdrv, wdrv, &scenarios[i]) != 0) + fail++; + } + + if (fail) { + fprintf(stderr, "794 xmod_struct_field_layout_collide: %d/%d " + "scenarios failed\n", fail, n); + return 1; + } + printf("xmod_struct_field_layout_collide: %d/%d ok (cstage+wwstage run + " + "cs==ww byte-id)\n", n, n); + return 0; +}