// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "doctest.h" #include "Fixture.h" #include "Luau/Subtyping.h" #include "Luau/TypePack.h" using namespace Luau; struct SubtypeFixture : Fixture { TypeArena arena; InternalErrorReporter iceReporter; UnifierSharedState sharedState{&ice}; Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}}; Subtyping subtyping{builtinTypes, NotNull{&arena}, NotNull{&normalizer}, NotNull{&iceReporter}}; 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 cyclicTable(std::function&& cb) { TypeId res = arena.addType(GenericType{}); TableType tt{}; cb(res, &tt); emplaceType(asMutable(res), std::move(tt)); return res; } TypeId genericT = arena.addType(GenericType{"T"}); TypeId genericU = arena.addType(GenericType{"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 helloOrWorldType = arena.addType(UnionType{{helloType, worldType}}); TypeId trueOrFalseType = arena.addType(UnionType{{builtinTypes->trueType, builtinTypes->falseType}}); // "hello" | "hello" TypeId helloOrHelloType = arena.addType(UnionType{{helloType, helloType}}); // () -> () const TypeId nothingToNothingType = fn({}, {}); // ("hello") -> "world" TypeId helloAndWorldType = arena.addType(IntersectionType{{helloType, worldType}}); // (boolean) -> true TypeId booleanAndTrueType = arena.addType(IntersectionType{{builtinTypes->booleanType, builtinTypes->trueType}}); // (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 }); }; #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_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 numberType, builtinTypes->stringType); } 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); } 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, "(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, "{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, "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); } /* * (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_SUITE_END();