luau/tests/TypeVar.test.cpp

456 lines
14 KiB
C++
Raw Normal View History

// 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(builtinTypes->booleanType, builtinTypes->booleanType);
}
TEST_CASE_FIXTURE(Fixture, "bound_type_is_equal_to_that_which_it_is_bound")
{
Type bound(BoundType(builtinTypes->booleanType));
REQUIRE_EQ(bound, *builtinTypes->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{{builtinTypes->numberType}}};
auto returnsTwo = Type(FunctionType(frontend.globals.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{{builtinTypes->numberType, builtinTypes->numberType}}};
auto returnsTwo = Type(FunctionType(frontend.globals.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 = FreeTypePack(TypeLevel());
auto freePack = TypePackVar{TypePackVariant{free}};
auto returnPack = TypePackVar{TypePack{{builtinTypes->numberType}, &freePack}};
auto returnsTwo = Type(FunctionType(frontend.globals.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 = {builtinTypes->numberType, builtinTypes->stringType, builtinTypes->booleanType};
sub.options = {builtinTypes->numberType, builtinTypes->stringType};
notSub.options = {builtinTypes->numberType, builtinTypes->nilType};
CHECK(isSubset(super, sub));
CHECK(!isSubset(super, notSub));
}
TEST_CASE_FIXTURE(Fixture, "iterate_over_UnionType")
{
UnionType utv;
utv.options = {builtinTypes->numberType, builtinTypes->stringType, builtinTypes->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 = {builtinTypes->numberType, builtinTypes->stringType};
UnionType utv;
utv.options = {builtinTypes->anyType, &subunion};
std::vector<TypeId> result;
for (TypeId ty : &utv)
result.push_back(ty);
REQUIRE_EQ(result.size(), 3);
CHECK_EQ(result[0], builtinTypes->anyType);
CHECK_EQ(result[2], builtinTypes->stringType);
CHECK_EQ(result[1], builtinTypes->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(builtinTypes->numberType);
utv2->options.push_back(builtinTypes->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], builtinTypes->numberType);
CHECK_EQ(result[1], builtinTypes->stringType);
}
TEST_CASE_FIXTURE(Fixture, "iterator_descends_on_nested_in_first_operator*")
{
Type tv1{UnionType{{builtinTypes->stringType, builtinTypes->numberType}}};
Type tv2{UnionType{{&tv1, builtinTypes->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], builtinTypes->stringType);
CHECK_EQ(result[1], builtinTypes->numberType);
CHECK_EQ(result[2], builtinTypes->booleanType);
}
TEST_CASE_FIXTURE(Fixture, "UnionTypeIterator_with_vector_iter_ctor")
{
Type tv1{UnionType{{builtinTypes->stringType, builtinTypes->numberType}}};
Type tv2{UnionType{{&tv1, builtinTypes->booleanType}}};
auto utv = get<UnionType>(&tv2);
std::vector<TypeId> actual(begin(utv), end(utv));
std::vector<TypeId> expected{builtinTypes->stringType, builtinTypes->numberType, builtinTypes->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(&gtvK2);
ttvTweenResult2_->instantiatedTypeParams.push_back(&gtvV2);
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(&gtvK2);
ttvTween_->instantiatedTypeParams.push_back(&gtvV2);
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(&gtvK);
ttvTweenResult_->instantiatedTypeParams.push_back(&gtvV);
ttvTweenResult_->props["f"] = {&ftv3};
TypeId root = &ttvTweenResult;
Sync to upstream/release/572 (#899) * Fixed exported types not being suggested in autocomplete * `T...` is now convertible to `...any` (Fixes https://github.com/Roblox/luau/issues/767) * Fixed issue with `T?` not being convertible to `T | T` or `T?` (sometimes when internal pointer identity is different) * Fixed potential crash in missing table key error suggestion to use a similar existing key * `lua_topointer` now returns a pointer for strings C++ API Changes: * `prepareModuleScope` callback has moved from TypeChecker to Frontend * For LSPs, AstQuery functions (and `isWithinComment`) can be used without full Frontend data A lot of changes in our two experimental components as well. In our work on the new type-solver, the following issues were fixed: * Fixed table union and intersection indexing * Correct custom type environments are now used * Fixed issue with values of `free & number` type not accepted in numeric operations And these are the changes in native code generation (JIT): * arm64 lowering is almost complete with support for 99% of IR commands and all fastcalls * Fixed x64 assembly encoding for extended byte registers * More external x64 calls are aware of register allocator * `math.min`/`math.max` with more than 2 arguments are now lowered to IR as well * Fixed correctness issues with `math` library calls with multiple results in variadic context and with x64 register conflicts * x64 register allocator learnt to restore values from VM memory instead of always using stack spills * x64 exception unwind information now supports multiple functions and fixes function start offset in Dwarf2 info
2023-04-15 02:06:22 +08:00
ModulePtr currentModule = std::make_shared<Module>();
Anyification anyification(&currentModule->internalTypes, frontend.globals.globalScope, builtinTypes, &frontend.iceHandler, builtinTypes->anyType,
builtinTypes->anyTypePack);
std::optional<TypeId> any = anyification.substitute(root);
REQUIRE(!anyification.normalizationTooComplex);
REQUIRE(any.has_value());
CHECK_EQ("{| f: t1 |} where t1 = () -> {| f: () -> {| f: ({| f: t1 |}) -> (), signal: {| f: (any) -> () |} |} |}", toString(*any));
}
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;
2022-05-06 08:03:43 +08:00
void cycle(TypeId) override {}
void cycle(TypePackId) override {}
template<typename T>
bool operator()(TypeId ty, const T& t)
{
2022-05-06 08:03:43 +08:00
return visit(ty);
}
template<typename T>
bool operator()(TypePackId tp, const T&)
2022-05-06 08:03:43 +08:00
{
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();