From d78137a051aaefc23386d0b01257e0ae45455b30 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sat, 8 Aug 2026 17:14:59 +0900 Subject: [PATCH] w6c: fold dot chains through inferred-alloc struct pointers (#24 residue) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Makefile | 7 +------ internal/wwfixture/types.ww | 8 ++++---- selfhost/cmd/wcc/cgenutil.ww | 16 ++++++++++++--- test/lang/heapptr_dotread_test.ww | 22 +++++++++++++++++++++ test/wcc/data/alias_g73_heapfill/case.ww | 2 +- test/wcc/data/alias_infptr_global/case.ww | 10 ++++++++++ test/wcc/data/alias_infptr_nest/case.ww | 16 +++++++++++++++ test/wcc/data/alias_infptr_slicecap/case.ww | 12 +++++++++++ 8 files changed, 79 insertions(+), 14 deletions(-) create mode 100644 test/lang/heapptr_dotread_test.ww create mode 100644 test/wcc/data/alias_infptr_global/case.ww create mode 100644 test/wcc/data/alias_infptr_nest/case.ww create mode 100644 test/wcc/data/alias_infptr_slicecap/case.ww diff --git a/Makefile b/Makefile index 8fbc78e2..90fd794f 100644 --- a/Makefile +++ b/Makefile @@ -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; \ diff --git a/internal/wwfixture/types.ww b/internal/wwfixture/types.ww index e5f76923..e80011e5 100644 --- a/internal/wwfixture/types.ww +++ b/internal/wwfixture/types.ww @@ -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, diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index c82e41cd..7a964dfc 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -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; diff --git a/test/lang/heapptr_dotread_test.ww b/test/lang/heapptr_dotread_test.ww new file mode 100644 index 00000000..9a8cb19a --- /dev/null +++ b/test/lang/heapptr_dotread_test.ww @@ -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); +}; diff --git a/test/wcc/data/alias_g73_heapfill/case.ww b/test/wcc/data/alias_g73_heapfill/case.ww index eff74b1c..ff96742c 100644 --- a/test/wcc/data/alias_g73_heapfill/case.ww +++ b/test/wcc/data/alias_g73_heapfill/case.ww @@ -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; diff --git a/test/wcc/data/alias_infptr_global/case.ww b/test/wcc/data/alias_infptr_global/case.ww new file mode 100644 index 00000000..58d936a1 --- /dev/null +++ b/test/wcc/data/alias_infptr_global/case.ww @@ -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; +}; diff --git a/test/wcc/data/alias_infptr_nest/case.ww b/test/wcc/data/alias_infptr_nest/case.ww new file mode 100644 index 00000000..cf1dd0de --- /dev/null +++ b/test/wcc/data/alias_infptr_nest/case.ww @@ -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; +}; diff --git a/test/wcc/data/alias_infptr_slicecap/case.ww b/test/wcc/data/alias_infptr_slicecap/case.ww new file mode 100644 index 00000000..892ab59d --- /dev/null +++ b/test/wcc/data/alias_infptr_slicecap/case.ww @@ -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; +};