From 009c4b35e286b4b98307ee27d8623342307c1359 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 21 May 2026 21:27:56 +0900 Subject: [PATCH] selfhost/cmd/wcc: stamp e.type_ for N_MATCH + matchyieldtype port (A.6.2.0g) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port cstage's match_yield_type walker (cmd/wcc/check.c:110-135) as `matchyieldtype` (Plan-9-cased) — recursive arm-body walker that finds the first reachable yield's operand type, descends N_BLOCK / N_IF / N_FOR / N_FORRANGE, and stops at nested N_MATCH (each match opens its own yield scope). Sole structural divergence from cstage: where cstage reads `body->lhs->type`, wwstage calls `exprtype(c, body.lhs, nil)` — wwstage's AST is untyped at parse time and the type lives in the tinfocache; exprtype is the canonical reader (tinfocache-idempotent per L467). Port cstage's N_MATCH match-as-expression stamp (check.c:1316- 1330) into a new exprtype arm. Walks the first non-nil arm yield via matchyieldtype, defaults to void if no arm yields. Closes the consumer half of the match-as-expression contract that A.6.2.0f opened on the producer side (N_YIELD). One documented divergence from cstage (lenient, intentional): the arm-yield-unification check (cstage L1322-1327) is skipped. That's a checker-correctness concern; this arm only stamps. Cstage's `match_yield_type` IS a helper there too — porting it is structural fidelity per rule 10, not a new helper invention under rule 9. β scope per Drew (2026-05-21): α (stamp void unconditionally) would bury a latent miscompile that A.6.2.1 assertion can't catch (it sees nil, not wrong). Hare's `match_expr` AST has no type field (ref/hare/hare/ast/expr.ha:341-348); harec/cstage unify arms at check time — same architecture wwstage mirrors. A.6.2 step 7 of 8 (γ order). The assertion closer (#15) is next, which lands the invariant on a green tree. `make test-unit` green; full `make test` batched per option B. --- selfhost/cmd/w6c/main.combined.ww | 55 ++++++++++++++++++++++++++++ selfhost/cmd/wcc/check.ww | 55 ++++++++++++++++++++++++++++ selfhost/cmd/wwdump/main.combined.ww | 55 ++++++++++++++++++++++++++++ 3 files changed, 165 insertions(+) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index e80cb000..7207319a 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -7785,6 +7785,40 @@ fn astsize(c: *checker, t: *node) i64 = { return 0i64; }; +// 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. +fn matchyieldtype(c: *checker, body: *node) *node = { + if (body == nil) { return nil; }; + let k: nkind = body.kind; + if (k == nkind.N_YIELD) { + if (body.lhs == nil) { return nil; }; + return exprtype(c, body.lhs, nil); + }; + if (k == nkind.N_MATCH) { return nil; }; + if (k == nkind.N_BLOCK) { + let s: *node = body.list; + for (s != nil) { + let t: *node = matchyieldtype(c, s); + if (t != nil) { return t; }; + s = s.next; + }; + return nil; + }; + if (k == nkind.N_IF) { + let t: *node = matchyieldtype(c, body.body); + if (t != nil) { return t; }; + return matchyieldtype(c, body.els); + }; + if (k == nkind.N_FOR || k == nkind.N_FORRANGE) { + return matchyieldtype(c, body.body); + }; + return nil; +}; + // astoffset — byte offset of `dot.str` inside the struct type of // `dot.lhs`. Mirrors cstage cmd/wcc/check.c:932-961: peel one N_TPTR // (for `p.field` where p is *Struct), require N_TSTRUCT, walk fields @@ -8897,6 +8931,27 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { if (t != nil) { e.type_ = tinfofornode(c, t): *void; }; return t; }; + if (k == nkind.N_MATCH) { + // A.6.2.0g — port of cstage cmd/wcc/check.c:1316-1330 match-as- + // expression stamp. The match's type is the first arm's yield + // operand type; void if no arm yields. Wwstage skips cstage's + // arm-yield-unification check (L1322-1327) — that's a checker + // 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; + let cs: *node = e.list; + for (cs != nil) { + let t: *node = matchyieldtype(c, cs.body); + if (t != nil) { yt = t; break; }; + cs = cs.next; + }; + if (yt == nil) { + yt = mktname(c, "void"); + }; + e.type_ = tinfofornode(c, yt): *void; + return yt; + }; if (k == nkind.N_YIELD) { // A.6.2.0f — pass-through stamp; cstage check.c:1708 does NOT // stamp N_YIELD (statement-shaped). Wwstage's A.6.2 invariant diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 0b226e51..00221e72 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -821,6 +821,40 @@ fn astsize(c: *checker, t: *node) i64 = { return 0i64; }; +// 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. +fn matchyieldtype(c: *checker, body: *node) *node = { + if (body == nil) { return nil; }; + let k: nkind = body.kind; + if (k == nkind.N_YIELD) { + if (body.lhs == nil) { return nil; }; + return exprtype(c, body.lhs, nil); + }; + if (k == nkind.N_MATCH) { return nil; }; + if (k == nkind.N_BLOCK) { + let s: *node = body.list; + for (s != nil) { + let t: *node = matchyieldtype(c, s); + if (t != nil) { return t; }; + s = s.next; + }; + return nil; + }; + if (k == nkind.N_IF) { + let t: *node = matchyieldtype(c, body.body); + if (t != nil) { return t; }; + return matchyieldtype(c, body.els); + }; + if (k == nkind.N_FOR || k == nkind.N_FORRANGE) { + return matchyieldtype(c, body.body); + }; + return nil; +}; + // astoffset — byte offset of `dot.str` inside the struct type of // `dot.lhs`. Mirrors cstage cmd/wcc/check.c:932-961: peel one N_TPTR // (for `p.field` where p is *Struct), require N_TSTRUCT, walk fields @@ -1933,6 +1967,27 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { if (t != nil) { e.type_ = tinfofornode(c, t): *void; }; return t; }; + if (k == nkind.N_MATCH) { + // A.6.2.0g — port of cstage cmd/wcc/check.c:1316-1330 match-as- + // expression stamp. The match's type is the first arm's yield + // operand type; void if no arm yields. Wwstage skips cstage's + // arm-yield-unification check (L1322-1327) — that's a checker + // 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; + let cs: *node = e.list; + for (cs != nil) { + let t: *node = matchyieldtype(c, cs.body); + if (t != nil) { yt = t; break; }; + cs = cs.next; + }; + if (yt == nil) { + yt = mktname(c, "void"); + }; + e.type_ = tinfofornode(c, yt): *void; + return yt; + }; if (k == nkind.N_YIELD) { // A.6.2.0f — pass-through stamp; cstage check.c:1708 does NOT // stamp N_YIELD (statement-shaped). Wwstage's A.6.2 invariant diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index b12d889b..6eb2071d 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -7785,6 +7785,40 @@ fn astsize(c: *checker, t: *node) i64 = { return 0i64; }; +// 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. +fn matchyieldtype(c: *checker, body: *node) *node = { + if (body == nil) { return nil; }; + let k: nkind = body.kind; + if (k == nkind.N_YIELD) { + if (body.lhs == nil) { return nil; }; + return exprtype(c, body.lhs, nil); + }; + if (k == nkind.N_MATCH) { return nil; }; + if (k == nkind.N_BLOCK) { + let s: *node = body.list; + for (s != nil) { + let t: *node = matchyieldtype(c, s); + if (t != nil) { return t; }; + s = s.next; + }; + return nil; + }; + if (k == nkind.N_IF) { + let t: *node = matchyieldtype(c, body.body); + if (t != nil) { return t; }; + return matchyieldtype(c, body.els); + }; + if (k == nkind.N_FOR || k == nkind.N_FORRANGE) { + return matchyieldtype(c, body.body); + }; + return nil; +}; + // astoffset — byte offset of `dot.str` inside the struct type of // `dot.lhs`. Mirrors cstage cmd/wcc/check.c:932-961: peel one N_TPTR // (for `p.field` where p is *Struct), require N_TSTRUCT, walk fields @@ -8897,6 +8931,27 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { if (t != nil) { e.type_ = tinfofornode(c, t): *void; }; return t; }; + if (k == nkind.N_MATCH) { + // A.6.2.0g — port of cstage cmd/wcc/check.c:1316-1330 match-as- + // expression stamp. The match's type is the first arm's yield + // operand type; void if no arm yields. Wwstage skips cstage's + // arm-yield-unification check (L1322-1327) — that's a checker + // 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; + let cs: *node = e.list; + for (cs != nil) { + let t: *node = matchyieldtype(c, cs.body); + if (t != nil) { yt = t; break; }; + cs = cs.next; + }; + if (yt == nil) { + yt = mktname(c, "void"); + }; + e.type_ = tinfofornode(c, yt): *void; + return yt; + }; if (k == nkind.N_YIELD) { // A.6.2.0f — pass-through stamp; cstage check.c:1708 does NOT // stamp N_YIELD (statement-shaped). Wwstage's A.6.2 invariant