luau/tests/Subtyping.test.cpp
Junseo Yoo b6b74b4425
Sync to upstream/release/634 (#1325)
# What's Changed?

- Performance improvement in the old solver
- Bugfixes in the new solver

## Old Solver

- Mark types that do not need instantiation when being exported to
prevent unnecessary work from being done

## New Solver

- Refactored instances of "type family" with "type function"
- Index-out-of-bounds bug fix in the resolution resolver
- Subtyping reasonings are merged only if all failed

---
### Internal Contributors

Co-authored-by: Aaron Weiss <aaronweiss@roblox.com>
Co-authored-by: Vighnesh Vijay <vvijay@roblox.com>
Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>

---------

Co-authored-by: Aaron Weiss <aaronweiss@roblox.com>
Co-authored-by: Alexander McCord <amccord@roblox.com>
Co-authored-by: Andy Friesen <afriesen@roblox.com>
Co-authored-by: Vighnesh <vvijay@roblox.com>
Co-authored-by: Aviral Goel <agoel@roblox.com>
Co-authored-by: David Cope <dcope@roblox.com>
Co-authored-by: Lily Brown <lbrown@roblox.com>
Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
2024-07-12 10:03:36 -07:00

1480 lines
57 KiB
C++

// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/TypeFwd.h"
#include "Luau/TypePath.h"
#include "Luau/Normalize.h"
#include "Luau/Subtyping.h"
#include "Luau/Type.h"
#include "Luau/TypePack.h"
#include "Luau/TypeFunction.h"
#include "doctest.h"
#include "Fixture.h"
#include "RegisterCallbacks.h"
#include <initializer_list>
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
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) << " </: " << toString(reasoning.superPath) << " (" << reasoning.variance << ")";
}
bool operator==(const DenseHashSet<SubtypingReasoning, SubtypingReasoningHash>& set, const std::vector<SubtypingReasoning>& 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}};
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true};
ScopePtr rootScope{new Scope(builtinTypes->emptyTypePack)};
ScopePtr moduleScope{new Scope(rootScope)};
Subtyping subtyping = mkSubtyping(rootScope);
BuiltinTypeFunctions builtinTypeFunctions{};
Subtyping mkSubtyping(const ScopePtr& scope)
{
return Subtyping{builtinTypes, NotNull{&arena}, NotNull{&normalizer}, NotNull{&iceReporter}, NotNull{scope.get()}};
}
TypePackId pack(std::initializer_list<TypeId> tys)
{
return arena.addTypePack(tys);
}
TypePackId pack(std::initializer_list<TypeId> tys, TypePackVariant tail)
{
return arena.addTypePack(tys, arena.addTypePack(std::move(tail)));
}
TypeId fn(std::initializer_list<TypeId> args, std::initializer_list<TypeId> rets)
{
return arena.addType(FunctionType{pack(args), pack(rets)});
}
TypeId fn(std::initializer_list<TypeId> argHead, TypePackVariant argTail, std::initializer_list<TypeId> rets)
{
return arena.addType(FunctionType{pack(argHead, std::move(argTail)), pack(rets)});
}
TypeId fn(std::initializer_list<TypeId> args, std::initializer_list<TypeId> retHead, TypePackVariant retTail)
{
return arena.addType(FunctionType{pack(args), pack(retHead, std::move(retTail))});
}
TypeId fn(std::initializer_list<TypeId> argHead, TypePackVariant argTail, std::initializer_list<TypeId> 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<TypeId> 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<ClassType>(ty)->props = std::move(props);
return ty;
}
TypeId opt(TypeId ty)
{
return join(ty, builtinTypes->nilType);
}
TypeId cyclicTable(std::function<void(TypeId, TableType*)>&& cb)
{
TypeId res = arena.addType(GenericType{});
TableType tt{};
cb(res, &tt);
emplaceType<TableType>(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},
});
TypeId readOnlyVec2Class = cls("ReadOnlyVec2", {
{"X", Property::readonly(builtinTypes->numberType)},
{"Y", Property::readonly(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>(T) -> ()
const TypeId genericTToNothingType = arena.addType(FunctionType{{genericT}, {}, arena.addTypePack({genericT}), builtinTypes->emptyTypePack});
// <T>(T) -> T
const TypeId genericTToTType = arena.addType(FunctionType{{genericT}, {}, arena.addTypePack({genericT}), arena.addTypePack({genericT})});
// <U>(U) -> ()
const TypeId genericUToNothingType = arena.addType(FunctionType{{genericU}, {}, arena.addTypePack({genericU}), builtinTypes->emptyTypePack});
// <T>() -> T
const TypeId genericNothingToTType = arena.addType(FunctionType{{genericT}, {}, builtinTypes->emptyTypePack, arena.addTypePack({genericT})});
// <A...>(A...) -> A...
const TypeId genericAsToAsType = arena.addType(FunctionType{{}, {genericAs}, genericAs, genericAs});
// <A...>(A...) -> number
const TypeId genericAsToNumberType = arena.addType(FunctionType{{}, {genericAs}, genericAs, arena.addTypePack({builtinTypes->numberType})});
// <B...>(B...) -> B...
const TypeId genericBsToBsType = arena.addType(FunctionType{{}, {genericBs}, genericBs, genericBs});
// <B..., C...>(B...) -> C...
const TypeId genericBsToCsType = arena.addType(FunctionType{{}, {genericBs, genericCs}, genericBs, genericCs});
// <A...>() -> 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 << " </: " << rightTy); \
} while (0)
/// Internal macro for registering a generated test case.
///
/// @param der the name of the derived fixture struct
/// @param reg the name of the registration callback, invoked immediately before
/// tests are ran to register the test
/// @param run the name of the run callback, invoked to actually run the test case
#define TEST_REGISTER(der, reg, run) \
static inline DOCTEST_NOINLINE void run() \
{ \
der fix; \
fix.test(); \
} \
static inline DOCTEST_NOINLINE void reg() \
{ \
/* we have to mark this as `static` to ensure the memory remains alive \
for the entirety of the test process */ \
static std::string name = der().testName; \
doctest::detail::regTest(doctest::detail::TestCase(run, __FILE__, __LINE__, \
doctest_detail_test_suite_ns::getCurrentTestSuite()) /* the test case's name, determined at runtime */ \
* name.c_str() /* getCurrentTestSuite() only works at static initialization \
time due to implementation details. To ensure that test cases \
are grouped where they should be, manually override the suite \
with the test_suite decorator. */ \
* doctest::test_suite("Subtyping")); \
} \
DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(DOCTEST_ANON_VAR_), addTestCallback(reg));
/// Internal macro for deriving a test case fixture. Roughly analogous to
/// DOCTEST_IMPLEMENT_FIXTURE.
///
/// @param op a function (or macro) to call that compares the subtype to
/// the supertype.
/// @param symbol the symbol to use in stringification
/// @param der the name of the derived fixture struct
/// @param left the subtype expression
/// @param right the supertype expression
#define TEST_DERIVE(op, symbol, der, left, right) \
namespace \
{ \
struct der : SubtypeFixture \
{ \
const TypeId subTy = (left); \
const TypeId superTy = (right); \
const std::string testName = toString(subTy) + " " symbol " " + toString(superTy); \
inline DOCTEST_NOINLINE void test() \
{ \
op(subTy, superTy); \
} \
}; \
TEST_REGISTER(der, DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_)); \
}
/// Generates a test that checks if a type is a subtype of another.
#define TEST_IS_SUBTYPE(left, right) TEST_DERIVE(CHECK_IS_SUBTYPE, "<:", DOCTEST_ANONYMOUS(DOCTEST_ANON_CLASS_), left, right)
/// Generates a test that checks if a type is _not_ a subtype of another.
/// Uses <!: instead of </: to ensure that rotest doesn't explode when it splits
/// on / characters.
#define TEST_IS_NOT_SUBTYPE(left, right) TEST_DERIVE(CHECK_IS_NOT_SUBTYPE, "<!:", DOCTEST_ANONYMOUS(DOCTEST_ANON_CLASS_), left, right)
TEST_SUITE_BEGIN("Subtyping");
// We would like to write </: to mean "is not a subtype," but rotest does not like that at all, so we instead use <!:
TEST_IS_SUBTYPE(builtinTypes->numberType, builtinTypes->anyType);
TEST_IS_NOT_SUBTYPE(builtinTypes->numberType, builtinTypes->stringType);
TEST_CASE_FIXTURE(SubtypeFixture, "basic_reducible_sub_type_function")
{
// add<number, number> <: number
TypeId typeFunctionNum =
arena.addType(TypeFunctionInstanceType{NotNull{&builtinTypeFunctions.addFunc}, {builtinTypes->numberType, builtinTypes->numberType}, {}});
TypeId superTy = builtinTypes->numberType;
SubtypingResult result = isSubtype(typeFunctionNum, superTy);
CHECK(result.isSubtype);
}
TEST_CASE_FIXTURE(SubtypeFixture, "basic_reducible_super_type_function")
{
// number <: add<number, number> ~ number
TypeId typeFunctionNum =
arena.addType(TypeFunctionInstanceType{NotNull{&builtinTypeFunctions.addFunc}, {builtinTypes->numberType, builtinTypes->numberType}, {}});
TypeId subTy = builtinTypes->numberType;
SubtypingResult result = isSubtype(subTy, typeFunctionNum);
CHECK(result.isSubtype);
}
TEST_CASE_FIXTURE(SubtypeFixture, "basic_irreducible_sub_type_function")
{
// add<string, boolean> ~ never <: number
TypeId typeFunctionNum =
arena.addType(TypeFunctionInstanceType{NotNull{&builtinTypeFunctions.addFunc}, {builtinTypes->stringType, builtinTypes->booleanType}, {}});
TypeId superTy = builtinTypes->numberType;
SubtypingResult result = isSubtype(typeFunctionNum, superTy);
CHECK(result.isSubtype);
}
TEST_CASE_FIXTURE(SubtypeFixture, "basic_irreducible_super_type_function")
{
// number <\: add<string, boolean> ~ irreducible/never
TypeId typeFunctionNum =
arena.addType(TypeFunctionInstanceType{NotNull{&builtinTypeFunctions.addFunc}, {builtinTypes->stringType, builtinTypes->booleanType}, {}});
TypeId subTy = builtinTypes->numberType;
SubtypingResult result = isSubtype(subTy, typeFunctionNum);
CHECK(!result.isSubtype);
}
TEST_CASE_FIXTURE(SubtypeFixture, "basic_type_function_with_generics")
{
// <T,U>(x: T, x: U) -> add<T,U> <: (number, number) -> number
TypeId addTypeFunction = arena.addType(TypeFunctionInstanceType{NotNull{&builtinTypeFunctions.addFunc}, {genericT, genericU}, {}});
FunctionType ft{{genericT, genericU}, {}, arena.addTypePack({genericT, genericU}), arena.addTypePack({addTypeFunction})};
TypeId functionType = arena.addType(std::move(ft));
FunctionType superFt{arena.addTypePack({builtinTypes->numberType, builtinTypes->numberType}), arena.addTypePack({builtinTypes->numberType})};
TypeId superFunction = arena.addType(std::move(superFt));
SubtypingResult result = isSubtype(functionType, superFunction);
CHECK(result.isSubtype);
}
TEST_CASE_FIXTURE(SubtypeFixture, "variadic_subpath_in_pack")
{
TypePackId subTArgs = arena.addTypePack(TypePack{{builtinTypes->stringType, builtinTypes->stringType}, builtinTypes->anyTypePack});
TypePackId superTArgs = arena.addTypePack(TypePack{{builtinTypes->numberType}, builtinTypes->anyTypePack});
// (string, string, ...any) -> number
TypeId functionSub = arena.addType(FunctionType{subTArgs, arena.addTypePack({builtinTypes->numberType})});
// (number, ...any) -> string
TypeId functionSuper = arena.addType(FunctionType{superTArgs, arena.addTypePack({builtinTypes->stringType})});
SubtypingResult result = isSubtype(functionSub, functionSuper);
CHECK(result.reasoning == std::vector{SubtypingReasoning{TypePath::PathBuilder().rets().index(0).build(),
TypePath::PathBuilder().rets().index(0).build(), SubtypingVariance::Covariant},
SubtypingReasoning{TypePath::PathBuilder().args().index(0).build(), TypePath::PathBuilder().args().index(0).build(),
SubtypingVariance::Contravariant},
SubtypingReasoning{TypePath::PathBuilder().args().index(1).build(),
TypePath::PathBuilder().args().tail().variadic().build(), SubtypingVariance::Contravariant}});
CHECK(!result.isSubtype);
}
TEST_CASE_FIXTURE(SubtypeFixture, "any <!: unknown")
{
CHECK_IS_NOT_SUBTYPE(builtinTypes->anyType, builtinTypes->unknownType);
}
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 <!: \"hello\"")
{
CHECK_IS_NOT_SUBTYPE(builtinTypes->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 <!: true")
{
CHECK_IS_NOT_SUBTYPE(trueOrFalseType, builtinTypes->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 <!: ('hello' | 'hello')")
{
CHECK_IS_NOT_SUBTYPE(builtinTypes->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 <!: boolean & true")
{
CHECK_IS_NOT_SUBTYPE(builtinTypes->falseType, booleanAndTrueType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "(unknown) -> string <: (number) -> string")
{
CHECK_IS_SUBTYPE(unknownToStringType, numberToStringType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "(number) -> string <!: (unknown) -> string")
{
CHECK_IS_NOT_SUBTYPE(numberToStringType, unknownToStringType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "(number, number) -> string <!: (number) -> string")
{
CHECK_IS_NOT_SUBTYPE(numberNumberToStringType, numberToStringType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "(number) -> string <!: (number, number) -> string")
{
CHECK_IS_NOT_SUBTYPE(numberToStringType, numberNumberToStringType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "(number, number) -> string <!: (unknown, number) -> 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) <!: (number) -> (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) <!: (number) -> string")
{
CHECK_IS_NOT_SUBTYPE(numberToTwoStringsType, numberToStringType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "(number) -> string <!: (number) -> (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 <!: (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 <!: (number, ...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 <!: (number, ...string) -> string")
{
CHECK_IS_NOT_SUBTYPE(numberAndStringToStringType, numberAndStringsToStringType);
}
/*
* <A>(A) -> A <: <X>(X) -> X
* A can be bound to X.
*
* <A>(A) -> A </: <X>(X) -> number
* A can be bound to X, but A </: number
*
* (number) -> number </: <A>(A) -> A
* Only generics on the left side can be bound.
* number </: A
*
* <A, B>(A, B) -> boolean <: <X>(X, X) -> boolean
* It is ok to bind both A and B to X.
*
* <A>(A, A) -> boolean </: <X, Y>(X, Y) -> boolean
* A cannot be bound to both X and Y.
*/
TEST_CASE_FIXTURE(SubtypeFixture, "<T>() -> T <: () -> number")
{
CHECK_IS_SUBTYPE(genericNothingToTType, nothingToNumberType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "<T>(T) -> () <: <U>(U) -> ()")
{
CHECK_IS_SUBTYPE(genericTToNothingType, genericUToNothingType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "() -> number <!: <T>() -> T")
{
CHECK_IS_NOT_SUBTYPE(nothingToNumberType, genericNothingToTType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "<T>(T) -> () <: (number) -> ()")
{
CHECK_IS_SUBTYPE(genericTToNothingType, numberToNothingType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "<T>(T) -> T <: (number) -> number")
{
CHECK_IS_SUBTYPE(genericTToTType, numberToNumberType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "<T>(T) -> T <!: (number) -> string")
{
CHECK_IS_NOT_SUBTYPE(genericTToTType, numberToStringType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "<T>(T) -> () <: <U>(U) -> ()")
{
CHECK_IS_SUBTYPE(genericTToNothingType, genericUToNothingType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "(number) -> () <!: <T>(T) -> ()")
{
CHECK_IS_NOT_SUBTYPE(numberToNothingType, genericTToNothingType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "<T>() -> (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...) -> A... <: (number) -> number")
{
CHECK_IS_SUBTYPE(genericAsToAsType, numberToNumberType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "(number) -> number <!: <A...>(A...) -> A...")
{
CHECK_IS_NOT_SUBTYPE(numberToNumberType, genericAsToAsType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "<A...>(A...) -> A... <: <B...>(B...) -> B...")
{
CHECK_IS_SUBTYPE(genericAsToAsType, genericBsToBsType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "<B..., C...>(B...) -> C... <: <A...>(A...) -> A...")
{
CHECK_IS_SUBTYPE(genericBsToCsType, genericAsToAsType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "<A...>(A...) -> A... <!: <B..., C...>(B...) -> C...")
{
CHECK_IS_NOT_SUBTYPE(genericAsToAsType, genericBsToCsType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "<A...>(A...) -> number <: (number) -> number")
{
CHECK_IS_SUBTYPE(genericAsToNumberType, numberToNumberType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "(number) -> number <!: <A...>(A...) -> number")
{
CHECK_IS_NOT_SUBTYPE(numberToNumberType, genericAsToNumberType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "<A...>(A...) -> number <: (...number) -> number")
{
CHECK_IS_SUBTYPE(genericAsToNumberType, numbersToNumberType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "(...number) -> number <!: <A...>(A...) -> number")
{
CHECK_IS_NOT_SUBTYPE(numbersToNumberType, genericAsToNumberType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "<A...>() -> A... <: () -> ()")
{
CHECK_IS_SUBTYPE(genericNothingToAsType, nothingToNothingType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "() -> () <!: <A...>() -> A...")
{
CHECK_IS_NOT_SUBTYPE(nothingToNothingType, genericNothingToAsType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "<A...>(A...) -> A... <: () -> ()")
{
CHECK_IS_SUBTYPE(genericAsToAsType, nothingToNothingType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "() -> () <!: <A...>(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}")
{
CHECK_IS_NOT_SUBTYPE(tbl({}), tbl({{"x", builtinTypes->numberType}}));
}
TEST_CASE_FIXTURE(SubtypeFixture, "{x: number} <!: {x: string}")
{
CHECK_IS_NOT_SUBTYPE(tbl({{"x", builtinTypes->numberType}}), tbl({{"x", builtinTypes->stringType}}));
}
TEST_CASE_FIXTURE(SubtypeFixture, "{x: number} <!: {x: number?}")
{
CHECK_IS_NOT_SUBTYPE(tbl({{"x", builtinTypes->numberType}}), tbl({{"x", builtinTypes->optionalNumberType}}));
}
TEST_CASE_FIXTURE(SubtypeFixture, "{x: number?} <!: {x: number}")
{
CHECK_IS_NOT_SUBTYPE(tbl({{"x", builtinTypes->optionalNumberType}}), tbl({{"x", builtinTypes->numberType}}));
}
TEST_CASE_FIXTURE(SubtypeFixture, "{x: <T>(T) -> ()} <: {x: <U>(U) -> ()}")
{
CHECK_IS_SUBTYPE(tbl({{"x", genericTToNothingType}}), tbl({{"x", genericUToNothingType}}));
}
TEST_CASE_FIXTURE(SubtypeFixture, "{ x: number } <: { read x: number }")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true};
CHECK_IS_SUBTYPE(tbl({{"x", builtinTypes->numberType}}), tbl({{"x", Property::readonly(builtinTypes->numberType)}}));
}
TEST_CASE_FIXTURE(SubtypeFixture, "{ x: number } <: { write x: number }")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true};
CHECK_IS_SUBTYPE(tbl({{"x", builtinTypes->numberType}}), tbl({{"x", Property::writeonly(builtinTypes->numberType)}}));
}
TEST_CASE_FIXTURE(SubtypeFixture, "{ x: \"hello\" } <: { read x: string }")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true};
CHECK_IS_SUBTYPE(tbl({{"x", helloType}}), tbl({{"x", Property::readonly(builtinTypes->stringType)}}));
}
TEST_CASE_FIXTURE(SubtypeFixture, "{ x: string } <: { write x: string }")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true};
CHECK_IS_SUBTYPE(tbl({{"x", builtinTypes->stringType}}), tbl({{"x", Property::writeonly(builtinTypes->stringType)}}));
}
TEST_CASE_FIXTURE(SubtypeFixture, "{ @metatable { x: number } } <: { @metatable {} }")
{
CHECK_IS_SUBTYPE(meta({{"x", builtinTypes->numberType}}), meta({}));
}
TEST_CASE_FIXTURE(SubtypeFixture, "{ @metatable { x: number } } <!: { @metatable { x: boolean } }")
{
CHECK_IS_NOT_SUBTYPE(meta({{"x", builtinTypes->numberType}}), meta({{"x", builtinTypes->booleanType}}));
}
TEST_CASE_FIXTURE(SubtypeFixture, "{ @metatable {} } <!: { @metatable { x: boolean } }")
{
CHECK_IS_NOT_SUBTYPE(meta({}), meta({{"x", builtinTypes->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 } } <!: { x: number }")
{
CHECK_IS_NOT_SUBTYPE(meta({{"x", builtinTypes->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_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_NOT_SUBTYPE(builtinTypes->stringType, negate(builtinTypes->stringType));
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 <!: number")
{
CHECK_IS_NOT_SUBTYPE(meet(childClass, negate(grandchildOneClass)), builtinTypes->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 where t2 = {trim: (t2) -> 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} <!: t2 where t2 = {trim: (t2) -> 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 } <!: Vec2")
{
TypeId xy = tbl({
{"X", builtinTypes->numberType},
{"Y", builtinTypes->numberType},
});
CHECK_IS_NOT_SUBTYPE(xy, vec2Class);
}
TEST_CASE_FIXTURE(SubtypeFixture, "{ X: number } <!: Vec2")
{
TypeId x = tbl({
{"X", builtinTypes->numberType},
});
CHECK_IS_NOT_SUBTYPE(x, vec2Class);
}
TEST_CASE_FIXTURE(SubtypeFixture, "table & { X: number, Y: number } <!: Vec2")
{
TypeId x = tbl({
{"X", builtinTypes->numberType},
{"Y", builtinTypes->numberType},
});
CHECK_IS_NOT_SUBTYPE(meet(builtinTypes->tableType, x), vec2Class);
}
TEST_CASE_FIXTURE(SubtypeFixture, "Vec2 <!: table & { X: number, Y: number }")
{
TypeId xy = tbl({
{"X", builtinTypes->numberType},
{"Y", builtinTypes->numberType},
});
CHECK_IS_NOT_SUBTYPE(vec2Class, meet(builtinTypes->tableType, xy));
}
TEST_CASE_FIXTURE(SubtypeFixture, "ReadOnlyVec2 <!: { X: number, Y: number}")
{
CHECK_IS_NOT_SUBTYPE(readOnlyVec2Class, tbl({{"X", builtinTypes->numberType}, {"Y", builtinTypes->numberType}}));
}
TEST_CASE_FIXTURE(SubtypeFixture, "ReadOnlyVec2 <: { read X: number, read Y: number}")
{
CHECK_IS_SUBTYPE(
readOnlyVec2Class, tbl({{"X", Property::readonly(builtinTypes->numberType)}, {"Y", Property::readonly(builtinTypes->numberType)}}));
}
TEST_IS_SUBTYPE(vec2Class, tbl({{"X", Property::readonly(builtinTypes->numberType)}, {"Y", Property::readonly(builtinTypes->numberType)}}));
TEST_IS_NOT_SUBTYPE(tbl({{"P", grandchildOneClass}}), tbl({{"P", Property::rw(rootClass)}}));
TEST_IS_SUBTYPE(tbl({{"P", grandchildOneClass}}), tbl({{"P", Property::readonly(rootClass)}}));
TEST_IS_SUBTYPE(tbl({{"P", rootClass}}), tbl({{"P", Property::writeonly(grandchildOneClass)}}));
TEST_IS_NOT_SUBTYPE(cls("HasChild", {{"P", childClass}}), tbl({{"P", rootClass}}));
TEST_IS_SUBTYPE(cls("HasChild", {{"P", childClass}}), tbl({{"P", Property::readonly(rootClass)}}));
TEST_IS_NOT_SUBTYPE(cls("HasChild", {{"P", childClass}}), tbl({{"P", grandchildOneClass}}));
TEST_IS_SUBTYPE(cls("HasChild", {{"P", childClass}}), tbl({{"P", Property::writeonly(grandchildOneClass)}}));
TEST_CASE_FIXTURE(SubtypeFixture, "\"hello\" <: { lower : (string) -> string }")
{
CHECK_IS_SUBTYPE(helloType, tableWithLower);
}
TEST_CASE_FIXTURE(SubtypeFixture, "\"hello\" <!: { insaneThingNoScalarHas : () -> () }")
{
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 <!: { insaneThingNoScalarHas : () -> () }")
{
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);
}
// See https://github.com/luau-lang/luau/issues/767
TEST_CASE_FIXTURE(SubtypeFixture, "(...any) -> () <: <T>(T...) -> ()")
{
TypeId anysToNothing = arena.addType(FunctionType{builtinTypes->anyTypePack, builtinTypes->emptyTypePack});
TypeId genericTToAnys = arena.addType(FunctionType{genericAs, builtinTypes->emptyTypePack});
CHECK_MESSAGE(subtyping.isSubtype(anysToNothing, genericTToAnys).isSubtype, "(...any) -> () <: <T>(T...) -> ()");
}
// See https://github.com/luau-lang/luau/issues/767
TEST_CASE_FIXTURE(SubtypeFixture, "(...unknown) -> () <: <T>(T...) -> ()")
{
TypeId anysToNothing = arena.addType(FunctionType{arena.addTypePack(VariadicTypePack{builtinTypes->unknownType}), builtinTypes->emptyTypePack});
TypeId genericTToAnys = arena.addType(FunctionType{genericAs, builtinTypes->emptyTypePack});
CHECK_MESSAGE(subtyping.isSubtype(anysToNothing, genericTToAnys).isSubtype, "(...unknown) -> () <: <T>(T...) -> ()");
}
/*
* 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 << " </: " << genericX);
Subtyping childSubtyping{mkSubtyping(childScope)};
SubtypingResult usingChildScope = childSubtyping.isSubtype(builtinTypes->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) -> 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<BoundType>(tableA2);
TypeId tableB = arena.addType(BlockedType{});
TypeId tableB2 = tbl({{"self", tableB}});
asMutable(tableB)->ty.emplace<BoundType>(tableB2);
CHECK_IS_SUBTYPE(tableA, tableB);
CHECK(!subtyping.peekCache().find({tableA, tableB}));
}
TEST_CASE_FIXTURE(SubtypeFixture, "<T>({ x: T }) -> T <: ({ method: <T>({ x: T }) -> T, x: number }) -> number")
{
// <T>({ x: T }) -> T
TypeId tableToPropType = arena.addType(FunctionType{{genericT}, {}, arena.addTypePack({tbl({{"x", genericT}})}), arena.addTypePack({genericT})});
// ({ method: <T>({ x: T }) -> T, x: number }) -> number
TypeId otherType = fn({tbl({{"method", tableToPropType}, {"x", builtinTypes->numberType}})}, {builtinTypes->numberType});
CHECK_IS_SUBTYPE(tableToPropType, otherType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "subtyping_reasonings_to_follow_a_reduced_type_family_instance")
{
TypeId longTy = arena.addType(UnionType{{builtinTypes->booleanType, builtinTypes->bufferType, builtinTypes->classType, builtinTypes->functionType,
builtinTypes->numberType, builtinTypes->stringType, builtinTypes->tableType, builtinTypes->threadType}});
TypeId tblTy = tbl({{"depth", builtinTypes->unknownType}});
TypeId combined = meet(longTy, tblTy);
TypeId subTy = arena.addType(TypeFunctionInstanceType{NotNull{&builtinTypeFunctions.unionFunc}, {combined, builtinTypes->neverType}, {}});
TypeId superTy = builtinTypes->neverType;
SubtypingResult result = isSubtype(subTy, superTy);
CHECK(!result.isSubtype);
for (const SubtypingReasoning& reasoning : result.reasoning)
{
if (reasoning.subPath.empty() && reasoning.superPath.empty())
continue;
std::optional<TypeOrPack> optSubLeaf = traverse(subTy, reasoning.subPath, builtinTypes);
std::optional<TypeOrPack> optSuperLeaf = traverse(superTy, reasoning.superPath, builtinTypes);
if (!optSubLeaf || !optSuperLeaf)
CHECK(false);
}
}
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);
REQUIRE(result.reasoning.size() == 1);
CHECK(*result.reasoning.begin() == SubtypingReasoning{/* subPath */ Path(TypePath::Property::read("X")),
/* superPath */ Path(TypePath::Property::read("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, "arity_mismatch")
{
TypeId subTy = fn({builtinTypes->numberType}, {});
TypeId superTy = fn({}, {});
SubtypingResult result = isSubtype(subTy, superTy);
CHECK(!result.isSubtype);
CHECK(result.reasoning == std::vector{SubtypingReasoning{
/* subPath */ TypePath::PathBuilder().args().build(),
/* superPath */ TypePath::PathBuilder().args().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);
REQUIRE(result.reasoning.size() == 1);
CHECK(*result.reasoning.begin() == 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);
REQUIRE(result.reasoning.size() == 1);
CHECK(*result.reasoning.begin() == 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);
REQUIRE(result.reasoning.size() == 1);
CHECK(*result.reasoning.begin() == SubtypingReasoning{
/* subPath */ TypePath::PathBuilder().readProp("X").readProp("Y").readProp("Z").build(),
/* superPath */ TypePath::PathBuilder().readProp("X").readProp("Y").readProp("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().readProp("__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::read("X")), /* superPath */ Path(TypePath::Property::read("X")),
/* variance */ SubtypingVariance::Invariant},
SubtypingReasoning{/* subPath */ Path(TypePath::Property::read("Y")), /* superPath */ Path(TypePath::Property::read("Y")),
/* variance */ SubtypingVariance::Invariant},
});
}
TEST_SUITE_END();