luau/tests/Normalize.test.cpp
2022-08-18 14:04:33 -07:00

1171 lines
29 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 "doctest.h"
#include "Luau/Normalize.h"
#include "Luau/BuiltinDefinitions.h"
using namespace Luau;
struct NormalizeFixture : Fixture
{
ScopedFastFlag sff1{"LuauLowerBoundsCalculation", true};
bool isSubtype(TypeId a, TypeId b)
{
return ::Luau::isSubtype(a, b, NotNull{getMainModule()->getModuleScope().get()}, ice);
}
};
void createSomeClasses(TypeChecker& typeChecker)
{
auto& arena = typeChecker.globalTypes;
unfreeze(arena);
TypeId parentType = arena.addType(ClassTypeVar{"Parent", {}, std::nullopt, std::nullopt, {}, nullptr, "Test"});
ClassTypeVar* parentClass = getMutable<ClassTypeVar>(parentType);
parentClass->props["method"] = {makeFunction(arena, parentType, {}, {})};
parentClass->props["virtual_method"] = {makeFunction(arena, parentType, {}, {})};
addGlobalBinding(typeChecker, "Parent", {parentType});
typeChecker.globalScope->exportedTypeBindings["Parent"] = TypeFun{{}, parentType};
TypeId childType = arena.addType(ClassTypeVar{"Child", {}, parentType, std::nullopt, {}, nullptr, "Test"});
ClassTypeVar* childClass = getMutable<ClassTypeVar>(childType);
childClass->props["virtual_method"] = {makeFunction(arena, childType, {}, {})};
addGlobalBinding(typeChecker, "Child", {childType});
typeChecker.globalScope->exportedTypeBindings["Child"] = TypeFun{{}, childType};
TypeId unrelatedType = arena.addType(ClassTypeVar{"Unrelated", {}, std::nullopt, std::nullopt, {}, nullptr, "Test"});
addGlobalBinding(typeChecker, "Unrelated", {unrelatedType});
typeChecker.globalScope->exportedTypeBindings["Unrelated"] = TypeFun{{}, unrelatedType};
for (const auto& [name, ty] : typeChecker.globalScope->exportedTypeBindings)
persist(ty.type);
freeze(arena);
}
TEST_SUITE_BEGIN("isSubtype");
TEST_CASE_FIXTURE(NormalizeFixture, "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(NormalizeFixture, "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(NormalizeFixture, "functions_and_any")
{
check(R"(
function a(n: number) return "string" end
function b(q: any) return 5 :: any end
)");
TypeId a = requireType("a");
TypeId b = requireType("b");
// Intuition:
// We cannot use b where a is required because we cannot rely on b to return a string.
// We cannot use a where b is required because we cannot rely on a to accept non-number arguments.
CHECK(!isSubtype(b, a));
CHECK(!isSubtype(a, b));
}
TEST_CASE_FIXTURE(NormalizeFixture, "intersection_of_functions_of_different_arities")
{
check(R"(
type A = (any) -> ()
type B = (any, any) -> ()
type T = A & B
local a: A
local b: B
local t: T
)");
TypeId a = requireType("a");
TypeId b = requireType("b");
CHECK(!isSubtype(a, b)); // !!
CHECK(!isSubtype(b, a));
CHECK("((any) -> ()) & ((any, any) -> ())" == toString(requireType("t")));
}
TEST_CASE_FIXTURE(NormalizeFixture, "functions_with_mismatching_arity")
{
check(R"(
local a: (number) -> ()
local b: () -> ()
local c: () -> number
)");
TypeId a = requireType("a");
TypeId b = requireType("b");
TypeId c = requireType("c");
CHECK(!isSubtype(b, a));
CHECK(!isSubtype(c, a));
CHECK(!isSubtype(a, b));
CHECK(!isSubtype(c, b));
CHECK(!isSubtype(a, c));
CHECK(!isSubtype(b, c));
}
TEST_CASE_FIXTURE(NormalizeFixture, "functions_with_mismatching_arity_but_optional_parameters")
{
/*
* (T0..TN) <: (T0..TN, A?)
* (T0..TN) <: (T0..TN, any)
* (T0..TN, A?) </: (T0..TN) We don't technically need to spell this out, but it's quite important.
* T <: T
* if A <: B and B <: C then A <: C
* T -> R <: U -> S if U <: T and R <: S
* A | B <: T if A <: T and B <: T
* T <: A | B if T <: A or T <: B
*/
check(R"(
local a: (number?) -> ()
local b: (number) -> ()
local c: (number, number?) -> ()
)");
TypeId a = requireType("a");
TypeId b = requireType("b");
TypeId c = requireType("c");
/*
* (number) -> () </: (number?) -> ()
* because number? </: number (because number <: number, but nil </: number)
*/
CHECK(!isSubtype(b, a));
/*
* (number, number?) </: (number?) -> ()
* because number? </: number (as above)
*/
CHECK(!isSubtype(c, a));
/*
* (number?) -> () <: (number) -> ()
* because number <: number? (because number <: number)
*/
CHECK(isSubtype(a, b));
/*
* (number, number?) -> () <: (number) -> (number)
* The packs have inequal lengths, but (number) <: (number, number?)
* and number <: number
*/
CHECK(!isSubtype(c, b));
/*
* (number?) -> () </: (number, number?) -> ()
* because (number, number?) </: (number)
*/
CHECK(!isSubtype(a, c));
/*
* (number) -> () </: (number, number?) -> ()
* because (number, number?) </: (number)
*/
CHECK(!isSubtype(b, c));
}
TEST_CASE_FIXTURE(NormalizeFixture, "functions_with_mismatching_arity_but_any_is_an_optional_param")
{
check(R"(
local a: (number?) -> ()
local b: (number) -> ()
local c: (number, any) -> ()
)");
TypeId a = requireType("a");
TypeId b = requireType("b");
TypeId c = requireType("c");
/*
* (number) -> () </: (number?) -> ()
* because number? </: number (because number <: number, but nil </: number)
*/
CHECK(!isSubtype(b, a));
/*
* (number, any) </: (number?) -> ()
* because number? </: number (as above)
*/
CHECK(!isSubtype(c, a));
/*
* (number?) -> () <: (number) -> ()
* because number <: number? (because number <: number)
*/
CHECK(isSubtype(a, b));
/*
* (number, any) -> () </: (number) -> (number)
* The packs have inequal lengths
*/
CHECK(!isSubtype(c, b));
/*
* (number?) -> () </: (number, any) -> ()
* The packs have inequal lengths
*/
CHECK(!isSubtype(a, c));
/*
* (number) -> () </: (number, any) -> ()
* The packs have inequal lengths
*/
CHECK(!isSubtype(b, c));
}
TEST_CASE_FIXTURE(NormalizeFixture, "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(NormalizeFixture, "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(NormalizeFixture, "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(NormalizeFixture, "table_with_union_prop")
{
check(R"(
local a: {x: number}
local b: {x: number?}
)");
TypeId a = requireType("a");
TypeId b = requireType("b");
CHECK(isSubtype(a, b));
CHECK(!isSubtype(b, a));
}
TEST_CASE_FIXTURE(NormalizeFixture, "table_with_any_prop")
{
check(R"(
local a: {x: number}
local b: {x: any}
)");
TypeId a = requireType("a");
TypeId b = requireType("b");
CHECK(isSubtype(a, b));
CHECK(!isSubtype(b, a));
}
TEST_CASE_FIXTURE(NormalizeFixture, "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));
CHECK(!isSubtype(d, a));
CHECK(!isSubtype(a, d));
}
TEST_CASE_FIXTURE(NormalizeFixture, "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(NormalizeFixture, "table_with_table_prop")
{
check(R"(
type T = {x: {y: number}} & {x: {y: string}}
local a: T
)");
CHECK_EQ("{| x: {| y: number & string |} |}", toString(requireType("a")));
}
TEST_CASE_FIXTURE(NormalizeFixture, "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");
CHECK(isSubtype(a, b));
CHECK(!isSubtype(b, a));
CHECK(!isSubtype(c, a));
CHECK(!isSubtype(a, c));
CHECK(isSubtype(d, a));
CHECK(!isSubtype(a, d));
CHECK(isSubtype(d, b));
CHECK(!isSubtype(b, d));
}
#if 0
TEST_CASE_FIXTURE(NormalizeFixture, "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(NormalizeFixture, "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(NormalizeFixture, "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(NormalizeFixture, "classes")
{
createSomeClasses(typeChecker);
check(""); // Ensure that we have a main Module.
TypeId p = typeChecker.globalScope->lookupType("Parent")->type;
TypeId c = typeChecker.globalScope->lookupType("Child")->type;
TypeId u = typeChecker.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(NormalizeFixture, "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(NormalizeFixture, "intersection_of_tables")
{
check(R"(
type T = {x: number} & ({x: number} & {y: string?})
local t: T
)");
CHECK("{| x: number, y: string? |}" == toString(requireType("t")));
}
TEST_SUITE_END();
TEST_SUITE_BEGIN("Normalize");
TEST_CASE_FIXTURE(NormalizeFixture, "intersection_of_disjoint_tables")
{
check(R"(
type T = {a: number} & {b: number}
local t: T
)");
CHECK_EQ("{| a: number, b: number |}", toString(requireType("t")));
}
TEST_CASE_FIXTURE(NormalizeFixture, "intersection_of_overlapping_tables")
{
check(R"(
type T = {a: number, b: string} & {b: number, c: string}
local t: T
)");
CHECK_EQ("{| a: number, b: number & string, c: string |}", toString(requireType("t")));
}
TEST_CASE_FIXTURE(NormalizeFixture, "intersection_of_confluent_overlapping_tables")
{
check(R"(
type T = {a: number, b: string} & {b: string, c: string}
local t: T
)");
CHECK_EQ("{| a: number, b: string, c: string |}", toString(requireType("t")));
}
TEST_CASE_FIXTURE(NormalizeFixture, "union_with_overlapping_field_that_has_a_subtype_relationship")
{
check(R"(
local t: {x: number} | {x: number?}
)");
ModulePtr tempModule{new Module};
tempModule->scopes.emplace_back(Location(), std::make_shared<Scope>(getSingletonTypes().anyTypePack));
// HACK: Normalization is an in-place operation. We need to cheat a little here and unfreeze
// the arena that the type lives in.
ModulePtr mainModule = getMainModule();
unfreeze(mainModule->internalTypes);
TypeId tType = requireType("t");
normalize(tType, tempModule, *typeChecker.iceHandler);
CHECK_EQ("{| x: number? |}", toString(tType, {true}));
}
TEST_CASE_FIXTURE(NormalizeFixture, "intersection_of_functions")
{
check(R"(
type T = ((any) -> string) & ((number) -> string)
local t: T
)");
CHECK_EQ("(any) -> string", toString(requireType("t")));
}
TEST_CASE_FIXTURE(Fixture, "normalize_module_return_type")
{
ScopedFastFlag sff[] = {
{"LuauLowerBoundsCalculation", true},
};
check(R"(
--!nonstrict
if Math.random() then
return function(initialState, handlers)
return function(state, action)
return state
end
end
else
return function(initialState, handlers)
return function(state, action)
return state
end
end
end
)");
CHECK_EQ("(any, any) -> (...any)", toString(getMainModule()->getModuleScope()->returnType));
}
TEST_CASE_FIXTURE(Fixture, "return_type_is_not_a_constrained_intersection")
{
check(R"(
function foo(x:number, y:number)
return x + y
end
)");
CHECK_EQ("(number, number) -> number", toString(requireType("foo")));
}
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")
{
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")));
}
// Unfortunately, getting this right in the general case is difficult.
TEST_CASE_FIXTURE(Fixture, "cyclic_table_is_not_marked_normal")
{
ScopedFastFlag flags[] = {
{"LuauLowerBoundsCalculation", true},
};
check(R"(
type Fiber = {
return_: Fiber?
}
local f: Fiber
)");
TypeId t = requireType("f");
CHECK(!t->normal);
}
TEST_CASE_FIXTURE(Fixture, "variadic_tail_is_marked_normal")
{
ScopedFastFlag flags[] = {
{"LuauLowerBoundsCalculation", true},
};
CheckResult result = check(R"(
type Weirdo = (...{x: number}) -> ()
local w: Weirdo
)");
LUAU_REQUIRE_NO_ERRORS(result);
TypeId t = requireType("w");
auto ftv = get<FunctionTypeVar>(t);
REQUIRE(ftv);
auto [argHead, argTail] = flatten(ftv->argTypes);
CHECK(argHead.empty());
REQUIRE(argTail.has_value());
auto vtp = get<VariadicTypePack>(*argTail);
REQUIRE(vtp);
CHECK(vtp->ty->normal);
}
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(Fixture, "cyclic_union")
{
ScopedFastFlag sff[] = {
{"LuauLowerBoundsCalculation", true},
{"LuauFixNormalizationOfCyclicUnions", true},
};
CheckResult result = check(R"(
type T = {T?}?
local a: T
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK("t1? where t1 = {t1?}" == toString(requireType("a")));
}
TEST_CASE_FIXTURE(Fixture, "cyclic_intersection")
{
ScopedFastFlag sff[] = {
{"LuauLowerBoundsCalculation", true},
{"LuauFixNormalizationOfCyclicUnions", true},
};
CheckResult result = check(R"(
type T = {T & {}}
local a: T
)");
LUAU_REQUIRE_NO_ERRORS(result);
// FIXME: We are not properly normalizing this type, but we are at least not improperly discarding information
CHECK("t1 where t1 = {{t1 & {| |}}}" == toString(requireType("a"), {true}));
}
TEST_CASE_FIXTURE(Fixture, "intersection_of_tables_with_indexers")
{
ScopedFastFlag sff[] = {
{"LuauLowerBoundsCalculation", true},
{"LuauFixNormalizationOfCyclicUnions", true},
};
CheckResult result = check(R"(
type A = {number}
type B = {string}
type C = A & B
local a: C
)");
LUAU_REQUIRE_NO_ERRORS(result);
// FIXME: We are not properly normalizing this type, but we are at least not improperly discarding information
CHECK("{number & string}" == toString(requireType("a"), {true}));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "union_of_distinct_free_types")
{
ScopedFastFlag flags[] = {
{"LuauLowerBoundsCalculation", true},
};
CheckResult result = check(R"(
function fussy(a, b)
if math.random() > 0.5 then
return a
else
return b
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK("<a, b>(a, b) -> a | b" == toString(requireType("fussy")));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "constrained_intersection_of_intersections")
{
ScopedFastFlag flags[] = {
{"LuauLowerBoundsCalculation", true},
};
CheckResult result = check(R"(
local f : (() -> number) | ((number) -> number)
local g : (() -> number) | ((string) -> number)
function h()
if math.random() then
return f
else
return g
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
TypeId h = requireType("h");
CHECK("() -> (() -> number) | ((number) -> number) | ((string) -> number)" == toString(h));
}
TEST_CASE_FIXTURE(Fixture, "intersection_inside_a_table_inside_another_intersection")
{
ScopedFastFlag flags[] = {
{"LuauLowerBoundsCalculation", true},
};
CheckResult result = check(R"(
type X = {}
type Y = {y: number}
type Z = {z: string}
type W = {w: boolean}
type T = {x: Y & X} & {x:Z & W}
local x: X
local y: Y
local z: Z
local w: W
local t: T
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK("{| |}" == toString(requireType("x"), {true}));
CHECK("{| y: number |}" == toString(requireType("y"), {true}));
CHECK("{| z: string |}" == toString(requireType("z"), {true}));
CHECK("{| w: boolean |}" == toString(requireType("w"), {true}));
CHECK("{| x: {| w: boolean, y: number, z: string |} |}" == toString(requireType("t"), {true}));
}
TEST_CASE_FIXTURE(Fixture, "intersection_inside_a_table_inside_another_intersection_2")
{
ScopedFastFlag flags[] = {
{"LuauLowerBoundsCalculation", true},
};
// We use a function and inferred parameter types to prevent intermediate normalizations from being performed.
// This exposes a bug where the type of y is mutated.
CheckResult result = check(R"(
function strange(w, x, y, z)
y.y = 5
z.z = "five"
w.w = true
type Z = {x: typeof(x) & typeof(y)} & {x: typeof(w) & typeof(z)}
return ((nil :: any) :: Z)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
TypeId t = requireType("strange");
auto ftv = get<FunctionTypeVar>(t);
REQUIRE(ftv != nullptr);
std::vector<TypeId> args = flatten(ftv->argTypes).first;
REQUIRE(4 == args.size());
CHECK("{+ w: boolean +}" == toString(args[0]));
CHECK("a" == toString(args[1]));
CHECK("{+ y: number +}" == toString(args[2]));
CHECK("{+ z: string +}" == toString(args[3]));
std::vector<TypeId> ret = flatten(ftv->retTypes).first;
REQUIRE(1 == ret.size());
CHECK("{| x: a & {+ w: boolean, y: number, z: string +} |}" == toString(ret[0]));
}
TEST_CASE_FIXTURE(Fixture, "intersection_inside_a_table_inside_another_intersection_3")
{
ScopedFastFlag flags[] = {
{"LuauLowerBoundsCalculation", true},
};
// We use a function and inferred parameter types to prevent intermediate normalizations from being performed.
// This exposes a bug where the type of y is mutated.
CheckResult result = check(R"(
function strange(x, y, z)
x.x = true
y.y = y
z.z = "five"
type Z = {x: typeof(y)} & {x: typeof(x) & typeof(z)}
return ((nil :: any) :: Z)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
TypeId t = requireType("strange");
auto ftv = get<FunctionTypeVar>(t);
REQUIRE(ftv != nullptr);
std::vector<TypeId> args = flatten(ftv->argTypes).first;
REQUIRE(3 == args.size());
CHECK("{+ x: boolean +}" == toString(args[0]));
CHECK("t1 where t1 = {+ y: t1 +}" == toString(args[1]));
CHECK("{+ z: string +}" == toString(args[2]));
std::vector<TypeId> ret = flatten(ftv->retTypes).first;
REQUIRE(1 == ret.size());
CHECK("{| x: {+ x: boolean, y: t1, z: string +} |} where t1 = {+ y: t1 +}" == toString(ret[0]));
}
TEST_CASE_FIXTURE(Fixture, "intersection_inside_a_table_inside_another_intersection_4")
{
ScopedFastFlag flags[] = {
{"LuauLowerBoundsCalculation", true},
};
// We use a function and inferred parameter types to prevent intermediate normalizations from being performed.
// This exposes a bug where the type of y is mutated.
CheckResult result = check(R"(
function strange(x, y, z)
x.x = true
z.z = "five"
type R = {x: typeof(y)} & {x: typeof(x) & typeof(z)}
local r: R
y.y = r
return r
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
TypeId t = requireType("strange");
auto ftv = get<FunctionTypeVar>(t);
REQUIRE(ftv != nullptr);
std::vector<TypeId> args = flatten(ftv->argTypes).first;
REQUIRE(3 == args.size());
CHECK("{+ x: boolean +}" == toString(args[0]));
CHECK("{+ y: t1 +} where t1 = {| x: {+ x: boolean, y: t1, z: string +} |}" == toString(args[1]));
CHECK("{+ z: string +}" == toString(args[2]));
std::vector<TypeId> ret = flatten(ftv->retTypes).first;
REQUIRE(1 == ret.size());
CHECK("t1 where t1 = {| x: {+ x: boolean, y: t1, z: string +} |}" == toString(ret[0]));
}
TEST_CASE_FIXTURE(Fixture, "nested_table_normalization_with_non_table__no_ice")
{
ScopedFastFlag flags[] = {
{"LuauLowerBoundsCalculation", true},
{"LuauNormalizeCombineTableFix", true},
};
// CLI-52787
// ends up combining {_:any} with any, recursively
// which used to ICE because this combines a table with a non-table.
CheckResult result = check(R"(
export type t0 = any & { _: {_:any} } & { _:any }
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "visiting_a_type_twice_is_not_considered_normal")
{
ScopedFastFlag sff{"LuauLowerBoundsCalculation", true};
CheckResult result = check(R"(
--!strict
function f(a, b)
local function g()
if math.random() > 0.5 then
return a()
else
return b
end
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("<a>(() -> a, a) -> ()", toString(requireType("f")));
}
TEST_CASE_FIXTURE(Fixture, "fuzz_failure_instersection_combine_must_follow")
{
ScopedFastFlag flags[] = {
{"LuauLowerBoundsCalculation", true},
};
CheckResult result = check(R"(
export type t0 = {_:{_:any} & {_:any|string}} & {_:{_:{}}}
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "fuzz_failure_bound_type_is_normal_but_not_its_bounded_to")
{
ScopedFastFlag sff{"LuauLowerBoundsCalculation", true};
CheckResult result = check(R"(
type t252 = ((t0<t252...>)|(any))|(any)
type t0 = t252<t0<any,t24...>,t24...>
)");
LUAU_REQUIRE_ERRORS(result);
}
// We had an issue where a normal BoundTypeVar might point at a non-normal BoundTypeVar if it in turn pointed to a
// normal TypeVar because we were calling follow() in an improper place.
TEST_CASE_FIXTURE(Fixture, "bound_typevars_should_only_be_marked_normal_if_their_pointee_is_normal")
{
ScopedFastFlag sff[]{
{"LuauLowerBoundsCalculation", true},
};
CheckResult result = check(R"(
local T = {}
function T:M()
local function f(a)
print(self.prop)
self:g(a)
self.prop = a
end
end
return T
)");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "skip_force_normal_on_external_types")
{
createSomeClasses(typeChecker);
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(Fixture, "normalize_unions_containing_never")
{
ScopedFastFlag sff{"LuauLowerBoundsCalculation", true};
CheckResult result = check(R"(
type Foo = string | never
local foo: Foo
)");
CHECK_EQ("string", toString(requireType("foo")));
}
TEST_CASE_FIXTURE(Fixture, "normalize_unions_containing_unknown")
{
ScopedFastFlag sff{"LuauLowerBoundsCalculation", true};
CheckResult result = check(R"(
type Foo = string | unknown
local foo: Foo
)");
CHECK_EQ("unknown", toString(requireType("foo")));
}
TEST_CASE_FIXTURE(Fixture, "any_wins_the_battle_over_unknown_in_unions")
{
ScopedFastFlag sff{"LuauLowerBoundsCalculation", true};
CheckResult result = check(R"(
type Foo = unknown | any
local foo: Foo
type Bar = any | unknown
local bar: Bar
)");
CHECK_EQ("any", toString(requireType("foo")));
CHECK_EQ("any", toString(requireType("bar")));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "normalization_does_not_convert_ever")
{
ScopedFastFlag sff[]{
{"LuauLowerBoundsCalculation", true},
};
CheckResult result = check(R"(
--!strict
local function f()
if math.random() > 0.5 then
return true
end
type Ret = typeof(f())
if math.random() > 0.5 then
return "something"
end
return "something" :: Ret
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("() -> boolean | string", toString(requireType("f")));
}
TEST_SUITE_END();