From bec1e7d6b00fdc8bbb9a107cfc07edadba81caf3 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 2 Jun 2026 16:22:22 +0900 Subject: [PATCH] w6c+wwstage: #264 read cached operand tinfo for match-expr yield type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wwstage checker rejected a match-bound binder used in a `yield` arm of a match-AS-EXPRESSION (`let v = match (x) { case let p: *T => yield *p; ... }`) with asserttyped:un/bin/index; cstage compiled it. resolvewalk stamps the yield operand's type_ during the in-scope N_MCASE arm walk. exprtype's N_MATCH arm then derived the match's type by re-running exprtype on the same operand to recover a type NODE — but the arm binder's scope is already popped, so the re-derive returned nil and the N_UN/N_BIN/N_INDEX restamp arms overwrote the good in-scope stamp with nil. cstage never re-runs: match_yield_type reads the operand's cached ->type (cmd/wcc/check.c:121). Root fix (align wwstage UP): matchyieldtype now returns a *tinfo and, at the post-walk call, READS the operand's cached node.type_ instead of re-running exprtype — so no operand shape can be clobbered by construction (deref/bin/index all vanish, no per-arm guards). The exprtype N_MATCH consumer stamps e.type_ from that tinfo directly (no tinfofornode round-trip). The pre-walk call (checkletassign L302 / checkretassign L303 run before the in-scope arm walk, so the operand is nil there) keeps the nil-safe re-derive — benign and load-bearing: it types the void-arm literal so let/return-assign has a usable node. The re-derived node (or btype for the bare-binder idiom) is carried back via an out-param for the assignability check and for the N_MLET/N_MASSIGN tuple-destructure consumers (`let (a,b) = match { case let t => yield t }`, test 945). cstage is single-pass so its else is dead; eliminating the pre-walk call is #279. Supersedes the narrow N_UN non-clobber guard (removed — its match consumer is gone). @test check_match_ptr_deref extended to pin the whole operand class (deref / bin / slice-index / deref-then-field), dual-stage (910 + 997) with correct runtime + cs==ww byte-id. The *[N]T ptr-to- array index variant is blocked separately by #278. Both compiler- imported combined.ww regenerated. smoke + test-unit (242) + 994 w6c_ww byte-id (18 corpus incl. selfhost combined.ww) green. --- selfhost/cmd/w6c/main.combined.ww | 101 ++++++++++++++++++++------- selfhost/cmd/wcc/check.ww | 101 ++++++++++++++++++++------- selfhost/cmd/wwdump/main.combined.ww | 101 ++++++++++++++++++++------- test/wcc/data/attest_pass.ww | 60 ++++++++++++++++ 4 files changed, 282 insertions(+), 81 deletions(-) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index bbfba37d..39176c46 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -11501,30 +11501,68 @@ fn astunsized(c: *checker, t: *node) bool = { // matchyieldtype — port of cstage cmd/wcc/check.c:110-135. Walks a // match arm body for the first `yield expr;` and returns its operand -// type. Returns nil if no yield is reachable from `body`. Doesn't -// descend into a nested N_MATCH — each match opens its own yield -// scope. exprtype is idempotent on already-stamped nodes (tinfocache -// path at L467) so re-entering it on the yield operand here is safe. -// bname/btype carry the enclosing arm's case-binding name + declared type -// node. #241: when exprtype can't re-derive a `yield ` operand — -// the arm binder's scope is already popped by the time exprtype(N_MATCH) -// runs post-order (resolvewalk's N_MCASE restores c.cur before this), so -// scopelookup of the bare binder ident returns nil — the yielded type IS -// the binding type (btype). cstage avoids this by reading the operand's -// already-stamped ->type (check.c:122) rather than re-running cexpr; wwstage -// caches only node.type_ (a tinfo, not a type NODE), so this binder-typed -// fallback is the node-form recovery for the dominant match-bind-then-yield -// idiom (Hare's parseint `case let t => yield t`). -fn matchyieldtype(c: *checker, body: *node, bname: str, btype: *node) *node = { +// type as a resolved *tinfo (the match-as-expression's type). Returns +// nil if no yield is reachable from `body`. Doesn't descend into a +// nested N_MATCH — each match opens its own yield scope. bname/btype +// carry the enclosing arm's case-binding name + declared type node. +// +// #264: returns a *tinfo (not a type *node) because the post-walk call +// READS the operand's cached node.type_ (a tinfo), mirroring cstage's +// match_yield_type which reads body->lhs->type (cmd/wcc/check.c:121-122). +// The two callsites differ on whether the operand is stamped yet: +// - POST-walk (exprtype N_MATCH post-order, resolvewalk L631): the +// in-scope N_MCASE arm walk (L411) has already stamped the operand, +// so we read body.lhs.type_ directly — NO exprtype re-run. Re-running +// exprtype out of the (popped) arm scope returned nil and the +// N_UN/N_BIN/N_INDEX restamp arms overwrote the good deref/element +// stamp with nil -> asserttyped:un/bin/index. Reading cached avoids +// the re-derive entirely, so the clobber cannot occur by construction. +// - PRE-walk (checkletassign L302 / checkretassign L303 run exprtype +// on the match rhs BEFORE the L533 in-scope descent): the operand is +// nil here, so we re-derive via exprtype — benign (nil->nil no-op on +// the unstamped operand) AND load-bearing: it types the void-arm +// literal (`yield -1`) so the let/return-assign has a usable type +// node for isassignable. cstage is single-pass (no pre-walk call), so +// its match_yield_type has no such branch; eliminating this pre-walk +// call is #279. `nodeout` carries that re-derived type NODE back to +// the consumer for the pre-walk assignability check (isassignable is +// node-based); it stays nil on the post-walk cached read, where the +// consumer's *node return is discarded (no nested match-as-subexpr +// consumes it — only checkletassign/checkretassign at the pre-walk +// call use it). The #241 `yield ` fallback (the dominant +// match-bind-then-yield idiom, Hare's parseint `case let t => yield t`) +// stays, now resolving btype to a tinfo. +fn matchyieldtype(c: *checker, body: *node, bname: str, btype: *node, + nodeout: **node) *tinfo = { if (body == nil) { return nil; }; let k: nkind = body.kind; if (k == nkind.N_YIELD) { if (body.lhs == nil) { return nil; }; + if (body.lhs.type_ != nil) { + // For the bare-binder idiom `yield `, ALSO surface + // btype as the *node: the tuple-destructure consumers + // (N_MLET/N_MASSIGN at resolvewalk L482/L503) read + // exprtype(N_MATCH)'s *node return as an N_TTUPLE to + // distribute onto `let (a,b) = match(x){ case let t => yield + // t }` (test 945 match_yield — the only tuple-destructure-of- + // match idiom in tree, grep-confirmed). btype is the binder's + // declared type node, which IS the match's type here; the + // cached tinfo returned below equals tinfofornode(btype). + if (body.lhs.kind == nkind.N_IDENT && bname.len > 0 + && streq(body.lhs.str, bname)) { + *nodeout = btype; + }; + return body.lhs.type_: *tinfo; + }; let t: *node = exprtype(c, body.lhs, nil); - if (t != nil) { return t; }; + if (t != nil) { + *nodeout = t; + return tinfofornode(c, t); + }; if (body.lhs.kind == nkind.N_IDENT && bname.len > 0 && streq(body.lhs.str, bname)) { - return btype; + *nodeout = btype; + return tinfofornode(c, btype); }; return nil; }; @@ -11532,19 +11570,19 @@ fn matchyieldtype(c: *checker, body: *node, bname: str, btype: *node) *node = { if (k == nkind.N_BLOCK) { let s: *node = body.list; for (s != nil) { - let t: *node = matchyieldtype(c, s, bname, btype); + let t: *tinfo = matchyieldtype(c, s, bname, btype, nodeout); if (t != nil) { return t; }; s = s.next; }; return nil; }; if (k == nkind.N_IF) { - let t: *node = matchyieldtype(c, body.body, bname, btype); + let t: *tinfo = matchyieldtype(c, body.body, bname, btype, nodeout); if (t != nil) { return t; }; - return matchyieldtype(c, body.els, bname, btype); + return matchyieldtype(c, body.els, bname, btype, nodeout); }; if (k == nkind.N_FOR || k == nkind.N_FORRANGE) { - return matchyieldtype(c, body.body, bname, btype); + return matchyieldtype(c, body.body, bname, btype, nodeout); }; return nil; }; @@ -13379,21 +13417,30 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { // concern, this arm only stamps. Closes the consumer half of // the match-as-expression contract that A.6.2.0f opened on the // producer side (N_YIELD). - let yt: *node = nil; + // #264: consume matchyieldtype's *tinfo DIRECTLY (no tinfofornode + // round-trip) — the post-walk call reads the operand's cached + // stamp, so the out-of-scope re-derive that clobbered *p/p[i]/*p+1 + // is never reached. `retn` captures the pre-walk re-derived type + // node (nil on the post-walk cached read) for the assignability + // node the consumers (checkletassign/checkretassign) read off this + // arm's *node return; see matchyieldtype's docstring + #279. + let yt: *tinfo = nil; + let retn: *node = nil; let cs: *node = e.list; for (cs != nil) { // cs.str/cs.lhs = the arm's case-binding name + declared // type (the N_MCASE binder); fed to matchyieldtype's #241 // scope-popped `yield ` fallback. - let t: *node = matchyieldtype(c, cs.body, cs.str, cs.lhs); - if (t != nil) { yt = t; break; }; + let armn: *node = nil; + let t: *tinfo = matchyieldtype(c, cs.body, cs.str, cs.lhs, &armn); + if (t != nil) { yt = t; retn = armn; break; }; cs = cs.next; }; if (yt == nil) { - yt = mktname(c, "void"); + yt = tinfofornode(c, mktname(c, "void")); }; - e.type_ = tinfofornode(c, yt): *void; - return yt; + e.type_ = yt: *void; + return retn; }; if (k == nkind.N_YIELD) { // A.6.2.0f — pass-through stamp; cstage check.c:1708 does NOT diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index bcb7f218..166da5e6 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -1096,30 +1096,68 @@ fn astunsized(c: *checker, t: *node) bool = { // matchyieldtype — port of cstage cmd/wcc/check.c:110-135. Walks a // match arm body for the first `yield expr;` and returns its operand -// type. Returns nil if no yield is reachable from `body`. Doesn't -// descend into a nested N_MATCH — each match opens its own yield -// scope. exprtype is idempotent on already-stamped nodes (tinfocache -// path at L467) so re-entering it on the yield operand here is safe. -// bname/btype carry the enclosing arm's case-binding name + declared type -// node. #241: when exprtype can't re-derive a `yield ` operand — -// the arm binder's scope is already popped by the time exprtype(N_MATCH) -// runs post-order (resolvewalk's N_MCASE restores c.cur before this), so -// scopelookup of the bare binder ident returns nil — the yielded type IS -// the binding type (btype). cstage avoids this by reading the operand's -// already-stamped ->type (check.c:122) rather than re-running cexpr; wwstage -// caches only node.type_ (a tinfo, not a type NODE), so this binder-typed -// fallback is the node-form recovery for the dominant match-bind-then-yield -// idiom (Hare's parseint `case let t => yield t`). -fn matchyieldtype(c: *checker, body: *node, bname: str, btype: *node) *node = { +// type as a resolved *tinfo (the match-as-expression's type). Returns +// nil if no yield is reachable from `body`. Doesn't descend into a +// nested N_MATCH — each match opens its own yield scope. bname/btype +// carry the enclosing arm's case-binding name + declared type node. +// +// #264: returns a *tinfo (not a type *node) because the post-walk call +// READS the operand's cached node.type_ (a tinfo), mirroring cstage's +// match_yield_type which reads body->lhs->type (cmd/wcc/check.c:121-122). +// The two callsites differ on whether the operand is stamped yet: +// - POST-walk (exprtype N_MATCH post-order, resolvewalk L631): the +// in-scope N_MCASE arm walk (L411) has already stamped the operand, +// so we read body.lhs.type_ directly — NO exprtype re-run. Re-running +// exprtype out of the (popped) arm scope returned nil and the +// N_UN/N_BIN/N_INDEX restamp arms overwrote the good deref/element +// stamp with nil -> asserttyped:un/bin/index. Reading cached avoids +// the re-derive entirely, so the clobber cannot occur by construction. +// - PRE-walk (checkletassign L302 / checkretassign L303 run exprtype +// on the match rhs BEFORE the L533 in-scope descent): the operand is +// nil here, so we re-derive via exprtype — benign (nil->nil no-op on +// the unstamped operand) AND load-bearing: it types the void-arm +// literal (`yield -1`) so the let/return-assign has a usable type +// node for isassignable. cstage is single-pass (no pre-walk call), so +// its match_yield_type has no such branch; eliminating this pre-walk +// call is #279. `nodeout` carries that re-derived type NODE back to +// the consumer for the pre-walk assignability check (isassignable is +// node-based); it stays nil on the post-walk cached read, where the +// consumer's *node return is discarded (no nested match-as-subexpr +// consumes it — only checkletassign/checkretassign at the pre-walk +// call use it). The #241 `yield ` fallback (the dominant +// match-bind-then-yield idiom, Hare's parseint `case let t => yield t`) +// stays, now resolving btype to a tinfo. +fn matchyieldtype(c: *checker, body: *node, bname: str, btype: *node, + nodeout: **node) *tinfo = { if (body == nil) { return nil; }; let k: nkind = body.kind; if (k == nkind.N_YIELD) { if (body.lhs == nil) { return nil; }; + if (body.lhs.type_ != nil) { + // For the bare-binder idiom `yield `, ALSO surface + // btype as the *node: the tuple-destructure consumers + // (N_MLET/N_MASSIGN at resolvewalk L482/L503) read + // exprtype(N_MATCH)'s *node return as an N_TTUPLE to + // distribute onto `let (a,b) = match(x){ case let t => yield + // t }` (test 945 match_yield — the only tuple-destructure-of- + // match idiom in tree, grep-confirmed). btype is the binder's + // declared type node, which IS the match's type here; the + // cached tinfo returned below equals tinfofornode(btype). + if (body.lhs.kind == nkind.N_IDENT && bname.len > 0 + && streq(body.lhs.str, bname)) { + *nodeout = btype; + }; + return body.lhs.type_: *tinfo; + }; let t: *node = exprtype(c, body.lhs, nil); - if (t != nil) { return t; }; + if (t != nil) { + *nodeout = t; + return tinfofornode(c, t); + }; if (body.lhs.kind == nkind.N_IDENT && bname.len > 0 && streq(body.lhs.str, bname)) { - return btype; + *nodeout = btype; + return tinfofornode(c, btype); }; return nil; }; @@ -1127,19 +1165,19 @@ fn matchyieldtype(c: *checker, body: *node, bname: str, btype: *node) *node = { if (k == nkind.N_BLOCK) { let s: *node = body.list; for (s != nil) { - let t: *node = matchyieldtype(c, s, bname, btype); + let t: *tinfo = matchyieldtype(c, s, bname, btype, nodeout); if (t != nil) { return t; }; s = s.next; }; return nil; }; if (k == nkind.N_IF) { - let t: *node = matchyieldtype(c, body.body, bname, btype); + let t: *tinfo = matchyieldtype(c, body.body, bname, btype, nodeout); if (t != nil) { return t; }; - return matchyieldtype(c, body.els, bname, btype); + return matchyieldtype(c, body.els, bname, btype, nodeout); }; if (k == nkind.N_FOR || k == nkind.N_FORRANGE) { - return matchyieldtype(c, body.body, bname, btype); + return matchyieldtype(c, body.body, bname, btype, nodeout); }; return nil; }; @@ -2974,21 +3012,30 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { // concern, this arm only stamps. Closes the consumer half of // the match-as-expression contract that A.6.2.0f opened on the // producer side (N_YIELD). - let yt: *node = nil; + // #264: consume matchyieldtype's *tinfo DIRECTLY (no tinfofornode + // round-trip) — the post-walk call reads the operand's cached + // stamp, so the out-of-scope re-derive that clobbered *p/p[i]/*p+1 + // is never reached. `retn` captures the pre-walk re-derived type + // node (nil on the post-walk cached read) for the assignability + // node the consumers (checkletassign/checkretassign) read off this + // arm's *node return; see matchyieldtype's docstring + #279. + let yt: *tinfo = nil; + let retn: *node = nil; let cs: *node = e.list; for (cs != nil) { // cs.str/cs.lhs = the arm's case-binding name + declared // type (the N_MCASE binder); fed to matchyieldtype's #241 // scope-popped `yield ` fallback. - let t: *node = matchyieldtype(c, cs.body, cs.str, cs.lhs); - if (t != nil) { yt = t; break; }; + let armn: *node = nil; + let t: *tinfo = matchyieldtype(c, cs.body, cs.str, cs.lhs, &armn); + if (t != nil) { yt = t; retn = armn; break; }; cs = cs.next; }; if (yt == nil) { - yt = mktname(c, "void"); + yt = tinfofornode(c, mktname(c, "void")); }; - e.type_ = tinfofornode(c, yt): *void; - return yt; + e.type_ = yt: *void; + return retn; }; if (k == nkind.N_YIELD) { // A.6.2.0f — pass-through stamp; cstage check.c:1708 does NOT diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 5ab1cba4..21345c63 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -11501,30 +11501,68 @@ fn astunsized(c: *checker, t: *node) bool = { // matchyieldtype — port of cstage cmd/wcc/check.c:110-135. Walks a // match arm body for the first `yield expr;` and returns its operand -// type. Returns nil if no yield is reachable from `body`. Doesn't -// descend into a nested N_MATCH — each match opens its own yield -// scope. exprtype is idempotent on already-stamped nodes (tinfocache -// path at L467) so re-entering it on the yield operand here is safe. -// bname/btype carry the enclosing arm's case-binding name + declared type -// node. #241: when exprtype can't re-derive a `yield ` operand — -// the arm binder's scope is already popped by the time exprtype(N_MATCH) -// runs post-order (resolvewalk's N_MCASE restores c.cur before this), so -// scopelookup of the bare binder ident returns nil — the yielded type IS -// the binding type (btype). cstage avoids this by reading the operand's -// already-stamped ->type (check.c:122) rather than re-running cexpr; wwstage -// caches only node.type_ (a tinfo, not a type NODE), so this binder-typed -// fallback is the node-form recovery for the dominant match-bind-then-yield -// idiom (Hare's parseint `case let t => yield t`). -fn matchyieldtype(c: *checker, body: *node, bname: str, btype: *node) *node = { +// type as a resolved *tinfo (the match-as-expression's type). Returns +// nil if no yield is reachable from `body`. Doesn't descend into a +// nested N_MATCH — each match opens its own yield scope. bname/btype +// carry the enclosing arm's case-binding name + declared type node. +// +// #264: returns a *tinfo (not a type *node) because the post-walk call +// READS the operand's cached node.type_ (a tinfo), mirroring cstage's +// match_yield_type which reads body->lhs->type (cmd/wcc/check.c:121-122). +// The two callsites differ on whether the operand is stamped yet: +// - POST-walk (exprtype N_MATCH post-order, resolvewalk L631): the +// in-scope N_MCASE arm walk (L411) has already stamped the operand, +// so we read body.lhs.type_ directly — NO exprtype re-run. Re-running +// exprtype out of the (popped) arm scope returned nil and the +// N_UN/N_BIN/N_INDEX restamp arms overwrote the good deref/element +// stamp with nil -> asserttyped:un/bin/index. Reading cached avoids +// the re-derive entirely, so the clobber cannot occur by construction. +// - PRE-walk (checkletassign L302 / checkretassign L303 run exprtype +// on the match rhs BEFORE the L533 in-scope descent): the operand is +// nil here, so we re-derive via exprtype — benign (nil->nil no-op on +// the unstamped operand) AND load-bearing: it types the void-arm +// literal (`yield -1`) so the let/return-assign has a usable type +// node for isassignable. cstage is single-pass (no pre-walk call), so +// its match_yield_type has no such branch; eliminating this pre-walk +// call is #279. `nodeout` carries that re-derived type NODE back to +// the consumer for the pre-walk assignability check (isassignable is +// node-based); it stays nil on the post-walk cached read, where the +// consumer's *node return is discarded (no nested match-as-subexpr +// consumes it — only checkletassign/checkretassign at the pre-walk +// call use it). The #241 `yield ` fallback (the dominant +// match-bind-then-yield idiom, Hare's parseint `case let t => yield t`) +// stays, now resolving btype to a tinfo. +fn matchyieldtype(c: *checker, body: *node, bname: str, btype: *node, + nodeout: **node) *tinfo = { if (body == nil) { return nil; }; let k: nkind = body.kind; if (k == nkind.N_YIELD) { if (body.lhs == nil) { return nil; }; + if (body.lhs.type_ != nil) { + // For the bare-binder idiom `yield `, ALSO surface + // btype as the *node: the tuple-destructure consumers + // (N_MLET/N_MASSIGN at resolvewalk L482/L503) read + // exprtype(N_MATCH)'s *node return as an N_TTUPLE to + // distribute onto `let (a,b) = match(x){ case let t => yield + // t }` (test 945 match_yield — the only tuple-destructure-of- + // match idiom in tree, grep-confirmed). btype is the binder's + // declared type node, which IS the match's type here; the + // cached tinfo returned below equals tinfofornode(btype). + if (body.lhs.kind == nkind.N_IDENT && bname.len > 0 + && streq(body.lhs.str, bname)) { + *nodeout = btype; + }; + return body.lhs.type_: *tinfo; + }; let t: *node = exprtype(c, body.lhs, nil); - if (t != nil) { return t; }; + if (t != nil) { + *nodeout = t; + return tinfofornode(c, t); + }; if (body.lhs.kind == nkind.N_IDENT && bname.len > 0 && streq(body.lhs.str, bname)) { - return btype; + *nodeout = btype; + return tinfofornode(c, btype); }; return nil; }; @@ -11532,19 +11570,19 @@ fn matchyieldtype(c: *checker, body: *node, bname: str, btype: *node) *node = { if (k == nkind.N_BLOCK) { let s: *node = body.list; for (s != nil) { - let t: *node = matchyieldtype(c, s, bname, btype); + let t: *tinfo = matchyieldtype(c, s, bname, btype, nodeout); if (t != nil) { return t; }; s = s.next; }; return nil; }; if (k == nkind.N_IF) { - let t: *node = matchyieldtype(c, body.body, bname, btype); + let t: *tinfo = matchyieldtype(c, body.body, bname, btype, nodeout); if (t != nil) { return t; }; - return matchyieldtype(c, body.els, bname, btype); + return matchyieldtype(c, body.els, bname, btype, nodeout); }; if (k == nkind.N_FOR || k == nkind.N_FORRANGE) { - return matchyieldtype(c, body.body, bname, btype); + return matchyieldtype(c, body.body, bname, btype, nodeout); }; return nil; }; @@ -13379,21 +13417,30 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { // concern, this arm only stamps. Closes the consumer half of // the match-as-expression contract that A.6.2.0f opened on the // producer side (N_YIELD). - let yt: *node = nil; + // #264: consume matchyieldtype's *tinfo DIRECTLY (no tinfofornode + // round-trip) — the post-walk call reads the operand's cached + // stamp, so the out-of-scope re-derive that clobbered *p/p[i]/*p+1 + // is never reached. `retn` captures the pre-walk re-derived type + // node (nil on the post-walk cached read) for the assignability + // node the consumers (checkletassign/checkretassign) read off this + // arm's *node return; see matchyieldtype's docstring + #279. + let yt: *tinfo = nil; + let retn: *node = nil; let cs: *node = e.list; for (cs != nil) { // cs.str/cs.lhs = the arm's case-binding name + declared // type (the N_MCASE binder); fed to matchyieldtype's #241 // scope-popped `yield ` fallback. - let t: *node = matchyieldtype(c, cs.body, cs.str, cs.lhs); - if (t != nil) { yt = t; break; }; + let armn: *node = nil; + let t: *tinfo = matchyieldtype(c, cs.body, cs.str, cs.lhs, &armn); + if (t != nil) { yt = t; retn = armn; break; }; cs = cs.next; }; if (yt == nil) { - yt = mktname(c, "void"); + yt = tinfofornode(c, mktname(c, "void")); }; - e.type_ = tinfofornode(c, yt): *void; - return yt; + e.type_ = yt: *void; + return retn; }; if (k == nkind.N_YIELD) { // A.6.2.0f — pass-through stamp; cstage check.c:1708 does NOT diff --git a/test/wcc/data/attest_pass.ww b/test/wcc/data/attest_pass.ww index 40119e2a..fc602be3 100644 --- a/test/wcc/data/attest_pass.ww +++ b/test/wcc/data/attest_pass.ww @@ -44,3 +44,63 @@ type point = struct { x: i32, y: i32 }; let _: i32 = 1 / 0; }; }; + +// #264: a match-bound binder used inside a `yield` arm of a match-AS- +// EXPRESSION. Pre-fix the wwstage checker stamped the operand in-scope +// during the N_MCASE arm walk, then RE-TYPED it out of the (popped) arm +// scope while deriving the match's yield type (exprtype N_MATCH → +// matchyieldtype). Out of scope the operand couldn't re-resolve, the +// re-derive returned nil, and the N_UN/N_BIN/N_INDEX restamp arms +// overwrote the good in-scope stamp with nil → asserttyped:un/bin/index +// aborted (cstage accepted it). Root fix: matchyieldtype now READS the +// cached operand tinfo at the post-walk call instead of re-deriving +// (mirrors cstage match_yield_type reading body->lhs->type, check.c:121), +// so no operand shape can be clobbered. These rows pin the whole operand +// class by construction: deref (*p), bin (*p+1), slice-index (p[i]), and +// deref-then-field ((*p).x). (The *[N]T ptr-to-array index variant is +// blocked separately by #278's exhaustiveness false-reject, so it uses a +// []i32 slice binder here.) +@test fn check_match_ptr_deref() void = { + let n: i32 = 42i32; + let xi: (*i32 | void) = &n; + let v: i32 = match (xi) { + case let p: *i32 => yield *p; + case void => yield -1i32; + }; + if (v != 42) { + let _: i32 = 1 / 0; + }; + + // N_BIN operand: `yield *p + 1`. + let m: i32 = 9i32; + let xb: (*i32 | void) = &m; + let vb: i32 = match (xb) { + case let p: *i32 => yield *p + 1i32; + case void => yield -1i32; + }; + if (vb != 10) { + let _: i32 = 1 / 0; + }; + + // N_INDEX operand: `yield p[1]` over a []i32 binder. + let arr: [3]i32 = [4i32, 5i32, 6i32]; + let sl: []i32 = arr; + let xs: ([]i32 | void) = sl; + let vs: i32 = match (xs) { + case let p: []i32 => yield p[1]; + case void => yield -1i32; + }; + if (vs != 5) { + let _: i32 = 1 / 0; + }; + + let pt: point = point { x = 7i32, y = 9i32 }; + let xp: (*point | void) = &pt; + let w: i32 = match (xp) { + case let p: *point => yield (*p).x; + case void => yield -1i32; + }; + if (w != 7) { + let _: i32 = 1 / 0; + }; +};