diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h b/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h index aa59fce11..a06688a7f 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h @@ -2894,24 +2894,36 @@ class MLIRTypeHelper if (auto genericClass = getGenericClassInfoByFullName(srcClassInfo->originClassType.getName().getValue())) { + // every type argument has to extend, not only the first: + // Pair is not a Pair + auto result = ExtendsResult::True; for (auto &typeParam : genericClass->typeParams) { auto name = typeParam->getName(); auto srcFound = srcClassInfo->typeParamsWithArgs.find(name); auto extFound = extClassInfo->typeParamsWithArgs.find(name); - if (srcFound != srcClassInfo->typeParamsWithArgs.end() && - extFound != extClassInfo->typeParamsWithArgs.end()) + if (srcFound == srcClassInfo->typeParamsWithArgs.end() || + extFound == extClassInfo->typeParamsWithArgs.end()) { - auto srcType = srcFound->getValue().second; - auto extType = extFound->getValue().second; + return ExtendsResult::False; + } - return extendsType(location, srcType, extType, typeParamsWithArgs); + auto srcType = srcFound->getValue().second; + auto extType = extFound->getValue().second; + + auto argResult = extendsType(location, srcType, extType, typeParamsWithArgs); + if (!isTrue(argResult)) + { + return argResult; } - else + + if (argResult == ExtendsResult::Any) { - return ExtendsResult::False; + result = argResult; } } + + return result; } // default behavior - false, because something is different diff --git a/tslang/lib/TypeScript/MLIRGenCast.cpp b/tslang/lib/TypeScript/MLIRGenCast.cpp index 7375426e9..aeb601759 100644 --- a/tslang/lib/TypeScript/MLIRGenCast.cpp +++ b/tslang/lib/TypeScript/MLIRGenCast.cpp @@ -1515,6 +1515,30 @@ namespace mlirgen } } + // Two specializations of one generic class keep fields of different types in the same + // place: `const b: Box = new Box(1)` read a number as a string and crashed. + // One's type arguments have to extend the other's - Box as Box, or back as + // a downcast - which is TypeScript's rule for a type assertion as well. + if (auto classType = dyn_cast(type)) + { + if (auto valueClassType = dyn_cast(valueType)) + { + auto classInfo = getClassInfoByFullName(classType.getName().getValue()); + auto valueClassInfo = getClassInfoByFullName(valueClassType.getName().getValue()); + if (classInfo && valueClassInfo && classInfo->originClassType + && classInfo->originClassType == valueClassInfo->originClassType) + { + llvm::StringMap> typeParamsWithArgs; + if (!isTrue(mth.extendsType(location, valueType, type, typeParamsWithArgs)) + && !isTrue(mth.extendsType(location, type, valueType, typeParamsWithArgs))) + { + emitError(location, "type ") << to_print(valueType) << " is not assignable to type " << to_print(type); + return mlir::failure(); + } + } + } + } + if (isa(type) || isa(type)) { if (isa(valueType) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index c23b97214..30492ad5b 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -387,6 +387,7 @@ tslang_add_test(NAME test-compile-01-sizeof COMMAND test-runner "${PROJECT_SOURC tslang_add_test(NAME test-compile-02-sizeof COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/02sizeof.ts") tslang_add_test(NAME test-compile-00-new-delete COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00new_delete.ts") tslang_add_test(NAME test-compile-00-new-generic-from-receiver COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00new_generic_from_receiver.ts") +tslang_add_test(NAME test-compile-00-generic-specialization-cast COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00generic_specialization_cast.ts") tslang_add_test(NAME test-compile-00-owned-locals COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00owned_locals.ts") # The JIT variants are the ones that matter here: this shape was a JIT-only miscompile until # the catchable-type size was fixed (see the file header). Kept in all three models, since the @@ -838,6 +839,7 @@ tslang_add_test(NAME test-jit-01-sizeof COMMAND test-runner -jit "${PROJECT_SOUR tslang_add_test(NAME test-jit-02-sizeof COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/02sizeof.ts") tslang_add_test(NAME test-jit-00-new-delete COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00new_delete.ts") tslang_add_test(NAME test-jit-00-new-generic-from-receiver COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00new_generic_from_receiver.ts") +tslang_add_test(NAME test-jit-00-generic-specialization-cast COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00generic_specialization_cast.ts") tslang_add_test(NAME test-jit-00-owned-locals COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00owned_locals.ts") tslang_add_test(NAME test-jit-00-throw-in-catch COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00throw_in_catch.ts") tslang_add_test(NAME test-jit-00-throw-inlined COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00throw_inlined.ts") @@ -1711,6 +1713,7 @@ set(TSLANG_CORPUS 00try_mismatch_in_catch_clause_using.ts 00new_delete.ts 00new_generic_from_receiver.ts + 00generic_specialization_cast.ts 00ns_bug.ts 00ns.ts 00ns2.ts @@ -2501,6 +2504,24 @@ set_tests_properties(test-compile-declare-overloads-error PROPERTIES PASS_REGULAR_EXPRESSION "'abs' is declared again with another signature" FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed|redefinition of symbol") +# A cast between two specializations of one generic class whose type arguments don't extend each +# other is an error - it compiled, and then read one field type as another. +add_test(NAME test-compile-specialization-cast-box-error + COMMAND $ --emit=obj --no-default-lib -mm=none + "${PROJECT_SOURCE_DIR}/test/tester/specialization-cast/box.ts" + -o "${CMAKE_CURRENT_BINARY_DIR}/specialization-cast-box.obj") +set_tests_properties(test-compile-specialization-cast-box-error + PROPERTIES PASS_REGULAR_EXPRESSION "Box is not assignable to type Box" + FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed") + +add_test(NAME test-compile-specialization-cast-pair-error + COMMAND $ --emit=obj --no-default-lib -mm=none + "${PROJECT_SOURCE_DIR}/test/tester/specialization-cast/pair.ts" + -o "${CMAKE_CURRENT_BINARY_DIR}/specialization-cast-pair.obj") +set_tests_properties(test-compile-specialization-cast-pair-error + PROPERTIES PASS_REGULAR_EXPRESSION "is not assignable to type Pair" + FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed") + # The shared-component tier under the other two models. A shared library records the model # it was built under, so both halves of a pair are built with the same flag - which is what # these run. The file pairs are the default model's, verbatim. diff --git a/tslang/test/tester/specialization-cast/box.ts b/tslang/test/tester/specialization-cast/box.ts new file mode 100644 index 000000000..39d88f972 --- /dev/null +++ b/tslang/test/tester/specialization-cast/box.ts @@ -0,0 +1,10 @@ +// Box and Box keep a different type in `v`; reading one as the other crashed. +class Box { + constructor(public v: T) {} +} + +function main() { + const a = new Box(1); + const b: Box = a; + print(b.v); +} diff --git a/tslang/test/tester/specialization-cast/pair.ts b/tslang/test/tester/specialization-cast/pair.ts new file mode 100644 index 000000000..e3fdf19b5 --- /dev/null +++ b/tslang/test/tester/specialization-cast/pair.ts @@ -0,0 +1,10 @@ +// Every type argument has to match, not only the first. +class Pair { + constructor(public a: A, public b: B) {} +} + +function main() { + const p = new Pair(1, 2); + const q: Pair = p; + print(q.b); +} diff --git a/tslang/test/tester/tests/00generic_specialization_cast.ts b/tslang/test/tester/tests/00generic_specialization_cast.ts new file mode 100644 index 000000000..61809f2ac --- /dev/null +++ b/tslang/test/tester/tests/00generic_specialization_cast.ts @@ -0,0 +1,34 @@ +// A cast between two specializations of one generic class is allowed when one's type arguments +// extend the other's: up (Box as Box) and back down again. +class Animal { + name = "animal"; +} + +class Dog extends Animal { + bark() { + return 1; + } +} + +class Box { + constructor(public v: T) {} +} + +class Pair { + constructor(public a: A, public b: B) {} +} + +function main() { + const d = new Box(new Dog()); + const up: Box = d; + assert(up.v.name == "animal", "up"); + + const down = >up; + assert(down.v.bark() == 1, "down"); + + const p = new Pair(new Dog(), 2); + const q: Pair = p; + assert(q.b == 2, "pair"); + + print("done."); +}