wcc: TYPE-based forward-tagged predicate in wwstage cgreturn (#201)

cgreturn's forwardtagged detection was keyed on the CALLEE NAME
(N_IDENT/N_DOT only via fnretlookupmod), so any other callee shape
fell through to the variant-tag synthesis path — clobbering the
just-returned AX/DX/CX/R8 tagged-ABI words. The deref-call case
`(*r)(...)` (impl-e1-resume STOP, 994 w6c_ww byte-id red) was the
proximate trigger.

Replace with a TYPE-BASED predicate over the checker-stamped tinfos
(rhs.type_ vs c.fnret.type_), mirroring cstage cgen.c:8007 passthrough.
Peel TY_NAMED on both sides then identity-check the underlying
TY_TAGGED — sufficient for the NAMED case because tinfocache memoizes
per typedecl (#191 lineage). Variant-pointer fallback walks the
params chain when identity fails so anonymous unions like the
cross-module (i32 | void) shared between strings.byteindex and
bytes.index still forward correctly; full recursive tinfo
structural-eq is gated by #178 (typeeqast's TY_TAGGED arm
conservatively returns false today).

Probe 770_return_tagged_forward covers 6 rows — IDENT forward, widen
non-matching, deref-call (the bug), scalar (sanity), nested call,
cross-module forward — each gated on cstage runtime + wwstage runtime
+ cs.s == ww.s byte-identity.
This commit is contained in:
2026-05-29 01:00:02 +09:00
parent b2ac8cbf81
commit fb53b4798b
5 changed files with 477 additions and 72 deletions

View File

@@ -25018,32 +25018,42 @@ fn cgreturn(c: *cgen, n: *node) void = {
if (istaggedtype(c, c.fnret)) {
// Forwarding a fallible call: `return f();` where f
// also returns a tagged union. The result is already
// in (AX=tag, DX=v0, CX=v1) — no shuffle, no tag.
// Mirrors the rhsreturnstagged path in cglet and the
// !type_istagged guard in C cgen's N_RETURN.
// in (AX=tag, DX=v0, CX=v1, R8=v2) — no shuffle, no
// tag synthesis. Mirrors cstage cgen.c:8007 passthrough
// = istagged && (vu == rt || type_eq(vt, cg_ret_type)).
// TYPE-BASED predicate (was name-keyed via fnretlookupmod
// IDENT/DOT-only) covers all callee shapes — including
// deref-call N_UN(TK_STAR) per #201. Identity-on-peeled
// handles the NAMED case (tinfocache memoizes per typedecl,
// #191 lineage); the variant-pointer fallback handles the
// anonymous case (each anonymous `(A|B)` decl gets its own
// NAMED-less tinfo, so identity fails — e.g. cross-module
// strings.byteindex returns the same anonymous (i32|void)
// as bytes.index). Variant-pointer equality on the params
// chain suffices because variants are primitives (single
// tctx tinfo) or NAMED (per-decl identity); a full recursive
// tinfo structural-eq helper is gated by #178.
let forwardtagged: bool = false;
if (rhs.kind == nkind.N_CALL) {
let callee: *node = rhs.lhs;
if (callee != nil) {
let calleename: str;
calleename.ptr = nil; calleename.len = 0;
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.kind == nkind.N_IDENT) {
calleename = callee.str;
cmod = c.curmod;
};
if (callee.kind == nkind.N_DOT) {
calleename = callee.str;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
if (rhs.kind == nkind.N_CALL && rhs.type_ != nil && c.fnret != nil && c.fnret.type_ != nil) {
let ru: *tinfo = rhs.type_: *tinfo;
for (ru != nil && ru.kind == tykind.TY_NAMED) { ru = ru.under; };
let fu: *tinfo = c.fnret.type_: *tinfo;
for (fu != nil && fu.kind == tykind.TY_NAMED) { fu = fu.under; };
if (ru != nil && fu != nil && ru.kind == tykind.TY_TAGGED && fu.kind == tykind.TY_TAGGED) {
if (ru == fu) {
forwardtagged = true;
} else if (ru.nullable == fu.nullable) {
let pa: *tparam = ru.params;
let pb: *tparam = fu.params;
let same: bool = true;
for (pa != nil && pb != nil) {
if (pa.type_ != pb.type_) { same = false; };
pa = pa.tnext;
pb = pb.tnext;
};
if (same && pa == nil && pb == nil) {
forwardtagged = true;
};
};
if (calleename.len > 0) {
let rtyp: *node = fnretlookupmod(c, calleename, cmod);
if (istaggedtype(c, rtyp)) { forwardtagged = true; };
};
};
};

View File

@@ -357,32 +357,42 @@ fn cgreturn(c: *cgen, n: *node) void = {
if (istaggedtype(c, c.fnret)) {
// Forwarding a fallible call: `return f();` where f
// also returns a tagged union. The result is already
// in (AX=tag, DX=v0, CX=v1) — no shuffle, no tag.
// Mirrors the rhsreturnstagged path in cglet and the
// !type_istagged guard in C cgen's N_RETURN.
// in (AX=tag, DX=v0, CX=v1, R8=v2) — no shuffle, no
// tag synthesis. Mirrors cstage cgen.c:8007 passthrough
// = istagged && (vu == rt || type_eq(vt, cg_ret_type)).
// TYPE-BASED predicate (was name-keyed via fnretlookupmod
// IDENT/DOT-only) covers all callee shapes — including
// deref-call N_UN(TK_STAR) per #201. Identity-on-peeled
// handles the NAMED case (tinfocache memoizes per typedecl,
// #191 lineage); the variant-pointer fallback handles the
// anonymous case (each anonymous `(A|B)` decl gets its own
// NAMED-less tinfo, so identity fails — e.g. cross-module
// strings.byteindex returns the same anonymous (i32|void)
// as bytes.index). Variant-pointer equality on the params
// chain suffices because variants are primitives (single
// tctx tinfo) or NAMED (per-decl identity); a full recursive
// tinfo structural-eq helper is gated by #178.
let forwardtagged: bool = false;
if (rhs.kind == nkind.N_CALL) {
let callee: *node = rhs.lhs;
if (callee != nil) {
let calleename: str;
calleename.ptr = nil; calleename.len = 0;
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.kind == nkind.N_IDENT) {
calleename = callee.str;
cmod = c.curmod;
};
if (callee.kind == nkind.N_DOT) {
calleename = callee.str;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
if (rhs.kind == nkind.N_CALL && rhs.type_ != nil && c.fnret != nil && c.fnret.type_ != nil) {
let ru: *tinfo = rhs.type_: *tinfo;
for (ru != nil && ru.kind == tykind.TY_NAMED) { ru = ru.under; };
let fu: *tinfo = c.fnret.type_: *tinfo;
for (fu != nil && fu.kind == tykind.TY_NAMED) { fu = fu.under; };
if (ru != nil && fu != nil && ru.kind == tykind.TY_TAGGED && fu.kind == tykind.TY_TAGGED) {
if (ru == fu) {
forwardtagged = true;
} else if (ru.nullable == fu.nullable) {
let pa: *tparam = ru.params;
let pb: *tparam = fu.params;
let same: bool = true;
for (pa != nil && pb != nil) {
if (pa.type_ != pb.type_) { same = false; };
pa = pa.tnext;
pb = pb.tnext;
};
if (same && pa == nil && pb == nil) {
forwardtagged = true;
};
};
if (calleename.len > 0) {
let rtyp: *node = fnretlookupmod(c, calleename, cmod);
if (istaggedtype(c, rtyp)) { forwardtagged = true; };
};
};
};

View File

@@ -25018,32 +25018,42 @@ fn cgreturn(c: *cgen, n: *node) void = {
if (istaggedtype(c, c.fnret)) {
// Forwarding a fallible call: `return f();` where f
// also returns a tagged union. The result is already
// in (AX=tag, DX=v0, CX=v1) — no shuffle, no tag.
// Mirrors the rhsreturnstagged path in cglet and the
// !type_istagged guard in C cgen's N_RETURN.
// in (AX=tag, DX=v0, CX=v1, R8=v2) — no shuffle, no
// tag synthesis. Mirrors cstage cgen.c:8007 passthrough
// = istagged && (vu == rt || type_eq(vt, cg_ret_type)).
// TYPE-BASED predicate (was name-keyed via fnretlookupmod
// IDENT/DOT-only) covers all callee shapes — including
// deref-call N_UN(TK_STAR) per #201. Identity-on-peeled
// handles the NAMED case (tinfocache memoizes per typedecl,
// #191 lineage); the variant-pointer fallback handles the
// anonymous case (each anonymous `(A|B)` decl gets its own
// NAMED-less tinfo, so identity fails — e.g. cross-module
// strings.byteindex returns the same anonymous (i32|void)
// as bytes.index). Variant-pointer equality on the params
// chain suffices because variants are primitives (single
// tctx tinfo) or NAMED (per-decl identity); a full recursive
// tinfo structural-eq helper is gated by #178.
let forwardtagged: bool = false;
if (rhs.kind == nkind.N_CALL) {
let callee: *node = rhs.lhs;
if (callee != nil) {
let calleename: str;
calleename.ptr = nil; calleename.len = 0;
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.kind == nkind.N_IDENT) {
calleename = callee.str;
cmod = c.curmod;
};
if (callee.kind == nkind.N_DOT) {
calleename = callee.str;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
if (rhs.kind == nkind.N_CALL && rhs.type_ != nil && c.fnret != nil && c.fnret.type_ != nil) {
let ru: *tinfo = rhs.type_: *tinfo;
for (ru != nil && ru.kind == tykind.TY_NAMED) { ru = ru.under; };
let fu: *tinfo = c.fnret.type_: *tinfo;
for (fu != nil && fu.kind == tykind.TY_NAMED) { fu = fu.under; };
if (ru != nil && fu != nil && ru.kind == tykind.TY_TAGGED && fu.kind == tykind.TY_TAGGED) {
if (ru == fu) {
forwardtagged = true;
} else if (ru.nullable == fu.nullable) {
let pa: *tparam = ru.params;
let pb: *tparam = fu.params;
let same: bool = true;
for (pa != nil && pb != nil) {
if (pa.type_ != pb.type_) { same = false; };
pa = pa.tnext;
pb = pb.tnext;
};
if (same && pa == nil && pb == nil) {
forwardtagged = true;
};
};
if (calleename.len > 0) {
let rtyp: *node = fnretlookupmod(c, calleename, cmod);
if (istaggedtype(c, rtyp)) { forwardtagged = true; };
};
};
};