w6c: fold dot chains through inferred-alloc struct pointers (#24 residue)

wwstage dotchainresolve gated its *T roots on an N_TPTR-over-N_TNAME
tnode; an inferred `let p = alloc(S{...})!` rides the checker-
SYNTHESIZED N_TPTR whose pointee is the struct BODY node (the #24
TNAME-normalize covers only the direct struct-lit binding), so the
root gate failed and chained p.field.pseudo reads fell to the
unfused deref arms — shape-only divergence vs cstage's type-keyed
fold (both stages runtime-correct; the historical field(SB) leak the
pin described was already fixed). Widen the local and global root
gates to accept the N_TSTRUCT pointee; correctness stays enforced by
the stamped-tinfo peel below (TY_PTR -> pointee TY_STRUCT).

Graduates alias_g73_heapfill out of DATABYTEID_DIVERGED (2 pins
remain, both the tagged-spill family); adds compile fixtures pinning
the newly-converged siblings (nested read+store, slice .cap,
inferred-global root; pin 1730/22/3460) and a runtime lang row that
mutates then re-points the heap base — a read bypassing the pointer
returns the wrong len.
This commit is contained in:
2026-08-08 17:14:59 +09:00
parent 7631f4327c
commit d78137a051
8 changed files with 79 additions and 14 deletions

View File

@@ -730,12 +730,7 @@ DATABYTEID_EXPECTED_MIN = 915
# routes the cross-module tagged call-result through a 16B scratch pair
# where cstage PUSHQes AX/DX (frame 16 vs 32); runtime-equivalent
# (both stages run exit 42).
# alias_g73_heapfill: held divergence — the ww-side heap struct-lit
# str-arm deref-field READ leaks a field(SB) load (task #24); both
# stages frontend-compile rc 0, only the .s differs. Graduates out
# when #24 lands.
DATABYTEID_DIVERGED = r700_strings_byteindex r839_xmod_nominal_match \
alias_g73_heapfill
DATABYTEID_DIVERGED = r700_strings_byteindex r839_xmod_nominal_match
$(if $(DATABYTEID_FILES),,$(error test-byteid: empty data corpus))
test-data-byteid: $(BIN)/ww $(BIN)/w6c $(BIN)/w6c_ww
@work=$$(mktemp -d "$(CURDIR)/$(DATABYTEID_DIR).XXXXXX") || exit 1; \

View File

@@ -1,13 +1,13 @@
package wwfixture;
def protocolversion: i32 = 1;
def corpuscount: i32 = 1727;
def corpuscount: i32 = 1730;
def errorcount: i32 = 343;
def compilecount: i32 = 19;
def compilecount: i32 = 22;
def runcount: i32 = 209;
def runexitcount: i32 = 1156;
def nativecount: i32 = 3454;
def corpushash: str = "40ad5fd79515a743f2adb53e4a26396c6f13859df956fa079299a4b459594177";
def nativecount: i32 = 3460;
def corpushash: str = "5c20c19b826359fcceb45c8db5cf405f39183a68783582eaaf5f38f0e88f3771";
type directive = enum i32 {
ERROR = 0,

View File

@@ -5403,11 +5403,20 @@ export fn dotchainresolve(c: *cgen, n: *syntax.node,
// `*T` root (param/local): dereference at emit time;
// pointee struct supplies the field layout. Callers
// that opt in via *outptrroot emit a MOVQ load of the
// slot before indexing.
// slot before indexing. N_TSTRUCT pointee: an inferred
// `let p = alloc(S{...})!` rides the checker-synthesized
// N_TPTR whose pointee is the struct BODY node (the #24
// TNAME-normalize covers only the direct struct-lit
// binding); correctness is enforced by the stamped-tinfo
// peel below (TY_PTR → pointee TY_STRUCT), mirroring
// cstage's type-keyed root gate (cgen.c cg N_DOT
// chained-spine ptr_root) — this kind check is only a
// shape filter.
if (lc.tnode.kind == syntax.nkind.N_TPTR) {
let pe: *syntax.node = lc.tnode.lhs;
if (pe != nil) {
if (pe.kind == syntax.nkind.N_TNAME) {
if (pe.kind == syntax.nkind.N_TNAME
|| pe.kind == syntax.nkind.N_TSTRUCT) {
*outrootoff = lc.off;
*outptrroot = true;
resolved = true;
@@ -5429,7 +5438,8 @@ export fn dotchainresolve(c: *cgen, n: *syntax.node,
if (gtn.kind == syntax.nkind.N_TPTR) {
let pe: *syntax.node = gtn.lhs;
if (pe != nil) {
if (pe.kind == syntax.nkind.N_TNAME) {
if (pe.kind == syntax.nkind.N_TNAME
|| pe.kind == syntax.nkind.N_TSTRUCT) {
*outisglobal = true;
*outptrroot = true;
resolved = true;

View File

@@ -0,0 +1,22 @@
// heapptr_dotread_test — chained field reads through an INFERRED alloc
// pointer must go THROUGH the pointer: mutate the heap str after the
// fill, then re-point the base at a second object — a read that
// bypasses the pointer (stale cached addr or literal-global load)
// returns the wrong len.
package heapptr_dotread_test;
type s1t = str;
type s2t = s1t;
type box = struct { s: s2t, n: int };
@test fn mutate_then_repoint() void = {
let p = alloc(box { s = "hello", n = 5 })!;
assert(p.s.len == 5);
p.s = "worldwide";
assert(p.s.len == 9);
let q = alloc(box { s = "xy", n = 7 })!;
p = q;
assert(p.s.len == 2);
assert(p.n == 7);
};

View File

@@ -1,5 +1,5 @@
//ww:compile
// migrated from test/wcc/944_alias_cgen_b5_run.c: heap struct-lit str-arm fill through a 2-level str alias — pins the absence of the pre-c2 cstage #73 fatal; ww .s carries the #24 field(SB) deref-read leak (DATABYTEID_DIVERGED).
// migrated from test/wcc/944_alias_cgen_b5_run.c: heap struct-lit str-arm fill through a 2-level str alias — pins the absence of the pre-c2 cstage #73 fatal AND the chained deref-read fold through an inferred alloc pointer (the historical #24 field(SB) leak's residue; byte-id since the dotchainresolve N_TSTRUCT-pointee gate).
package main;
type s1t = str;
type s2t = s1t;

View File

@@ -0,0 +1,10 @@
//ww:compile
// INFERRED-global *struct root: the letvartnode gate must accept the
// checker-synthesized N_TPTR-over-N_TSTRUCT exactly as the local arm.
package main;
type box = struct { s: str, n: int };
let gp = alloc(box { s = "hi", n = 1 })!;
export fn main() i32 = {
if (gp.s.len != 2) { return 1; };
return 0;
};

View File

@@ -0,0 +1,16 @@
//ww:compile
// nested-struct chained read + store through an INFERRED alloc pointer:
// the checker-synthesized N_TPTR carries the struct BODY node, and the
// dotchainresolve root gate must still fold (byte-id with the annotated
// twin and with cstage's type-keyed fold).
package main;
type inner = struct { a: int, b: int };
type outer = struct { i: inner, n: int };
export fn main() i32 = {
let p = alloc(outer { i = inner { a = 1, b = 2 }, n = 3 })!;
if (p.i.b != 2) { return 1; };
p.i.b = 9;
if (p.i.b != 9) { return 2; };
if (p.n != 3) { return 3; };
return 0;
};

View File

@@ -0,0 +1,12 @@
//ww:compile
// slice-typed field pseudo-read (.cap) through an INFERRED alloc
// pointer — the slice sibling of the str-arm fold.
package main;
type box = struct { sl: []int, n: int };
export fn main() i32 = {
let s: []int = alloc([], 4)!;
let p = alloc(box { sl = s, n = 5 })!;
if (p.sl.cap != 4) { return 1; };
if (p.n != 5) { return 2; };
return 0;
};