mirror of
https://github.com/luau-lang/luau.git
synced 2025-05-04 10:33:46 +01:00
# General * Internally rename `ClassType` to `ExternType`. In definition files, the syntax to define these types has changed to `declare extern type Foo with prop: type end` * Add `luarequire_registermodule` to Luau.Require * Support yieldable Luau C functions calling other functions * Store return types as `AstTypePack*` on Ast nodes ## New Solver * Improve the logic that determines constraint dispatch ordering * Fix a crash in the type solver that arose when using multi-return functions with `string.format` * Fix https://github.com/luau-lang/luau/issues/1736 * Initial steps toward rethinking function generalization: * Instead of generalizing every type in a function all at once, we will instead generalize individual type variables once their bounds have been fully resolved. This will make it possible to properly interleave type function reduction and generalization. * Magic functions are no longer considered magical in cases where they are not explicitly called by the code. * The most prominent example of this is in `for..in` loops where the function call is part of the desugaring process. * Almost all magic functions work by directly inspecting the AST, so they can't work without an AST fragment anyway. * Further, none of the magic functions we have are usefully used in this way. Co-authored-by: Andy Friesen <afriesen@roblox.com> Co-authored-by: Ariel Weiss <aaronweiss@roblox.com> Co-authored-by: Hunter Goldstein <hgoldstein@roblox.com> Co-authored-by: Sora Kanosue <skanosue@roblox.com> Co-authored-by: Talha Pathan <tpathan@roblox.com> Co-authored-by: Varun Saini <vsaini@roblox.com> Co-authored-by: Vighnesh Vijay <vvijay@roblox.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
1267 lines
36 KiB
C++
1267 lines
36 KiB
C++
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
|
|
|
|
#include "Fixture.h"
|
|
|
|
#include "Luau/AstQuery.h"
|
|
#include "Luau/Common.h"
|
|
#include "Luau/Type.h"
|
|
#include "doctest.h"
|
|
|
|
#include "Luau/Normalize.h"
|
|
#include "Luau/BuiltinDefinitions.h"
|
|
|
|
LUAU_FASTFLAG(LuauSolverV2)
|
|
LUAU_FASTINT(LuauTypeInferRecursionLimit)
|
|
LUAU_FASTINT(LuauNormalizeIntersectionLimit)
|
|
LUAU_FASTINT(LuauNormalizeUnionLimit)
|
|
LUAU_FASTFLAG(DebugLuauGreedyGeneralization)
|
|
LUAU_FASTFLAG(LuauNormalizationCatchMetatableCycles)
|
|
LUAU_FASTFLAG(LuauSubtypingEnableReasoningLimit)
|
|
LUAU_FASTFLAG(LuauTypePackDetectCycles)
|
|
LUAU_FASTFLAG(LuauNonReentrantGeneralization2)
|
|
|
|
using namespace Luau;
|
|
|
|
namespace
|
|
{
|
|
struct IsSubtypeFixture : Fixture
|
|
{
|
|
bool isSubtype(TypeId a, TypeId b)
|
|
{
|
|
ModulePtr module = getMainModule();
|
|
REQUIRE(module);
|
|
|
|
if (!module->hasModuleScope())
|
|
FAIL("isSubtype: module scope data is not available");
|
|
|
|
SimplifierPtr simplifier = newSimplifier(NotNull{&module->internalTypes}, builtinTypes);
|
|
|
|
return ::Luau::isSubtype(a, b, NotNull{module->getModuleScope().get()}, builtinTypes, NotNull{simplifier.get()}, ice);
|
|
}
|
|
};
|
|
} // namespace
|
|
|
|
TEST_SUITE_BEGIN("isSubtype");
|
|
|
|
TEST_CASE_FIXTURE(IsSubtypeFixture, "primitives")
|
|
{
|
|
check(R"(
|
|
local a = 41
|
|
local b = 32
|
|
|
|
local c = "hello"
|
|
local d = "world"
|
|
)");
|
|
|
|
TypeId a = requireType("a");
|
|
TypeId b = requireType("b");
|
|
TypeId c = requireType("c");
|
|
TypeId d = requireType("d");
|
|
|
|
CHECK(isSubtype(b, a));
|
|
CHECK(isSubtype(d, c));
|
|
CHECK(!isSubtype(d, a));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(IsSubtypeFixture, "functions")
|
|
{
|
|
check(R"(
|
|
function a(x: number): number return x end
|
|
function b(x: number): number return x end
|
|
|
|
function c(x: number?): number return x end
|
|
function d(x: number): number? return x end
|
|
)");
|
|
|
|
TypeId a = requireType("a");
|
|
TypeId b = requireType("b");
|
|
TypeId c = requireType("c");
|
|
TypeId d = requireType("d");
|
|
|
|
CHECK(isSubtype(b, a));
|
|
CHECK(isSubtype(c, a));
|
|
CHECK(!isSubtype(d, a));
|
|
CHECK(isSubtype(a, d));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(IsSubtypeFixture, "variadic_functions_with_no_head")
|
|
{
|
|
check(R"(
|
|
local a: (...number) -> ()
|
|
local b: (...number?) -> ()
|
|
)");
|
|
|
|
TypeId a = requireType("a");
|
|
TypeId b = requireType("b");
|
|
|
|
CHECK(isSubtype(b, a));
|
|
CHECK(!isSubtype(a, b));
|
|
}
|
|
|
|
#if 0
|
|
TEST_CASE_FIXTURE(IsSubtypeFixture, "variadic_function_with_head")
|
|
{
|
|
check(R"(
|
|
local a: (...number) -> ()
|
|
local b: (number, number) -> ()
|
|
)");
|
|
|
|
TypeId a = requireType("a");
|
|
TypeId b = requireType("b");
|
|
|
|
CHECK(!isSubtype(b, a));
|
|
CHECK(isSubtype(a, b));
|
|
}
|
|
#endif
|
|
|
|
TEST_CASE_FIXTURE(IsSubtypeFixture, "union")
|
|
{
|
|
check(R"(
|
|
local a: number | string
|
|
local b: number
|
|
local c: string
|
|
local d: number?
|
|
)");
|
|
|
|
TypeId a = requireType("a");
|
|
TypeId b = requireType("b");
|
|
TypeId c = requireType("c");
|
|
TypeId d = requireType("d");
|
|
|
|
CHECK(isSubtype(b, a));
|
|
CHECK(!isSubtype(a, b));
|
|
|
|
CHECK(isSubtype(c, a));
|
|
CHECK(!isSubtype(a, c));
|
|
|
|
CHECK(!isSubtype(d, a));
|
|
CHECK(!isSubtype(a, d));
|
|
|
|
CHECK(isSubtype(b, d));
|
|
CHECK(!isSubtype(d, b));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(IsSubtypeFixture, "table_with_union_prop")
|
|
{
|
|
check(R"(
|
|
local a: {x: number}
|
|
local b: {x: number?}
|
|
)");
|
|
|
|
TypeId a = requireType("a");
|
|
TypeId b = requireType("b");
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
CHECK(!isSubtype(a, b)); // table properties are invariant
|
|
else
|
|
CHECK(isSubtype(a, b));
|
|
CHECK(!isSubtype(b, a));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(IsSubtypeFixture, "table_with_any_prop")
|
|
{
|
|
check(R"(
|
|
local a: {x: number}
|
|
local b: {x: any}
|
|
)");
|
|
|
|
TypeId a = requireType("a");
|
|
TypeId b = requireType("b");
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
CHECK(!isSubtype(a, b)); // table properties are invariant
|
|
else
|
|
CHECK(isSubtype(a, b));
|
|
CHECK(!isSubtype(b, a));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(IsSubtypeFixture, "intersection")
|
|
{
|
|
check(R"(
|
|
local a: number & string
|
|
local b: number
|
|
local c: string
|
|
local d: number & nil
|
|
)");
|
|
|
|
TypeId a = requireType("a");
|
|
TypeId b = requireType("b");
|
|
TypeId c = requireType("c");
|
|
TypeId d = requireType("d");
|
|
|
|
CHECK(!isSubtype(b, a));
|
|
CHECK(isSubtype(a, b));
|
|
|
|
CHECK(!isSubtype(c, a));
|
|
CHECK(isSubtype(a, c));
|
|
|
|
// These types are both equivalent to never
|
|
CHECK(isSubtype(d, a));
|
|
CHECK(isSubtype(a, d));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(IsSubtypeFixture, "union_and_intersection")
|
|
{
|
|
check(R"(
|
|
local a: number & string
|
|
local b: number | nil
|
|
)");
|
|
|
|
TypeId a = requireType("a");
|
|
TypeId b = requireType("b");
|
|
|
|
CHECK(!isSubtype(b, a));
|
|
CHECK(isSubtype(a, b));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(IsSubtypeFixture, "tables")
|
|
{
|
|
check(R"(
|
|
local a: {x: number}
|
|
local b: {x: any}
|
|
local c: {y: number}
|
|
local d: {x: number, y: number}
|
|
)");
|
|
|
|
TypeId a = requireType("a");
|
|
TypeId b = requireType("b");
|
|
TypeId c = requireType("c");
|
|
TypeId d = requireType("d");
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
CHECK(!isSubtype(a, b)); // table properties are invariant
|
|
else
|
|
CHECK(isSubtype(a, b));
|
|
CHECK(!isSubtype(b, a));
|
|
|
|
CHECK(!isSubtype(c, a));
|
|
CHECK(!isSubtype(a, c));
|
|
|
|
CHECK(isSubtype(d, a));
|
|
CHECK(!isSubtype(a, d));
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
CHECK(!isSubtype(d, b)); // table properties are invariant
|
|
else
|
|
CHECK(isSubtype(d, b));
|
|
CHECK(!isSubtype(b, d));
|
|
}
|
|
|
|
#if 0
|
|
TEST_CASE_FIXTURE(IsSubtypeFixture, "table_indexers_are_invariant")
|
|
{
|
|
check(R"(
|
|
local a: {[string]: number}
|
|
local b: {[string]: any}
|
|
local c: {[string]: number}
|
|
)");
|
|
|
|
TypeId a = requireType("a");
|
|
TypeId b = requireType("b");
|
|
TypeId c = requireType("c");
|
|
|
|
CHECK(!isSubtype(b, a));
|
|
CHECK(!isSubtype(a, b));
|
|
|
|
CHECK(isSubtype(c, a));
|
|
CHECK(isSubtype(a, c));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(IsSubtypeFixture, "mismatched_indexers")
|
|
{
|
|
check(R"(
|
|
local a: {x: number}
|
|
local b: {[string]: number}
|
|
local c: {}
|
|
)");
|
|
|
|
TypeId a = requireType("a");
|
|
TypeId b = requireType("b");
|
|
TypeId c = requireType("c");
|
|
|
|
CHECK(isSubtype(b, a));
|
|
CHECK(!isSubtype(a, b));
|
|
|
|
CHECK(!isSubtype(c, b));
|
|
CHECK(isSubtype(b, c));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(IsSubtypeFixture, "cyclic_table")
|
|
{
|
|
check(R"(
|
|
type A = {method: (A) -> ()}
|
|
local a: A
|
|
|
|
type B = {method: (any) -> ()}
|
|
local b: B
|
|
|
|
type C = {method: (C) -> ()}
|
|
local c: C
|
|
|
|
type D = {method: (D) -> (), another: (D) -> ()}
|
|
local d: D
|
|
|
|
type E = {method: (A) -> (), another: (E) -> ()}
|
|
local e: E
|
|
)");
|
|
|
|
TypeId a = requireType("a");
|
|
TypeId b = requireType("b");
|
|
TypeId c = requireType("c");
|
|
TypeId d = requireType("d");
|
|
TypeId e = requireType("e");
|
|
|
|
CHECK(isSubtype(b, a));
|
|
CHECK(!isSubtype(a, b));
|
|
|
|
CHECK(isSubtype(c, a));
|
|
CHECK(isSubtype(a, c));
|
|
|
|
CHECK(!isSubtype(d, a));
|
|
CHECK(!isSubtype(a, d));
|
|
|
|
CHECK(isSubtype(e, a));
|
|
CHECK(!isSubtype(a, e));
|
|
}
|
|
#endif
|
|
|
|
TEST_CASE_FIXTURE(IsSubtypeFixture, "extern_types")
|
|
{
|
|
createSomeExternTypes(&frontend);
|
|
|
|
check(""); // Ensure that we have a main Module.
|
|
|
|
TypeId p = frontend.globals.globalScope->lookupType("Parent")->type;
|
|
TypeId c = frontend.globals.globalScope->lookupType("Child")->type;
|
|
TypeId u = frontend.globals.globalScope->lookupType("Unrelated")->type;
|
|
|
|
CHECK(isSubtype(c, p));
|
|
CHECK(!isSubtype(p, c));
|
|
CHECK(!isSubtype(u, p));
|
|
CHECK(!isSubtype(p, u));
|
|
}
|
|
|
|
#if 0
|
|
TEST_CASE_FIXTURE(IsSubtypeFixture, "metatable" * doctest::expected_failures{1})
|
|
{
|
|
check(R"(
|
|
local T = {}
|
|
T.__index = T
|
|
function T.new()
|
|
return setmetatable({}, T)
|
|
end
|
|
|
|
function T:method() end
|
|
|
|
local a: typeof(T.new)
|
|
local b: {method: (any) -> ()}
|
|
)");
|
|
|
|
TypeId a = requireType("a");
|
|
TypeId b = requireType("b");
|
|
|
|
CHECK(isSubtype(a, b));
|
|
}
|
|
#endif
|
|
|
|
TEST_CASE_FIXTURE(IsSubtypeFixture, "any_is_unknown_union_error")
|
|
{
|
|
check(R"(
|
|
local err = 5.nope.nope -- err is now an error type
|
|
local a : any
|
|
local b : (unknown | typeof(err))
|
|
)");
|
|
|
|
TypeId a = requireType("a");
|
|
TypeId b = requireType("b");
|
|
|
|
CHECK(isSubtype(a, b));
|
|
CHECK(isSubtype(b, a));
|
|
CHECK_EQ("*error-type*", toString(requireType("err")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(IsSubtypeFixture, "any_intersect_T_is_T")
|
|
{
|
|
check(R"(
|
|
local a : (any & string)
|
|
local b : string
|
|
local c : number
|
|
)");
|
|
|
|
TypeId a = requireType("a");
|
|
TypeId b = requireType("b");
|
|
TypeId c = requireType("c");
|
|
|
|
CHECK(isSubtype(a, b));
|
|
CHECK(isSubtype(b, a));
|
|
CHECK(!isSubtype(a, c));
|
|
CHECK(!isSubtype(c, a));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(IsSubtypeFixture, "error_suppression")
|
|
{
|
|
check("");
|
|
|
|
TypeId any = builtinTypes->anyType;
|
|
TypeId err = builtinTypes->errorType;
|
|
TypeId str = builtinTypes->stringType;
|
|
TypeId unk = builtinTypes->unknownType;
|
|
|
|
CHECK(!isSubtype(any, err));
|
|
CHECK(isSubtype(err, any));
|
|
|
|
CHECK(!isSubtype(any, str));
|
|
CHECK(isSubtype(str, any));
|
|
|
|
// We have added this as an exception - the set of inhabitants of any is exactly the set of inhabitants of unknown (since error has no
|
|
// inhabitants). any = err | unknown, so under semantic subtyping, {} U unknown = unknown
|
|
if (FFlag::LuauSolverV2)
|
|
{
|
|
CHECK(isSubtype(any, unk));
|
|
}
|
|
else
|
|
{
|
|
CHECK(!isSubtype(any, unk));
|
|
}
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
{
|
|
CHECK(isSubtype(err, str));
|
|
}
|
|
else
|
|
{
|
|
CHECK(!isSubtype(err, str));
|
|
}
|
|
|
|
CHECK(!isSubtype(str, err));
|
|
|
|
CHECK(!isSubtype(err, unk));
|
|
CHECK(!isSubtype(unk, err));
|
|
|
|
CHECK(isSubtype(str, unk));
|
|
CHECK(!isSubtype(unk, str));
|
|
}
|
|
|
|
TEST_SUITE_END();
|
|
|
|
struct NormalizeFixture : Fixture
|
|
{
|
|
TypeArena arena;
|
|
InternalErrorReporter iceHandler;
|
|
UnifierSharedState unifierState{&iceHandler};
|
|
Normalizer normalizer{&arena, builtinTypes, NotNull{&unifierState}};
|
|
Scope globalScope{builtinTypes->anyTypePack};
|
|
|
|
NormalizeFixture()
|
|
{
|
|
registerHiddenTypes(&frontend);
|
|
}
|
|
|
|
std::shared_ptr<const NormalizedType> toNormalizedType(const std::string& annotation, int expectedErrors = 0)
|
|
{
|
|
normalizer.clearCaches();
|
|
CheckResult result = check("type _Res = " + annotation);
|
|
LUAU_REQUIRE_ERROR_COUNT(expectedErrors, result);
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
{
|
|
SourceModule* sourceModule = getMainSourceModule();
|
|
REQUIRE(sourceModule);
|
|
AstNode* node = findNodeAtPosition(*sourceModule, {0, 5});
|
|
REQUIRE(node);
|
|
AstStatTypeAlias* alias = node->as<AstStatTypeAlias>();
|
|
REQUIRE(alias);
|
|
TypeId* originalTy = getMainModule()->astResolvedTypes.find(alias->type);
|
|
REQUIRE(originalTy);
|
|
return normalizer.normalize(*originalTy);
|
|
}
|
|
else
|
|
{
|
|
std::optional<TypeId> ty = lookupType("_Res");
|
|
REQUIRE(ty);
|
|
return normalizer.normalize(*ty);
|
|
}
|
|
}
|
|
|
|
TypeId normal(const std::string& annotation)
|
|
{
|
|
std::shared_ptr<const NormalizedType> norm = toNormalizedType(annotation);
|
|
REQUIRE(norm);
|
|
return normalizer.typeFromNormal(*norm);
|
|
}
|
|
};
|
|
|
|
TEST_SUITE_BEGIN("Normalize");
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "string_intersection_is_commutative")
|
|
{
|
|
auto c4 = toString(normal(R"(
|
|
string & (string & Not<"a"> & Not<"b">)
|
|
)"));
|
|
auto c4Reverse = toString(normal(R"(
|
|
(string & Not<"a"> & Not<"b">) & string
|
|
)"));
|
|
CHECK(c4 == c4Reverse);
|
|
CHECK_EQ("string & ~\"a\" & ~\"b\"", c4);
|
|
|
|
auto c5 = toString(normal(R"(
|
|
(string & Not<"a"> & Not<"b">) & (string & Not<"b"> & Not<"c">)
|
|
)"));
|
|
auto c5Reverse = toString(normal(R"(
|
|
(string & Not<"b"> & Not<"c">) & (string & Not<"a"> & Not<"c">)
|
|
)"));
|
|
CHECK(c5 == c5Reverse);
|
|
CHECK_EQ("string & ~\"a\" & ~\"b\" & ~\"c\"", c5);
|
|
|
|
auto c6 = toString(normal(R"(
|
|
("a" | "b") & (string & Not<"b"> & Not<"c">)
|
|
)"));
|
|
auto c6Reverse = toString(normal(R"(
|
|
(string & Not<"b"> & Not<"c">) & ("a" | "b")
|
|
)"));
|
|
CHECK(c6 == c6Reverse);
|
|
CHECK_EQ("\"a\"", c6);
|
|
|
|
auto c7 = toString(normal(R"(
|
|
string & ("b" | "c")
|
|
)"));
|
|
auto c7Reverse = toString(normal(R"(
|
|
("b" | "c") & string
|
|
)"));
|
|
CHECK(c7 == c7Reverse);
|
|
CHECK_EQ("\"b\" | \"c\"", c7);
|
|
|
|
auto c8 = toString(normal(R"(
|
|
(string & Not<"a"> & Not<"b">) & ("b" | "c")
|
|
)"));
|
|
auto c8Reverse = toString(normal(R"(
|
|
("b" | "c") & (string & Not<"a"> & Not<"b">)
|
|
)"));
|
|
CHECK(c8 == c8Reverse);
|
|
CHECK_EQ("\"c\"", c8);
|
|
auto c9 = toString(normal(R"(
|
|
("a" | "b") & ("b" | "c")
|
|
)"));
|
|
auto c9Reverse = toString(normal(R"(
|
|
("b" | "c") & ("a" | "b")
|
|
)"));
|
|
CHECK(c9 == c9Reverse);
|
|
CHECK_EQ("\"b\"", c9);
|
|
|
|
auto l = toString(normal(R"(
|
|
(string | number) & ("a" | true)
|
|
)"));
|
|
auto r = toString(normal(R"(
|
|
("a" | true) & (string | number)
|
|
)"));
|
|
CHECK(l == r);
|
|
CHECK_EQ("\"a\"", l);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "negate_string")
|
|
{
|
|
CHECK("number" == toString(normal(R"(
|
|
(number | string) & Not<string>
|
|
)")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "negate_string_from_cofinite_string_intersection")
|
|
{
|
|
CHECK("number" == toString(normal(R"(
|
|
(number | (string & Not<"hello"> & Not<"world">)) & Not<string>
|
|
)")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "no_op_negation_is_dropped")
|
|
{
|
|
CHECK("number" == toString(normal(R"(
|
|
number & Not<string>
|
|
)")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "union_of_negation")
|
|
{
|
|
CHECK("string" == toString(normal(R"(
|
|
(string & Not<"hello">) | "hello"
|
|
)")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "intersect_truthy")
|
|
{
|
|
CHECK("number | string | true" == toString(normal(R"(
|
|
(string | number | boolean | nil) & Not<false | nil>
|
|
)")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "intersect_truthy_expressed_as_intersection")
|
|
{
|
|
CHECK("number | string | true" == toString(normal(R"(
|
|
(string | number | boolean | nil) & Not<false> & Not<nil>
|
|
)")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "intersect_error")
|
|
{
|
|
std::shared_ptr<const NormalizedType> norm = toNormalizedType(R"(string & AAA)", 1);
|
|
REQUIRE(norm);
|
|
CHECK("*error-type*" == toString(normalizer.typeFromNormal(*norm)));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "intersect_not_error")
|
|
{
|
|
std::shared_ptr<const NormalizedType> norm = toNormalizedType(R"(string & Not<)", 1);
|
|
REQUIRE(norm);
|
|
CHECK("*error-type*" == toString(normalizer.typeFromNormal(*norm)));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "union_of_union")
|
|
{
|
|
CHECK(R"("alpha" | "beta" | "gamma")" == toString(normal(R"(
|
|
("alpha" | "beta") | "gamma"
|
|
)")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "union_of_negations")
|
|
{
|
|
CHECK(R"(string & ~"world")" == toString(normal(R"(
|
|
(string & Not<"hello"> & Not<"world">) | (string & Not<"goodbye"> & Not<"world">)
|
|
)")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "disjoint_negations_normalize_to_string")
|
|
{
|
|
CHECK(R"(string)" == toString(normal(R"(
|
|
(string & Not<"hello"> & Not<"world">) | (string & Not<"goodbye">)
|
|
)")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "negate_boolean")
|
|
{
|
|
CHECK("true" == toString(normal(R"(
|
|
boolean & Not<false>
|
|
)")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "negate_boolean_2")
|
|
{
|
|
CHECK("never" == toString(normal(R"(
|
|
true & Not<true>
|
|
)")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "double_negation")
|
|
{
|
|
CHECK("number" == toString(normal(R"(
|
|
number & Not<Not<any>>
|
|
)")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "negate_any")
|
|
{
|
|
CHECK("number" == toString(normal(R"(
|
|
number & Not<any>
|
|
)")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "intersect_function_and_top_function")
|
|
{
|
|
CHECK("() -> ()" == toString(normal(R"(
|
|
fun & (() -> ())
|
|
)")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "intersect_function_and_top_function_reverse")
|
|
{
|
|
CHECK("() -> ()" == toString(normal(R"(
|
|
(() -> ()) & fun
|
|
)")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "union_function_and_top_function")
|
|
{
|
|
CHECK("function" == toString(normal(R"(
|
|
fun | (() -> ())
|
|
)")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "negated_function_is_anything_except_a_function")
|
|
{
|
|
CHECK("(boolean | buffer | class | number | string | table | thread)?" == toString(normal(R"(
|
|
Not<fun>
|
|
)")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "specific_functions_cannot_be_negated")
|
|
{
|
|
CHECK(nullptr == toNormalizedType("Not<(boolean) -> boolean>", FFlag::LuauSolverV2 ? 1 : 0));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "trivial_intersection_inhabited")
|
|
{
|
|
// this test was used to fix a bug in normalization when working with intersections/unions of the same type.
|
|
|
|
TypeId a = arena.addType(FunctionType{builtinTypes->emptyTypePack, builtinTypes->anyTypePack, std::nullopt, false});
|
|
TypeId c = arena.addType(IntersectionType{{a, a}});
|
|
|
|
std::shared_ptr<const NormalizedType> n = normalizer.normalize(c);
|
|
REQUIRE(n);
|
|
|
|
CHECK(normalizer.isInhabited(n.get()) == NormalizationResult::True);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "bare_negated_boolean")
|
|
{
|
|
CHECK("(buffer | class | function | number | string | table | thread)?" == toString(normal(R"(
|
|
Not<boolean>
|
|
)")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "higher_order_function")
|
|
{
|
|
check(R"(
|
|
function apply(f, x)
|
|
return f(x)
|
|
end
|
|
|
|
local a = apply(function(x: number) return x + x end, 5)
|
|
)");
|
|
|
|
TypeId aType = requireType("a");
|
|
CHECK_MESSAGE(isNumber(follow(aType)), "Expected a number but got ", toString(aType));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "higher_order_function_with_annotation")
|
|
{
|
|
// CLI-117088 - Inferring the type of a higher order function with an annotation sometimes doesn't fully constrain the type (there are free types
|
|
// left over).
|
|
if (FFlag::LuauSolverV2)
|
|
return;
|
|
check(R"(
|
|
function apply<a, b>(f: (a) -> b, x)
|
|
return f(x)
|
|
end
|
|
)");
|
|
|
|
CHECK_EQ("<a, b>((a) -> b, a) -> b", toString(requireType("apply")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "cyclic_table_normalizes_sensibly")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local Cyclic = {}
|
|
function Cyclic.get()
|
|
return Cyclic
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
TypeId ty = requireType("Cyclic");
|
|
CHECK_EQ("t1 where t1 = { get: () -> t1 }", toString(ty, {true}));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "skip_force_normal_on_external_types")
|
|
{
|
|
createSomeExternTypes(&frontend);
|
|
|
|
CheckResult result = check(R"(
|
|
export type t0 = { a: Child }
|
|
export type t1 = { a: typeof(string.byte) }
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "intersection_combine_on_bound_self")
|
|
{
|
|
CheckResult result = check(R"(
|
|
export type t0 = (((any)&({_:l0.t0,n0:t0,_G:any,}))&({_:any,}))&(((any)&({_:l0.t0,n0:t0,_G:any,}))&({_:any,}))
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "unions_of_extern_types")
|
|
{
|
|
createSomeExternTypes(&frontend);
|
|
CHECK("Parent | Unrelated" == toString(normal("Parent | Unrelated")));
|
|
CHECK("Parent" == toString(normal("Parent | Child")));
|
|
CHECK("Parent | Unrelated" == toString(normal("Parent | Child | Unrelated")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "intersections_of_extern_types")
|
|
{
|
|
createSomeExternTypes(&frontend);
|
|
CHECK("Child" == toString(normal("Parent & Child")));
|
|
CHECK("never" == toString(normal("Child & Unrelated")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "narrow_union_of_extern_types_with_intersection")
|
|
{
|
|
createSomeExternTypes(&frontend);
|
|
CHECK("Child" == toString(normal("(Child | Unrelated) & Child")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "intersection_of_metatables_where_the_metatable_is_top_or_bottom")
|
|
{
|
|
if (FFlag::LuauSolverV2)
|
|
CHECK("{ @metatable *error-type*, { } }" == toString(normal("Mt<{}, any> & Mt<{}, err>")));
|
|
else
|
|
CHECK("{ @metatable *error-type*, {| |} }" == toString(normal("Mt<{}, any> & Mt<{}, err>")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "recurring_intersection")
|
|
{
|
|
CheckResult result = check(R"(
|
|
type A = any?
|
|
type B = A & A
|
|
)");
|
|
|
|
std::optional<TypeId> t = lookupType("B");
|
|
REQUIRE(t);
|
|
|
|
std::shared_ptr<const NormalizedType> nt = normalizer.normalize(*t);
|
|
REQUIRE(nt);
|
|
|
|
CHECK("any" == toString(normalizer.typeFromNormal(*nt)));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "cyclic_union")
|
|
{
|
|
// T where T = any & (number | T)
|
|
TypeId t = arena.addType(BlockedType{});
|
|
TypeId u = arena.addType(UnionType{{builtinTypes->numberType, t}});
|
|
asMutable(t)->ty.emplace<IntersectionType>(IntersectionType{{builtinTypes->anyType, u}});
|
|
|
|
std::shared_ptr<const NormalizedType> nt = normalizer.normalize(t);
|
|
REQUIRE(nt);
|
|
|
|
CHECK("number" == toString(normalizer.typeFromNormal(*nt)));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "cyclic_union_of_intersection")
|
|
{
|
|
// t1 where t1 = (string & t1) | string
|
|
TypeId boundTy = arena.addType(BlockedType{});
|
|
TypeId intersectTy = arena.addType(IntersectionType{{builtinTypes->stringType, boundTy}});
|
|
TypeId unionTy = arena.addType(UnionType{{builtinTypes->stringType, intersectTy}});
|
|
asMutable(boundTy)->reassign(Type{BoundType{unionTy}});
|
|
|
|
std::shared_ptr<const NormalizedType> nt = normalizer.normalize(unionTy);
|
|
|
|
CHECK("string" == toString(normalizer.typeFromNormal(*nt)));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "cyclic_intersection_of_unions")
|
|
{
|
|
// t1 where t1 = (string & t1) | string
|
|
TypeId boundTy = arena.addType(BlockedType{});
|
|
TypeId unionTy = arena.addType(UnionType{{builtinTypes->stringType, boundTy}});
|
|
TypeId intersectionTy = arena.addType(IntersectionType{{builtinTypes->stringType, unionTy}});
|
|
asMutable(boundTy)->reassign(Type{BoundType{intersectionTy}});
|
|
|
|
std::shared_ptr<const NormalizedType> nt = normalizer.normalize(intersectionTy);
|
|
|
|
CHECK("string" == toString(normalizer.typeFromNormal(*nt)));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "crazy_metatable")
|
|
{
|
|
CHECK("never" == toString(normal("Mt<{}, number> & Mt<{}, string>")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "negations_of_extern_types")
|
|
{
|
|
createSomeExternTypes(&frontend);
|
|
CHECK("(Parent & ~Child) | Unrelated" == toString(normal("(Parent & Not<Child>) | Unrelated")));
|
|
CHECK("((class & ~Child) | boolean | buffer | function | number | string | table | thread)?" == toString(normal("Not<Child>")));
|
|
CHECK("never" == toString(normal("Not<Parent> & Child")));
|
|
CHECK("((class & ~Parent) | Child | boolean | buffer | function | number | string | table | thread)?" == toString(normal("Not<Parent> | Child")));
|
|
CHECK("(boolean | buffer | function | number | string | table | thread)?" == toString(normal("Not<cls>")));
|
|
CHECK(
|
|
"(Parent | Unrelated | boolean | buffer | function | number | string | table | thread)?" ==
|
|
toString(normal("Not<cls & Not<Parent> & Not<Child> & Not<Unrelated>>"))
|
|
);
|
|
CHECK("Child" == toString(normal("(Child | Unrelated) & Not<Unrelated>")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "extern_types_and_unknown")
|
|
{
|
|
createSomeExternTypes(&frontend);
|
|
CHECK("Parent" == toString(normal("Parent & unknown")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "extern_types_and_never")
|
|
{
|
|
createSomeExternTypes(&frontend);
|
|
CHECK("never" == toString(normal("Parent & never")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "top_table_type")
|
|
{
|
|
CHECK("table" == toString(normal("{} | tbl")));
|
|
if (FFlag::LuauSolverV2)
|
|
CHECK("{ }" == toString(normal("{} & tbl")));
|
|
else
|
|
CHECK("{| |}" == toString(normal("{} & tbl")));
|
|
CHECK("never" == toString(normal("number & tbl")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "negations_of_tables")
|
|
{
|
|
CHECK(nullptr == toNormalizedType("Not<{}>", FFlag::LuauSolverV2 ? 1 : 0));
|
|
CHECK("(boolean | buffer | class | function | number | string | thread)?" == toString(normal("Not<tbl>")));
|
|
CHECK("table" == toString(normal("Not<Not<tbl>>")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "normalize_blocked_types")
|
|
{
|
|
Type blocked{BlockedType{}};
|
|
|
|
std::shared_ptr<const NormalizedType> norm = normalizer.normalize(&blocked);
|
|
|
|
CHECK_EQ(normalizer.typeFromNormal(*norm), &blocked);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "normalize_is_exactly_number")
|
|
{
|
|
std::shared_ptr<const NormalizedType> number = normalizer.normalize(builtinTypes->numberType);
|
|
// 1. all types for which Types::number say true for, NormalizedType::isExactlyNumber should say true as well
|
|
CHECK(Luau::isNumber(builtinTypes->numberType) == number->isExactlyNumber());
|
|
// 2. isExactlyNumber should handle cases like `number & number`
|
|
TypeId intersection = arena.addType(IntersectionType{{builtinTypes->numberType, builtinTypes->numberType}});
|
|
std::shared_ptr<const NormalizedType> normIntersection = normalizer.normalize(intersection);
|
|
CHECK(normIntersection->isExactlyNumber());
|
|
|
|
// 3. isExactlyNumber should reject things that are definitely not precisely numbers `number | any`
|
|
|
|
TypeId yoonion = arena.addType(UnionType{{builtinTypes->anyType, builtinTypes->numberType}});
|
|
std::shared_ptr<const NormalizedType> unionIntersection = normalizer.normalize(yoonion);
|
|
CHECK(!unionIntersection->isExactlyNumber());
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "normalize_unknown")
|
|
{
|
|
auto nt = toNormalizedType("Not<string> | Not<number>");
|
|
CHECK(nt);
|
|
CHECK(nt->isUnknown());
|
|
CHECK(toString(normalizer.typeFromNormal(*nt)) == "unknown");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "read_only_props")
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
|
|
|
|
CHECK("{ x: string }" == toString(normal("{ read x: string } & { x: string }"), {true}));
|
|
CHECK("{ x: string }" == toString(normal("{ x: string } & { read x: string }"), {true}));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "read_only_props_2")
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
|
|
|
|
CHECK(R"({ x: "hello" })" == toString(normal(R"({ x: "hello" } & { x: string })"), {true}));
|
|
CHECK(R"(never)" == toString(normal(R"({ x: "hello" } & { x: "world" })"), {true}));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "read_only_props_3")
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
|
|
|
|
CHECK(R"({ read x: "hello" })" == toString(normal(R"({ read x: "hello" } & { read x: string })"), {true}));
|
|
CHECK("never" == toString(normal(R"({ read x: "hello" } & { read x: "world" })"), {true}));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "final_types_are_cached")
|
|
{
|
|
std::shared_ptr<const NormalizedType> na1 = normalizer.normalize(builtinTypes->numberType);
|
|
std::shared_ptr<const NormalizedType> na2 = normalizer.normalize(builtinTypes->numberType);
|
|
|
|
CHECK(na1 == na2);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "non_final_types_can_be_normalized_but_are_not_cached")
|
|
{
|
|
TypeId a = arena.freshType(builtinTypes, &globalScope);
|
|
|
|
std::shared_ptr<const NormalizedType> na1 = normalizer.normalize(a);
|
|
std::shared_ptr<const NormalizedType> na2 = normalizer.normalize(a);
|
|
|
|
CHECK(na1 != na2);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "intersect_with_not_unknown")
|
|
{
|
|
TypeId notUnknown = arena.addType(NegationType{builtinTypes->unknownType});
|
|
TypeId type = arena.addType(IntersectionType{{builtinTypes->numberType, notUnknown}});
|
|
std::shared_ptr<const NormalizedType> normalized = normalizer.normalize(type);
|
|
|
|
CHECK("never" == toString(normalizer.typeFromNormal(*normalized.get())));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "cyclic_stack_overflow_1")
|
|
{
|
|
ScopedFastInt sfi{FInt::LuauTypeInferRecursionLimit, 165};
|
|
this->unifierState.counters.recursionLimit = FInt::LuauTypeInferRecursionLimit;
|
|
TypeId t1 = arena.addType(TableType{});
|
|
TypeId t2 = arena.addType(TableType{});
|
|
TypeId t3 = arena.addType(IntersectionType{{t1, t2}});
|
|
asMutable(t1)->ty.get_if<TableType>()->props = {{"foo", Property::readonly(t2)}};
|
|
asMutable(t2)->ty.get_if<TableType>()->props = {{"foo", Property::readonly(t1)}};
|
|
|
|
std::shared_ptr<const NormalizedType> normalized = normalizer.normalize(t3);
|
|
CHECK(normalized);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "cyclic_stack_overflow_2")
|
|
{
|
|
ScopedFastInt sfi{FInt::LuauTypeInferRecursionLimit, 165};
|
|
this->unifierState.counters.recursionLimit = FInt::LuauTypeInferRecursionLimit;
|
|
TypeId t1 = arena.addType(TableType{});
|
|
TypeId t2 = arena.addType(TableType{});
|
|
TypeId t3 = arena.addType(IntersectionType{{t1, t2}});
|
|
asMutable(t1)->ty.get_if<TableType>()->props = {{"foo", Property::readonly(t3)}};
|
|
asMutable(t2)->ty.get_if<TableType>()->props = {{"foo", Property::readonly(t1)}};
|
|
|
|
std::shared_ptr<const NormalizedType> normalized = normalizer.normalize(t3);
|
|
CHECK(normalized);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "truthy_table_property_and_optional_table_with_optional_prop")
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
|
|
|
|
// { x: ~(false?) }
|
|
TypeId t1 = arena.addType(TableType{TableType::Props{{"x", builtinTypes->truthyType}}, std::nullopt, TypeLevel{}, TableState::Sealed});
|
|
|
|
// { x: number? }?
|
|
TypeId t2 = arena.addType(UnionType{
|
|
{arena.addType(TableType{TableType::Props{{"x", builtinTypes->optionalNumberType}}, std::nullopt, TypeLevel{}, TableState::Sealed}),
|
|
builtinTypes->nilType}
|
|
});
|
|
|
|
TypeId intersection = arena.addType(IntersectionType{{t2, t1}});
|
|
|
|
auto norm = normalizer.normalize(intersection);
|
|
REQUIRE(norm);
|
|
|
|
TypeId ty = normalizer.typeFromNormal(*norm);
|
|
CHECK("{ x: number }" == toString(ty));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "free_type_and_not_truthy")
|
|
{
|
|
ScopedFastFlag sff[] = {
|
|
{FFlag::LuauSolverV2, true}, // Only because it affects the stringification of free types
|
|
};
|
|
|
|
TypeId freeTy = arena.freshType(builtinTypes, &globalScope);
|
|
TypeId notTruthy = arena.addType(NegationType{builtinTypes->truthyType}); // ~~(false?)
|
|
|
|
TypeId intersectionTy = arena.addType(IntersectionType{{freeTy, notTruthy}}); // 'a & ~~(false?)
|
|
|
|
auto norm = normalizer.normalize(intersectionTy);
|
|
REQUIRE(norm);
|
|
|
|
TypeId result = normalizer.typeFromNormal(*norm);
|
|
|
|
CHECK("'a & (false?)" == toString(result));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(NormalizeFixture, "normalize_recursive_metatable")
|
|
{
|
|
ScopedFastFlag sff[] = {{FFlag::LuauSolverV2, true}, {FFlag::LuauNormalizationCatchMetatableCycles, true}};
|
|
|
|
TypeId root = arena.addType(BlockedType{});
|
|
TypeId emptyTable = arena.addType(TableType(TableState::Sealed, {}));
|
|
TypeId metatable = arena.addType(MetatableType{emptyTable, root});
|
|
emplaceType<BoundType>(asMutable(root), metatable);
|
|
auto normalized = normalizer.normalize(root);
|
|
REQUIRE(normalized);
|
|
CHECK_EQ("t1 where t1 = { @metatable t1, { } }", toString(normalizer.typeFromNormal(*normalized)));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "normalizer_should_be_able_to_detect_cyclic_tables_and_not_stack_overflow")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
ScopedFastInt sfi{FInt::LuauTypeInferRecursionLimit, 0};
|
|
|
|
CheckResult result = check(R"(
|
|
--!strict
|
|
|
|
type Array<T> = { [number] : T}
|
|
type Object = { [number] : any}
|
|
|
|
type Set<T> = typeof(setmetatable(
|
|
{} :: {
|
|
size: number,
|
|
-- method definitions
|
|
add: (self: Set<T>, T) -> Set<T>,
|
|
clear: (self: Set<T>) -> (),
|
|
delete: (self: Set<T>, T) -> boolean,
|
|
has: (self: Set<T>, T) -> boolean,
|
|
ipairs: (self: Set<T>) -> any,
|
|
},
|
|
{} :: {
|
|
__index: Set<T>,
|
|
__iter: (self: Set<T>) -> (<K, V>({ [K]: V }, K?) -> (K, V), T),
|
|
}
|
|
))
|
|
|
|
type Map<K, V> = typeof(setmetatable(
|
|
{} :: {
|
|
size: number,
|
|
-- method definitions
|
|
set: (self: Map<K, V>, K, V) -> Map<K, V>,
|
|
get: (self: Map<K, V>, K) -> V | nil,
|
|
clear: (self: Map<K, V>) -> (),
|
|
delete: (self: Map<K, V>, K) -> boolean,
|
|
[K]: V,
|
|
has: (self: Map<K, V>, K) -> boolean,
|
|
keys: (self: Map<K, V>) -> Array<K>,
|
|
values: (self: Map<K, V>) -> Array<V>,
|
|
entries: (self: Map<K, V>) -> Array<Tuple<K, V>>,
|
|
ipairs: (self: Map<K, V>) -> any,
|
|
_map: { [K]: V },
|
|
_array: { [number]: K },
|
|
__index: (self: Map<K, V>, key: K) -> V,
|
|
__iter: (self: Map<K, V>) -> (<K, V>({ [K]: V }, K?) -> (K?, V), V),
|
|
__newindex: (self: Map<K, V>, key: K, value: V) -> (),
|
|
},
|
|
{} :: {
|
|
__index: Map<K, V>,
|
|
__iter: (self: Map<K, V>) -> (<K, V>({ [K]: V }, K?) -> (K, V), V),
|
|
__newindex: (self: Map<K, V>, key: K, value: V) -> (),
|
|
}
|
|
))
|
|
type mapFn<T, U> = (element: T, index: number) -> U
|
|
type mapFnWithThisArg<T, U> = (thisArg: any, element: T, index: number) -> U
|
|
|
|
function fromSet<T, U>(
|
|
value: Set<T>,
|
|
mapFn: (mapFn<T, U> | mapFnWithThisArg<T, U>)?,
|
|
thisArg: Object?
|
|
-- FIXME Luau: need overloading so the return type on this is more sane and doesn't require manual casts
|
|
): Array<U> | Array<T> | Array<string>
|
|
|
|
local array : { [number] : string} = {"foo"}
|
|
return array
|
|
end
|
|
|
|
function instanceof(tbl: any, class: any): boolean
|
|
return true
|
|
end
|
|
|
|
function fromArray<T, U>(
|
|
value: Array<T>,
|
|
mapFn: (mapFn<T, U> | mapFnWithThisArg<T, U>)?,
|
|
thisArg: Object?
|
|
-- FIXME Luau: need overloading so the return type on this is more sane and doesn't require manual casts
|
|
): Array<U> | Array<T> | Array<string>
|
|
local array : {[number] : string} = {}
|
|
return array
|
|
end
|
|
|
|
return function<T, U>(
|
|
value: string | Array<T> | Set<T> | Map<any, any>,
|
|
mapFn: (mapFn<T, U> | mapFnWithThisArg<T, U>)?,
|
|
thisArg: Object?
|
|
-- FIXME Luau: need overloading so the return type on this is more sane and doesn't require manual casts
|
|
): Array<U> | Array<T> | Array<string>
|
|
if value == nil then
|
|
error("cannot create array from a nil value")
|
|
end
|
|
local array: Array<U> | Array<T> | Array<string>
|
|
|
|
if instanceof(value, Set) then
|
|
array = fromSet(value :: Set<T>, mapFn, thisArg)
|
|
else
|
|
array = {}
|
|
end
|
|
|
|
|
|
return array
|
|
end
|
|
)");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "fuzz_limit_function_intersection_complexity")
|
|
{
|
|
ScopedFastInt luauTypeInferRecursionLimit{FInt::LuauTypeInferRecursionLimit, 80};
|
|
ScopedFastInt luauNormalizeIntersectionLimit{FInt::LuauNormalizeIntersectionLimit, 50};
|
|
ScopedFastInt luauNormalizeUnionLimit{FInt::LuauNormalizeUnionLimit, 20};
|
|
|
|
CheckResult result = check(R"(
|
|
function _(_).readu32(l0)
|
|
return ({[_(_(_))]=_,[_(if _ then _)]=_,n0=_,})[_],nil
|
|
end
|
|
_(_)[_(n32)] %= _(_(_))
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERRORS(result);
|
|
}
|
|
|
|
#if !(defined(_WIN32) && !(defined(_M_X64) || defined(_M_ARM64)))
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "fuzz_propagate_normalization_failures")
|
|
{
|
|
ScopedFastInt luauNormalizeIntersectionLimit{FInt::LuauNormalizeIntersectionLimit, 50};
|
|
ScopedFastInt luauNormalizeUnionLimit{FInt::LuauNormalizeUnionLimit, 20};
|
|
ScopedFastFlag luauSubtypingEnableReasoningLimit{FFlag::LuauSubtypingEnableReasoningLimit, true};
|
|
ScopedFastFlag luauTurnOffNonreentrantGeneralization{FFlag::LuauNonReentrantGeneralization2, false};
|
|
|
|
CheckResult result = check(R"(
|
|
function _(_,"").readu32(l0)
|
|
return ({[_(_(_))]=_,[_(if _ then _,_())]=_,[""]=_,})[_],nil
|
|
end
|
|
_().readu32 %= _(_(_(_),_))
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERRORS(result);
|
|
}
|
|
#endif
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "fuzz_flatten_type_pack_cycle")
|
|
{
|
|
ScopedFastFlag sff[] = {{FFlag::LuauSolverV2, true}, {FFlag::LuauTypePackDetectCycles, true}};
|
|
|
|
// Note: if this stops throwing an exception, it means we fixed cycle construction and can replace with a regular check
|
|
CHECK_THROWS_AS(
|
|
check(R"(
|
|
function _(_).readu32<t0...>()
|
|
repeat
|
|
until function<t4>()
|
|
end
|
|
return if _ then _,_(_)
|
|
end
|
|
_(_(_(_)),``)
|
|
do end
|
|
)"),
|
|
InternalCompilerError
|
|
);
|
|
}
|
|
#if 0
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "fuzz_union_type_pack_cycle")
|
|
{
|
|
// FIXME? This test code happens not to ICE with eager generalization
|
|
// enabled. This could either be because the problem is fixed, or because
|
|
// another bug is obscuring the problem.
|
|
if (FFlag::DebugLuauGreedyGeneralization)
|
|
return;
|
|
|
|
ScopedFastFlag sff[] = {{FFlag::LuauSolverV2, true}, {FFlag::LuauTypePackDetectCycles, true}};
|
|
|
|
// Note: if this stops throwing an exception, it means we fixed cycle construction and can replace with a regular check
|
|
CHECK_THROWS_AS(
|
|
check(R"(
|
|
function _(_).n0(l32,...)
|
|
return ({n0=_,[_(if _ then _,nil)]=- _,[_(_(_))]=_,})[_],_(_)
|
|
end
|
|
_[_] ^= _(_(_))
|
|
)"),
|
|
InternalCompilerError
|
|
);
|
|
}
|
|
#endif
|
|
|
|
TEST_SUITE_END();
|