From b029e68e9a99020346a29feaedf37a8d257b17f8 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 28 Sep 2026 12:10:28 +0100 Subject: [PATCH] A cast between unrelated classes needs their fields to fit `const b: B = new A(1)` with `A { v: number }` and `B { v: string }` compiled to a plain ts.Cast, and reading `b.v` read a number as a string and crashed. Two classes where neither extends the other are now compatible as in TypeScript, by their fields: one side's fields, inherited ones included, must all be found in the other with types that extend them (either direction, as for an assertion). A type guard's narrowing to an unrelated class (`isB(a)`) is checked at run time and keeps its cast. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenCast.cpp | 100 ++++++++++++++++++ tslang/lib/TypeScript/MLIRGenImpl.h | 17 ++- tslang/test/tester/CMakeLists.txt | 12 +++ tslang/test/tester/class-cast/unrelated.ts | 15 +++ .../tester/tests/00class_cast_structural.ts | 46 ++++++++ 5 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 tslang/test/tester/class-cast/unrelated.ts create mode 100644 tslang/test/tester/tests/00class_cast_structural.ts diff --git a/tslang/lib/TypeScript/MLIRGenCast.cpp b/tslang/lib/TypeScript/MLIRGenCast.cpp index aeb601759..81928e493 100644 --- a/tslang/lib/TypeScript/MLIRGenCast.cpp +++ b/tslang/lib/TypeScript/MLIRGenCast.cpp @@ -1565,9 +1565,109 @@ namespace mlirgen } } + if (mlir::failed(verifyUnrelatedClassCast(location, type, valueType))) + { + return mlir::failure(); + } + + return mlir::success(); + } + + // Two classes where neither extends the other are compatible by their members, as in + // TypeScript: `const b: B = new A()` with `A { v: number }` and `B { v: string }` compiled to a + // plain cast, and reading `b.v` read a number as a string and crashed. Like an assertion, the + // cast needs one side's fields to be found in the other with types that extend them. + mlir::LogicalResult MLIRGenImpl::verifyUnrelatedClassCast(mlir::Location location, mlir::Type type, mlir::Type valueType) + { + std::string mismatch; + if (areIncompatibleUnrelatedClasses(location, type, valueType, mismatch)) + { + emitError(location, "type ") << to_print(valueType) << " is not assignable to type " << to_print(type) << ": " << mismatch; + return mlir::failure(); + } + return mlir::success(); } + bool MLIRGenImpl::areIncompatibleUnrelatedClasses(mlir::Location location, mlir::Type type, mlir::Type valueType, std::string &mismatch) + { + auto classType = dyn_cast(type); + auto valueClassType = dyn_cast(valueType); + if (!classType || !valueClassType || classType == valueClassType + || mth.isGenericType(classType) || mth.isGenericType(valueClassType)) + { + return false; + } + + auto classInfo = getClassInfoByFullName(classType.getName().getValue()); + auto valueClassInfo = getClassInfoByFullName(valueClassType.getName().getValue()); + if (!classInfo || !valueClassInfo + || classInfo->hasBase(valueClassType) || valueClassInfo->hasBase(classType) + // two specializations of one generic compare by their type arguments + || (classInfo->originClassType && classInfo->originClassType == valueClassInfo->originClassType)) + { + return false; + } + + // the data fields, inherited ones included; not the internal ones (.vtbl) nor the storage + // a derived class embeds for its base + std::function &)> collectFields = + [&](ClassInfo::TypePtr info, llvm::StringMap &fields) { + for (auto &base : info->baseClasses) + { + collectFields(base, fields); + } + + auto storageType = dyn_cast(info->classType.getStorageType()); + if (!storageType) + { + return; + } + + for (auto &field : storageType.getFields()) + { + auto strId = dyn_cast_or_null(field.id); + if (!strId || strId.getValue().starts_with(".") + || llvm::any_of(info->baseClasses, [&](auto &base) { return strId.getValue() == base->fullName; })) + { + continue; + } + + fields[strId.getValue()] = field.type; + } + }; + + llvm::StringMap fields; + llvm::StringMap valueFields; + collectFields(classInfo, fields); + collectFields(valueClassInfo, valueFields); + + // every field of `to` is in `from`, of a type that extends it + auto fits = [&](llvm::StringMap &from, llvm::StringMap &to, std::string &mismatch) { + for (auto &field : to) + { + auto found = from.find(field.getKey()); + if (found == from.end()) + { + mismatch = "'" + field.getKey().str() + "' is missing"; + return false; + } + + llvm::StringMap> typeParamsWithArgs; + if (!isTrue(mth.extendsType(location, found->getValue(), field.getValue(), typeParamsWithArgs))) + { + mismatch = "'" + field.getKey().str() + "' is " + to_print(found->getValue()) + ", not " + to_print(field.getValue()); + return false; + } + } + + return true; + }; + + std::string reverseMismatch; + return !fits(valueFields, fields, mismatch) && !fits(fields, valueFields, reverseMismatch); + } + ValueOrLogicalResult MLIRGenImpl::castPrimitiveTypeFromAny(mlir::Location location, mlir::Type type, mlir::Value value, const GenContext &genContext) { // info, we add "_" extra as scanner append "_" in front of "__"; diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 0b72eee2f..866ade055 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -3441,8 +3441,19 @@ class MLIRGenImpl return mlir::success(); } - CAST_A(result, location, safeType, exprValue, genContext); - castedValue = V(result); + // A type guard narrows an A to an unrelated class B (`isB(a)`) where the value is + // checked at run time to be one - TypeScript narrows it to A & B. The cast that + // a plain assignment would be refused is right here. + std::string mismatch; + if (areIncompatibleUnrelatedClasses(location, safeType, exprValue.getType(), mismatch)) + { + castedValue = builder.create(location, safeType, exprValue); + } + else + { + CAST_A(result, location, safeType, exprValue, genContext); + castedValue = V(result); + } } LLVM_DEBUG(llvm::dbgs() << "\n!! Safe Type: [" << parameterName << "] is [" << safeType << "]\n"); @@ -11268,6 +11279,8 @@ class MLIRGenImpl // wrong casts // TODO: put it into Cast::Verify mlir::LogicalResult verifyCastCompatibility(mlir::Location location, mlir::Type type, mlir::Type valueType); + mlir::LogicalResult verifyUnrelatedClassCast(mlir::Location location, mlir::Type type, mlir::Type valueType); + bool areIncompatibleUnrelatedClasses(mlir::Location location, mlir::Type type, mlir::Type valueType, std::string &mismatch); ValueOrLogicalResult castPrimitiveTypeFromAny(mlir::Location location, mlir::Type type, mlir::Value value, const GenContext &genContext); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 22d6f2d72..a45c1f43d 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -605,6 +605,7 @@ tslang_add_test(NAME test-compile-01-symbol COMMAND test-runner "${PROJECT_SOURC tslang_add_test(NAME test-compile-00-as COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00as.ts") tslang_add_test(NAME test-compile-00-as-const COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00as_const.ts") tslang_add_test(NAME test-compile-00-type-guard-function COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00type_guard_function.ts") +tslang_add_test(NAME test-compile-00-class-cast-structural COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_cast_structural.ts") tslang_add_test(NAME test-compile-00-names-conflict COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00names_conflict.ts") tslang_add_test(NAME test-compile-00-generic-arguments-name-conflict COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00generic_arguments_name_conflict.ts") tslang_add_test(NAME test-compile-00-decorators COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00decorators.ts") @@ -1063,6 +1064,7 @@ tslang_add_test(NAME test-jit-01-symbol COMMAND test-runner -jit "${PROJECT_SOUR tslang_add_test(NAME test-jit-00-as COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00as.ts") tslang_add_test(NAME test-jit-00-as-const COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00as_const.ts") tslang_add_test(NAME test-jit-00-type-guard-function COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00type_guard_function.ts") +tslang_add_test(NAME test-jit-00-class-cast-structural COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_cast_structural.ts") tslang_add_test(NAME test-jit-00-names-conflict COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00names_conflict.ts") tslang_add_test(NAME test-jit-00-generic-arguments-name-conflict COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00generic_arguments_name_conflict.ts") tslang_add_test(NAME test-jit-00-decorators COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00decorators.ts") @@ -1825,6 +1827,7 @@ set(TSLANG_CORPUS 00tuple.ts 00type_aliases_in_generics.ts 00type_guard_function.ts + 00class_cast_structural.ts 00typed_array.ts 00typeof_function_narrowing.ts 00typeof_static_fold.ts @@ -2563,6 +2566,15 @@ set_tests_properties(test-compile-export-all-no-locals PROPERTIES PASS_REGULAR_EXPRESSION "let moduleLevel" FAIL_REGULAR_EXPRESSION "namespace [.]f_|Stack dump|error:") +# A cast between unrelated classes whose fields do not fit is an error; it compiled and crashed. +add_test(NAME test-compile-class-cast-unrelated-error + COMMAND $ --emit=obj --no-default-lib -mm=none + "${PROJECT_SOURCE_DIR}/test/tester/class-cast/unrelated.ts" + -o "${CMAKE_CURRENT_BINARY_DIR}/class-cast-unrelated.obj") +set_tests_properties(test-compile-class-cast-unrelated-error + PROPERTIES PASS_REGULAR_EXPRESSION "type A is not assignable to type B: 'v' is number, not string" + 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/class-cast/unrelated.ts b/tslang/test/tester/class-cast/unrelated.ts new file mode 100644 index 000000000..deac282fc --- /dev/null +++ b/tslang/test/tester/class-cast/unrelated.ts @@ -0,0 +1,15 @@ +// A and B are unrelated and their `v` fields have different types; the cast compiled, and reading +// b.v read a number as a string. +class A { + constructor(public v: number) {} +} + +class B { + constructor(public v: string) {} +} + +function main() { + const a = new A(1); + const b: B = a; + print(b.v); +} diff --git a/tslang/test/tester/tests/00class_cast_structural.ts b/tslang/test/tester/tests/00class_cast_structural.ts new file mode 100644 index 000000000..a5b5dce30 --- /dev/null +++ b/tslang/test/tester/tests/00class_cast_structural.ts @@ -0,0 +1,46 @@ +// Classes where neither extends the other are compatible by their fields, as in TypeScript. +class Point { + constructor(public x: number, public y: number) {} +} + +class Vec { + constructor(public x: number, public y: number) {} +} + +class Named { + name = "n"; +} + +class Dog { + bark() { + return 1; + } +} + +class Cat { + meow() { + return 2; + } +} + +function isCat(p: Dog | Cat): p is Cat { + return p instanceof Cat; +} + +function main() { + // the same fields + const p = new Point(1, 2); + const v: Vec = p; + assert(v.x == 1 && v.y == 2, "same shape"); + + // no fields on either side + const d = new Dog(); + if (isCat(d)) { + assert(false, "a dog is not a cat"); + } + + const n = new Named(); + assert(n.name == "n", "named"); + + print("done."); +}