From 7aa6ad42340d31cc790e66192f9d9cfbaf78d428 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 31 Oct 2026 21:45:30 +0000 Subject: [PATCH] A @cpp class | null tested for truth with a boolean (h && cond) no longer asks instanceof `h && n == 3` with h a `Handle | null` (@cpp) is a union of the class, null and boolean; castFromUnion's ___cast helper tested each class member with instanceof, which a @cpp class refuses (its objects carry no type info). When the class need not be told apart - the only class of the union, or the union tested for truth - typeof has narrowed t enough: `return t`. Otherwise only the non-@cpp classes are tested. TSLANG_ISSUES 23. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenCast.cpp | 52 +++++++++------- tslang/test/tester/CMakeLists.txt | 5 ++ tslang/test/tester/tests/00cpp_union_truth.ts | 59 +++++++++++++++++++ 3 files changed, 96 insertions(+), 20 deletions(-) create mode 100644 tslang/test/tester/tests/00cpp_union_truth.ts diff --git a/tslang/lib/TypeScript/MLIRGenCast.cpp b/tslang/lib/TypeScript/MLIRGenCast.cpp index c4025fe17..23b5dc72b 100644 --- a/tslang/lib/TypeScript/MLIRGenCast.cpp +++ b/tslang/lib/TypeScript/MLIRGenCast.cpp @@ -3177,6 +3177,36 @@ namespace mlirgen auto toBoolean = isa(type) || isa(type); + // The tag says "class"; `instanceof` tells the classes apart. A @cpp class's objects + // are C++ objects with no type information, so `instanceof` refuses them: when the + // class need not be told - the only class of the union, or tested for truth, as + // `h && n == 3` with h a `Handle | null` is - `typeof` has already narrowed t enough. + // Otherwise only the other classes are tested. + auto writeClassTests = [&]() { + auto hasCppClass = llvm::any_of(classInstances, [&](mlir::Type classType) { return isCppVTableClass(classType); }); + if (hasCppClass && (toBoolean || classInstances.size() == 1)) + { + ss << S("return t;\n"); + return; + } + + ss << S("{ \n"); + + for (auto [index, classType] : enumerate(classInstances)) + { + if (isCppVTableClass(classType)) + { + continue; + } + + ss << S("if (t instanceof TYPE_INST_ALIAS"); + ss << index; + ss << S(") return t;\n"); + } + + ss << S(" }\n"); + }; + // A union whose actual type has no null member but whose members all hold null in // their own pointer (`A | B | null`) is null when that pointer is null, whatever its // tag: asked first, so that no member's test - `instanceof` reads the vtable - meets a @@ -3216,16 +3246,7 @@ namespace mlirgen ss << S("') "); if (pair.getKey() == "class") { - ss << S("{ \n"); - - for (auto [index, _] : enumerate(classInstances)) - { - ss << S("if (t instanceof TYPE_INST_ALIAS"); - ss << index; - ss << S(") return t;\n"); - } - - ss << S(" }\n"); + writeClassTests(); } else { @@ -3261,16 +3282,7 @@ namespace mlirgen } else if (pair.getKey() == "class") { - ss << S("{ \n"); - - for (auto [index, _] : enumerate(classInstances)) - { - ss << S("if (t instanceof TYPE_INST_ALIAS"); - ss << index; - ss << S(") return t;\n"); - } - - ss << S(" }\n"); + writeClassTests(); } else { diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 63898ecf6..28cfd8b04 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -487,6 +487,8 @@ tslang_add_test(NAME test-compile-own-00-cpp-slot COMMAND test-runner -mm=own "$ tslang_add_test(NAME test-compile-own-00-cpp-overloads COMMAND test-runner -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00cpp_overloads.ts") tslang_add_test(NAME test-compile-00-cpp-slot COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00cpp_slot.ts") tslang_add_test(NAME test-compile-00-cpp-overloads COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00cpp_overloads.ts") +tslang_add_test(NAME test-compile-00-cpp-union-truth COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00cpp_union_truth.ts") +tslang_add_test(NAME test-compile-own-00-cpp-union-truth COMMAND test-runner -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00cpp_union_truth.ts") tslang_add_test(NAME test-compile-00-union-value-equality COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_value_equality.ts") tslang_add_test(NAME test-compile-00-strict-equals-mismatched-types COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00strict_equals_mismatched_types.ts") tslang_add_test(NAME test-compile-00-any-number-compare COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00any_number_compare.ts") @@ -1139,6 +1141,8 @@ tslang_add_test(NAME test-jit-00-cpp-overloads COMMAND test-runner -jit "${PROJE tslang_add_test(NAME test-jit-own-00-cppvtable-class COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00cppvtable_class.ts") tslang_add_test(NAME test-jit-own-00-cpp-slot COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00cpp_slot.ts") tslang_add_test(NAME test-jit-own-00-cpp-overloads COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00cpp_overloads.ts") +tslang_add_test(NAME test-jit-00-cpp-union-truth COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00cpp_union_truth.ts") +tslang_add_test(NAME test-jit-own-00-cpp-union-truth COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00cpp_union_truth.ts") tslang_add_test(NAME test-jit-00-union-value-equality COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_value_equality.ts") tslang_add_test(NAME test-jit-00-strict-equals-mismatched-types COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00strict_equals_mismatched_types.ts") tslang_add_test(NAME test-jit-00-any-number-compare COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00any_number_compare.ts") @@ -2747,6 +2751,7 @@ set(TSLANG_CORPUS 00cppvtable_class.ts 00cpp_slot.ts 00cpp_overloads.ts + 00cpp_union_truth.ts ) # the default model runs these ahead of time only, so the other models do too diff --git a/tslang/test/tester/tests/00cpp_union_truth.ts b/tslang/test/tester/tests/00cpp_union_truth.ts new file mode 100644 index 000000000..e0d60b8c2 --- /dev/null +++ b/tslang/test/tester/tests/00cpp_union_truth.ts @@ -0,0 +1,59 @@ +// A @cpp class that may be null in a union with a boolean (`h && n == 3`) is tested for truth by +// its pointer alone: its objects are C++ objects, and `instanceof` can't ask them what they are. +// The "C++ object" is made by hand, as in tests/00cppvtable_class.ts. + +@cpp +class Handle { + value: s32; + @slot(0) first(): s32; +} + +@cpp +class Other { + @slot(0) first(): s32; +} + +type ObjectData = [vtable: Opaque, value: s32]; + +function slot0(self: Opaque): s32 { + return 10; +} + +function both(h: Handle | null, n: number): boolean { + if (h && n == 3) { + return true; + } + + return false; +} + +function either(enabled: boolean, h: Handle | null): boolean { + return enabled && h ? true : false; +} + +function main() { + let vtable: Opaque[] = [slot0]; + let data: ObjectData = [Ref(vtable[0]), 32]; + const object = Ref(data); + + const handle = object; + const none: Handle | null = null; + + assert(both(handle, 3), "a handle and true"); + assert(!both(handle, 4), "a handle and false"); + assert(!both(none, 3), "null and true"); + + assert(either(true, handle), "true and a handle"); + assert(!either(false, handle), "false and a handle"); + assert(!either(true, none), "true and null"); + + // two @cpp classes and a boolean: neither class needs telling apart to be tested for truth + const other = object; + const flag = true; + let a: Handle | Other | boolean | null = flag && other; + assert(a ? true : false, "a @cpp object of either class"); + a = null; + assert(!(a ? true : false), "null"); + + print("done."); +}