wcc/ww: match-as-expression unifies arm yields (coarse-family gate)

exprtype N_MATCH took the first arm's yield type without walking the
rest — int-vs-str arms silently produced garbage downstream. Walk all
arms: typeeq-equal accepts; a definite coarse-family mismatch
(num/str/bool via the new yieldclass classifier, unknown classes stay
lenient) rejects. Same-family non-assignable pairs remain lenient —
the documented precision residual is task #52 (needs tinfo-level
type_assignable). The wave's table-driven reject test lands here:
989_catA_f2_reject, 19 rows x 2 stages, each member pre-fix-red-proven.
This commit is contained in:
2026-06-12 11:31:53 +09:00
parent 780e680c1b
commit 87367ae332
5 changed files with 548 additions and 3 deletions

View File

@@ -11840,6 +11840,25 @@ fn matchyieldtype(c: *checker, body: *node, bname: str, btype: *node,
return nil;
};
// yieldclass — #38/F2 (review item 6): the coarse assignability family of a
// match-arm yield type, used to approximate cstage's type_assignable in the
// cross-arm unification (ww has tinfo typeeq but no tinfo type_assignable).
// 1=numeric (int/float/enum/rune + untyped_int/float/rune, NAMED-chased),
// 2=str (str + untyped_str), 3=bool, 0=unknown/other (ptr/struct/tuple/slice/
// tagged/...). Class 0 stays LENIENT so the unification rejects only a DEFINITE
// family mismatch (the catA repro: an int arm vs a str arm) and never an
// untyped->concrete promotion (untyped_int vs i32/f64 both land in class 1)
// that cstage accepts. The families mirror typeisnum/typeisstr (lib/ww/typ.ww).
fn yieldclass(t: *tinfo) i32 = {
let u: *tinfo = tichase(t);
if (u == nil) { return 0i32; };
if (typeisnum(u)) { return 1i32; };
if (typeisstr(u)) { return 2i32; };
if (u.kind == tykind.TY_BOOL) { return 3i32; };
if (u.kind == tykind.TY_UNTYPED_BOOL) { return 3i32; };
return 0i32;
};
// 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
@@ -14288,7 +14307,41 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = {
// scope-popped `yield <binder>` fallback.
let armn: *node = nil;
let t: *tinfo = matchyieldtype(c, cs.body, cs.str, cs.lhs, &armn);
if (t != nil) { yt = t; retn = armn; break; };
if (t != nil) {
if (yt == nil) {
// First yielding arm: it is the match's type;
// retn carries its *node for the consumers (#264).
yt = t; retn = armn;
} else {
// #38/F2 (review item 6): cross-arm yield
// unification. cstage check.c:2080 rejects an arm
// whose yield is neither type_eq nor type_assignable
// to the first arm's. ww has tinfo typeeq but no
// tinfo type_assignable, so reject only a DEFINITE
// coarse-family mismatch (yieldclass) — closing the
// catA silent accept (int arm vs str arm reads the
// str header through an int-stamped slot at runtime)
// while staying lenient on same-family / untyped
// promotions cstage admits. Walks ALL arms (no early
// break): the post-walk matchyieldtype reads each
// arm's cached operand stamp, a pure read (#264).
// RETAINED DIVERGENCE (match-yield precision-gap task,
// lead #52 - distinct from the enum-reinterpret #52
// note elsewhere): the coarse yieldclass ALSO under-
// rejects same-family-but-not-assignable arms cstage
// DOES reject (untyped_int vs i64 / untyped_int vs
// untyped_float) - a precision gap, not a silent
// miscompile of the catA repro; closeable only with a
// real tinfo type_assignable.
if (!typeeq(yt, t)) {
let ca: i32 = yieldclass(yt);
let cb: i32 = yieldclass(t);
if (ca != 0i32 && cb != 0i32 && ca != cb) {
deffolderr(c, cs, "match arm yields an incompatible type");
};
};
};
};
cs = cs.next;
};
if (yt == nil) {

View File

@@ -1420,6 +1420,25 @@ fn matchyieldtype(c: *checker, body: *node, bname: str, btype: *node,
return nil;
};
// yieldclass — #38/F2 (review item 6): the coarse assignability family of a
// match-arm yield type, used to approximate cstage's type_assignable in the
// cross-arm unification (ww has tinfo typeeq but no tinfo type_assignable).
// 1=numeric (int/float/enum/rune + untyped_int/float/rune, NAMED-chased),
// 2=str (str + untyped_str), 3=bool, 0=unknown/other (ptr/struct/tuple/slice/
// tagged/...). Class 0 stays LENIENT so the unification rejects only a DEFINITE
// family mismatch (the catA repro: an int arm vs a str arm) and never an
// untyped->concrete promotion (untyped_int vs i32/f64 both land in class 1)
// that cstage accepts. The families mirror typeisnum/typeisstr (lib/ww/typ.ww).
fn yieldclass(t: *tinfo) i32 = {
let u: *tinfo = tichase(t);
if (u == nil) { return 0i32; };
if (typeisnum(u)) { return 1i32; };
if (typeisstr(u)) { return 2i32; };
if (u.kind == tykind.TY_BOOL) { return 3i32; };
if (u.kind == tykind.TY_UNTYPED_BOOL) { return 3i32; };
return 0i32;
};
// 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
@@ -3868,7 +3887,41 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = {
// scope-popped `yield <binder>` fallback.
let armn: *node = nil;
let t: *tinfo = matchyieldtype(c, cs.body, cs.str, cs.lhs, &armn);
if (t != nil) { yt = t; retn = armn; break; };
if (t != nil) {
if (yt == nil) {
// First yielding arm: it is the match's type;
// retn carries its *node for the consumers (#264).
yt = t; retn = armn;
} else {
// #38/F2 (review item 6): cross-arm yield
// unification. cstage check.c:2080 rejects an arm
// whose yield is neither type_eq nor type_assignable
// to the first arm's. ww has tinfo typeeq but no
// tinfo type_assignable, so reject only a DEFINITE
// coarse-family mismatch (yieldclass) — closing the
// catA silent accept (int arm vs str arm reads the
// str header through an int-stamped slot at runtime)
// while staying lenient on same-family / untyped
// promotions cstage admits. Walks ALL arms (no early
// break): the post-walk matchyieldtype reads each
// arm's cached operand stamp, a pure read (#264).
// RETAINED DIVERGENCE (match-yield precision-gap task,
// lead #52 - distinct from the enum-reinterpret #52
// note elsewhere): the coarse yieldclass ALSO under-
// rejects same-family-but-not-assignable arms cstage
// DOES reject (untyped_int vs i64 / untyped_int vs
// untyped_float) - a precision gap, not a silent
// miscompile of the catA repro; closeable only with a
// real tinfo type_assignable.
if (!typeeq(yt, t)) {
let ca: i32 = yieldclass(yt);
let cb: i32 = yieldclass(t);
if (ca != 0i32 && cb != 0i32 && ca != cb) {
deffolderr(c, cs, "match arm yields an incompatible type");
};
};
};
};
cs = cs.next;
};
if (yt == nil) {

View File

@@ -11840,6 +11840,25 @@ fn matchyieldtype(c: *checker, body: *node, bname: str, btype: *node,
return nil;
};
// yieldclass — #38/F2 (review item 6): the coarse assignability family of a
// match-arm yield type, used to approximate cstage's type_assignable in the
// cross-arm unification (ww has tinfo typeeq but no tinfo type_assignable).
// 1=numeric (int/float/enum/rune + untyped_int/float/rune, NAMED-chased),
// 2=str (str + untyped_str), 3=bool, 0=unknown/other (ptr/struct/tuple/slice/
// tagged/...). Class 0 stays LENIENT so the unification rejects only a DEFINITE
// family mismatch (the catA repro: an int arm vs a str arm) and never an
// untyped->concrete promotion (untyped_int vs i32/f64 both land in class 1)
// that cstage accepts. The families mirror typeisnum/typeisstr (lib/ww/typ.ww).
fn yieldclass(t: *tinfo) i32 = {
let u: *tinfo = tichase(t);
if (u == nil) { return 0i32; };
if (typeisnum(u)) { return 1i32; };
if (typeisstr(u)) { return 2i32; };
if (u.kind == tykind.TY_BOOL) { return 3i32; };
if (u.kind == tykind.TY_UNTYPED_BOOL) { return 3i32; };
return 0i32;
};
// 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
@@ -14288,7 +14307,41 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = {
// scope-popped `yield <binder>` fallback.
let armn: *node = nil;
let t: *tinfo = matchyieldtype(c, cs.body, cs.str, cs.lhs, &armn);
if (t != nil) { yt = t; retn = armn; break; };
if (t != nil) {
if (yt == nil) {
// First yielding arm: it is the match's type;
// retn carries its *node for the consumers (#264).
yt = t; retn = armn;
} else {
// #38/F2 (review item 6): cross-arm yield
// unification. cstage check.c:2080 rejects an arm
// whose yield is neither type_eq nor type_assignable
// to the first arm's. ww has tinfo typeeq but no
// tinfo type_assignable, so reject only a DEFINITE
// coarse-family mismatch (yieldclass) — closing the
// catA silent accept (int arm vs str arm reads the
// str header through an int-stamped slot at runtime)
// while staying lenient on same-family / untyped
// promotions cstage admits. Walks ALL arms (no early
// break): the post-walk matchyieldtype reads each
// arm's cached operand stamp, a pure read (#264).
// RETAINED DIVERGENCE (match-yield precision-gap task,
// lead #52 - distinct from the enum-reinterpret #52
// note elsewhere): the coarse yieldclass ALSO under-
// rejects same-family-but-not-assignable arms cstage
// DOES reject (untyped_int vs i64 / untyped_int vs
// untyped_float) - a precision gap, not a silent
// miscompile of the catA repro; closeable only with a
// real tinfo type_assignable.
if (!typeeq(yt, t)) {
let ca: i32 = yieldclass(yt);
let cb: i32 = yieldclass(t);
if (ca != 0i32 && cb != 0i32 && ca != cb) {
deffolderr(c, cs, "match arm yields an incompatible type");
};
};
};
};
cs = cs.next;
};
if (yt == nil) {