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).
This commit is contained in:
2026-06-29 14:01:47 +09:00
parent 5ae6e3419e
commit eb28dcd5b7
4 changed files with 578 additions and 198 deletions

View File

@@ -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

View File

@@ -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;

View File

@@ -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;
};
};
};

View File

@@ -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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
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;
}