#include "Luau/AstQuery.h"
#include "Luau/BuiltinDefinitions.h"
#include "Luau/Error.h"
#include "Luau/Scope.h"
#include "Luau/TypeInfer.h"
#include "Luau/Type.h"
#include "ClassFixture.h"
#include "Fixture.h"
#include "ScopedFlags.h"
#include "doctest.h"
using namespace Luau;
LUAU_FASTFLAG(DebugLuauAssertOnForcedConstraint)
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAG(DebugLuauForceOldSolver)
LUAU_FASTINT(LuauTarjanChildLimit)
LUAU_FASTFLAG(LuauFormatUseLastPosition)
LUAU_FASTFLAG(LuauCheckFunctionStatementTypes)
LUAU_FASTFLAG(LuauCaptureRecursiveCallsForTablesAndGlobals2)
LUAU_FASTFLAG(LuauRelateHandlesCoincidentTables)
LUAU_FASTFLAG(LuauOverloadGetsInstantiated2)
LUAU_FASTFLAG(LuauReplacerRespectsReboundGenerics)
LUAU_FASTFLAG(LuauCaptureRecursiveCallsForTablesAndGlobals2)
LUAU_FASTFLAG(LuauForwardPolarityForFunctionTypes)
LUAU_FASTFLAG(LuauReplacerRespectsReboundGenerics)
LUAU_FASTFLAG(LuauKeepExplicitMapForGlobalTypes2)
LUAU_FASTFLAG(LuauBidirectionalInferenceBetterUnionHandling)
LUAU_FASTFLAG(LuauExplicitTypeInstantiationSupport)
TEST_SUITE_BEGIN("TypeInferFunctions");
TEST_CASE_FIXTURE(Fixture, "general_case_table_literal_blocks")
{
CheckResult result = check(R"(
--!strict
function f(x : {[any]: number})
return x
end
local Foo = {bar = "$$$"}
f({[Foo.bar] = 0})
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "overload_resolution")
{
CheckResult result = check(R"(
type A = (number) -> string
type B = (string) -> number
local function foo(f: A & B)
return f(1), f("five")
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
TypeId t = requireType("foo");
const FunctionType* fooType = get<FunctionType>(requireType("foo"));
REQUIRE(fooType != nullptr);
CHECK(toString(t) == "(((number) -> string) & ((string) -> number)) -> (string, number)");
}
TEST_CASE_FIXTURE(Fixture, "tc_function")
{
CheckResult result = check("function five() return 5 end");
LUAU_REQUIRE_NO_ERRORS(result);
const FunctionType* fiveType = get<FunctionType>(requireType("five"));
REQUIRE(fiveType != nullptr);
}
TEST_CASE_FIXTURE(Fixture, "check_function_bodies")
{
CheckResult result = check(R"(
function myFunction(): number
local a = 0
a = true
return a
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (!FFlag::DebugLuauForceOldSolver)
{
const TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE_MESSAGE(tm, "Expected TypeMismatch but got " << result.errors[0]);
CHECK(toString(tm->wantedType) == "number");
CHECK(toString(tm->givenType) == "boolean");
}
else
{
CHECK_EQ(
result.errors[0],
(TypeError{
Location{Position{3, 16}, Position{3, 20}},
TypeMismatch{
getBuiltins()->numberType,
getBuiltins()->booleanType,
}
})
);
}
}
TEST_CASE_FIXTURE(Fixture, "cannot_hoist_interior_defns_into_signature")
{
CheckResult result = check(R"(
local function f(x: T)
type T = number
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(
result.errors[0] == TypeError{
Location{{1, 28}, {1, 29}},
getMainSourceModule()->name,
UnknownSymbol{
"T",
UnknownSymbol::Context::Type,
}
}
);
}
TEST_CASE_FIXTURE(Fixture, "infer_return_type")
{
CheckResult result = check("function take_five() return 5 end");
LUAU_REQUIRE_NO_ERRORS(result);
const FunctionType* takeFiveType = get<FunctionType>(requireType("take_five"));
REQUIRE(takeFiveType != nullptr);
std::vector<TypeId> retVec = flatten(takeFiveType->retTypes).first;
REQUIRE(!retVec.empty());
CHECK("number" == toString(retVec[0]));
}
TEST_CASE_FIXTURE(Fixture, "infer_from_function_return_type")
{
CheckResult result = check("function take_five() return 5 end local five = take_five()");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK("number" == toString(requireType("five")));
}
TEST_CASE_FIXTURE(Fixture, "infer_that_function_does_not_return_a_table")
{
CheckResult result = check(R"(
function take_five()
return 5
end
take_five().prop = 888
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(result.errors[0], (TypeError{Location{Position{5, 8}, Position{5, 24}}, NotATable{getBuiltins()->numberType}}));
}
TEST_CASE_FIXTURE(Fixture, "generalize_table_property")
{
CheckResult result = check(R"(
local T = {}
T.foo = function(x)
return x
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
TypeId t = requireType("T");
const TableType* tt = get<TableType>(follow(t));
REQUIRE(tt);
const Property& foo = tt->props.at("foo");
REQUIRE(foo.readTy);
TypeId fooTy = *foo.readTy;
CHECK("<a>(a) -> a" == toString(fooTy));
}
TEST_CASE_FIXTURE(Fixture, "vararg_functions_should_allow_calls_of_any_types_and_size")
{
CheckResult result = check(R"(
function f(...) end
f(1)
f("foo", 2)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "vararg_function_is_quantified")
{
CheckResult result = check(R"(
local T = {}
function T.f(...)
local result = {}
for i = 1, select("#", ...) do
local dictionary = select(i, ...)
for key, value in pairs(dictionary) do
result[key] = value
end
end
return result
end
return T
)");
LUAU_REQUIRE_NO_ERRORS(result);
auto r = first(getMainModule()->returnType);
REQUIRE(r);
TableType* ttv = getMutable<TableType>(*r);
REQUIRE(ttv);
REQUIRE(ttv->props.count("f"));
const Property& f = ttv->props["f"];
REQUIRE(f.readTy);
TypeId k = *f.readTy;
REQUIRE(k);
}
TEST_CASE_FIXTURE(Fixture, "list_only_alternative_overloads_that_match_argument_count")
{
CheckResult result = check(R"(
local multiply: ((number)->number) & ((number)->string) & ((number, number)->number)
multiply("")
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
if (!FFlag::DebugLuauForceOldSolver)
{
MultipleNonviableOverloads* mno = get<MultipleNonviableOverloads>(result.errors[0]);
REQUIRE_MESSAGE(mno, "Expected MultipleNonviableOverloads but got " << result.errors[0]);
CHECK_EQ(mno->attemptedArgCount, 1);
}
else
{
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm);
CHECK_EQ(getBuiltins()->numberType, tm->wantedType);
CHECK_EQ(getBuiltins()->stringType, tm->givenType);
}
ExtraInformation* ei = get<ExtraInformation>(result.errors[1]);
REQUIRE(ei);
if (!FFlag::DebugLuauForceOldSolver)
{
CHECK("Available overloads: (number) -> number; and (number) -> string" == ei->message);
}
else
CHECK_EQ("Other overloads are also not viable: (number) -> string", ei->message);
}
TEST_CASE_FIXTURE(Fixture, "list_all_overloads_if_no_overload_takes_given_argument_count")
{
CheckResult result = check(R"(
local multiply: ((number)->number) & ((number)->string) & ((number, number)->number)
multiply()
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
GenericError* ge = get<GenericError>(result.errors[0]);
REQUIRE(ge);
CHECK_EQ("No overload for function accepts 0 arguments.", ge->message);
ExtraInformation* ei = get<ExtraInformation>(result.errors[1]);
REQUIRE(ei);
CHECK_EQ("Available overloads: (number) -> number; (number) -> string; and (number, number) -> number", ei->message);
}
TEST_CASE_FIXTURE(Fixture, "dont_give_other_overloads_message_if_only_one_argument_matching_overload_exists")
{
CheckResult result = check(R"(
local multiply: ((number)->number) & ((number)->string) & ((number, number)->number)
multiply(1, "")
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm);
CHECK_EQ(getBuiltins()->numberType, tm->wantedType);
CHECK_EQ(getBuiltins()->stringType, tm->givenType);
}
TEST_CASE_FIXTURE(Fixture, "infer_return_type_from_selected_overload")
{
CheckResult result = check(R"(
type T = {method: ((T, number) -> number) & ((number) -> string)}
local T: T
local a = T.method(T, 4)
local b = T.method(5)
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("number", toString(requireType("a")));
CHECK_EQ("string", toString(requireType("b")));
}
TEST_CASE_FIXTURE(Fixture, "too_many_arguments")
{
DOES_NOT_PASS_NEW_SOLVER_GUARD();
CheckResult result = check(R"(
--!nonstrict
function g(a: number) end
g()
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
auto err = result.errors[0];
auto acm = get<CountMismatch>(err);
REQUIRE(acm);
CHECK_EQ(1, acm->expected);
CHECK_EQ(0, acm->actual);
}
TEST_CASE_FIXTURE(Fixture, "too_many_arguments_error_location")
{
CheckResult result = check(R"(
--!strict
function myfunction(a: number, b:number) end
myfunction(1)
function getmyfunction()
return myfunction
end
getmyfunction()()
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
{
TypeError err = result.errors[0];
CHECK_EQ(err.location, Location(Position(4, 8), Position(4, 18)));
auto acm = get<CountMismatch>(err);
REQUIRE(acm);
CHECK_EQ(2, acm->expected);
CHECK_EQ(1, acm->actual);
}
{
TypeError err = result.errors[1];
CHECK_EQ(err.location, Location(Position(9, 8), Position(9, 23)));
auto acm = get<CountMismatch>(err);
REQUIRE(acm);
CHECK_EQ(2, acm->expected);
CHECK_EQ(0, acm->actual);
}
}
TEST_CASE_FIXTURE(Fixture, "recursive_function")
{
CheckResult result = check(R"(
function count(n: number)
if n == 0 then
return 0
else
return count(n - 1)
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "lambda_form_of_local_function_cannot_be_recursive")
{
CheckResult result = check(R"(
local f = function() return f() end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(Fixture, "recursive_local_function")
{
CheckResult result = check(R"(
local function count(n: number)
if n == 0 then
return 0
else
return count(n - 1)
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "another_recursive_local_function")
{
CheckResult result = check(R"(
local count
function count(n: number)
if n == 0 then
return 0
else
return count(n - 1)
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "recursive_calls_must_refer_to_the_ungeneralized_type")
{
CheckResult result = check(R"(
function foo()
string.format('%s: %s', "51", foo())
end
)");
}
TEST_CASE_FIXTURE(Fixture, "cyclic_function_type_in_rets")
{
CheckResult result = check(R"(
function f()
return f
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("t1 where t1 = () -> t1", toString(requireType("f")));
}
TEST_CASE_FIXTURE(Fixture, "another_higher_order_function")
{
CheckResult result = check(R"(
local Get_des
function Get_des(func)
Get_des(func)
end
local function f(d)
d:IsA("BasePart")
d.Parent:FindFirstChild("Humanoid")
d:IsA("Decal")
end
Get_des(f)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "another_other_higher_order_function")
{
if (!FFlag::DebugLuauForceOldSolver)
{
CheckResult result = check(R"(
local function f(d)
d:foo()
d:foo()
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
else
{
CheckResult result = check(R"(
local d
d:foo()
d:foo()
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
}
TEST_CASE_FIXTURE(Fixture, "local_function")
{
CheckResult result = check(R"(
function f()
return 8
end
function g()
local function f()
return 'hello'
end
return f
end
local h = g()
)");
LUAU_REQUIRE_NO_ERRORS(result);
TypeId h = follow(requireType("h"));
const FunctionType* ftv = get<FunctionType>(h);
REQUIRE(ftv != nullptr);
std::optional<TypeId> rt = first(ftv->retTypes);
REQUIRE(bool(rt));
TypeId retType = follow(*rt);
CHECK_EQ(PrimitiveType::String, getPrimitiveType(retType));
}
TEST_CASE_FIXTURE(Fixture, "func_expr_doesnt_leak_free")
{
CheckResult result = check(R"(
local p = function(x) return x end
)");
LUAU_REQUIRE_NO_ERRORS(result);
const Luau::FunctionType* fn = get<FunctionType>(requireType("p"));
REQUIRE(fn);
auto ret = first(fn->retTypes);
REQUIRE(ret);
REQUIRE(get<GenericType>(follow(*ret)));
}
TEST_CASE_FIXTURE(Fixture, "first_argument_can_be_optional")
{
CheckResult result = check(R"(
local T = {}
function T.new(a: number?, b: number?, c: number?) return 5 end
local m = T.new()
)");
LUAU_REQUIRE_NO_ERRORS(result);
dumpErrors(result);
}
TEST_CASE_FIXTURE(Fixture, "it_is_ok_not_to_supply_enough_retvals")
{
CheckResult result = check(R"(
function get_two() return 5, 6 end
local a = get_two()
)");
LUAU_REQUIRE_NO_ERRORS(result);
dumpErrors(result);
}
TEST_CASE_FIXTURE(Fixture, "duplicate_functions2")
{
CheckResult result = check(R"(
function foo() end
function bar()
local function foo() end
end
)");
LUAU_REQUIRE_ERROR_COUNT(0, result);
}
TEST_CASE_FIXTURE(Fixture, "duplicate_functions_allowed_in_nonstrict")
{
CheckResult result = check(R"(
--!nonstrict
function foo() end
function foo() end
function bar()
local function foo() end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "duplicate_functions_with_different_signatures_not_allowed_in_nonstrict")
{
DOES_NOT_PASS_NEW_SOLVER_GUARD();
CheckResult result = check(R"(
--!nonstrict
function foo(): number
return 1
end
foo()
function foo(n: number): number
return 2
end
foo()
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm);
CHECK_EQ("() -> number", toString(tm->wantedType));
CHECK_EQ("(number) -> number", toString(tm->givenType));
}
TEST_CASE_FIXTURE(Fixture, "complicated_return_types_require_an_explicit_annotation")
{
CheckResult result = check(R"(
local i = 0
function most_of_the_natural_numbers(): number?
if i < 10 then
i += 1
return i
else
return nil
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
TypeId ty = requireType("most_of_the_natural_numbers");
const FunctionType* functionType = get<FunctionType>(ty);
REQUIRE_MESSAGE(functionType, "Expected function but got " << toString(ty));
std::optional<TypeId> retType = first(functionType->retTypes);
REQUIRE(retType);
CHECK(get<UnionType>(*retType));
}
TEST_CASE_FIXTURE(Fixture, "infer_higher_order_function")
{
CheckResult result = check(R"(
function apply(f, x)
return f(x)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
const FunctionType* ftv = get<FunctionType>(requireType("apply"));
REQUIRE(ftv != nullptr);
std::vector<TypeId> argVec = flatten(ftv->argTypes).first;
REQUIRE_EQ(2, argVec.size());
const FunctionType* fType = get<FunctionType>(follow(argVec[0]));
REQUIRE_MESSAGE(fType != nullptr, "Expected a function but got " << toString(argVec[0]));
std::vector<TypeId> fArgs = flatten(fType->argTypes).first;
TypeId xType = follow(argVec[1]);
CHECK_EQ(1, fArgs.size());
CHECK_EQ(xType, follow(fArgs[0]));
}
TEST_CASE_FIXTURE(Fixture, "higher_order_function_2")
{
DOES_NOT_PASS_NEW_SOLVER_GUARD();
CheckResult result = check(R"(
function bottomupmerge(comp, a, b, left, mid, right)
local i, j = left, mid
for k = left, right do
if i < mid and (j > right or not comp(a[j], a[i])) then
b[k] = a[i]
i = i + 1
else
b[k] = a[j]
j = j + 1
end
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
const FunctionType* ftv = get<FunctionType>(requireType("bottomupmerge"));
REQUIRE(ftv != nullptr);
std::vector<TypeId> argVec = flatten(ftv->argTypes).first;
REQUIRE_EQ(6, argVec.size());
const FunctionType* fType = get<FunctionType>(follow(argVec[0]));
REQUIRE(fType != nullptr);
}
TEST_CASE_FIXTURE(Fixture, "higher_order_function_3")
{
ScopedFastFlag sffs[] = {
{FFlag::DebugLuauForceOldSolver, false}, {FFlag::LuauReplacerRespectsReboundGenerics, true}, {FFlag::LuauOverloadGetsInstantiated2, true}
};
CheckResult result = check(R"(
function swap(p)
local t = p[0]
p[0] = p[1]
p[1] = t
return nil
end
function swapTwice(p)
swap(p)
swap(p)
return p
end
function swapTwiceOn(t: { number })
swapTwice(t)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("<a, b>({a} & {b}) -> {a} & {b}", toString(requireType("swapTwice")));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "higher_order_function_4")
{
DOES_NOT_PASS_NEW_SOLVER_GUARD();
CheckResult result = check(R"(
function bottomupmerge(comp, a, b, left, mid, right)
local i, j = left, mid
for k = left, right do
if i < mid and (j > right or not comp(a[j], a[i])) then
b[k] = a[i]
i = i + 1
else
b[k] = a[j]
j = j + 1
end
end
end
function mergesort<T>(arr: {T}, comp: (T, T) -> boolean)
local work = {}
for i = 1, #arr do
work[i] = arr[i]
end
local width = 1
while width < #arr do
for i = 1, #arr, 2*width do
bottomupmerge(comp, arr, work, i, math.min(i+width, #arr), math.min(i+2*width-1, #arr))
end
local temp = work
work = arr
arr = temp
width = width * 2
end
return arr
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
const FunctionType* ftv = get<FunctionType>(requireType("mergesort"));
REQUIRE(ftv != nullptr);
std::vector<TypeId> argVec = flatten(ftv->argTypes).first;
REQUIRE_EQ(2, argVec.size());
const TableType* arg0 = get<TableType>(follow(argVec[0]));
REQUIRE(arg0 != nullptr);
REQUIRE(bool(arg0->indexer));
const FunctionType* arg1 = get<FunctionType>(follow(argVec[1]));
REQUIRE(arg1 != nullptr);
REQUIRE_EQ(2, size(arg1->argTypes));
std::vector<TypeId> arg1Args = flatten(arg1->argTypes).first;
CHECK(follow(arg0->indexer->indexResultType) == follow(arg1Args[0]));
CHECK(follow(arg0->indexer->indexResultType) == follow(arg1Args[1]));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "mutual_recursion")
{
CheckResult result = check(R"(
--!strict
function newPlayerCharacter()
startGui() -- Unknown symbol 'startGui'
end
local characterAddedConnection: any
function startGui()
characterAddedConnection = game:GetService("Players").LocalPlayer.CharacterAdded:connect(newPlayerCharacter)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "toposort_doesnt_break_mutual_recursion")
{
CheckResult result = check(R"(
--!strict
local x = nil
function f() g() end
-- make sure print(x) doesn't get toposorted here, breaking the mutual block
function g() x = f end
print(x)
)");
LUAU_REQUIRE_NO_ERRORS(result);
dumpErrors(result);
}
TEST_CASE_FIXTURE(Fixture, "check_function_before_lambda_that_uses_it")
{
CheckResult result = check(R"(
--!nonstrict
function f()
return 114
end
return function()
return f():andThen()
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "it_is_ok_to_oversaturate_a_higher_order_function_argument")
{
CheckResult result = check(R"(
function onerror() end
function foo() end
xpcall(foo, onerror)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "another_indirect_function_case_where_it_is_ok_to_provide_too_many_arguments")
{
CheckResult result = check(R"(
local mycb: (number, number) -> ()
function f() end
mycb = f
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "report_exiting_without_return_nonstrict")
{
DOES_NOT_PASS_NEW_SOLVER_GUARD();
CheckResult result = check(R"(
--!nonstrict
local function f1(v): number?
if v then
return 1
end
end
local function f2(v)
if v then
return 1
end
end
local function f3(v): ()
if v then
return
end
end
local function f4(v)
if v then
return
end
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
FunctionExitsWithoutReturning* err = get<FunctionExitsWithoutReturning>(result.errors[0]);
CHECK(err);
}
TEST_CASE_FIXTURE(Fixture, "report_exiting_without_return_strict")
{
CheckResult result = check(R"(
--!strict
local function f1(v): number?
if v then
return 1
end
end
local function f2(v)
if v then
return 1
end
end
local function f3(v): ()
if v then
return
end
end
local function f4(v)
if v then
return
end
end
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
FunctionExitsWithoutReturning* annotatedErr = get<FunctionExitsWithoutReturning>(result.errors[0]);
CHECK(annotatedErr);
FunctionExitsWithoutReturning* inferredErr = get<FunctionExitsWithoutReturning>(result.errors[1]);
CHECK(inferredErr);
}
TEST_CASE_FIXTURE(Fixture, "calling_function_with_incorrect_argument_type_yields_errors_spanning_argument")
{
CheckResult result = check(R"(
function foo(a: number, b: string) end
foo("Test", 123)
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ(
result.errors[0],
(TypeError{
Location{Position{3, 12}, Position{3, 18}},
TypeMismatch{
getBuiltins()->numberType,
getBuiltins()->stringType,
}
})
);
CHECK_EQ(
result.errors[1],
(TypeError{
Location{Position{3, 20}, Position{3, 23}},
TypeMismatch{
getBuiltins()->stringType,
getBuiltins()->numberType,
}
})
);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "calling_function_with_anytypepack_doesnt_leak_free_types")
{
CheckResult result = check(R"(
--!nonstrict
function Test(a)
return 1, ""
end
local tab = {}
table.insert(tab, Test(1));
)");
LUAU_REQUIRE_NO_ERRORS(result);
ToStringOptions opts;
opts.exhaustive = true;
opts.maxTableLength = 0;
if (!FFlag::DebugLuauForceOldSolver)
CHECK_EQ("{string}", toString(requireType("tab"), opts));
else
CHECK_EQ("{any}", toString(requireType("tab"), opts));
}
TEST_CASE_FIXTURE(Fixture, "too_many_return_values")
{
CheckResult result = check(R"(
--!strict
function f()
return 55
end
local a, b = f()
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CountMismatch* acm = get<CountMismatch>(result.errors[0]);
REQUIRE(acm);
CHECK_EQ(acm->context, CountMismatch::FunctionResult);
CHECK_EQ(acm->expected, 1);
CHECK_EQ(acm->actual, 2);
}
TEST_CASE_FIXTURE(Fixture, "too_many_return_values_in_parentheses")
{
DOES_NOT_PASS_NEW_SOLVER_GUARD();
CheckResult result = check(R"(
--!strict
function f()
return 55
end
local a, b = (f())
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CountMismatch* acm = get<CountMismatch>(result.errors[0]);
REQUIRE(acm);
CHECK_EQ(acm->context, CountMismatch::FunctionResult);
CHECK_EQ(acm->expected, 1);
CHECK_EQ(acm->actual, 2);
}
TEST_CASE_FIXTURE(Fixture, "too_many_return_values_no_function")
{
DOES_NOT_PASS_NEW_SOLVER_GUARD();
CheckResult result = check(R"(
--!strict
local a, b = 55
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CountMismatch* acm = get<CountMismatch>(result.errors[0]);
REQUIRE(acm);
CHECK_EQ(acm->context, CountMismatch::ExprListResult);
CHECK_EQ(acm->expected, 1);
CHECK_EQ(acm->actual, 2);
}
TEST_CASE_FIXTURE(Fixture, "ignored_return_values")
{
CheckResult result = check(R"(
--!strict
function f()
return 55, ""
end
local a = f()
)");
LUAU_REQUIRE_ERROR_COUNT(0, result);
}
TEST_CASE_FIXTURE(Fixture, "function_does_not_return_enough_values")
{
CheckResult result = check(R"(
--!strict
function f(): (number, string)
return 55
end
)");
if (!FFlag::DebugLuauForceOldSolver)
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
auto tpm = get<TypePackMismatch>(result.errors[0]);
REQUIRE(tpm);
CHECK("number, string" == toString(tpm->wantedTp));
CHECK("number" == toString(tpm->givenTp));
}
else
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
CountMismatch* acm = get<CountMismatch>(result.errors[0]);
REQUIRE(acm);
CHECK_EQ(acm->context, CountMismatch::Return);
CHECK_EQ(acm->expected, 2);
CHECK_EQ(acm->actual, 1);
}
}
TEST_CASE_FIXTURE(Fixture, "function_cast_error_uses_correct_language")
{
CheckResult result = check(R"(
function foo(a, b): number
return 0
end
local a: (string)->number = foo
local b: (number, number)->(number, number) = foo
local c: (string, number)->number = foo -- no error
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
auto tm1 = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm1);
CHECK_EQ("(string) -> number", toString(tm1->wantedType));
if (!FFlag::DebugLuauForceOldSolver)
CHECK_EQ("(unknown, unknown) -> number", toString(tm1->givenType));
else
CHECK_EQ("(string, *error-type*) -> number", toString(tm1->givenType));
auto tm2 = get<TypeMismatch>(result.errors[1]);
REQUIRE(tm2);
CHECK_EQ("(number, number) -> (number, number)", toString(tm2->wantedType));
if (!FFlag::DebugLuauForceOldSolver)
CHECK_EQ("(unknown, unknown) -> number", toString(tm1->givenType));
else
CHECK_EQ("(string, *error-type*) -> number", toString(tm2->givenType));
}
TEST_CASE_FIXTURE(Fixture, "no_lossy_function_type")
{
CheckResult result = check(R"(
--!strict
local tbl = {}
function tbl:abc(a: number, b: number)
return a
end
tbl:abc(1, 2) -- Line 6
-- | Column 14
)");
LUAU_REQUIRE_NO_ERRORS(result);
TypeId type = requireTypeAtPosition(Position(6, 14));
if (!FFlag::DebugLuauForceOldSolver)
CHECK_EQ("(unknown, number, number) -> number", toString(type));
else
CHECK_EQ("(tbl, number, number) -> number", toString(type));
auto ftv = get<FunctionType>(follow(type));
REQUIRE(ftv);
CHECK(ftv->hasSelf);
}
TEST_CASE_FIXTURE(Fixture, "record_matching_overload")
{
CheckResult result = check(R"(
type Overload = ((string) -> string) & ((number) -> number)
local abc: Overload
abc(1)
)");
LUAU_REQUIRE_NO_ERRORS(result);
std::vector<AstNode*> ancestry = findAstAncestryOfPosition(*getMainSourceModule(), Position(3, 10));
REQUIRE_GE(ancestry.size(), 2);
AstExpr* parentExpr = ancestry[ancestry.size() - 2]->asExpr();
REQUIRE(bool(parentExpr));
REQUIRE(parentExpr->is<AstExprCall>());
ModulePtr module = getMainModule();
auto it = module->astOverloadResolvedTypes.find(parentExpr);
REQUIRE(it);
CHECK_EQ(toString(*it), "(number) -> number");
}
TEST_CASE_FIXTURE(Fixture, "return_type_by_overload")
{
CheckResult result = check(R"(
type Overload = ((string) -> string) & ((number, number) -> number)
local abc: Overload
local x = abc(true)
local y = abc(true,true)
local z = abc(true,true,true)
)");
LUAU_REQUIRE_ERRORS(result);
CHECK_EQ("string", toString(requireType("x")));
CHECK_EQ("number", toString(requireType("y")));
if (!FFlag::DebugLuauForceOldSolver)
CHECK_EQ("*error-type*", toString(requireType("z")));
else
CHECK_EQ("string", toString(requireType("z")));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "infer_anonymous_function_arguments")
{
DOES_NOT_PASS_NEW_SOLVER_GUARD();
CheckResult result = check(R"(
type Table = { x: number, y: number }
local function f(a: (Table) -> number) return a({x = 1, y = 2}) end
f(function(a) return a.x + a.y end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
result = check(R"(
type Table = { x: number, y: number }
local function f(a: ((Table) -> number)?) if a then return a({x = 1, y = 2}) else return 0 end end
f(function(a) return a.x + a.y end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
result = check(R"(
type Table = { x: number, y: number }
local x = {}
x.b = {x = 1, y = 2}
function x:f(a: (Table) -> number) return a(self.b) end
x:f(function(a) return a.x + a.y end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
result = check(R"(
function f(a: (a: number, b: number, c: boolean) -> number) return a(1, 2, true) end
f(function(a: number, b, c) return c and a + b or b - a end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
result = check(R"(
type Table = { x: number, y: number }
local function f(a: (Table) -> number) return a({x = 1, y = 2}) end
f(function(...) return select(1, ...).z end)
)");
LUAU_REQUIRE_ERRORS(result);
CHECK_EQ("Key 'z' not found in table 'Table'", toString(result.errors[0]));
result = check(R"(
function f(a: (a: number, b: number) -> number) return a(1, 2) end
f(function(a, b, c, ...) return a + b end)
)");
LUAU_REQUIRE_ERRORS(result);
std::string expected;
if (FFlag::LuauInstantiateInSubtyping)
{
expected = "Expected this to be\n\t"
"'(number, number) -> number'"
"\nbut got\n\t"
"'<a>(number, number, a) -> number'"
"\ncaused by:\n"
" Argument count mismatch. Function expects 3 arguments, but only 2 are specified";
}
else
{
expected = "Expected this to be\n\t"
"'(number, number) -> number'"
"\nbut got\n\t"
"'(number, number, *error-type*) -> number'"
"\ncaused by:\n"
" Argument count mismatch. Function expects 3 arguments, but only 2 are specified";
}
CHECK_EQ(expected, toString(result.errors[0]));
result = check(R"(
function f(a: (...number) -> number) return a(1, 2) end
f(function(a, b) return a + b end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
result = check(R"(
type Table = { x: number, y: number }
function f(a: (...Table) -> number) return a({x = 1, y = 2}, {x = 3, y = 4}) end
f(function(a, ...) local b = ... return b.z end)
)");
LUAU_REQUIRE_ERRORS(result);
CHECK_EQ("Key 'z' not found in table 'Table'", toString(result.errors[0]));
result = check(R"(
type Table = { x: number, y: number }
function f(a: (number) -> Table) return a(4) end
f(function(x) return x * 2 end)
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Expected this to be 'Table', but got 'number'", toString(result.errors[0]));
result = check(R"(
function f(a: (number) -> nil) return a(4) end
f(function(x) print(x) end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "infer_generic_function_function_argument")
{
DOES_NOT_PASS_NEW_SOLVER_GUARD();
CheckResult result = check(R"(
local function sum<a>(x: a, y: a, f: (a, a) -> a) return f(x, y) end
return sum(2, 3, function(a, b) return a + b end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
result = check(R"(
local function map<a, b>(arr: {a}, f: (a) -> b) local r = {} for i,v in ipairs(arr) do table.insert(r, f(v)) end return r end
local a = {1, 2, 3}
local r = map(a, function(a) return a + a > 100 end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
REQUIRE_EQ("{boolean}", toString(requireType("r")));
check(R"(
local function foldl<a, b>(arr: {a}, init: b, f: (b, a) -> b) local r = init for i,v in ipairs(arr) do r = f(r, v) end return r end
local a = {1, 2, 3}
local r = foldl(a, {s=0,c=0}, function(a, b) return {s = a.s + b, c = a.c + 1} end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
REQUIRE_EQ("{| c: number, s: number |}", toString(requireType("r")));
}
TEST_CASE_FIXTURE(Fixture, "infer_generic_function_function_argument_overloaded")
{
DOES_NOT_PASS_NEW_SOLVER_GUARD();
CheckResult result = check(R"(
local function g1<T>(a: T, f: (T) -> T) return f(a) end
local function g2<T>(a: T, b: T, f: (T, T) -> T) return f(a, b) end
local g12: typeof(g1) & typeof(g2)
g12(1, function(x) return x + x end)
g12(1, 2, function(x, y) return x + y end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
result = check(R"(
local function g1<T>(a: T, f: (T) -> T) return f(a) end
local function g2<T>(a: T, b: T, f: (T, T) -> T) return f(a, b) end
local g12: typeof(g1) & typeof(g2)
g12({x=1}, function(x) return {x=-x.x} end)
g12({x=1}, {x=2}, function(x, y) return {x=x.x + y.x} end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "infer_generic_lib_function_function_argument")
{
ScopedFastFlag sffs[] = {
{FFlag::DebugLuauForceOldSolver, false},
{FFlag::LuauReplacerRespectsReboundGenerics, true},
{FFlag::LuauOverloadGetsInstantiated2, true},
};
CheckResult result = check(R"(
local a = {{x=4}, {x=7}, {x=1}}
table.sort(a, function(x, y) return x.x < y.x end)
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(get<CannotInferBinaryOperation>(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "variadic_any_is_compatible_with_a_generic_TypePack")
{
CheckResult result = check(R"(
--!strict
local function f(...) return ... end
local g = function(...) return f(...) end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "variadic_any_is_compatible_with_a_generic_TypePack_2")
{
CheckResult result = check(R"(
local function somethingThatsAny(...: any)
print(...)
end
local function x<T...>(...: T...)
somethingThatsAny(...) -- Failed to unify variadic type packs
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "infer_anonymous_function_arguments_outside_call")
{
CheckResult result = check(R"(
type Table = { x: number, y: number }
local f: (Table) -> number = function(t) return t.x + t.y end
type TableWithFunc = { x: number, y: number, f: (number, number) -> number }
local a: TableWithFunc = { x = 3, y = 4, f = function(a, b) return a + b end }
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "infer_return_value_type")
{
CheckResult result = check(R"(
local function f(): {string|number}
return {1, "b", 3}
end
local function g(): (number, {string|number})
return 4, {1, "b", 3}
end
local function h(): ...{string|number}
return {4}, {1, "b", 3}, {"s"}
end
local function i(): ...{string|number}
return {1, "b", 3}, h()
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_arg_count")
{
DOES_NOT_PASS_NEW_SOLVER_GUARD();
CheckResult result = check(R"(
type A = (number, number) -> string
type B = (number) -> string
local a: A
local b: B = a
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = "Expected this to be\n\t"
"'(number) -> string'"
"\nbut got\n\t"
"'(number, number) -> string'"
"\ncaused by:\n"
" Argument count mismatch. Function expects 2 arguments, but only 1 is specified";
CHECK_EQ(expected, toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_arg")
{
DOES_NOT_PASS_NEW_SOLVER_GUARD();
CheckResult result = check(R"(
type A = (number, number) -> string
type B = (number, string) -> string
local a: A
local b: B = a
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = "Expected this to be\n\t"
"'(number, string) -> string'"
"\nbut got\n\t"
"'(number, number) -> string'"
"\ncaused by:\n"
" Argument #2 type is not compatible.\n"
"Expected this to be 'number', but got 'string'";
CHECK_EQ(expected, toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_ret_count")
{
DOES_NOT_PASS_NEW_SOLVER_GUARD();
CheckResult result = check(R"(
type A = (number, number) -> (number)
type B = (number, number) -> (number, boolean)
local a: A
local b: B = a
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = "Expected this to be\n\t"
"'(number, number) -> (number, boolean)'"
"\nbut got\n\t"
"'(number, number) -> number'"
"\ncaused by:\n"
" Function only returns 1 value, but 2 are required here";
CHECK_EQ(expected, toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_ret")
{
DOES_NOT_PASS_NEW_SOLVER_GUARD();
CheckResult result = check(R"(
type A = (number, number) -> string
type B = (number, number) -> number
local a: A
local b: B = a
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = "Expected this to be\n\t"
"'(number, number) -> number'"
"\nbut got\n\t"
"'(number, number) -> string'"
"\ncaused by:\n"
" Return type is not compatible.\n"
"Expected this to be 'number', but got 'string'";
CHECK_EQ(expected, toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_ret_mult")
{
DOES_NOT_PASS_NEW_SOLVER_GUARD();
CheckResult result = check(R"(
type A = (number, number) -> (number, string)
type B = (number, number) -> (number, boolean)
local a: A
local b: B = a
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = "Expected this to be\n\t"
"'(number, number) -> (number, boolean)'"
"\nbut got\n\t"
"'(number, number) -> (number, string)'"
"\ncaused by:\n"
" Return #2 type is not compatible.\n"
"Expected this to be 'boolean', but got 'string'";
CHECK_EQ(expected, toString(result.errors[0]));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "function_decl_quantify_right_type")
{
fileResolver.source["game/isAMagicMock"] = R"(
--!nonstrict
return function(value)
return false
end
)";
CheckResult result = check(R"(
--!nonstrict
local MagicMock = {}
MagicMock.is = require(game.isAMagicMock)
function MagicMock.is(value)
return false
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "function_decl_non_self_sealed_overwrite")
{
CheckResult result = check(R"(
function string.len(): number
return 1
end
local s = string
)");
LUAU_REQUIRE_NO_ERRORS(result);
getFrontend().clear();
CheckResult result2 = check(R"(
print(string.len('hello'))
)");
LUAU_REQUIRE_NO_ERRORS(result2);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "function_decl_non_self_sealed_overwrite_2")
{
CheckResult result = check(R"(
local t: { f: ((x: number) -> number)? } = {}
function t.f(x)
print(x + 5)
return x .. "asd" -- 1st error: we know that return type is a number, not a string
end
t.f = function(x)
print(x + 5)
return x .. "asd" -- 2nd error: we know that return type is a number, not a string
end
)");
if (!FFlag::DebugLuauForceOldSolver)
{
LUAU_CHECK_ERROR_COUNT(2, result);
LUAU_CHECK_ERROR(result, WhereClauseNeeded); }
else
{
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ(toString(result.errors[0]), R"(Expected this to be 'number', but got 'string')");
CHECK_EQ(toString(result.errors[1]), R"(Expected this to be 'number', but got 'string')");
}
}
TEST_CASE_FIXTURE(Fixture, "inferred_higher_order_functions_are_quantified_at_the_right_time2")
{
CheckResult result = check(R"(
--!strict
local function resolveDispatcher()
return (nil :: any) :: {useContext: (number?) -> any}
end
local useContext
useContext = function(unstable_observedBits: number?)
resolveDispatcher().useContext(unstable_observedBits)
end
)");
CHECK(result.errors.empty());
if (!result.errors.empty())
{
for (const auto& e : result.errors)
MESSAGE(e.moduleName << " " << toString(e.location) << ": " << toString(e));
}
}
TEST_CASE_FIXTURE(Fixture, "inferred_higher_order_functions_are_quantified_at_the_right_time3")
{
DOES_NOT_PASS_NEW_SOLVER_GUARD();
CheckResult result = check(R"(
local foo
foo():bar(function()
return foo()
end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "function_decl_non_self_unsealed_overwrite")
{
ScopedFastFlag _{FFlag::LuauCheckFunctionStatementTypes, true};
CheckResult result = check(R"(
local t = { f = nil :: ((x: number) -> number)? }
function t.f(x: string): string -- 1st error: new function value type is incompatible
return x .. "asd"
end
t.f = function(x)
print(x + 5)
return x .. "asd" -- 2nd error: we know that return type is a number, not a string
end
)");
if (!FFlag::DebugLuauForceOldSolver)
{
LUAU_CHECK_ERROR_COUNT(2, result);
LUAU_CHECK_ERROR(result, WhereClauseNeeded);
}
else
{
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ(toString(result.errors[0]), R"(Expected this to be
'((number) -> number)?'
but got
'(string) -> string'
caused by:
None of the union options are compatible. For example:
Expected this to be
'(number) -> number'
but got
'(string) -> string'
caused by:
Argument #1 type is not compatible.
Expected this to be 'string', but got 'number')");
CHECK_EQ(toString(result.errors[1]), R"(Expected this to be 'number', but got 'string')");
}
}
TEST_CASE_FIXTURE(Fixture, "strict_mode_ok_with_missing_arguments")
{
CheckResult result = check(R"(
local function f(x: any) end
f()
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "function_statement_sealed_table_assignment_through_indexer")
{
DOES_NOT_PASS_NEW_SOLVER_GUARD();
CheckResult result = check(R"(
local t: {[string]: () -> number} = {}
function t.a() return 1 end -- OK
function t:b() return 2 end -- not OK
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(
"Expected this to be\n\t"
"'() -> number'"
"\nbut got\n\t"
"'(*error-type*) -> number'"
"\ncaused by:\n"
" Argument count mismatch. Function expects 1 argument, but none are specified",
toString(result.errors[0])
);
}
TEST_CASE_FIXTURE(Fixture, "too_few_arguments_variadic")
{
CheckResult result = check(R"(
function test(a: number, b: string, ...)
end
test(1)
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
auto err = result.errors[0];
auto acm = get<CountMismatch>(err);
REQUIRE(acm);
CHECK_EQ(2, acm->expected);
CHECK_EQ(1, acm->actual);
CHECK_EQ(CountMismatch::Context::Arg, acm->context);
CHECK(acm->isVariadic);
}
TEST_CASE_FIXTURE(Fixture, "too_few_arguments_variadic_generic")
{
DOES_NOT_PASS_NEW_SOLVER_GUARD();
CheckResult result = check(R"(
function test(a: number, b: string, ...)
return 1
end
function wrapper<A...>(f: (A...) -> number, ...: A...)
end
wrapper(test)
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
auto err = result.errors[0];
auto acm = get<CountMismatch>(err);
REQUIRE(acm);
CHECK_EQ(3, acm->expected);
CHECK_EQ(1, acm->actual);
CHECK_EQ(CountMismatch::Context::Arg, acm->context);
CHECK(acm->isVariadic);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "too_few_arguments_variadic_generic2")
{
DOES_NOT_PASS_NEW_SOLVER_GUARD();
CheckResult result = check(R"(
function test(a: number, b: string, ...)
return 1
end
function wrapper<A...>(f: (A...) -> number, ...: A...)
end
pcall(wrapper, test)
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
auto err = result.errors[0];
auto acm = get<CountMismatch>(err);
REQUIRE(acm);
CHECK_EQ(4, acm->expected);
CHECK_EQ(2, acm->actual);
CHECK_EQ(CountMismatch::Context::Arg, acm->context);
CHECK(acm->isVariadic);
}
TEST_CASE_FIXTURE(Fixture, "occurs_check_failure_in_function_return_type")
{
CheckResult result = check(R"(
function f()
return 5, f()
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(nullptr != get<OccursCheckFailed>(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "free_is_not_bound_to_unknown")
{
if (!FFlag::DebugLuauForceOldSolver)
return;
CheckResult result = check(R"(
local function foo(f: (unknown) -> (), x)
f(x)
end
)");
CHECK_EQ("<a>((unknown) -> (), a) -> ()", toString(requireType("foo")));
}
TEST_CASE_FIXTURE(Fixture, "dont_infer_parameter_types_for_functions_from_their_call_site")
{
CheckResult result = check(R"(
local t = {}
function t.f(x)
return x
end
t.__index = t
function g(s)
local q = s.p and s.p.q or nil
return q and t.f(q) or nil
end
local f = t.f
)");
CHECK_EQ("<a>(a) -> a", toString(requireType("f")));
if (!FFlag::DebugLuauForceOldSolver)
{
LUAU_CHECK_NO_ERRORS(result);
}
else
{
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("({+ p: {+ q: nil +} +}) -> nil", toString(requireType("g")));
}
}
TEST_CASE_FIXTURE(Fixture, "dont_mutate_the_underlying_head_of_typepack_when_calling_with_self")
{
CheckResult result = check(R"(
local t = {}
function t:m(x) end
function f(): never return 5 :: never end
t:m(f())
t:m(f())
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "improved_function_arg_mismatch_errors")
{
CheckResult result = check(R"(
local function foo1(a: number) end
foo1()
local function foo2(a: number, b: string?) end
foo2()
local function foo3(a: number, b: string?, c: any) end -- any is optional
foo3()
string.find()
local t = {}
function t.foo(x: number, y: string?, ...: any) return 1 end
function t:bar(x: number, y: string?) end
t.foo()
t:bar()
local u = { a = t, b = function() return t end }
u.a.foo()
local x = (u.a).foo()
u.b().foo()
)");
LUAU_REQUIRE_ERROR_COUNT(9, result);
if (!FFlag::DebugLuauForceOldSolver)
{
CHECK_EQ(toString(result.errors[0]), "Argument count mismatch. Function expects 1 argument, but none are specified");
CHECK_EQ(toString(result.errors[1]), "Argument count mismatch. Function expects 1 to 2 arguments, but none are specified");
CHECK_EQ(toString(result.errors[2]), "Argument count mismatch. Function expects 1 to 3 arguments, but none are specified");
CHECK_EQ(toString(result.errors[3]), "Argument count mismatch. Function expects 2 to 4 arguments, but none are specified");
CHECK_EQ(toString(result.errors[4]), "Argument count mismatch. Function expects at least 1 argument, but none are specified");
CHECK_EQ(toString(result.errors[5]), "Argument count mismatch. Function expects 2 to 3 arguments, but only 1 is specified");
CHECK_EQ(toString(result.errors[6]), "Argument count mismatch. Function expects at least 1 argument, but none are specified");
CHECK_EQ(toString(result.errors[7]), "Argument count mismatch. Function expects at least 1 argument, but none are specified");
CHECK_EQ(toString(result.errors[8]), "Argument count mismatch. Function expects at least 1 argument, but none are specified");
}
else
{
CHECK_EQ(toString(result.errors[0]), "Argument count mismatch. Function 'foo1' expects 1 argument, but none are specified");
CHECK_EQ(toString(result.errors[1]), "Argument count mismatch. Function 'foo2' expects 1 to 2 arguments, but none are specified");
CHECK_EQ(toString(result.errors[2]), "Argument count mismatch. Function 'foo3' expects 1 to 3 arguments, but none are specified");
CHECK_EQ(toString(result.errors[3]), "Argument count mismatch. Function 'string.find' expects 2 to 4 arguments, but none are specified");
CHECK_EQ(toString(result.errors[4]), "Argument count mismatch. Function 't.foo' expects at least 1 argument, but none are specified");
CHECK_EQ(toString(result.errors[5]), "Argument count mismatch. Function 't.bar' expects 2 to 3 arguments, but only 1 is specified");
CHECK_EQ(toString(result.errors[6]), "Argument count mismatch. Function 'u.a.foo' expects at least 1 argument, but none are specified");
CHECK_EQ(toString(result.errors[7]), "Argument count mismatch. Function 'u.a.foo' expects at least 1 argument, but none are specified");
CHECK_EQ(toString(result.errors[8]), "Argument count mismatch. Function expects at least 1 argument, but none are specified");
}
}
TEST_CASE_FIXTURE(BuiltinsFixture, "improved_function_arg_mismatch_error_nonstrict")
{
DOES_NOT_PASS_NEW_SOLVER_GUARD();
CheckResult result = check(R"(
--!nonstrict
local function foo(a, b) end
foo(string.find("hello", "e"))
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Argument count mismatch. Function 'foo' expects 0 to 2 arguments, but 3 are specified");
}
TEST_CASE_FIXTURE(Fixture, "luau_subtyping_is_np_hard")
{
DOES_NOT_PASS_NEW_SOLVER_GUARD();
CheckResult result = check(R"(
--!strict
-- An example of coding up graph coloring in the Luau type system.
-- This codes a three-node, two color problem.
-- A three-node triangle is uncolorable,
-- but a three-node line is colorable.
type Red = "red"
type Blue = "blue"
type Color = Red | Blue
type Coloring = (Color) -> (Color) -> (Color) -> boolean
type Uncolorable = (Color) -> (Color) -> (Color) -> false
type Line = Coloring
& ((Red) -> (Red) -> (Color) -> false)
& ((Blue) -> (Blue) -> (Color) -> false)
& ((Color) -> (Red) -> (Red) -> false)
& ((Color) -> (Blue) -> (Blue) -> false)
type Triangle = Line
& ((Red) -> (Color) -> (Red) -> false)
& ((Blue) -> (Color) -> (Blue) -> false)
local x : Triangle
local y : Line
local z : Uncolorable
z = x -- OK, so the triangle is uncolorable
z = y -- Not OK, so the line is colorable
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected =
"Expected this to be\n\t"
R"('("blue" | "red") -> ("blue" | "red") -> ("blue" | "red") -> false')"
"\nbut got\n\t"
R"('(("blue" | "red") -> ("blue" | "red") -> ("blue" | "red") -> boolean) & (("blue" | "red") -> ("blue") -> ("blue") -> false) & (("blue" | "red") -> ("red") -> ("red") -> false) & (("blue") -> ("blue") -> ("blue" | "red") -> false) & (("red") -> ("red") -> ("blue" | "red") -> false)')"
"; none of the intersection parts are compatible";
CHECK_EQ(expected, toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "function_is_supertype_of_concrete_functions")
{
registerHiddenTypes(getFrontend());
CheckResult result = check(R"(
function foo(f: fun) end
function a() end
function id(x) return x end
foo(a)
foo(id)
foo(foo)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "concrete_functions_are_not_supertypes_of_function")
{
registerHiddenTypes(getFrontend());
CheckResult result = check(R"(
local a: fun = function() end
function one(arg: () -> ()) end
function two(arg: <T>(T) -> T) end
one(a)
two(a)
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK(6 == result.errors[0].location.begin.line);
auto tm1 = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm1);
CHECK("() -> ()" == toString(tm1->wantedType));
CHECK("function" == toString(tm1->givenType));
CHECK(7 == result.errors[1].location.begin.line);
auto tm2 = get<TypeMismatch>(result.errors[1]);
REQUIRE(tm2);
CHECK("<T>(T) -> T" == toString(tm2->wantedType));
CHECK("function" == toString(tm2->givenType));
}
TEST_CASE_FIXTURE(Fixture, "other_things_are_not_related_to_function")
{
registerHiddenTypes(getFrontend());
CheckResult result = check(R"(
local a: fun = function() end
local b: {} = a
local c: boolean = a
local d: fun = true
local e: fun = {}
)");
LUAU_REQUIRE_ERROR_COUNT(4, result);
CHECK(2 == result.errors[0].location.begin.line);
CHECK(3 == result.errors[1].location.begin.line);
CHECK(4 == result.errors[2].location.begin.line);
CHECK(5 == result.errors[3].location.begin.line);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "fuzz_must_follow_in_overload_resolution")
{
CheckResult result = check(R"(
for _ in function<t0>():(t0)&((()->())&(()->()))
end do
_(_(_,_,_),_)
end
)");
LUAU_REQUIRE_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "dont_assert_when_the_tarjan_limit_is_exceeded_during_generalization")
{
ScopedFastFlag sff{FFlag::DebugLuauForceOldSolver, false};
ScopedFastInt sfi{FInt::LuauTarjanChildLimit, 1};
CheckResult result = check(R"(
function f(t)
t.x.y.z = 441
end
)");
LUAU_REQUIRE_ERROR(result, UnificationTooComplex);
}
TEST_CASE_FIXTURE(Fixture, "instantiated_type_packs_must_have_a_non_null_scope")
{
CheckResult result = check(R"(
function pcall<A..., R...>(...: (A...) -> R...): (boolean, R...)
return nil :: any
end
type Dispatch<A> = (A) -> ()
function mountReducer()
dispatchAction()
return nil :: any
end
function dispatchAction()
end
function useReducer(): Dispatch<any>
local result, setResult = pcall(mountReducer)
return setResult
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "inner_frees_become_generic_in_dcr")
{
if (FFlag::DebugLuauForceOldSolver)
return;
CheckResult result = check(R"(
function f(x)
local z = x
return x
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
std::optional<TypeId> ty = findTypeAtPosition(Position{3, 19});
REQUIRE(ty);
CHECK(get<GenericType>(follow(*ty)));
}
TEST_CASE_FIXTURE(Fixture, "function_exprs_are_generalized_at_signature_scope_not_enclosing")
{
CheckResult result = check(R"(
local foo
local bar
-- foo being a function expression is deliberate: the bug we're testing
-- only existed for function expressions, not for function statements.
foo = function(a)
return bar
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
if (!FFlag::DebugLuauForceOldSolver)
CHECK(toString(requireType("foo")) == "((unknown) -> nil)?");
else
{
CHECK(toString(requireType("foo")) == "<a>(a) -> 'b");
}
}
TEST_CASE_FIXTURE(BuiltinsFixture, "param_1_and_2_both_takes_the_same_generic_but_their_arguments_are_incompatible")
{
ScopedFastFlag sff{FFlag::LuauRelateHandlesCoincidentTables, true};
CheckResult result = check(R"(
local function foo<a>(x: a, y: a?)
return x
end
local vec2 = { x = 5, y = 7 }
local ret: number = foo(vec2, { x = 5 })
)");
if (!FFlag::DebugLuauForceOldSolver)
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
auto tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm);
CHECK("number" == toString(tm->wantedType));
CHECK("{ x: number } | { x: number, y: number }" == toString(tm->givenType, true));
}
else
{
LUAU_REQUIRE_ERROR_COUNT(2, result);
const std::string expected = R"(Expected this to be 'vec2?', but got '{| x: number |}'
caused by:
None of the union options are compatible. For example:
Table type '{| x: number |}' not compatible with type 'vec2' because the former is missing field 'y')";
CHECK_EQ(expected, toString(result.errors[0]));
CHECK_EQ("Expected this to be 'number', but got 'vec2'", toString(result.errors[1]));
}
}
TEST_CASE_FIXTURE(BuiltinsFixture, "param_1_and_2_both_takes_the_same_generic_but_their_arguments_are_incompatible_2")
{
CheckResult result = check(R"(
local function f<a>(x: a, y: a): a
return if math.random() > 0.5 then x else y
end
local z: boolean = f(5, "five")
)");
if (!FFlag::DebugLuauForceOldSolver)
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
auto tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm);
CHECK("boolean" == toString(tm->wantedType));
CHECK("number | string" == toString(tm->givenType));
}
else
{
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ(toString(result.errors[0]), "Expected this to be 'number', but got 'string'");
CHECK_EQ(toString(result.errors[1]), "Expected this to be 'boolean', but got 'number'");
}
}
TEST_CASE_FIXTURE(Fixture, "attempt_to_call_an_intersection_of_tables")
{
CheckResult result = check(R"(
local function f(t: { x: number } & { y: string })
t()
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (!FFlag::DebugLuauForceOldSolver)
CHECK_EQ(toString(result.errors[0]), "Cannot call a value of type { x: number } & { y: string }");
else
CHECK_EQ(toString(result.errors[0]), "Cannot call a value of type { x: number }");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "attempt_to_call_an_intersection_of_tables_with_call_metamethod")
{
CheckResult result = check(R"(
type Callable = typeof(setmetatable({}, {
__call = function(self, ...) return ... end
}))
local function f(t: Callable & { x: number })
t()
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "generic_packs_are_not_variadic")
{
ScopedFastFlag sffs[] = {
{FFlag::DebugLuauForceOldSolver, false},
{FFlag::LuauReplacerRespectsReboundGenerics, true},
{FFlag::LuauOverloadGetsInstantiated2, true},
};
CheckResult result = check(R"(
local function apply<a, b..., c...>(f: (a, b...) -> c..., x: a)
return f(x)
end
local function add(x: number, y: number)
return x + y
end
local function addToSix(x: number)
return x + 6
end
apply(addToSix, 7)
apply(add, 5)
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(Location{{2, 21}, {2, 22}} == result.errors.at(0).location);
auto err = get<TypePackMismatch>(result.errors[0]);
CHECK_EQ("a", toString(err->givenTp));
CHECK_EQ("b...", toString(err->wantedTp));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "num_is_solved_before_num_or_str")
{
CheckResult result = check(R"(
function num()
return 5
end
local function num_or_str()
if math.random() > 0.5 then
return num()
else
return "some string"
end
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Expected this to be 'number', but got 'string'", toString(result.errors[0]));
CHECK_EQ("() -> number", toString(requireType("num_or_str")));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "num_is_solved_after_num_or_str")
{
CheckResult result = check(R"(
local function num_or_str()
if math.random() > 0.5 then
return num()
else
return "some string"
end
end
function num()
return 5
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Expected this to be 'number', but got 'string'", toString(result.errors[0]));
CHECK_EQ("() -> number", toString(requireType("num_or_str")));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "apply_of_lambda_with_inferred_and_explicit_types")
{
CheckResult result = check(R"(
local function apply(f, x) return f(x) end
local x = apply(function(x: string): number return 5 end, "hello!")
local function apply_explicit<A, B...>(f: (A) -> B..., x: A): B... return f(x) end
local x = apply_explicit(function(x: string): number return 5 end, "hello!")
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "regex_benchmark_string_format_minimization")
{
CheckResult result = check(R"(
(nil :: any)(function(n)
if tonumber(n) then
n = tonumber(n)
elseif n ~= nil then
string.format("invalid argument #4 to 'sub': number expected, got %s", typeof(n))
end
end);
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "subgeneric_type_function_super_monomorphic")
{
CheckResult result = check(R"(
local a: (number, number) -> number = function(a, b) return a - b end
a = function(a, b) return a + b end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "simple_unannotated_mutual_recursion")
{
if (!FFlag::DebugLuauForceOldSolver)
return;
CheckResult result = check(R"(
function even(n)
if n == 0 then
return true
else
return odd(n - 1)
end
end
function odd(n)
if n == 0 then
return false
elseif n == 1 then
return true
else
return even(n - 1)
end
end
)");
if (!FFlag::DebugLuauForceOldSolver)
{
LUAU_REQUIRE_ERROR_COUNT(5, result);
CHECK(get<ConstraintSolvingIncompleteError>(result.errors[0]));
bool r = toString(result.errors[1]) == "Expected this to be 'boolean', but got '*blocked-tp-1*'; type *blocked-tp-1*.tail() "
"(*blocked-tp-1*) is not a subtype of boolean (boolean)";
CHECK(r);
CHECK(
toString(result.errors[2]) ==
"Operator '-' could not be applied to operands of types unknown and number; there is no corresponding overload for __sub"
);
CHECK(
toString(result.errors[3]) ==
"Operator '-' could not be applied to operands of types unknown and number; there is no corresponding overload for __sub"
);
CHECK(
toString(result.errors[4]) ==
"Operator '-' could not be applied to operands of types unknown and number; there is no corresponding overload for __sub"
);
}
else
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(toString(result.errors[0]) == "Unknown type used in - operation; consider adding a type annotation to 'n'");
}
}
TEST_CASE_FIXTURE(BuiltinsFixture, "simple_lightly_annotated_mutual_recursion")
{
CheckResult result = check(R"(
function even(n: number)
if n == 0 then
return true
else
return odd(n - 1)
end
end
function odd(n: number)
if n == 0 then
return false
elseif n == 1 then
return true
else
return even(n - 1)
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("(number) -> boolean", toString(requireType("even")));
CHECK_EQ("(number) -> boolean", toString(requireType("odd")));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "tf_suggest_return_type")
{
ScopedFastFlag sffs[] = {
{FFlag::DebugLuauForceOldSolver, false},
};
CheckResult result = check(R"(
function fib(n)
return n < 2 and 1 or fib(n-1) + fib(n-2)
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
auto err = get<ExplicitFunctionAnnotationRecommended>(result.errors.back());
LUAU_ASSERT(err);
CHECK("false | number" == toString(err->recommendedReturn));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "tf_suggest_arg_type")
{
if (FFlag::DebugLuauForceOldSolver)
return;
CheckResult result = check(R"(
function fib(n, u)
return (n or u) and (n < u and n + fib(n,u))
end
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK(get<CannotInferBinaryOperation>(result.errors[0]));
auto err2 = get<ExplicitFunctionAnnotationRecommended>(result.errors[1]);
LUAU_ASSERT(err2);
CHECK("number" == toString(err2->recommendedReturn));
REQUIRE(err2->recommendedArgs.size() == 2);
CHECK("number" == toString(err2->recommendedArgs[0].second));
CHECK("number" == toString(err2->recommendedArgs[1].second));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "tf_suggest_arg_type_2")
{
if (FFlag::DebugLuauForceOldSolver)
return;
getFrontend().options.retainFullTypeGraphs = false;
CheckResult result = check(R"(
local function escape_fslash(pre)
return (#pre % 2 == 0 and '\\' or '') .. pre .. '.'
end
)");
LUAU_REQUIRE_ERROR(result, NotATable);
}
TEST_CASE_FIXTURE(Fixture, "local_function_fwd_decl_doesnt_crash")
{
CheckResult result = check(R"(
local foo
local function bar()
foo()
end
function foo()
end
bar()
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "bidirectional_checking_of_callback_property")
{
CheckResult result = check(R"(
function print(x: number) end
type Point = {x: number, y: number}
local T : {callback: ((Point) -> ())?} = {}
T.callback = function(p) -- No error here
print(p.z) -- error here. Point has no property z
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (!FFlag::DebugLuauForceOldSolver)
{
auto tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm);
CHECK("((Point) -> ())?" == toString(tm->wantedType));
CHECK("({ read z: number }) -> ()" == toString(tm->givenType));
Location location = result.errors[0].location;
CHECK(location.begin.line == 6);
CHECK(location.end.line == 8);
}
else
{
CHECK_MESSAGE(get<UnknownProperty>(result.errors[0]), "Expected UnknownProperty but got " << result.errors[0]);
Location location = result.errors[0].location;
CHECK(location.begin.line == 7);
CHECK(location.end.line == 7);
}
}
TEST_CASE_FIXTURE(ExternTypeFixture, "bidirectional_inference_of_class_methods")
{
CheckResult result = check(R"(
local c = ChildClass.New()
-- Instead of reporting that the lambda is the wrong type, report that we are using its argument improperly.
c.Touched:Connect(function(other)
print(other.ThisDoesNotExist)
end)
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
UnknownProperty* err = get<UnknownProperty>(result.errors[0]);
REQUIRE(err);
CHECK("ThisDoesNotExist" == err->key);
CHECK("BaseClass" == toString(err->table));
}
TEST_CASE_FIXTURE(Fixture, "pass_table_literal_to_function_expecting_optional_prop")
{
CheckResult result = check(R"(
type T = {prop: number?}
function f(t: T) end
f({prop=5})
f({})
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "dont_infer_overloaded_functions")
{
CheckResult result = check(R"(
function getR6Attachments(model)
model:FindFirstChild("Right Leg")
model:FindFirstChild("Left Leg")
model:FindFirstChild("Torso")
model:FindFirstChild("Torso")
model:FindFirstChild("Head")
model:FindFirstChild("Left Arm")
model:FindFirstChild("Right Arm")
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
if (!FFlag::DebugLuauForceOldSolver)
CHECK("(t1) -> () where t1 = { read FindFirstChild: (t1, string) -> (...unknown) }" == toString(requireType("getR6Attachments")));
else
CHECK("<a...>(t1) -> () where t1 = {+ FindFirstChild: (t1, string) -> (a...) +}" == toString(requireType("getR6Attachments")));
}
TEST_CASE_FIXTURE(Fixture, "param_y_is_bounded_by_x_of_type_string")
{
CheckResult result = check(R"(
local function f(x: string, y)
x = y
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK("(string, string) -> ()" == toString(requireType("f")));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "function_that_could_return_anything_is_compatible_with_function_that_is_expected_to_return_nothing")
{
CheckResult result = check(R"(
-- We infer foo : (g: (number) -> (...unknown)) -> ()
function foo(g)
g(0)
end
-- a requires a function that returns no values
function a(f: ((number) -> ()) -> ())
end
-- "Returns an unknown number of values" is close enough to "returns no values."
a(foo)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "self_application_does_not_segfault")
{
(void)check(R"(
function f(a)
f(f)
return f(), a
end
)");
}
TEST_CASE_FIXTURE(Fixture, "function_definition_in_a_do_block")
{
CheckResult result = check(R"(
local f
do
function f()
end
end
f()
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "function_definition_in_a_do_block_with_global")
{
CheckResult result = check(R"(
function f() print("a") end
do
function f()
print("b")
end
end
f()
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "fuzzer_alias_global_function_doesnt_hit_nil_assert")
{
CheckResult result = check(R"(
function _()
end
local function l0()
function _()
end
end
_ = _
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "fuzzer_bug_missing_follow_causes_assertion")
{
CheckResult result = check(R"(
local _ = ({_=function()
return _
end,}),true,_[_()]
for l0=_[_[_[`{function(l0)
end}`]]],_[_.n6[_[_.n6]]],_[_[_.n6[_[_.n6]]]] do
_ += if _ then ""
end
return _
)");
}
TEST_CASE_FIXTURE(Fixture, "cannot_call_union_of_functions")
{
CheckResult result = check(R"(
local f: (() -> ()) | (() -> () -> ()) = nil :: any
f()
)");
if (!FFlag::DebugLuauForceOldSolver)
LUAU_REQUIRE_NO_ERRORS(result);
else
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
std::string expected = R"(Cannot call a value of the union type:
| () -> ()
| () -> () -> ()
We are unable to determine the appropriate result type for such a call.)";
CHECK(expected == toString(result.errors[0]));
}
}
TEST_CASE_FIXTURE(Fixture, "fuzzer_missing_follow_in_ast_stat_fun")
{
(void)check(R"(
local _ = function<t0...>()
end ~= _
while (_) do
_,_,_,_,_,_,_,_,_,_._,_ = nil
function _(...):<t0...>()->()
end
function _<t0...>(...):any
_ ..= ...
end
_,_,_,_,_,_,_,_,_,_,_ = nil
end
)");
}
TEST_CASE_FIXTURE(Fixture, "unifier_should_not_bind_free_types")
{
CheckResult result = check(R"(
function foo(player)
local success,result = player:thing()
if(success) then
return "Successfully posted message.";
elseif(not result) then
return false;
else
return result;
end
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
auto tm1 = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm1);
CHECK(toString(tm1->wantedType) == "string");
CHECK(toString(tm1->givenType) == "boolean");
}
TEST_CASE_FIXTURE(Fixture, "captured_local_is_assigned_a_function")
{
CheckResult result = check(R"(
local f
local function g()
f()
end
function f()
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "error_suppression_propagates_through_function_calls")
{
CheckResult result = check(R"(
function first(x: any)
return pairs(x)(x)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK("(any) -> (any?, any)" == toString(requireType("first")));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "fuzzer_normalizer_out_of_resources")
{
CheckResult result = check(R"(
Module 'l0':
local _ = true,...,_
if ... then
while _:_(_._G) do
do end
_ = _ and _
_ = 0 and {# _,}
local _ = "CCCCCCCCCCCCCCCCCCCCCCCCCCC"
local l0 = require(module0)
end
local function l0()
end
elseif _ then
l0 = _
end
do end
while _ do
_ = if _ then _ elseif _ then _,if _ then _ else _
_ = _()
do end
do end
if _ then
end
end
_ = _,{}
)");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "overload_resolution_crash_when_argExprs_is_smaller_than_type_args")
{
CheckResult result = check(R"(
--!strict
local parseError
type Set<T> = {[T]: any}
local function captureDependencies(
saveToSet: Set<PubTypes.Dependency>,
callback: (...any) -> any,
...
)
local data = table.pack(xpcall(callback, parseError, ...))
end
)");
}
TEST_CASE_FIXTURE(Fixture, "unpack_depends_on_rhs_pack_to_be_fully_resolved")
{
CheckResult result = check(R"(
--!strict
local function id(x)
return x
end
local u,v = id(3), id(id(44))
)");
CHECK_EQ(getBuiltins()->numberType, requireType("v"));
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "hidden_variadics_should_not_break_subtyping")
{
CheckResult result = check(R"(
--!strict
type FooType = {
SetValue: (Value: number) -> ()
}
local Foo: FooType = {
SetValue = function(Value: number)
end
}
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "coroutine_wrap_result_call")
{
CheckResult result = check(R"(
function foo(a, b)
coroutine.wrap(a)(b)
end
)");
}
TEST_CASE_FIXTURE(Fixture, "recursive_function_calls_should_not_use_the_generalized_type")
{
ScopedFastFlag crashOnForce{FFlag::DebugLuauAssertOnForcedConstraint, true};
CheckResult result = check(R"(
--!strict
function random()
return true -- chosen by fair coin toss
end
local f
f = 5
function f()
if random() then f() end
end
)");
if (!FFlag::DebugLuauForceOldSolver)
LUAU_REQUIRE_NO_ERRORS(result);
else
LUAU_REQUIRE_ERRORS(result); }
TEST_CASE_FIXTURE(Fixture, "recursive_function_calls_should_not_use_the_generalized_type_2")
{
ScopedFastFlag crashOnForce{FFlag::DebugLuauAssertOnForcedConstraint, true};
CheckResult result = check(R"(
--!strict
function random()
return true -- chosen by fair coin toss
end
local function f()
if random() then f() end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "fuzz_unwind_mutually_recursive_union_type_func")
{
ScopedFastFlag _{FFlag::DebugLuauForceOldSolver, false};
CheckResult result = check(R"(
local _ = ...
function _()
_ = _
end
_[function(...) repeat until _(_[l100]) _ = _ end] += _
)");
LUAU_REQUIRE_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "string_format_pack")
{
LUAU_REQUIRE_NO_ERRORS(check(R"(
local function foo(): (string, string, string)
return "", "", ""
end
print(string.format("%s %s %s", foo()))
)"));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "string_format_pack_variadic")
{
LUAU_REQUIRE_NO_ERRORS(check(R"(
local foo : () -> (...string) = (nil :: any)
print(string.format("%s %s %s", foo()))
)"));
}
TEST_CASE_FIXTURE(Fixture, "table_annotated_explicit_self")
{
ScopedFastFlag sff{FFlag::DebugLuauForceOldSolver, false};
CheckResult results = check(R"(
type MyObject = {
fn: (self: MyObject) -> number,
field: number
}
local Foo = {} :: MyObject
function Foo:fn()
local _ = self
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, results);
LUAU_REQUIRE_ERROR(results, FunctionExitsWithoutReturning); CHECK_EQ("MyObject", toString(requireTypeAtPosition({9, 24})));
}
TEST_CASE_FIXTURE(Fixture, "oss_1871")
{
ScopedFastFlag sff{FFlag::DebugLuauForceOldSolver, false};
LUAU_REQUIRE_NO_ERRORS(check(R"(
export type Test = {
[string]: (string) -> ()
}
local TestTbl: Test = {}
function TestTbl.Hello(Param)
local _ = Param
end
)"));
CHECK_EQ("string", toString(requireTypeAtPosition({8, 25})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "io_manager_oop_ish")
{
ScopedFastFlag sff{FFlag::DebugLuauForceOldSolver, false};
LUAU_REQUIRE_NO_ERRORS(check(R"(
type IIOManager = {
__index: IIOManager,
write: (self: IOManager, text: string, label: string?) -> number,
}
export type IOManager = setmetatable<{
buffer: {string},
memory: { [string]: number }
}, IIOManager>;
local IO = {} :: IIOManager
IO.__index = IO
function IO:write(text, label)
local _ = self
local _ = text
local _ = label
return 42
end
return IO
)"));
CHECK_EQ("IOManager", toString(requireTypeAtPosition({15, 25})));
CHECK_EQ("string", toString(requireTypeAtPosition({16, 25})));
CHECK_EQ("string?", toString(requireTypeAtPosition({17, 25})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "generic_function_statement")
{
ScopedFastFlag sff{FFlag::DebugLuauForceOldSolver, false};
LUAU_REQUIRE_NO_ERRORS(check(R"(
type Object = {
foobar: <T>(number, string, T) -> T
}
local Obj = {} :: Object
function Obj.foobar(bing, quxx, dunno)
local _ = bing
local _ = quxx
return dunno
end
)"));
CHECK_EQ("number", toString(requireTypeAtPosition({7, 24})));
CHECK_EQ("string", toString(requireTypeAtPosition({8, 24})));
CHECK_EQ("a", toString(requireTypeAtPosition({9, 21})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "function_calls_should_not_crash")
{
ScopedFastFlag sffs[] = {
{FFlag::DebugLuauForceOldSolver, false},
};
CheckResult result = check(R"(
return {
StartAPI = function()
local pointers = {}
local API = {}
local function getRealEnvResult(PointerOrPath)
if pointers[PointerOrPath] then
return pointers[PointerOrPath]
end
end
API.OnInvoke = function()
local realEnvResult, isResultPointer = getRealEnvResult(FunctionInEnvToRunPath)
return realEnvResult(table.unpack(args, 2, args.n))
if TableInEnvPath and type(TableInEnvPath) == 'string' then
local realEnvResult, isResultPointer = getRealEnvResult(TableInEnvPath)
return getmetatable(realEnvResult)
end
local realEnvResult, isResultPointer = getRealEnvResult(TableInEnvPath)
local metaTableInEnv = getmetatable(realEnvResult)
local result = metaTableInEnv[FuncToRun](realEnvResult,table.unpack(args, 3, args.n))
end
end
}
)");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "unnecessary_nil_in_lower_bound_of_generic")
{
CheckResult result = check(
Mode::Nonstrict,
R"(
function isAnArray(value)
if type(value) == "table" then
for index, _ in next, value do
-- assert index is not nil
math.max(0, index)
end
return true
else
return false
end
end
)"
);
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "call_function_with_nothing_but_nil")
{
LUAU_REQUIRE_NO_ERRORS(check(R"(
local function f(n: number, x: string?, y: string?, z: string?) end
local function g(n)
f(n)
end
)"));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "oss_1640")
{
LUAU_REQUIRE_NO_ERRORS(check(R"(
--!strict
table.create(1) -- top function call
local function f(): string
if true then
table.create(1) -- middle function call
end
return table.concat({})
end
)"));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "oss_1854")
{
LUAU_REQUIRE_NO_ERRORS(check(R"(
--!strict
local function bug()
local counter = 1
local work = buffer.create(64)
local function get_block()
buffer.writeu32(work, 48, counter)
counter = (counter + 1) % 0x100000000
return work
end
end
)"));
}
TEST_CASE_FIXTURE(Fixture, "cli_119545_pass_lambda_inside_table")
{
ScopedFastFlag _{FFlag::DebugLuauForceOldSolver, false};
LUAU_REQUIRE_NO_ERRORS(check(R"(
--!strict
type foo1 = { foo: (number) -> () }
type foo2 = { read foo: (number) -> () }
local function bar1(foo: foo1) end
local function bar2(foo: foo2) end
local baz = { foo = function(number: number) end, }
bar1(baz)
bar2(baz)
)"));
}
TEST_CASE_FIXTURE(Fixture, "oss_2065_bidirectional_inference_function_call")
{
ScopedFastFlag sffs[] = {
{FFlag::DebugLuauForceOldSolver, false},
{FFlag::DebugLuauAssertOnForcedConstraint, true},
};
LUAU_REQUIRE_NO_ERRORS(check(R"(
local function foo(callback: () -> (() -> ())?)
end
local someCondition: boolean = true
foo(function()
if someCondition then
return nil
end
return function() end
end)
)"));
}
TEST_CASE_FIXTURE(Fixture, "bidirectionally_infer_lambda_with_partially_resolved_generic")
{
ScopedFastFlag sffs[] = {
{FFlag::DebugLuauForceOldSolver, false},
{FFlag::DebugLuauAssertOnForcedConstraint, true},
};
LUAU_REQUIRE_NO_ERRORS(check(R"(
local function foo<T>(value: T)
return function<R>(callback: (T) -> R)
end
end
foo(3)(function (data)
local _ = data
return 42
end)
)"));
CHECK_EQ("number", toString(requireTypeAtPosition({7, 23})));
}
TEST_CASE_FIXTURE(Fixture, "bidirectional_inference_goes_through_ifelse")
{
ScopedFastFlag sffs[] = {
{FFlag::DebugLuauForceOldSolver, false},
{FFlag::DebugLuauAssertOnForcedConstraint, true},
};
LUAU_REQUIRE_NO_ERRORS(check(R"(
type Input = "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
local function getInputs(isDragonPunch: boolean): { Input }
return if isDragonPunch then { "6", "8", "7" } else { "8", "7", "6" }
end
)"));
}
TEST_CASE_FIXTURE(Fixture, "overload_one_ok_one_potential")
{
ScopedFastFlag _{FFlag::DebugLuauForceOldSolver, false};
auto result = check(R"(
local f: ((number) -> "one") & ((string) -> "two")
local g = f(42)
local h = f("huh")
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("\"one\"", toString(requireType("g")));
CHECK_EQ("\"two\"", toString(requireType("h")));
}
TEST_CASE_FIXTURE(Fixture, "overload_selection_ambiguous_call")
{
ScopedFastFlag _{FFlag::DebugLuauForceOldSolver, false};
auto result = check(R"(
local f: ((number | string) -> "one") & ((number | boolean) -> "two")
local g = f(42)
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
auto err = get<AmbiguousFunctionCall>(result.errors[0]);
REQUIRE(err);
CHECK_EQ("number", toString(err->arguments));
CHECK_EQ("((boolean | number) -> \"two\") & ((number | string) -> \"one\")", toString(err->function));
CHECK_EQ("*error-type*", toString(requireType("g")));
}
TEST_CASE_FIXTURE(Fixture, "overload_selection_pick_better_arity")
{
ScopedFastFlag _{FFlag::DebugLuauForceOldSolver, false};
auto result = check(R"(
local f: ((number) -> "one") & ((number, number) -> "two")
-- Casting here so that we always hit the case in overload selection
-- where one part has the correct arity but incorrect argument types,
-- and the other has the incorrect arity.
local g = f("s" :: string)
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
auto err = get<TypeMismatch>(result.errors[0]);
REQUIRE(err);
CHECK_EQ("number", toString(err->wantedType));
CHECK_EQ("string", toString(err->givenType));
CHECK_EQ("\"one\"", toString(requireType("g")));
}
TEST_CASE_FIXTURE(Fixture, "overload_selection_no_compatible_option")
{
ScopedFastFlag _{FFlag::DebugLuauForceOldSolver, false};
auto result = check(R"(
local f: ((number) -> "one") & ((boolean) -> "two")
local g = f("s" :: string)
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ("*error-type*", toString(requireType("g")));
}
TEST_CASE_FIXTURE(Fixture, "overload_selection_bad_arity")
{
ScopedFastFlag _{FFlag::DebugLuauForceOldSolver, false};
auto result = check(R"(
local function foo<T>(f: ((number, number) -> "one") & T)
local huh = f(42)
local _ = huh
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("*error-type*", toString(requireTypeAtPosition({3, 23})));
}
TEST_CASE_FIXTURE(Fixture, "overload_selection_union_of_functions")
{
ScopedFastFlag _{FFlag::DebugLuauForceOldSolver, false};
auto result = check(R"(
local function foo(f: (() -> (number)) | (() -> (string)))
return f()
end
local g = foo(nil :: any)
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("number", toString(requireType("g")));
}
TEST_CASE_FIXTURE(Fixture, "overload_selection_needs_to_retry")
{
ScopedFastFlag _{FFlag::DebugLuauForceOldSolver, false};
auto results = check(R"(
type RGB = { r: number, b: number, g: number }
local BrickColor: ((number) -> RGB) & ((number, number, number) -> RGB) & ((string) -> RGB)
function Lightning(li, Color)
li.BrickColor = BrickColor(Color)
end
)");
LUAU_REQUIRE_NO_ERRORS(results);
CHECK_EQ("({ BrickColor: RGB }, number) -> ()", toString(requireType("Lightning")));
}
TEST_CASE_FIXTURE(Fixture, "overload_selection_unambiguous_with_constraint")
{
ScopedFastFlag _{FFlag::DebugLuauForceOldSolver, false};
LUAU_REQUIRE_NO_ERRORS(check(R"(
local f: ((string, number) -> string) & ((number, boolean) -> number)
local function g(x)
-- When selecting an overload at this point, we'll reject the
-- second overload, and claim that this is the only possible
-- overload with a constraint of `x <: string`.
f(x, 42)
end
)"));
CHECK_EQ("(string) -> ()", toString(requireType("g")));
}
TEST_CASE_FIXTURE(Fixture, "oss_2118")
{
LUAU_REQUIRE_NO_ERRORS(check(R"(
local foo: <P>(constructor: (P) -> any) -> (P) -> any = (nil :: any)
local fn = foo(function (value: { test: true })
return value.test
end)
)"));
CHECK_EQ("({ test: true }) -> any", toString(requireType("fn")));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "oss_2125")
{
ScopedFastFlag _{FFlag::DebugLuauForceOldSolver, false};
LUAU_REQUIRE_NO_ERRORS(check(R"(
export type function CombineTableAndSetIndexer(a: type, b: type, c: type)
local t = {}
for key, value in a:properties() do
t[key] = value.read
end
if b.tag == "table" then
for key, value in b:properties() do
t[key] = value.read
end
end
return types.newtable(t :: any, { index = types.number, readresult = c, writeresult = c })
end
type SpecialProperties = {
test: string?,
}
local function component<Properties>(
constructor: (props: Properties) -> ()
): (
CombineTableAndSetIndexer<SpecialProperties, Properties, any>
) -> ()
return function(props: Properties) end
end
local mrrp = component(function(thing: {
meow: number,
}) end)
mrrp({
meow = 5,
})
)"));
}
TEST_CASE_FIXTURE(Fixture, "function_argument_error_suppression")
{
ScopedFastFlag sff[]{
{FFlag::DebugLuauForceOldSolver, false},
};
CheckResult result = check(R"(
local functions: {[any]: (any) -> ()} = {}
functions.func1 = function(value: string) end
functions.func2 = function(value: boolean) end
functions.func3 = function(value: number) end
functions.func4 = function(value: any) end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "bidirectional_lambda_inference_applies_nilable_functions")
{
ScopedFastFlag sffs[] = {
{FFlag::DebugLuauForceOldSolver, false},
{FFlag::DebugLuauAssertOnForcedConstraint, true},
};
LUAU_REQUIRE_NO_ERRORS(check(R"(
local listdir: (string, ((string) -> boolean)?) -> { string } = nil :: any
listdir("my_directory", function (path)
print(path)
return true
end)
)"));
CHECK_EQ("string", toString(requireTypeAtPosition({3, 19})));
}
TEST_CASE_FIXTURE(Fixture, "function_statement_with_incorrect_function_type")
{
ScopedFastFlag _{FFlag::LuauCheckFunctionStatementTypes, true};
CheckResult result = check(R"(
local Library: { isnan: (number) -> number } = {} :: any
function Library.isnan(s: string): boolean
return s == "NaN"
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
auto err = get<TypeMismatch>(result.errors[0]);
REQUIRE(err);
CHECK_EQ("(number) -> number", toString(err->wantedType));
CHECK_EQ("(string) -> boolean", toString(err->givenType));
}
TEST_CASE_FIXTURE(Fixture, "bidirectional_inference_allow_internal_generics")
{
ScopedFastFlag sffs[] = {
{FFlag::DebugLuauForceOldSolver, false},
{FFlag::DebugLuauAssertOnForcedConstraint, true},
};
CheckResult result = check(R"(
type testsuite = { case: (self: testsuite, <T>(T) -> T) -> () }
local test1: { suite: (string, (testsuite) -> ()) -> () } = nil :: any
test1.suite("LuteTestCommand", function(suite)
suite:case(42)
end)
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
auto err = get<TypeMismatch>(result.errors[0]);
CHECK_EQ("<T>(T) -> T", toString(err->wantedType));
CHECK_EQ("number", toString(err->givenType));
}
TEST_CASE_FIXTURE(Fixture, "oss_2143")
{
CheckResult result = check(R"(
local function call<A..., R...>(c: (A...) -> R..., ...: A...): R...
return c(...)
end
local function fn(a: number): { number }
return nil :: any
end
local function fn2<T>(b: { T }, x: (T) -> ())
return b
end
local values = call(fn, 2)
fn2(values, function(x: number)
end)
local a = values[1]
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "apply_example_from_oss")
{
CheckResult result = check(R"(
type something = { Something: number }
type example = { Example: number }
local function test(a: something): example
return nil :: any
end
local function apply<T..., U...>(func: (T...) -> U..., ...: T...): (boolean, U...)
return nil :: any
end
local b, result = apply(test, {
Something = 1
})
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK("{ Example: number }" == toString(requireType("result"), {true}));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "oss_2109")
{
LUAU_REQUIRE_NO_ERRORS(check(R"(
local function Retry<T..., K...>(
MaxRetries: number,
RetryInterval: number,
Function: (T...) -> (K...),
...: T...
): K...
local Results
local CurrentRetry = 0
repeat
Results = {pcall(Function, ...)}
if not Results[1] then
CurrentRetry += 1
end
until Results[1] or CurrentRetry == MaxRetries
return unpack(Results :: any, 2)
end
local function Test(a: number, b: number): number
return a + b
end
local a = Retry(5, 1, Test, 5, 10)
)"));
CHECK_EQ("number", toString(requireType("a")));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "pcall_example")
{
LUAU_REQUIRE_NO_ERRORS(check(R"(
local function makestr(n: number): string
return tostring(n)
end
-- `s` now has type `string` and not `unknown`
local success, s = pcall(makestr, 42)
)"));
CHECK_EQ("string", toString(requireType("s")));
}
TEST_CASE_FIXTURE(ExternTypeFixture, "bidirectional_function_statement_inference_with_extern")
{
LUAU_REQUIRE_NO_ERRORS(check(R"(
type HasClass = { f: (ClassWithGenericMethod) -> () }
local t = {} :: HasClass
function t.f(cls)
local _ = cls
local foobar = cls.identity(42)
local _ = foobar
end
)"));
CHECK_EQ("ClassWithGenericMethod", toString(requireTypeAtPosition({4, 23})));
CHECK_EQ("number", toString(requireTypeAtPosition({6, 23})));
}
TEST_CASE_FIXTURE(Fixture, "table_containing_factorial_standalone")
{
ScopedFastFlag sffs[] = {
{FFlag::LuauCaptureRecursiveCallsForTablesAndGlobals2, true},
{FFlag::DebugLuauAssertOnForcedConstraint, true},
};
LUAU_REQUIRE_NO_ERRORS(check(R"(
local coolmath = {}
function coolmath.factorial(n: number)
if n <= 1 then
return 1
end
return coolmath.factorial(n - 1) * n
end
)"));
}
TEST_CASE_FIXTURE(Fixture, "table_containing_factorial_assign_later")
{
ScopedFastFlag sffs[] = {
{FFlag::DebugLuauForceOldSolver, false},
{FFlag::LuauCaptureRecursiveCallsForTablesAndGlobals2, true},
{FFlag::DebugLuauAssertOnForcedConstraint, true},
};
CheckResult results = check(R"(
local coolmath = {}
function coolmath.factorial(n: number)
if n <= 1 then
return 1
end
return coolmath.factorial(n - 1) * n
end
coolmath.factorial = function (s: string) end
)");
LUAU_REQUIRE_ERROR_COUNT(1, results);
auto err = get<TypeMismatch>(results.errors[0]);
CHECK_EQ("(number) -> number", toString(err->wantedType));
CHECK_EQ("(string) -> ()", toString(err->givenType));
}
TEST_CASE_FIXTURE(Fixture, "table_containing_factorial_assign_with_correct_typing")
{
ScopedFastFlag sffs[] = {
{FFlag::LuauCaptureRecursiveCallsForTablesAndGlobals2, true},
{FFlag::DebugLuauAssertOnForcedConstraint, true},
};
CheckResult results = check(R"(
local coolmath = {}
function coolmath.factorial(n: number)
if n <= 1 then
return 1
end
return coolmath.factorial(n - 1) * n
end
coolmath.factorial = function (n: number) return n end
coolmath.factorial = "not a function"
)");
LUAU_REQUIRE_ERROR_COUNT(1, results);
auto err = get<TypeMismatch>(results.errors[0]);
REQUIRE(err);
CHECK_EQ("(number) -> number", toString(err->wantedType));
CHECK_EQ("string", toString(err->givenType));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "recursive_static_method_must_refer_to_the_ungeneralized_type")
{
ScopedFastFlag _{FFlag::LuauCaptureRecursiveCallsForTablesAndGlobals2, true};
CheckResult result = check(R"(
local lexer = {}
local subContent: string = ""
function lexer.scan(s: string)
for innerToken, innerContent in lexer.scan(subContent) do
table.insert(innerToken, innerContent)
end
return {}, nil, nil
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "oss_2216_recursive_global_function_works_as_expected")
{
ScopedFastFlag sffs[] = {
{FFlag::LuauCaptureRecursiveCallsForTablesAndGlobals2, true},
{FFlag::DebugLuauAssertOnForcedConstraint, true},
};
CheckResult result = check(R"(
type tb_any = {[any]:any}
function flatten(... : tb_any) : tb_any
local out = {}
local par = {...}
for i = 1,#par do
if par[i] and typeof(par[i]) == "table" then
for n,v in par[i] do
if typeof(n) == "number" then
for m,u in flatten(v) do
out[m] = u -- type error
end
else
out[n] = v
end
end
end
end
return out
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "cli_187542_recursive_call_in_loop")
{
ScopedFastFlag sffs[] = {
{FFlag::DebugLuauForceOldSolver, false},
{FFlag::LuauCaptureRecursiveCallsForTablesAndGlobals2, true},
{FFlag::DebugLuauAssertOnForcedConstraint, true},
};
CheckResult result = check(R"(
function a(b)
if true then return b end
while false do
b = a(b)
end
if true then return b end
end
)");
LUAU_REQUIRE_ERROR_COUNT(4, result);
LUAU_REQUIRE_NO_ERROR(result, ConstraintSolvingIncompleteError);
}
TEST_CASE_FIXTURE(Fixture, "global_function_redefinition")
{
ScopedFastFlag sffs[] = {{FFlag::LuauCaptureRecursiveCallsForTablesAndGlobals2, true}, {FFlag::DebugLuauAssertOnForcedConstraint, true}};
CheckResult result = check(R"(
function fact(n: number)
return if n < 1 then 1 else n * fact(n - 1)
end
fact = "huh"
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
auto err = get<TypeMismatch>(result.errors[0]);
CHECK_EQ("(number) -> number", toString(err->wantedType));
CHECK_EQ("string", toString(err->givenType));
}
TEST_CASE_FIXTURE(Fixture, "oss_2061_modify_visited_generic_ice")
{
ScopedFastFlag _{FFlag::DebugLuauForceOldSolver, false};
CheckResult results = check(R"(
type actions<T=unknown, A...=...unknown> = { [string]: (state: T, A...) -> (T) }
type disconnect = () -> ()
type producer<state, actions = actions> = {
get:
& (() -> state)
& (<T>(selector: (state) -> T) -> T),
} & actions
type interface = {
create: <state>(default: state) -> <actions>(actions: actions) -> producer<state, actions>,
}
local a: interface
a.create()
)");
LUAU_REQUIRE_ERROR_COUNT(1, results);
CHECK(get<CountMismatch>(results.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "unify_type_pack_stack_overflow")
{
ScopedFastFlag sffs[] = {
{FFlag::DebugLuauForceOldSolver, false},
};
CheckResult results = check(R"(
local function a(): ...string
return "hello", "world"
end
local function g<T...>()
local function f(... : T...)
end
f("what", "is", "going", a())
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, results);
auto err = get<TypePackMismatch>(results.errors[0]);
REQUIRE(err);
CHECK_EQ("T...", toString(err->wantedTp));
CHECK_EQ("string, string, string, ...string", toString(err->givenTp));
}
TEST_CASE_FIXTURE(Fixture, "global_function_blocked")
{
ScopedFastFlag sffs[] = {
{FFlag::DebugLuauForceOldSolver, false},
{FFlag::DebugLuauAssertOnForcedConstraint, true},
{FFlag::LuauCaptureRecursiveCallsForTablesAndGlobals2, true}
};
LUAU_REQUIRE_NO_ERRORS(check(R"(
--!strict
local addInstanceToState: any = nil
local inst: any = nil
function ingestAllInstances(...): ()
local id: number = addInstanceToState()
local child: any = nil
ingestAllInstances(child)
end
function handleDmQuery()
ingestAllInstances()
end
return {}
)"));
}
TEST_CASE_FIXTURE(Fixture, "generic_polarity_of_annotated_code")
{
ScopedFastFlag sffs[] = {
{FFlag::DebugLuauForceOldSolver, false},
{FFlag::LuauForwardPolarityForFunctionTypes, true},
};
check(R"(
local f: <T>(T) -> T = nil :: any
)");
auto ftv = get<FunctionType>(requireType("f"));
LUAU_ASSERT(ftv);
auto gen = get<GenericType>(ftv->generics.at(0));
LUAU_ASSERT(gen && gen->polarity == Polarity::Mixed);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "lute_tasklib_createtask")
{
ScopedFastFlag sffs[] = {
{FFlag::DebugLuauForceOldSolver, false},
{FFlag::LuauOverloadGetsInstantiated2, true},
{FFlag::LuauReplacerRespectsReboundGenerics, true},
};
LUAU_REQUIRE_NO_ERRORS(check(R"(
local function createtask(f, ...)
local data = {}
data.co = coroutine.create(function(...)
local success, result = pcall(f, ...)
data.success = success
data.result = result
end)
coroutine.resume(data.co, ...)
return data
end
)"));
CHECK_EQ("((...any) -> (unknown, ...unknown), ...any) -> { co: thread, result: unknown, success: boolean }", toString(requireType("createtask")));
}
TEST_CASE_FIXTURE(Fixture, "global_emplacing_steals_type_from_elsewhere")
{
ScopedFastFlag sffs[] = {
{FFlag::DebugLuauForceOldSolver, false},
{FFlag::LuauKeepExplicitMapForGlobalTypes2, true},
};
CheckResult result = check(R"(
local function f()
return 42
end
local a = f()
b = a
local c = b
function b()
end
)");
CHECK_EQ("number", toString(requireType("a")));
CHECK_EQ("() -> ()", toString(requireType("b")));
CHECK_EQ("number", toString(requireType("c")));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "are_we_in_the_new_solver")
{
ScopedFastFlag sffs[] = {
{FFlag::DebugLuauForceOldSolver, false},
{FFlag::LuauReplacerRespectsReboundGenerics, true},
{FFlag::LuauOverloadGetsInstantiated2, true},
};
CheckResult result = check(R"(
-- This file should fail the old solver
function add(a, b)
return a + b
end
local vec2 = {}
function vec2.new(x, y)
return setmetatable({ x = x or 0, y = y or 0 }, {
__add = function(v1, v2)
return { x = v1.x + v2.x, y = v1.y + v2.y }
end,
})
end
local a = add(1, 1)
local b = add(vec2.new(0, 0), vec2.new(1, 1))
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("number", toString(requireType("a")));
CHECK_EQ("{ x: number, y: number }", toString(requireType("b")));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "dont_leak_generics_keyof")
{
ScopedFastFlag sffs[] = {
{FFlag::DebugLuauForceOldSolver, false},
{FFlag::LuauReplacerRespectsReboundGenerics, true},
{FFlag::LuauOverloadGetsInstantiated2, true},
};
LUAU_REQUIRE_NO_ERRORS(check(R"(
local function makeOtherThing(template)
return {
Stuff = template
}
end
local function makeThing(tbl)
local returnThis = { Input = makeOtherThing(tbl) }
function returnThis.Test(key: keyof<typeof(returnThis.Input.Stuff)>) end
return returnThis
end
local thing = makeThing({a=1})
thing.Test("a")
local otherthing = makeThing({b = 42, c = 13})
otherthing.Test("b")
otherthing.Test("c")
)"));
CHECK_EQ("{ Input: { Stuff: { a: number } }, Test: (\"a\") -> () }", toString(requireType("thing")));
CHECK_EQ("{ Input: { Stuff: { b: number, c: number } }, Test: (\"b\" | \"c\") -> () }", toString(requireType("otherthing")));
}
TEST_CASE_FIXTURE(Fixture, "bidi_inference_functions_complete_ex")
{
ScopedFastFlag sffs[] = {
{FFlag::DebugLuauForceOldSolver, false},
{FFlag::LuauBidirectionalInferenceBetterUnionHandling, true},
{FFlag::LuauExplicitTypeInstantiationSupport, true},
};
LUAU_REQUIRE_NO_ERRORS(check(R"(
--!strict
type Player = {}
export type RemoteEventWrapper<T...> = {
connect:( self: RemoteEventWrapper<T...>, callback: ((T...) -> ()) | ((player: Player, T...) -> ()) ) -> () -> (),
}
local function useRemoteEvent<T...>(remoteEventName: string, isUnreliable: boolean?): RemoteEventWrapper<T...>
return nil :: any
end
type Payload = {
name: string,
time: number,
data: { [string]: any },
}
local payload = useRemoteEvent<<(Payload)>>("initial-payload")
-- We expect bidirectional inference to kick in here and ensure that
-- player and payload have non-unknown types.
payload:connect(function(player, payload)
local _ = player
local _ = payload
end)
return useRemoteEvent
)"));
CHECK_EQ("Player", toString(requireTypeAtPosition({23, 23})));
CHECK_EQ("Payload", toString(requireTypeAtPosition({24, 23})));
}
TEST_CASE_FIXTURE(Fixture, "bidi_inference_union_of_functions_1")
{
ScopedFastFlag sffs[] = {
{FFlag::DebugLuauForceOldSolver, false},
{FFlag::LuauBidirectionalInferenceBetterUnionHandling, true},
};
LUAU_REQUIRE_NO_ERRORS(check(R"(
local function f(_: ((string) -> ()) | ((number, number) -> ()))
end
f(function (one, two)
local _ = one
local _ = two
end)
)"));
CHECK_EQ("number", toString(requireTypeAtPosition({5, 23})));
CHECK_EQ("number", toString(requireTypeAtPosition({6, 23})));
}
TEST_CASE_FIXTURE(Fixture, "bidi_inference_union_of_functions_2")
{
ScopedFastFlag sffs[] = {
{FFlag::DebugLuauForceOldSolver, false},
{FFlag::LuauBidirectionalInferenceBetterUnionHandling, true},
};
LUAU_REQUIRE_NO_ERRORS(check(R"(
local function f(_: ((string) -> ()) | ((number, number) -> ()))
end
f(function (one)
local _ = one
end)
)"));
CHECK_EQ("string", toString(requireTypeAtPosition({5, 23})));
}
TEST_CASE_FIXTURE(Fixture, "bidi_inference_union_of_functions_3")
{
ScopedFastFlag sffs[] = {
{FFlag::DebugLuauForceOldSolver, false},
{FFlag::LuauBidirectionalInferenceBetterUnionHandling, true},
};
LUAU_REQUIRE_NO_ERRORS(check(R"(
local function f(_: ((string) -> ()) | ((number) -> ()))
end
f(function (one)
local _ = one
end)
)"));
CHECK_EQ("string", toString(requireTypeAtPosition({5, 23})));
}
TEST_CASE_FIXTURE(Fixture, "bidi_inference_union_of_functions_4")
{
ScopedFastFlag sffs[] = {
{FFlag::DebugLuauForceOldSolver, false},
{FFlag::LuauBidirectionalInferenceBetterUnionHandling, true},
};
LUAU_REQUIRE_NO_ERRORS(check(R"(
local function f(_: ((string) -> ())?)
end
f(function (one)
local _ = one
end)
)"));
CHECK_EQ("string", toString(requireTypeAtPosition({5, 23})));
}
TEST_SUITE_END();