luau/tests/TypeReduction.test.cpp

1510 lines
42 KiB
C++

// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/TypeReduction.h"
#include "Fixture.h"
#include "doctest.h"
using namespace Luau;
namespace
{
struct ReductionFixture : Fixture
{
TypeReductionOptions typeReductionOpts{/* allowTypeReductionsFromOtherArenas */ true};
ToStringOptions toStringOpts{true};
TypeArena arena;
InternalErrorReporter iceHandler;
UnifierSharedState unifierState{&iceHandler};
TypeReduction reduction{NotNull{&arena}, builtinTypes, NotNull{&iceHandler}, typeReductionOpts};
ReductionFixture()
{
registerHiddenTypes(&frontend);
createSomeClasses(&frontend);
}
TypeId reductionof(TypeId ty)
{
std::optional<TypeId> reducedTy = reduction.reduce(ty);
REQUIRE(reducedTy);
return *reducedTy;
}
std::optional<TypeId> tryReduce(const std::string& annotation)
{
check("type _Res = " + annotation);
return reduction.reduce(requireTypeAlias("_Res"));
}
TypeId reductionof(const std::string& annotation)
{
check("type _Res = " + annotation);
return reductionof(requireTypeAlias("_Res"));
}
std::string toStringFull(TypeId ty)
{
return toString(ty, toStringOpts);
}
};
} // namespace
TEST_SUITE_BEGIN("TypeReductionTests");
TEST_CASE_FIXTURE(ReductionFixture, "cartesian_product_exceeded")
{
ScopedFastInt sfi{"LuauTypeReductionCartesianProductLimit", 5};
CheckResult result = check(R"(
type T
= string
& (number | string | boolean)
& (number | string | boolean)
)");
CHECK(!reduction.reduce(requireTypeAlias("T")));
// LUAU_REQUIRE_ERROR_COUNT(1, result);
// CHECK("Code is too complex to typecheck! Consider simplifying the code around this area" == toString(result.errors[0]));
}
TEST_CASE_FIXTURE(ReductionFixture, "cartesian_product_exceeded_with_normal_limit")
{
CheckResult result = check(R"(
type T
= string -- 1 = 1
& (number | string | boolean) -- 1 * 3 = 3
& (number | string | boolean) -- 3 * 3 = 9
& (number | string | boolean) -- 9 * 3 = 27
& (number | string | boolean) -- 27 * 3 = 81
& (number | string | boolean) -- 81 * 3 = 243
& (number | string | boolean) -- 243 * 3 = 729
& (number | string | boolean) -- 729 * 3 = 2187
& (number | string | boolean) -- 2187 * 3 = 6561
& (number | string | boolean) -- 6561 * 3 = 19683
& (number | string | boolean) -- 19683 * 3 = 59049
& (number | string) -- 59049 * 2 = 118098
)");
CHECK(!reduction.reduce(requireTypeAlias("T")));
// LUAU_REQUIRE_ERROR_COUNT(1, result);
// CHECK("Code is too complex to typecheck! Consider simplifying the code around this area" == toString(result.errors[0]));
}
TEST_CASE_FIXTURE(ReductionFixture, "cartesian_product_is_zero")
{
ScopedFastInt sfi{"LuauTypeReductionCartesianProductLimit", 5};
CheckResult result = check(R"(
type T
= string
& (number | string | boolean)
& (number | string | boolean)
& never
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(ReductionFixture, "stress_test_recursion_limits")
{
TypeId ty = arena.addType(IntersectionType{{builtinTypes->numberType, builtinTypes->stringType}});
for (size_t i = 0; i < 20'000; ++i)
{
TableType table;
table.state = TableState::Sealed;
table.props["x"] = {ty};
ty = arena.addType(IntersectionType{{arena.addType(table), arena.addType(table)}});
}
CHECK(!reduction.reduce(ty));
}
TEST_CASE_FIXTURE(ReductionFixture, "caching")
{
SUBCASE("free_tables")
{
TypeId ty1 = arena.addType(TableType{});
getMutable<TableType>(ty1)->state = TableState::Free;
getMutable<TableType>(ty1)->props["x"] = {builtinTypes->stringType};
TypeId ty2 = arena.addType(TableType{});
getMutable<TableType>(ty2)->state = TableState::Sealed;
TypeId intersectionTy = arena.addType(IntersectionType{{ty1, ty2}});
CHECK("{- x: string -} & {| |}" == toStringFull(reductionof(intersectionTy)));
getMutable<TableType>(ty1)->state = TableState::Sealed;
CHECK("{| x: string |}" == toStringFull(reductionof(intersectionTy)));
}
SUBCASE("unsealed_tables")
{
TypeId ty1 = arena.addType(TableType{});
getMutable<TableType>(ty1)->state = TableState::Unsealed;
getMutable<TableType>(ty1)->props["x"] = {builtinTypes->stringType};
TypeId ty2 = arena.addType(TableType{});
getMutable<TableType>(ty2)->state = TableState::Sealed;
TypeId intersectionTy = arena.addType(IntersectionType{{ty1, ty2}});
CHECK("{| x: string |}" == toStringFull(reductionof(intersectionTy)));
getMutable<TableType>(ty1)->state = TableState::Sealed;
CHECK("{| x: string |}" == toStringFull(reductionof(intersectionTy)));
}
SUBCASE("free_types")
{
TypeId ty1 = arena.freshType(nullptr);
TypeId ty2 = arena.addType(TableType{});
getMutable<TableType>(ty2)->state = TableState::Sealed;
TypeId intersectionTy = arena.addType(IntersectionType{{ty1, ty2}});
CHECK("a & {| |}" == toStringFull(reductionof(intersectionTy)));
*asMutable(ty1) = BoundType{ty2};
CHECK("{| |}" == toStringFull(reductionof(intersectionTy)));
}
SUBCASE("we_can_see_that_the_cache_works_if_we_mutate_a_normally_not_mutated_type")
{
TypeId ty1 = arena.addType(BoundType{builtinTypes->stringType});
TypeId ty2 = builtinTypes->numberType;
TypeId intersectionTy = arena.addType(IntersectionType{{ty1, ty2}});
CHECK("never" == toStringFull(reductionof(intersectionTy))); // Bound<string> & number ~ never
*asMutable(ty1) = BoundType{ty2};
CHECK("never" == toStringFull(reductionof(intersectionTy))); // Bound<number> & number ~ number, but the cache is `never`.
}
SUBCASE("ptr_eq_irreducible_unions")
{
TypeId unionTy = arena.addType(UnionType{{builtinTypes->stringType, builtinTypes->numberType}});
TypeId reducedTy = reductionof(unionTy);
REQUIRE(unionTy == reducedTy);
}
SUBCASE("ptr_eq_irreducible_intersections")
{
TypeId intersectionTy = arena.addType(IntersectionType{{builtinTypes->stringType, arena.addType(GenericType{"G"})}});
TypeId reducedTy = reductionof(intersectionTy);
REQUIRE(intersectionTy == reducedTy);
}
SUBCASE("ptr_eq_free_table")
{
TypeId tableTy = arena.addType(TableType{});
getMutable<TableType>(tableTy)->state = TableState::Free;
TypeId reducedTy = reductionof(tableTy);
REQUIRE(tableTy == reducedTy);
}
SUBCASE("ptr_eq_unsealed_table")
{
TypeId tableTy = arena.addType(TableType{});
getMutable<TableType>(tableTy)->state = TableState::Unsealed;
TypeId reducedTy = reductionof(tableTy);
REQUIRE(tableTy == reducedTy);
}
} // caching
TEST_CASE_FIXTURE(ReductionFixture, "intersections_without_negations")
{
SUBCASE("string_and_string")
{
TypeId ty = reductionof("string & string");
CHECK("string" == toStringFull(ty));
}
SUBCASE("never_and_string")
{
TypeId ty = reductionof("never & string");
CHECK("never" == toStringFull(ty));
}
SUBCASE("string_and_never")
{
TypeId ty = reductionof("string & never");
CHECK("never" == toStringFull(ty));
}
SUBCASE("unknown_and_string")
{
TypeId ty = reductionof("unknown & string");
CHECK("string" == toStringFull(ty));
}
SUBCASE("string_and_unknown")
{
TypeId ty = reductionof("string & unknown");
CHECK("string" == toStringFull(ty));
}
SUBCASE("any_and_string")
{
TypeId ty = reductionof("any & string");
CHECK("string" == toStringFull(ty));
}
SUBCASE("string_and_any")
{
TypeId ty = reductionof("string & any");
CHECK("string" == toStringFull(ty));
}
SUBCASE("string_or_number_and_string")
{
TypeId ty = reductionof("(string | number) & string");
CHECK("string" == toStringFull(ty));
}
SUBCASE("string_and_string_or_number")
{
TypeId ty = reductionof("string & (string | number)");
CHECK("string" == toStringFull(ty));
}
SUBCASE("string_and_a")
{
TypeId ty = reductionof(R"(string & "a")");
CHECK(R"("a")" == toStringFull(ty));
}
SUBCASE("boolean_and_true")
{
TypeId ty = reductionof("boolean & true");
CHECK("true" == toStringFull(ty));
}
SUBCASE("boolean_and_a")
{
TypeId ty = reductionof(R"(boolean & "a")");
CHECK("never" == toStringFull(ty));
}
SUBCASE("a_and_a")
{
TypeId ty = reductionof(R"("a" & "a")");
CHECK(R"("a")" == toStringFull(ty));
}
SUBCASE("a_and_b")
{
TypeId ty = reductionof(R"("a" & "b")");
CHECK("never" == toStringFull(ty));
}
SUBCASE("a_and_true")
{
TypeId ty = reductionof(R"("a" & true)");
CHECK("never" == toStringFull(ty));
}
SUBCASE("a_and_true")
{
TypeId ty = reductionof(R"(true & false)");
CHECK("never" == toStringFull(ty));
}
SUBCASE("function_type_and_function")
{
TypeId ty = reductionof("() -> () & fun");
CHECK("() -> ()" == toStringFull(ty));
}
SUBCASE("function_type_and_string")
{
TypeId ty = reductionof("() -> () & string");
CHECK("never" == toStringFull(ty));
}
SUBCASE("parent_and_child")
{
TypeId ty = reductionof("Parent & Child");
CHECK("Child" == toStringFull(ty));
}
SUBCASE("child_and_parent")
{
TypeId ty = reductionof("Child & Parent");
CHECK("Child" == toStringFull(ty));
}
SUBCASE("child_and_unrelated")
{
TypeId ty = reductionof("Child & Unrelated");
CHECK("never" == toStringFull(ty));
}
SUBCASE("string_and_table")
{
TypeId ty = reductionof("string & {}");
CHECK("never" == toStringFull(ty));
}
SUBCASE("string_and_child")
{
TypeId ty = reductionof("string & Child");
CHECK("never" == toStringFull(ty));
}
SUBCASE("string_and_function")
{
TypeId ty = reductionof("string & () -> ()");
CHECK("never" == toStringFull(ty));
}
SUBCASE("function_and_table")
{
TypeId ty = reductionof("() -> () & {}");
CHECK("never" == toStringFull(ty));
}
SUBCASE("function_and_class")
{
TypeId ty = reductionof("() -> () & Child");
CHECK("never" == toStringFull(ty));
}
SUBCASE("function_and_function")
{
TypeId ty = reductionof("() -> () & () -> ()");
CHECK("(() -> ()) & (() -> ())" == toStringFull(ty));
}
SUBCASE("table_and_table")
{
TypeId ty = reductionof("{} & {}");
CHECK("{| |}" == toStringFull(ty));
}
SUBCASE("table_and_metatable")
{
// No setmetatable in ReductionFixture, so we mix and match.
BuiltinsFixture fixture;
fixture.check(R"(
type Ty = {} & typeof(setmetatable({}, {}))
)");
TypeId ty = reductionof(fixture.requireTypeAlias("Ty"));
CHECK("{ @metatable { }, { } } & {| |}" == toStringFull(ty));
}
SUBCASE("a_and_string")
{
TypeId ty = reductionof(R"("a" & string)");
CHECK(R"("a")" == toStringFull(ty));
}
SUBCASE("reducible_function_and_function")
{
TypeId ty = reductionof("((string | string) -> (number | number)) & fun");
CHECK("(string) -> number" == toStringFull(ty));
}
SUBCASE("string_and_error")
{
TypeId ty = reductionof("string & err");
CHECK("*error-type* & string" == toStringFull(ty));
}
SUBCASE("table_p_string_and_table_p_number")
{
TypeId ty = reductionof("{ p: string } & { p: number }");
CHECK("never" == toStringFull(ty));
}
SUBCASE("table_p_string_and_table_p_string")
{
TypeId ty = reductionof("{ p: string } & { p: string }");
CHECK("{| p: string |}" == toStringFull(ty));
}
SUBCASE("table_x_table_p_string_and_table_x_table_p_number")
{
TypeId ty = reductionof("{ x: { p: string } } & { x: { p: number } }");
CHECK("never" == toStringFull(ty));
}
SUBCASE("table_p_and_table_q")
{
TypeId ty = reductionof("{ p: string } & { q: number }");
CHECK("{| p: string, q: number |}" == toStringFull(ty));
}
SUBCASE("table_tag_a_or_table_tag_b_and_table_b")
{
TypeId ty = reductionof("({ tag: string, a: number } | { tag: number, b: string }) & { b: string }");
CHECK("{| a: number, b: string, tag: string |} | {| b: string, tag: number |}" == toStringFull(ty));
}
SUBCASE("table_string_number_indexer_and_table_string_number_indexer")
{
TypeId ty = reductionof("{ [string]: number } & { [string]: number }");
CHECK("{| [string]: number |}" == toStringFull(ty));
}
SUBCASE("table_string_number_indexer_and_empty_table")
{
TypeId ty = reductionof("{ [string]: number } & {}");
CHECK("{| [string]: number |}" == toStringFull(ty));
}
SUBCASE("empty_table_table_string_number_indexer")
{
TypeId ty = reductionof("{} & { [string]: number }");
CHECK("{| [string]: number |}" == toStringFull(ty));
}
SUBCASE("string_number_indexer_and_number_number_indexer")
{
TypeId ty = reductionof("{ [string]: number } & { [number]: number }");
CHECK("{number} & {| [string]: number |}" == toStringFull(ty));
}
SUBCASE("table_p_string_and_indexer_number_number")
{
TypeId ty = reductionof("{ p: string } & { [number]: number }");
CHECK("{| [number]: number, p: string |}" == toStringFull(ty));
}
SUBCASE("table_p_string_and_indexer_string_number")
{
TypeId ty = reductionof("{ p: string } & { [string]: number }");
CHECK("{| [string]: number, p: string |}" == toStringFull(ty));
}
SUBCASE("table_p_string_and_table_p_string_plus_indexer_string_number")
{
TypeId ty = reductionof("{ p: string } & { p: string, [string]: number }");
CHECK("{| [string]: number, p: string |}" == toStringFull(ty));
}
SUBCASE("array_number_and_array_string")
{
TypeId ty = reductionof("{number} & {string}");
CHECK("{never}" == toStringFull(ty));
}
SUBCASE("array_string_and_array_string")
{
TypeId ty = reductionof("{string} & {string}");
CHECK("{string}" == toStringFull(ty));
}
SUBCASE("array_string_or_number_and_array_string")
{
TypeId ty = reductionof("{string | number} & {string}");
CHECK("{string}" == toStringFull(ty));
}
SUBCASE("fresh_type_and_string")
{
TypeId freshTy = arena.freshType(nullptr);
TypeId ty = reductionof(arena.addType(IntersectionType{{freshTy, builtinTypes->stringType}}));
CHECK("a & string" == toStringFull(ty));
}
SUBCASE("string_and_fresh_type")
{
TypeId freshTy = arena.freshType(nullptr);
TypeId ty = reductionof(arena.addType(IntersectionType{{builtinTypes->stringType, freshTy}}));
CHECK("a & string" == toStringFull(ty));
}
SUBCASE("generic_and_string")
{
TypeId genericTy = arena.addType(GenericType{"G"});
TypeId ty = reductionof(arena.addType(IntersectionType{{genericTy, builtinTypes->stringType}}));
CHECK("G & string" == toStringFull(ty));
}
SUBCASE("string_and_generic")
{
TypeId genericTy = arena.addType(GenericType{"G"});
TypeId ty = reductionof(arena.addType(IntersectionType{{builtinTypes->stringType, genericTy}}));
CHECK("G & string" == toStringFull(ty));
}
SUBCASE("parent_and_child_or_parent_and_anotherchild_or_parent_and_unrelated")
{
TypeId ty = reductionof("Parent & (Child | AnotherChild | Unrelated)");
CHECK("AnotherChild | Child" == toString(ty));
}
SUBCASE("parent_and_child_or_parent_and_anotherchild_or_parent_and_unrelated_2")
{
TypeId ty = reductionof("(Parent & Child) | (Parent & AnotherChild) | (Parent & Unrelated)");
CHECK("AnotherChild | Child" == toString(ty));
}
SUBCASE("top_table_and_table")
{
TypeId ty = reductionof("tbl & {}");
CHECK("{| |}" == toString(ty));
}
SUBCASE("top_table_and_non_table")
{
TypeId ty = reductionof("tbl & \"foo\"");
CHECK("never" == toString(ty));
}
SUBCASE("top_table_and_metatable")
{
BuiltinsFixture fixture;
registerHiddenTypes(&fixture.frontend);
fixture.check(R"(
type Ty = tbl & typeof(setmetatable({}, {}))
)");
TypeId ty = reductionof(fixture.requireTypeAlias("Ty"));
CHECK("{ @metatable { }, { } }" == toString(ty));
}
} // intersections_without_negations
TEST_CASE_FIXTURE(ReductionFixture, "intersections_with_negations")
{
SUBCASE("nil_and_not_nil")
{
TypeId ty = reductionof("nil & Not<nil>");
CHECK("never" == toStringFull(ty));
}
SUBCASE("nil_and_not_false")
{
TypeId ty = reductionof("nil & Not<false>");
CHECK("nil" == toStringFull(ty));
}
SUBCASE("string_or_nil_and_not_nil")
{
TypeId ty = reductionof("(string?) & Not<nil>");
CHECK("string" == toStringFull(ty));
}
SUBCASE("string_or_nil_and_not_false_or_nil")
{
TypeId ty = reductionof("(string?) & Not<false | nil>");
CHECK("string" == toStringFull(ty));
}
SUBCASE("string_or_nil_and_not_false_and_not_nil")
{
TypeId ty = reductionof("(string?) & Not<false> & Not<nil>");
CHECK("string" == toStringFull(ty));
}
SUBCASE("not_false_and_bool")
{
TypeId ty = reductionof("Not<false> & boolean");
CHECK("true" == toStringFull(ty));
}
SUBCASE("function_type_and_not_function")
{
TypeId ty = reductionof("() -> () & Not<fun>");
CHECK("never" == toStringFull(ty));
}
SUBCASE("function_type_and_not_string")
{
TypeId ty = reductionof("() -> () & Not<string>");
CHECK("() -> ()" == toStringFull(ty));
}
SUBCASE("not_a_and_string_or_nil")
{
TypeId ty = reductionof(R"(Not<"a"> & (string | nil))");
CHECK(R"((string & ~"a")?)" == toStringFull(ty));
}
SUBCASE("not_a_and_a")
{
TypeId ty = reductionof(R"(Not<"a"> & "a")");
CHECK("never" == toStringFull(ty));
}
SUBCASE("not_a_and_b")
{
TypeId ty = reductionof(R"(Not<"a"> & "b")");
CHECK(R"("b")" == toStringFull(ty));
}
SUBCASE("not_string_and_a")
{
TypeId ty = reductionof(R"(Not<string> & "a")");
CHECK("never" == toStringFull(ty));
}
SUBCASE("not_bool_and_true")
{
TypeId ty = reductionof("Not<boolean> & true");
CHECK("never" == toStringFull(ty));
}
SUBCASE("not_string_and_true")
{
TypeId ty = reductionof("Not<string> & true");
CHECK("true" == toStringFull(ty));
}
SUBCASE("parent_and_not_child")
{
TypeId ty = reductionof("Parent & Not<Child>");
CHECK("Parent & ~Child" == toStringFull(ty));
}
SUBCASE("not_child_and_parent")
{
TypeId ty = reductionof("Not<Child> & Parent");
CHECK("Parent & ~Child" == toStringFull(ty));
}
SUBCASE("child_and_not_parent")
{
TypeId ty = reductionof("Child & Not<Parent>");
CHECK("never" == toStringFull(ty));
}
SUBCASE("not_parent_and_child")
{
TypeId ty = reductionof("Not<Parent> & Child");
CHECK("never" == toStringFull(ty));
}
SUBCASE("not_parent_and_unrelated")
{
TypeId ty = reductionof("Not<Parent> & Unrelated");
CHECK("Unrelated" == toStringFull(ty));
}
SUBCASE("unrelated_and_not_parent")
{
TypeId ty = reductionof("Unrelated & Not<Parent>");
CHECK("Unrelated" == toStringFull(ty));
}
SUBCASE("not_unrelated_and_parent")
{
TypeId ty = reductionof("Not<Unrelated> & Parent");
CHECK("Parent" == toStringFull(ty));
}
SUBCASE("parent_and_not_unrelated")
{
TypeId ty = reductionof("Parent & Not<Unrelated>");
CHECK("Parent" == toStringFull(ty));
}
SUBCASE("reducible_function_and_not_function")
{
TypeId ty = reductionof("((string | string) -> (number | number)) & Not<fun>");
CHECK("never" == toStringFull(ty));
}
SUBCASE("string_and_not_error")
{
TypeId ty = reductionof("string & Not<err>");
CHECK("string" == toStringFull(ty));
}
SUBCASE("table_p_string_and_table_p_not_number")
{
TypeId ty = reductionof("{ p: string } & { p: Not<number> }");
CHECK("{| p: string |}" == toStringFull(ty));
}
SUBCASE("table_p_string_and_table_p_not_string")
{
TypeId ty = reductionof("{ p: string } & { p: Not<string> }");
CHECK("never" == toStringFull(ty));
}
SUBCASE("table_x_table_p_string_and_table_x_table_p_not_number")
{
TypeId ty = reductionof("{ x: { p: string } } & { x: { p: Not<number> } }");
CHECK("{| x: {| p: string |} |}" == toStringFull(ty));
}
SUBCASE("table_or_nil_and_truthy")
{
TypeId ty = reductionof("({ x: number | string }?) & Not<false?>");
CHECK("{| x: number | string |}" == toString(ty));
}
SUBCASE("not_top_table_and_table")
{
TypeId ty = reductionof("Not<tbl> & {}");
CHECK("never" == toString(ty));
}
SUBCASE("not_top_table_and_metatable")
{
BuiltinsFixture fixture;
registerHiddenTypes(&fixture.frontend);
fixture.check(R"(
type Ty = Not<tbl> & typeof(setmetatable({}, {}))
)");
TypeId ty = reductionof(fixture.requireTypeAlias("Ty"));
CHECK("never" == toString(ty));
}
} // intersections_with_negations
TEST_CASE_FIXTURE(ReductionFixture, "unions_without_negations")
{
SUBCASE("never_or_string")
{
TypeId ty = reductionof("never | string");
CHECK("string" == toStringFull(ty));
}
SUBCASE("string_or_never")
{
TypeId ty = reductionof("string | never");
CHECK("string" == toStringFull(ty));
}
SUBCASE("unknown_or_string")
{
TypeId ty = reductionof("unknown | string");
CHECK("unknown" == toStringFull(ty));
}
SUBCASE("string_or_unknown")
{
TypeId ty = reductionof("string | unknown");
CHECK("unknown" == toStringFull(ty));
}
SUBCASE("any_or_string")
{
TypeId ty = reductionof("any | string");
CHECK("any" == toStringFull(ty));
}
SUBCASE("string_or_any")
{
TypeId ty = reductionof("string | any");
CHECK("any" == toStringFull(ty));
}
SUBCASE("string_or_string_and_number")
{
TypeId ty = reductionof("string | (string & number)");
CHECK("string" == toStringFull(ty));
}
SUBCASE("string_or_string")
{
TypeId ty = reductionof("string | string");
CHECK("string" == toStringFull(ty));
}
SUBCASE("string_or_number")
{
TypeId ty = reductionof("string | number");
CHECK("number | string" == toStringFull(ty));
}
SUBCASE("number_or_string")
{
TypeId ty = reductionof("number | string");
CHECK("number | string" == toStringFull(ty));
}
SUBCASE("string_or_number_or_string")
{
TypeId ty = reductionof("(string | number) | string");
CHECK("number | string" == toStringFull(ty));
}
SUBCASE("string_or_number_or_string_2")
{
TypeId ty = reductionof("string | (number | string)");
CHECK("number | string" == toStringFull(ty));
}
SUBCASE("string_or_string_or_number")
{
TypeId ty = reductionof("string | (string | number)");
CHECK("number | string" == toStringFull(ty));
}
SUBCASE("string_or_string_or_number_or_boolean")
{
TypeId ty = reductionof("string | (string | number | boolean)");
CHECK("boolean | number | string" == toStringFull(ty));
}
SUBCASE("string_or_string_or_boolean_or_number")
{
TypeId ty = reductionof("string | (string | boolean | number)");
CHECK("boolean | number | string" == toStringFull(ty));
}
SUBCASE("string_or_boolean_or_string_or_number")
{
TypeId ty = reductionof("string | (boolean | string | number)");
CHECK("boolean | number | string" == toStringFull(ty));
}
SUBCASE("boolean_or_string_or_number_or_string")
{
TypeId ty = reductionof("(boolean | string | number) | string");
CHECK("boolean | number | string" == toStringFull(ty));
}
SUBCASE("boolean_or_true")
{
TypeId ty = reductionof("boolean | true");
CHECK("boolean" == toStringFull(ty));
}
SUBCASE("boolean_or_false")
{
TypeId ty = reductionof("boolean | false");
CHECK("boolean" == toStringFull(ty));
}
SUBCASE("boolean_or_true_or_false")
{
TypeId ty = reductionof("boolean | true | false");
CHECK("boolean" == toStringFull(ty));
}
SUBCASE("string_or_a")
{
TypeId ty = reductionof(R"(string | "a")");
CHECK("string" == toStringFull(ty));
}
SUBCASE("a_or_a")
{
TypeId ty = reductionof(R"("a" | "a")");
CHECK(R"("a")" == toStringFull(ty));
}
SUBCASE("a_or_b")
{
TypeId ty = reductionof(R"("a" | "b")");
CHECK(R"("a" | "b")" == toStringFull(ty));
}
SUBCASE("a_or_b_or_string")
{
TypeId ty = reductionof(R"("a" | "b" | string)");
CHECK("string" == toStringFull(ty));
}
SUBCASE("unknown_or_any")
{
TypeId ty = reductionof("unknown | any");
CHECK("unknown" == toStringFull(ty));
}
SUBCASE("any_or_unknown")
{
TypeId ty = reductionof("any | unknown");
CHECK("unknown" == toStringFull(ty));
}
SUBCASE("function_type_or_function")
{
TypeId ty = reductionof("() -> () | fun");
CHECK("function" == toStringFull(ty));
}
SUBCASE("function_or_string")
{
TypeId ty = reductionof("fun | string");
CHECK("function | string" == toStringFull(ty));
}
SUBCASE("parent_or_child")
{
TypeId ty = reductionof("Parent | Child");
CHECK("Parent" == toStringFull(ty));
}
SUBCASE("child_or_parent")
{
TypeId ty = reductionof("Child | Parent");
CHECK("Parent" == toStringFull(ty));
}
SUBCASE("parent_or_unrelated")
{
TypeId ty = reductionof("Parent | Unrelated");
CHECK("Parent | Unrelated" == toStringFull(ty));
}
SUBCASE("parent_or_child_or_unrelated")
{
TypeId ty = reductionof("Parent | Child | Unrelated");
CHECK("Parent | Unrelated" == toStringFull(ty));
}
SUBCASE("parent_or_unrelated_or_child")
{
TypeId ty = reductionof("Parent | Unrelated | Child");
CHECK("Parent | Unrelated" == toStringFull(ty));
}
SUBCASE("parent_or_child_or_unrelated_or_child")
{
TypeId ty = reductionof("Parent | Child | Unrelated | Child");
CHECK("Parent | Unrelated" == toStringFull(ty));
}
SUBCASE("string_or_true")
{
TypeId ty = reductionof("string | true");
CHECK("string | true" == toStringFull(ty));
}
SUBCASE("string_or_function")
{
TypeId ty = reductionof("string | () -> ()");
CHECK("(() -> ()) | string" == toStringFull(ty));
}
SUBCASE("string_or_err")
{
TypeId ty = reductionof("string | err");
CHECK("*error-type* | string" == toStringFull(ty));
}
SUBCASE("top_table_or_table")
{
TypeId ty = reductionof("tbl | {}");
CHECK("table" == toString(ty));
}
SUBCASE("top_table_or_metatable")
{
BuiltinsFixture fixture;
registerHiddenTypes(&fixture.frontend);
fixture.check(R"(
type Ty = tbl | typeof(setmetatable({}, {}))
)");
TypeId ty = reductionof(fixture.requireTypeAlias("Ty"));
CHECK("table" == toString(ty));
}
SUBCASE("top_table_or_non_table")
{
TypeId ty = reductionof("tbl | number");
CHECK("number | table" == toString(ty));
}
} // unions_without_negations
TEST_CASE_FIXTURE(ReductionFixture, "unions_with_negations")
{
SUBCASE("string_or_not_string")
{
TypeId ty = reductionof("string | Not<string>");
CHECK("unknown" == toStringFull(ty));
}
SUBCASE("not_string_or_string")
{
TypeId ty = reductionof("Not<string> | string");
CHECK("unknown" == toStringFull(ty));
}
SUBCASE("not_number_or_string")
{
TypeId ty = reductionof("Not<number> | string");
CHECK("~number" == toStringFull(ty));
}
SUBCASE("string_or_not_number")
{
TypeId ty = reductionof("string | Not<number>");
CHECK("~number" == toStringFull(ty));
}
SUBCASE("not_hi_or_string_and_not_hi")
{
TypeId ty = reductionof(R"(Not<"hi"> | (string & Not<"hi">))");
CHECK(R"(~"hi")" == toStringFull(ty));
}
SUBCASE("string_and_not_hi_or_not_hi")
{
TypeId ty = reductionof(R"((string & Not<"hi">) | Not<"hi">)");
CHECK(R"(~"hi")" == toStringFull(ty));
}
SUBCASE("string_or_not_never")
{
TypeId ty = reductionof("string | Not<never>");
CHECK("unknown" == toStringFull(ty));
}
SUBCASE("not_a_or_not_a")
{
TypeId ty = reductionof(R"(Not<"a"> | Not<"a">)");
CHECK(R"(~"a")" == toStringFull(ty));
}
SUBCASE("not_a_or_a")
{
TypeId ty = reductionof(R"(Not<"a"> | "a")");
CHECK("unknown" == toStringFull(ty));
}
SUBCASE("a_or_not_a")
{
TypeId ty = reductionof(R"("a" | Not<"a">)");
CHECK("unknown" == toStringFull(ty));
}
SUBCASE("not_a_or_string")
{
TypeId ty = reductionof(R"(Not<"a"> | string)");
CHECK("unknown" == toStringFull(ty));
}
SUBCASE("string_or_not_a")
{
TypeId ty = reductionof(R"(string | Not<"a">)");
CHECK("unknown" == toStringFull(ty));
}
SUBCASE("not_string_or_a")
{
TypeId ty = reductionof(R"(Not<string> | "a")");
CHECK(R"("a" | ~string)" == toStringFull(ty));
}
SUBCASE("a_or_not_string")
{
TypeId ty = reductionof(R"("a" | Not<string>)");
CHECK(R"("a" | ~string)" == toStringFull(ty));
}
SUBCASE("not_number_or_a")
{
TypeId ty = reductionof(R"(Not<number> | "a")");
CHECK("~number" == toStringFull(ty));
}
SUBCASE("a_or_not_number")
{
TypeId ty = reductionof(R"("a" | Not<number>)");
CHECK("~number" == toStringFull(ty));
}
SUBCASE("not_a_or_not_b")
{
TypeId ty = reductionof(R"(Not<"a"> | Not<"b">)");
CHECK("unknown" == toStringFull(ty));
}
SUBCASE("boolean_or_not_false")
{
TypeId ty = reductionof("boolean | Not<false>");
CHECK("unknown" == toStringFull(ty));
}
SUBCASE("boolean_or_not_true")
{
TypeId ty = reductionof("boolean | Not<true>");
CHECK("unknown" == toStringFull(ty));
}
SUBCASE("false_or_not_false")
{
TypeId ty = reductionof("false | Not<false>");
CHECK("unknown" == toStringFull(ty));
}
SUBCASE("true_or_not_false")
{
TypeId ty = reductionof("true | Not<false>");
CHECK("~false" == toStringFull(ty));
}
SUBCASE("not_boolean_or_true")
{
TypeId ty = reductionof("Not<boolean> | true");
CHECK("~false" == toStringFull(ty));
}
SUBCASE("not_false_or_not_boolean")
{
TypeId ty = reductionof("Not<false> | Not<boolean>");
CHECK("~false" == toStringFull(ty));
}
SUBCASE("function_type_or_not_function")
{
TypeId ty = reductionof("() -> () | Not<fun>");
CHECK("(() -> ()) | ~function" == toStringFull(ty));
}
SUBCASE("not_parent_or_child")
{
TypeId ty = reductionof("Not<Parent> | Child");
CHECK("Child | ~Parent" == toStringFull(ty));
}
SUBCASE("child_or_not_parent")
{
TypeId ty = reductionof("Child | Not<Parent>");
CHECK("Child | ~Parent" == toStringFull(ty));
}
SUBCASE("parent_or_not_child")
{
TypeId ty = reductionof("Parent | Not<Child>");
CHECK("unknown" == toStringFull(ty));
}
SUBCASE("not_child_or_parent")
{
TypeId ty = reductionof("Not<Child> | Parent");
CHECK("unknown" == toStringFull(ty));
}
SUBCASE("parent_or_not_unrelated")
{
TypeId ty = reductionof("Parent | Not<Unrelated>");
CHECK("~Unrelated" == toStringFull(ty));
}
SUBCASE("not_string_or_string_and_not_a")
{
TypeId ty = reductionof(R"(Not<string> | (string & Not<"a">))");
CHECK(R"(~"a")" == toStringFull(ty));
}
SUBCASE("not_string_or_not_string")
{
TypeId ty = reductionof("Not<string> | Not<string>");
CHECK("~string" == toStringFull(ty));
}
SUBCASE("not_string_or_not_number")
{
TypeId ty = reductionof("Not<string> | Not<number>");
CHECK("unknown" == toStringFull(ty));
}
SUBCASE("not_a_or_not_boolean")
{
TypeId ty = reductionof(R"(Not<"a"> | Not<boolean>)");
CHECK("unknown" == toStringFull(ty));
}
SUBCASE("not_a_or_boolean")
{
TypeId ty = reductionof(R"(Not<"a"> | boolean)");
CHECK(R"(~"a")" == toStringFull(ty));
}
SUBCASE("string_or_err")
{
TypeId ty = reductionof("string | Not<err>");
CHECK("string | ~*error-type*" == toStringFull(ty));
}
SUBCASE("not_top_table_or_table")
{
TypeId ty = reductionof("Not<tbl> | {}");
CHECK("{| |} | ~table" == toString(ty));
}
SUBCASE("not_top_table_or_metatable")
{
BuiltinsFixture fixture;
registerHiddenTypes(&fixture.frontend);
fixture.check(R"(
type Ty = Not<tbl> | typeof(setmetatable({}, {}))
)");
TypeId ty = reductionof(fixture.requireTypeAlias("Ty"));
CHECK("{ @metatable { }, { } } | ~table" == toString(ty));
}
} // unions_with_negations
TEST_CASE_FIXTURE(ReductionFixture, "tables")
{
SUBCASE("reduce_props")
{
TypeId ty = reductionof("{ x: string | string, y: number | number }");
CHECK("{| x: string, y: number |}" == toStringFull(ty));
}
SUBCASE("reduce_indexers")
{
TypeId ty = reductionof("{ [string | string]: number | number }");
CHECK("{| [string]: number |}" == toStringFull(ty));
}
SUBCASE("reduce_instantiated_type_parameters")
{
check(R"(
type Foo<T> = { x: T }
local foo: Foo<string | string> = { x = "hello" }
)");
TypeId ty = reductionof(requireType("foo"));
CHECK("Foo<string>" == toString(ty));
}
SUBCASE("reduce_instantiated_type_pack_parameters")
{
check(R"(
type Foo<T...> = { x: () -> T... }
local foo: Foo<string | string, number | number> = { x = function() return "hi", 5 end }
)");
TypeId ty = reductionof(requireType("foo"));
CHECK("Foo<string, number>" == toString(ty));
}
SUBCASE("reduce_tables_within_tables")
{
TypeId ty = reductionof("{ x: { y: string & number } }");
CHECK("never" == toStringFull(ty));
}
SUBCASE("array_of_never")
{
TypeId ty = reductionof("{never}");
CHECK("{never}" == toStringFull(ty));
}
}
TEST_CASE_FIXTURE(ReductionFixture, "metatables")
{
SUBCASE("reduce_table_part")
{
TableType table;
table.state = TableState::Sealed;
table.props["x"] = {arena.addType(UnionType{{builtinTypes->stringType, builtinTypes->stringType}})};
TypeId tableTy = arena.addType(std::move(table));
TypeId ty = reductionof(arena.addType(MetatableType{tableTy, arena.addType(TableType{})}));
CHECK("{ @metatable { }, {| x: string |} }" == toStringFull(ty));
}
SUBCASE("reduce_metatable_part")
{
TableType table;
table.state = TableState::Sealed;
table.props["x"] = {arena.addType(UnionType{{builtinTypes->stringType, builtinTypes->stringType}})};
TypeId tableTy = arena.addType(std::move(table));
TypeId ty = reductionof(arena.addType(MetatableType{arena.addType(TableType{}), tableTy}));
CHECK("{ @metatable {| x: string |}, { } }" == toStringFull(ty));
}
}
TEST_CASE_FIXTURE(ReductionFixture, "functions")
{
SUBCASE("reduce_parameters")
{
TypeId ty = reductionof("(string | string) -> ()");
CHECK("(string) -> ()" == toStringFull(ty));
}
SUBCASE("reduce_returns")
{
TypeId ty = reductionof("() -> (string | string)");
CHECK("() -> string" == toStringFull(ty));
}
SUBCASE("reduce_parameters_and_returns")
{
TypeId ty = reductionof("(string | string) -> (number | number)");
CHECK("(string) -> number" == toStringFull(ty));
}
SUBCASE("reduce_tail")
{
TypeId ty = reductionof("() -> ...(string | string)");
CHECK("() -> (...string)" == toStringFull(ty));
}
SUBCASE("reduce_head_and_tail")
{
TypeId ty = reductionof("() -> (string | string, number | number, ...(boolean | boolean))");
CHECK("() -> (string, number, ...boolean)" == toStringFull(ty));
}
SUBCASE("reduce_overloaded_functions")
{
TypeId ty = reductionof("((number | number) -> ()) & ((string | string) -> ())");
CHECK("((number) -> ()) & ((string) -> ())" == toStringFull(ty));
}
} // functions
TEST_CASE_FIXTURE(ReductionFixture, "negations")
{
SUBCASE("not_unknown")
{
TypeId ty = reductionof("Not<unknown>");
CHECK("never" == toStringFull(ty));
}
SUBCASE("not_never")
{
TypeId ty = reductionof("Not<never>");
CHECK("unknown" == toStringFull(ty));
}
SUBCASE("not_any")
{
TypeId ty = reductionof("Not<any>");
CHECK("any" == toStringFull(ty));
}
SUBCASE("not_not_reduction")
{
TypeId ty = reductionof("Not<Not<never>>");
CHECK("never" == toStringFull(ty));
}
SUBCASE("not_string")
{
TypeId ty = reductionof("Not<string>");
CHECK("~string" == toStringFull(ty));
}
SUBCASE("not_string_or_number")
{
TypeId ty = reductionof("Not<string | number>");
CHECK("~number & ~string" == toStringFull(ty));
}
SUBCASE("not_string_and_number")
{
TypeId ty = reductionof("Not<string & number>");
CHECK("unknown" == toStringFull(ty));
}
SUBCASE("not_error")
{
TypeId ty = reductionof("Not<err>");
CHECK("~*error-type*" == toStringFull(ty));
}
} // negations
TEST_CASE_FIXTURE(ReductionFixture, "discriminable_unions")
{
SUBCASE("cat_or_dog_and_dog")
{
TypeId ty = reductionof(R"(({ tag: "cat", catfood: string } | { tag: "dog", dogfood: string }) & { tag: "dog" })");
CHECK(R"({| dogfood: string, tag: "dog" |})" == toStringFull(ty));
}
SUBCASE("cat_or_dog_and_not_dog")
{
TypeId ty = reductionof(R"(({ tag: "cat", catfood: string } | { tag: "dog", dogfood: string }) & { tag: Not<"dog"> })");
CHECK(R"({| catfood: string, tag: "cat" |})" == toStringFull(ty));
}
SUBCASE("string_or_number_and_number")
{
TypeId ty = reductionof("({ tag: string, a: number } | { tag: number, b: string }) & { tag: string }");
CHECK("{| a: number, tag: string |}" == toStringFull(ty));
}
SUBCASE("string_or_number_and_number")
{
TypeId ty = reductionof("({ tag: string, a: number } | { tag: number, b: string }) & { tag: number }");
CHECK("{| b: string, tag: number |}" == toStringFull(ty));
}
SUBCASE("child_or_unrelated_and_parent")
{
TypeId ty = reductionof("({ tag: Child, x: number } | { tag: Unrelated, y: string }) & { tag: Parent }");
CHECK("{| tag: Child, x: number |}" == toStringFull(ty));
}
SUBCASE("child_or_unrelated_and_not_parent")
{
TypeId ty = reductionof("({ tag: Child, x: number } | { tag: Unrelated, y: string }) & { tag: Not<Parent> }");
CHECK("{| tag: Unrelated, y: string |}" == toStringFull(ty));
}
}
TEST_CASE_FIXTURE(ReductionFixture, "cycles")
{
SUBCASE("recursively_defined_function")
{
check("type F = (f: F) -> ()");
TypeId ty = reductionof(requireTypeAlias("F"));
CHECK("t1 where t1 = (t1) -> ()" == toStringFull(ty));
}
SUBCASE("recursively_defined_function_and_function")
{
check("type F = (f: F & fun) -> ()");
TypeId ty = reductionof(requireTypeAlias("F"));
CHECK("t1 where t1 = (function & t1) -> ()" == toStringFull(ty));
}
SUBCASE("recursively_defined_table")
{
check("type T = { x: T }");
TypeId ty = reductionof(requireTypeAlias("T"));
CHECK("t1 where t1 = {| x: t1 |}" == toStringFull(ty));
}
SUBCASE("recursively_defined_table_and_table")
{
check("type T = { x: T & {} }");
TypeId ty = reductionof(requireTypeAlias("T"));
CHECK("t1 where t1 = {| x: t1 & {| |} |}" == toStringFull(ty));
}
SUBCASE("recursively_defined_table_and_table_2")
{
check("type T = { x: T } & { x: number }");
TypeId ty = reductionof(requireTypeAlias("T"));
CHECK("t1 where t1 = {| x: number |} & {| x: t1 |}" == toStringFull(ty));
}
SUBCASE("recursively_defined_table_and_table_3")
{
check("type T = { x: T } & { x: T }");
TypeId ty = reductionof(requireTypeAlias("T"));
CHECK("t1 where t1 = {| x: t1 |} & {| x: t1 |}" == toStringFull(ty));
}
}
TEST_CASE_FIXTURE(ReductionFixture, "string_singletons")
{
TypeId ty = reductionof("(string & Not<\"A\">)?");
CHECK("(string & ~\"A\")?" == toStringFull(ty));
}
TEST_CASE_FIXTURE(ReductionFixture, "string_singletons_2")
{
TypeId ty = reductionof("Not<\"A\"> & Not<\"B\"> & (string?)");
CHECK("(string & ~\"A\" & ~\"B\")?" == toStringFull(ty));
}
TEST_SUITE_END();