mirror of
https://github.com/luau-lang/luau.git
synced 2024-11-15 14:25:44 +08:00
be52bd91e4
* Fixed garbage data in module scopes when type graph is not retained * LOP_MOVE with the same source and target registers is no longer generated (Fixes https://github.com/Roblox/luau/issues/793)
454 lines
13 KiB
C++
454 lines
13 KiB
C++
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
|
|
#include "Luau/Scope.h"
|
|
#include "Luau/Type.h"
|
|
#include "Luau/TypeInfer.h"
|
|
#include "Luau/TypeReduction.h"
|
|
#include "Luau/VisitType.h"
|
|
|
|
#include "Fixture.h"
|
|
#include "ScopedFlags.h"
|
|
|
|
#include "doctest.h"
|
|
|
|
using namespace Luau;
|
|
|
|
TEST_SUITE_BEGIN("TypeTests");
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "primitives_are_equal")
|
|
{
|
|
REQUIRE_EQ(typeChecker.booleanType, typeChecker.booleanType);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "bound_type_is_equal_to_that_which_it_is_bound")
|
|
{
|
|
Type bound(BoundType(typeChecker.booleanType));
|
|
REQUIRE_EQ(bound, *typeChecker.booleanType);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "equivalent_cyclic_tables_are_equal")
|
|
{
|
|
Type cycleOne{TypeVariant(TableType())};
|
|
TableType* tableOne = getMutable<TableType>(&cycleOne);
|
|
tableOne->props["self"] = {&cycleOne};
|
|
|
|
Type cycleTwo{TypeVariant(TableType())};
|
|
TableType* tableTwo = getMutable<TableType>(&cycleTwo);
|
|
tableTwo->props["self"] = {&cycleTwo};
|
|
|
|
CHECK_EQ(cycleOne, cycleTwo);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "different_cyclic_tables_are_not_equal")
|
|
{
|
|
Type cycleOne{TypeVariant(TableType())};
|
|
TableType* tableOne = getMutable<TableType>(&cycleOne);
|
|
tableOne->props["self"] = {&cycleOne};
|
|
|
|
Type cycleTwo{TypeVariant(TableType())};
|
|
TableType* tableTwo = getMutable<TableType>(&cycleTwo);
|
|
tableTwo->props["this"] = {&cycleTwo};
|
|
|
|
CHECK_NE(cycleOne, cycleTwo);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "return_type_of_function_is_not_parenthesized_if_just_one_value")
|
|
{
|
|
auto emptyArgumentPack = TypePackVar{TypePack{}};
|
|
auto returnPack = TypePackVar{TypePack{{typeChecker.numberType}}};
|
|
auto returnsTwo = Type(FunctionType(typeChecker.globalScope->level, &emptyArgumentPack, &returnPack));
|
|
|
|
std::string res = toString(&returnsTwo);
|
|
CHECK_EQ("() -> number", res);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "return_type_of_function_is_parenthesized_if_not_just_one_value")
|
|
{
|
|
auto emptyArgumentPack = TypePackVar{TypePack{}};
|
|
auto returnPack = TypePackVar{TypePack{{typeChecker.numberType, typeChecker.numberType}}};
|
|
auto returnsTwo = Type(FunctionType(typeChecker.globalScope->level, &emptyArgumentPack, &returnPack));
|
|
|
|
std::string res = toString(&returnsTwo);
|
|
CHECK_EQ("() -> (number, number)", res);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "return_type_of_function_is_parenthesized_if_tail_is_free")
|
|
{
|
|
auto emptyArgumentPack = TypePackVar{TypePack{}};
|
|
auto free = Unifiable::Free(TypeLevel());
|
|
auto freePack = TypePackVar{TypePackVariant{free}};
|
|
auto returnPack = TypePackVar{TypePack{{typeChecker.numberType}, &freePack}};
|
|
auto returnsTwo = Type(FunctionType(typeChecker.globalScope->level, &emptyArgumentPack, &returnPack));
|
|
|
|
std::string res = toString(&returnsTwo);
|
|
CHECK_EQ(res, "() -> (number, a...)");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "subset_check")
|
|
{
|
|
UnionType super, sub, notSub;
|
|
super.options = {typeChecker.numberType, typeChecker.stringType, typeChecker.booleanType};
|
|
sub.options = {typeChecker.numberType, typeChecker.stringType};
|
|
notSub.options = {typeChecker.numberType, typeChecker.nilType};
|
|
|
|
CHECK(isSubset(super, sub));
|
|
CHECK(!isSubset(super, notSub));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "iterate_over_UnionType")
|
|
{
|
|
UnionType utv;
|
|
utv.options = {typeChecker.numberType, typeChecker.stringType, typeChecker.anyType};
|
|
|
|
std::vector<TypeId> result;
|
|
for (TypeId ty : &utv)
|
|
result.push_back(ty);
|
|
|
|
CHECK(result == utv.options);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "iterating_over_nested_UnionTypes")
|
|
{
|
|
Type subunion{UnionType{}};
|
|
UnionType* innerUtv = getMutable<UnionType>(&subunion);
|
|
innerUtv->options = {typeChecker.numberType, typeChecker.stringType};
|
|
|
|
UnionType utv;
|
|
utv.options = {typeChecker.anyType, &subunion};
|
|
|
|
std::vector<TypeId> result;
|
|
for (TypeId ty : &utv)
|
|
result.push_back(ty);
|
|
|
|
REQUIRE_EQ(result.size(), 3);
|
|
CHECK_EQ(result[0], typeChecker.anyType);
|
|
CHECK_EQ(result[2], typeChecker.stringType);
|
|
CHECK_EQ(result[1], typeChecker.numberType);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "iterator_detects_cyclic_UnionTypes_and_skips_over_them")
|
|
{
|
|
Type atv{UnionType{}};
|
|
UnionType* utv1 = getMutable<UnionType>(&atv);
|
|
|
|
Type btv{UnionType{}};
|
|
UnionType* utv2 = getMutable<UnionType>(&btv);
|
|
utv2->options.push_back(typeChecker.numberType);
|
|
utv2->options.push_back(typeChecker.stringType);
|
|
utv2->options.push_back(&atv);
|
|
|
|
utv1->options.push_back(&btv);
|
|
|
|
std::vector<TypeId> result;
|
|
for (TypeId ty : utv2)
|
|
result.push_back(ty);
|
|
|
|
REQUIRE_EQ(result.size(), 2);
|
|
CHECK_EQ(result[0], typeChecker.numberType);
|
|
CHECK_EQ(result[1], typeChecker.stringType);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "iterator_descends_on_nested_in_first_operator*")
|
|
{
|
|
Type tv1{UnionType{{typeChecker.stringType, typeChecker.numberType}}};
|
|
Type tv2{UnionType{{&tv1, typeChecker.booleanType}}};
|
|
auto utv = get<UnionType>(&tv2);
|
|
|
|
std::vector<TypeId> result;
|
|
for (TypeId ty : utv)
|
|
result.push_back(ty);
|
|
|
|
REQUIRE_EQ(result.size(), 3);
|
|
CHECK_EQ(result[0], typeChecker.stringType);
|
|
CHECK_EQ(result[1], typeChecker.numberType);
|
|
CHECK_EQ(result[2], typeChecker.booleanType);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "UnionTypeIterator_with_vector_iter_ctor")
|
|
{
|
|
Type tv1{UnionType{{typeChecker.stringType, typeChecker.numberType}}};
|
|
Type tv2{UnionType{{&tv1, typeChecker.booleanType}}};
|
|
auto utv = get<UnionType>(&tv2);
|
|
|
|
std::vector<TypeId> actual(begin(utv), end(utv));
|
|
std::vector<TypeId> expected{typeChecker.stringType, typeChecker.numberType, typeChecker.booleanType};
|
|
CHECK_EQ(actual, expected);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "UnionTypeIterator_with_empty_union")
|
|
{
|
|
Type tv{UnionType{}};
|
|
auto utv = get<UnionType>(&tv);
|
|
|
|
std::vector<TypeId> actual(begin(utv), end(utv));
|
|
CHECK(actual.empty());
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "UnionTypeIterator_with_only_cyclic_union")
|
|
{
|
|
Type tv{UnionType{}};
|
|
auto utv = getMutable<UnionType>(&tv);
|
|
utv->options.push_back(&tv);
|
|
utv->options.push_back(&tv);
|
|
|
|
std::vector<TypeId> actual(begin(utv), end(utv));
|
|
CHECK(actual.empty());
|
|
}
|
|
|
|
|
|
/* FIXME: This test is pretty weird. It would be much nicer if we could
|
|
* perform this operation without a TypeChecker so that we don't have to jam
|
|
* all this state into it to make stuff work.
|
|
*/
|
|
TEST_CASE_FIXTURE(Fixture, "substitution_skip_failure")
|
|
{
|
|
Type ftv11{FreeType{TypeLevel{}}};
|
|
|
|
TypePackVar tp24{TypePack{{&ftv11}}};
|
|
TypePackVar tp17{TypePack{}};
|
|
|
|
Type ftv23{FunctionType{&tp24, &tp17}};
|
|
|
|
Type ttvConnection2{TableType{}};
|
|
TableType* ttvConnection2_ = getMutable<TableType>(&ttvConnection2);
|
|
ttvConnection2_->instantiatedTypeParams.push_back(&ftv11);
|
|
ttvConnection2_->props["f"] = {&ftv23};
|
|
|
|
TypePackVar tp21{TypePack{{&ftv11}}};
|
|
TypePackVar tp20{TypePack{}};
|
|
|
|
Type ftv19{FunctionType{&tp21, &tp20}};
|
|
|
|
Type ttvSignal{TableType{}};
|
|
TableType* ttvSignal_ = getMutable<TableType>(&ttvSignal);
|
|
ttvSignal_->instantiatedTypeParams.push_back(&ftv11);
|
|
ttvSignal_->props["f"] = {&ftv19};
|
|
|
|
// Back edge
|
|
ttvConnection2_->props["signal"] = {&ttvSignal};
|
|
|
|
Type gtvK2{GenericType{}};
|
|
Type gtvV2{GenericType{}};
|
|
|
|
Type ttvTweenResult2{TableType{}};
|
|
TableType* ttvTweenResult2_ = getMutable<TableType>(&ttvTweenResult2);
|
|
ttvTweenResult2_->instantiatedTypeParams.push_back(>vK2);
|
|
ttvTweenResult2_->instantiatedTypeParams.push_back(>vV2);
|
|
|
|
TypePackVar tp13{TypePack{{&ttvTweenResult2}}};
|
|
Type ftv12{FunctionType{&tp13, &tp17}};
|
|
|
|
Type ttvConnection{TableType{}};
|
|
TableType* ttvConnection_ = getMutable<TableType>(&ttvConnection);
|
|
ttvConnection_->instantiatedTypeParams.push_back(&ttvTweenResult2);
|
|
ttvConnection_->props["f"] = {&ftv12};
|
|
ttvConnection_->props["signal"] = {&ttvSignal};
|
|
|
|
TypePackVar tp9{TypePack{}};
|
|
TypePackVar tp10{TypePack{{&ttvConnection}}};
|
|
|
|
Type ftv8{FunctionType{&tp9, &tp10}};
|
|
|
|
Type ttvTween{TableType{}};
|
|
TableType* ttvTween_ = getMutable<TableType>(&ttvTween);
|
|
ttvTween_->instantiatedTypeParams.push_back(>vK2);
|
|
ttvTween_->instantiatedTypeParams.push_back(>vV2);
|
|
ttvTween_->props["f"] = {&ftv8};
|
|
|
|
TypePackVar tp4{TypePack{}};
|
|
TypePackVar tp5{TypePack{{&ttvTween}}};
|
|
|
|
Type ftv3{FunctionType{&tp4, &tp5}};
|
|
|
|
// Back edge
|
|
ttvTweenResult2_->props["f"] = {&ftv3};
|
|
|
|
Type gtvK{GenericType{}};
|
|
Type gtvV{GenericType{}};
|
|
|
|
Type ttvTweenResult{TableType{}};
|
|
TableType* ttvTweenResult_ = getMutable<TableType>(&ttvTweenResult);
|
|
ttvTweenResult_->instantiatedTypeParams.push_back(>vK);
|
|
ttvTweenResult_->instantiatedTypeParams.push_back(>vV);
|
|
ttvTweenResult_->props["f"] = {&ftv3};
|
|
|
|
TypeId root = &ttvTweenResult;
|
|
|
|
typeChecker.currentModule = std::make_shared<Module>();
|
|
typeChecker.currentModule->scopes.emplace_back(Location{}, std::make_shared<Scope>(builtinTypes->anyTypePack));
|
|
|
|
TypeId result = typeChecker.anyify(typeChecker.globalScope, root, Location{});
|
|
|
|
CHECK_EQ("{| f: t1 |} where t1 = () -> {| f: () -> {| f: ({| f: t1 |}) -> (), signal: {| f: (any) -> () |} |} |}", toString(result));
|
|
}
|
|
|
|
TEST_CASE("tagging_tables")
|
|
{
|
|
Type ttv{TableType{}};
|
|
CHECK(!Luau::hasTag(&ttv, "foo"));
|
|
Luau::attachTag(&ttv, "foo");
|
|
CHECK(Luau::hasTag(&ttv, "foo"));
|
|
}
|
|
|
|
TEST_CASE("tagging_classes")
|
|
{
|
|
Type base{ClassType{"Base", {}, std::nullopt, std::nullopt, {}, nullptr, "Test"}};
|
|
CHECK(!Luau::hasTag(&base, "foo"));
|
|
Luau::attachTag(&base, "foo");
|
|
CHECK(Luau::hasTag(&base, "foo"));
|
|
}
|
|
|
|
TEST_CASE("tagging_subclasses")
|
|
{
|
|
Type base{ClassType{"Base", {}, std::nullopt, std::nullopt, {}, nullptr, "Test"}};
|
|
Type derived{ClassType{"Derived", {}, &base, std::nullopt, {}, nullptr, "Test"}};
|
|
|
|
CHECK(!Luau::hasTag(&base, "foo"));
|
|
CHECK(!Luau::hasTag(&derived, "foo"));
|
|
|
|
Luau::attachTag(&base, "foo");
|
|
CHECK(Luau::hasTag(&base, "foo"));
|
|
CHECK(Luau::hasTag(&derived, "foo"));
|
|
|
|
Luau::attachTag(&derived, "bar");
|
|
CHECK(!Luau::hasTag(&base, "bar"));
|
|
CHECK(Luau::hasTag(&derived, "bar"));
|
|
}
|
|
|
|
TEST_CASE("tagging_functions")
|
|
{
|
|
TypePackVar empty{TypePack{}};
|
|
Type ftv{FunctionType{&empty, &empty}};
|
|
CHECK(!Luau::hasTag(&ftv, "foo"));
|
|
Luau::attachTag(&ftv, "foo");
|
|
CHECK(Luau::hasTag(&ftv, "foo"));
|
|
}
|
|
|
|
TEST_CASE("tagging_props")
|
|
{
|
|
Property prop{};
|
|
CHECK(!Luau::hasTag(prop, "foo"));
|
|
Luau::attachTag(prop, "foo");
|
|
CHECK(Luau::hasTag(prop, "foo"));
|
|
}
|
|
|
|
struct VisitCountTracker final : TypeOnceVisitor
|
|
{
|
|
std::unordered_map<TypeId, unsigned> tyVisits;
|
|
std::unordered_map<TypePackId, unsigned> tpVisits;
|
|
|
|
void cycle(TypeId) override {}
|
|
void cycle(TypePackId) override {}
|
|
|
|
template<typename T>
|
|
bool operator()(TypeId ty, const T& t)
|
|
{
|
|
return visit(ty);
|
|
}
|
|
|
|
template<typename T>
|
|
bool operator()(TypePackId tp, const T&)
|
|
{
|
|
return visit(tp);
|
|
}
|
|
|
|
bool visit(TypeId ty) override
|
|
{
|
|
tyVisits[ty]++;
|
|
return true;
|
|
}
|
|
|
|
bool visit(TypePackId tp) override
|
|
{
|
|
tpVisits[tp]++;
|
|
return true;
|
|
}
|
|
};
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "visit_once")
|
|
{
|
|
CheckResult result = check(R"(
|
|
type T = { a: number, b: () -> () }
|
|
local b: (T, T, T) -> T
|
|
)");
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
TypeId bType = requireType("b");
|
|
|
|
VisitCountTracker tester;
|
|
tester.traverse(bType);
|
|
|
|
for (auto [_, count] : tester.tyVisits)
|
|
CHECK_EQ(count, 1);
|
|
|
|
for (auto [_, count] : tester.tpVisits)
|
|
CHECK_EQ(count, 1);
|
|
}
|
|
|
|
TEST_CASE("isString_on_string_singletons")
|
|
{
|
|
Type helloString{SingletonType{StringSingleton{"hello"}}};
|
|
CHECK(isString(&helloString));
|
|
}
|
|
|
|
TEST_CASE("isString_on_unions_of_various_string_singletons")
|
|
{
|
|
Type helloString{SingletonType{StringSingleton{"hello"}}};
|
|
Type byeString{SingletonType{StringSingleton{"bye"}}};
|
|
Type union_{UnionType{{&helloString, &byeString}}};
|
|
|
|
CHECK(isString(&union_));
|
|
}
|
|
|
|
TEST_CASE("proof_that_isString_uses_all_of")
|
|
{
|
|
Type helloString{SingletonType{StringSingleton{"hello"}}};
|
|
Type byeString{SingletonType{StringSingleton{"bye"}}};
|
|
Type booleanType{PrimitiveType{PrimitiveType::Boolean}};
|
|
Type union_{UnionType{{&helloString, &byeString, &booleanType}}};
|
|
|
|
CHECK(!isString(&union_));
|
|
}
|
|
|
|
TEST_CASE("isBoolean_on_boolean_singletons")
|
|
{
|
|
Type trueBool{SingletonType{BooleanSingleton{true}}};
|
|
CHECK(isBoolean(&trueBool));
|
|
}
|
|
|
|
TEST_CASE("isBoolean_on_unions_of_true_or_false_singletons")
|
|
{
|
|
Type trueBool{SingletonType{BooleanSingleton{true}}};
|
|
Type falseBool{SingletonType{BooleanSingleton{false}}};
|
|
Type union_{UnionType{{&trueBool, &falseBool}}};
|
|
|
|
CHECK(isBoolean(&union_));
|
|
}
|
|
|
|
TEST_CASE("proof_that_isBoolean_uses_all_of")
|
|
{
|
|
Type trueBool{SingletonType{BooleanSingleton{true}}};
|
|
Type falseBool{SingletonType{BooleanSingleton{false}}};
|
|
Type stringType{PrimitiveType{PrimitiveType::String}};
|
|
Type union_{UnionType{{&trueBool, &falseBool, &stringType}}};
|
|
|
|
CHECK(!isBoolean(&union_));
|
|
}
|
|
|
|
TEST_CASE("content_reassignment")
|
|
{
|
|
Type myAny{AnyType{}, /*presistent*/ true};
|
|
myAny.documentationSymbol = "@global/any";
|
|
|
|
TypeArena arena;
|
|
|
|
TypeId futureAny = arena.addType(FreeType{TypeLevel{}});
|
|
asMutable(futureAny)->reassign(myAny);
|
|
|
|
CHECK(get<AnyType>(futureAny) != nullptr);
|
|
CHECK(!futureAny->persistent);
|
|
CHECK(futureAny->documentationSymbol == "@global/any");
|
|
CHECK(futureAny->owningArena == &arena);
|
|
}
|
|
|
|
TEST_SUITE_END();
|