2023-05-20 02:59:59 +08:00
|
|
|
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
|
|
|
|
|
|
|
|
#include "Fixture.h"
|
|
|
|
|
|
|
|
#include "doctest.h"
|
|
|
|
|
|
|
|
#include "Luau/Simplify.h"
|
|
|
|
|
|
|
|
using namespace Luau;
|
|
|
|
|
2023-12-02 10:04:44 +08:00
|
|
|
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
|
2024-03-16 05:01:00 +08:00
|
|
|
LUAU_DYNAMIC_FASTINT(LuauSimplificationComplexityLimit)
|
2023-12-02 10:04:44 +08:00
|
|
|
|
2023-05-20 02:59:59 +08:00
|
|
|
namespace
|
|
|
|
{
|
|
|
|
|
|
|
|
struct SimplifyFixture : Fixture
|
|
|
|
{
|
|
|
|
TypeArena _arena;
|
|
|
|
const NotNull<TypeArena> arena{&_arena};
|
|
|
|
|
|
|
|
ToStringOptions opts;
|
|
|
|
|
|
|
|
Scope scope{builtinTypes->anyTypePack};
|
|
|
|
|
|
|
|
const TypeId anyTy = builtinTypes->anyType;
|
|
|
|
const TypeId unknownTy = builtinTypes->unknownType;
|
|
|
|
const TypeId neverTy = builtinTypes->neverType;
|
|
|
|
const TypeId errorTy = builtinTypes->errorType;
|
|
|
|
|
|
|
|
const TypeId functionTy = builtinTypes->functionType;
|
|
|
|
const TypeId tableTy = builtinTypes->tableType;
|
|
|
|
|
|
|
|
const TypeId numberTy = builtinTypes->numberType;
|
|
|
|
const TypeId stringTy = builtinTypes->stringType;
|
|
|
|
const TypeId booleanTy = builtinTypes->booleanType;
|
|
|
|
const TypeId nilTy = builtinTypes->nilType;
|
|
|
|
|
|
|
|
const TypeId classTy = builtinTypes->classType;
|
|
|
|
|
|
|
|
const TypeId trueTy = builtinTypes->trueType;
|
|
|
|
const TypeId falseTy = builtinTypes->falseType;
|
|
|
|
|
|
|
|
const TypeId truthyTy = builtinTypes->truthyType;
|
|
|
|
const TypeId falsyTy = builtinTypes->falsyType;
|
|
|
|
|
2023-08-05 01:01:35 +08:00
|
|
|
const TypeId freeTy = freshType(arena, builtinTypes, &scope);
|
2023-05-20 02:59:59 +08:00
|
|
|
const TypeId genericTy = arena->addType(GenericType{});
|
|
|
|
const TypeId blockedTy = arena->addType(BlockedType{});
|
|
|
|
const TypeId pendingTy = arena->addType(PendingExpansionType{{}, {}, {}, {}});
|
|
|
|
|
|
|
|
const TypeId helloTy = arena->addType(SingletonType{StringSingleton{"hello"}});
|
|
|
|
const TypeId worldTy = arena->addType(SingletonType{StringSingleton{"world"}});
|
|
|
|
|
|
|
|
const TypePackId emptyTypePack = arena->addTypePack({});
|
|
|
|
|
|
|
|
const TypeId fn1Ty = arena->addType(FunctionType{emptyTypePack, emptyTypePack});
|
|
|
|
const TypeId fn2Ty = arena->addType(FunctionType{builtinTypes->anyTypePack, emptyTypePack});
|
|
|
|
|
|
|
|
TypeId parentClassTy = nullptr;
|
|
|
|
TypeId childClassTy = nullptr;
|
|
|
|
TypeId anotherChildClassTy = nullptr;
|
|
|
|
TypeId unrelatedClassTy = nullptr;
|
|
|
|
|
2023-12-02 10:04:44 +08:00
|
|
|
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true};
|
2023-08-05 01:01:35 +08:00
|
|
|
|
2023-05-20 02:59:59 +08:00
|
|
|
SimplifyFixture()
|
|
|
|
{
|
|
|
|
createSomeClasses(&frontend);
|
|
|
|
|
|
|
|
parentClassTy = frontend.globals.globalScope->linearSearchForBinding("Parent")->typeId;
|
|
|
|
childClassTy = frontend.globals.globalScope->linearSearchForBinding("Child")->typeId;
|
|
|
|
anotherChildClassTy = frontend.globals.globalScope->linearSearchForBinding("AnotherChild")->typeId;
|
|
|
|
unrelatedClassTy = frontend.globals.globalScope->linearSearchForBinding("Unrelated")->typeId;
|
|
|
|
}
|
|
|
|
|
|
|
|
TypeId intersect(TypeId a, TypeId b)
|
|
|
|
{
|
|
|
|
return simplifyIntersection(builtinTypes, arena, a, b).result;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string intersectStr(TypeId a, TypeId b)
|
|
|
|
{
|
|
|
|
return toString(intersect(a, b), opts);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool isIntersection(TypeId a)
|
|
|
|
{
|
|
|
|
return bool(get<IntersectionType>(follow(a)));
|
|
|
|
}
|
|
|
|
|
|
|
|
TypeId mkTable(std::map<Name, TypeId> propTypes)
|
|
|
|
{
|
|
|
|
TableType::Props props;
|
|
|
|
for (const auto& [name, ty] : propTypes)
|
|
|
|
props[name] = Property{ty};
|
|
|
|
|
|
|
|
return arena->addType(TableType{props, {}, TypeLevel{}, TableState::Sealed});
|
|
|
|
}
|
|
|
|
|
|
|
|
TypeId mkNegation(TypeId ty)
|
|
|
|
{
|
|
|
|
return arena->addType(NegationType{ty});
|
|
|
|
}
|
|
|
|
|
|
|
|
TypeId mkFunction(TypeId arg, TypeId ret)
|
|
|
|
{
|
|
|
|
return arena->addType(FunctionType{arena->addTypePack({arg}), arena->addTypePack({ret})});
|
|
|
|
}
|
|
|
|
|
|
|
|
TypeId union_(TypeId a, TypeId b)
|
|
|
|
{
|
|
|
|
return simplifyUnion(builtinTypes, arena, a, b).result;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
TEST_SUITE_BEGIN("Simplify");
|
|
|
|
|
2023-07-28 19:37:00 +08:00
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "overload_negation_refinement_is_never")
|
|
|
|
{
|
|
|
|
TypeId f1 = mkFunction(stringTy, numberTy);
|
|
|
|
TypeId f2 = mkFunction(numberTy, stringTy);
|
|
|
|
TypeId intersection = arena->addType(IntersectionType{{f1, f2}});
|
|
|
|
TypeId unionT = arena->addType(UnionType{{errorTy, functionTy}});
|
|
|
|
TypeId negationT = mkNegation(unionT);
|
|
|
|
// The intersection of string -> number & number -> string, ~(error | function)
|
|
|
|
CHECK(neverTy == intersect(intersection, negationT));
|
|
|
|
}
|
|
|
|
|
2023-05-20 02:59:59 +08:00
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "unknown_and_other_tops_and_bottom_types")
|
|
|
|
{
|
2024-03-01 21:58:44 +08:00
|
|
|
|
2023-05-20 02:59:59 +08:00
|
|
|
CHECK(unknownTy == intersect(unknownTy, unknownTy));
|
|
|
|
|
2024-03-16 05:01:00 +08:00
|
|
|
CHECK("any" == intersectStr(unknownTy, anyTy));
|
|
|
|
CHECK("any" == intersectStr(anyTy, unknownTy));
|
2023-05-20 02:59:59 +08:00
|
|
|
|
|
|
|
CHECK(neverTy == intersect(unknownTy, neverTy));
|
|
|
|
CHECK(neverTy == intersect(neverTy, unknownTy));
|
|
|
|
|
2024-03-01 21:58:44 +08:00
|
|
|
CHECK(errorTy == intersect(unknownTy, errorTy));
|
|
|
|
CHECK(errorTy == intersect(errorTy, unknownTy));
|
2023-05-20 02:59:59 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "nil")
|
|
|
|
{
|
|
|
|
CHECK(nilTy == intersect(nilTy, nilTy));
|
|
|
|
CHECK(neverTy == intersect(nilTy, numberTy));
|
|
|
|
CHECK(neverTy == intersect(nilTy, trueTy));
|
|
|
|
CHECK(neverTy == intersect(nilTy, tableTy));
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "boolean_singletons")
|
|
|
|
{
|
|
|
|
CHECK(trueTy == intersect(trueTy, booleanTy));
|
|
|
|
CHECK(trueTy == intersect(booleanTy, trueTy));
|
|
|
|
|
|
|
|
CHECK(falseTy == intersect(falseTy, booleanTy));
|
|
|
|
CHECK(falseTy == intersect(booleanTy, falseTy));
|
|
|
|
|
|
|
|
CHECK(neverTy == intersect(falseTy, trueTy));
|
|
|
|
CHECK(neverTy == intersect(trueTy, falseTy));
|
|
|
|
|
|
|
|
CHECK(booleanTy == union_(trueTy, booleanTy));
|
|
|
|
CHECK(booleanTy == union_(booleanTy, trueTy));
|
|
|
|
CHECK(booleanTy == union_(falseTy, booleanTy));
|
|
|
|
CHECK(booleanTy == union_(booleanTy, falseTy));
|
|
|
|
CHECK(booleanTy == union_(falseTy, trueTy));
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "boolean_and_truthy_and_falsy")
|
|
|
|
{
|
|
|
|
TypeId optionalBooleanTy = arena->addType(UnionType{{booleanTy, nilTy}});
|
|
|
|
|
|
|
|
CHECK(trueTy == intersect(booleanTy, truthyTy));
|
|
|
|
|
|
|
|
CHECK(trueTy == intersect(optionalBooleanTy, truthyTy));
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "any_and_indeterminate_types")
|
|
|
|
{
|
2024-03-01 21:58:44 +08:00
|
|
|
CHECK("'a | *error-type*" == intersectStr(anyTy, freeTy));
|
|
|
|
CHECK("'a | *error-type*" == intersectStr(freeTy, anyTy));
|
|
|
|
|
|
|
|
CHECK("*error-type* | b" == intersectStr(anyTy, genericTy));
|
|
|
|
CHECK("*error-type* | b" == intersectStr(genericTy, anyTy));
|
|
|
|
|
|
|
|
auto anyRhsBlocked = get<UnionType>(intersect(anyTy, blockedTy));
|
|
|
|
auto anyLhsBlocked = get<UnionType>(intersect(blockedTy, anyTy));
|
2023-05-20 02:59:59 +08:00
|
|
|
|
2024-03-01 21:58:44 +08:00
|
|
|
REQUIRE(anyRhsBlocked);
|
|
|
|
REQUIRE(anyRhsBlocked->options.size() == 2);
|
|
|
|
CHECK(blockedTy == anyRhsBlocked->options[0]);
|
|
|
|
CHECK(errorTy == anyRhsBlocked->options[1]);
|
2023-05-20 02:59:59 +08:00
|
|
|
|
2024-03-01 21:58:44 +08:00
|
|
|
REQUIRE(anyLhsBlocked);
|
|
|
|
REQUIRE(anyLhsBlocked->options.size() == 2);
|
|
|
|
CHECK(blockedTy == anyLhsBlocked->options[0]);
|
|
|
|
CHECK(errorTy == anyLhsBlocked->options[1]);
|
2023-05-20 02:59:59 +08:00
|
|
|
|
2024-03-01 21:58:44 +08:00
|
|
|
auto anyRhsPending = get<UnionType>(intersect(anyTy, pendingTy));
|
|
|
|
auto anyLhsPending = get<UnionType>(intersect(pendingTy, anyTy));
|
|
|
|
|
|
|
|
REQUIRE(anyRhsPending);
|
|
|
|
REQUIRE(anyRhsPending->options.size() == 2);
|
|
|
|
CHECK(pendingTy == anyRhsPending->options[0]);
|
|
|
|
CHECK(errorTy == anyRhsPending->options[1]);
|
|
|
|
|
|
|
|
REQUIRE(anyLhsPending);
|
|
|
|
REQUIRE(anyLhsPending->options.size() == 2);
|
|
|
|
CHECK(pendingTy == anyLhsPending->options[0]);
|
|
|
|
CHECK(errorTy == anyLhsPending->options[1]);
|
2023-05-20 02:59:59 +08:00
|
|
|
}
|
|
|
|
|
2024-05-26 23:33:40 +08:00
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "union_where_lhs_elements_are_a_subset_of_the_rhs")
|
|
|
|
{
|
|
|
|
TypeId lhs = union_(numberTy, stringTy);
|
|
|
|
TypeId rhs = union_(stringTy, numberTy);
|
|
|
|
|
|
|
|
CHECK("number | string" == toString(union_(lhs, rhs)));
|
|
|
|
}
|
|
|
|
|
2023-05-20 02:59:59 +08:00
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "unknown_and_indeterminate_types")
|
|
|
|
{
|
2023-08-05 01:01:35 +08:00
|
|
|
CHECK(freeTy == intersect(unknownTy, freeTy));
|
|
|
|
CHECK(freeTy == intersect(freeTy, unknownTy));
|
|
|
|
|
2024-03-01 21:58:44 +08:00
|
|
|
CHECK(genericTy == intersect(unknownTy, genericTy));
|
|
|
|
CHECK(genericTy == intersect(genericTy, unknownTy));
|
2023-05-20 02:59:59 +08:00
|
|
|
|
2024-03-01 21:58:44 +08:00
|
|
|
CHECK(blockedTy == intersect(unknownTy, blockedTy));
|
|
|
|
CHECK(blockedTy == intersect(unknownTy, blockedTy));
|
2023-05-20 02:59:59 +08:00
|
|
|
|
2024-03-01 21:58:44 +08:00
|
|
|
CHECK(pendingTy == intersect(unknownTy, pendingTy));
|
|
|
|
CHECK(pendingTy == intersect(unknownTy, pendingTy));
|
2023-05-20 02:59:59 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "unknown_and_concrete")
|
|
|
|
{
|
|
|
|
CHECK(numberTy == intersect(numberTy, unknownTy));
|
|
|
|
CHECK(numberTy == intersect(unknownTy, numberTy));
|
|
|
|
CHECK(trueTy == intersect(trueTy, unknownTy));
|
|
|
|
CHECK(trueTy == intersect(unknownTy, trueTy));
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "error_and_other_tops_and_bottom_types")
|
|
|
|
{
|
|
|
|
CHECK(errorTy == intersect(errorTy, errorTy));
|
|
|
|
|
|
|
|
CHECK(errorTy == intersect(errorTy, anyTy));
|
|
|
|
CHECK(errorTy == intersect(anyTy, errorTy));
|
|
|
|
|
|
|
|
CHECK(neverTy == intersect(errorTy, neverTy));
|
|
|
|
CHECK(neverTy == intersect(neverTy, errorTy));
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "error_and_indeterminate_types")
|
|
|
|
{
|
2023-08-05 01:01:35 +08:00
|
|
|
CHECK("'a & *error-type*" == intersectStr(errorTy, freeTy));
|
|
|
|
CHECK("'a & *error-type*" == intersectStr(freeTy, errorTy));
|
2023-05-20 02:59:59 +08:00
|
|
|
|
|
|
|
CHECK("*error-type* & b" == intersectStr(errorTy, genericTy));
|
|
|
|
CHECK("*error-type* & b" == intersectStr(genericTy, errorTy));
|
|
|
|
|
|
|
|
CHECK(isIntersection(intersect(errorTy, blockedTy)));
|
|
|
|
CHECK(isIntersection(intersect(blockedTy, errorTy)));
|
|
|
|
|
|
|
|
CHECK(isIntersection(intersect(errorTy, pendingTy)));
|
|
|
|
CHECK(isIntersection(intersect(pendingTy, errorTy)));
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "unknown_and_concrete")
|
|
|
|
{
|
|
|
|
CHECK(neverTy == intersect(numberTy, errorTy));
|
|
|
|
CHECK(neverTy == intersect(errorTy, numberTy));
|
|
|
|
CHECK(neverTy == intersect(trueTy, errorTy));
|
|
|
|
CHECK(neverTy == intersect(errorTy, trueTy));
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "primitives")
|
|
|
|
{
|
|
|
|
// This shouldn't be possible, but we'll make it work even if it is.
|
|
|
|
TypeId numberTyDuplicate = arena->addType(PrimitiveType{PrimitiveType::Number});
|
|
|
|
|
|
|
|
CHECK(numberTy == intersect(numberTy, numberTyDuplicate));
|
|
|
|
CHECK(neverTy == intersect(numberTy, stringTy));
|
|
|
|
|
|
|
|
CHECK(neverTy == intersect(neverTy, numberTy));
|
|
|
|
CHECK(neverTy == intersect(numberTy, neverTy));
|
|
|
|
|
|
|
|
CHECK(neverTy == intersect(neverTy, functionTy));
|
|
|
|
CHECK(neverTy == intersect(functionTy, neverTy));
|
|
|
|
|
|
|
|
CHECK(neverTy == intersect(neverTy, tableTy));
|
|
|
|
CHECK(neverTy == intersect(tableTy, neverTy));
|
|
|
|
|
2024-03-01 21:58:44 +08:00
|
|
|
CHECK("*error-type* | number" == intersectStr(anyTy, numberTy));
|
|
|
|
CHECK("*error-type* | number" == intersectStr(numberTy, anyTy));
|
2023-05-20 02:59:59 +08:00
|
|
|
|
|
|
|
CHECK(neverTy == intersect(stringTy, nilTy));
|
|
|
|
CHECK(neverTy == intersect(nilTy, stringTy));
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "primitives_and_falsy")
|
|
|
|
{
|
|
|
|
CHECK(neverTy == intersect(numberTy, falsyTy));
|
|
|
|
CHECK(neverTy == intersect(falsyTy, numberTy));
|
|
|
|
|
|
|
|
CHECK(nilTy == intersect(nilTy, falsyTy));
|
|
|
|
CHECK(nilTy == intersect(falsyTy, nilTy));
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "primitives_and_singletons")
|
|
|
|
{
|
|
|
|
CHECK(helloTy == intersect(helloTy, stringTy));
|
|
|
|
CHECK(helloTy == intersect(stringTy, helloTy));
|
|
|
|
|
|
|
|
CHECK(neverTy == intersect(worldTy, helloTy));
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "functions")
|
|
|
|
{
|
|
|
|
CHECK(fn1Ty == intersect(fn1Ty, functionTy));
|
|
|
|
CHECK(fn1Ty == intersect(functionTy, fn1Ty));
|
|
|
|
|
|
|
|
// Intersections of functions are super weird if you think about it.
|
|
|
|
CHECK("(() -> ()) & ((...any) -> ())" == intersectStr(fn1Ty, fn2Ty));
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "negated_top_function_type")
|
|
|
|
{
|
|
|
|
TypeId negatedFunctionTy = mkNegation(functionTy);
|
|
|
|
|
|
|
|
CHECK(numberTy == intersect(numberTy, negatedFunctionTy));
|
|
|
|
CHECK(numberTy == intersect(negatedFunctionTy, numberTy));
|
|
|
|
|
|
|
|
CHECK(falsyTy == intersect(falsyTy, negatedFunctionTy));
|
|
|
|
CHECK(falsyTy == intersect(negatedFunctionTy, falsyTy));
|
|
|
|
|
|
|
|
TypeId f = mkFunction(stringTy, numberTy);
|
|
|
|
|
|
|
|
CHECK(neverTy == intersect(f, negatedFunctionTy));
|
|
|
|
CHECK(neverTy == intersect(negatedFunctionTy, f));
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "optional_overloaded_function_and_top_function")
|
|
|
|
{
|
|
|
|
// (((number) -> string) & ((string) -> number))? & ~function
|
|
|
|
|
|
|
|
TypeId f1 = mkFunction(numberTy, stringTy);
|
|
|
|
TypeId f2 = mkFunction(stringTy, numberTy);
|
|
|
|
|
|
|
|
TypeId f12 = arena->addType(IntersectionType{{f1, f2}});
|
|
|
|
|
|
|
|
TypeId t = arena->addType(UnionType{{f12, nilTy}});
|
|
|
|
|
|
|
|
TypeId notFunctionTy = mkNegation(functionTy);
|
|
|
|
|
|
|
|
CHECK(nilTy == intersect(t, notFunctionTy));
|
|
|
|
CHECK(nilTy == intersect(notFunctionTy, t));
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "negated_function_does_not_intersect_cleanly_with_truthy")
|
|
|
|
{
|
|
|
|
// ~function & ~(false?)
|
|
|
|
// ~function & ~(false | nil)
|
|
|
|
// ~function & ~false & ~nil
|
|
|
|
|
|
|
|
TypeId negatedFunctionTy = mkNegation(functionTy);
|
|
|
|
CHECK(isIntersection(intersect(negatedFunctionTy, truthyTy)));
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "tables")
|
|
|
|
{
|
|
|
|
TypeId t1 = mkTable({{"tag", stringTy}});
|
|
|
|
|
|
|
|
CHECK(t1 == intersect(t1, tableTy));
|
|
|
|
CHECK(neverTy == intersect(t1, functionTy));
|
|
|
|
|
|
|
|
TypeId t2 = mkTable({{"tag", helloTy}});
|
|
|
|
|
|
|
|
CHECK(t2 == intersect(t1, t2));
|
|
|
|
CHECK(t2 == intersect(t2, t1));
|
|
|
|
|
|
|
|
TypeId t3 = mkTable({});
|
2024-08-02 07:25:12 +08:00
|
|
|
// {tag : string} intersect {}
|
2023-05-20 02:59:59 +08:00
|
|
|
CHECK(t1 == intersect(t1, t3));
|
|
|
|
CHECK(t1 == intersect(t3, t1));
|
|
|
|
}
|
|
|
|
|
2024-08-02 07:25:12 +08:00
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "combine_disjoint_sealed_tables")
|
|
|
|
{
|
|
|
|
TypeId t1 = mkTable({{"prop", stringTy}});
|
|
|
|
TypeId t2 = mkTable({{"second_prop", numberTy}});
|
|
|
|
|
|
|
|
CHECK("{ prop: string, second_prop: number }" == toString(intersect(t1, t2)));
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "non_disjoint_tables_do_not_simplify")
|
|
|
|
{
|
|
|
|
TypeId t1 = mkTable({{"prop", stringTy}});
|
|
|
|
TypeId t2 = mkTable({{"prop", unknownTy}, {"second_prop", numberTy}});
|
|
|
|
|
|
|
|
CHECK("{ prop: string } & { prop: unknown, second_prop: number }" == toString(intersect(t1, t2)));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Simplification has an extra code path especially for intersections with
|
|
|
|
// single-property tables, so it's worthwhile to separately test the case where
|
|
|
|
// both tables have multiple properties.
|
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "non_disjoint_tables_do_not_simplify_2")
|
|
|
|
{
|
|
|
|
TypeId t1 = mkTable({{"prop", stringTy}, {"third_prop", numberTy}});
|
|
|
|
TypeId t2 = mkTable({{"prop", unknownTy}, {"second_prop", numberTy}});
|
|
|
|
|
|
|
|
CHECK("{ prop: string, third_prop: number } & { prop: unknown, second_prop: number }" == toString(intersect(t1, t2)));
|
|
|
|
}
|
|
|
|
|
2023-05-20 02:59:59 +08:00
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "tables_and_top_table")
|
|
|
|
{
|
|
|
|
TypeId notTableType = mkNegation(tableTy);
|
|
|
|
TypeId t1 = mkTable({{"prop", stringTy}, {"another", numberTy}});
|
|
|
|
|
|
|
|
CHECK(t1 == intersect(t1, tableTy));
|
|
|
|
CHECK(t1 == intersect(tableTy, t1));
|
|
|
|
|
|
|
|
CHECK(neverTy == intersect(t1, notTableType));
|
|
|
|
CHECK(neverTy == intersect(notTableType, t1));
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "tables_and_truthy")
|
|
|
|
{
|
|
|
|
TypeId t1 = mkTable({{"prop", stringTy}, {"another", numberTy}});
|
|
|
|
|
|
|
|
CHECK(t1 == intersect(t1, truthyTy));
|
|
|
|
CHECK(t1 == intersect(truthyTy, t1));
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "table_with_a_tag")
|
|
|
|
{
|
|
|
|
// {tag: string, prop: number} & {tag: "hello"}
|
|
|
|
// I think we can decline to simplify this:
|
|
|
|
TypeId t1 = mkTable({{"tag", stringTy}, {"prop", numberTy}});
|
|
|
|
TypeId t2 = mkTable({{"tag", helloTy}});
|
|
|
|
|
2023-09-30 08:22:06 +08:00
|
|
|
CHECK("{ prop: number, tag: string } & { tag: \"hello\" }" == intersectStr(t1, t2));
|
|
|
|
CHECK("{ prop: number, tag: string } & { tag: \"hello\" }" == intersectStr(t2, t1));
|
2023-05-20 02:59:59 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "nested_table_tag_test")
|
|
|
|
{
|
|
|
|
TypeId t1 = mkTable({
|
2024-08-02 07:25:12 +08:00
|
|
|
{"subtable",
|
|
|
|
mkTable({
|
|
|
|
{"tag", helloTy},
|
|
|
|
{"subprop", numberTy},
|
|
|
|
})},
|
2023-05-20 02:59:59 +08:00
|
|
|
{"prop", stringTy},
|
|
|
|
});
|
|
|
|
TypeId t2 = mkTable({
|
2024-08-02 07:25:12 +08:00
|
|
|
{"subtable",
|
|
|
|
mkTable({
|
|
|
|
{"tag", helloTy},
|
|
|
|
})},
|
2023-05-20 02:59:59 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
CHECK(t1 == intersect(t1, t2));
|
|
|
|
CHECK(t1 == intersect(t2, t1));
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "union")
|
|
|
|
{
|
|
|
|
TypeId t1 = arena->addType(UnionType{{numberTy, stringTy, nilTy, tableTy}});
|
|
|
|
|
|
|
|
CHECK(nilTy == intersect(t1, nilTy));
|
|
|
|
// CHECK(nilTy == intersect(nilTy, t1)); // TODO?
|
|
|
|
|
|
|
|
CHECK(builtinTypes->stringType == intersect(builtinTypes->optionalStringType, truthyTy));
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "two_unions")
|
|
|
|
{
|
2024-03-16 05:01:00 +08:00
|
|
|
ScopedFastInt sfi{DFInt::LuauSimplificationComplexityLimit, 10};
|
2023-05-20 02:59:59 +08:00
|
|
|
TypeId t1 = arena->addType(UnionType{{numberTy, booleanTy, stringTy, nilTy, tableTy}});
|
|
|
|
|
|
|
|
CHECK("false?" == intersectStr(t1, falsyTy));
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "curious_union")
|
|
|
|
{
|
|
|
|
// (a & false) | (a & nil)
|
|
|
|
TypeId curious =
|
|
|
|
arena->addType(UnionType{{arena->addType(IntersectionType{{freeTy, falseTy}}), arena->addType(IntersectionType{{freeTy, nilTy}})}});
|
|
|
|
|
2023-08-05 01:01:35 +08:00
|
|
|
CHECK("('a & false) | ('a & nil) | number" == toString(union_(curious, numberTy)));
|
2023-05-20 02:59:59 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "negations")
|
|
|
|
{
|
|
|
|
TypeId notNumberTy = mkNegation(numberTy);
|
|
|
|
TypeId notStringTy = mkNegation(stringTy);
|
|
|
|
|
|
|
|
CHECK(neverTy == intersect(numberTy, notNumberTy));
|
|
|
|
|
|
|
|
CHECK(numberTy == intersect(numberTy, notStringTy));
|
|
|
|
CHECK(numberTy == intersect(notStringTy, numberTy));
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "top_class_type")
|
|
|
|
{
|
|
|
|
CHECK(neverTy == intersect(classTy, stringTy));
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "classes")
|
|
|
|
{
|
|
|
|
CHECK(childClassTy == intersect(childClassTy, parentClassTy));
|
|
|
|
CHECK(childClassTy == intersect(parentClassTy, childClassTy));
|
|
|
|
|
|
|
|
CHECK(parentClassTy == union_(childClassTy, parentClassTy));
|
|
|
|
CHECK(parentClassTy == union_(parentClassTy, childClassTy));
|
|
|
|
|
|
|
|
CHECK(neverTy == intersect(childClassTy, unrelatedClassTy));
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "negations_of_classes")
|
|
|
|
{
|
|
|
|
TypeId notChildClassTy = mkNegation(childClassTy);
|
|
|
|
TypeId notParentClassTy = mkNegation(parentClassTy);
|
|
|
|
|
|
|
|
CHECK(neverTy == intersect(childClassTy, notParentClassTy));
|
|
|
|
CHECK(neverTy == intersect(notParentClassTy, childClassTy));
|
|
|
|
|
|
|
|
CHECK("Parent & ~Child" == intersectStr(notChildClassTy, parentClassTy));
|
|
|
|
CHECK("Parent & ~Child" == intersectStr(parentClassTy, notChildClassTy));
|
|
|
|
|
|
|
|
CHECK(notParentClassTy == intersect(notChildClassTy, notParentClassTy));
|
|
|
|
CHECK(notParentClassTy == intersect(notParentClassTy, notChildClassTy));
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "intersection_of_intersection_of_a_free_type_can_result_in_removal_of_that_free_type")
|
|
|
|
{
|
|
|
|
// a & string and number
|
|
|
|
// (a & number) & (string & number)
|
|
|
|
|
|
|
|
TypeId t1 = arena->addType(IntersectionType{{freeTy, stringTy}});
|
|
|
|
|
|
|
|
CHECK(neverTy == intersect(t1, numberTy));
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "some_tables_are_really_never")
|
|
|
|
{
|
|
|
|
TypeId notAnyTy = mkNegation(anyTy);
|
|
|
|
|
|
|
|
TypeId t1 = mkTable({{"someKey", notAnyTy}});
|
|
|
|
|
|
|
|
CHECK(neverTy == intersect(t1, numberTy));
|
|
|
|
CHECK(neverTy == intersect(numberTy, t1));
|
2024-03-01 21:58:44 +08:00
|
|
|
CHECK(t1 == intersect(t1, t1));
|
|
|
|
|
|
|
|
TypeId notUnknownTy = mkNegation(unknownTy);
|
|
|
|
|
|
|
|
TypeId t2 = mkTable({{"someKey", notUnknownTy}});
|
|
|
|
|
|
|
|
CHECK(neverTy == intersect(t2, numberTy));
|
|
|
|
CHECK(neverTy == intersect(numberTy, t2));
|
|
|
|
CHECK(neverTy == intersect(t2, t2));
|
2023-05-20 02:59:59 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "simplify_stops_at_cycles")
|
|
|
|
{
|
|
|
|
TypeId t = mkTable({});
|
|
|
|
TableType* tt = getMutable<TableType>(t);
|
|
|
|
REQUIRE(tt);
|
|
|
|
|
|
|
|
TypeId t2 = mkTable({});
|
|
|
|
TableType* t2t = getMutable<TableType>(t2);
|
|
|
|
REQUIRE(t2t);
|
|
|
|
|
|
|
|
tt->props["cyclic"] = Property{t2};
|
|
|
|
t2t->props["cyclic"] = Property{t};
|
|
|
|
|
2024-03-01 21:58:44 +08:00
|
|
|
CHECK(t == intersect(t, unknownTy));
|
|
|
|
CHECK(t == intersect(unknownTy, t));
|
|
|
|
|
|
|
|
CHECK(t2 == intersect(t2, unknownTy));
|
|
|
|
CHECK(t2 == intersect(unknownTy, t2));
|
|
|
|
|
|
|
|
CHECK("*error-type* | t1 where t1 = { cyclic: { cyclic: t1 } }" == intersectStr(t, anyTy));
|
|
|
|
CHECK("*error-type* | t1 where t1 = { cyclic: { cyclic: t1 } }" == intersectStr(anyTy, t));
|
2023-05-20 02:59:59 +08:00
|
|
|
|
2024-03-01 21:58:44 +08:00
|
|
|
CHECK("*error-type* | t1 where t1 = { cyclic: { cyclic: t1 } }" == intersectStr(t2, anyTy));
|
|
|
|
CHECK("*error-type* | t1 where t1 = { cyclic: { cyclic: t1 } }" == intersectStr(anyTy, t2));
|
2023-05-20 02:59:59 +08:00
|
|
|
}
|
|
|
|
|
2023-08-05 01:01:35 +08:00
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "free_type_bound_by_any_with_any")
|
|
|
|
{
|
2024-03-01 21:58:44 +08:00
|
|
|
CHECK("'a | *error-type*" == intersectStr(freeTy, anyTy));
|
|
|
|
CHECK("'a | *error-type*" == intersectStr(anyTy, freeTy));
|
2023-08-05 01:01:35 +08:00
|
|
|
|
2024-03-01 21:58:44 +08:00
|
|
|
CHECK("'a | *error-type*" == intersectStr(freeTy, anyTy));
|
|
|
|
CHECK("'a | *error-type*" == intersectStr(anyTy, freeTy));
|
2023-08-05 01:01:35 +08:00
|
|
|
}
|
|
|
|
|
2024-03-09 07:57:12 +08:00
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "bound_intersected_by_itself_should_be_itself")
|
|
|
|
{
|
|
|
|
TypeId blocked = arena->addType(BlockedType{});
|
|
|
|
CHECK(toString(blocked) == intersectStr(blocked, blocked));
|
|
|
|
}
|
|
|
|
|
2024-04-12 18:44:40 +08:00
|
|
|
TEST_CASE_FIXTURE(SimplifyFixture, "cyclic_never_union_and_string")
|
|
|
|
{
|
|
|
|
// t1 where t1 = never | t1
|
|
|
|
TypeId leftType = arena->addType(UnionType{{builtinTypes->neverType, builtinTypes->neverType}});
|
|
|
|
UnionType* leftUnion = getMutable<UnionType>(leftType);
|
|
|
|
REQUIRE(leftUnion);
|
|
|
|
leftUnion->options[0] = leftType;
|
|
|
|
|
|
|
|
CHECK(builtinTypes->stringType == union_(leftType, builtinTypes->stringType));
|
|
|
|
}
|
|
|
|
|
2023-05-20 02:59:59 +08:00
|
|
|
TEST_SUITE_END();
|