luau/tests/TypeInfer.unionTypes.test.cpp
vegorov-rbx f5dabc2998
Sync to upstream/release/644 (#1432)
In this update we improve overall stability of the new type solver and
address some type inference issues with it.

If you use the new solver and want to use all new fixes included in this
release, you have to reference an additional Luau flag:
```c++
LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease)
```
And set its value to `644`:
```c++
DFInt::LuauTypeSolverRelease.value = 644; // Or a higher value for future updates
```

## New Solver
* Fixed a debug assertion failure in autocomplete (Fixes #1391)
* Fixed type function distribution issue which transformed `len<>` and
`unm<>` into `not<>` (Fixes #1416)
* Placed a limit on the possible normalized table intersection size as a
temporary measure to avoid hangs and out-of-memory issues for complex
type refinements
* Internal recursion limits are now respected in the subtyping
operations and in autocomplete, to avoid stack overflow crashes
* Fixed false positive errors on assignments to tables whose indexers
are unions of strings
* Fixed memory corruption crashes in subtyping of generic types
containing other generic types in their bounds

---

Internal Contributors:

Co-authored-by: Aaron Weiss <aaronweiss@roblox.com>
Co-authored-by: Andy Friesen <afriesen@roblox.com>
Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
2024-09-20 09:53:26 -07:00

996 lines
27 KiB
C++

// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/TypeInfer.h"
#include "Luau/Type.h"
#include "Fixture.h"
#include "doctest.h"
using namespace Luau;
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauAcceptIndexingTableUnionsIntersections)
TEST_SUITE_BEGIN("UnionTypes");
TEST_CASE_FIXTURE(Fixture, "fuzzer_union_with_one_part_assertion")
{
CheckResult result = check(R"(
local _ = {},nil
repeat
_,_ = if _.number == "" or _.number or _._ then
_
elseif _.__index == _._G then
tostring
elseif _ then
_
else
``,_._G
until _._
)");
}
TEST_CASE_FIXTURE(Fixture, "return_types_can_be_disjoint")
{
// CLI-114134 We need egraphs to consistently reduce the cyclic union
// introduced by the increment here.
ScopedFastFlag sff{FFlag::LuauSolverV2, false};
CheckResult result = check(R"(
local count = 0
function most_of_the_natural_numbers(): number?
if count < 10 then
count = count + 1
return count
else
return nil
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
const FunctionType* utv = get<FunctionType>(requireType("most_of_the_natural_numbers"));
REQUIRE(utv != nullptr);
}
TEST_CASE_FIXTURE(Fixture, "return_types_can_be_disjoint_using_compound_assignment")
{
CheckResult result = check(R"(
local count = 0
function most_of_the_natural_numbers(): number?
if count < 10 then
-- count = count + 1
count += 1
return count
else
return nil
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
const FunctionType* utv = get<FunctionType>(requireType("most_of_the_natural_numbers"));
REQUIRE(utv != nullptr);
}
TEST_CASE_FIXTURE(Fixture, "allow_specific_assign")
{
CheckResult result = check(R"(
local a:number|string = 22
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "allow_more_specific_assign")
{
CheckResult result = check(R"(
function f(a: number | string, b: (number | string)?)
b = a
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "disallow_less_specific_assign")
{
CheckResult result = check(R"(
function f(a: number, b: number | string)
a = b
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(Fixture, "optional_arguments")
{
CheckResult result = check(R"(
function f(a:string, b:string?)
end
f("s")
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "optional_arguments_table")
{
// CLI-115588 - Bidirectional inference does not happen for assignments
ScopedFastFlag sff{FFlag::LuauSolverV2, false};
CheckResult result = check(R"(
local a:{a:string, b:string?}
a = {a="ok"}
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "optional_arguments_table2")
{
CheckResult result = check(R"(
local a:{a:string, b:string}
a = {a=""}
)");
REQUIRE(!result.errors.empty());
}
TEST_CASE_FIXTURE(BuiltinsFixture, "error_takes_optional_arguments")
{
CheckResult result = check(R"(
error("message")
error("message", 2)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "error_optional_argument_enforces_type")
{
CheckResult result = check(R"(
error("message", "2")
)");
REQUIRE(result.errors.size() == 1);
}
TEST_CASE_FIXTURE(Fixture, "index_on_a_union_type_with_property_guaranteed_to_exist")
{
CheckResult result = check(R"(
type A = {x: number}
type B = {x: number}
function f(t: A | B)
return t.x
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("(A | B) -> number", toString(requireType("f")));
}
TEST_CASE_FIXTURE(Fixture, "index_on_a_union_type_with_mixed_types")
{
CheckResult result = check(R"(
type A = {x: number}
type B = {x: string}
function f(t: A | B)
return t.x
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("(A | B) -> number | string", toString(requireType("f")));
}
TEST_CASE_FIXTURE(Fixture, "index_on_a_union_type_works_at_arbitrary_depth")
{
CheckResult result = check(R"(
type A = {x: {y: {z: {thing: number}}}}
type B = {x: {y: {z: {thing: string}}}}
function f(t: A | B)
return t.x.y.z.thing
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("(A | B) -> number | string", toString(requireType("f")));
}
TEST_CASE_FIXTURE(Fixture, "index_on_a_union_type_with_one_optional_property")
{
CheckResult result = check(R"(
type A = {x: number}
type B = {x: number?}
function f(t: A | B)
return t.x
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("(A | B) -> number?", toString(requireType("f")));
}
TEST_CASE_FIXTURE(Fixture, "index_on_a_union_type_with_missing_property")
{
CheckResult result = check(R"(
type A = {x: number}
type B = {}
function f(t: A | B)
return t.x
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
MissingUnionProperty* mup = get<MissingUnionProperty>(result.errors[0]);
REQUIRE(mup);
CHECK_EQ("Key 'x' is missing from 'B' in the type 'A | B'", toString(result.errors[0]));
if (FFlag::LuauSolverV2)
CHECK_EQ("(A | B) -> number", toString(requireType("f")));
else
CHECK_EQ("(A | B) -> *error-type*", toString(requireType("f")));
}
TEST_CASE_FIXTURE(Fixture, "index_on_a_union_type_with_one_property_of_type_any")
{
CheckResult result = check(R"(
type A = {x: number}
type B = {x: any}
function f(t: A | B)
return t.x
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("(A | B) -> any", toString(requireType("f")));
}
TEST_CASE_FIXTURE(Fixture, "union_equality_comparisons")
{
CheckResult result = check(R"(
type A = number | string | nil
type B = number | nil
type C = number | boolean
function f(a: A, b: B, c: C)
local n = 1
local x = a == b
local y = a == n
local z = a == c
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "optional_union_members")
{
CheckResult result = check(R"(
local a = { a = { x = 1, y = 2 }, b = 3 }
type A = typeof(a)
function f(b: A?)
return b.a.y
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Value of type 'A?' could be nil", toString(result.errors[0]));
CHECK_EQ("(A?) -> number", toString(requireType("f")));
}
TEST_CASE_FIXTURE(Fixture, "optional_union_functions")
{
CheckResult result = check(R"(
local a = {}
function a.foo(x:number, y:number) return x + y end
type A = typeof(a)
function f(b: A?)
return b.foo(1, 2)
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Value of type 'A?' could be nil", toString(result.errors[0]));
CHECK_EQ("(A?) -> number", toString(requireType("f")));
}
TEST_CASE_FIXTURE(Fixture, "optional_union_methods")
{
CheckResult result = check(R"(
local a = {}
function a:foo(x:number, y:number) return x + y end
type A = typeof(a)
function f(b: A?)
return b:foo(1, 2)
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Value of type 'A?' could be nil", toString(result.errors[0]));
CHECK_EQ("(A?) -> number", toString(requireType("f")));
}
TEST_CASE_FIXTURE(Fixture, "optional_union_follow")
{
CheckResult result = check(R"(
local y: number? = 2
local x = y
function f(a: number, b: number?, c: number?) return -a end
return f()
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
auto acm = get<CountMismatch>(result.errors[0]);
REQUIRE(acm);
CHECK_EQ(1, acm->expected);
CHECK_EQ(0, acm->actual);
CHECK_FALSE(acm->isVariadic);
}
TEST_CASE_FIXTURE(Fixture, "optional_field_access_error")
{
CheckResult result = check(R"(
type A = { x: number }
function f(b: A?)
local c = b.x
local d = b.y
end
)");
LUAU_REQUIRE_ERROR_COUNT(3, result);
CHECK_EQ("Value of type 'A?' could be nil", toString(result.errors[0]));
CHECK_EQ("Value of type 'A?' could be nil", toString(result.errors[1]));
CHECK_EQ("Key 'y' not found in table 'A'", toString(result.errors[2]));
}
TEST_CASE_FIXTURE(Fixture, "optional_index_error")
{
CheckResult result = check(R"(
type A = {number}
function f(a: A?)
local b = a[1]
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Value of type 'A?' could be nil", toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "optional_call_error")
{
CheckResult result = check(R"(
type A = (number) -> number
function f(a: A?)
local b = a(4)
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Value of type '((number) -> number)?' could be nil", toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "optional_assignment_errors")
{
CheckResult result = check(R"(
type A = { x: number }
function f(a: A?)
a.x = 2
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Value of type 'A?' could be nil", toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "optional_assignment_errors_2")
{
CheckResult result = check(R"(
type A = { x: number } & { y: number }
function f(a: A?)
a.x = 2
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
auto s = toString(result.errors[0]);
if (FFlag::LuauSolverV2)
CHECK_EQ("Value of type '({ x: number } & { y: number })?' could be nil", s);
else
CHECK_EQ("Value of type '({| x: number |} & {| y: number |})?' could be nil", s);
}
TEST_CASE_FIXTURE(Fixture, "optional_length_error")
{
ScopedFastFlag _{FFlag::LuauSolverV2, true};
CheckResult result = check(R"(
type A = {number}
function f(a: A?)
local b = #a
end
)");
// CLI-119936: This shouldn't double error but does under the new solver.
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ("Operator '#' could not be applied to operand of type A?; there is no corresponding overload for __len", toString(result.errors[0]));
CHECK_EQ("Value of type 'A?' could be nil", toString(result.errors[1]));
}
TEST_CASE_FIXTURE(Fixture, "optional_missing_key_error_details")
{
CheckResult result = check(R"(
type A = { x: number, y: number }
type B = { x: number, y: number }
type C = { x: number }
type D = { x: number }
function f(a: A | B | C | D)
local y = a.y
local z = a.z
end
function g(c: A | B | C | D | nil)
local d = c.y
end
)");
LUAU_REQUIRE_ERROR_COUNT(4, result);
CHECK_EQ("Key 'y' is missing from 'C', 'D' in the type 'A | B | C | D'", toString(result.errors[0]));
CHECK_EQ("Type 'A | B | C | D' does not have key 'z'", toString(result.errors[1]));
CHECK_EQ("Value of type '(A | B | C | D)?' could be nil", toString(result.errors[2]));
CHECK_EQ("Key 'y' is missing from 'C', 'D' in the type 'A | B | C | D'", toString(result.errors[3]));
}
TEST_CASE_FIXTURE(Fixture, "optional_iteration")
{
CheckResult result = check(R"(
function foo(values: {number}?)
local s = 0
for _, value in values do
s += value
end
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Value of type '{number}?' could be nil", toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "unify_unsealed_table_union_check")
{
ScopedFastFlag sff{FFlag::LuauSolverV2, false};
CheckResult result = check(R"(
local x = { x = 3 }
type A = number?
type B = string?
local y: { x: number, y: A | B }
y = x
)");
LUAU_REQUIRE_NO_ERRORS(result);
result = check(R"(
local x = { x = 3 }
local a: number? = 2
local y = {}
y.x = 2
y.y = a
y = x
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "unify_sealed_table_union_check")
{
CheckResult result = check(R"(
-- the difference between this and unify_unsealed_table_union_check is the type annotation on x
local t = { x = 3, y = true }
local x: { x: number } = t
type A = number?
type B = string?
local y: { x: number, y: A | B }
-- Shouldn't typecheck!
y = x
-- If it does, we can convert any type to any other type
y.y = 5
local oh : boolean = t.y
)");
LUAU_REQUIRE_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "error_detailed_union_part")
{
CheckResult result = check(R"(
type X = { x: number }
type Y = { y: number }
type Z = { z: number }
type XYZ = X | Y | Z
function f(a: XYZ)
local b: { w: number } = a
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::LuauSolverV2)
{
CHECK_EQ(
toString(result.errors[0]),
"Type 'X | Y | Z' could not be converted into '{ w: number }'; type X | Y | Z[0] (X) is not a subtype "
"of { w: number } ({ w: number })\n\t"
"type X | Y | Z[1] (Y) is not a subtype of { w: number } ({ w: number })\n\t"
"type X | Y | Z[2] (Z) is not a subtype of { w: number } ({ w: number })"
);
}
else
{
CHECK_EQ(toString(result.errors[0]), R"(Type 'X | Y | Z' could not be converted into '{| w: number |}'
caused by:
Not all union options are compatible.
Table type 'X' not compatible with type '{| w: number |}' because the former is missing field 'w')");
}
}
TEST_CASE_FIXTURE(Fixture, "error_detailed_union_all")
{
CheckResult result = check(R"(
type X = { x: number }
type Y = { y: number }
type Z = { z: number }
type XYZ = X | Y | Z
local a: XYZ = { w = 4 }
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::LuauSolverV2)
CHECK(toString(result.errors[0]) == "Type '{ w: number }' could not be converted into 'X | Y | Z'");
else
CHECK_EQ(toString(result.errors[0]), R"(Type 'a' could not be converted into 'X | Y | Z'; none of the union options are compatible)");
}
TEST_CASE_FIXTURE(Fixture, "error_detailed_optional")
{
CheckResult result = check(R"(
type X = { x: number }
local a: X? = { w = 4 }
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::LuauSolverV2)
CHECK("Type '{ w: number }' could not be converted into 'X?'" == toString(result.errors[0]));
else
{
const std::string expected = R"(Type 'a' could not be converted into 'X?'
caused by:
None of the union options are compatible. For example:
Table type 'a' not compatible with type 'X' because the former is missing field 'x')";
CHECK_EQ(expected, toString(result.errors[0]));
}
}
// We had a bug where a cyclic union caused a stack overflow.
// ex type U = number | U
TEST_CASE_FIXTURE(Fixture, "dont_allow_cyclic_unions_to_be_inferred")
{
CheckResult result = check(R"(
--!strict
function f(a, b)
a:g(b or {})
a:g(b)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "indexing_into_a_cyclic_union_doesnt_crash")
{
// It shouldn't be possible to craft a cyclic union, but even if we do, we
// shouldn't blow up.
TypeArena& arena = frontend.globals.globalTypes;
unfreeze(arena);
TypeId badCyclicUnionTy = arena.freshType(frontend.globals.globalScope.get());
UnionType u;
u.options.push_back(badCyclicUnionTy);
u.options.push_back(arena.addType(TableType{
{}, TableIndexer{builtinTypes->numberType, builtinTypes->numberType}, TypeLevel{}, frontend.globals.globalScope.get(), TableState::Sealed
}));
asMutable(badCyclicUnionTy)->ty.emplace<UnionType>(std::move(u));
frontend.globals.globalScope->exportedTypeBindings["BadCyclicUnion"] = TypeFun{{}, badCyclicUnionTy};
freeze(arena);
CheckResult result = check(R"(
function f(x: BadCyclicUnion)
return x[0]
end
)");
// this is a cyclic union of number arrays, so it _is_ a table, even if it's a nonsense type.
// no need to generate a NotATable error here. The new solver automatically handles this and
// correctly reports no errors.
if (FFlag::LuauAcceptIndexingTableUnionsIntersections || FFlag::LuauSolverV2)
LUAU_REQUIRE_NO_ERRORS(result);
else
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "table_union_write_indirect")
{
ScopedFastFlag sff{FFlag::LuauSolverV2, false};
CheckResult result = check(R"(
type A = { x: number, y: (number) -> string } | { z: number, y: (number) -> string }
function f(a: A)
function a.y(x)
return tostring(x * 2)
end
function a.y(x: string): number
return tonumber(x) or 0
end
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
// NOTE: union normalization will improve this message
const std::string expected = R"(Type
'(string) -> number'
could not be converted into
'((number) -> string) | ((number) -> string)'; none of the union options are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "union_true_and_false")
{
CheckResult result = check(R"(
function f(x : boolean)
local y1 : (true | false) = x -- OK
local y2 : (true | false | (string & number)) = x -- OK
local y3 : (true | (string & number) | false) = x -- OK
local y4 : (true | (boolean & true) | false) = x -- OK
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "union_of_functions")
{
CheckResult result = check(R"(
function f(x : (number) -> number?)
local y : ((number?) -> number?) | ((number) -> number) = x -- OK
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "union_of_generic_functions")
{
CheckResult result = check(R"(
function f(x : <a>(a) -> a?)
local y : (<a>(a?) -> a?) | (<b>(b) -> b) = x -- Not OK
end
)");
// TODO: should this example typecheck?
LUAU_REQUIRE_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "union_of_generic_typepack_functions")
{
CheckResult result = check(R"(
function f(x : <a...>(number, a...) -> (number?, a...))
local y : (<a...>(number?, a...) -> (number?, a...)) | (<b...>(number, b...) -> (number, b...)) = x -- Not OK
end
)");
// TODO: should this example typecheck?
LUAU_REQUIRE_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "union_of_functions_mentioning_generics")
{
ScopedFastFlag sff{FFlag::LuauSolverV2, false};
CheckResult result = check(R"(
function f<a,b>()
function g(x : (a) -> a?)
local y : ((a?) -> nil) | ((a) -> a) = x -- OK
local z : ((b?) -> nil) | ((b) -> b) = x -- Not OK
end
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(
toString(result.errors[0]),
"Type '(a) -> a?' could not be converted into '((b) -> b) | ((b?) -> nil)'; none of the union options are compatible"
);
}
TEST_CASE_FIXTURE(Fixture, "union_of_functions_mentioning_generic_typepacks")
{
ScopedFastFlag sff{FFlag::LuauSolverV2, false};
CheckResult result = check(R"(
function f<a...>()
function g(x : (number, a...) -> (number?, a...))
local y : ((number | string, a...) -> (number, a...)) | ((number?, a...) -> (nil, a...)) = x -- OK
local z : ((number) -> number) | ((number?, a...) -> (number?, a...)) = x -- Not OK
end
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
'(number, a...) -> (number?, a...)'
could not be converted into
'((number) -> number) | ((number?, a...) -> (number?, a...))'; none of the union options are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_arg_arities")
{
ScopedFastFlag sff{FFlag::LuauSolverV2, false};
CheckResult result = check(R"(
function f(x : (number) -> number?)
local y : ((number?) -> number) | ((number | string) -> nil) = x -- OK
local z : ((number, string?) -> number) | ((number) -> nil) = x -- Not OK
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
'(number) -> number?'
could not be converted into
'((number) -> nil) | ((number, string?) -> number)'; none of the union options are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_result_arities")
{
ScopedFastFlag sff{FFlag::LuauSolverV2, false};
CheckResult result = check(R"(
function f(x : () -> (number | string))
local y : (() -> number) | (() -> string) = x -- OK
local z : (() -> number) | (() -> (string, string)) = x -- Not OK
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
'() -> number | string'
could not be converted into
'(() -> (string, string)) | (() -> number)'; none of the union options are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_variadics")
{
ScopedFastFlag sff{FFlag::LuauSolverV2, false};
CheckResult result = check(R"(
function f(x : (...nil) -> (...number?))
local y : ((...string?) -> (...number)) | ((...number?) -> nil) = x -- OK
local z : ((...string?) -> (...number)) | ((...string?) -> nil) = x -- OK
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
'(...nil) -> (...number?)'
could not be converted into
'((...string?) -> (...number)) | ((...string?) -> nil)'; none of the union options are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_arg_variadics")
{
CheckResult result = check(R"(
function f(x : (number) -> ())
local y : ((number?) -> ()) | ((...number) -> ()) = x -- OK
local z : ((number?) -> ()) | ((...number?) -> ()) = x -- Not OK
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::LuauSolverV2)
{
CHECK(R"(Type
'(number) -> ()'
could not be converted into
'((...number?) -> ()) | ((number?) -> ())')" == toString(result.errors[0]));
}
else
{
const std::string expected = R"(Type
'(number) -> ()'
could not be converted into
'((...number?) -> ()) | ((number?) -> ())'; none of the union options are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
}
}
TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_result_variadics")
{
ScopedFastFlag sff{FFlag::LuauSolverV2, false};
CheckResult result = check(R"(
function f(x : () -> (number?, ...number))
local y : (() -> (...number)) | (() -> nil) = x -- OK
local z : (() -> (...number)) | (() -> number) = x -- OK
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
'() -> (number?, ...number)'
could not be converted into
'(() -> (...number)) | (() -> number)'; none of the union options are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "less_greedy_unification_with_union_types")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
local function f(t): { x: number } | { x: string }
local x = t.x
return t
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("(({ read x: unknown } & { x: number }) | ({ read x: unknown } & { x: string })) -> { x: number } | { x: string }", toString(requireType("f")));
}
TEST_CASE_FIXTURE(Fixture, "less_greedy_unification_with_union_types_2")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
local function f(t: { x: number } | { x: string })
return t.x
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("({ x: number } | { x: string }) -> number | string", toString(requireType("f")));
}
TEST_CASE_FIXTURE(Fixture, "union_table_any_property")
{
CheckResult result = check(R"(
function f(x)
-- x : X
-- sup : { p : { q : X } }?
local sup = if true then { p = { q = x } } else nil
local sub : { p : any }
sup = nil
sup = sub
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "union_function_any_args")
{
ScopedFastFlag sff{FFlag::LuauSolverV2, false};
CheckResult result = check(R"(
function f(sup : ((...any) -> (...any))?, sub : ((number) -> (...any)))
sup = sub
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "optional_any")
{
CheckResult result = check(R"(
function f(sup : any?, sub : number)
sup = sub
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "generic_function_with_optional_arg")
{
ScopedFastFlag sff{FFlag::LuauSolverV2, false};
CheckResult result = check(R"(
function f<T>(x : T?) : {T}
local result = {}
if x then
result[1] = x
end
return result
end
local t : {string} = f(nil)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "lookup_prop_of_intersection_containing_unions")
{
CheckResult result = check(R"(
local function mergeOptions<T>(options: T & ({} | {}))
return options.variables
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
const UnknownProperty* unknownProp = get<UnknownProperty>(result.errors[0]);
REQUIRE(unknownProp);
CHECK("variables" == unknownProp->key);
}
TEST_CASE_FIXTURE(Fixture, "suppress_errors_for_prop_lookup_of_a_union_that_includes_error")
{
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
registerHiddenTypes(&frontend);
CheckResult result = check(R"(
function f(a: err | Not<nil>)
local b = a.foo
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END();