cgen: #33 wwstage untyped-source variant match — mirror cg_variant_match's type_assignable arm

A bare untyped init into a tagged union resolved its variant through
taggedvariantindext's str/slice SHAPE fallback, whose first
non-str/slice variant can be void: `let e: (void | size) = 5` stored
tag 0 while the is/as side resolved size to 1 — runtime-FALSE is-test
on wwstage only (cstage resolves untyped sources in cg_variant_match
:801 via type_assignable; cs=0/ww=1 on ken's f33, divergent asm,
gate-blind).

Fix adds the untyped-source arm at the top of cgvariantmatch — the
single flatvariantidxt pass-1 predicate, same funnel position as
cstage — backed by tyassignableuntyped, a focused tinfo-keyed mirror
of type_assignable's untyped→typed subset (cmd/wcc/type.c:355-370)
plus its concrete→tagged variant drill (:316-324). Typed/loose
sources keep the shape fallback unchanged; tuple-in-union keeps its
AST-shape loud-stops (#241/#242, checked: TY_TUPLE is never untyped).

Probes converge byte-id: f33 let-init, assign-after-void, bool-leading
skip, bare arg widen, untyped-str, cast no-drift control. 938 row
untyped_int_bare_widen pins all of them (pre-fix ww_run=1 at 322667b).

Frees the fold-5b at-site #33 workaround (lib/regex/regex.ww:772
cast-form pin `min = (0: size)`) — the bare Hare-verbatim `min = 0`
spelling now tags correctly; the regex comment update rides the next
regex touch.

Task #33.
This commit is contained in:
2026-06-05 08:07:08 +09:00
parent 322667b820
commit 9fad59354f
4 changed files with 278 additions and 43 deletions

View File

@@ -18724,10 +18724,10 @@ fn taggedvariantindex(c: *cgen, tagged: *node, rhs: *node) i32 = {
//
// #66 Phase-N step 3: match the value's stamped type against the variant
// types by typeeq (flatvariantidxt), replacing the rhstargetname surface-
// name compare. Untyped/loose values (whose .type_ is untyped_* and can't
// typeeq a concrete variant) return -1 there and drop to the str/slice
// shape scan below — ww has no type_assignable to mirror cg_variant_match's
// untyped-src arm.
// name compare. #33: untyped values now resolve in flatvariantidxt's
// pass 1 (cgvariantmatch's tyassignableuntyped arm, the cg_variant_match
// :801 mirror); the str/slice shape scan below covers the remaining
// LOOSE sources (concrete types that typeeq no variant).
fn taggedvariantindext(c: *cgen, du: *tinfo, rhs: *node) i32 = {
if (du == nil) { return -1; };
if (rhs == nil) { return -1; };
@@ -18738,11 +18738,11 @@ fn taggedvariantindext(c: *cgen, du: *tinfo, rhs: *node) i32 = {
let r: i32 = flatvariantidxt(ti, rhs.type_: *tinfo);
if (r >= 0) { return r; };
// Shape fallback: classify rhs as (str, slice, scalar/other) and
// pick the first variant of matching shape. Stands in for cstage's
// type_assignable on an untyped src — the typeeq pass above can't
// match untyped_* against a concrete variant, and the slice axis
// keeps a (u8 | []u8) widen off the leading scalar variant (task
// #19). tinfo.params is already spread-flattened (#61a — `...inner`
// pick the first variant of matching shape. Covers LOOSE concrete
// sources that typeeq no variant (untyped sources resolve above
// via tyassignableuntyped since #33); the slice axis keeps a
// (u8 | []u8) widen off the leading scalar variant (task #19).
// tinfo.params is already spread-flattened (#61a — `...inner`
// inlined in declaration order), so the old N_TTAGGED.list spread-
// walk collapses to a flat scan over p.type_.
let wantstr: bool = nodeisstr(c, rhs);
@@ -18844,9 +18844,74 @@ fn flatvariantidxt(tagged: *tinfo, want: *tinfo) i32 = {
return -1;
};
// tyassignableuntyped — can a value slot of type `dst` HOLD an untyped
// source value? The untyped→typed subset of cstage type_assignable
// (cmd/wcc/type.c:355-370), plus its concrete→tagged variant drill
// (type.c:316-324) for a variant that is itself a union. ww's checker
// isassignable is node-keyed (AST type exprs), so the tinfo-keyed
// widen/match funnel needs this focused mirror (#33).
fn tyassignableuntyped(dst: *tinfo, want: *tinfo) bool = {
if (dst == nil) { return false; };
if (want == nil) { return false; };
let du: *tinfo = dst;
if (du.kind == tykind.TY_NAMED && du.under != nil) { du = du.under; };
if (du.kind == tykind.TY_TAGGED) {
// concrete→tagged drill (type.c:316-324): a NESTED tagged
// variant compares by type_eq there — never equal to an
// untyped source — so only non-tagged variants recurse.
let p: *tparam = du.params;
for (p != nil) {
let pu: *tinfo = p.type_;
if (pu != nil && pu.kind == tykind.TY_NAMED
&& pu.under != nil) { pu = pu.under; };
let nestedtagged: bool = false;
if (pu != nil) {
if (pu.kind == tykind.TY_TAGGED) { nestedtagged = true; };
};
if (!nestedtagged) {
if (tyassignableuntyped(p.type_, want)) { return true; };
};
p = p.tnext;
};
return false;
};
// type.c:357-369 — the per-kind holds checks run on the UNPEELED
// dst: typeisnum/typeisfloat/typeisint self-recurse TY_NAMED like
// their cstage twins; STR/BOOL spell out the one-level unwrap.
if (want.kind == tykind.TY_UNTYPED_INT) { return typeisnum(dst); };
if (want.kind == tykind.TY_UNTYPED_FLOAT) { return typeisfloat(dst); };
if (want.kind == tykind.TY_UNTYPED_STR) {
if (dst.kind == tykind.TY_STR) { return true; };
if (dst.kind == tykind.TY_NAMED && dst.under != nil) {
return dst.under.kind == tykind.TY_STR;
};
return false;
};
if (want.kind == tykind.TY_UNTYPED_RUNE) {
if (typeisint(dst)) { return true; };
return dst.kind == tykind.TY_RUNE;
};
if (want.kind == tykind.TY_UNTYPED_BOOL) {
if (dst.kind == tykind.TY_BOOL) { return true; };
if (dst.kind == tykind.TY_NAMED && dst.under != nil) {
return dst.under.kind == tykind.TY_BOOL;
};
return false;
};
if (want.kind == tykind.TY_UNTYPED_NIL) {
if (du.kind == tykind.TY_PTR) { return true; };
if (du.kind == tykind.TY_SLICE) { return true; };
if (du.kind == tykind.TY_CHAN) { return true; };
return du.kind == tykind.TY_FN;
};
return false;
};
// cgvariantmatch — does a source value of type `want` tag as variant
// `vt` in a tagged-union dispatch? Mirrors cstage cg_variant_match
// (cmd/w6c/cgen.c):
// - untyped source → first variant that can HOLD it (cgen.c:801 →
// type_assignable; #33 — see tyassignableuntyped)
// - both NAMED → nominal ptr-id (typeeq line 545 = same ptr only)
// - exactly one NAMED → #218 nominal lost: fall back to structural
// equality of the two unwrapped tagged unions, so an outer widen of
@@ -18855,12 +18920,15 @@ fn flatvariantidxt(tagged: *tinfo, want: *tinfo) i32 = {
// nominal-lossy; the collision guard in cgwidentaggedstorebp enforces
// the invariant for when #199b/B-full lands true nominal layout.
// - neither NAMED → structural typeeq
// The untyped/loose arm (cstage's type_assignable) is NOT mirrored here —
// ww has no type_assignable, so it lives in taggedvariantindext's str/
// slice shape fallback (the existing documented divergence).
fn cgvariantmatch(vt: *tinfo, want: *tinfo) bool = {
if (vt == nil) { return false; };
if (want == nil) { return false; };
// #33: pre-fix an untyped scalar fell past the typeeq passes to
// taggedvariantindext's str/slice SHAPE fallback, whose first
// non-str/slice variant can be `void` — a bare
// `let e: (void | size) = 5` stored tag 0 while the is/as side
// resolved `size` to 1 (wwstage-only; cstage resolves here).
if (typeisuntyped(want)) { return tyassignableuntyped(vt, want); };
if (vt.kind == tykind.TY_NAMED && want.kind == tykind.TY_NAMED) {
return typeeq(vt, want);
};

View File

@@ -2714,10 +2714,10 @@ fn taggedvariantindex(c: *cgen, tagged: *node, rhs: *node) i32 = {
//
// #66 Phase-N step 3: match the value's stamped type against the variant
// types by typeeq (flatvariantidxt), replacing the rhstargetname surface-
// name compare. Untyped/loose values (whose .type_ is untyped_* and can't
// typeeq a concrete variant) return -1 there and drop to the str/slice
// shape scan below — ww has no type_assignable to mirror cg_variant_match's
// untyped-src arm.
// name compare. #33: untyped values now resolve in flatvariantidxt's
// pass 1 (cgvariantmatch's tyassignableuntyped arm, the cg_variant_match
// :801 mirror); the str/slice shape scan below covers the remaining
// LOOSE sources (concrete types that typeeq no variant).
fn taggedvariantindext(c: *cgen, du: *tinfo, rhs: *node) i32 = {
if (du == nil) { return -1; };
if (rhs == nil) { return -1; };
@@ -2728,11 +2728,11 @@ fn taggedvariantindext(c: *cgen, du: *tinfo, rhs: *node) i32 = {
let r: i32 = flatvariantidxt(ti, rhs.type_: *tinfo);
if (r >= 0) { return r; };
// Shape fallback: classify rhs as (str, slice, scalar/other) and
// pick the first variant of matching shape. Stands in for cstage's
// type_assignable on an untyped src — the typeeq pass above can't
// match untyped_* against a concrete variant, and the slice axis
// keeps a (u8 | []u8) widen off the leading scalar variant (task
// #19). tinfo.params is already spread-flattened (#61a — `...inner`
// pick the first variant of matching shape. Covers LOOSE concrete
// sources that typeeq no variant (untyped sources resolve above
// via tyassignableuntyped since #33); the slice axis keeps a
// (u8 | []u8) widen off the leading scalar variant (task #19).
// tinfo.params is already spread-flattened (#61a — `...inner`
// inlined in declaration order), so the old N_TTAGGED.list spread-
// walk collapses to a flat scan over p.type_.
let wantstr: bool = nodeisstr(c, rhs);
@@ -2834,9 +2834,74 @@ fn flatvariantidxt(tagged: *tinfo, want: *tinfo) i32 = {
return -1;
};
// tyassignableuntyped — can a value slot of type `dst` HOLD an untyped
// source value? The untyped→typed subset of cstage type_assignable
// (cmd/wcc/type.c:355-370), plus its concrete→tagged variant drill
// (type.c:316-324) for a variant that is itself a union. ww's checker
// isassignable is node-keyed (AST type exprs), so the tinfo-keyed
// widen/match funnel needs this focused mirror (#33).
fn tyassignableuntyped(dst: *tinfo, want: *tinfo) bool = {
if (dst == nil) { return false; };
if (want == nil) { return false; };
let du: *tinfo = dst;
if (du.kind == tykind.TY_NAMED && du.under != nil) { du = du.under; };
if (du.kind == tykind.TY_TAGGED) {
// concrete→tagged drill (type.c:316-324): a NESTED tagged
// variant compares by type_eq there — never equal to an
// untyped source — so only non-tagged variants recurse.
let p: *tparam = du.params;
for (p != nil) {
let pu: *tinfo = p.type_;
if (pu != nil && pu.kind == tykind.TY_NAMED
&& pu.under != nil) { pu = pu.under; };
let nestedtagged: bool = false;
if (pu != nil) {
if (pu.kind == tykind.TY_TAGGED) { nestedtagged = true; };
};
if (!nestedtagged) {
if (tyassignableuntyped(p.type_, want)) { return true; };
};
p = p.tnext;
};
return false;
};
// type.c:357-369 — the per-kind holds checks run on the UNPEELED
// dst: typeisnum/typeisfloat/typeisint self-recurse TY_NAMED like
// their cstage twins; STR/BOOL spell out the one-level unwrap.
if (want.kind == tykind.TY_UNTYPED_INT) { return typeisnum(dst); };
if (want.kind == tykind.TY_UNTYPED_FLOAT) { return typeisfloat(dst); };
if (want.kind == tykind.TY_UNTYPED_STR) {
if (dst.kind == tykind.TY_STR) { return true; };
if (dst.kind == tykind.TY_NAMED && dst.under != nil) {
return dst.under.kind == tykind.TY_STR;
};
return false;
};
if (want.kind == tykind.TY_UNTYPED_RUNE) {
if (typeisint(dst)) { return true; };
return dst.kind == tykind.TY_RUNE;
};
if (want.kind == tykind.TY_UNTYPED_BOOL) {
if (dst.kind == tykind.TY_BOOL) { return true; };
if (dst.kind == tykind.TY_NAMED && dst.under != nil) {
return dst.under.kind == tykind.TY_BOOL;
};
return false;
};
if (want.kind == tykind.TY_UNTYPED_NIL) {
if (du.kind == tykind.TY_PTR) { return true; };
if (du.kind == tykind.TY_SLICE) { return true; };
if (du.kind == tykind.TY_CHAN) { return true; };
return du.kind == tykind.TY_FN;
};
return false;
};
// cgvariantmatch — does a source value of type `want` tag as variant
// `vt` in a tagged-union dispatch? Mirrors cstage cg_variant_match
// (cmd/w6c/cgen.c):
// - untyped source → first variant that can HOLD it (cgen.c:801 →
// type_assignable; #33 — see tyassignableuntyped)
// - both NAMED → nominal ptr-id (typeeq line 545 = same ptr only)
// - exactly one NAMED → #218 nominal lost: fall back to structural
// equality of the two unwrapped tagged unions, so an outer widen of
@@ -2845,12 +2910,15 @@ fn flatvariantidxt(tagged: *tinfo, want: *tinfo) i32 = {
// nominal-lossy; the collision guard in cgwidentaggedstorebp enforces
// the invariant for when #199b/B-full lands true nominal layout.
// - neither NAMED → structural typeeq
// The untyped/loose arm (cstage's type_assignable) is NOT mirrored here —
// ww has no type_assignable, so it lives in taggedvariantindext's str/
// slice shape fallback (the existing documented divergence).
fn cgvariantmatch(vt: *tinfo, want: *tinfo) bool = {
if (vt == nil) { return false; };
if (want == nil) { return false; };
// #33: pre-fix an untyped scalar fell past the typeeq passes to
// taggedvariantindext's str/slice SHAPE fallback, whose first
// non-str/slice variant can be `void` — a bare
// `let e: (void | size) = 5` stored tag 0 while the is/as side
// resolved `size` to 1 (wwstage-only; cstage resolves here).
if (typeisuntyped(want)) { return tyassignableuntyped(vt, want); };
if (vt.kind == tykind.TY_NAMED && want.kind == tykind.TY_NAMED) {
return typeeq(vt, want);
};

View File

@@ -18724,10 +18724,10 @@ fn taggedvariantindex(c: *cgen, tagged: *node, rhs: *node) i32 = {
//
// #66 Phase-N step 3: match the value's stamped type against the variant
// types by typeeq (flatvariantidxt), replacing the rhstargetname surface-
// name compare. Untyped/loose values (whose .type_ is untyped_* and can't
// typeeq a concrete variant) return -1 there and drop to the str/slice
// shape scan below — ww has no type_assignable to mirror cg_variant_match's
// untyped-src arm.
// name compare. #33: untyped values now resolve in flatvariantidxt's
// pass 1 (cgvariantmatch's tyassignableuntyped arm, the cg_variant_match
// :801 mirror); the str/slice shape scan below covers the remaining
// LOOSE sources (concrete types that typeeq no variant).
fn taggedvariantindext(c: *cgen, du: *tinfo, rhs: *node) i32 = {
if (du == nil) { return -1; };
if (rhs == nil) { return -1; };
@@ -18738,11 +18738,11 @@ fn taggedvariantindext(c: *cgen, du: *tinfo, rhs: *node) i32 = {
let r: i32 = flatvariantidxt(ti, rhs.type_: *tinfo);
if (r >= 0) { return r; };
// Shape fallback: classify rhs as (str, slice, scalar/other) and
// pick the first variant of matching shape. Stands in for cstage's
// type_assignable on an untyped src — the typeeq pass above can't
// match untyped_* against a concrete variant, and the slice axis
// keeps a (u8 | []u8) widen off the leading scalar variant (task
// #19). tinfo.params is already spread-flattened (#61a — `...inner`
// pick the first variant of matching shape. Covers LOOSE concrete
// sources that typeeq no variant (untyped sources resolve above
// via tyassignableuntyped since #33); the slice axis keeps a
// (u8 | []u8) widen off the leading scalar variant (task #19).
// tinfo.params is already spread-flattened (#61a — `...inner`
// inlined in declaration order), so the old N_TTAGGED.list spread-
// walk collapses to a flat scan over p.type_.
let wantstr: bool = nodeisstr(c, rhs);
@@ -18844,9 +18844,74 @@ fn flatvariantidxt(tagged: *tinfo, want: *tinfo) i32 = {
return -1;
};
// tyassignableuntyped — can a value slot of type `dst` HOLD an untyped
// source value? The untyped→typed subset of cstage type_assignable
// (cmd/wcc/type.c:355-370), plus its concrete→tagged variant drill
// (type.c:316-324) for a variant that is itself a union. ww's checker
// isassignable is node-keyed (AST type exprs), so the tinfo-keyed
// widen/match funnel needs this focused mirror (#33).
fn tyassignableuntyped(dst: *tinfo, want: *tinfo) bool = {
if (dst == nil) { return false; };
if (want == nil) { return false; };
let du: *tinfo = dst;
if (du.kind == tykind.TY_NAMED && du.under != nil) { du = du.under; };
if (du.kind == tykind.TY_TAGGED) {
// concrete→tagged drill (type.c:316-324): a NESTED tagged
// variant compares by type_eq there — never equal to an
// untyped source — so only non-tagged variants recurse.
let p: *tparam = du.params;
for (p != nil) {
let pu: *tinfo = p.type_;
if (pu != nil && pu.kind == tykind.TY_NAMED
&& pu.under != nil) { pu = pu.under; };
let nestedtagged: bool = false;
if (pu != nil) {
if (pu.kind == tykind.TY_TAGGED) { nestedtagged = true; };
};
if (!nestedtagged) {
if (tyassignableuntyped(p.type_, want)) { return true; };
};
p = p.tnext;
};
return false;
};
// type.c:357-369 — the per-kind holds checks run on the UNPEELED
// dst: typeisnum/typeisfloat/typeisint self-recurse TY_NAMED like
// their cstage twins; STR/BOOL spell out the one-level unwrap.
if (want.kind == tykind.TY_UNTYPED_INT) { return typeisnum(dst); };
if (want.kind == tykind.TY_UNTYPED_FLOAT) { return typeisfloat(dst); };
if (want.kind == tykind.TY_UNTYPED_STR) {
if (dst.kind == tykind.TY_STR) { return true; };
if (dst.kind == tykind.TY_NAMED && dst.under != nil) {
return dst.under.kind == tykind.TY_STR;
};
return false;
};
if (want.kind == tykind.TY_UNTYPED_RUNE) {
if (typeisint(dst)) { return true; };
return dst.kind == tykind.TY_RUNE;
};
if (want.kind == tykind.TY_UNTYPED_BOOL) {
if (dst.kind == tykind.TY_BOOL) { return true; };
if (dst.kind == tykind.TY_NAMED && dst.under != nil) {
return dst.under.kind == tykind.TY_BOOL;
};
return false;
};
if (want.kind == tykind.TY_UNTYPED_NIL) {
if (du.kind == tykind.TY_PTR) { return true; };
if (du.kind == tykind.TY_SLICE) { return true; };
if (du.kind == tykind.TY_CHAN) { return true; };
return du.kind == tykind.TY_FN;
};
return false;
};
// cgvariantmatch — does a source value of type `want` tag as variant
// `vt` in a tagged-union dispatch? Mirrors cstage cg_variant_match
// (cmd/w6c/cgen.c):
// - untyped source → first variant that can HOLD it (cgen.c:801 →
// type_assignable; #33 — see tyassignableuntyped)
// - both NAMED → nominal ptr-id (typeeq line 545 = same ptr only)
// - exactly one NAMED → #218 nominal lost: fall back to structural
// equality of the two unwrapped tagged unions, so an outer widen of
@@ -18855,12 +18920,15 @@ fn flatvariantidxt(tagged: *tinfo, want: *tinfo) i32 = {
// nominal-lossy; the collision guard in cgwidentaggedstorebp enforces
// the invariant for when #199b/B-full lands true nominal layout.
// - neither NAMED → structural typeeq
// The untyped/loose arm (cstage's type_assignable) is NOT mirrored here —
// ww has no type_assignable, so it lives in taggedvariantindext's str/
// slice shape fallback (the existing documented divergence).
fn cgvariantmatch(vt: *tinfo, want: *tinfo) bool = {
if (vt == nil) { return false; };
if (want == nil) { return false; };
// #33: pre-fix an untyped scalar fell past the typeeq passes to
// taggedvariantindext's str/slice SHAPE fallback, whose first
// non-str/slice variant can be `void` — a bare
// `let e: (void | size) = 5` stored tag 0 while the is/as side
// resolved `size` to 1 (wwstage-only; cstage resolves here).
if (typeisuntyped(want)) { return tyassignableuntyped(vt, want); };
if (vt.kind == tykind.TY_NAMED && want.kind == tykind.TY_NAMED) {
return typeeq(vt, want);
};

View File

@@ -55,13 +55,12 @@
*
* Field readback goes through MATCH BINDINGS only: direct field reads
* on a struct-w/-tagged-field LOCAL are the open #24 (wwstage
* field(SB) fallback), and tagged-field inits use explicit `: size`
* casts: the BARE untyped-int form is the open #33 wwstage mis-tag.
* Both are filed separately and out of #23's arm. A tagged field
* inside a NESTED plain-struct field is the open #38 family (fails
* identically for plain locals — pre-existing in the canonical fill/
* read path, NOT a #23 regression; probed both ways at 6699158) and
* is deliberately NOT a row here.
* field(SB) fallback). Tagged-field inits use explicit `: size`
* casts in the #23 rows; the BARE untyped-int form (the old #33
* wwstage mis-tag) is pinned by its own row (untyped_int_bare_widen)
* since the cgvariantmatch untyped arm landed. The chained-dot
* tagged-leaf read/assign family (#38a) is pinned by the
* chained_* rows below since the dot-spine tagged arms landed.
*
* Every K_RUN row also asserts cstage/wwstage asm byte-id. NNN<950,
* self-contained (/tmp, no imports) — rule-14's selfhost-sibling race
@@ -427,6 +426,38 @@ static const struct row rows[] = {
" };\n"
" return 0;\n"
"};\n", 0, 0 },
/* #33: a BARE untyped-int init into (void|T) — pre-fix wwstage's
* variant scan fell to the str/slice shape fallback whose first
* non-str/slice variant is `void` (tag 0 stored, is-test expects
* 1; cstage resolved via type_assignable). Pins let-init, assign
* after void, arg widen, the bool-leading skip, and the signed
* (void|i64) variant (ken hB3); the cast form rides alongside as
* the no-drift control (it was always right). */
{ "untyped_int_bare_widen",
"package main;\n"
"fn take(v: (void | size)) i32 = {\n"
" if (v is size) { return (v as size): i32; };\n"
" return -1;\n"
"};\n"
"export fn main() i32 = {\n"
" let e: (void | size) = 5;\n"
" if (!(e is size)) { return 1; };\n"
" if (e as size != 5) { return 2; };\n"
" let mn: (void | size) = void;\n"
" mn = 7;\n"
" if (!(mn is size)) { return 3; };\n"
" if (mn as size != 7) { return 4; };\n"
" let b: (bool | u64) = 9;\n"
" if (!(b is u64)) { return 5; };\n"
" if (take(9) != 9) { return 6; };\n"
" let cf: (void | size) = 5: size;\n"
" if (!(cf is size)) { return 7; };\n"
" if (cf as size != 5) { return 8; };\n"
" let f: (void | i64) = 9;\n"
" if (!(f is i64)) { return 9; };\n"
" if (f as i64 != 9) { return 10; };\n"
" return 0;\n"
"};\n", 0, 0 },
};
static int