diff --git a/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h b/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h index 8edb85217..979ee8513 100644 --- a/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h +++ b/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h @@ -628,7 +628,35 @@ class CastLogicHelper bool needTag = mth.isUnionTypeNeedsTag(loc, resUnionType, baseType); if (needTag) { - auto typeOfValue = rewriter.create(loc, mlir_ts::StringType::get(rewriter.getContext()), in); + auto stringType = mlir_ts::StringType::get(rewriter.getContext()); + mlir::Value typeOfValue = rewriter.create(loc, stringType, in); + + // A class, string, array or object value that holds null is null: where the union + // has a null member of its own, that member is what it becomes. Tagged as its type + // instead, a null `Handle | null` widened to `boolean | Handle | null` was a class + // holding a null pointer, which `=== null` did not see and `instanceof` read through. + auto storageTypes = mlir::cast(baseType).getTypes(); + if (isPointerNullableType(inType) + && llvm::any_of(storageTypes, [](mlir::Type type) { return isa(type); })) + { + auto llvmStringType = tch.convertType(stringType); + auto nullValue = rewriter.create(loc, mlir_ts::NullType::get(rewriter.getContext())); + auto nullTag = rewriter.create(loc, stringType, nullValue); + + mlir::Value pointer = in; + if (!isa(pointer.getType())) + { + pointer = rewriter.create(loc, th.getPtrType(), in); + } + + auto isNull = rewriter.create(loc, LLVM::ICmpPredicate::eq, pointer, + rewriter.create(loc, th.getPtrType())); + auto tag = rewriter.create( + loc, isNull, rewriter.create(loc, llvmStringType, nullTag), + rewriter.create(loc, llvmStringType, typeOfValue)); + typeOfValue = rewriter.create(loc, stringType, tag); + } + auto unionValue = rewriter.create(loc, resUnionType, in, typeOfValue); return unionValue; } @@ -680,6 +708,12 @@ class CastLogicHelper return mlir::Value(); } + // A type whose value is a single pointer that is null when it holds null. + static bool isPointerNullableType(mlir::Type type) + { + return isa(type); + } + bool isBool(mlir::Type type) { return type.isInteger(1); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 716985222..74d71b5c8 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -409,6 +409,7 @@ tslang_add_test(NAME test-compile-00-union-to-any COMMAND test-runner "${PROJECT tslang_add_test(NAME test-compile-00-union-errors COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_errors.ts") 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") tslang_add_test(NAME test-compile-00-union-null-truthiness COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_null_truthiness.ts") +tslang_add_test(NAME test-compile-00-null-into-tagged-union COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00null_into_tagged_union.ts") tslang_add_test(NAME test-compile-00-union-null-undefined COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_null_undefined.ts") 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") tslang_add_test(NAME test-compile-00-optional-number-compare COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00optional_number_compare.ts") @@ -936,6 +937,7 @@ tslang_add_test(NAME test-jit-00-union-to-any COMMAND test-runner -jit "${PROJEC tslang_add_test(NAME test-jit-00-union-errors COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_errors.ts") 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") tslang_add_test(NAME test-jit-00-union-null-truthiness COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_null_truthiness.ts") +tslang_add_test(NAME test-jit-00-null-into-tagged-union COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00null_into_tagged_union.ts") tslang_add_test(NAME test-jit-00-union-null-undefined COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_null_undefined.ts") 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") tslang_add_test(NAME test-jit-00-optional-number-compare COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00optional_number_compare.ts") @@ -2052,6 +2054,7 @@ set(TSLANG_CORPUS 00union_errors.ts 00union_null_to_string.ts 00union_null_truthiness.ts + 00null_into_tagged_union.ts 00union_ops.ts 00union_to_any.ts 00union_type.ts diff --git a/tslang/test/tester/tests/00null_into_tagged_union.ts b/tslang/test/tester/tests/00null_into_tagged_union.ts new file mode 100644 index 000000000..e80c8022d --- /dev/null +++ b/tslang/test/tester/tests/00null_into_tagged_union.ts @@ -0,0 +1,62 @@ +// A class, string or array value holding null that is widened to a union with a null member of its +// own becomes that null member. It was tagged as its type instead - a class holding a null pointer - +// so `=== null` missed it, `typeof` said "class", and a truth test read its vtable through the null. + +class A { + a = 1; +} + +class B { + b = 2; +} + +function widenClass(h: A | null): boolean | A | B | null { + return h; +} + +function widenString(s: string | null): string | number | null { + return s; +} + +function widenArray(v: number[] | null): number[] | boolean | null { + return v; +} + +function classes() { + const n = widenClass(null); + assert(n === null, "null A === null"); + assert(!(n !== null), "null A !== null"); + assert(typeof n == "null", "typeof null A"); + assert(!n, "null A is falsy"); + assert((n ?? true) === true, "null A ?? true"); + + const v = widenClass(new A()); + assert(v !== null, "A !== null"); + assert(typeof v == "class", "typeof A"); + assert(!!v, "A is truthy"); +} + +function strings() { + const n = widenString(null); + assert(n === null, "null string === null"); + assert(typeof n == "null", "typeof null string"); + + const s = widenString("x"); + assert(s !== null, "string !== null"); + assert(typeof s == "string", "typeof string"); +} + +function arrays() { + const n = widenArray(null); + assert(n === null, "null array === null"); + + const v = widenArray([1, 2]); + assert(v !== null, "array !== null"); +} + +function main() { + classes(); + strings(); + arrays(); + print("done."); +}