From 8d2d157a58fb3fff283b6f47241c6887592f7916 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 11 Jun 2026 20:41:20 +0900 Subject: [PATCH] wcc/ww: flexible rune-const promotion at scalar seams (coercerunelit) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wwstage stamped N_RUNELIT as concrete rune (cstage: untyped_rune, check.c:1296), so an uncast rune literal into a fitting integer slot over-rejected where harec accepts (promote_flexible). Add coercerunelit (mirror of coercefloatlit) at the scalar seams — let-init, return, call-arg, index-store; coarse accept, no range check (mirror cstage type.c:379); array-literal elements keep the range gate (arrlit_init_fits, types.c:923). Teeth: the fnmatch.ww '\\': u8 workaround cast and getopttest's 'X': u8 index-store casts are removed and compile green. Checker-only (no e.type_ restamp): byte-id held. --- lib/fnmatch/fnmatch.ww | 3 +- lib/getopt/getopttest.ww | 22 ++++----- selfhost/cmd/w6c/main.combined.ww | 68 ++++++++++++++++++++++++++++ selfhost/cmd/wcc/check.ww | 68 ++++++++++++++++++++++++++++ selfhost/cmd/wwdump/main.combined.ww | 68 ++++++++++++++++++++++++++++ 5 files changed, 216 insertions(+), 13 deletions(-) diff --git a/lib/fnmatch/fnmatch.ww b/lib/fnmatch/fnmatch.ww index b18235ed..f5fd9a33 100644 --- a/lib/fnmatch/fnmatch.ww +++ b/lib/fnmatch/fnmatch.ww @@ -123,8 +123,7 @@ fn pat_next(pat: str, pos: *i32, fl: flag) (u8 | star | question | bracket | end *pos += 1; return c2; }; - // #29: wwstage over-rejects the implicit flexible-rune-const narrow (harec promote_flexible accepts); explicit cast until flexible-const promotion lands — revert to bare '\\' then. - return '\\': u8; + return '\\'; }; return c; }; diff --git a/lib/getopt/getopttest.ww b/lib/getopt/getopttest.ww index 89537b70..f9250185 100644 --- a/lib/getopt/getopttest.ww +++ b/lib/getopt/getopttest.ww @@ -63,10 +63,10 @@ fn streq(a: str, b: str) bool = { // (wantflag, wantval) parallel arrays, indexed by option order. let wantf: [4]u8; let wantv: [4]str; - wantf[0] = 'F': u8; wantv[0] = ""; - wantf[1] = 'a': u8; wantv[1] = ""; - wantf[2] = 'h': u8; wantv[2] = ""; - wantf[3] = 's': u8; wantv[3] = ""; + wantf[0] = 'F'; wantv[0] = ""; + wantf[1] = 'a'; wantv[1] = ""; + wantf[2] = 'h'; wantv[2] = ""; + wantf[3] = 's'; wantv[3] = ""; let i: i32 = 0; for (i < 4) { @@ -108,8 +108,8 @@ fn streq(a: str, b: str) bool = { let wantf: [2]u8; let wantv: [2]str; - wantf[0] = 'e': u8; wantv[0] = "s/foo/bar/"; - wantf[1] = 'f': u8; wantv[1] = "/tmp/x.sed"; + wantf[0] = 'e'; wantv[0] = "s/foo/bar/"; + wantf[1] = 'f'; wantv[1] = "/tmp/x.sed"; let i: i32 = 0; for (i < 2) { @@ -280,9 +280,9 @@ fn streq(a: str, b: str) bool = { let argn: [3]i32; let wantk: [3]i32; let wantf: [3]u8; - argo[0] = 0; argn[0] = 2; wantk[0] = 1; wantf[0] = 'x': u8; // UNKNOWNOPT=1 - argo[1] = 2; argn[1] = 2; wantk[1] = 0; wantf[1] = 'e': u8; // REQUIRESARG=0 - argo[2] = 4; argn[2] = 3; wantk[2] = 0; wantf[2] = 'e': u8; + argo[0] = 0; argn[0] = 2; wantk[0] = 1; wantf[0] = 'x'; // UNKNOWNOPT=1 + argo[1] = 2; argn[1] = 2; wantk[1] = 0; wantf[1] = 'e'; // REQUIRESARG=0 + argo[2] = 4; argn[2] = 3; wantk[2] = 0; wantf[2] = 'e'; let i: i32 = 0; for (i < 3) { @@ -316,8 +316,8 @@ fn streq(a: str, b: str) bool = { let flags: [2]u8; let names: [2]str; let wants: [2]str; - kinds[0]=1; flags[0]='x': u8; names[0]="prog"; wants[0]="prog: unrecognized option: -x"; - kinds[1]=0; flags[1]='e': u8; names[1]="sed"; wants[1]="sed: option -e requires an argument"; + kinds[0]=1; flags[0]='x'; names[0]="prog"; wants[0]="prog: unrecognized option: -x"; + kinds[1]=0; flags[1]='e'; names[1]="sed"; wants[1]="sed: option -e requires an argument"; let i: i32 = 0; for (i < 2) { diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 06856511..478892b6 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -12756,6 +12756,48 @@ fn coercefloatlit(c: *checker, e: *node, target: *node) void = { }; }; +// coercerunelit — #29: an un-suffixed rune literal narrowing into an +// integer slot. Same intent as coercefloatlit (:2324) but CHECKER-only: +// wwstage stamps N_RUNELIT concrete `rune` (:2652), so isassignable's +// untyped arms never fire and the operand falls to the "two known +// primitives, different names → confident reject" arm (:4139) — the #29 +// over-reject. cstage stamps N_RUNELIT untyped_rune (cmd/wcc/check.c:1296) +// which type_assignable admits into ANY integer UNCONDITIONALLY +// (cmd/wcc/type.c:379 — coarse, NO range check; harec's range-precise +// promote_flexible at types.c:928 is the reference cstage already +// flattened, so wwstage mirrors cstage's coarse rule, not harec's gate). +// Return the integer target tnode so the caller overrides its `src`/ +// `atype` and isassignable sees su==target → accept. Does NOT touch +// e.type_: cgen narrows a rune immediate by the DESTINATION width (proven +// byte-id at let/call-arg/index-store; #251 array-elem twin) and for a +// tagged-union target the variant boxing falls to taggedvariantindext's +// scalar-shape fallback (cgenutil.ww:3054) which picks the first scalar +// variant — the same u8 variant cstage's untyped_rune boxes into — so the +// unchanged node stamp keeps cgen provably identical to the pre-fix +// bare-literal lowering. Array-LITERAL elements are NOT routed here: they +// keep harec's per-element range gate via checkarrlitfits's foldint / +// defcastfits path (:4634). For a tagged-union target (fnmatch pat_next's +// (u8 | star | ...)) return the first integer variant. +fn coercerunelit(c: *checker, e: *node, target: *node) *node = { + if (e == nil) { return nil; }; + if (e.kind != nkind.N_RUNELIT) { return nil; }; + if (target == nil) { return nil; }; + let tu: *node = resolvealias(c, unwrapbang(target)); + if (tu == nil) { return nil; }; + if (isinttypeast(tu)) { return tu; }; + if (tu.kind == nkind.N_TTAGGED) { + let v: *node = tu.list; + for (v != nil) { + let vu: *node = resolvealias(c, unwrapbang(v)); + if (vu != nil) { + if (isinttypeast(vu)) { return vu; }; + }; + v = v.next; + }; + }; + return nil; +}; + // binoptype — derive the result tnode of an N_BIN operator expression. // Mirrors cstage cmd/wcc/check.c:598-640 `cbinop`. Operates on tnodes // returned by exprtype; ptr arithmetic / bitwise / shifts / comparisons / @@ -15215,6 +15257,14 @@ fn desugarcallargs(c: *checker, n: *node) void = { if (param.kind == nkind.N_PARAM) { if (param.op != tkind.TK_ELLIPSIS) { let atype: *node = exprtype(c, a, nil); + // #29: a rune literal narrowing into an integer param + // (`take('b')` where take(b: u8)). Override atype to the + // integer target so c3's general isassignable accepts, + // mirroring cstage's coarse untyped_rune rule. See + // coercerunelit. cgen is byte-id-neutral (narrows by the + // param/destination width). + let runet: *node = coercerunelit(c, a, param.lhs); + if (runet != nil) { atype = runet; }; // #258: an array arg into a []T param with a // MISMATCHED element is not a borrow — loud reject, // mirror cstage's call-arg type_assignable failure. @@ -15267,6 +15317,13 @@ fn checkassign(c: *checker, n: *node) void = { if (n.rhs == nil) { return; }; let ltn: *node = exprtype(c, n.lhs, nil); let rtn: *node = exprtype(c, n.rhs, nil); + // #29: a rune literal narrowing into an integer assign/index-store + // target (`buf[i] = 'F'`). Override rtn to the integer target so a + // general assign typecheck accepts, mirroring cstage's coarse + // untyped_rune rule. See coercerunelit. cgen narrows by the destination + // width (byte-id-neutral). Removes the getopt `'X': u8` index casts. + let runet: *node = coercerunelit(c, n.rhs, ltn); + if (runet != nil) { rtn = runet; }; // #31/#33: bare array-literal rhs has no backing — loud-reject // (supported only at a `let`). if (!rejectarrlitborrow(c, ltn, n.rhs)) { @@ -15508,6 +15565,11 @@ fn checkletassign(c: *checker, n: *node) void = { n.rhs.lhs = arr; src = arr; }; + // #29: an un-suffixed rune literal narrowing into an integer let target + // (`let b: u8 = 'a'`). Override src to the integer target so isassignable + // accepts, mirroring cstage's coarse untyped_rune rule. See coercerunelit. + let runet: *node = coercerunelit(c, n.rhs, n.lhs); + if (runet != nil) { src = runet; }; let conf: bool = false; let ok: bool = isassignable(c, n.lhs, src, &conf); // #206: direct `&fn` → `*alias` / `(*alias | void)` slot. @@ -15573,6 +15635,12 @@ fn checkretassign(c: *checker, n: *node) void = { }; let src: *node = exprtype(c, n.lhs, nil); if (src == nil) { return; }; + // #29: a rune literal returned into an integer (or integer-variant) + // fnret (`return '\\';` into u8 or (u8 | star | ...)). Override src so + // isassignable accepts; cgen boxes via the shape fallback. See + // coercerunelit. Removes the fnmatch.ww:126 `'\\': u8` workaround cast. + let runet: *node = coercerunelit(c, n.lhs, c.fnret); + if (runet != nil) { src = runet; }; let conf: bool = false; let ok: bool = isassignable(c, c.fnret, src, &conf); // #206: direct `&fn` returned into a `*alias` / `(*alias | void)`. diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 7a238248..36424d0a 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -2336,6 +2336,48 @@ fn coercefloatlit(c: *checker, e: *node, target: *node) void = { }; }; +// coercerunelit — #29: an un-suffixed rune literal narrowing into an +// integer slot. Same intent as coercefloatlit (:2324) but CHECKER-only: +// wwstage stamps N_RUNELIT concrete `rune` (:2652), so isassignable's +// untyped arms never fire and the operand falls to the "two known +// primitives, different names → confident reject" arm (:4139) — the #29 +// over-reject. cstage stamps N_RUNELIT untyped_rune (cmd/wcc/check.c:1296) +// which type_assignable admits into ANY integer UNCONDITIONALLY +// (cmd/wcc/type.c:379 — coarse, NO range check; harec's range-precise +// promote_flexible at types.c:928 is the reference cstage already +// flattened, so wwstage mirrors cstage's coarse rule, not harec's gate). +// Return the integer target tnode so the caller overrides its `src`/ +// `atype` and isassignable sees su==target → accept. Does NOT touch +// e.type_: cgen narrows a rune immediate by the DESTINATION width (proven +// byte-id at let/call-arg/index-store; #251 array-elem twin) and for a +// tagged-union target the variant boxing falls to taggedvariantindext's +// scalar-shape fallback (cgenutil.ww:3054) which picks the first scalar +// variant — the same u8 variant cstage's untyped_rune boxes into — so the +// unchanged node stamp keeps cgen provably identical to the pre-fix +// bare-literal lowering. Array-LITERAL elements are NOT routed here: they +// keep harec's per-element range gate via checkarrlitfits's foldint / +// defcastfits path (:4634). For a tagged-union target (fnmatch pat_next's +// (u8 | star | ...)) return the first integer variant. +fn coercerunelit(c: *checker, e: *node, target: *node) *node = { + if (e == nil) { return nil; }; + if (e.kind != nkind.N_RUNELIT) { return nil; }; + if (target == nil) { return nil; }; + let tu: *node = resolvealias(c, unwrapbang(target)); + if (tu == nil) { return nil; }; + if (isinttypeast(tu)) { return tu; }; + if (tu.kind == nkind.N_TTAGGED) { + let v: *node = tu.list; + for (v != nil) { + let vu: *node = resolvealias(c, unwrapbang(v)); + if (vu != nil) { + if (isinttypeast(vu)) { return vu; }; + }; + v = v.next; + }; + }; + return nil; +}; + // binoptype — derive the result tnode of an N_BIN operator expression. // Mirrors cstage cmd/wcc/check.c:598-640 `cbinop`. Operates on tnodes // returned by exprtype; ptr arithmetic / bitwise / shifts / comparisons / @@ -4795,6 +4837,14 @@ fn desugarcallargs(c: *checker, n: *node) void = { if (param.kind == nkind.N_PARAM) { if (param.op != tkind.TK_ELLIPSIS) { let atype: *node = exprtype(c, a, nil); + // #29: a rune literal narrowing into an integer param + // (`take('b')` where take(b: u8)). Override atype to the + // integer target so c3's general isassignable accepts, + // mirroring cstage's coarse untyped_rune rule. See + // coercerunelit. cgen is byte-id-neutral (narrows by the + // param/destination width). + let runet: *node = coercerunelit(c, a, param.lhs); + if (runet != nil) { atype = runet; }; // #258: an array arg into a []T param with a // MISMATCHED element is not a borrow — loud reject, // mirror cstage's call-arg type_assignable failure. @@ -4847,6 +4897,13 @@ fn checkassign(c: *checker, n: *node) void = { if (n.rhs == nil) { return; }; let ltn: *node = exprtype(c, n.lhs, nil); let rtn: *node = exprtype(c, n.rhs, nil); + // #29: a rune literal narrowing into an integer assign/index-store + // target (`buf[i] = 'F'`). Override rtn to the integer target so a + // general assign typecheck accepts, mirroring cstage's coarse + // untyped_rune rule. See coercerunelit. cgen narrows by the destination + // width (byte-id-neutral). Removes the getopt `'X': u8` index casts. + let runet: *node = coercerunelit(c, n.rhs, ltn); + if (runet != nil) { rtn = runet; }; // #31/#33: bare array-literal rhs has no backing — loud-reject // (supported only at a `let`). if (!rejectarrlitborrow(c, ltn, n.rhs)) { @@ -5088,6 +5145,11 @@ fn checkletassign(c: *checker, n: *node) void = { n.rhs.lhs = arr; src = arr; }; + // #29: an un-suffixed rune literal narrowing into an integer let target + // (`let b: u8 = 'a'`). Override src to the integer target so isassignable + // accepts, mirroring cstage's coarse untyped_rune rule. See coercerunelit. + let runet: *node = coercerunelit(c, n.rhs, n.lhs); + if (runet != nil) { src = runet; }; let conf: bool = false; let ok: bool = isassignable(c, n.lhs, src, &conf); // #206: direct `&fn` → `*alias` / `(*alias | void)` slot. @@ -5153,6 +5215,12 @@ fn checkretassign(c: *checker, n: *node) void = { }; let src: *node = exprtype(c, n.lhs, nil); if (src == nil) { return; }; + // #29: a rune literal returned into an integer (or integer-variant) + // fnret (`return '\\';` into u8 or (u8 | star | ...)). Override src so + // isassignable accepts; cgen boxes via the shape fallback. See + // coercerunelit. Removes the fnmatch.ww:126 `'\\': u8` workaround cast. + let runet: *node = coercerunelit(c, n.lhs, c.fnret); + if (runet != nil) { src = runet; }; let conf: bool = false; let ok: bool = isassignable(c, c.fnret, src, &conf); // #206: direct `&fn` returned into a `*alias` / `(*alias | void)`. diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index d92ddfa9..f111fb9d 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -12756,6 +12756,48 @@ fn coercefloatlit(c: *checker, e: *node, target: *node) void = { }; }; +// coercerunelit — #29: an un-suffixed rune literal narrowing into an +// integer slot. Same intent as coercefloatlit (:2324) but CHECKER-only: +// wwstage stamps N_RUNELIT concrete `rune` (:2652), so isassignable's +// untyped arms never fire and the operand falls to the "two known +// primitives, different names → confident reject" arm (:4139) — the #29 +// over-reject. cstage stamps N_RUNELIT untyped_rune (cmd/wcc/check.c:1296) +// which type_assignable admits into ANY integer UNCONDITIONALLY +// (cmd/wcc/type.c:379 — coarse, NO range check; harec's range-precise +// promote_flexible at types.c:928 is the reference cstage already +// flattened, so wwstage mirrors cstage's coarse rule, not harec's gate). +// Return the integer target tnode so the caller overrides its `src`/ +// `atype` and isassignable sees su==target → accept. Does NOT touch +// e.type_: cgen narrows a rune immediate by the DESTINATION width (proven +// byte-id at let/call-arg/index-store; #251 array-elem twin) and for a +// tagged-union target the variant boxing falls to taggedvariantindext's +// scalar-shape fallback (cgenutil.ww:3054) which picks the first scalar +// variant — the same u8 variant cstage's untyped_rune boxes into — so the +// unchanged node stamp keeps cgen provably identical to the pre-fix +// bare-literal lowering. Array-LITERAL elements are NOT routed here: they +// keep harec's per-element range gate via checkarrlitfits's foldint / +// defcastfits path (:4634). For a tagged-union target (fnmatch pat_next's +// (u8 | star | ...)) return the first integer variant. +fn coercerunelit(c: *checker, e: *node, target: *node) *node = { + if (e == nil) { return nil; }; + if (e.kind != nkind.N_RUNELIT) { return nil; }; + if (target == nil) { return nil; }; + let tu: *node = resolvealias(c, unwrapbang(target)); + if (tu == nil) { return nil; }; + if (isinttypeast(tu)) { return tu; }; + if (tu.kind == nkind.N_TTAGGED) { + let v: *node = tu.list; + for (v != nil) { + let vu: *node = resolvealias(c, unwrapbang(v)); + if (vu != nil) { + if (isinttypeast(vu)) { return vu; }; + }; + v = v.next; + }; + }; + return nil; +}; + // binoptype — derive the result tnode of an N_BIN operator expression. // Mirrors cstage cmd/wcc/check.c:598-640 `cbinop`. Operates on tnodes // returned by exprtype; ptr arithmetic / bitwise / shifts / comparisons / @@ -15215,6 +15257,14 @@ fn desugarcallargs(c: *checker, n: *node) void = { if (param.kind == nkind.N_PARAM) { if (param.op != tkind.TK_ELLIPSIS) { let atype: *node = exprtype(c, a, nil); + // #29: a rune literal narrowing into an integer param + // (`take('b')` where take(b: u8)). Override atype to the + // integer target so c3's general isassignable accepts, + // mirroring cstage's coarse untyped_rune rule. See + // coercerunelit. cgen is byte-id-neutral (narrows by the + // param/destination width). + let runet: *node = coercerunelit(c, a, param.lhs); + if (runet != nil) { atype = runet; }; // #258: an array arg into a []T param with a // MISMATCHED element is not a borrow — loud reject, // mirror cstage's call-arg type_assignable failure. @@ -15267,6 +15317,13 @@ fn checkassign(c: *checker, n: *node) void = { if (n.rhs == nil) { return; }; let ltn: *node = exprtype(c, n.lhs, nil); let rtn: *node = exprtype(c, n.rhs, nil); + // #29: a rune literal narrowing into an integer assign/index-store + // target (`buf[i] = 'F'`). Override rtn to the integer target so a + // general assign typecheck accepts, mirroring cstage's coarse + // untyped_rune rule. See coercerunelit. cgen narrows by the destination + // width (byte-id-neutral). Removes the getopt `'X': u8` index casts. + let runet: *node = coercerunelit(c, n.rhs, ltn); + if (runet != nil) { rtn = runet; }; // #31/#33: bare array-literal rhs has no backing — loud-reject // (supported only at a `let`). if (!rejectarrlitborrow(c, ltn, n.rhs)) { @@ -15508,6 +15565,11 @@ fn checkletassign(c: *checker, n: *node) void = { n.rhs.lhs = arr; src = arr; }; + // #29: an un-suffixed rune literal narrowing into an integer let target + // (`let b: u8 = 'a'`). Override src to the integer target so isassignable + // accepts, mirroring cstage's coarse untyped_rune rule. See coercerunelit. + let runet: *node = coercerunelit(c, n.rhs, n.lhs); + if (runet != nil) { src = runet; }; let conf: bool = false; let ok: bool = isassignable(c, n.lhs, src, &conf); // #206: direct `&fn` → `*alias` / `(*alias | void)` slot. @@ -15573,6 +15635,12 @@ fn checkretassign(c: *checker, n: *node) void = { }; let src: *node = exprtype(c, n.lhs, nil); if (src == nil) { return; }; + // #29: a rune literal returned into an integer (or integer-variant) + // fnret (`return '\\';` into u8 or (u8 | star | ...)). Override src so + // isassignable accepts; cgen boxes via the shape fallback. See + // coercerunelit. Removes the fnmatch.ww:126 `'\\': u8` workaround cast. + let runet: *node = coercerunelit(c, n.lhs, c.fnret); + if (runet != nil) { src = runet; }; let conf: bool = false; let ok: bool = isassignable(c, c.fnret, src, &conf); // #206: direct `&fn` returned into a `*alias` / `(*alias | void)`.