// 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(&cycleOne); tableOne->props["self"] = {&cycleOne}; Type cycleTwo{TypeVariant(TableType())}; TableType* tableTwo = getMutable(&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(&cycleOne); tableOne->props["self"] = {&cycleOne}; Type cycleTwo{TypeVariant(TableType())}; TableType* tableTwo = getMutable(&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 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(&subunion); innerUtv->options = {builtinTypes->numberType, builtinTypes->stringType}; UnionType utv; utv.options = {builtinTypes->anyType, &subunion}; std::vector 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(&atv); Type btv{UnionType{}}; UnionType* utv2 = getMutable(&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 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(&tv2); std::vector 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(&tv2); std::vector actual(begin(utv), end(utv)); std::vector 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(&tv); std::vector actual(begin(utv), end(utv)); CHECK(actual.empty()); } TEST_CASE_FIXTURE(Fixture, "UnionTypeIterator_with_only_cyclic_union") { Type tv{UnionType{}}; auto utv = getMutable(&tv); utv->options.push_back(&tv); utv->options.push_back(&tv); std::vector 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(&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(&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(&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(&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(&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(&ttvTweenResult); ttvTweenResult_->instantiatedTypeParams.push_back(>vK); ttvTweenResult_->instantiatedTypeParams.push_back(>vV); ttvTweenResult_->props["f"] = {&ftv3}; TypeId root = &ttvTweenResult; frontend.typeChecker.currentModule = std::make_shared(); frontend.typeChecker.currentModule->scopes.emplace_back(Location{}, std::make_shared(builtinTypes->anyTypePack)); TypeId result = frontend.typeChecker.anyify(frontend.globals.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 tyVisits; std::unordered_map tpVisits; void cycle(TypeId) override {} void cycle(TypePackId) override {} template bool operator()(TypeId ty, const T& t) { return visit(ty); } template 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(futureAny) != nullptr); CHECK(!futureAny->persistent); CHECK(futureAny->documentationSymbol == "@global/any"); CHECK(futureAny->owningArena == &arena); } TEST_SUITE_END();