From a1d9f36d112a17fcce17a2c225b42da80d0daafd Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 19 May 2026 15:09:57 +0900 Subject: [PATCH] selfhost+cstage+test: graduate alias-chain unwrap to transitive (#22) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Single-peel TY_NAMED.under bottoms out at the inner alias when chain length is 2+, surfaces in two stages with different mechanisms: cstage's gates inline `if (t->kind == TY_NAMED) t = t->under` at every callsite (cgreturn, cglet sizing, cgexpr N_DOT, cgassign N_DOT, cg_sret_retsize) — graduated to a while-loop via new type_chase_named helper across 11 sites. wwstage routes all field-walks through structlookup, which registers only direct struct definitions (not aliases) — missing the alias-recurse fallback. New structlookupchain helper mirrors slotsize's N_TARRAY arm precedent; sretretsize + 4 cgenexpr.ww sites route through it. Splitting would either land cstage without unblocking wwstage's strings.tokenize wrapper shape (rule 10 byte-id regression) or land wwstage without cstage gate parity (breaking 995 self-rebuild). 756 sentinel exercises 4 rows × cstage RC + wwstage RC + byte-id = 12 fixtures; pre-fix rows 2 + 4 (slice-fields single alias, i32 double alias) fail on both RC and byte-id. The ~67 cstage / ~26 wwstage candidate sibling sites are #17-style structural-close follow-up; this commit fixes the immediate strings.tokenize-wrapper blockers. --- Makefile | 7 + cmd/w6c/cgen.c | 77 ++++++-- selfhost/cmd/w6c/main.combined.ww | 66 ++++++- selfhost/cmd/wcc/cgenexpr.ww | 23 ++- selfhost/cmd/wcc/cgenutil.ww | 43 ++++- selfhost/cmd/wwdump/main.combined.ww | 66 ++++++- test/wcc/756_alias_chain_unwrap.c | 271 +++++++++++++++++++++++++++ 7 files changed, 513 insertions(+), 40 deletions(-) create mode 100644 test/wcc/756_alias_chain_unwrap.c diff --git a/Makefile b/Makefile index 20723507..2ffedc61 100644 --- a/Makefile +++ b/Makefile @@ -290,6 +290,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_convwrap_audit \ $(BIN)/test_slice_of_slice_index \ $(BIN)/test_amp_dot_idx \ + $(BIN)/test_alias_chain_unwrap \ $(BIN)/test_param_shadow_mod \ $(BIN)/test_localoff_scope \ $(BIN)/test_cast_enum_movl \ @@ -755,6 +756,12 @@ $(BIN)/test_amp_dot_idx: test/wcc/755_amp_dot_idx.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_alias_chain_unwrap: test/wcc/756_alias_chain_unwrap.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 $@ $< + $(BIN)/test_match_4arm_cross_module_run: test/wcc/929_match_4arm_cross_module_run.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index d833e962..b0dd259d 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -123,6 +123,19 @@ cg_isfloat(Type *t) || t->kind == TY_UNTYPED_FLOAT; } +/* type_chase_named — walk the TY_NAMED.under chain to the deepest non- + * named type. Chain-of-aliases (#22): `type b = a; type a = struct;` + * stacks two TY_NAMED layers — a single peel leaves `t` pointing at + * the inner alias (still TY_NAMED), so kind-gated arms (TY_STRUCT, + * TY_SLICE, TY_TAGGED, TY_PTR) miss and the codegen silently falls + * through to a scalar shape. Mirror of wwstage's structlookupchain. */ +static Type * +type_chase_named(Type *t) +{ + while (t && t->kind == TY_NAMED) t = t->under; + return t; +} + /* cg_sret_retsize — if `rt` is a plain TY_STRUCT > 24B, return its * natural size (the sret threshold); else 0. Tagged unions, tuples, * str, and slices route through their existing register-return ABIs @@ -130,8 +143,7 @@ cg_isfloat(Type *t) static int cg_sret_retsize(Type *rt) { - if (rt == NULL) return 0; - if (rt->kind == TY_NAMED) rt = rt->under; + rt = type_chase_named(rt); if (rt == NULL || rt->kind != TY_STRUCT) return 0; if ((int)rt->size <= 24) return 0; return (int)rt->size; @@ -2408,12 +2420,16 @@ cgexpr(Cg *c, Node *n, Local *locals) Node *base = n->lhs->lhs; if (base->kind == N_UN) base = base->lhs; Type *bt = base->type; - Type *u = (bt && bt->kind == TY_NAMED) ? bt->under : bt; + /* type_chase_named (#22): a chain `type b = a; a = struct` + * left u at TY_NAMED a after a single peel, missing the + * TY_STRUCT field-walk gate below — the assignment + * silently dropped (the `break` at the bottom of the + * N_DOT-lhs arm). */ + Type *u = type_chase_named(bt); int via_ptr = 0; if (u && u->kind == TY_PTR) { via_ptr = 1; - u = u->sub; - if (u && u->kind == TY_NAMED) u = u->under; + u = type_chase_named(u->sub); } /* slice/str pseudo-field write (.ptr/.len/.cap) */ if (u && (u->kind == TY_SLICE || u->kind == TY_STR)) { @@ -3013,10 +3029,11 @@ cgexpr(Cg *c, Node *n, Local *locals) if (n->lhs && n->lhs->kind == N_DOT && n->lhs->lhs && n->lhs->lhs->kind != N_IDENT) { Type *bt = n->lhs->lhs->type; - Type *bu = (bt && bt->kind == TY_NAMED) ? bt->under : bt; + /* type_chase_named (#22); same rationale as the cgexpr- + * side pointer-to-struct field branch. */ + Type *bu = type_chase_named(bt); if (bu && bu->kind == TY_PTR && bu->sub) { - Type *inner = bu->sub; - if (inner->kind == TY_NAMED) inner = inner->under; + Type *inner = type_chase_named(bu->sub); if (inner && inner->kind == TY_STRUCT) { Tfield *f = NULL; for (Tfield *fl = inner->fields; fl; fl = fl->next) @@ -5325,7 +5342,12 @@ cgexpr(Cg *c, Node *n, Local *locals) && dot_lhs->lhs && dot_lhs->lhs->kind == N_IDENT) dot_lhs = dot_lhs->lhs; Type *bt = dot_lhs ? dot_lhs->type : NULL; - Type *u = (bt && bt->kind == TY_NAMED) ? bt->under : bt; + /* type_chase_named (#22): `type b = a; type a = struct;` stacks + * two TY_NAMED layers; single peel left `u` still TY_NAMED, + * missing the TY_STRUCT field-walk gate below and collapsing + * `s.field` to a base-only MOVQ read (offset 0 instead of + * the field's declared offset). */ + Type *u = type_chase_named(bt); /* Module-qualified value reference: `mod.name`. The checker * leaves SK_USE idents untyped (NULL/ty_err); detect that and * look up the leaf in the flat (driver-concatenated) sym/def @@ -5797,8 +5819,7 @@ cgexpr(Cg *c, Node *n, Local *locals) * IDENT)) emits the same load as `p.len` after the case-top * retarget. */ if (u && u->kind == TY_PTR && u->sub) { - Type *inner = u->sub; - if (inner->kind == TY_NAMED) inner = inner->under; + Type *inner = type_chase_named(u->sub); if (inner && (inner->kind == TY_SLICE || inner->kind == TY_STR) && (lenfld || capfld || ptrfld) && dot_lhs && dot_lhs->kind == N_IDENT) { @@ -5813,10 +5834,15 @@ cgexpr(Cg *c, Node *n, Local *locals) * fn move(p: *point) ... { p.x += dx; ... } * dot_lhs gates this branch so both `p.f` (n->lhs is IDENT) * and `(*p).f` (n->lhs is N_UN(STAR, IDENT), retargeted to - * inner IDENT at case-top) emit the same load sequence. */ + * inner IDENT at case-top) emit the same load sequence. + * + * type_chase_named (#22): `type b = a;` inside the pointer + * (`*b`) leaves a single peel still at TY_NAMED. Bites the + * strings.tokenize wrapper shape — caller signature + * `next_token(s: *strings.tokenizer)` where strings.tokenizer + * aliases bytes.tokenizer. */ if (u && u->kind == TY_PTR && u->sub) { - Type *inner = u->sub; - if (inner->kind == TY_NAMED) inner = inner->under; + Type *inner = type_chase_named(u->sub); if (inner && inner->kind == TY_STRUCT && dot_lhs && dot_lhs->kind == N_IDENT) { int off = localfind(locals, dot_lhs->str); @@ -6290,7 +6316,12 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) break; case N_LET: { Type *lt = n->type; - Type *lu = (lt && lt->kind == TY_NAMED) ? lt->under : lt; + /* type_chase_named (#22): a chain `type a = struct{...}; + * type b = a;` stacks two TY_NAMED layers. A single peel + * left `lu` pointing at the inner alias (still TY_NAMED), + * collapsed the struct/slice/tagged sizing arms to the 8B + * fallback, and the slot under-allocated the local. */ + Type *lu = type_chase_named(lt); int sz = 8; if (lu && lu->kind == TY_ARRAY) sz = (int)lu->size; else if (lu && lu->kind == TY_SLICE) sz = 24; @@ -6829,8 +6860,14 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) * SysV sret discipline of "return the pointer". No * AX/DX/CX shuffle, no scratch slot beyond @sretarg. */ if (n->lhs && cg_ret_type && cg_sret_arg_off != 0) { - Type *rt = cg_ret_type; - if (rt->kind == TY_NAMED) rt = rt->under; + /* type_chase_named (#22). A single peel left `rt` still + * TY_NAMED when the declared return type is `type b + * = a;` where a is itself a NAMED alias of a struct, + * so the TY_STRUCT gate below missed and the sret + * return arm fell through to the scalar-AX default — + * corrupting the caller's receive slot even though + * the prologue wired @sretarg. */ + Type *rt = type_chase_named(cg_ret_type); /* sret return-forwarding (task #9 follow-up to #23): * `return f();` where outer + inner both return the * same >24B struct shape. Outer's @sretarg already @@ -6939,8 +6976,10 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) * N_STRUCTLIT (field-by-field store at scratch+foff). Call- * result chain return is deferred to #5's receive side. */ if (n->lhs && cg_ret_type) { - Type *rt = cg_ret_type; - if (rt->kind == TY_NAMED) rt = rt->under; + /* type_chase_named (#22); see the >24B sret arm above + * for the same rationale. The ≤24B register-return + * ABI uses the same TY_STRUCT gate. */ + Type *rt = type_chase_named(cg_ret_type); if (rt && rt->kind == TY_STRUCT && rt->size <= 24 && (n->lhs->kind == N_IDENT || n->lhs->kind == N_STRUCTLIT)) { diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 670cfb91..46cecbc8 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -9150,6 +9150,39 @@ fn structnaturalsize(si: *structinfo) i32 = { // cg_sret_retsize predicate. Resolves N_TNAME → struct via structlookup // and unwraps one leading N_TBANG so `type box = !big;` still // triggers sret on the underlying big. +// +// Chain-of-aliases (#22): `type a = struct{...}; type b = a;` registers +// `b → a` in c.aliases (target node = N_TNAME "a"), not `b → struct`. +// When structlookup(c, "b") misses, fall through to aliaslookup and +// recurse on the alias target — mirrors slotsize's N_TNAME arm +// (cgenutil.ww:1955) and the cstage while-loop in cg_sret_retsize. + +// structlookupchain — resolve TNAME `tn` to its registered struct, +// chasing alias-of-alias (#22). Returns nil if the chain doesn't +// bottom out at a struct. Mirrors cstage's transitive +// `while (t->kind == TY_NAMED) t = t->under` peel; consumed by +// cgdot / cgassign at every "field-walk on a struct-typed local" +// site so a transitively-aliased struct name resolves to its +// fieldinfo list regardless of chain depth. +export fn structlookupchain(c: *cgen, tn: *node) *structinfo = { + if (tn == nil) { return nil; }; + if (tn.kind != nkind.N_TNAME) { return nil; }; + let si: *structinfo = structlookup(c, tn.str); + if (si != nil) { return si; }; + let cur: *node = tn; + for (cur != nil && cur.kind == nkind.N_TNAME && si == nil) { + let aliased: *node = aliaslookup(c, cur.str); + if (aliased == nil) { cur = nil; } + else { + if (aliased.kind == nkind.N_TNAME) { + si = structlookup(c, aliased.str); + cur = aliased; + } else { cur = nil; }; + }; + }; + return si; +}; + export fn sretretsize(c: *cgen, t: *node) i32 = { if (t == nil) { return 0; }; let r: *node = t; @@ -9162,7 +9195,15 @@ export fn sretretsize(c: *cgen, t: *node) i32 = { if (primsize(r.str) > 0) { return 0; }; if (streq(r.str, "str")) { return 0; }; let si: *structinfo = structlookup(c, r.str); - if (si == nil) { return 0; }; + if (si == nil) { + if (c != nil) { + let aliased: *node = aliaslookup(c, r.str); + if (aliased != nil) { + return sretretsize(c, aliased); + }; + }; + return 0; + }; let n: i32 = structnaturalsize(si); if (n <= 24) { return 0; }; return n; @@ -12807,7 +12848,11 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; if (sname.len > 0) { - let si: *structinfo = structlookup(c, sname); + // 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) { @@ -12890,8 +12935,11 @@ fn cgdot(c: *cgen, n: *node) void = { }; // Direct struct local: field load at off+foff. if (lkind == nkind.N_TNAME) { - let sname: str = tn.str; - let si: *structinfo = structlookup(c, sname); + // structlookupchain walks the alias chain on + // miss so a transitively-aliased struct (`type + // b = a; a = struct`) still resolves to the + // underlying fieldinfo (#22). + let si: *structinfo = structlookupchain(c, tn); if (si != nil) { let fi: *fieldinfo = si.fields; for (fi != nil) { @@ -15556,7 +15604,10 @@ fn cgassign(c: *cgen, n: *node) void = { if (inner.kind == nkind.N_TNAME) { sname = inner.str; }; }; if (sname.len > 0) { - let si: *structinfo = structlookup(c, sname); + // 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) { @@ -15794,8 +15845,9 @@ fn cgassign(c: *cgen, n: *node) void = { }; // Direct struct local: store at off+foff. if (lkind == nkind.N_TNAME) { - let sname: str = tn.str; - let si: *structinfo = structlookup(c, sname); + // 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) { diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 50ccda59..e311fc3d 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -1280,7 +1280,11 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; if (sname.len > 0) { - let si: *structinfo = structlookup(c, sname); + // 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) { @@ -1363,8 +1367,11 @@ fn cgdot(c: *cgen, n: *node) void = { }; // Direct struct local: field load at off+foff. if (lkind == nkind.N_TNAME) { - let sname: str = tn.str; - let si: *structinfo = structlookup(c, sname); + // structlookupchain walks the alias chain on + // miss so a transitively-aliased struct (`type + // b = a; a = struct`) still resolves to the + // underlying fieldinfo (#22). + let si: *structinfo = structlookupchain(c, tn); if (si != nil) { let fi: *fieldinfo = si.fields; for (fi != nil) { @@ -4029,7 +4036,10 @@ fn cgassign(c: *cgen, n: *node) void = { if (inner.kind == nkind.N_TNAME) { sname = inner.str; }; }; if (sname.len > 0) { - let si: *structinfo = structlookup(c, sname); + // 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) { @@ -4267,8 +4277,9 @@ fn cgassign(c: *cgen, n: *node) void = { }; // Direct struct local: store at off+foff. if (lkind == nkind.N_TNAME) { - let sname: str = tn.str; - let si: *structinfo = structlookup(c, sname); + // 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) { diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 4944e1a5..affe90e5 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -1520,6 +1520,39 @@ fn structnaturalsize(si: *structinfo) i32 = { // cg_sret_retsize predicate. Resolves N_TNAME → struct via structlookup // and unwraps one leading N_TBANG so `type box = !big;` still // triggers sret on the underlying big. +// +// Chain-of-aliases (#22): `type a = struct{...}; type b = a;` registers +// `b → a` in c.aliases (target node = N_TNAME "a"), not `b → struct`. +// When structlookup(c, "b") misses, fall through to aliaslookup and +// recurse on the alias target — mirrors slotsize's N_TNAME arm +// (cgenutil.ww:1955) and the cstage while-loop in cg_sret_retsize. + +// structlookupchain — resolve TNAME `tn` to its registered struct, +// chasing alias-of-alias (#22). Returns nil if the chain doesn't +// bottom out at a struct. Mirrors cstage's transitive +// `while (t->kind == TY_NAMED) t = t->under` peel; consumed by +// cgdot / cgassign at every "field-walk on a struct-typed local" +// site so a transitively-aliased struct name resolves to its +// fieldinfo list regardless of chain depth. +export fn structlookupchain(c: *cgen, tn: *node) *structinfo = { + if (tn == nil) { return nil; }; + if (tn.kind != nkind.N_TNAME) { return nil; }; + let si: *structinfo = structlookup(c, tn.str); + if (si != nil) { return si; }; + let cur: *node = tn; + for (cur != nil && cur.kind == nkind.N_TNAME && si == nil) { + let aliased: *node = aliaslookup(c, cur.str); + if (aliased == nil) { cur = nil; } + else { + if (aliased.kind == nkind.N_TNAME) { + si = structlookup(c, aliased.str); + cur = aliased; + } else { cur = nil; }; + }; + }; + return si; +}; + export fn sretretsize(c: *cgen, t: *node) i32 = { if (t == nil) { return 0; }; let r: *node = t; @@ -1532,7 +1565,15 @@ export fn sretretsize(c: *cgen, t: *node) i32 = { if (primsize(r.str) > 0) { return 0; }; if (streq(r.str, "str")) { return 0; }; let si: *structinfo = structlookup(c, r.str); - if (si == nil) { return 0; }; + if (si == nil) { + if (c != nil) { + let aliased: *node = aliaslookup(c, r.str); + if (aliased != nil) { + return sretretsize(c, aliased); + }; + }; + return 0; + }; let n: i32 = structnaturalsize(si); if (n <= 24) { return 0; }; return n; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index d4acb44a..99122ad1 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -9150,6 +9150,39 @@ fn structnaturalsize(si: *structinfo) i32 = { // cg_sret_retsize predicate. Resolves N_TNAME → struct via structlookup // and unwraps one leading N_TBANG so `type box = !big;` still // triggers sret on the underlying big. +// +// Chain-of-aliases (#22): `type a = struct{...}; type b = a;` registers +// `b → a` in c.aliases (target node = N_TNAME "a"), not `b → struct`. +// When structlookup(c, "b") misses, fall through to aliaslookup and +// recurse on the alias target — mirrors slotsize's N_TNAME arm +// (cgenutil.ww:1955) and the cstage while-loop in cg_sret_retsize. + +// structlookupchain — resolve TNAME `tn` to its registered struct, +// chasing alias-of-alias (#22). Returns nil if the chain doesn't +// bottom out at a struct. Mirrors cstage's transitive +// `while (t->kind == TY_NAMED) t = t->under` peel; consumed by +// cgdot / cgassign at every "field-walk on a struct-typed local" +// site so a transitively-aliased struct name resolves to its +// fieldinfo list regardless of chain depth. +export fn structlookupchain(c: *cgen, tn: *node) *structinfo = { + if (tn == nil) { return nil; }; + if (tn.kind != nkind.N_TNAME) { return nil; }; + let si: *structinfo = structlookup(c, tn.str); + if (si != nil) { return si; }; + let cur: *node = tn; + for (cur != nil && cur.kind == nkind.N_TNAME && si == nil) { + let aliased: *node = aliaslookup(c, cur.str); + if (aliased == nil) { cur = nil; } + else { + if (aliased.kind == nkind.N_TNAME) { + si = structlookup(c, aliased.str); + cur = aliased; + } else { cur = nil; }; + }; + }; + return si; +}; + export fn sretretsize(c: *cgen, t: *node) i32 = { if (t == nil) { return 0; }; let r: *node = t; @@ -9162,7 +9195,15 @@ export fn sretretsize(c: *cgen, t: *node) i32 = { if (primsize(r.str) > 0) { return 0; }; if (streq(r.str, "str")) { return 0; }; let si: *structinfo = structlookup(c, r.str); - if (si == nil) { return 0; }; + if (si == nil) { + if (c != nil) { + let aliased: *node = aliaslookup(c, r.str); + if (aliased != nil) { + return sretretsize(c, aliased); + }; + }; + return 0; + }; let n: i32 = structnaturalsize(si); if (n <= 24) { return 0; }; return n; @@ -12807,7 +12848,11 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; if (sname.len > 0) { - let si: *structinfo = structlookup(c, sname); + // 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) { @@ -12890,8 +12935,11 @@ fn cgdot(c: *cgen, n: *node) void = { }; // Direct struct local: field load at off+foff. if (lkind == nkind.N_TNAME) { - let sname: str = tn.str; - let si: *structinfo = structlookup(c, sname); + // structlookupchain walks the alias chain on + // miss so a transitively-aliased struct (`type + // b = a; a = struct`) still resolves to the + // underlying fieldinfo (#22). + let si: *structinfo = structlookupchain(c, tn); if (si != nil) { let fi: *fieldinfo = si.fields; for (fi != nil) { @@ -15556,7 +15604,10 @@ fn cgassign(c: *cgen, n: *node) void = { if (inner.kind == nkind.N_TNAME) { sname = inner.str; }; }; if (sname.len > 0) { - let si: *structinfo = structlookup(c, sname); + // 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) { @@ -15794,8 +15845,9 @@ fn cgassign(c: *cgen, n: *node) void = { }; // Direct struct local: store at off+foff. if (lkind == nkind.N_TNAME) { - let sname: str = tn.str; - let si: *structinfo = structlookup(c, sname); + // 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) { diff --git a/test/wcc/756_alias_chain_unwrap.c b/test/wcc/756_alias_chain_unwrap.c new file mode 100644 index 00000000..e04ecfe5 --- /dev/null +++ b/test/wcc/756_alias_chain_unwrap.c @@ -0,0 +1,271 @@ +/* + * 756_alias_chain_unwrap — sentinel for #22 alias-chain SRET/N_DOT + * wedge. Pre-fix both stages single-peeled `if (t->kind == TY_NAMED) + * t = t->under` in cg_sret_retsize / N_LET sizing / cgreturn / N_DOT + * field-access (cstage) and structlookup-on-N_TNAME (wwstage); a + * `type b = a;` over an alias-of-struct stacked two TY_NAMED layers + * and the single peel bottomed out at the inner alias — still + * TY_NAMED, not TY_STRUCT. Outcome: SRET-shaped returns silently + * routed through the scalar-AX ABI (caller's receive slot + * corrupted), let-init slots under-sized, and `s.field` reads + * collapsed to base+0 instead of base+field_offset. + * + * Polarity (rule 10, mutual-symmetric): cstage's hot-path peels + * graduated to a transitive `while (t->kind == TY_NAMED) t = t->under` + * walk; wwstage's structlookup-misses fall through to aliaslookup + + * recurse, mirroring slotsize's existing N_TNAME arm (cgenutil.ww + * line 1955). The strings.tokenize wrapper shape (`type tokenizer = + * bytes::tokenizer;`) is the original surfacing site (#22). + * + * row | shape | gate + * ---------------------+----------------------------------------+-------- + * r1_i32_struct | direct TY_STRUCT, no alias | works pre-fix + * r2_slice_single | `type a = struct{[]u8,[]u8,i64}` | wedge pre-fix (RC=12) + * | aliased once → `type b = a;` | + * r3_slice_direct | direct struct, slice fields | works pre-fix + * r4_i32_double | `type b = a; type a = struct{i32...}` | wedge pre-fix (RC=14) + * | aliased twice → `type c = b;` | + * + * Each row asserts: + * - cstage RC == 0 + * - wwstage RC == 0 (when w6c_ww present) + * - cstage and wwstage emit byte-identical asm + * + * Pre-fix: rows 2 and 4 fail the RC check on both stages and the + * byte-id is also broken on rows 2/4 (one stage emits sret discipline, + * the other emits the scalar-AX fall-through). Post-fix all 12 + * fixtures pass. + */ +#include +#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; +} + +struct row { const char *label; const char *src; }; + +static const struct row rows[] = { + /* r1: direct i32-fields struct, no alias. Baseline — single + * TY_NAMED layer (the type itself) suffices for the existing + * single-peel pattern. */ + { "r1_i32_struct", + "package main;\n" + "type r1 = struct {\n" + "\ta: i32, b: i32, c: i32, d: i32, e: i32, f: i32, g: i32,\n" + "\th: i32, i: i32, j: i32, k: i32, l: i32, m: i32, p: i32,\n" + "};\n" + "fn make_r1() r1 = {\n" + "\tlet b: r1;\n" + "\tb.p = 99i32;\n" + "\treturn b;\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet s: r1 = make_r1();\n" + "\tif (s.p != 99i32) { return 11; };\n" + "\treturn 0;\n" + "};\n", + }, + /* r2: slice-fields struct aliased once. Pre-fix wedge: + * `type r2_alias = r2_struct;` stacks two TY_NAMED layers, the + * single peel lands on the inner alias and the SRET gate falls + * through to scalar-AX. RC=12 pre-fix on both stages. */ + { "r2_slice_single", + "package main;\n" + "type r2_struct = struct { in: []u8, delim: []u8, p: i64 };\n" + "type r2_alias = r2_struct;\n" + "fn make_r2() r2_alias = {\n" + "\tlet b: r2_struct;\n" + "\tb.p = 99i64;\n" + "\treturn b;\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet s: r2_alias = make_r2();\n" + "\tif (s.p != 99i64) { return 12; };\n" + "\treturn 0;\n" + "};\n", + }, + /* r3: slice-fields struct, no alias — baseline that proves the + * slice-payload sret discipline itself is unaffected. */ + { "r3_slice_direct", + "package main;\n" + "type r3 = struct { in: []u8, delim: []u8, p: i64 };\n" + "fn make_r3() r3 = {\n" + "\tlet b: r3;\n" + "\tb.p = 99i64;\n" + "\treturn b;\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet s: r3 = make_r3();\n" + "\tif (s.p != 99i64) { return 13; };\n" + "\treturn 0;\n" + "};\n", + }, + /* r4: i32-fields struct double-aliased. Same wedge as r2 but + * chain length 3 — extra TY_NAMED layer between the alias and + * the struct. Confirms the discriminator is the chain length, + * not the field shape. RC=14 pre-fix on both stages. */ + { "r4_i32_double", + "package main;\n" + "type r4_struct = struct {\n" + "\ta: i32, b: i32, c: i32, d: i32, e: i32, f: i32, g: i32,\n" + "\th: i32, i: i32, j: i32, k: i32, l: i32, m: i32, p: i32,\n" + "};\n" + "type r4_base = r4_struct;\n" + "type r4_alias = r4_base;\n" + "fn make_r4() r4_alias = {\n" + "\tlet b: r4_alias;\n" + "\tb.p = 99i32;\n" + "\treturn b;\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet s: r4_alias = make_r4();\n" + "\tif (s.p != 99i32) { return 14; };\n" + "\treturn 0;\n" + "};\n", + }, +}; + +static int +build_and_run(const char *driver, const struct row *r, int i) +{ + char src[64], tmpdir[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/acu_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/acu_%d_d_%d", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s build %s", + tmpdir, driver, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: build via %s failed\n", + r->label, driver); + unlink(src); rmdir(tmpdir); + return -1; + } + + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + char outbin[128]; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + int got = runwait(outbin); + + unlink(src); unlink(outbin); rmdir(tmpdir); + return got; +} + +static int +emit_s(const char *w6c, const struct row *r, int i, char *out_s, size_t cap) +{ + char src[96], cmd[1024]; + snprintf(src, sizeof src, "/tmp/acu_s_%d_%d.ww", getpid(), i); + snprintf(out_s, cap, "/tmp/acu_s_%d_%d_%s.s", + getpid(), i, w6c[strlen(w6c) - 1] == 'w' ? "ww" : "c"); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, out_s, src); + int rc = runwait(cmd); + unlink(src); + return rc; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[512]; + if (bin[0] != '/') { + char cwd[256]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char cdrv[640], wdrv[640]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + char cw6c[640], ww6c[640]; + snprintf(cw6c, sizeof cw6c, "%s/w6c", bin); + snprintf(ww6c, sizeof ww6c, "%s/w6c_ww", bin); + + int have_ww = (access(wdrv, X_OK) == 0) + && (access(ww6c, X_OK) == 0); + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + + for (int i = 0; i < n; i++) { + /* cstage RC */ + total++; + int got_c = build_and_run(cdrv, &rows[i], i); + if (got_c != 0) { + fprintf(stderr, + "alias_chain[cstage][%s]: exit=%d want=0\n", + rows[i].label, got_c); + fail++; + } + + if (!have_ww) continue; + + /* wwstage RC */ + total++; + int got_w = build_and_run(wdrv, &rows[i], i); + if (got_w != 0) { + fprintf(stderr, + "alias_chain[wwstage][%s]: exit=%d want=0\n", + rows[i].label, got_w); + fail++; + } + + /* byte-id cstage vs wwstage */ + total++; + char cs_path[160], ws_path[160]; + if (emit_s(cw6c, &rows[i], i, cs_path, sizeof cs_path) != 0 + || emit_s(ww6c, &rows[i], i, ws_path, sizeof ws_path) != 0) { + fprintf(stderr, + "alias_chain[byte-id][%s]: emit failed\n", + rows[i].label); + fail++; + unlink(cs_path); unlink(ws_path); + continue; + } + char cmpcmd[512]; + snprintf(cmpcmd, sizeof cmpcmd, "cmp -s %s %s", + cs_path, ws_path); + if (runwait(cmpcmd) != 0) { + fprintf(stderr, + "alias_chain[byte-id][%s]: cstage vs wwstage asm differs\n", + rows[i].label); + fail++; + } + unlink(cs_path); unlink(ws_path); + } + + if (fail) { + fprintf(stderr, + "alias_chain: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("alias_chain: %d/%d ok\n", total, total); + return 0; +}