// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/TypePath.h" #include "Luau/Normalize.h" #include "Luau/Subtyping.h" #include "Luau/Type.h" #include "Luau/TypePack.h" #include "doctest.h" #include "Fixture.h" #include "RegisterCallbacks.h" #include using namespace Luau; namespace Luau { std::ostream& operator<<(std::ostream& lhs, const SubtypingVariance& variance) { switch (variance) { case SubtypingVariance::Covariant: return lhs << "covariant"; case SubtypingVariance::Contravariant: return lhs << "contravariant"; case SubtypingVariance::Invariant: return lhs << "invariant"; case SubtypingVariance::Invalid: return lhs << "*invalid*"; } return lhs; } std::ostream& operator<<(std::ostream& lhs, const SubtypingReasoning& reasoning) { return lhs << toString(reasoning.subPath) << " & set, const std::vector& items) { if (items.size() != set.size()) return false; for (const SubtypingReasoning& r : items) { if (!set.contains(r)) return false; } return true; } }; // namespace Luau struct SubtypeFixture : Fixture { TypeArena arena; InternalErrorReporter iceReporter; UnifierSharedState sharedState{&ice}; Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}}; ScopePtr rootScope{new Scope(builtinTypes->emptyTypePack)}; ScopePtr moduleScope{new Scope(rootScope)}; Subtyping subtyping = mkSubtyping(rootScope); Subtyping mkSubtyping(const ScopePtr& scope) { return Subtyping{builtinTypes, NotNull{&arena}, NotNull{&normalizer}, NotNull{&iceReporter}, NotNull{scope.get()}}; } TypePackId pack(std::initializer_list tys) { return arena.addTypePack(tys); } TypePackId pack(std::initializer_list tys, TypePackVariant tail) { return arena.addTypePack(tys, arena.addTypePack(std::move(tail))); } TypeId fn(std::initializer_list args, std::initializer_list rets) { return arena.addType(FunctionType{pack(args), pack(rets)}); } TypeId fn(std::initializer_list argHead, TypePackVariant argTail, std::initializer_list rets) { return arena.addType(FunctionType{pack(argHead, std::move(argTail)), pack(rets)}); } TypeId fn(std::initializer_list args, std::initializer_list retHead, TypePackVariant retTail) { return arena.addType(FunctionType{pack(args), pack(retHead, std::move(retTail))}); } TypeId fn(std::initializer_list argHead, TypePackVariant argTail, std::initializer_list retHead, TypePackVariant retTail) { return arena.addType(FunctionType{pack(argHead, std::move(argTail)), pack(retHead, std::move(retTail))}); } TypeId tbl(TableType::Props&& props) { return arena.addType(TableType{std::move(props), std::nullopt, {}, TableState::Sealed}); } TypeId idx(TypeId keyTy, TypeId valueTy) { return arena.addType(TableType{{}, TableIndexer{keyTy, valueTy}, {}, TableState::Sealed}); } // `&` TypeId meet(TypeId a, TypeId b) { return arena.addType(IntersectionType{{a, b}}); } // `|` TypeId join(TypeId a, TypeId b) { return arena.addType(UnionType{{a, b}}); } // `~` TypeId negate(TypeId ty) { return arena.addType(NegationType{ty}); } // "literal" TypeId str(const char* literal) { return arena.addType(SingletonType{StringSingleton{literal}}); } TypeId cls(const std::string& name, std::optional parent = std::nullopt) { return arena.addType(ClassType{name, {}, parent.value_or(builtinTypes->classType), {}, {}, nullptr, ""}); } TypeId cls(const std::string& name, ClassType::Props&& props) { TypeId ty = cls(name); getMutable(ty)->props = std::move(props); return ty; } TypeId opt(TypeId ty) { return join(ty, builtinTypes->nilType); } TypeId cyclicTable(std::function&& cb) { TypeId res = arena.addType(GenericType{}); TableType tt{}; cb(res, &tt); emplaceType(asMutable(res), std::move(tt)); return res; } TypeId meta(TableType::Props&& metaProps, TableType::Props&& tableProps = {}) { return arena.addType(MetatableType{tbl(std::move(tableProps)), tbl(std::move(metaProps))}); } TypeId genericT = arena.addType(GenericType{moduleScope.get(), "T"}); TypeId genericU = arena.addType(GenericType{moduleScope.get(), "U"}); TypePackId genericAs = arena.addTypePack(GenericTypePack{"A"}); TypePackId genericBs = arena.addTypePack(GenericTypePack{"B"}); TypePackId genericCs = arena.addTypePack(GenericTypePack{"C"}); SubtypingResult isSubtype(TypeId subTy, TypeId superTy) { return subtyping.isSubtype(subTy, superTy); } TypeId helloType = arena.addType(SingletonType{StringSingleton{"hello"}}); TypeId helloType2 = arena.addType(SingletonType{StringSingleton{"hello"}}); TypeId worldType = arena.addType(SingletonType{StringSingleton{"world"}}); TypeId aType = arena.addType(SingletonType{StringSingleton{"a"}}); TypeId bType = arena.addType(SingletonType{StringSingleton{"b"}}); TypeId trueSingleton = arena.addType(SingletonType{BooleanSingleton{true}}); TypeId falseSingleton = arena.addType(SingletonType{BooleanSingleton{false}}); TypeId helloOrWorldType = join(helloType, worldType); TypeId trueOrFalseType = join(builtinTypes->trueType, builtinTypes->falseType); TypeId helloAndWorldType = meet(helloType, worldType); TypeId booleanAndTrueType = meet(builtinTypes->booleanType, builtinTypes->trueType); /** * class * \- Root * |- Child * | |-GrandchildOne * | \-GrandchildTwo * \- AnotherChild * |- AnotherGrandchildOne * \- AnotherGrandchildTwo */ TypeId rootClass = cls("Root"); TypeId childClass = cls("Child", rootClass); TypeId grandchildOneClass = cls("GrandchildOne", childClass); TypeId grandchildTwoClass = cls("GrandchildTwo", childClass); TypeId anotherChildClass = cls("AnotherChild", rootClass); TypeId anotherGrandchildOneClass = cls("AnotherGrandchildOne", anotherChildClass); TypeId anotherGrandchildTwoClass = cls("AnotherGrandchildTwo", anotherChildClass); TypeId vec2Class = cls("Vec2", { {"X", builtinTypes->numberType}, {"Y", builtinTypes->numberType}, }); // "hello" | "hello" TypeId helloOrHelloType = arena.addType(UnionType{{helloType, helloType}}); // () -> () const TypeId nothingToNothingType = fn({}, {}); // (number) -> string const TypeId numberToStringType = fn({builtinTypes->numberType}, {builtinTypes->stringType}); // (unknown) -> string const TypeId unknownToStringType = fn({builtinTypes->unknownType}, {builtinTypes->stringType}); // (number) -> () const TypeId numberToNothingType = fn({builtinTypes->numberType}, {}); // () -> number const TypeId nothingToNumberType = fn({}, {builtinTypes->numberType}); // (number) -> number const TypeId numberToNumberType = fn({builtinTypes->numberType}, {builtinTypes->numberType}); // (number) -> unknown const TypeId numberToUnknownType = fn({builtinTypes->numberType}, {builtinTypes->unknownType}); // (number) -> (string, string) const TypeId numberToTwoStringsType = fn({builtinTypes->numberType}, {builtinTypes->stringType, builtinTypes->stringType}); // (number) -> (string, unknown) const TypeId numberToStringAndUnknownType = fn({builtinTypes->numberType}, {builtinTypes->stringType, builtinTypes->unknownType}); // (number, number) -> string const TypeId numberNumberToStringType = fn({builtinTypes->numberType, builtinTypes->numberType}, {builtinTypes->stringType}); // (unknown, number) -> string const TypeId unknownNumberToStringType = fn({builtinTypes->unknownType, builtinTypes->numberType}, {builtinTypes->stringType}); // (number, string) -> string const TypeId numberAndStringToStringType = fn({builtinTypes->numberType, builtinTypes->stringType}, {builtinTypes->stringType}); // (number, ...string) -> string const TypeId numberAndStringsToStringType = fn({builtinTypes->numberType}, VariadicTypePack{builtinTypes->stringType}, {builtinTypes->stringType}); // (number, ...string?) -> string const TypeId numberAndOptionalStringsToStringType = fn({builtinTypes->numberType}, VariadicTypePack{builtinTypes->optionalStringType}, {builtinTypes->stringType}); // (...number) -> number const TypeId numbersToNumberType = arena.addType(FunctionType{arena.addTypePack(VariadicTypePack{builtinTypes->numberType}), arena.addTypePack({builtinTypes->numberType})}); // (T) -> () const TypeId genericTToNothingType = arena.addType(FunctionType{{genericT}, {}, arena.addTypePack({genericT}), builtinTypes->emptyTypePack}); // (T) -> T const TypeId genericTToTType = arena.addType(FunctionType{{genericT}, {}, arena.addTypePack({genericT}), arena.addTypePack({genericT})}); // (U) -> () const TypeId genericUToNothingType = arena.addType(FunctionType{{genericU}, {}, arena.addTypePack({genericU}), builtinTypes->emptyTypePack}); // () -> T const TypeId genericNothingToTType = arena.addType(FunctionType{{genericT}, {}, builtinTypes->emptyTypePack, arena.addTypePack({genericT})}); // (A...) -> A... const TypeId genericAsToAsType = arena.addType(FunctionType{{}, {genericAs}, genericAs, genericAs}); // (A...) -> number const TypeId genericAsToNumberType = arena.addType(FunctionType{{}, {genericAs}, genericAs, arena.addTypePack({builtinTypes->numberType})}); // (B...) -> B... const TypeId genericBsToBsType = arena.addType(FunctionType{{}, {genericBs}, genericBs, genericBs}); // (B...) -> C... const TypeId genericBsToCsType = arena.addType(FunctionType{{}, {genericBs, genericCs}, genericBs, genericCs}); // () -> A... const TypeId genericNothingToAsType = arena.addType(FunctionType{{}, {genericAs}, builtinTypes->emptyTypePack, genericAs}); // { lower : string -> string } TypeId tableWithLower = tbl(TableType::Props{{"lower", fn({builtinTypes->stringType}, {builtinTypes->stringType})}}); // { insaneThingNoScalarHas : () -> () } TypeId tableWithoutScalarProp = tbl(TableType::Props{{"insaneThingNoScalarHas", fn({}, {})}}); }; #define CHECK_IS_SUBTYPE(left, right) \ do \ { \ const auto& leftTy = (left); \ const auto& rightTy = (right); \ SubtypingResult result = isSubtype(leftTy, rightTy); \ CHECK_MESSAGE(result.isSubtype, "Expected " << leftTy << " <: " << rightTy); \ } while (0) #define CHECK_IS_NOT_SUBTYPE(left, right) \ do \ { \ const auto& leftTy = (left); \ const auto& rightTy = (right); \ SubtypingResult result = isSubtype(leftTy, rightTy); \ CHECK_MESSAGE(!result.isSubtype, "Expected " << leftTy << " numberType, builtinTypes->anyType); TEST_IS_NOT_SUBTYPE(builtinTypes->numberType, builtinTypes->stringType); TEST_CASE_FIXTURE(SubtypeFixture, "any anyType, builtinTypes->unknownType); CHECK(!result.isSubtype); CHECK(result.isErrorSuppressing); } TEST_CASE_FIXTURE(SubtypeFixture, "number? <: unknown") { CHECK_IS_SUBTYPE(builtinTypes->optionalNumberType, builtinTypes->unknownType); } TEST_CASE_FIXTURE(SubtypeFixture, "number <: unknown") { CHECK_IS_SUBTYPE(builtinTypes->numberType, builtinTypes->unknownType); } TEST_CASE_FIXTURE(SubtypeFixture, "number <: number") { CHECK_IS_SUBTYPE(builtinTypes->numberType, builtinTypes->numberType); } TEST_CASE_FIXTURE(SubtypeFixture, "number <: number?") { CHECK_IS_SUBTYPE(builtinTypes->numberType, builtinTypes->optionalNumberType); } TEST_CASE_FIXTURE(SubtypeFixture, "\"hello\" <: string") { CHECK_IS_SUBTYPE(helloType, builtinTypes->stringType); } TEST_CASE_FIXTURE(SubtypeFixture, "string stringType, helloType); } TEST_CASE_FIXTURE(SubtypeFixture, "\"hello\" <: \"hello\"") { CHECK_IS_SUBTYPE(helloType, helloType2); } TEST_CASE_FIXTURE(SubtypeFixture, "true <: boolean") { CHECK_IS_SUBTYPE(builtinTypes->trueType, builtinTypes->booleanType); } TEST_CASE_FIXTURE(SubtypeFixture, "true <: true | false") { CHECK_IS_SUBTYPE(builtinTypes->trueType, trueOrFalseType); } TEST_CASE_FIXTURE(SubtypeFixture, "true | false trueType); } TEST_CASE_FIXTURE(SubtypeFixture, "true | false <: boolean") { CHECK_IS_SUBTYPE(trueOrFalseType, builtinTypes->booleanType); } TEST_CASE_FIXTURE(SubtypeFixture, "true | false <: true | false") { CHECK_IS_SUBTYPE(trueOrFalseType, trueOrFalseType); } TEST_CASE_FIXTURE(SubtypeFixture, "\"hello\" | \"world\" <: number") { CHECK_IS_NOT_SUBTYPE(helloOrWorldType, builtinTypes->numberType); } TEST_CASE_FIXTURE(SubtypeFixture, "string stringType, helloOrHelloType); } TEST_CASE_FIXTURE(SubtypeFixture, "true <: boolean & true") { CHECK_IS_SUBTYPE(builtinTypes->trueType, booleanAndTrueType); } TEST_CASE_FIXTURE(SubtypeFixture, "boolean & true <: true") { CHECK_IS_SUBTYPE(booleanAndTrueType, builtinTypes->trueType); } TEST_CASE_FIXTURE(SubtypeFixture, "boolean & true <: boolean & true") { CHECK_IS_SUBTYPE(booleanAndTrueType, booleanAndTrueType); } TEST_CASE_FIXTURE(SubtypeFixture, "\"hello\" & \"world\" <: number") { CHECK_IS_SUBTYPE(helloAndWorldType, builtinTypes->numberType); } TEST_CASE_FIXTURE(SubtypeFixture, "false falseType, booleanAndTrueType); } TEST_CASE_FIXTURE(SubtypeFixture, "(unknown) -> string <: (number) -> string") { CHECK_IS_SUBTYPE(unknownToStringType, numberToStringType); } TEST_CASE_FIXTURE(SubtypeFixture, "(number) -> string string") { CHECK_IS_NOT_SUBTYPE(numberToStringType, unknownToStringType); } TEST_CASE_FIXTURE(SubtypeFixture, "(number, number) -> string string") { CHECK_IS_NOT_SUBTYPE(numberNumberToStringType, numberToStringType); } TEST_CASE_FIXTURE(SubtypeFixture, "(number) -> string string") { CHECK_IS_NOT_SUBTYPE(numberToStringType, numberNumberToStringType); } TEST_CASE_FIXTURE(SubtypeFixture, "(number, number) -> string string") { CHECK_IS_NOT_SUBTYPE(numberNumberToStringType, unknownNumberToStringType); } TEST_CASE_FIXTURE(SubtypeFixture, "(unknown, number) -> string <: (number, number) -> string") { CHECK_IS_SUBTYPE(unknownNumberToStringType, numberNumberToStringType); } TEST_CASE_FIXTURE(SubtypeFixture, "(number) -> (string, unknown) (string, string)") { CHECK_IS_NOT_SUBTYPE(numberToStringAndUnknownType, numberToTwoStringsType); } TEST_CASE_FIXTURE(SubtypeFixture, "(number) -> (string, string) <: (number) -> (string, unknown)") { CHECK_IS_SUBTYPE(numberToTwoStringsType, numberToStringAndUnknownType); } TEST_CASE_FIXTURE(SubtypeFixture, "(number) -> (string, string) string") { CHECK_IS_NOT_SUBTYPE(numberToTwoStringsType, numberToStringType); } TEST_CASE_FIXTURE(SubtypeFixture, "(number) -> string (string, string)") { CHECK_IS_NOT_SUBTYPE(numberToStringType, numberToTwoStringsType); } TEST_CASE_FIXTURE(SubtypeFixture, "(number, ...string) -> string <: (number) -> string") { CHECK_IS_SUBTYPE(numberAndStringsToStringType, numberToStringType); } TEST_CASE_FIXTURE(SubtypeFixture, "(number) -> string string") { CHECK_IS_NOT_SUBTYPE(numberToStringType, numberAndStringsToStringType); } TEST_CASE_FIXTURE(SubtypeFixture, "(number, ...string?) -> string <: (number, ...string) -> string") { CHECK_IS_SUBTYPE(numberAndOptionalStringsToStringType, numberAndStringsToStringType); } TEST_CASE_FIXTURE(SubtypeFixture, "(number, ...string) -> string string") { CHECK_IS_NOT_SUBTYPE(numberAndStringsToStringType, numberAndOptionalStringsToStringType); } TEST_CASE_FIXTURE(SubtypeFixture, "(number, ...string) -> string <: (number, string) -> string") { CHECK_IS_SUBTYPE(numberAndStringsToStringType, numberAndStringToStringType); } TEST_CASE_FIXTURE(SubtypeFixture, "(number, string) -> string string") { CHECK_IS_NOT_SUBTYPE(numberAndStringToStringType, numberAndStringsToStringType); } /* * (A) -> A <: (X) -> X * A can be bound to X. * * (A) -> A (X) -> number * A can be bound to X, but A number (A) -> A * Only generics on the left side can be bound. * number (A, B) -> boolean <: (X, X) -> boolean * It is ok to bind both A and B to X. * * (A, A) -> boolean (X, Y) -> boolean * A cannot be bound to both X and Y. */ TEST_CASE_FIXTURE(SubtypeFixture, "() -> T <: () -> number") { CHECK_IS_SUBTYPE(genericNothingToTType, nothingToNumberType); } TEST_CASE_FIXTURE(SubtypeFixture, "(T) -> () <: (U) -> ()") { CHECK_IS_SUBTYPE(genericTToNothingType, genericUToNothingType); } TEST_CASE_FIXTURE(SubtypeFixture, "() -> number () -> T") { CHECK_IS_NOT_SUBTYPE(nothingToNumberType, genericNothingToTType); } TEST_CASE_FIXTURE(SubtypeFixture, "(T) -> () <: (number) -> ()") { CHECK_IS_SUBTYPE(genericTToNothingType, numberToNothingType); } TEST_CASE_FIXTURE(SubtypeFixture, "(T) -> T <: (number) -> number") { CHECK_IS_SUBTYPE(genericTToTType, numberToNumberType); } TEST_CASE_FIXTURE(SubtypeFixture, "(T) -> T string") { CHECK_IS_NOT_SUBTYPE(genericTToTType, numberToStringType); } TEST_CASE_FIXTURE(SubtypeFixture, "(T) -> () <: (U) -> ()") { CHECK_IS_SUBTYPE(genericTToNothingType, genericUToNothingType); } TEST_CASE_FIXTURE(SubtypeFixture, "(number) -> () (T) -> ()") { CHECK_IS_NOT_SUBTYPE(numberToNothingType, genericTToNothingType); } TEST_CASE_FIXTURE(SubtypeFixture, "() -> (T, T) (string, number)") { TypeId nothingToTwoTs = arena.addType(FunctionType{{genericT}, {}, builtinTypes->emptyTypePack, arena.addTypePack({genericT, genericT})}); TypeId nothingToStringAndNumber = fn({}, {builtinTypes->stringType, builtinTypes->numberType}); CHECK_IS_NOT_SUBTYPE(nothingToTwoTs, nothingToStringAndNumber); } TEST_CASE_FIXTURE(SubtypeFixture, "(A...) -> A... <: (number) -> number") { CHECK_IS_SUBTYPE(genericAsToAsType, numberToNumberType); } TEST_CASE_FIXTURE(SubtypeFixture, "(number) -> number (A...) -> A...") { CHECK_IS_NOT_SUBTYPE(numberToNumberType, genericAsToAsType); } TEST_CASE_FIXTURE(SubtypeFixture, "(A...) -> A... <: (B...) -> B...") { CHECK_IS_SUBTYPE(genericAsToAsType, genericBsToBsType); } TEST_CASE_FIXTURE(SubtypeFixture, "(B...) -> C... <: (A...) -> A...") { CHECK_IS_SUBTYPE(genericBsToCsType, genericAsToAsType); } TEST_CASE_FIXTURE(SubtypeFixture, "(A...) -> A... (B...) -> C...") { CHECK_IS_NOT_SUBTYPE(genericAsToAsType, genericBsToCsType); } TEST_CASE_FIXTURE(SubtypeFixture, "(A...) -> number <: (number) -> number") { CHECK_IS_SUBTYPE(genericAsToNumberType, numberToNumberType); } TEST_CASE_FIXTURE(SubtypeFixture, "(number) -> number (A...) -> number") { CHECK_IS_NOT_SUBTYPE(numberToNumberType, genericAsToNumberType); } TEST_CASE_FIXTURE(SubtypeFixture, "(A...) -> number <: (...number) -> number") { CHECK_IS_SUBTYPE(genericAsToNumberType, numbersToNumberType); } TEST_CASE_FIXTURE(SubtypeFixture, "(...number) -> number (A...) -> number") { CHECK_IS_NOT_SUBTYPE(numbersToNumberType, genericAsToNumberType); } TEST_CASE_FIXTURE(SubtypeFixture, "() -> A... <: () -> ()") { CHECK_IS_SUBTYPE(genericNothingToAsType, nothingToNothingType); } TEST_CASE_FIXTURE(SubtypeFixture, "() -> () () -> A...") { CHECK_IS_NOT_SUBTYPE(nothingToNothingType, genericNothingToAsType); } TEST_CASE_FIXTURE(SubtypeFixture, "(A...) -> A... <: () -> ()") { CHECK_IS_SUBTYPE(genericAsToAsType, nothingToNothingType); } TEST_CASE_FIXTURE(SubtypeFixture, "() -> () (A...) -> A...") { CHECK_IS_NOT_SUBTYPE(nothingToNothingType, genericAsToAsType); } TEST_CASE_FIXTURE(SubtypeFixture, "{} <: {}") { CHECK_IS_SUBTYPE(tbl({}), tbl({})); } TEST_CASE_FIXTURE(SubtypeFixture, "{x: number} <: {}") { CHECK_IS_SUBTYPE(tbl({{"x", builtinTypes->numberType}}), tbl({})); } TEST_CASE_FIXTURE(SubtypeFixture, "{} numberType}})); } TEST_CASE_FIXTURE(SubtypeFixture, "{x: number} numberType}}), tbl({{"x", builtinTypes->stringType}})); } TEST_CASE_FIXTURE(SubtypeFixture, "{x: number} numberType}}), tbl({{"x", builtinTypes->optionalNumberType}})); } TEST_CASE_FIXTURE(SubtypeFixture, "{x: number?} optionalNumberType}}), tbl({{"x", builtinTypes->numberType}})); } TEST_CASE_FIXTURE(SubtypeFixture, "{x: (T) -> ()} <: {x: (U) -> ()}") { CHECK_IS_SUBTYPE(tbl({{"x", genericTToNothingType}}), tbl({{"x", genericUToNothingType}})); } TEST_CASE_FIXTURE(SubtypeFixture, "{ @metatable { x: number } } <: { @metatable {} }") { CHECK_IS_SUBTYPE(meta({{"x", builtinTypes->numberType}}), meta({})); } TEST_CASE_FIXTURE(SubtypeFixture, "{ @metatable { x: number } } numberType}}), meta({{"x", builtinTypes->booleanType}})); } TEST_CASE_FIXTURE(SubtypeFixture, "{ @metatable {} } booleanType}})); } TEST_CASE_FIXTURE(SubtypeFixture, "{ @metatable {} } <: {}") { CHECK_IS_SUBTYPE(meta({}), tbl({})); } TEST_CASE_FIXTURE(SubtypeFixture, "{ @metatable { u: boolean }, x: number } <: { x: number }") { CHECK_IS_SUBTYPE(meta({{"u", builtinTypes->booleanType}}, {{"x", builtinTypes->numberType}}), tbl({{"x", builtinTypes->numberType}})); } TEST_CASE_FIXTURE(SubtypeFixture, "{ @metatable { x: number } } numberType}}), tbl({{"x", builtinTypes->numberType}})); } // Negated subtypes TEST_IS_NOT_SUBTYPE(negate(builtinTypes->neverType), builtinTypes->stringType); TEST_IS_SUBTYPE(negate(builtinTypes->unknownType), builtinTypes->stringType); TEST_IS_NOT_SUBTYPE(negate(builtinTypes->anyType), builtinTypes->stringType); TEST_IS_SUBTYPE(negate(meet(builtinTypes->neverType, builtinTypes->unknownType)), builtinTypes->stringType); TEST_IS_SUBTYPE(negate(join(builtinTypes->neverType, builtinTypes->unknownType)), builtinTypes->stringType); // Negated supertypes: never/unknown/any/error TEST_IS_SUBTYPE(builtinTypes->stringType, negate(builtinTypes->neverType)); TEST_IS_SUBTYPE(builtinTypes->neverType, negate(builtinTypes->unknownType)); TEST_IS_NOT_SUBTYPE(builtinTypes->stringType, negate(builtinTypes->unknownType)); TEST_IS_SUBTYPE(builtinTypes->numberType, negate(builtinTypes->anyType)); TEST_IS_SUBTYPE(builtinTypes->unknownType, negate(builtinTypes->anyType)); // Negated supertypes: unions TEST_IS_SUBTYPE(builtinTypes->booleanType, negate(join(builtinTypes->stringType, builtinTypes->numberType))); TEST_IS_SUBTYPE(rootClass, negate(join(childClass, builtinTypes->numberType))); TEST_IS_SUBTYPE(str("foo"), negate(join(builtinTypes->numberType, builtinTypes->booleanType))); TEST_IS_NOT_SUBTYPE(str("foo"), negate(join(builtinTypes->stringType, builtinTypes->numberType))); TEST_IS_NOT_SUBTYPE(childClass, negate(join(rootClass, builtinTypes->numberType))); TEST_IS_NOT_SUBTYPE(numbersToNumberType, negate(join(builtinTypes->functionType, rootClass))); // Negated supertypes: intersections TEST_IS_SUBTYPE(builtinTypes->booleanType, negate(meet(builtinTypes->stringType, str("foo")))); TEST_IS_SUBTYPE(builtinTypes->trueType, negate(meet(builtinTypes->booleanType, builtinTypes->numberType))); TEST_IS_SUBTYPE(rootClass, negate(meet(builtinTypes->classType, childClass))); TEST_IS_SUBTYPE(childClass, negate(meet(builtinTypes->classType, builtinTypes->numberType))); TEST_IS_SUBTYPE(builtinTypes->unknownType, negate(meet(builtinTypes->classType, builtinTypes->numberType))); TEST_IS_NOT_SUBTYPE(str("foo"), negate(meet(builtinTypes->stringType, negate(str("bar"))))); // Negated supertypes: tables and metatables TEST_IS_SUBTYPE(tbl({}), negate(builtinTypes->numberType)); TEST_IS_NOT_SUBTYPE(tbl({}), negate(builtinTypes->tableType)); TEST_IS_SUBTYPE(meta({}), negate(builtinTypes->numberType)); TEST_IS_NOT_SUBTYPE(meta({}), negate(builtinTypes->tableType)); // Negated supertypes: Functions TEST_IS_SUBTYPE(numberToNumberType, negate(builtinTypes->classType)); TEST_IS_NOT_SUBTYPE(numberToNumberType, negate(builtinTypes->functionType)); // Negated supertypes: Primitives and singletons TEST_IS_SUBTYPE(builtinTypes->stringType, negate(builtinTypes->numberType)); TEST_IS_SUBTYPE(str("foo"), meet(builtinTypes->stringType, negate(str("bar")))); TEST_IS_NOT_SUBTYPE(builtinTypes->trueType, negate(builtinTypes->booleanType)); TEST_IS_NOT_SUBTYPE(str("foo"), negate(str("foo"))); TEST_IS_NOT_SUBTYPE(str("foo"), negate(builtinTypes->stringType)); TEST_IS_SUBTYPE(builtinTypes->falseType, negate(builtinTypes->trueType)); TEST_IS_SUBTYPE(builtinTypes->falseType, meet(builtinTypes->booleanType, negate(builtinTypes->trueType))); TEST_IS_NOT_SUBTYPE(builtinTypes->stringType, meet(builtinTypes->booleanType, negate(builtinTypes->trueType))); TEST_IS_NOT_SUBTYPE(builtinTypes->stringType, negate(str("foo"))); TEST_IS_NOT_SUBTYPE(builtinTypes->booleanType, negate(builtinTypes->falseType)); // Negated supertypes: Classes TEST_IS_SUBTYPE(rootClass, negate(builtinTypes->tableType)); TEST_IS_NOT_SUBTYPE(rootClass, negate(builtinTypes->classType)); TEST_IS_NOT_SUBTYPE(childClass, negate(rootClass)); TEST_IS_NOT_SUBTYPE(childClass, meet(builtinTypes->classType, negate(rootClass))); TEST_IS_SUBTYPE(anotherChildClass, meet(builtinTypes->classType, negate(childClass))); TEST_CASE_FIXTURE(SubtypeFixture, "Root <: class") { CHECK_IS_SUBTYPE(rootClass, builtinTypes->classType); } TEST_CASE_FIXTURE(SubtypeFixture, "Child | AnotherChild <: class") { CHECK_IS_SUBTYPE(join(childClass, anotherChildClass), builtinTypes->classType); } TEST_CASE_FIXTURE(SubtypeFixture, "Child | AnotherChild <: Child | AnotherChild") { CHECK_IS_SUBTYPE(join(childClass, anotherChildClass), join(childClass, anotherChildClass)); } TEST_CASE_FIXTURE(SubtypeFixture, "Child | Root <: Root") { CHECK_IS_SUBTYPE(join(childClass, rootClass), rootClass); } TEST_CASE_FIXTURE(SubtypeFixture, "Child & AnotherChild <: class") { CHECK_IS_SUBTYPE(meet(childClass, anotherChildClass), builtinTypes->classType); } TEST_CASE_FIXTURE(SubtypeFixture, "Child & Root <: class") { CHECK_IS_SUBTYPE(meet(childClass, rootClass), builtinTypes->classType); } TEST_CASE_FIXTURE(SubtypeFixture, "Child & ~Root <: class") { CHECK_IS_SUBTYPE(meet(childClass, negate(rootClass)), builtinTypes->classType); } TEST_CASE_FIXTURE(SubtypeFixture, "Child & AnotherChild <: number") { CHECK_IS_SUBTYPE(meet(childClass, anotherChildClass), builtinTypes->numberType); } TEST_CASE_FIXTURE(SubtypeFixture, "Child & ~GrandchildOne numberType); } TEST_CASE_FIXTURE(SubtypeFixture, "semantic_subtyping_disj") { TypeId subTy = builtinTypes->unknownType; TypeId superTy = join(negate(builtinTypes->numberType), negate(builtinTypes->stringType)); SubtypingResult result = isSubtype(subTy, superTy); CHECK(result.isSubtype); } TEST_CASE_FIXTURE(SubtypeFixture, "t1 where t1 = {trim: (t1) -> string} <: t2 where t2 = {trim: (t2) -> string}") { TypeId t1 = cyclicTable([&](TypeId ty, TableType* tt) { tt->props["trim"] = fn({ty}, {builtinTypes->stringType}); }); TypeId t2 = cyclicTable([&](TypeId ty, TableType* tt) { tt->props["trim"] = fn({ty}, {builtinTypes->stringType}); }); CHECK_IS_SUBTYPE(t1, t2); } TEST_CASE_FIXTURE(SubtypeFixture, "t1 where t1 = {trim: (t1) -> string} t2}") { TypeId t1 = cyclicTable([&](TypeId ty, TableType* tt) { tt->props["trim"] = fn({ty}, {builtinTypes->stringType}); }); TypeId t2 = cyclicTable([&](TypeId ty, TableType* tt) { tt->props["trim"] = fn({ty}, {ty}); }); CHECK_IS_NOT_SUBTYPE(t1, t2); } TEST_CASE_FIXTURE(SubtypeFixture, "t1 where t1 = {trim: (t1) -> t1} string}") { TypeId t1 = cyclicTable([&](TypeId ty, TableType* tt) { tt->props["trim"] = fn({ty}, {ty}); }); TypeId t2 = cyclicTable([&](TypeId ty, TableType* tt) { tt->props["trim"] = fn({ty}, {builtinTypes->stringType}); }); CHECK_IS_NOT_SUBTYPE(t1, t2); } TEST_CASE_FIXTURE(SubtypeFixture, "Vec2 <: { X: number, Y: number }") { TypeId xy = tbl({ {"X", builtinTypes->numberType}, {"Y", builtinTypes->numberType}, }); CHECK_IS_SUBTYPE(vec2Class, xy); } TEST_CASE_FIXTURE(SubtypeFixture, "Vec2 <: { X: number }") { TypeId x = tbl({ {"X", builtinTypes->numberType}, }); CHECK_IS_SUBTYPE(vec2Class, x); } TEST_CASE_FIXTURE(SubtypeFixture, "{ X: number, Y: number } numberType}, {"Y", builtinTypes->numberType}, }); CHECK_IS_NOT_SUBTYPE(xy, vec2Class); } TEST_CASE_FIXTURE(SubtypeFixture, "{ X: number } numberType}, }); CHECK_IS_NOT_SUBTYPE(x, vec2Class); } TEST_CASE_FIXTURE(SubtypeFixture, "table & { X: number, Y: number } numberType}, {"Y", builtinTypes->numberType}, }); CHECK_IS_NOT_SUBTYPE(meet(builtinTypes->tableType, x), vec2Class); } TEST_CASE_FIXTURE(SubtypeFixture, "Vec2 numberType}, {"Y", builtinTypes->numberType}, }); CHECK_IS_NOT_SUBTYPE(vec2Class, meet(builtinTypes->tableType, xy)); } TEST_CASE_FIXTURE(SubtypeFixture, "\"hello\" <: { lower : (string) -> string }") { CHECK_IS_SUBTYPE(helloType, tableWithLower); } TEST_CASE_FIXTURE(SubtypeFixture, "\"hello\" () }") { CHECK_IS_NOT_SUBTYPE(helloType, tableWithoutScalarProp); } TEST_CASE_FIXTURE(SubtypeFixture, "string <: { lower : (string) -> string }") { CHECK_IS_SUBTYPE(builtinTypes->stringType, tableWithLower); } TEST_CASE_FIXTURE(SubtypeFixture, "string () }") { CHECK_IS_NOT_SUBTYPE(builtinTypes->stringType, tableWithoutScalarProp); } TEST_CASE_FIXTURE(SubtypeFixture, "~fun & (string) -> number <: (string) -> number") { CHECK_IS_SUBTYPE(meet(negate(builtinTypes->functionType), numberToStringType), numberToStringType); } TEST_CASE_FIXTURE(SubtypeFixture, "(string) -> number <: ~fun & (string) -> number") { CHECK_IS_NOT_SUBTYPE(numberToStringType, meet(negate(builtinTypes->functionType), numberToStringType)); } TEST_CASE_FIXTURE(SubtypeFixture, "~\"a\" & ~\"b\" & string <: { lower : (string) -> ()}") { CHECK_IS_SUBTYPE(meet(meet(negate(aType), negate(bType)), builtinTypes->stringType), tableWithLower); } TEST_CASE_FIXTURE(SubtypeFixture, "\"a\" | (~\"b\" & string) <: { lower : (string) -> ()}") { CHECK_IS_SUBTYPE(join(aType, meet(negate(bType), builtinTypes->stringType)), tableWithLower); } TEST_CASE_FIXTURE(SubtypeFixture, "(string | number) & (\"a\" | true) <: { lower: (string) -> string }") { auto base = meet(join(builtinTypes->stringType, builtinTypes->numberType), join(aType, trueSingleton)); CHECK_IS_SUBTYPE(base, tableWithLower); } TEST_CASE_FIXTURE(SubtypeFixture, "number <: ~~number") { CHECK_IS_SUBTYPE(builtinTypes->numberType, negate(negate(builtinTypes->numberType))); } TEST_CASE_FIXTURE(SubtypeFixture, "~~number <: number") { CHECK_IS_SUBTYPE(negate(negate(builtinTypes->numberType)), builtinTypes->numberType); } /* * Within the scope to which a generic belongs, that generic ought to be treated * as its bounds. * * We do not yet support bounded generics, so all generics are considered to be * bounded by unknown. */ TEST_CASE_FIXTURE(SubtypeFixture, "unknown <: X") { ScopePtr childScope{new Scope(rootScope)}; ScopePtr grandChildScope{new Scope(childScope)}; TypeId genericX = arena.addType(GenericType(childScope.get(), "X")); SubtypingResult usingGlobalScope = subtyping.isSubtype(builtinTypes->unknownType, genericX); CHECK_MESSAGE(!usingGlobalScope.isSubtype, "Expected " << builtinTypes->unknownType << " unknownType, genericX); CHECK_MESSAGE(usingChildScope.isSubtype, "Expected " << builtinTypes->unknownType << " <: " << genericX); Subtyping grandChildSubtyping{mkSubtyping(grandChildScope)}; SubtypingResult usingGrandChildScope = grandChildSubtyping.isSubtype(builtinTypes->unknownType, genericX); CHECK_MESSAGE(usingGrandChildScope.isSubtype, "Expected " << builtinTypes->unknownType << " <: " << genericX); } TEST_IS_SUBTYPE(idx(builtinTypes->numberType, builtinTypes->numberType), tbl({})); TEST_IS_NOT_SUBTYPE(tbl({}), idx(builtinTypes->numberType, builtinTypes->numberType)); TEST_IS_NOT_SUBTYPE(tbl({{"X", builtinTypes->numberType}}), idx(builtinTypes->numberType, builtinTypes->numberType)); TEST_IS_NOT_SUBTYPE(idx(builtinTypes->numberType, builtinTypes->numberType), tbl({{"X", builtinTypes->numberType}})); TEST_IS_NOT_SUBTYPE( idx(join(builtinTypes->numberType, builtinTypes->stringType), builtinTypes->numberType), idx(builtinTypes->numberType, builtinTypes->numberType)); TEST_IS_NOT_SUBTYPE( idx(builtinTypes->numberType, builtinTypes->numberType), idx(join(builtinTypes->numberType, builtinTypes->stringType), builtinTypes->numberType)); TEST_IS_NOT_SUBTYPE( idx(builtinTypes->numberType, join(builtinTypes->stringType, builtinTypes->numberType)), idx(builtinTypes->numberType, builtinTypes->numberType)); TEST_IS_NOT_SUBTYPE( idx(builtinTypes->numberType, builtinTypes->numberType), idx(builtinTypes->numberType, join(builtinTypes->stringType, builtinTypes->numberType))); TEST_IS_NOT_SUBTYPE(tbl({{"X", builtinTypes->numberType}}), idx(builtinTypes->stringType, builtinTypes->numberType)); TEST_IS_SUBTYPE(idx(builtinTypes->stringType, builtinTypes->numberType), tbl({{"X", builtinTypes->numberType}})); TEST_IS_NOT_SUBTYPE(tbl({{"X", opt(builtinTypes->numberType)}}), idx(builtinTypes->stringType, builtinTypes->numberType)); TEST_IS_NOT_SUBTYPE(idx(builtinTypes->stringType, builtinTypes->numberType), tbl({{"X", opt(builtinTypes->numberType)}})); TEST_IS_SUBTYPE(tbl({{"X", builtinTypes->numberType}, {"Y", builtinTypes->numberType}}), tbl({{"X", builtinTypes->numberType}})); TEST_IS_NOT_SUBTYPE(tbl({{"X", builtinTypes->numberType}}), tbl({{"X", builtinTypes->numberType}, {"Y", builtinTypes->numberType}})); TEST_CASE_FIXTURE(SubtypeFixture, "interior_tests_are_cached") { TypeId tableA = tbl({{"X", builtinTypes->numberType}, {"Y", builtinTypes->numberType}}); TypeId tableB = tbl({{"X", builtinTypes->optionalNumberType}, {"Y", builtinTypes->optionalNumberType}}); CHECK_IS_NOT_SUBTYPE(tableA, tableB); const SubtypingResult* cachedResult = subtyping.peekCache().find({builtinTypes->numberType, builtinTypes->optionalNumberType}); REQUIRE(cachedResult); CHECK(cachedResult->isSubtype); cachedResult = subtyping.peekCache().find({tableA, tableB}); REQUIRE(cachedResult); CHECK(!cachedResult->isSubtype); } TEST_CASE_FIXTURE(SubtypeFixture, "results_that_are_contingent_on_generics_are_not_cached") { // (T) -> T <: (number) -> number CHECK_IS_SUBTYPE(genericTToTType, numberToNumberType); CHECK(subtyping.peekCache().empty()); } TEST_CASE_FIXTURE(SubtypeFixture, "dont_cache_tests_involving_cycles") { TypeId tableA = arena.addType(BlockedType{}); TypeId tableA2 = tbl({{"self", tableA}}); asMutable(tableA)->ty.emplace(tableA2); TypeId tableB = arena.addType(BlockedType{}); TypeId tableB2 = tbl({{"self", tableB}}); asMutable(tableB)->ty.emplace(tableB2); CHECK_IS_SUBTYPE(tableA, tableB); CHECK(!subtyping.peekCache().find({tableA, tableB})); } TEST_SUITE_END(); TEST_SUITE_BEGIN("Subtyping.Subpaths"); TEST_CASE_FIXTURE(SubtypeFixture, "table_property") { TypeId subTy = tbl({{"X", builtinTypes->numberType}}); TypeId superTy = tbl({{"X", builtinTypes->booleanType}}); SubtypingResult result = isSubtype(subTy, superTy); CHECK(!result.isSubtype); CHECK(result.reasoning == std::vector{SubtypingReasoning{/* subPath */ Path(TypePath::Property("X")), /* superPath */ Path(TypePath::Property("X")), /* variance */ SubtypingVariance::Invariant}}); } TEST_CASE_FIXTURE(SubtypeFixture, "table_indexers") { TypeId subTy = idx(builtinTypes->numberType, builtinTypes->stringType); TypeId superTy = idx(builtinTypes->stringType, builtinTypes->numberType); SubtypingResult result = isSubtype(subTy, superTy); CHECK(!result.isSubtype); CHECK(result.reasoning == std::vector{SubtypingReasoning{ /* subPath */ Path(TypePath::TypeField::IndexLookup), /* superPath */ Path(TypePath::TypeField::IndexLookup), /* variance */ SubtypingVariance::Invariant, }, SubtypingReasoning{ /* subPath */ Path(TypePath::TypeField::IndexResult), /* superPath */ Path(TypePath::TypeField::IndexResult), /* variance */ SubtypingVariance::Invariant, }}); } TEST_CASE_FIXTURE(SubtypeFixture, "fn_arguments") { TypeId subTy = fn({builtinTypes->numberType}, {}); TypeId superTy = fn({builtinTypes->stringType}, {}); SubtypingResult result = isSubtype(subTy, superTy); CHECK(!result.isSubtype); CHECK(result.reasoning == std::vector{SubtypingReasoning{ /* subPath */ TypePath::PathBuilder().args().index(0).build(), /* superPath */ TypePath::PathBuilder().args().index(0).build(), /* variance */ SubtypingVariance::Contravariant, }}); } TEST_CASE_FIXTURE(SubtypeFixture, "fn_arguments_tail") { TypeId subTy = fn({}, VariadicTypePack{builtinTypes->numberType}, {}); TypeId superTy = fn({}, VariadicTypePack{builtinTypes->stringType}, {}); SubtypingResult result = isSubtype(subTy, superTy); CHECK(!result.isSubtype); CHECK(result.reasoning == std::vector{SubtypingReasoning{ /* subPath */ TypePath::PathBuilder().args().tail().variadic().build(), /* superPath */ TypePath::PathBuilder().args().tail().variadic().build(), /* variance */ SubtypingVariance::Contravariant, }}); } TEST_CASE_FIXTURE(SubtypeFixture, "fn_rets") { TypeId subTy = fn({}, {builtinTypes->numberType}); TypeId superTy = fn({}, {builtinTypes->stringType}); SubtypingResult result = isSubtype(subTy, superTy); CHECK(!result.isSubtype); CHECK(result.reasoning == std::vector{SubtypingReasoning{ /* subPath */ TypePath::PathBuilder().rets().index(0).build(), /* superPath */ TypePath::PathBuilder().rets().index(0).build(), }}); } TEST_CASE_FIXTURE(SubtypeFixture, "fn_rets_tail") { TypeId subTy = fn({}, {}, VariadicTypePack{builtinTypes->numberType}); TypeId superTy = fn({}, {}, VariadicTypePack{builtinTypes->stringType}); SubtypingResult result = isSubtype(subTy, superTy); CHECK(!result.isSubtype); CHECK(result.reasoning == std::vector{SubtypingReasoning{ /* subPath */ TypePath::PathBuilder().rets().tail().variadic().build(), /* superPath */ TypePath::PathBuilder().rets().tail().variadic().build(), }}); } TEST_CASE_FIXTURE(SubtypeFixture, "nested_table_properties") { TypeId subTy = tbl({{"X", tbl({{"Y", tbl({{"Z", builtinTypes->numberType}})}})}}); TypeId superTy = tbl({{"X", tbl({{"Y", tbl({{"Z", builtinTypes->stringType}})}})}}); SubtypingResult result = isSubtype(subTy, superTy); CHECK(!result.isSubtype); CHECK(result.reasoning == std::vector{SubtypingReasoning{ /* subPath */ TypePath::PathBuilder().prop("X").prop("Y").prop("Z").build(), /* superPath */ TypePath::PathBuilder().prop("X").prop("Y").prop("Z").build(), /* variance */ SubtypingVariance::Invariant, }}); } TEST_CASE_FIXTURE(SubtypeFixture, "string_table_mt") { TypeId subTy = builtinTypes->stringType; TypeId superTy = tbl({{"X", builtinTypes->numberType}}); SubtypingResult result = isSubtype(subTy, superTy); CHECK(!result.isSubtype); // This check is weird. Because we don't have built-in types, we don't have // the string metatable. That means subtyping will see that the entire // metatable is empty, and abort there, without looking at the metatable // properties (because there aren't any). CHECK(result.reasoning == std::vector{SubtypingReasoning{ /* subPath */ TypePath::PathBuilder().mt().prop("__index").build(), /* superPath */ TypePath::kEmpty, }}); } TEST_CASE_FIXTURE(SubtypeFixture, "negation") { TypeId subTy = builtinTypes->numberType; TypeId superTy = negate(builtinTypes->numberType); SubtypingResult result = isSubtype(subTy, superTy); CHECK(!result.isSubtype); CHECK(result.reasoning == std::vector{SubtypingReasoning{ /* subPath */ TypePath::kEmpty, /* superPath */ Path(TypePath::TypeField::Negated), }}); } TEST_CASE_FIXTURE(SubtypeFixture, "multiple_reasonings") { TypeId subTy = tbl({{"X", builtinTypes->stringType}, {"Y", builtinTypes->numberType}}); TypeId superTy = tbl({{"X", builtinTypes->numberType}, {"Y", builtinTypes->stringType}}); SubtypingResult result = isSubtype(subTy, superTy); CHECK(!result.isSubtype); CHECK(result.reasoning == std::vector{ SubtypingReasoning{/* subPath */ Path(TypePath::Property("X")), /* superPath */ Path(TypePath::Property("X")), /* variance */ SubtypingVariance::Invariant}, SubtypingReasoning{/* subPath */ Path(TypePath::Property("Y")), /* superPath */ Path(TypePath::Property("Y")), /* variance */ SubtypingVariance::Invariant}, }); } TEST_SUITE_END();