luau/tests/Normalize.test.cpp
Aviral Goel ee1c6bf0db
Sync to upstream/release/668 (#1760)
## New Type Solver

1. Update resolved types for singleton unions and intersections to avoid
crashing when type checking type assertions.
2. Generalize free return type pack of a function type inferred at call
site to ensure that the free type does not leak to another module.
3. Fix crash from cyclic indexers by reducing if possible or producing
an error otherwise.
4. Fix handling of irreducible type functions to prevent type inference
from failing.
5. Fix handling of recursive metatables to avoid infinite recursion.

## New and Old Type Solver

Fix accidental capture of all exceptions in multi-threaded typechecking
by converting all typechecking exceptions to `InternalCompilerError` and
only capturing those.

## Fragment Autocomplete

1. Add a block based diff algorithm based on class index and span for
re-typechecking. This reduces the granularity of fragment autocomplete
to avoid flakiness when the fragment does not have enough type
information.
2. Fix bugs arising from incorrect scope selection for autocompletion.

## Roundtrippable AST

Store type alias location in `TypeFun` class to ensure it is accessible
for exported types as part of the public interface.

## Build System

1. Bump minimum supported CMake version to 3.10 since GitHub is phasing
out the currently supported minimum version 3.0, released 11 years ago.
2. Fix compilation when `HARDSTACKTESTS` is enabled.

## Miscellaneous

Flag removals and cleanup of unused code.

## Internal Contributors

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: Talha Pathan <tpathan@roblox.com>
Co-authored-by: Vighnesh Vijay <vvijay@roblox.com>
Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>

## External Contributors

Thanks to [@grh-official](https://github.com/grh-official) for PR #1759 

**Full Changelog**:
https://github.com/luau-lang/luau/compare/0.667...0.668

---------

Co-authored-by: Hunter Goldstein <hgoldstein@roblox.com>
Co-authored-by: Varun Saini <61795485+vrn-sn@users.noreply.github.com>
Co-authored-by: Alexander Youngblood <ayoungblood@roblox.com>
Co-authored-by: Menarul Alam <malam@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>
2025-04-04 14:11:51 -07:00

1230 lines
35 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(LuauNormalizeNegatedErrorToAnError)
LUAU_FASTFLAG(LuauNormalizeIntersectErrorToAnError)
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTINT(LuauTypeInferRecursionLimit)
LUAU_FASTINT(LuauNormalizeIntersectionLimit)
LUAU_FASTINT(LuauNormalizeUnionLimit)
LUAU_FASTFLAG(LuauNormalizeLimitFunctionSet)
LUAU_FASTFLAG(LuauSubtypingStopAtNormFail)
LUAU_FASTFLAG(LuauNormalizationCatchMetatableCycles)
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, "classes")
{
createSomeClasses(&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")
{
ScopedFastFlag luauNormalizeIntersectErrorToAnError{FFlag::LuauNormalizeIntersectErrorToAnError, true};
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")
{
ScopedFastFlag luauNormalizeIntersectErrorToAnError{FFlag::LuauNormalizeIntersectErrorToAnError, true};
ScopedFastFlag luauNormalizeNegatedErrorToAnError{FFlag::LuauNormalizeNegatedErrorToAnError, true};
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")
{
createSomeClasses(&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_classes")
{
createSomeClasses(&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_classes")
{
createSomeClasses(&frontend);
CHECK("Child" == toString(normal("Parent & Child")));
CHECK("never" == toString(normal("Child & Unrelated")));
}
TEST_CASE_FIXTURE(NormalizeFixture, "narrow_union_of_classes_with_intersection")
{
createSomeClasses(&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_classes")
{
createSomeClasses(&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, "classes_and_unknown")
{
createSomeClasses(&frontend);
CHECK("Parent" == toString(normal("Parent & unknown")));
}
TEST_CASE_FIXTURE(NormalizeFixture, "classes_and_never")
{
createSomeClasses(&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 luauNormalizeIntersectionLimit{FInt::LuauNormalizeIntersectionLimit, 50};
ScopedFastInt luauNormalizeUnionLimit{FInt::LuauNormalizeUnionLimit, 20};
ScopedFastFlag luauNormalizeLimitFunctionSet{FFlag::LuauNormalizeLimitFunctionSet, true};
ScopedFastFlag luauSubtypingStopAtNormFail{FFlag::LuauSubtypingStopAtNormFail, true};
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 luauNormalizeLimitFunctionSet{FFlag::LuauNormalizeLimitFunctionSet, true};
ScopedFastFlag luauSubtypingStopAtNormFail{FFlag::LuauSubtypingStopAtNormFail, true};
CheckResult result = check(R"(
function _(_,"").readu32(l0)
return ({[_(_(_))]=_,[_(if _ then _,_())]=_,[""]=_,})[_],nil
end
_().readu32 %= _(_(_(_),_))
)");
LUAU_REQUIRE_ERRORS(result);
}
#endif
TEST_SUITE_END();