luau/tests/Normalize.test.cpp
Varun Saini 5965818283
Sync to upstream/release/675 (#1845)
## General 
- Introduce `Frontend::parseModules` for parsing a group of modules at
once.
- Support chained function types in the CST.

## New Type Solver
- Enable write-only table properties (described in [this
RFC](https://rfcs.luau.org/property-writeonly.html)).
- Disable singleton inference for large tables to improve performance.
- Fix a bug that occurs when we try to expand a type alias to itself.
- Catch cancelation during the type-checking phase in addition to during
constraint solving.
- Fix stringification of the empty type pack: `()`.
- Improve errors for calls being rejected on the primitive `function`
type.
- Rework generalization: We now generalize types as soon as the last
constraint relating to them is finished. We think this will reduce the
number of cases where type inference fails to complete and reduce the
number of instances where `*blocked*` types appear in the inference
result.

## VM/Runtime
- Dynamically disable native execution for functions that incur a
slowdown (relative to bytecode execution).
- Improve names for `thread`/`closure`/`proto` in the Luau heap dump.

---

Co-authored-by: Andy Friesen <afriesen@roblox.com>
Co-authored-by: Ariel Weiss <aaronweiss@roblox.com>
Co-authored-by: Aviral Goel <agoel@roblox.com>
Co-authored-by: Hunter Goldstein <hgoldstein@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>

---------

Co-authored-by: Hunter Goldstein <hgoldstein@roblox.com>
Co-authored-by: Alexander Youngblood <ayoungblood@roblox.com>
Co-authored-by: Menarul Alam <malam@roblox.com>
Co-authored-by: Aviral Goel <agoel@roblox.com>
Co-authored-by: Vighnesh <vvijay@roblox.com>
Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
Co-authored-by: Ariel Weiss <aaronweiss@roblox.com>
Co-authored-by: Andy Friesen <afriesen@roblox.com>
2025-05-27 14:24:46 -07:00

1226 lines
34 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(LuauEagerGeneralization)
LUAU_FASTFLAG(LuauRefineWaitForBlockedTypesInTarget)
LUAU_FASTFLAG(LuauSimplifyOutOfLine)
LUAU_FASTFLAG(LuauOptimizeFalsyAndTruthyIntersect)
LUAU_FASTFLAG(LuauClipVariadicAnysFromArgsToGenericFuncs2)
LUAU_FASTFLAG(LuauSubtypeGenericsAndNegations)
LUAU_FASTFLAG(LuauNoMoreInjectiveTypeFunctions)
LUAU_FASTFLAG(LuauSubtypeGenericsAndNegations)
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(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_flatten_type_pack_cycle")
{
ScopedFastFlag sff[] = {{FFlag::LuauSolverV2, 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")
{
ScopedFastFlag sff[] = {
{FFlag::LuauSolverV2, true},
{FFlag::LuauRefineWaitForBlockedTypesInTarget, true},
{FFlag::LuauSimplifyOutOfLine, true},
{FFlag::LuauSubtypeGenericsAndNegations, true},
{FFlag::LuauNoMoreInjectiveTypeFunctions, true},
{FFlag::LuauOptimizeFalsyAndTruthyIntersect, true},
{FFlag::LuauClipVariadicAnysFromArgsToGenericFuncs2, true},
{FFlag::LuauEagerGeneralization, true}
};
ScopedFastInt sfi{FInt::LuauTypeInferRecursionLimit, 0};
// FIXME CLI-153131: This is constructing a cyclic type pack
CHECK_THROWS_AS(
check(R"(
function _(_).n0(l32,...)
return ({n0=_,[_(if _ then _,nil)]=- _,[_(_(_))]=_,})[_],_(_)
end
_[_] ^= _(_(_))
)"),
InternalCompilerError
);
}
#endif
TEST_SUITE_END();