selfhost/cmd/wcc/check: enum<->int reinterpret bypasses tagged check

Wwstage checkisas fell straight through to the tagged-union arm on
`enum_val as i32` reinterprets, false-positiving on every enum→int
cast in lib/ (lib/time/instant.ww, lib/os, lib/os/lseek). Cstage
admits these at cmd/wcc/check.c:1346-1357: when N_TYPEASSERT has
LHS-or-RHS enum AND both ends are integer-typed, the target type
returns without the tagged check. `is` (TYPETEST) stays rejected —
cstage gates only N_TYPEASSERT.

isinttypeast helper covers N_TENUM + i8..i64/u8..u64/int/uint/
uintptr/rune. Excludes floats so `enum as f64` still rejects.
N_TYPEASSERT branch in checkisas detects enum on either side via
resolvealias-unwrap, gates on both-ends-int, returns target type
before the tagged-union check.

4/5 selfhost main.combined.ww files now resolve clean via
wwdump_ww -r. Residual on selfhost/cmd/ww tracked as #53
(separate checkletassign u64→i32 path).
This commit is contained in:
2026-05-20 03:57:47 +09:00
parent c7c756d9c8
commit 1b7fde21cd
3 changed files with 129 additions and 0 deletions

View File

@@ -7722,6 +7722,31 @@ fn isuntypednil(t: *node) bool = {
return streq(t.str, "untyped_nil"); return streq(t.str, "untyped_nil");
}; };
// isinttypeast — int-typed AST node. Either a primitive int name
// (i8..i64/u8..u64/int/uint/uintptr/rune) or an N_TENUM. Floats are
// excluded so the enum↔int reinterpret in checkisas (#52) refuses a
// surprise `enum as f64` shape. Mirrors cstage's type_isint
// (cmd/wcc/type.c) restricted to the kinds reachable from AST.
fn isinttypeast(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind == nkind.N_TENUM) { return true; };
if (t.kind != nkind.N_TNAME) { return false; };
let s: str = t.str;
if (streq(s, "i8")) { return true; };
if (streq(s, "i16")) { return true; };
if (streq(s, "i32")) { return true; };
if (streq(s, "i64")) { return true; };
if (streq(s, "u8")) { return true; };
if (streq(s, "u16")) { return true; };
if (streq(s, "u32")) { return true; };
if (streq(s, "u64")) { return true; };
if (streq(s, "int")) { return true; };
if (streq(s, "uint")) { return true; };
if (streq(s, "uintptr")) { return true; };
if (streq(s, "rune")) { return true; };
return false;
};
fn isnumerictname(t: *node) bool = { fn isnumerictname(t: *node) bool = {
if (t == nil) { return false; }; if (t == nil) { return false; };
if (t.kind != nkind.N_TNAME) { return false; }; if (t.kind != nkind.N_TNAME) { return false; };
@@ -8095,6 +8120,24 @@ fn checkisas(c: *checker, n: *node) void = {
let st: *node = scruttype(c, n.lhs); let st: *node = scruttype(c, n.lhs);
let u: *node = resolvealias(c, unwrapbang(st)); let u: *node = resolvealias(c, unwrapbang(st));
if (u == nil) { return; }; if (u == nil) { return; };
// #52: enum ↔ int reinterpret (`enum as intT` / `intT as enum`).
// Mirrors cstage cmd/wcc/check.c:1346-1357 — N_TYPEASSERT with an
// enum on either side and integer types on both reinterprets in
// the same register, no tag check involved. Returns early before
// the tagged-union gate so lib/time/instant.ww `(c as i32)` and
// the lib/os syscall casts stop false-positiving. `is` (TYPETEST)
// stays rejected on non-tagged operands — cstage cmd/wcc/check.c
// gates the bypass on N_TYPEASSERT only.
if (n.kind == nkind.N_TYPEASSERT) {
let v: *node = resolvealias(c, unwrapbang(n.rhs));
let lhsenum: bool = false;
let rhsenum: bool = false;
if (u != nil) { if (u.kind == nkind.N_TENUM) { lhsenum = true; }; };
if (v != nil) { if (v.kind == nkind.N_TENUM) { rhsenum = true; }; };
if (lhsenum || rhsenum) {
if (isinttypeast(u)) { if (isinttypeast(v)) { return; }; };
};
};
if (u.kind != nkind.N_TTAGGED) { if (u.kind != nkind.N_TTAGGED) {
os.write(2, "is/as: operand is not a tagged union\n".ptr, 37u64); os.write(2, "is/as: operand is not a tagged union\n".ptr, 37u64);
c.errs += 1; c.errs += 1;

View File

@@ -728,6 +728,31 @@ fn isuntypednil(t: *node) bool = {
return streq(t.str, "untyped_nil"); return streq(t.str, "untyped_nil");
}; };
// isinttypeast — int-typed AST node. Either a primitive int name
// (i8..i64/u8..u64/int/uint/uintptr/rune) or an N_TENUM. Floats are
// excluded so the enum↔int reinterpret in checkisas (#52) refuses a
// surprise `enum as f64` shape. Mirrors cstage's type_isint
// (cmd/wcc/type.c) restricted to the kinds reachable from AST.
fn isinttypeast(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind == nkind.N_TENUM) { return true; };
if (t.kind != nkind.N_TNAME) { return false; };
let s: str = t.str;
if (streq(s, "i8")) { return true; };
if (streq(s, "i16")) { return true; };
if (streq(s, "i32")) { return true; };
if (streq(s, "i64")) { return true; };
if (streq(s, "u8")) { return true; };
if (streq(s, "u16")) { return true; };
if (streq(s, "u32")) { return true; };
if (streq(s, "u64")) { return true; };
if (streq(s, "int")) { return true; };
if (streq(s, "uint")) { return true; };
if (streq(s, "uintptr")) { return true; };
if (streq(s, "rune")) { return true; };
return false;
};
fn isnumerictname(t: *node) bool = { fn isnumerictname(t: *node) bool = {
if (t == nil) { return false; }; if (t == nil) { return false; };
if (t.kind != nkind.N_TNAME) { return false; }; if (t.kind != nkind.N_TNAME) { return false; };
@@ -1101,6 +1126,24 @@ fn checkisas(c: *checker, n: *node) void = {
let st: *node = scruttype(c, n.lhs); let st: *node = scruttype(c, n.lhs);
let u: *node = resolvealias(c, unwrapbang(st)); let u: *node = resolvealias(c, unwrapbang(st));
if (u == nil) { return; }; if (u == nil) { return; };
// #52: enum ↔ int reinterpret (`enum as intT` / `intT as enum`).
// Mirrors cstage cmd/wcc/check.c:1346-1357 — N_TYPEASSERT with an
// enum on either side and integer types on both reinterprets in
// the same register, no tag check involved. Returns early before
// the tagged-union gate so lib/time/instant.ww `(c as i32)` and
// the lib/os syscall casts stop false-positiving. `is` (TYPETEST)
// stays rejected on non-tagged operands — cstage cmd/wcc/check.c
// gates the bypass on N_TYPEASSERT only.
if (n.kind == nkind.N_TYPEASSERT) {
let v: *node = resolvealias(c, unwrapbang(n.rhs));
let lhsenum: bool = false;
let rhsenum: bool = false;
if (u != nil) { if (u.kind == nkind.N_TENUM) { lhsenum = true; }; };
if (v != nil) { if (v.kind == nkind.N_TENUM) { rhsenum = true; }; };
if (lhsenum || rhsenum) {
if (isinttypeast(u)) { if (isinttypeast(v)) { return; }; };
};
};
if (u.kind != nkind.N_TTAGGED) { if (u.kind != nkind.N_TTAGGED) {
os.write(2, "is/as: operand is not a tagged union\n".ptr, 37u64); os.write(2, "is/as: operand is not a tagged union\n".ptr, 37u64);
c.errs += 1; c.errs += 1;

View File

@@ -7722,6 +7722,31 @@ fn isuntypednil(t: *node) bool = {
return streq(t.str, "untyped_nil"); return streq(t.str, "untyped_nil");
}; };
// isinttypeast — int-typed AST node. Either a primitive int name
// (i8..i64/u8..u64/int/uint/uintptr/rune) or an N_TENUM. Floats are
// excluded so the enum↔int reinterpret in checkisas (#52) refuses a
// surprise `enum as f64` shape. Mirrors cstage's type_isint
// (cmd/wcc/type.c) restricted to the kinds reachable from AST.
fn isinttypeast(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind == nkind.N_TENUM) { return true; };
if (t.kind != nkind.N_TNAME) { return false; };
let s: str = t.str;
if (streq(s, "i8")) { return true; };
if (streq(s, "i16")) { return true; };
if (streq(s, "i32")) { return true; };
if (streq(s, "i64")) { return true; };
if (streq(s, "u8")) { return true; };
if (streq(s, "u16")) { return true; };
if (streq(s, "u32")) { return true; };
if (streq(s, "u64")) { return true; };
if (streq(s, "int")) { return true; };
if (streq(s, "uint")) { return true; };
if (streq(s, "uintptr")) { return true; };
if (streq(s, "rune")) { return true; };
return false;
};
fn isnumerictname(t: *node) bool = { fn isnumerictname(t: *node) bool = {
if (t == nil) { return false; }; if (t == nil) { return false; };
if (t.kind != nkind.N_TNAME) { return false; }; if (t.kind != nkind.N_TNAME) { return false; };
@@ -8095,6 +8120,24 @@ fn checkisas(c: *checker, n: *node) void = {
let st: *node = scruttype(c, n.lhs); let st: *node = scruttype(c, n.lhs);
let u: *node = resolvealias(c, unwrapbang(st)); let u: *node = resolvealias(c, unwrapbang(st));
if (u == nil) { return; }; if (u == nil) { return; };
// #52: enum ↔ int reinterpret (`enum as intT` / `intT as enum`).
// Mirrors cstage cmd/wcc/check.c:1346-1357 — N_TYPEASSERT with an
// enum on either side and integer types on both reinterprets in
// the same register, no tag check involved. Returns early before
// the tagged-union gate so lib/time/instant.ww `(c as i32)` and
// the lib/os syscall casts stop false-positiving. `is` (TYPETEST)
// stays rejected on non-tagged operands — cstage cmd/wcc/check.c
// gates the bypass on N_TYPEASSERT only.
if (n.kind == nkind.N_TYPEASSERT) {
let v: *node = resolvealias(c, unwrapbang(n.rhs));
let lhsenum: bool = false;
let rhsenum: bool = false;
if (u != nil) { if (u.kind == nkind.N_TENUM) { lhsenum = true; }; };
if (v != nil) { if (v.kind == nkind.N_TENUM) { rhsenum = true; }; };
if (lhsenum || rhsenum) {
if (isinttypeast(u)) { if (isinttypeast(v)) { return; }; };
};
};
if (u.kind != nkind.N_TTAGGED) { if (u.kind != nkind.N_TTAGGED) {
os.write(2, "is/as: operand is not a tagged union\n".ptr, 37u64); os.write(2, "is/as: operand is not a tagged union\n".ptr, 37u64);
c.errs += 1; c.errs += 1;