Skip to content

Commit 4e57a5a

Browse files
A union of null and several types can be tested for truth (#540)
`if (x)` with `x: boolean | string | null` failed to compile ("can't instantiate function. '___cast' not all generic types can be identified"), and so did `if (enabled && handle)` for a boolean and a `Handle | null`, whose `&&` makes such a union. The generated union cast answered `return t` for the null member, which strict null mode refuses to cast to boolean. Cast to boolean, a null or undefined member is now false, as castFromAny already does for an `any`. A null `Handle` carries the class's tag in that union, and the class member's `instanceof` test read its vtable through the null pointer; for a truth test of a union with one class, the class member is now just tested for null. Reported in docs/bugs/native-ts-graphics-findings.md (issue 6). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 5391b05 commit 4e57a5a

3 files changed

Lines changed: 92 additions & 1 deletion

File tree

‎tslang/lib/TypeScript/MLIRGenCast.cpp‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2272,6 +2272,13 @@ namespace mlirgen
22722272
}
22732273
else
22742274
{
2275+
// Tested for truth, a null or undefined member is false, as for an `any`
2276+
// (castFromAny). `return t` there cast null to boolean - "can't cast from 'null'
2277+
// to 'boolean'" - so a union of null and two or more other types, which is what
2278+
// `enabled && handle` makes of a boolean and a `Handle | null`, could not be
2279+
// tested in an `if` at all.
2280+
auto toBoolean = isa<mlir_ts::BooleanType>(type) || isa<mlir_ts::TypePredicateType>(type);
2281+
22752282
// default
22762283
auto next = false;
22772284
for (auto& pair : typeOfs)
@@ -2281,7 +2288,19 @@ namespace mlirgen
22812288
ss << S("if (typeof t == '");
22822289
ss << stows(pair.getKey().str());
22832290
ss << S("') ");
2284-
if (pair.getKey() == "class")
2291+
if (toBoolean && (pair.getKey() == "null" || pair.getKey() == "undefined"))
2292+
{
2293+
ss << S("return false;\n");
2294+
}
2295+
else if (toBoolean && pair.getKey() == "class" && classInstances.size() == 1)
2296+
{
2297+
// tested for truth, an instance is true and null is false - and a null
2298+
// instance does carry the class's tag, when a `Class | null` became this
2299+
// union (`enabled && handle` with handle null). `instanceof` would read
2300+
// its vtable through the null pointer.
2301+
ss << S("return t;\n");
2302+
}
2303+
else if (pair.getKey() == "class")
22852304
{
22862305
ss << S("{ \n");
22872306

‎tslang/test/tester/CMakeLists.txt‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,7 @@ tslang_add_test(NAME test-compile-00-union-bin-ops-2 COMMAND test-runner "${PROJ
407407
tslang_add_test(NAME test-compile-00-union-to-any COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_to_any.ts")
408408
tslang_add_test(NAME test-compile-00-union-errors COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_errors.ts")
409409
tslang_add_test(NAME test-compile-00-union-null-to-string COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_null_to_string.ts")
410+
tslang_add_test(NAME test-compile-00-union-null-truthiness COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_null_truthiness.ts")
410411
tslang_add_test(NAME test-compile-00-union-null-undefined COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_null_undefined.ts")
411412
tslang_add_test(NAME test-compile-00-union-null-undefined-nonstrict COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_null_undefined_nonstrict.ts")
412413
tslang_add_test(NAME test-compile-00-optional-number-compare COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00optional_number_compare.ts")
@@ -931,6 +932,7 @@ tslang_add_test(NAME test-jit-00-union-bin-ops-2 COMMAND test-runner -jit "${PRO
931932
tslang_add_test(NAME test-jit-00-union-to-any COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_to_any.ts")
932933
tslang_add_test(NAME test-jit-00-union-errors COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_errors.ts")
933934
tslang_add_test(NAME test-jit-00-union-null-to-string COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_null_to_string.ts")
935+
tslang_add_test(NAME test-jit-00-union-null-truthiness COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_null_truthiness.ts")
934936
tslang_add_test(NAME test-jit-00-union-null-undefined COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_null_undefined.ts")
935937
tslang_add_test(NAME test-jit-00-union-null-undefined-nonstrict COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_null_undefined_nonstrict.ts")
936938
tslang_add_test(NAME test-jit-00-optional-number-compare COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00optional_number_compare.ts")
@@ -2044,6 +2046,7 @@ set(TSLANG_CORPUS
20442046
00union_bin_ops2.ts
20452047
00union_errors.ts
20462048
00union_null_to_string.ts
2049+
00union_null_truthiness.ts
20472050
00union_ops.ts
20482051
00union_to_any.ts
20492052
00union_type.ts
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Testing a union of null and two or more other types for truth failed to compile: "can't
2+
// instantiate function. '___cast' not all generic types can be identified", after "can't cast
3+
// from 'null' to 'boolean'" reported at an unrelated place. `boolean && (Handle | null)` is such
4+
// a union, so `if (enabled && handle)` failed the same way.
5+
6+
class Handle {
7+
value: number;
8+
constructor() {
9+
this.value = 7;
10+
}
11+
}
12+
13+
function use(enabled: boolean, handle: Handle | null): number {
14+
if (enabled && handle) {
15+
return handle.value;
16+
}
17+
18+
return 0;
19+
}
20+
21+
function useReversed(enabled: boolean, handle: Handle | null): number {
22+
if (handle && enabled) {
23+
return 1;
24+
}
25+
26+
return 0;
27+
}
28+
29+
function truthy(x: boolean | string | null): boolean {
30+
if (x) {
31+
return true;
32+
}
33+
34+
return false;
35+
}
36+
37+
function notTruthy(x: boolean | number | null): boolean {
38+
return !x;
39+
}
40+
41+
function andText(e: boolean, s: string | null): number {
42+
return e && s ? 1 : 0;
43+
}
44+
45+
function main() {
46+
const h = new Handle();
47+
assert(use(true, h) == 7, "use(true, h)");
48+
assert(use(false, h) == 0, "use(false, h)");
49+
assert(use(true, null) == 0, "use(true, null)");
50+
assert(useReversed(true, h) == 1, "useReversed(true, h)");
51+
assert(useReversed(true, null) == 0, "useReversed(true, null)");
52+
53+
assert(truthy("a"), "truthy(\"a\")");
54+
assert(!truthy(""), "truthy(\"\")");
55+
assert(truthy(true), "truthy(true)");
56+
assert(!truthy(false), "truthy(false)");
57+
assert(!truthy(null), "truthy(null)");
58+
59+
assert(notTruthy(null), "!null");
60+
assert(notTruthy(0), "!0");
61+
assert(!notTruthy(3), "!3");
62+
assert(!notTruthy(true), "!true");
63+
64+
assert(andText(true, "x") == 1, "true && \"x\"");
65+
assert(andText(true, null) == 0, "true && null");
66+
assert(andText(false, "x") == 0, "false && \"x\"");
67+
68+
print("done.");
69+
}

0 commit comments

Comments
 (0)