selfhost/cmd/wcc: stamp e.type_ for N_MATCH + matchyieldtype port (A.6.2.0g)

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.
This commit is contained in:
2026-05-21 21:27:56 +09:00
parent f0a8a37077
commit 009c4b35e2
3 changed files with 165 additions and 0 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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