From 9fad59354f759960e05dbee4cf3912ebfe5689a7 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 5 Jun 2026 08:07:08 +0900 Subject: [PATCH] =?UTF-8?q?cgen:=20#33=20wwstage=20untyped-source=20varian?= =?UTF-8?q?t=20match=20=E2=80=94=20mirror=20cg=5Fvariant=5Fmatch's=20type?= =?UTF-8?q?=5Fassignable=20arm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- selfhost/cmd/w6c/main.combined.ww | 92 ++++++++++++++++++--- selfhost/cmd/wcc/cgenutil.ww | 92 ++++++++++++++++++--- selfhost/cmd/wwdump/main.combined.ww | 92 ++++++++++++++++++--- test/wcc/938_tagged_structlit_payload_run.c | 45 ++++++++-- 4 files changed, 278 insertions(+), 43 deletions(-) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 1f4ecc2f..fedb9892 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -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); }; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index b73e9926..9fe0ccd7 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -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); }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 7308b4e4..41d71a11 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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); }; diff --git a/test/wcc/938_tagged_structlit_payload_run.c b/test/wcc/938_tagged_structlit_payload_run.c index 440dda89..eecb65cb 100644 --- a/test/wcc/938_tagged_structlit_payload_run.c +++ b/test/wcc/938_tagged_structlit_payload_run.c @@ -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